Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sbb-tab-group): tab titles redesign #1975

Merged
merged 9 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { InterfaceSbbRadioButtonGroupAttributes } from "./components/sbb-radio-b
import { SelectChange } from "./components/sbb-select/sbb-select.custom";
import { InterfaceSbbSelectionPanelAttributes } from "./components/sbb-selection-panel/sbb-selection-panel.custom";
import { InterfaceSignetAttributes } from "./components/sbb-signet/sbb-signet.custom";
import { InterfaceSbbTabGroupTab } from "./components/sbb-tab-group/sbb-tab-group.custom";
import { TagStateChange } from "./components/sbb-tag/sbb-tag.custom";
import { InterfaceTimetableParkAndRailAttributes } from "./components/sbb-timetable-park-and-rail/sbb-timetable-park-and-rail.custom";
import { Boarding, Price } from "./components/sbb-timetable-row/sbb-timetable-row.custom";
Expand Down Expand Up @@ -84,6 +85,7 @@ export { InterfaceSbbRadioButtonGroupAttributes } from "./components/sbb-radio-b
export { SelectChange } from "./components/sbb-select/sbb-select.custom";
export { InterfaceSbbSelectionPanelAttributes } from "./components/sbb-selection-panel/sbb-selection-panel.custom";
export { InterfaceSignetAttributes } from "./components/sbb-signet/sbb-signet.custom";
export { InterfaceSbbTabGroupTab } from "./components/sbb-tab-group/sbb-tab-group.custom";
export { TagStateChange } from "./components/sbb-tag/sbb-tag.custom";
export { InterfaceTimetableParkAndRailAttributes } from "./components/sbb-timetable-park-and-rail/sbb-timetable-park-and-rail.custom";
export { Boarding, Price } from "./components/sbb-timetable-row/sbb-timetable-row.custom";
Expand Down Expand Up @@ -1554,6 +1556,10 @@ export namespace Components {
* Sets the initial tab. If it matches a disabled tab or exceeds the length of the tab group, the first enabled tab will be selected.
*/
"initialSelectedIndex": number;
/**
* Size variant, either l or xl.
*/
"size": InterfaceSbbTabGroupTab['size'];
}
interface SbbTabTitle {
/**
Expand Down Expand Up @@ -4394,6 +4400,10 @@ declare namespace LocalJSX {
* Emits an event on selected tab change
*/
"onDid-change"?: (event: SbbTabGroupCustomEvent<void>) => void;
/**
* Size variant, either l or xl.
*/
"size"?: InterfaceSbbTabGroupTab['size'];
}
interface SbbTabTitle {
/**
Expand Down
7 changes: 4 additions & 3 deletions src/components/sbb-tab-group/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ or using the `amount` slot of the `sbb-tab-title`.

## Properties

| Property | Attribute | Description | Type | Default |
| ---------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | -------- | ------- |
| `initialSelectedIndex` | `initial-selected-index` | Sets the initial tab. If it matches a disabled tab or exceeds the length of the tab group, the first enabled tab will be selected. | `number` | `0` |
| Property | Attribute | Description | Type | Default |
| ---------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------- |
| `initialSelectedIndex` | `initial-selected-index` | Sets the initial tab. If it matches a disabled tab or exceeds the length of the tab group, the first enabled tab will be selected. | `number` | `0` |
| `size` | `size` | Size variant, either l or xl. | `"l" \| "xl"` | `'l'` |


## Events
Expand Down
1 change: 1 addition & 0 deletions src/components/sbb-tab-group/sbb-tab-group.custom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export interface InterfaceSbbTabGroupTab extends HTMLStencilElement {
relatedContent?: HTMLElement;
index?: number;
tabGroupActions?: InterfaceSbbTabGroupActions;
size: 'l' | 'xl';
}
1 change: 0 additions & 1 deletion src/components/sbb-tab-group/sbb-tab-group.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
.tab-group {
display: flex;
flex-wrap: wrap;
gap: var(--sbb-spacing-fixed-3x);
}

.tab-content {
Expand Down
49 changes: 36 additions & 13 deletions src/components/sbb-tab-group/sbb-tab-group.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { withActions } from '@storybook/addon-actions/decorator';
import type { Meta, StoryObj, ArgTypes, Args, Decorator } from '@storybook/html';
import type { InputType } from '@storybook/types';

const firstTabTitle = ({ label, ...args }): JSX.Element => (
const firstTabTitle = (label, args): JSX.Element => (
<sbb-tab-title {...args}>{label}</sbb-tab-title>
);

Expand Down Expand Up @@ -40,9 +40,9 @@ const tabPanelFour = (): JSX.Element => (
</article>
);

const DefaultTemplate = (args): JSX.Element => (
<sbb-tab-group initial-selected-index="0">
{firstTabTitle(args)}
const DefaultTemplate = ({ size, label, ...args }): JSX.Element => (
<sbb-tab-group size={size} initial-selected-index="0">
{firstTabTitle(label, args)}
{tabPanelOne()}

<sbb-tab-title>Tab title two</sbb-tab-title>
Expand All @@ -56,9 +56,9 @@ const DefaultTemplate = (args): JSX.Element => (
</sbb-tab-group>
);

const IconsAndNumbersTemplate = (args): JSX.Element => (
<sbb-tab-group initial-selected-index="0">
{firstTabTitle(args)}
const IconsAndNumbersTemplate = ({ size, label, ...args }): JSX.Element => (
<sbb-tab-group size={size} initial-selected-index="0">
{firstTabTitle(label, args)}
{tabPanelOne()}

<sbb-tab-title amount={args.amount} icon-name="swisspass-small">
Expand All @@ -78,10 +78,10 @@ const IconsAndNumbersTemplate = (args): JSX.Element => (
</sbb-tab-group>
);

const NestedTemplate = (args): JSX.Element => (
<sbb-tab-group initial-selected-index="0">
{firstTabTitle(args)}
<sbb-tab-group initial-selected-index="1">
const NestedTemplate = ({ size, label, ...args }): JSX.Element => (
<sbb-tab-group size={size} initial-selected-index="0">
{firstTabTitle(label, args)}
<sbb-tab-group size={size} initial-selected-index="1">
<sbb-tab-title level="2">Nested tab</sbb-tab-title>
<div>
Diam maecenas ultricies mi eget mauris pharetra et ultrices neque ornare aenean euismod
Expand Down Expand Up @@ -143,16 +143,25 @@ const amount: InputType = {
},
};

const size: InputType = {
control: {
type: 'inline-radio',
},
options: ['l', 'xl'],
};

const basicArgTypes: ArgTypes = {
label,
'icon-name': iconName,
amount: amount,
size: size,
};

const basicArgs: Args = {
label: 'Tab label one',
'icon-name': undefined,
amount: undefined,
size: size.options[0],
};

const templateRes = [
Expand All @@ -164,20 +173,34 @@ const templateRes = [
withActions as Decorator,
];

export const defaultTabs: StoryObj = {
export const defaultTabsSizeL: StoryObj = {
render: DefaultTemplate,
argTypes: basicArgTypes,
args: { ...basicArgs },
decorators: templateRes,
};

export const numbersAndIcons: StoryObj = {
export const numbersAndIconsSizeL: StoryObj = {
render: IconsAndNumbersTemplate,
argTypes: basicArgTypes,
args: { ...basicArgs, amount: 16, 'icon-name': iconName.options[0] },
decorators: templateRes,
};

export const defaultTabsSizeXL: StoryObj = {
render: DefaultTemplate,
argTypes: basicArgTypes,
args: { ...basicArgs, size: size.options[1] },
decorators: templateRes,
};

export const numbersAndIconsSizeXL: StoryObj = {
render: IconsAndNumbersTemplate,
argTypes: basicArgTypes,
args: { ...basicArgs, amount: 16, 'icon-name': iconName.options[0], size: size.options[1] },
decorators: templateRes,
};

export const nestedTabGroups: StoryObj = {
render: NestedTemplate,
argTypes: basicArgTypes,
Expand Down
82 changes: 61 additions & 21 deletions src/components/sbb-tab-group/sbb-tab-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import {
Listen,
Method,
Prop,
Watch,
} from '@stencil/core';
import { InterfaceSbbTabGroupTab } from './sbb-tab-group.custom';
import { isArrowKeyPressed, getNextElementIndex, interactivityChecker } from '../../global/a11y';
import { isValidAttribute, hostContext } from '../../global/dom';
import { isValidAttribute, hostContext, toggleDatasetEntry } from '../../global/dom';
import { throttle } from '../../global/eventing';
import { AgnosticMutationObserver, AgnosticResizeObserver } from '../../global/observers';

Expand All @@ -40,25 +41,41 @@ let nextId = 0;
tag: 'sbb-tab-group',
})
export class SbbTabGroup implements ComponentInterface {
public tabs: InterfaceSbbTabGroupTab[] = [];
private _tabs: InterfaceSbbTabGroupTab[] = [];
private _selectedTab: InterfaceSbbTabGroupTab;
private _isNested: boolean;
private _tabGroupElement: HTMLElement;
private _tabContentElement: HTMLElement;
private _tabAttributeObserver = new AgnosticMutationObserver(
this._onTabAttributesChange.bind(this),
private _tabAttributeObserver = new AgnosticMutationObserver((mutationsList) =>
this._onTabAttributesChange(mutationsList),
);
private _tabContentResizeObserver = new AgnosticResizeObserver(
this._onTabContentElementResize.bind(this),
private _tabGroupResizeObserver = new AgnosticResizeObserver((entries) =>
this._onTabGroupElementResize(entries),
);
private _tabContentResizeObserver = new AgnosticResizeObserver((entries) =>
this._onTabContentElementResize(entries),
);

@Element() private _element: HTMLElement;

/**
* Size variant, either l or xl.
*/
@Prop() public size: InterfaceSbbTabGroupTab['size'] = 'l';

/**
* Sets the initial tab. If it matches a disabled tab or exceeds the length of
* the tab group, the first enabled tab will be selected.
*/
@Prop() public initialSelectedIndex = 0;

@Watch('size')
public updateSize(): void {
for (const tab of this._tabs) {
tab.setAttribute('data-size', this.size);
}
}

/**
* Emits an event on selected tab change
*/
Expand All @@ -73,7 +90,7 @@ export class SbbTabGroup implements ComponentInterface {
*/
@Method()
public async disableTab(tabIndex: number): Promise<void> {
this.tabs[tabIndex]?.tabGroupActions.disable();
this._tabs[tabIndex]?.tabGroupActions.disable();
}

/**
Expand All @@ -82,7 +99,7 @@ export class SbbTabGroup implements ComponentInterface {
*/
@Method()
public async enableTab(tabIndex: number): Promise<void> {
this.tabs[tabIndex]?.tabGroupActions.enable();
this._tabs[tabIndex]?.tabGroupActions.enable();
}

/**
Expand All @@ -91,7 +108,7 @@ export class SbbTabGroup implements ComponentInterface {
*/
@Method()
public async activateTab(tabIndex: number): Promise<void> {
this.tabs[tabIndex]?.tabGroupActions.select();
this._tabs[tabIndex]?.tabGroupActions.select();
}

private _getTabs(): InterfaceSbbTabGroupTab[] {
Expand All @@ -101,7 +118,7 @@ export class SbbTabGroup implements ComponentInterface {
}

private get _enabledTabs(): InterfaceSbbTabGroupTab[] {
return this.tabs.filter(
return this._tabs.filter(
(t) => !isValidAttribute(t, 'disabled') && interactivityChecker.isVisible(t),
);
}
Expand All @@ -111,39 +128,42 @@ export class SbbTabGroup implements ComponentInterface {
}

public componentDidLoad(): void {
this.tabs = this._getTabs();
this.tabs.forEach((tab) => this._configure(tab));
this._tabs = this._getTabs();
this._tabs.forEach((tab) => this._configure(tab));
this._initSelection();
this._tabGroupResizeObserver.observe(this._tabGroupElement);
}

public disconnectedCallback(): void {
this._tabAttributeObserver.disconnect();
this._tabContentResizeObserver.disconnect();
this._tabGroupResizeObserver.disconnect();
}

private _onContentSlotChange = (): void => {
this._tabContentElement = this._element.shadowRoot.querySelector('div.tab-content');
const loadedTabs = this._getTabs().filter((tab) => !this.tabs.includes(tab));
const loadedTabs = this._getTabs().filter((tab) => !this._tabs.includes(tab));

// if a new tab/content is added to the tab group
if (loadedTabs.length) {
loadedTabs.forEach((tab) => this._configure(tab));
this.tabs = this.tabs.concat(loadedTabs);
this._tabs = this._tabs.concat(loadedTabs);
}
};

private _onTabsSlotChange = (): void => {
const tabs = this._getTabs();

// if a tab is removed from the tab group
if (tabs.length < this.tabs.length) {
const removedTabs = this.tabs.filter((tab) => !tabs.includes(tab));
if (tabs.length < this._tabs.length) {
const removedTabs = this._tabs.filter((tab) => !tabs.includes(tab));

removedTabs.forEach((removedTab) => {
removedTab.relatedContent?.remove();
});
this.tabs = tabs;
this._tabs = tabs;
}
this._tabs.forEach((tab: HTMLSbbTabTitleElement) => tab.setAttribute('data-size', this.size));
};

private _assignId(): string {
Expand All @@ -153,10 +173,10 @@ export class SbbTabGroup implements ComponentInterface {
private _initSelection(): void {
if (
this.initialSelectedIndex >= 0 &&
this.initialSelectedIndex < this.tabs.length &&
!this.tabs[this.initialSelectedIndex].disabled
this.initialSelectedIndex < this._tabs.length &&
!this._tabs[this.initialSelectedIndex].disabled
) {
this.tabs[this.initialSelectedIndex].tabGroupActions.select();
this._tabs[this.initialSelectedIndex].tabGroupActions.select();
} else {
this._enabledTabs[0]?.tabGroupActions.select();
}
Expand Down Expand Up @@ -186,6 +206,26 @@ export class SbbTabGroup implements ComponentInterface {
}
}

private _onTabGroupElementResize(entries: ResizeObserverEntry[]): void {
for (const entry of entries) {
const tabTitles = (
entry.target.firstElementChild as HTMLSlotElement
).assignedElements() as HTMLSbbTabTitleElement[];

for (const tab of tabTitles) {
toggleDatasetEntry(
tab,
'hasDivider',
tab === tabTitles[0] || tab.offsetLeft === tabTitles[0].offsetLeft,
);
this._element.style.setProperty(
'--sbb-tab-group-width',
`${this._tabGroupElement.clientWidth}px`,
);
}
}
}

private _onTabContentElementResize(entries: ResizeObserverEntry[]): void {
for (const entry of entries) {
const contentHeight = Math.floor(entry.contentRect.height);
Expand Down Expand Up @@ -309,7 +349,7 @@ export class SbbTabGroup implements ComponentInterface {
public render(): JSX.Element {
return (
<Host class={this._isNested ? 'tab-group--nested' : ''}>
<div class="tab-group" role="tablist">
<div class="tab-group" role="tablist" ref={(el) => (this._tabGroupElement = el)}>
<slot name="tab-bar" onSlotchange={this._onTabsSlotChange}></slot>
</div>

Expand Down
Loading