Skip to content

Commit

Permalink
feat(sbb-tab-group): tab titles redesign (#1975)
Browse files Browse the repository at this point in the history
  • Loading branch information
dauriamarco authored Sep 28, 2023
1 parent 7ab9e75 commit 231c140
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 142 deletions.
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

0 comments on commit 231c140

Please sign in to comment.