From 71e02b0a6e9485ce32cee08dc036752e719f7018 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 30 Sep 2025 11:11:50 +0200 Subject: [PATCH 001/127] feat(component): implement `anchor-navigation` for `` --- .../post-tab-header/post-tab-header.tsx | 37 +++-- .../post-tab-panel/post-tab-panel.tsx | 12 +- .../src/components/post-tabs/post-tabs.tsx | 146 +++++++++++++----- 3 files changed, 144 insertions(+), 51 deletions(-) diff --git a/packages/components/src/components/post-tab-header/post-tab-header.tsx b/packages/components/src/components/post-tab-header/post-tab-header.tsx index 7aa08e7689..e9d57dd965 100644 --- a/packages/components/src/components/post-tab-header/post-tab-header.tsx +++ b/packages/components/src/components/post-tab-header/post-tab-header.tsx @@ -4,7 +4,7 @@ import { checkRequiredAndType } from '@/utils'; import { nanoid } from 'nanoid'; /** - * @slot default - Slot for the content of the tab header. + * @slot default - Slot for the content of the tab header. Can contain text or an element for navigation mode. */ @Component({ @@ -16,29 +16,48 @@ export class PostTabHeader { @Element() host: HTMLPostTabHeaderElement; @State() tabId: string; + @State() isNavigationMode = false; /** - * The name of the panel controlled by the tab header. + * The name of the tab, used to associate it with a tab panel or identify the active tab in navigation mode. */ - @Prop({ reflect: true }) readonly panel!: string; + @Prop({ reflect: true }) readonly name!: string; - @Watch('panel') - validateFor() { - checkRequiredAndType(this, 'panel', 'string'); + @Watch('name') + validateName() { + checkRequiredAndType(this, 'name', 'string'); } componentWillLoad() { this.tabId = `tab-${this.host.id || nanoid(6)}`; + this.checkNavigationMode(); + } + + componentDidLoad() { + // Re-check navigation mode after content is loaded + this.checkNavigationMode(); + } + + private checkNavigationMode() { + const hasAnchor = this.host.querySelector('a') !== null; + this.isNavigationMode = hasAnchor; + + // Expose mode to parent post-tabs via data-attribute (as per requirements) + this.host.setAttribute('data-navigation-mode', this.isNavigationMode.toString()); } render() { + const role = this.isNavigationMode ? undefined : 'tab'; + const ariaSelected = this.isNavigationMode ? undefined : 'false'; + const tabindex = this.isNavigationMode ? undefined : '-1'; + return ( diff --git a/packages/components/src/components/post-tab-panel/post-tab-panel.tsx b/packages/components/src/components/post-tab-panel/post-tab-panel.tsx index 748507d851..892804f206 100644 --- a/packages/components/src/components/post-tab-panel/post-tab-panel.tsx +++ b/packages/components/src/components/post-tab-panel/post-tab-panel.tsx @@ -18,16 +18,16 @@ export class PostTabPanel { @State() panelId: string; /** - * The name of the panel, used to associate it with a tab header. + * The name of the tab that this panel is associated with. */ - @Prop({ reflect: true }) readonly name!: string; + @Prop({ reflect: true }) readonly for!: string; - @Watch('name') - validateName() { - checkRequiredAndType(this, 'name', 'string'); + @Watch('for') + validateFor() { + checkRequiredAndType(this, 'for', 'string'); } componentWillLoad() { - this.validateName(); + this.validateFor(); // get the id set on the host element or use a random id by default this.panelId = `panel-${this.host.id || nanoid(6)}`; } diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 1ccd9718d9..c213b50f80 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -1,4 +1,4 @@ -import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core'; +import { Component, Element, Event, EventEmitter, h, Host, Method, Prop, State } from '@stencil/core'; import { version } from '@root/package.json'; import { fadeIn, fadeOut } from '@/animations'; import { componentOnReady } from '@/utils'; @@ -16,26 +16,34 @@ import { componentOnReady } from '@/utils'; shadow: true, }) export class PostTabs { - private activeTab: HTMLPostTabHeaderElement; + private currentActiveTab: HTMLPostTabHeaderElement; private showing: Animation; private hiding: Animation; private isLoaded = false; + @State() isNavigationMode: boolean = false; + private get tabs(): HTMLPostTabHeaderElement[] { return Array.from( this.host.querySelectorAll('post-tab-header'), ).filter(tab => tab.closest('post-tabs') === this.host); } + private get panels(): HTMLPostTabPanelElement[] { + return Array.from( + this.host.querySelectorAll('post-tab-panel'), + ).filter(panel => panel.closest('post-tabs') === this.host); + } + @Element() host: HTMLPostTabsElement; /** - * The name of the panel that is initially shown. - * If not specified, it defaults to the panel associated with the first tab. + * The name of the tab that is initially active. + * If not specified, it defaults to the first tab. * * **Changing this value after initialization has no effect.** */ - @Prop() readonly activePanel?: HTMLPostTabPanelElement['name']; + @Prop() readonly activeTab?: string; /** * When set to true, this property allows the tabs container to span the @@ -45,37 +53,70 @@ export class PostTabs { /** * An event emitted after the active tab changes, when the fade in transition of its associated panel is finished. - * The payload is the name of the newly shown panel. + * The payload is the name of the newly active tab. */ @Event() postChange: EventEmitter; componentDidLoad() { + this.detectMode(); this.moveMisplacedTabs(); this.enableTabs(); - const initiallyActivePanel = this.activePanel || this.tabs[0]?.panel; - void this.show(initiallyActivePanel); + const initiallyActiveTab = this.activeTab || this.tabs[0]?.getAttribute('name'); + void this.show(initiallyActiveTab); this.isLoaded = true; } + private detectMode() { + // Check if any tab headers contain anchor elements (via data-attribute exposure) + const hasNavigationTabs = this.tabs.some(tab => + tab.getAttribute('data-navigation-mode') === 'true' + ); + + // Check if there are any panels + const hasPanels = this.panels.length > 0; + + // Validate for mixed mode (error condition) + if (hasNavigationTabs && hasPanels) { + console.error('PostTabs: Mixed mode detected. Cannot use both navigation mode (anchor elements) and panel mode (post-tab-panel elements) at the same time.'); + return; + } + + this.isNavigationMode = hasNavigationTabs; + } + /** * Shows the panel with the given name and selects its associated tab. + * In navigation mode, only updates the active tab state. * Any other panel that was previously shown becomes hidden and its associated tab is unselected. */ @Method() - async show(panelName: string) { + async show(tabName: string) { // do nothing if the tab is already active - if (panelName === this.activeTab?.panel) { + if (tabName === this.currentActiveTab?.getAttribute('name')) { return; } - const previousTab = this.activeTab; + const previousTab = this.currentActiveTab; const newTab: HTMLPostTabHeaderElement = this.host.querySelector( - `post-tab-header[panel=${panelName}]`, + `post-tab-header[name=${tabName}]`, ); - this.activateTab(newTab); + + if (!newTab) { + console.warn(`PostTabs: No tab found with name "${tabName}"`); + return; + } + + await this.activateTab(newTab); + // In navigation mode, we don't need to handle panels + if (this.isNavigationMode) { + if (this.isLoaded) this.postChange.emit(this.currentActiveTab.getAttribute('name')); + return; + } + + // Panel mode logic // if a panel is currently being displayed, remove it from the view and complete the associated animation if (this.showing) { this.showing.effect['target'].style.display = 'none'; @@ -83,7 +124,7 @@ export class PostTabs { } // hide the currently visible panel only if no other animation is running - if (previousTab && !this.showing && !this.hiding) this.hidePanel(previousTab.panel); + if (previousTab && !this.showing && !this.hiding) this.hidePanel(previousTab.getAttribute('name')); // wait for any hiding animation to complete before showing the selected tab if (this.hiding) await this.hiding.finished; @@ -93,7 +134,7 @@ export class PostTabs { // wait for any display animation to complete for the returned promise to fully resolve if (this.showing) await this.showing.finished; - if (this.isLoaded) this.postChange.emit(this.activeTab.panel); + if (this.isLoaded) this.postChange.emit(this.currentActiveTab.getAttribute('name')); } private moveMisplacedTabs() { @@ -111,21 +152,29 @@ export class PostTabs { this.tabs.forEach(async tab => { await componentOnReady(tab); + // Skip tab setup in navigation mode - anchors handle their own navigation + if (this.isNavigationMode) { + return; + } + + // Panel mode: set up tab-panel relationships // if the tab has an "aria-controls" attribute it was already linked to its panel: do nothing if (tab.getAttribute('aria-controls')) return; - const tabPanel = this.getPanel(tab.panel); - tab.setAttribute('aria-controls', tabPanel.id); - tabPanel.setAttribute('aria-labelledby', tab.id); + const tabPanel = this.getPanel(tab.getAttribute('name')); + if (tabPanel) { + tab.setAttribute('aria-controls', tabPanel.id); + tabPanel.setAttribute('aria-labelledby', tab.id); + } tab.addEventListener('click', () => { - void this.show(tab.panel); + void this.show(tab.getAttribute('name')); }); tab.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - void this.show(tab.panel); + void this.show(tab.getAttribute('name')); } }); @@ -135,23 +184,37 @@ export class PostTabs { }); // if the currently active tab was removed from the DOM then select the first one - if (this.activeTab && !this.activeTab.isConnected) { - void this.show(this.tabs[0]?.panel); + if (this.currentActiveTab && !this.currentActiveTab.isConnected) { + void this.show(this.tabs[0]?.getAttribute('name')); } } - private activateTab(tab: HTMLPostTabHeaderElement) { - if (this.activeTab) { - this.activeTab.setAttribute('aria-selected', 'false'); - this.activeTab.setAttribute('tabindex', '-1'); - this.activeTab.classList.remove('active'); + private async activateTab(tab: HTMLPostTabHeaderElement) { + // Deactivate previous tab + if (this.currentActiveTab) { + this.currentActiveTab.setAttribute('aria-selected', 'false'); + this.currentActiveTab.setAttribute('tabindex', '-1'); + this.currentActiveTab.classList.remove('active'); + + // Remove aria-current from previous tab's anchor (navigation mode) + const previousAnchor = this.currentActiveTab.querySelector('a'); + if (previousAnchor) { + previousAnchor.removeAttribute('aria-current'); + } } + // Activate new tab tab.setAttribute('aria-selected', 'true'); tab.setAttribute('tabindex', '0'); tab.classList.add('active'); + + // Set aria-current on new tab's anchor (navigation mode) + const newAnchor = tab.querySelector('a'); + if (newAnchor) { + newAnchor.setAttribute('aria-current', 'page'); + } - this.activeTab = tab; + this.currentActiveTab = tab; } private hidePanel(panelName: HTMLPostTabPanelElement['name']) { @@ -167,7 +230,7 @@ export class PostTabs { } private showSelectedPanel() { - const panel = this.getPanel(this.activeTab.panel); + const panel = this.getPanel(this.currentActiveTab.getAttribute('name')); panel.style.display = 'block'; // prevent the initially selected panel from fading in @@ -180,7 +243,7 @@ export class PostTabs { } private getPanel(name: string): HTMLPostTabPanelElement { - return this.host.querySelector(`post-tab-panel[name=${name}]`); + return this.host.querySelector(`post-tab-panel[for=${name}]`); } private navigateTabs(tab: HTMLPostTabHeaderElement, key: 'ArrowRight' | 'ArrowLeft') { @@ -199,16 +262,27 @@ export class PostTabs { } render() { + const tabsRole = this.isNavigationMode ? undefined : 'tablist'; + const ariaLabel = this.isNavigationMode ? 'Tabs navigation' : undefined; + return (
-
- this.enableTabs()} /> -
-
-
- this.moveMisplacedTabs()} /> + {this.isNavigationMode ? ( + + ) : ( +
+ this.enableTabs()} /> +
+ )}
+ {!this.isNavigationMode && ( +
+ this.moveMisplacedTabs()} /> +
+ )}
); } From fa1c06d2f79f7ea9c6cafb3abad91862d9c1a990 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 30 Sep 2025 11:17:57 +0200 Subject: [PATCH 002/127] improved the render --- .../src/components/post-tabs/post-tabs.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index c213b50f80..126c9fd4d6 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -264,19 +264,14 @@ export class PostTabs { render() { const tabsRole = this.isNavigationMode ? undefined : 'tablist'; const ariaLabel = this.isNavigationMode ? 'Tabs navigation' : undefined; + const TabsContainer = this.isNavigationMode ? 'nav' : 'div'; return (
- {this.isNavigationMode ? ( - - ) : ( -
- this.enableTabs()} /> -
- )} + + this.enableTabs()} /> +
{!this.isNavigationMode && (
From 20cc211d9975bd3949bcf84f69a7900232accd91 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 30 Sep 2025 11:26:56 +0200 Subject: [PATCH 003/127] changed the logic for assigning aria current --- .../src/components/post-tabs/post-tabs.tsx | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 126c9fd4d6..22d304839a 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -62,8 +62,18 @@ export class PostTabs { this.moveMisplacedTabs(); this.enableTabs(); - const initiallyActiveTab = this.activeTab || this.tabs[0]?.getAttribute('name'); - void this.show(initiallyActiveTab); + if (this.isNavigationMode) { + // In navigation mode, find the tab with aria-current="page" + const activeTab = this.findActiveNavigationTab(); + if (activeTab) { + void this.show(activeTab.getAttribute('name')); + } + // If no aria-current="page" found, don't show any active tab + } else { + // Panel mode: use existing logic + const initiallyActiveTab = this.activeTab || this.tabs[0]?.getAttribute('name'); + void this.show(initiallyActiveTab); + } this.isLoaded = true; } @@ -86,6 +96,14 @@ export class PostTabs { this.isNavigationMode = hasNavigationTabs; } + private findActiveNavigationTab(): HTMLPostTabHeaderElement | null { + // Find the tab that contains an anchor with aria-current="page" + return this.tabs.find(tab => { + const anchor = tab.querySelector('a[aria-current="page"]'); + return anchor !== null; + }) || null; + } + /** * Shows the panel with the given name and selects its associated tab. * In navigation mode, only updates the active tab state. @@ -195,24 +213,12 @@ export class PostTabs { this.currentActiveTab.setAttribute('aria-selected', 'false'); this.currentActiveTab.setAttribute('tabindex', '-1'); this.currentActiveTab.classList.remove('active'); - - // Remove aria-current from previous tab's anchor (navigation mode) - const previousAnchor = this.currentActiveTab.querySelector('a'); - if (previousAnchor) { - previousAnchor.removeAttribute('aria-current'); - } } // Activate new tab tab.setAttribute('aria-selected', 'true'); tab.setAttribute('tabindex', '0'); tab.classList.add('active'); - - // Set aria-current on new tab's anchor (navigation mode) - const newAnchor = tab.querySelector('a'); - if (newAnchor) { - newAnchor.setAttribute('aria-current', 'page'); - } this.currentActiveTab = tab; } From 09440823dff26e2d8cf402836b7f6f4ff6cf83bc Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 30 Sep 2025 11:51:23 +0200 Subject: [PATCH 004/127] small changes --- packages/components/src/components.d.ts | 30 ++++++++-------- .../src/components/post-tab-header/readme.md | 12 +++---- .../src/components/post-tab-panel/readme.md | 6 ++-- .../src/components/post-tabs/post-tabs.tsx | 24 ++++++------- .../src/components/post-tabs/readme.md | 23 ++++++------ .../stories/components/tabs/tabs.stories.ts | 35 +++++++++---------- 6 files changed, 64 insertions(+), 66 deletions(-) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index a4d2a04df2..b893258473 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -448,30 +448,30 @@ export namespace Components { } interface PostTabHeader { /** - * The name of the panel controlled by the tab header. + * The name of the tab, used to associate it with a tab panel or identify the active tab in navigation mode. */ - "panel": string; + "name": string; } interface PostTabPanel { /** - * The name of the panel, used to associate it with a tab header. + * The name of the tab that this panel is associated with. */ - "name": string; + "for": string; } interface PostTabs { /** - * The name of the panel that is initially shown. If not specified, it defaults to the panel associated with the first tab. **Changing this value after initialization has no effect.** + * The name of the tab that is initially active. If not specified, it defaults to the first tab. **Changing this value after initialization has no effect.** */ - "activePanel"?: HTMLPostTabPanelElement['name']; + "activeTab"?: string; /** * When set to true, this property allows the tabs container to span the full width of the screen, from edge to edge. * @default false */ "fullWidth": boolean; /** - * Shows the panel with the given name and selects its associated tab. Any other panel that was previously shown becomes hidden and its associated tab is unselected. + * Shows the panel with the given name and selects its associated tab. In navigation mode, only updates the active tab state. Any other panel that was previously shown becomes hidden and its associated tab is unselected. */ - "show": (panelName: string) => Promise; + "show": (tabName: string) => Promise; } interface PostTogglebutton { /** @@ -1297,28 +1297,28 @@ declare namespace LocalJSX { } interface PostTabHeader { /** - * The name of the panel controlled by the tab header. + * The name of the tab, used to associate it with a tab panel or identify the active tab in navigation mode. */ - "panel": string; + "name": string; } interface PostTabPanel { /** - * The name of the panel, used to associate it with a tab header. + * The name of the tab that this panel is associated with. */ - "name": string; + "for": string; } interface PostTabs { /** - * The name of the panel that is initially shown. If not specified, it defaults to the panel associated with the first tab. **Changing this value after initialization has no effect.** + * The name of the tab that is initially active. If not specified, it defaults to the first tab. **Changing this value after initialization has no effect.** */ - "activePanel"?: HTMLPostTabPanelElement['name']; + "activeTab"?: string; /** * When set to true, this property allows the tabs container to span the full width of the screen, from edge to edge. * @default false */ "fullWidth"?: boolean; /** - * An event emitted after the active tab changes, when the fade in transition of its associated panel is finished. The payload is the name of the newly shown panel. + * An event emitted after the active tab changes, when the fade in transition of its associated panel is finished. The payload is the name of the newly active tab. */ "onPostChange"?: (event: PostTabsCustomEvent) => void; } diff --git a/packages/components/src/components/post-tab-header/readme.md b/packages/components/src/components/post-tab-header/readme.md index 6d09529475..1b7b29298f 100644 --- a/packages/components/src/components/post-tab-header/readme.md +++ b/packages/components/src/components/post-tab-header/readme.md @@ -7,16 +7,16 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| -------------------- | --------- | --------------------------------------------------- | -------- | ----------- | -| `panel` _(required)_ | `panel` | The name of the panel controlled by the tab header. | `string` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ------------------- | --------- | --------------------------------------------------------------------------------------------------------- | -------- | ----------- | +| `name` _(required)_ | `name` | The name of the tab, used to associate it with a tab panel or identify the active tab in navigation mode. | `string` | `undefined` | ## Slots -| Slot | Description | -| ----------- | --------------------------------------- | -| `"default"` | Slot for the content of the tab header. | +| Slot | Description | +| ----------- | ----------------------------------------------------------------------------------------------- | +| `"default"` | Slot for the content of the tab header. Can contain text or an element for navigation mode. | ---------------------------------------------- diff --git a/packages/components/src/components/post-tab-panel/readme.md b/packages/components/src/components/post-tab-panel/readme.md index 7655490f14..3df43b500a 100644 --- a/packages/components/src/components/post-tab-panel/readme.md +++ b/packages/components/src/components/post-tab-panel/readme.md @@ -7,9 +7,9 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ------------------- | --------- | -------------------------------------------------------------- | -------- | ----------- | -| `name` _(required)_ | `name` | The name of the panel, used to associate it with a tab header. | `string` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ------------------ | --------- | ------------------------------------------------------- | -------- | ----------- | +| `for` _(required)_ | `for` | The name of the tab that this panel is associated with. | `string` | `undefined` | ## Slots diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 22d304839a..93d2024d10 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -66,12 +66,12 @@ export class PostTabs { // In navigation mode, find the tab with aria-current="page" const activeTab = this.findActiveNavigationTab(); if (activeTab) { - void this.show(activeTab.getAttribute('name')); + void this.show(activeTab.name); } // If no aria-current="page" found, don't show any active tab } else { // Panel mode: use existing logic - const initiallyActiveTab = this.activeTab || this.tabs[0]?.getAttribute('name'); + const initiallyActiveTab = this.activeTab || this.tabs[0]?.name; void this.show(initiallyActiveTab); } @@ -112,7 +112,7 @@ export class PostTabs { @Method() async show(tabName: string) { // do nothing if the tab is already active - if (tabName === this.currentActiveTab?.getAttribute('name')) { + if (tabName === this.currentActiveTab?.name) { return; } @@ -130,7 +130,7 @@ export class PostTabs { // In navigation mode, we don't need to handle panels if (this.isNavigationMode) { - if (this.isLoaded) this.postChange.emit(this.currentActiveTab.getAttribute('name')); + if (this.isLoaded) this.postChange.emit(this.currentActiveTab.name); return; } @@ -142,7 +142,7 @@ export class PostTabs { } // hide the currently visible panel only if no other animation is running - if (previousTab && !this.showing && !this.hiding) this.hidePanel(previousTab.getAttribute('name')); + if (previousTab && !this.showing && !this.hiding) this.hidePanel(previousTab.name); // wait for any hiding animation to complete before showing the selected tab if (this.hiding) await this.hiding.finished; @@ -152,7 +152,7 @@ export class PostTabs { // wait for any display animation to complete for the returned promise to fully resolve if (this.showing) await this.showing.finished; - if (this.isLoaded) this.postChange.emit(this.currentActiveTab.getAttribute('name')); + if (this.isLoaded) this.postChange.emit(this.currentActiveTab.name); } private moveMisplacedTabs() { @@ -179,20 +179,20 @@ export class PostTabs { // if the tab has an "aria-controls" attribute it was already linked to its panel: do nothing if (tab.getAttribute('aria-controls')) return; - const tabPanel = this.getPanel(tab.getAttribute('name')); + const tabPanel = this.getPanel(tab.name); if (tabPanel) { tab.setAttribute('aria-controls', tabPanel.id); tabPanel.setAttribute('aria-labelledby', tab.id); } tab.addEventListener('click', () => { - void this.show(tab.getAttribute('name')); + void this.show(tab.name); }); tab.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - void this.show(tab.getAttribute('name')); + void this.show(tab.name); } }); @@ -203,7 +203,7 @@ export class PostTabs { // if the currently active tab was removed from the DOM then select the first one if (this.currentActiveTab && !this.currentActiveTab.isConnected) { - void this.show(this.tabs[0]?.getAttribute('name')); + void this.show(this.tabs[0]?.name); } } @@ -223,7 +223,7 @@ export class PostTabs { this.currentActiveTab = tab; } - private hidePanel(panelName: HTMLPostTabPanelElement['name']) { + private hidePanel(panelName: HTMLPostTabPanelElement['for']) { const previousPanel = this.getPanel(panelName); if (!previousPanel) return; @@ -236,7 +236,7 @@ export class PostTabs { } private showSelectedPanel() { - const panel = this.getPanel(this.currentActiveTab.getAttribute('name')); + const panel = this.getPanel(this.currentActiveTab.name); panel.style.display = 'block'; // prevent the initially selected panel from fading in diff --git a/packages/components/src/components/post-tabs/readme.md b/packages/components/src/components/post-tabs/readme.md index a2480e498e..423846e162 100644 --- a/packages/components/src/components/post-tabs/readme.md +++ b/packages/components/src/components/post-tabs/readme.md @@ -7,31 +7,32 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | -| `activePanel` | `active-panel` | The name of the panel that is initially shown. If not specified, it defaults to the panel associated with the first tab. **Changing this value after initialization has no effect.** | `string` | `undefined` | -| `fullWidth` | `full-width` | When set to true, this property allows the tabs container to span the full width of the screen, from edge to edge. | `boolean` | `false` | +| Property | Attribute | Description | Type | Default | +| ----------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | +| `activeTab` | `active-tab` | The name of the tab that is initially active. If not specified, it defaults to the first tab. **Changing this value after initialization has no effect.** | `string` | `undefined` | +| `fullWidth` | `full-width` | When set to true, this property allows the tabs container to span the full width of the screen, from edge to edge. | `boolean` | `false` | ## Events -| Event | Description | Type | -| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| `postChange` | An event emitted after the active tab changes, when the fade in transition of its associated panel is finished. The payload is the name of the newly shown panel. | `CustomEvent` | +| Event | Description | Type | +| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| `postChange` | An event emitted after the active tab changes, when the fade in transition of its associated panel is finished. The payload is the name of the newly active tab. | `CustomEvent` | ## Methods -### `show(panelName: string) => Promise` +### `show(tabName: string) => Promise` Shows the panel with the given name and selects its associated tab. +In navigation mode, only updates the active tab state. Any other panel that was previously shown becomes hidden and its associated tab is unselected. #### Parameters -| Name | Type | Description | -| ----------- | -------- | ----------- | -| `panelName` | `string` | | +| Name | Type | Description | +| --------- | -------- | ----------- | +| `tabName` | `string` | | #### Returns diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index bfc385178f..411f779ef1 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -17,8 +17,8 @@ const meta: MetaComponent = { }, }, argTypes: { - activePanel: { - name: 'active-panel', + activeTab: { + name: 'active-tab', control: 'select', options: ['first', 'second', 'third'], }, @@ -31,22 +31,19 @@ export default meta; function renderTabs(args: Partial) { return html` - First tab - Second tab - Third tab + + First page + + + Second page + + + Third page + - - This is the content of the first tab. By default it is shown initially. - - - This is the content of the second tab. By default it is hidden initially. - - - This is the content of the third tab. By default it is also hidden initially. - `; } @@ -62,7 +59,7 @@ export const Default: Story = { export const ActivePanel: Story = { args: { - activePanel: 'third', + activeTab: 'third', }, }; @@ -83,8 +80,8 @@ export const Async: Story = { tabIndex++; const newTab = ` - New tab ${tabIndex} - This is the content of the new tab ${tabIndex}. + New tab ${tabIndex} + This is the content of the new tab ${tabIndex}. `; tabs?.insertAdjacentHTML('beforeend', newTab); @@ -100,7 +97,7 @@ export const Async: Story = { activeHeader?.remove(); const activePanel: HTMLPostTabPanelElement | null = - document.querySelector(`post-tab-panel[name=${activeHeader?.panel}]`) ?? null; + document.querySelector(`post-tab-panel[name=${activeHeader?.name}]`) ?? null; activePanel?.remove(); }; From ba0fa3519ac46861edc76a0e925a245d93a2c422 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 30 Sep 2025 13:26:54 +0200 Subject: [PATCH 005/127] changed the docs --- .../components/post-tab-header/post-tab-header.tsx | 12 +++++------- .../src/components/post-tabs/post-tabs.tsx | 14 +++++++++++--- .../src/stories/components/tabs/tabs.stories.ts | 6 +++--- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/components/src/components/post-tab-header/post-tab-header.tsx b/packages/components/src/components/post-tab-header/post-tab-header.tsx index e9d57dd965..f0ebf5f810 100644 --- a/packages/components/src/components/post-tab-header/post-tab-header.tsx +++ b/packages/components/src/components/post-tab-header/post-tab-header.tsx @@ -47,17 +47,15 @@ export class PostTabHeader { } render() { - const role = this.isNavigationMode ? undefined : 'tab'; - const ariaSelected = this.isNavigationMode ? undefined : 'false'; - const tabindex = this.isNavigationMode ? undefined : '-1'; - + // Only set ARIA attributes and tabindex in panel mode + const isPanelMode = !this.isNavigationMode; return ( diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 93d2024d10..f047f17d5d 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -170,7 +170,7 @@ export class PostTabs { this.tabs.forEach(async tab => { await componentOnReady(tab); - // Skip tab setup in navigation mode - anchors handle their own navigation + // In navigation mode, do not add any event listeners or panel relationships; let anchors handle navigation natively if (this.isNavigationMode) { return; } @@ -211,13 +211,21 @@ export class PostTabs { // Deactivate previous tab if (this.currentActiveTab) { this.currentActiveTab.setAttribute('aria-selected', 'false'); - this.currentActiveTab.setAttribute('tabindex', '-1'); + if (!this.isNavigationMode) { + this.currentActiveTab.setAttribute('tabindex', '-1'); + } else { + this.currentActiveTab.removeAttribute('tabindex'); + } this.currentActiveTab.classList.remove('active'); } // Activate new tab tab.setAttribute('aria-selected', 'true'); - tab.setAttribute('tabindex', '0'); + if (!this.isNavigationMode) { + tab.setAttribute('tabindex', '0'); + } else { + tab.removeAttribute('tabindex'); + } tab.classList.add('active'); this.currentActiveTab = tab; diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index 411f779ef1..6b026386eb 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -35,13 +35,13 @@ function renderTabs(args: Partial) { full-width="${args.fullWidth ? true : nothing}" > - First page + First page - Second page + Second page - Third page + Third page From cc36657d073faf0bf6c509aa13c54b3c428e800b Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 30 Sep 2025 15:30:26 +0200 Subject: [PATCH 006/127] fixed the `Enter` button --- .../src/components/post-tabs/post-tabs.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index f047f17d5d..05f66f2989 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -78,11 +78,11 @@ export class PostTabs { this.isLoaded = true; } - private detectMode() { - // Check if any tab headers contain anchor elements (via data-attribute exposure) - const hasNavigationTabs = this.tabs.some(tab => - tab.getAttribute('data-navigation-mode') === 'true' - ); + private detectMode() { + const hasNavigationTabs = this.tabs.some(tab => { + const navMode = tab.getAttribute('data-navigation-mode') === 'true'; + return navMode; + }); // Check if there are any panels const hasPanels = this.panels.length > 0; @@ -165,18 +165,18 @@ export class PostTabs { } private enableTabs() { + // Prevent early call before detectMode() + if (!this.isLoaded) return; + if (!this.tabs) return; this.tabs.forEach(async tab => { await componentOnReady(tab); - // In navigation mode, do not add any event listeners or panel relationships; let anchors handle navigation natively if (this.isNavigationMode) { return; } - // Panel mode: set up tab-panel relationships - // if the tab has an "aria-controls" attribute it was already linked to its panel: do nothing if (tab.getAttribute('aria-controls')) return; const tabPanel = this.getPanel(tab.name); @@ -200,7 +200,6 @@ export class PostTabs { if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') this.navigateTabs(tab, e.key); }); }); - // if the currently active tab was removed from the DOM then select the first one if (this.currentActiveTab && !this.currentActiveTab.isConnected) { void this.show(this.tabs[0]?.name); From a51883bd440fd9e11cea9a5ba7094f1aeb21a00e Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 30 Sep 2025 15:35:02 +0200 Subject: [PATCH 007/127] reverted some redundant changes --- packages/components/src/components/post-tabs/post-tabs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 05f66f2989..242214d1ff 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -206,7 +206,7 @@ export class PostTabs { } } - private async activateTab(tab: HTMLPostTabHeaderElement) { + private activateTab(tab: HTMLPostTabHeaderElement) { // Deactivate previous tab if (this.currentActiveTab) { this.currentActiveTab.setAttribute('aria-selected', 'false'); From 428f9bc54b5abf2787a53d3a426c2a65a8038a69 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 30 Sep 2025 15:48:42 +0200 Subject: [PATCH 008/127] changed some PostTabHeader to PostTabItem --- packages/components/src/components.d.ts | 18 ++++++------- .../post-tab-item.scss} | 0 .../post-tab-item.tsx} | 10 +++---- .../readme.md | 6 ++--- .../src/components/post-tabs/post-tabs.tsx | 20 +++++++------- .../src/components/post-tabs/readme.md | 8 +++--- .../stories/components/tabs/tabs.stories.ts | 26 +++++++++---------- .../nextjs-integration/src/app/ssr/page.tsx | 14 +++++----- 8 files changed, 51 insertions(+), 51 deletions(-) rename packages/components/src/components/{post-tab-header/post-tab-header.scss => post-tab-item/post-tab-item.scss} (100%) rename packages/components/src/components/{post-tab-header/post-tab-header.tsx => post-tab-item/post-tab-item.tsx} (86%) rename packages/components/src/components/{post-tab-header => post-tab-item}/readme.md (79%) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index b893258473..5eb1513889 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -446,7 +446,7 @@ export namespace Components { */ "stars": number; } - interface PostTabHeader { + interface PostTabItem { /** * The name of the tab, used to associate it with a tab panel or identify the active tab in navigation mode. */ @@ -836,11 +836,11 @@ declare global { prototype: HTMLPostRatingElement; new (): HTMLPostRatingElement; }; - interface HTMLPostTabHeaderElement extends Components.PostTabHeader, HTMLStencilElement { + interface HTMLPostTabItemElement extends Components.PostTabItem, HTMLStencilElement { } - var HTMLPostTabHeaderElement: { - prototype: HTMLPostTabHeaderElement; - new (): HTMLPostTabHeaderElement; + var HTMLPostTabItemElement: { + prototype: HTMLPostTabItemElement; + new (): HTMLPostTabItemElement; }; interface HTMLPostTabPanelElement extends Components.PostTabPanel, HTMLStencilElement { } @@ -913,7 +913,7 @@ declare global { "post-popover": HTMLPostPopoverElement; "post-popovercontainer": HTMLPostPopovercontainerElement; "post-rating": HTMLPostRatingElement; - "post-tab-header": HTMLPostTabHeaderElement; + "post-tab-item": HTMLPostTabItemElement; "post-tab-panel": HTMLPostTabPanelElement; "post-tabs": HTMLPostTabsElement; "post-togglebutton": HTMLPostTogglebuttonElement; @@ -1295,7 +1295,7 @@ declare namespace LocalJSX { */ "stars"?: number; } - interface PostTabHeader { + interface PostTabItem { /** * The name of the tab, used to associate it with a tab panel or identify the active tab in navigation mode. */ @@ -1391,7 +1391,7 @@ declare namespace LocalJSX { "post-popover": PostPopover; "post-popovercontainer": PostPopovercontainer; "post-rating": PostRating; - "post-tab-header": PostTabHeader; + "post-tab-item": PostTabItem; "post-tab-panel": PostTabPanel; "post-tabs": PostTabs; "post-togglebutton": PostTogglebutton; @@ -1438,7 +1438,7 @@ declare module "@stencil/core" { "post-popover": LocalJSX.PostPopover & JSXBase.HTMLAttributes; "post-popovercontainer": LocalJSX.PostPopovercontainer & JSXBase.HTMLAttributes; "post-rating": LocalJSX.PostRating & JSXBase.HTMLAttributes; - "post-tab-header": LocalJSX.PostTabHeader & JSXBase.HTMLAttributes; + "post-tab-item": LocalJSX.PostTabItem & JSXBase.HTMLAttributes; "post-tab-panel": LocalJSX.PostTabPanel & JSXBase.HTMLAttributes; "post-tabs": LocalJSX.PostTabs & JSXBase.HTMLAttributes; "post-togglebutton": LocalJSX.PostTogglebutton & JSXBase.HTMLAttributes; diff --git a/packages/components/src/components/post-tab-header/post-tab-header.scss b/packages/components/src/components/post-tab-item/post-tab-item.scss similarity index 100% rename from packages/components/src/components/post-tab-header/post-tab-header.scss rename to packages/components/src/components/post-tab-item/post-tab-item.scss diff --git a/packages/components/src/components/post-tab-header/post-tab-header.tsx b/packages/components/src/components/post-tab-item/post-tab-item.tsx similarity index 86% rename from packages/components/src/components/post-tab-header/post-tab-header.tsx rename to packages/components/src/components/post-tab-item/post-tab-item.tsx index f0ebf5f810..42bc6228ec 100644 --- a/packages/components/src/components/post-tab-header/post-tab-header.tsx +++ b/packages/components/src/components/post-tab-item/post-tab-item.tsx @@ -4,16 +4,16 @@ import { checkRequiredAndType } from '@/utils'; import { nanoid } from 'nanoid'; /** - * @slot default - Slot for the content of the tab header. Can contain text or an element for navigation mode. + * @slot default - Slot for the content of the tab item. Can contain text or an element for navigation mode. */ @Component({ - tag: 'post-tab-header', - styleUrl: 'post-tab-header.scss', + tag: 'post-tab-item', + styleUrl: 'post-tab-item.scss', shadow: true, }) -export class PostTabHeader { - @Element() host: HTMLPostTabHeaderElement; +export class PostTabItem { + @Element() host: HTMLPostTabItemElement; @State() tabId: string; @State() isNavigationMode = false; diff --git a/packages/components/src/components/post-tab-header/readme.md b/packages/components/src/components/post-tab-item/readme.md similarity index 79% rename from packages/components/src/components/post-tab-header/readme.md rename to packages/components/src/components/post-tab-item/readme.md index 1b7b29298f..3a9f1b8fbe 100644 --- a/packages/components/src/components/post-tab-header/readme.md +++ b/packages/components/src/components/post-tab-item/readme.md @@ -14,9 +14,9 @@ ## Slots -| Slot | Description | -| ----------- | ----------------------------------------------------------------------------------------------- | -| `"default"` | Slot for the content of the tab header. Can contain text or an element for navigation mode. | +| Slot | Description | +| ----------- | --------------------------------------------------------------------------------------------- | +| `"default"` | Slot for the content of the tab item. Can contain text or an element for navigation mode. | ---------------------------------------------- diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 242214d1ff..a4987310fc 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -4,7 +4,7 @@ import { fadeIn, fadeOut } from '@/animations'; import { componentOnReady } from '@/utils'; /** - * @slot tabs - Slot for placing tab headers. Each tab header should be a element. + * @slot tabs - Slot for placing tab items. Each tab item should be a element. * @slot default - Slot for placing tab panels. Each tab panel should be a element. * @part tabs - The container element that holds the set of tabs. * @part content - The container element that displays the content of the currently active tab. @@ -16,16 +16,16 @@ import { componentOnReady } from '@/utils'; shadow: true, }) export class PostTabs { - private currentActiveTab: HTMLPostTabHeaderElement; + private currentActiveTab: HTMLPostTabItemElement; private showing: Animation; private hiding: Animation; private isLoaded = false; @State() isNavigationMode: boolean = false; - private get tabs(): HTMLPostTabHeaderElement[] { + private get tabs(): HTMLPostTabItemElement[] { return Array.from( - this.host.querySelectorAll('post-tab-header'), + this.host.querySelectorAll('post-tab-item'), ).filter(tab => tab.closest('post-tabs') === this.host); } @@ -96,7 +96,7 @@ export class PostTabs { this.isNavigationMode = hasNavigationTabs; } - private findActiveNavigationTab(): HTMLPostTabHeaderElement | null { + private findActiveNavigationTab(): HTMLPostTabItemElement | null { // Find the tab that contains an anchor with aria-current="page" return this.tabs.find(tab => { const anchor = tab.querySelector('a[aria-current="page"]'); @@ -117,8 +117,8 @@ export class PostTabs { } const previousTab = this.currentActiveTab; - const newTab: HTMLPostTabHeaderElement = this.host.querySelector( - `post-tab-header[name=${tabName}]`, + const newTab: HTMLPostTabItemElement = this.host.querySelector( + `post-tab-item[name=${tabName}]`, ); if (!newTab) { @@ -206,7 +206,7 @@ export class PostTabs { } } - private activateTab(tab: HTMLPostTabHeaderElement) { + private activateTab(tab: HTMLPostTabItemElement) { // Deactivate previous tab if (this.currentActiveTab) { this.currentActiveTab.setAttribute('aria-selected', 'false'); @@ -259,10 +259,10 @@ export class PostTabs { return this.host.querySelector(`post-tab-panel[for=${name}]`); } - private navigateTabs(tab: HTMLPostTabHeaderElement, key: 'ArrowRight' | 'ArrowLeft') { + private navigateTabs(tab: HTMLPostTabItemElement, key: 'ArrowRight' | 'ArrowLeft') { const activeTabIndex = Array.from(this.tabs).indexOf(tab); - let nextTab: HTMLPostTabHeaderElement; + let nextTab: HTMLPostTabItemElement; if (key === 'ArrowRight') { nextTab = this.tabs[activeTabIndex + 1] || this.tabs[0]; } else { diff --git a/packages/components/src/components/post-tabs/readme.md b/packages/components/src/components/post-tabs/readme.md index 423846e162..3fdb5f44cc 100644 --- a/packages/components/src/components/post-tabs/readme.md +++ b/packages/components/src/components/post-tabs/readme.md @@ -43,10 +43,10 @@ Type: `Promise` ## Slots -| Slot | Description | -| ----------- | ------------------------------------------------------------------------------------ | -| `"default"` | Slot for placing tab panels. Each tab panel should be a element. | -| `"tabs"` | Slot for placing tab headers. Each tab header should be a element. | +| Slot | Description | +| ----------- | --------------------------------------------------------------------------------- | +| `"default"` | Slot for placing tab panels. Each tab panel should be a element. | +| `"tabs"` | Slot for placing tab items. Each tab item should be a element. | ## Shadow Parts diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index 6b026386eb..ae1a728d55 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -34,15 +34,15 @@ function renderTabs(args: Partial) { active-tab="${ifDefined(args.activeTab)}" full-width="${args.fullWidth ? true : nothing}" > - + First page - - + + Second page - - + + Third page - + `; @@ -80,7 +80,7 @@ export const Async: Story = { tabIndex++; const newTab = ` - New tab ${tabIndex} + New tab ${tabIndex} This is the content of the new tab ${tabIndex}. `; @@ -88,16 +88,16 @@ export const Async: Story = { }; const removeActiveTab = () => { - const headers: NodeListOf | undefined = - document.querySelectorAll('post-tab-header'); + const items: NodeListOf | undefined = + document.querySelectorAll('post-tab-item'); - const activeHeader: HTMLPostTabHeaderElement | undefined = Array.from(headers ?? []).find( - () => document.querySelectorAll('post-tab-header.active'), + const activeItem: HTMLPostTabItemElement | undefined = Array.from(items ?? []).find( + () => document.querySelectorAll('post-tab-item.active'), ); - activeHeader?.remove(); + activeItem?.remove(); const activePanel: HTMLPostTabPanelElement | null = - document.querySelector(`post-tab-panel[name=${activeHeader?.name}]`) ?? null; + document.querySelector(`post-tab-panel[name=${activeItem?.name}]`) ?? null; activePanel?.remove(); }; diff --git a/packages/nextjs-integration/src/app/ssr/page.tsx b/packages/nextjs-integration/src/app/ssr/page.tsx index 0d3c6f80a2..86c782fc46 100644 --- a/packages/nextjs-integration/src/app/ssr/page.tsx +++ b/packages/nextjs-integration/src/app/ssr/page.tsx @@ -15,7 +15,7 @@ import { PostPopover, PostRating, PostTabs, - PostTabHeader, + PostTabItem, PostTabPanel, PostTooltipTrigger, PostTooltip, @@ -153,17 +153,17 @@ export default function Home() {

Tabs

- Unua langeto - Dua langeto - Tria langeto + Unua langeto + Dua langeto + Tria langeto - + Jen la enhavo de la unua langeto. Defaŭlte ĝi montriĝas komence. - + Jen la enhavo de la dua langeto. Defaŭlte ĝi estas kaŝita komence. - + Jen la enhavo de la tria langeto. Defaŭlte ĝi ankaŭ estas kaŝita komence. From ec120a52c1cf8d22be296359e31cacc226681f75 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 30 Sep 2025 21:10:47 +0200 Subject: [PATCH 009/127] some adjustments --- .../src/components/post-tabs/post-tabs.tsx | 34 +++++++++---------- .../stories/components/tabs/tabs.stories.ts | 9 +++-- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index a4987310fc..81783dce8c 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -58,25 +58,21 @@ export class PostTabs { @Event() postChange: EventEmitter; componentDidLoad() { - this.detectMode(); - this.moveMisplacedTabs(); - this.enableTabs(); - - if (this.isNavigationMode) { - // In navigation mode, find the tab with aria-current="page" - const activeTab = this.findActiveNavigationTab(); - if (activeTab) { - void this.show(activeTab.name); - } - // If no aria-current="page" found, don't show any active tab - } else { - // Panel mode: use existing logic - const initiallyActiveTab = this.activeTab || this.tabs[0]?.name; - void this.show(initiallyActiveTab); + this.detectMode(); + this.moveMisplacedTabs(); + this.isLoaded = true; // <-- Set isLoaded before enabling tabs + this.enableTabs(); + + if (this.isNavigationMode) { + const activeTab = this.findActiveNavigationTab(); + if (activeTab) { + void this.show(activeTab.name); } - - this.isLoaded = true; + } else { + const initiallyActiveTab = this.activeTab || this.tabs[0]?.name; + void this.show(initiallyActiveTab); } +} private detectMode() { const hasNavigationTabs = this.tabs.some(tab => { @@ -147,7 +143,9 @@ export class PostTabs { // wait for any hiding animation to complete before showing the selected tab if (this.hiding) await this.hiding.finished; - this.showSelectedPanel(); + if (!this.isNavigationMode) { + this.showSelectedPanel(); + } // wait for any display animation to complete for the returned promise to fully resolve if (this.showing) await this.showing.finished; diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index ae1a728d55..969c173ee5 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -35,15 +35,14 @@ function renderTabs(args: Partial) { full-width="${args.fullWidth ? true : nothing}" > - First page - + First page + - Second page + Second page - Third page + Third page - `; } From f279029ff615bbcb530905e0e297b9b9a5c5351b Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 30 Sep 2025 21:13:47 +0200 Subject: [PATCH 010/127] removed redundant code --- packages/components/src/components/post-tabs/post-tabs.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 81783dce8c..f8164820c0 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -60,7 +60,7 @@ export class PostTabs { componentDidLoad() { this.detectMode(); this.moveMisplacedTabs(); - this.isLoaded = true; // <-- Set isLoaded before enabling tabs + this.isLoaded = true; this.enableTabs(); if (this.isNavigationMode) { @@ -80,10 +80,8 @@ export class PostTabs { return navMode; }); - // Check if there are any panels const hasPanels = this.panels.length > 0; - // Validate for mixed mode (error condition) if (hasNavigationTabs && hasPanels) { console.error('PostTabs: Mixed mode detected. Cannot use both navigation mode (anchor elements) and panel mode (post-tab-panel elements) at the same time.'); return; @@ -93,7 +91,6 @@ export class PostTabs { } private findActiveNavigationTab(): HTMLPostTabItemElement | null { - // Find the tab that contains an anchor with aria-current="page" return this.tabs.find(tab => { const anchor = tab.querySelector('a[aria-current="page"]'); return anchor !== null; From 10aa194b07b8e804b8edb58c4af21af996423817 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 30 Sep 2025 21:21:35 +0200 Subject: [PATCH 011/127] removed redundant code --- .../post-tab-item/post-tab-item.tsx | 2 -- .../src/components/post-tabs/post-tabs.tsx | 26 +++++++++---------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/packages/components/src/components/post-tab-item/post-tab-item.tsx b/packages/components/src/components/post-tab-item/post-tab-item.tsx index 42bc6228ec..2bcb76940f 100644 --- a/packages/components/src/components/post-tab-item/post-tab-item.tsx +++ b/packages/components/src/components/post-tab-item/post-tab-item.tsx @@ -42,12 +42,10 @@ export class PostTabItem { const hasAnchor = this.host.querySelector('a') !== null; this.isNavigationMode = hasAnchor; - // Expose mode to parent post-tabs via data-attribute (as per requirements) this.host.setAttribute('data-navigation-mode', this.isNavigationMode.toString()); } render() { - // Only set ARIA attributes and tabindex in panel mode const isPanelMode = !this.isNavigationMode; return ( ; componentDidLoad() { - this.detectMode(); - this.moveMisplacedTabs(); - this.isLoaded = true; - this.enableTabs(); - - if (this.isNavigationMode) { - const activeTab = this.findActiveNavigationTab(); - if (activeTab) { - void this.show(activeTab.name); + this.detectMode(); + this.moveMisplacedTabs(); + this.isLoaded = true; + this.enableTabs(); + + if (this.isNavigationMode) { + const activeTab = this.findActiveNavigationTab(); + if (activeTab) { + void this.show(activeTab.name); + } + } else { + const initiallyActiveTab = this.activeTab || this.tabs[0]?.name; + void this.show(initiallyActiveTab); } - } else { - const initiallyActiveTab = this.activeTab || this.tabs[0]?.name; - void this.show(initiallyActiveTab); } -} private detectMode() { const hasNavigationTabs = this.tabs.some(tab => { From 0817546beadb6bbb7daf6b3eb1b3046bf17300b8 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 30 Sep 2025 21:26:32 +0200 Subject: [PATCH 012/127] removed comments --- packages/components/src/components/post-tabs/post-tabs.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 1ceb92427f..46cad5dfb6 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -127,7 +127,6 @@ export class PostTabs { return; } - // Panel mode logic // if a panel is currently being displayed, remove it from the view and complete the associated animation if (this.showing) { this.showing.effect['target'].style.display = 'none'; From 2d1daa3b324b8052454baed09cb9610f79eed29b Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 30 Sep 2025 21:27:16 +0200 Subject: [PATCH 013/127] removed comments --- packages/components/src/components/post-tabs/post-tabs.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 46cad5dfb6..dc0266db14 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -201,7 +201,6 @@ export class PostTabs { } private activateTab(tab: HTMLPostTabItemElement) { - // Deactivate previous tab if (this.currentActiveTab) { this.currentActiveTab.setAttribute('aria-selected', 'false'); if (!this.isNavigationMode) { @@ -212,7 +211,6 @@ export class PostTabs { this.currentActiveTab.classList.remove('active'); } - // Activate new tab tab.setAttribute('aria-selected', 'true'); if (!this.isNavigationMode) { tab.setAttribute('tabindex', '0'); From d2e814d03157818ea089212f837939b5a81c79a7 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 30 Sep 2025 21:32:43 +0200 Subject: [PATCH 014/127] changed the naming in the test files --- packages/components/cypress/e2e/tabs.cy.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/components/cypress/e2e/tabs.cy.ts b/packages/components/cypress/e2e/tabs.cy.ts index 0b302db62a..3a6332db8b 100644 --- a/packages/components/cypress/e2e/tabs.cy.ts +++ b/packages/components/cypress/e2e/tabs.cy.ts @@ -4,7 +4,7 @@ describe('tabs', () => { describe('default', () => { beforeEach(() => { cy.getComponent('tabs', TABS_ID); - cy.get('post-tab-header').as('headers'); + cy.get('post-tab-item').as('headers'); }); it('should render', () => { @@ -16,7 +16,7 @@ describe('tabs', () => { }); it('should only show the first tab header as active', () => { - cy.get('post-tab-header.active').each(($header, index) => { + cy.get('post-tab-item.active').each(($header, index) => { cy.wrap($header).should(index === 0 ? 'exist' : 'not.exist'); }); }); @@ -59,7 +59,7 @@ describe('tabs', () => { describe('active panel', () => { beforeEach(() => { cy.getComponent('tabs', TABS_ID, 'active-panel'); - cy.get('post-tab-header').as('headers'); + cy.get('post-tab-item').as('headers'); cy.get('post-tab-panel:visible').as('panel'); }); @@ -92,7 +92,7 @@ describe('tabs', () => { describe('async', () => { beforeEach(() => { cy.getComponent('tabs', TABS_ID, 'async'); - cy.get('post-tab-header').as('headers'); + cy.get('post-tab-item').as('headers'); }); it('should add a tab header', () => { @@ -116,7 +116,7 @@ describe('tabs', () => { it('should activate the newly added tab header after clicking on it', () => { cy.get('#add-tab').click(); - cy.get('post-tab-header').as('headers'); + cy.get('post-tab-item').as('headers'); cy.get('@headers').last().click(); cy.get('@headers').first().should('not.have.class', 'active'); @@ -126,7 +126,7 @@ describe('tabs', () => { it('should display the tab panel associated with the newly added tab after clicking on it', () => { cy.get('#add-tab').click(); - cy.get('post-tab-header').last().as('new-panel'); + cy.get('post-tab-item').last().as('new-panel'); cy.get('@new-panel').click(); // wait for the fade out animation to complete From 2a468b09e19f5f4b846f8ee07a9245f8a3358e9d Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 1 Oct 2025 08:20:17 +0200 Subject: [PATCH 015/127] added initial docs --- .../src/stories/components/tabs/tabs.docs.mdx | 69 +++++- .../stories/components/tabs/tabs.stories.ts | 203 ++++++++++++++++-- 2 files changed, 246 insertions(+), 26 deletions(-) diff --git a/packages/documentation/src/stories/components/tabs/tabs.docs.mdx b/packages/documentation/src/stories/components/tabs/tabs.docs.mdx index 6f3f4d3fc2..82feb97519 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.docs.mdx +++ b/packages/documentation/src/stories/components/tabs/tabs.docs.mdx @@ -16,21 +16,76 @@ import SampleCustomTrigger from './tabs-custom-trigger.sample?raw'; +
+ Try it: Use the Mode control above to switch between panels and navigation modes and see how the component adapts! +
+ +### Panels Mode + +Use panels mode to organize content into switchable sections within the same page. + + + +**How it works:** +- Each `` contains only text (no anchor links) +- Each `` references its tab using the `for` attribute +- Clicking a tab shows its associated panel + +```html + + First tab + +

Content for the first tab.

+
+ + Second tab + +

Content for the second tab.

+
+
+``` + +### Navigation Mode + +Use navigation mode to create a tab-style navigation menu for routing between pages. + + + +**How it works:** +- Each `` contains an `` element — this automatically activates navigation mode +- No `` elements are used +- The component renders as semantic `
-
-

Post Tabs

- - Unua langeto - Dua langeto - Tria langeto - - - Jen la enhavo de la unua langeto. Defaŭlte ĝi montriĝas komence. - - - Jen la enhavo de la dua langeto. Defaŭlte ĝi estas kaŝita komence. - - - Jen la enhavo de la tria langeto. Defaŭlte ĝi ankaŭ estas kaŝita komence. - - -
-

Post Tooltip

diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts index 344df05906..ec037fad3a 100644 --- a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts +++ b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts @@ -35,9 +35,6 @@ import { PostPopover, PostPopovercontainer, PostRating, - PostTabs, - PostTabItem, - PostTabPanel, PostTooltipTrigger, ] }) From d149ff8186f634495401539deef1ae361a7bca44 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Mon, 13 Oct 2025 11:39:46 +0200 Subject: [PATCH 056/127] removed duplicate code --- .../consumer-app/src/app/routes/home/home.component.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts index ec037fad3a..fabd8fffa9 100644 --- a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts +++ b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts @@ -12,9 +12,6 @@ import { PostPopover, PostPopovercontainer, PostRating, - PostTabs, - PostTabItem, - PostTabPanel, PostTooltipTrigger, } from 'components'; From e15089857b1b1a320462708e54149e2d4a4b195f Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 15 Oct 2025 09:54:07 +0200 Subject: [PATCH 057/127] fixed e2e tests for `post-tabs` --- packages/components-angular/package.json | 1 - .../consumer-app/cypress/e2e/tabs.cy.ts | 185 +----------------- .../consumer-app/cypress/support/e2e.ts | 1 - packages/nextjs-integration/package.json | 1 - 4 files changed, 10 insertions(+), 178 deletions(-) diff --git a/packages/components-angular/package.json b/packages/components-angular/package.json index 4f450fa544..696ee57ac5 100644 --- a/packages/components-angular/package.json +++ b/packages/components-angular/package.json @@ -49,7 +49,6 @@ "angular-eslint": "19.1.0", "copyfiles": "2.4.1", "cypress": "14.3.2", - "cypress-axe": "1.5.0", "eslint": "9.18.0", "globals": "16.0.0", "karma": "6.4.4", diff --git a/packages/components-angular/projects/consumer-app/cypress/e2e/tabs.cy.ts b/packages/components-angular/projects/consumer-app/cypress/e2e/tabs.cy.ts index 7fc7e2bf6e..b1fc78f062 100644 --- a/packages/components-angular/projects/consumer-app/cypress/e2e/tabs.cy.ts +++ b/packages/components-angular/projects/consumer-app/cypress/e2e/tabs.cy.ts @@ -1,181 +1,16 @@ describe('Tabs', () => { beforeEach(() => { - cy.visit('/tabs'); - cy.injectAxe(); - cy.get('post-tabs').first().as('panelTabs'); - cy.get('post-tabs').last().as('navTabs'); - }); - - describe('Panel Variant - Default', () => { - it('should render the tabs component', () => { - cy.get('@panelTabs').should('exist'); - }); - - it('should show three tab headers', () => { - cy.get('@panelTabs').find('post-tab-item').should('have.length', 3); - }); - - it('should only show the first tab header as active', () => { - cy.get('@panelTabs').find('post-tab-item').first().should('have.class', 'active'); - cy.get('@panelTabs').find('post-tab-item').eq(1).should('not.have.class', 'active'); - cy.get('@panelTabs').find('post-tab-item').eq(2).should('not.have.class', 'active'); - }); - - it('should only show the tab panel associated with the first tab header', () => { - cy.get('@panelTabs').find('post-tab-panel:visible').as('panel'); - cy.get('@panel').should('have.length', 1); - cy.get('@panelTabs') - .find('post-tab-item') - .first() - .invoke('attr', 'name') - .then(tabName => { - cy.get('@panel').invoke('attr', 'for').should('equal', tabName); - }); - }); - - it('should activate a clicked tab header and deactivate the tab header that was previously activated', () => { - cy.get('@panelTabs').find('post-tab-item').last().click(); - - cy.get('@panelTabs').find('post-tab-item').first().should('not.have.class', 'active'); - cy.get('@panelTabs').find('post-tab-item').last().should('have.class', 'active'); - }); - - it('should show the panel associated with a clicked tab header', () => { - cy.get('@panelTabs').find('post-tab-item').last().click(); - - cy.get('@panelTabs').find('post-tab-panel:visible').should('have.length', 1); - - cy.get('@panelTabs') - .find('post-tab-item') - .last() - .invoke('attr', 'name') - .then(tabName => { - cy.get('@panelTabs') - .find('post-tab-panel:visible') - .invoke('attr', 'for') - .should('equal', tabName); - }); - }); - }); - - describe('Navigation Variant', () => { - it('should render as navigation when tabs contain anchor elements', () => { - cy.get('@navTabs').should('exist'); - cy.get('@navTabs').find('post-tab-item').should('have.length', 3); - cy.get('@navTabs') - .find('post-tab-item') - .each($item => { - cy.wrap($item).find('a').should('exist'); - }); - }); - - it('should not render tab panels in navigation variant', () => { - cy.get('@navTabs').find('post-tab-panel').should('not.exist'); - cy.get('@navTabs').find('[part="content"]').should('not.exist'); - }); - - it('should render the tabs container as nav element', () => { - cy.get('@navTabs').find('nav[role="navigation"], nav').should('exist'); - }); - - it('should set proper ARIA attributes for navigation', () => { - cy.get('@navTabs').find('nav').should('have.attr', 'aria-label', 'Tabs navigation'); - }); - - it('should support programmatic tab activation via show() method', () => { - cy.get('@navTabs').then($tabs => { - const tabsElement = $tabs[0] as HTMLElement & { show: (tabName: string) => void }; - tabsElement.show('nav-second'); - }); - cy.get('@navTabs').find('post-tab-item').eq(1).should('have.class', 'active'); + cy.visit('/tabs', { + onBeforeLoad(win) { + cy.spy(win.console, 'error').as('consoleError'); + } }); }); - describe('Accessibility - Panel Variant', () => { - beforeEach(() => { - cy.get('@panelTabs').as('tabs'); - }); - - it('should have proper ARIA attributes for panels variant', () => { - cy.get('@tabs').find('[role="tablist"]').should('exist'); - cy.get('@tabs').find('post-tab-item').should('have.attr', 'role', 'tab'); - cy.get('@tabs').find('post-tab-item').should('have.attr', 'aria-selected'); - cy.get('@tabs').find('post-tab-item').first().should('have.attr', 'aria-selected', 'true'); - cy.get('@tabs').find('post-tab-item').not(':first').should('have.attr', 'aria-selected', 'false'); - }); - - it('should link tabs to panels with aria-controls and aria-labelledby', () => { - cy.get('@tabs') - .find('post-tab-item') - .first() - .then($tab => { - const tabId = $tab.attr('id'); - const ariaControls = $tab.attr('aria-controls'); - - cy.get(`post-tab-panel[id="${ariaControls}"]`).should('exist'); - cy.get(`post-tab-panel[id="${ariaControls}"]`).should( - 'have.attr', - 'aria-labelledby', - tabId, - ); - }); - }); - - it('should manage tabindex properly', () => { - cy.get('@tabs').find('post-tab-item').first().should('have.attr', 'tabindex', '0'); - cy.get('@tabs').find('post-tab-item').not(':first').should('have.attr', 'tabindex', '-1'); - - cy.get('@tabs').find('post-tab-item').last().click(); - cy.get('@tabs').find('post-tab-item').last().should('have.attr', 'tabindex', '0'); - cy.get('@tabs').find('post-tab-item').not(':last').should('have.attr', 'tabindex', '-1'); - }); - }); - - describe('Accessibility - Navigation Variant', () => { - beforeEach(() => { - cy.get('@navTabs').as('tabs'); - }); - - it('should have proper ARIA attributes for navigation variant', () => { - cy.get('@tabs').find('nav').should('have.attr', 'aria-label', 'Tabs navigation'); - cy.get('@tabs').find('post-tab-item').should('not.have.attr', 'role'); - cy.get('@tabs').find('post-tab-item').should('not.have.attr', 'tabindex'); - }); - - it('should not have tablist role in navigation variant', () => { - cy.get('@tabs').find('[role="tablist"]').should('not.exist'); - }); - }); - - describe('Variant Detection', () => { - it('should detect panel variant when no anchor elements are present', () => { - cy.get('@panelTabs').should('exist'); - cy.get('@panelTabs').find('post-tab-panel').should('exist'); - cy.get('@panelTabs').find('[part="content"]').should('exist'); - }); - - it('should detect navigation variant when anchor elements are present', () => { - cy.get('@navTabs').should('exist'); - cy.get('@navTabs').find('post-tab-panel').should('not.exist'); - cy.get('@navTabs').find('nav').should('exist'); - }); - }); - - describe('Accessibility Violations', () => { - it('should not have any automatically detectable accessibility issues in panels variant', () => { - cy.get('@panelTabs').should('be.visible'); - cy.get('@panelTabs').find('post-tab-item').first().should('be.visible'); - cy.get('@panelTabs').find('[role="tablist"]').then($tablist => { - cy.checkA11y($tablist[0]); - }); - }); - - it('should not have any automatically detectable accessibility issues in navigation variant', () => { - cy.get('@navTabs').should('be.visible'); - cy.get('@navTabs').find('post-tab-item').first().should('be.visible'); - cy.get('@navTabs').then($el => { - cy.checkA11y($el[0]); - }); - }); + it('should render tabs components without errors', () => { + cy.get('post-tabs').first().should('be.visible'); + cy.get('post-tabs').last().should('be.visible'); + + cy.get('@consoleError').should('not.be.called'); }); -}); +}); \ No newline at end of file diff --git a/packages/components-angular/projects/consumer-app/cypress/support/e2e.ts b/packages/components-angular/projects/consumer-app/cypress/support/e2e.ts index 48dc5963b1..959d46bc93 100644 --- a/packages/components-angular/projects/consumer-app/cypress/support/e2e.ts +++ b/packages/components-angular/projects/consumer-app/cypress/support/e2e.ts @@ -15,4 +15,3 @@ // When a command from ./commands is ready to use, import with `import './commands'` syntax import './commands'; -import 'cypress-axe'; diff --git a/packages/nextjs-integration/package.json b/packages/nextjs-integration/package.json index 219a2515c1..043f4d44fe 100644 --- a/packages/nextjs-integration/package.json +++ b/packages/nextjs-integration/package.json @@ -32,7 +32,6 @@ "react-dom": "19.1.1" }, "devDependencies": { - "@axe-core/playwright": "4.10.2", "@eslint/js": "9.18.0", "@next/eslint-plugin-next": "15.1.5", "@playwright/test": "1.55.0", From 8e1d8adfcfca0c2639bc258c29c986f8b62c2e6c Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 15 Oct 2025 10:16:17 +0200 Subject: [PATCH 058/127] removed redundant tests --- .../playwright/tests/post-tabs.spec.ts | 12 ------------ packages/nextjs-integration/src/app/ssr/page.tsx | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/nextjs-integration/playwright/tests/post-tabs.spec.ts b/packages/nextjs-integration/playwright/tests/post-tabs.spec.ts index e8b8c83a88..3e28734fdd 100644 --- a/packages/nextjs-integration/playwright/tests/post-tabs.spec.ts +++ b/packages/nextjs-integration/playwright/tests/post-tabs.spec.ts @@ -1,6 +1,5 @@ import { test, expect, Locator } from '@playwright/test'; import { PostTabs } from '@swisspost/design-system-components/dist/components/react/post-tabs.js'; -import AxeBuilder from '@axe-core/playwright'; test.describe('tabs', () => { let tabs: Locator; @@ -127,15 +126,4 @@ test.describe('tabs', () => { await expect(tabItems.nth(2)).toHaveClass(/active/); await expect(tabItems.first()).not.toHaveClass(/active/); }); - - test('should not have any automatically detectable accessibility issues', async ({ page }) => { - await expect(tabs).toBeVisible(); - await expect(tabItems.first()).toBeVisible(); - - const accessibilityScanResults = await new AxeBuilder({ page }) - .include('post-tabs[data-hydrated]') - .analyze(); - - expect(accessibilityScanResults.violations).toEqual([]); - }); }); diff --git a/packages/nextjs-integration/src/app/ssr/page.tsx b/packages/nextjs-integration/src/app/ssr/page.tsx index 86c782fc46..5d12fb8f87 100644 --- a/packages/nextjs-integration/src/app/ssr/page.tsx +++ b/packages/nextjs-integration/src/app/ssr/page.tsx @@ -151,7 +151,7 @@ export default function Home() {

Rating

-

Tabs

+

Tabs - Panel Variant

Unua langeto Dua langeto @@ -168,6 +168,19 @@ export default function Home() { +

Tabs - Navigation Variant

+ + + First + + + Second + + + Third + + +

Tag

Tooltip

From a615a95b833a0e91c8273936505a56a8b690e7d2 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 15 Oct 2025 10:57:58 +0200 Subject: [PATCH 059/127] fixed the tests --- .../consumer-app/cypress/e2e/tabs.cy.ts | 43 +++-- .../playwright/tests/post-tabs.spec.ts | 148 ++++-------------- 2 files changed, 66 insertions(+), 125 deletions(-) diff --git a/packages/components-angular/projects/consumer-app/cypress/e2e/tabs.cy.ts b/packages/components-angular/projects/consumer-app/cypress/e2e/tabs.cy.ts index b1fc78f062..8d19bef7ca 100644 --- a/packages/components-angular/projects/consumer-app/cypress/e2e/tabs.cy.ts +++ b/packages/components-angular/projects/consumer-app/cypress/e2e/tabs.cy.ts @@ -1,16 +1,39 @@ describe('Tabs', () => { beforeEach(() => { - cy.visit('/tabs', { - onBeforeLoad(win) { - cy.spy(win.console, 'error').as('consoleError'); - } + cy.visit('/tabs'); + }); + + describe('Panel mode', () => { + it('should render tabs component', () => { + cy.get('post-tabs').first().should('be.visible'); + }); + + it('should not have console errors', () => { + cy.visit('/tabs', { + onBeforeLoad(win) { + cy.spy(win.console, 'error').as('consoleError'); + } + }); + + cy.get('post-tabs').first().should('be.visible'); + cy.get('@consoleError').should('not.be.called'); }); }); - it('should render tabs components without errors', () => { - cy.get('post-tabs').first().should('be.visible'); - cy.get('post-tabs').last().should('be.visible'); - - cy.get('@consoleError').should('not.be.called'); + describe('Navigation mode', () => { + it('should render tabs component', () => { + cy.get('post-tabs').last().should('be.visible'); + }); + + it('should not have console errors', () => { + cy.visit('/tabs', { + onBeforeLoad(win) { + cy.spy(win.console, 'error').as('consoleError'); + } + }); + + cy.get('post-tabs').last().should('be.visible'); + cy.get('@consoleError').should('not.be.called'); + }); }); -}); \ No newline at end of file +}); diff --git a/packages/nextjs-integration/playwright/tests/post-tabs.spec.ts b/packages/nextjs-integration/playwright/tests/post-tabs.spec.ts index 3e28734fdd..33fd7da05e 100644 --- a/packages/nextjs-integration/playwright/tests/post-tabs.spec.ts +++ b/packages/nextjs-integration/playwright/tests/post-tabs.spec.ts @@ -1,129 +1,47 @@ -import { test, expect, Locator } from '@playwright/test'; -import { PostTabs } from '@swisspost/design-system-components/dist/components/react/post-tabs.js'; - -test.describe('tabs', () => { - let tabs: Locator; - let tabItems: Locator; +import { test, expect } from '@playwright/test'; +test.describe('Tabs', () => { test.beforeEach(async ({ page }) => { await page.goto('/ssr'); - - tabs = page.locator('post-tabs[data-hydrated]'); - tabItems = tabs.locator('post-tab-item'); - }); - - test('should render the tabs component', async () => { - await expect(tabs).toHaveCount(1); - }); - - test('should show three tab headers', async () => { - await expect(tabItems).toHaveCount(3); - }); - - test('should only show the first tab header as active', async () => { - await expect(tabItems.first()).toHaveClass(/active/); - await expect(tabItems.nth(1)).not.toHaveClass(/active/); - await expect(tabItems.nth(2)).not.toHaveClass(/active/); - }); - - test('should only show the tab panel associated with the first tab header', async ({ page }) => { - const visiblePanels = page.locator('post-tab-panel:visible'); - await expect(visiblePanels).toHaveCount(1); - - const firstTabName = await tabItems.first().getAttribute('name'); - const visiblePanelFor = await visiblePanels.first().getAttribute('for'); - expect(visiblePanelFor).toBe(firstTabName); - }); - - test('should activate a clicked tab header and deactivate the tab header that was previously activated', async () => { - await tabItems.last().click(); - - await expect(tabItems.first()).not.toHaveClass(/active/); - await expect(tabItems.last()).toHaveClass(/active/); - }); - - test('should show the panel associated with a clicked tab header', async ({ page }) => { - const lastTabName = await tabItems.last().getAttribute('name'); - - await tabItems.last().click(); - - // Wait for the correct panel to become visible - const expectedPanel = page.locator(`post-tab-panel[for="${lastTabName}"]:visible`); - await expect(expectedPanel).toBeVisible(); - - // Verify only one panel is visible - const visiblePanels = page.locator('post-tab-panel:visible'); - await expect(visiblePanels).toHaveCount(1); - }); - - test('should have proper ARIA attributes', async () => { - const tablist = tabs.locator('[role="tablist"]'); - await expect(tablist).toHaveCount(1); - - await expect(tabItems.first()).toHaveAttribute('role', 'tab'); - await expect(tabItems.first()).toHaveAttribute('aria-selected', 'true'); - - await expect(tabItems.nth(1)).toHaveAttribute('aria-selected', 'false'); - await expect(tabItems.nth(2)).toHaveAttribute('aria-selected', 'false'); - }); - - test('should link tabs to panels with aria-controls and aria-labelledby', async ({ page }) => { - const firstTab = tabItems.first(); - const tabId = await firstTab.getAttribute('id'); - const ariaControls = await firstTab.getAttribute('aria-controls'); - - expect(tabId).toBeTruthy(); - expect(ariaControls).toBeTruthy(); - - const associatedPanel = page.locator(`post-tab-panel[id="${ariaControls}"]`); - await expect(associatedPanel).toHaveAttribute('aria-labelledby', tabId!); }); - test('should manage tabindex properly', async () => { - await expect(tabItems.first()).toHaveAttribute('tabindex', '0'); - await expect(tabItems.nth(1)).toHaveAttribute('tabindex', '-1'); - await expect(tabItems.nth(2)).toHaveAttribute('tabindex', '-1'); + test.describe('Panel mode', () => { + test('should render tabs component', async ({ page }) => { + await expect(page.locator('post-tabs').first()).toBeVisible(); + }); - await tabItems.last().click(); - - await expect(tabItems.last()).toHaveAttribute('tabindex', '0'); - await expect(tabItems.first()).toHaveAttribute('tabindex', '-1'); - await expect(tabItems.nth(1)).toHaveAttribute('tabindex', '-1'); - }); - - test('should support programmatic tab activation via show() method', async ({ page }) => { - const tabsEl = await tabs.elementHandle(); - - if (tabsEl) { - const secondTabName = await tabItems.nth(1).getAttribute('name'); + test('should not have console errors', async ({ page }) => { + const consoleErrors: string[] = []; - await tabsEl.evaluate( - (el, tabName) => { - (el as PostTabs).show(tabName as string); - }, - secondTabName, - ); + page.on('console', (msg) => { + if (msg.type() === 'error') { + consoleErrors.push(msg.text()); + } + }); - await expect(tabItems.nth(1)).toHaveClass(/active/); - await expect(tabItems.nth(1)).toHaveAttribute('aria-selected', 'true'); - } + await page.locator('post-tabs').first().waitFor(); + + expect(consoleErrors).toHaveLength(0); + }); }); - test('should activate tab on Enter key press', async () => { - await expect(tabItems.nth(1)).toBeVisible(); - - await tabItems.nth(1).focus(); - await tabItems.nth(1).press('Enter'); + test.describe('Navigation mode', () => { + test('should render tabs component', async ({ page }) => { + await expect(page.locator('post-tabs').last()).toBeVisible(); + }); - await expect(tabItems.nth(1)).toHaveClass(/active/); - await expect(tabItems.first()).not.toHaveClass(/active/); - }); - - test('should activate tab on Space key press', async () => { - await tabItems.nth(2).focus(); - await tabItems.nth(2).press(' '); + test('should not have console errors', async ({ page }) => { + const consoleErrors: string[] = []; + + page.on('console', (msg) => { + if (msg.type() === 'error') { + consoleErrors.push(msg.text()); + } + }); - await expect(tabItems.nth(2)).toHaveClass(/active/); - await expect(tabItems.first()).not.toHaveClass(/active/); + await page.locator('post-tabs').last().waitFor(); + + expect(consoleErrors).toHaveLength(0); + }); }); }); From d18793a02c9134771d0ea5dd40c612bc13896f34 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 15 Oct 2025 11:05:26 +0200 Subject: [PATCH 060/127] reverted pnpm-lock.yaml --- pnpm-lock.yaml | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53be9a74e2..82bed3866a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -249,9 +249,6 @@ importers: cypress: specifier: 14.3.2 version: 14.3.2 - cypress-axe: - specifier: 1.5.0 - version: 1.5.0(axe-core@4.10.3)(cypress@14.3.2) eslint: specifier: 9.18.0 version: 9.18.0(jiti@2.4.2) @@ -789,9 +786,6 @@ importers: specifier: 19.1.1 version: 19.1.1(react@19.1.1) devDependencies: - '@axe-core/playwright': - specifier: 4.10.2 - version: 4.10.2(playwright-core@1.55.0) '@eslint/js': specifier: 9.18.0 version: 9.18.0 @@ -1563,11 +1557,6 @@ packages: '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} - '@axe-core/playwright@4.10.2': - resolution: {integrity: sha512-6/b5BJjG6hDaRNtgzLIfKr5DfwyiLHO4+ByTLB0cJgWSM8Ll7KqtdblIS6bEkwSF642/Ex91vNqIl3GLXGlceg==} - peerDependencies: - playwright-core: '>= 1.0.0' - '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -6724,13 +6713,6 @@ packages: custom-event@1.0.1: resolution: {integrity: sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==} - cypress-axe@1.5.0: - resolution: {integrity: sha512-Hy/owCjfj+25KMsecvDgo4fC/781ccL+e8p+UUYoadGVM2ogZF9XIKbiM6KI8Y3cEaSreymdD6ZzccbI2bY0lQ==} - engines: {node: '>=10'} - peerDependencies: - axe-core: ^3 || ^4 - cypress: ^10 || ^11 || ^12 || ^13 - cypress-axe@1.6.0: resolution: {integrity: sha512-C/ij50G8eebBrl/WsGT7E+T/SFyIsRZ3Epx9cRTLrPL9Y1GcxlQGFoAVdtSFWRrHSCWXq9HC6iJQMaI89O9yvQ==} engines: {node: '>=10'} @@ -13824,11 +13806,6 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 - '@axe-core/playwright@4.10.2(playwright-core@1.55.0)': - dependencies: - axe-core: 4.10.3 - playwright-core: 1.55.0 - '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -19501,11 +19478,6 @@ snapshots: custom-event@1.0.1: {} - cypress-axe@1.5.0(axe-core@4.10.3)(cypress@14.3.2): - dependencies: - axe-core: 4.10.3 - cypress: 14.3.2 - cypress-axe@1.6.0(axe-core@4.10.3)(cypress@14.3.2): dependencies: axe-core: 4.10.3 From a454af97cf3e42aa7d605abf74e1b684c82a4a07 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 15 Oct 2025 11:08:50 +0200 Subject: [PATCH 061/127] reverted pnpm-lock.yaml --- pnpm-lock.yaml | 60 +++++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82bed3866a..1fe0db096e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,10 +65,10 @@ importers: specifier: 0.6.0 version: 0.6.0 '@swisspost/design-system-icons': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../icons '@swisspost/design-system-styles': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../styles/dist ally.js: specifier: 1.4.1 @@ -108,7 +108,7 @@ importers: specifier: 3.0.12 version: 3.0.12(@stencil/core@4.35.0) '@swisspost/design-system-eslint': - specifier: workspace:1.0.1-next.1 + specifier: workspace:1.1.0-next.2 version: link:../eslint '@types/jest': specifier: 29.5.14 @@ -130,7 +130,7 @@ importers: version: 14.3.2 cypress-axe: specifier: 1.6.0 - version: 1.6.0(axe-core@4.10.3)(cypress@14.3.2) + version: 1.6.0(axe-core@4.7.0)(cypress@14.3.2) cypress-storybook: specifier: 1.0.0 version: 1.0.0(cypress@14.3.2) @@ -207,10 +207,10 @@ importers: specifier: 20.1.6 version: 20.1.6(@angular/common@20.1.6(@angular/core@20.1.6(@angular/compiler@20.1.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.6(@angular/compiler@20.1.6)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.6(@angular/animations@20.1.6(@angular/common@20.1.6(@angular/core@20.1.6(@angular/compiler@20.1.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.6(@angular/compiler@20.1.6)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.1.6(@angular/core@20.1.6(@angular/compiler@20.1.6)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.1.6(@angular/compiler@20.1.6)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) '@swisspost/design-system-components': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../components '@swisspost/design-system-styles': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../styles/dist rxjs: specifier: 7.8.2 @@ -238,7 +238,7 @@ importers: specifier: 9.18.0 version: 9.18.0 '@swisspost/design-system-components-angular': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:dist/components angular-eslint: specifier: 19.1.0 @@ -295,7 +295,7 @@ importers: specifier: '>=19.0.0 <21.0.0' version: 20.1.6(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.1.6(@angular/animations@20.1.6(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@19.2.0(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@19.2.0(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) '@swisspost/design-system-components': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../../../components tslib: specifier: 2.8.1 @@ -311,7 +311,7 @@ importers: specifier: 0.1.1 version: 0.1.1(next@15.3.3(@playwright/test@1.55.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.89.2))(vite@7.0.6(@types/node@22.10.5)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.89.2)(sass@1.89.2)(terser@5.43.1)(yaml@2.7.0))(webpack@5.99.9) '@swisspost/design-system-components': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../components devDependencies: '@eslint/js': @@ -357,22 +357,22 @@ importers: packages/documentation: dependencies: '@swisspost/design-system-components': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../components '@swisspost/design-system-components-react': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../components-react '@swisspost/design-system-icons': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../icons '@swisspost/design-system-styles': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../styles/dist '@swisspost/design-system-tokens': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../tokens/dist '@swisspost/internet-header': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../internet-header devDependencies: '@eslint/js': @@ -403,7 +403,7 @@ importers: specifier: 9.0.18 version: 9.0.18(lit@3.3.1)(storybook@9.0.18(@testing-library/dom@10.4.0)(prettier@2.8.8))(vite@6.3.6(@types/node@22.10.5)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass-embedded@1.78.0)(sass@1.89.2)(terser@5.43.1)(yaml@2.7.0)) '@swisspost/design-system-components-angular': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../components-angular/dist/components '@types/css-modules': specifier: 1.0.5 @@ -428,7 +428,7 @@ importers: version: 14.3.2 cypress-axe: specifier: 1.6.0 - version: 1.6.0(axe-core@4.10.3)(cypress@14.3.2) + version: 1.6.0(axe-core@4.7.0)(cypress@14.3.2) eslint: specifier: 9.18.0 version: 9.18.0(jiti@2.4.2) @@ -635,7 +635,7 @@ importers: packages/internet-header: dependencies: '@swisspost/design-system-styles': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../styles/dist body-scroll-lock: specifier: 4.0.0-beta.0 @@ -771,10 +771,10 @@ importers: packages/nextjs-integration: dependencies: '@swisspost/design-system-components-react': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../components-react '@swisspost/design-system-styles': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../styles/dist next: specifier: 15.3.3 @@ -796,7 +796,7 @@ importers: specifier: 1.55.0 version: 1.55.0 '@swisspost/design-system-components': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../components '@types/node': specifier: 22.10.5 @@ -847,14 +847,14 @@ importers: specifier: 2.11.8 version: 2.11.8 '@swisspost/design-system-icons': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../icons gulp-sourcemaps: specifier: 3.0.0 version: 3.0.0 devDependencies: '@swisspost/design-system-tokens': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../tokens/dist '@types/node': specifier: 22.10.5 @@ -948,7 +948,7 @@ importers: specifier: 19.0.9 version: 19.0.9 '@swisspost/design-system-styles': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../styles/dist primeng: specifier: 19.0.9 @@ -1036,7 +1036,7 @@ importers: packages/styles-primeng-workspace/projects/styles-primeng: dependencies: '@swisspost/design-system-styles': - specifier: workspace:10.0.0-next.48 + specifier: workspace:10.0.0-next.49 version: link:../../../styles/dist primeng: specifier: ^19.0.0 @@ -5964,8 +5964,8 @@ packages: aws4@1.13.2: resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} - axe-core@4.10.3: - resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} + axe-core@4.7.0: + resolution: {integrity: sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==} engines: {node: '>=4'} axios@1.11.0: @@ -18549,7 +18549,7 @@ snapshots: aws4@1.13.2: {} - axe-core@4.10.3: {} + axe-core@4.7.0: {} axios@1.11.0(debug@4.4.1): dependencies: @@ -19478,9 +19478,9 @@ snapshots: custom-event@1.0.1: {} - cypress-axe@1.6.0(axe-core@4.10.3)(cypress@14.3.2): + cypress-axe@1.6.0(axe-core@4.7.0)(cypress@14.3.2): dependencies: - axe-core: 4.10.3 + axe-core: 4.7.0 cypress: 14.3.2 cypress-each@1.14.0: {} From 20711db3e7d4827b0f6e87928e7fa7d4e6eb9eae Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 15 Oct 2025 11:36:24 +0200 Subject: [PATCH 062/127] fix --- packages/components/cypress/e2e/tabs.cy.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/components/cypress/e2e/tabs.cy.ts b/packages/components/cypress/e2e/tabs.cy.ts index 8247f2b4e7..e9ce1f0e28 100644 --- a/packages/components/cypress/e2e/tabs.cy.ts +++ b/packages/components/cypress/e2e/tabs.cy.ts @@ -227,6 +227,12 @@ describe('tabs', () => { describe('Accessibility', () => { it('Has no detectable a11y violations on load for all variants', () => { cy.getSnapshots('tabs'); + + cy.get('post-tabs').should('be.visible'); + cy.get('post-tab-item').should('exist'); + + cy.wait(100); + cy.checkA11y('#root-inner'); }); From e6369f55ba97a0ed66943ca1b37310f9f7d135b2 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 15 Oct 2025 11:46:00 +0200 Subject: [PATCH 063/127] fix --- packages/components/cypress/e2e/tabs.cy.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/components/cypress/e2e/tabs.cy.ts b/packages/components/cypress/e2e/tabs.cy.ts index e9ce1f0e28..298e328816 100644 --- a/packages/components/cypress/e2e/tabs.cy.ts +++ b/packages/components/cypress/e2e/tabs.cy.ts @@ -230,9 +230,7 @@ describe('Accessibility', () => { cy.get('post-tabs').should('be.visible'); cy.get('post-tab-item').should('exist'); - - cy.wait(100); - + cy.checkA11y('#root-inner'); }); From 4fd4a3d0f48ab077c3839bf8fddec54ff0363750 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 15 Oct 2025 14:14:28 +0200 Subject: [PATCH 064/127] test fix --- packages/components/cypress/e2e/tabs.cy.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/components/cypress/e2e/tabs.cy.ts b/packages/components/cypress/e2e/tabs.cy.ts index 298e328816..e899ad04ba 100644 --- a/packages/components/cypress/e2e/tabs.cy.ts +++ b/packages/components/cypress/e2e/tabs.cy.ts @@ -227,6 +227,8 @@ describe('tabs', () => { describe('Accessibility', () => { it('Has no detectable a11y violations on load for all variants', () => { cy.getSnapshots('tabs'); + + cy.wait(200); cy.get('post-tabs').should('be.visible'); cy.get('post-tab-item').should('exist'); From 098e7e729ec24bd0301a58d99d52f9ef77f34a84 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Fri, 17 Oct 2025 12:07:05 +0200 Subject: [PATCH 065/127] removed e2e files --- .../consumer-app/cypress/e2e/tabs.cy.ts | 39 --------------- .../playwright/tests/post-tabs.spec.ts | 47 ------------------- 2 files changed, 86 deletions(-) delete mode 100644 packages/components-angular/projects/consumer-app/cypress/e2e/tabs.cy.ts delete mode 100644 packages/nextjs-integration/playwright/tests/post-tabs.spec.ts diff --git a/packages/components-angular/projects/consumer-app/cypress/e2e/tabs.cy.ts b/packages/components-angular/projects/consumer-app/cypress/e2e/tabs.cy.ts deleted file mode 100644 index 8d19bef7ca..0000000000 --- a/packages/components-angular/projects/consumer-app/cypress/e2e/tabs.cy.ts +++ /dev/null @@ -1,39 +0,0 @@ -describe('Tabs', () => { - beforeEach(() => { - cy.visit('/tabs'); - }); - - describe('Panel mode', () => { - it('should render tabs component', () => { - cy.get('post-tabs').first().should('be.visible'); - }); - - it('should not have console errors', () => { - cy.visit('/tabs', { - onBeforeLoad(win) { - cy.spy(win.console, 'error').as('consoleError'); - } - }); - - cy.get('post-tabs').first().should('be.visible'); - cy.get('@consoleError').should('not.be.called'); - }); - }); - - describe('Navigation mode', () => { - it('should render tabs component', () => { - cy.get('post-tabs').last().should('be.visible'); - }); - - it('should not have console errors', () => { - cy.visit('/tabs', { - onBeforeLoad(win) { - cy.spy(win.console, 'error').as('consoleError'); - } - }); - - cy.get('post-tabs').last().should('be.visible'); - cy.get('@consoleError').should('not.be.called'); - }); - }); -}); diff --git a/packages/nextjs-integration/playwright/tests/post-tabs.spec.ts b/packages/nextjs-integration/playwright/tests/post-tabs.spec.ts deleted file mode 100644 index 33fd7da05e..0000000000 --- a/packages/nextjs-integration/playwright/tests/post-tabs.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Tabs', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/ssr'); - }); - - test.describe('Panel mode', () => { - test('should render tabs component', async ({ page }) => { - await expect(page.locator('post-tabs').first()).toBeVisible(); - }); - - test('should not have console errors', async ({ page }) => { - const consoleErrors: string[] = []; - - page.on('console', (msg) => { - if (msg.type() === 'error') { - consoleErrors.push(msg.text()); - } - }); - - await page.locator('post-tabs').first().waitFor(); - - expect(consoleErrors).toHaveLength(0); - }); - }); - - test.describe('Navigation mode', () => { - test('should render tabs component', async ({ page }) => { - await expect(page.locator('post-tabs').last()).toBeVisible(); - }); - - test('should not have console errors', async ({ page }) => { - const consoleErrors: string[] = []; - - page.on('console', (msg) => { - if (msg.type() === 'error') { - consoleErrors.push(msg.text()); - } - }); - - await page.locator('post-tabs').last().waitFor(); - - expect(consoleErrors).toHaveLength(0); - }); - }); -}); From 109e9d060522a5b60c208b9c6aa3daf99e9027f1 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 22 Oct 2025 12:51:38 +0200 Subject: [PATCH 066/127] clean up --- .../src/app/routes/home/home.component.html | 32 +++++++++++++++++++ .../src/app/routes/home/home.component.ts | 6 ++++ .../src/app/routes/tabs/tabs.component.html | 28 ---------------- .../src/app/routes/tabs/tabs.component.ts | 12 ------- 4 files changed, 38 insertions(+), 40 deletions(-) delete mode 100644 packages/components-angular/projects/consumer-app/src/app/routes/tabs/tabs.component.html delete mode 100644 packages/components-angular/projects/consumer-app/src/app/routes/tabs/tabs.component.ts diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html index 146274f400..2519a20c1c 100644 --- a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html +++ b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html @@ -79,6 +79,38 @@

Post Rating

+
+

Post Tabs - Panel Variant

+ + Unua langeto + Dua langeto + Tria langeto + + + Jen la enhavo de la unua langeto. Defaŭlte ĝi montriĝas komence. + + + Jen la enhavo de la dua langeto. Defaŭlte ĝi estas kaŝita komence. + + + Jen la enhavo de la tria langeto. Defaŭlte ĝi ankaŭ estas kaŝita komence. + + + +

Post Tabs - Navigation Variant

+ + + First + + + Second + + + Third + + +
+

Post Tooltip

diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts index fabd8fffa9..c1a4ebdcea 100644 --- a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts +++ b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts @@ -12,6 +12,9 @@ import { PostPopover, PostPopovercontainer, PostRating, + PostTabItem, + PostTabPanel, + PostTabs, PostTooltipTrigger, } from 'components'; @@ -32,6 +35,9 @@ import { PostPopover, PostPopovercontainer, PostRating, + PostTabItem, + PostTabPanel, + PostTabs, PostTooltipTrigger, ] }) diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/tabs/tabs.component.html b/packages/components-angular/projects/consumer-app/src/app/routes/tabs/tabs.component.html deleted file mode 100644 index 2d8b478d2d..0000000000 --- a/packages/components-angular/projects/consumer-app/src/app/routes/tabs/tabs.component.html +++ /dev/null @@ -1,28 +0,0 @@ -

Tabs - Panel Variant

- - First - Second - Third - -

Content of first tab

-
- -

Content of second tab

-
- -

Content of third tab

-
-
- -

Tabs - Navigation Variant

- - - First - - - Second - - - Third - - diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/tabs/tabs.component.ts b/packages/components-angular/projects/consumer-app/src/app/routes/tabs/tabs.component.ts deleted file mode 100644 index 26cc1886be..0000000000 --- a/packages/components-angular/projects/consumer-app/src/app/routes/tabs/tabs.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { ReactiveFormsModule } from '@angular/forms'; -import { PostTabs, PostTabItem, PostTabPanel } from '@swisspost/design-system-components-angular'; - -@Component({ - selector: 'tabs-page', - templateUrl: './tabs.component.html', - imports: [CommonModule, ReactiveFormsModule, PostTabs, PostTabItem, PostTabPanel], - standalone: true, -}) -export class TabsComponent {} From e16d55caf777ea1ae4982ac2497bf3608b3c5633 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 22 Oct 2025 12:52:35 +0200 Subject: [PATCH 067/127] fixed the order --- .../consumer-app/src/app/routes/home/home.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts index c1a4ebdcea..344df05906 100644 --- a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts +++ b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.ts @@ -12,9 +12,9 @@ import { PostPopover, PostPopovercontainer, PostRating, + PostTabs, PostTabItem, PostTabPanel, - PostTabs, PostTooltipTrigger, } from 'components'; @@ -35,9 +35,9 @@ import { PostPopover, PostPopovercontainer, PostRating, + PostTabs, PostTabItem, PostTabPanel, - PostTabs, PostTooltipTrigger, ] }) From 4709cb2a4dc80155ed334d3ba005b18ce3e9465a Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 22 Oct 2025 12:53:59 +0200 Subject: [PATCH 068/127] cleanup --- .../projects/consumer-app/src/app/app-routing.module.ts | 2 -- .../projects/consumer-app/src/app/app.module.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/components-angular/projects/consumer-app/src/app/app-routing.module.ts b/packages/components-angular/projects/consumer-app/src/app/app-routing.module.ts index f9a43d706d..5172e3b67c 100644 --- a/packages/components-angular/projects/consumer-app/src/app/app-routing.module.ts +++ b/packages/components-angular/projects/consumer-app/src/app/app-routing.module.ts @@ -2,13 +2,11 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HomeComponent } from './routes/home/home.component'; import { CardControlComponent } from './routes/card-control/card-control.component'; -import { TabsComponent } from './routes/tabs/tabs.component'; const routes: Routes = [ { path: '', redirectTo: 'home', pathMatch: 'full' }, { title: 'Home', path: 'home', component: HomeComponent }, { title: 'Card-Control', path: 'card-control', component: CardControlComponent }, - { title: 'Tabs', path: 'tabs', component: TabsComponent }, ]; @NgModule({ diff --git a/packages/components-angular/projects/consumer-app/src/app/app.module.ts b/packages/components-angular/projects/consumer-app/src/app/app.module.ts index 5927731fce..0fe07a924d 100644 --- a/packages/components-angular/projects/consumer-app/src/app/app.module.ts +++ b/packages/components-angular/projects/consumer-app/src/app/app.module.ts @@ -7,7 +7,6 @@ import { providePostComponents } from '@swisspost/design-system-components-angul import { AppComponent } from './app.component'; import { CardControlComponent } from './routes/card-control/card-control.component'; -import { TabsComponent } from './routes/tabs/tabs.component'; @NgModule({ imports: [ @@ -16,7 +15,6 @@ import { TabsComponent } from './routes/tabs/tabs.component'; AppRoutingModule, FormsModule, CardControlComponent, - TabsComponent, ], declarations: [AppComponent], providers: [providePostComponents()], From 86173c410be48827c360480dc772b31871051ef1 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 22 Oct 2025 16:10:13 +0200 Subject: [PATCH 069/127] fixed the markup --- .../src/app/routes/home/home.component.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html index 2519a20c1c..b928eb190f 100644 --- a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html +++ b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html @@ -82,17 +82,17 @@

Post Rating

Post Tabs - Panel Variant

- Unua langeto - Dua langeto - Tria langeto + Unua langeto + Dua langeto + Tria langeto - + Jen la enhavo de la unua langeto. Defaŭlte ĝi montriĝas komence. - + Jen la enhavo de la dua langeto. Defaŭlte ĝi estas kaŝita komence. - + Jen la enhavo de la tria langeto. Defaŭlte ĝi ankaŭ estas kaŝita komence. From 847040079bb574bc0cfe960a2769f67df0c9015c Mon Sep 17 00:00:00 2001 From: Alona Zherdetska <138328641+alionazherdetska@users.noreply.github.com> Date: Mon, 27 Oct 2025 18:28:13 +0100 Subject: [PATCH 070/127] Update packages/components/src/components/post-tab-item/post-tab-item.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alizé Debray <33580481+alizedebray@users.noreply.github.com> --- .../components/src/components/post-tab-item/post-tab-item.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/components/post-tab-item/post-tab-item.tsx b/packages/components/src/components/post-tab-item/post-tab-item.tsx index 8df7a683ed..e7be3deebe 100644 --- a/packages/components/src/components/post-tab-item/post-tab-item.tsx +++ b/packages/components/src/components/post-tab-item/post-tab-item.tsx @@ -19,7 +19,7 @@ export class PostTabItem { @State() isNavigationMode = false; /** - * The name of the tab, used to associate it with a tab panel or identify the active tab in navigation mode. + * The name of the tab, used to associate it with a tab panel or identify the active tab in panel mode. */ @Prop({ reflect: true }) readonly name!: string; From 9f47c5521e84fcf96f72e7463f12c00a6c575225 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 28 Oct 2025 13:39:54 +0100 Subject: [PATCH 071/127] added mutation observer to post-tab-item --- .../components/post-tab-item/post-tab-item.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/components/src/components/post-tab-item/post-tab-item.tsx b/packages/components/src/components/post-tab-item/post-tab-item.tsx index 8df7a683ed..6ed5a2fdb2 100644 --- a/packages/components/src/components/post-tab-item/post-tab-item.tsx +++ b/packages/components/src/components/post-tab-item/post-tab-item.tsx @@ -13,6 +13,8 @@ import { nanoid } from 'nanoid'; shadow: true, }) export class PostTabItem { + private mutationObserver = new MutationObserver(this.checkNavigationMode.bind(this)); + @Element() host: HTMLPostTabItemElement; @State() tabId: string; @@ -28,16 +30,25 @@ export class PostTabItem { checkRequiredAndType(this, 'name', 'string'); } + connectedCallback() { + this.mutationObserver.observe(this.host, { + childList: true, + subtree: true, + }); + } + componentWillLoad() { this.tabId = `tab-${this.host.id || nanoid(6)}`; - this.checkNavigationMode(); } componentDidLoad() { - // Re-check navigation mode after content is loaded this.checkNavigationMode(); } + disconnectedCallback() { + this.mutationObserver.disconnect(); + } + private checkNavigationMode() { const hasAnchor = this.host.querySelector('a') !== null; this.isNavigationMode = hasAnchor; From 965595d6c068aeddac70f96b0006095d35d3c6c3 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 28 Oct 2025 13:42:11 +0100 Subject: [PATCH 072/127] autogenerated files --- packages/components/src/components.d.ts | 4 ++-- packages/components/src/components/post-tab-item/readme.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 64145fd198..0a4d591ee8 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -463,7 +463,7 @@ export namespace Components { } interface PostTabItem { /** - * The name of the tab, used to associate it with a tab panel or identify the active tab in navigation mode. + * The name of the tab, used to associate it with a tab panel or identify the active tab in panel mode. */ "name": string; } @@ -1325,7 +1325,7 @@ declare namespace LocalJSX { } interface PostTabItem { /** - * The name of the tab, used to associate it with a tab panel or identify the active tab in navigation mode. + * The name of the tab, used to associate it with a tab panel or identify the active tab in panel mode. */ "name": string; } diff --git a/packages/components/src/components/post-tab-item/readme.md b/packages/components/src/components/post-tab-item/readme.md index 7b366c28ed..990b0c1ac3 100644 --- a/packages/components/src/components/post-tab-item/readme.md +++ b/packages/components/src/components/post-tab-item/readme.md @@ -7,9 +7,9 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ------------------- | --------- | --------------------------------------------------------------------------------------------------------- | -------- | ----------- | -| `name` _(required)_ | `name` | The name of the tab, used to associate it with a tab panel or identify the active tab in navigation mode. | `string` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ------------------- | --------- | ---------------------------------------------------------------------------------------------------- | -------- | ----------- | +| `name` _(required)_ | `name` | The name of the tab, used to associate it with a tab panel or identify the active tab in panel mode. | `string` | `undefined` | ## Slots From a49862c2ed5b2f2f62090aa02bc8a95bb10a1819 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Mon, 3 Nov 2025 16:00:33 +0100 Subject: [PATCH 073/127] fixed the docs --- .../src/stories/foundations/icons/icon.docs.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/documentation/src/stories/foundations/icons/icon.docs.mdx b/packages/documentation/src/stories/foundations/icons/icon.docs.mdx index 545ae400bd..2c1a676f57 100644 --- a/packages/documentation/src/stories/foundations/icons/icon.docs.mdx +++ b/packages/documentation/src/stories/foundations/icons/icon.docs.mdx @@ -20,14 +20,14 @@ import ComponentsPackageInstall from '@/shared/components-package-install.mdx'; The official Swiss Post Icon Library with {Math.floor(POST_ICONS_COUNT / 10) * 10}+ high-quality svg icons and the brand new UI Icon Set, exclusively designed and provided for the web with {Math.floor(UI_ICONS_COUNT / 10) * 10}+ icons and responsive level-of-detail.
- - Find your Icon - + + Find your Icon + - Installation - + Installation + ### Icon SVGs From e98957433c96933ba3d5c770fff4cbd0ef84d17e Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Mon, 3 Nov 2025 16:15:18 +0100 Subject: [PATCH 074/127] reverted redundant code --- .../documentation/src/stories/components/tabs/tabs.stories.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index 9c2db283b5..983ec7ff33 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -77,7 +77,6 @@ const meta: MetaComponent Date: Mon, 3 Nov 2025 17:14:40 +0100 Subject: [PATCH 075/127] removed redundant code --- .../stories/components/tabs/tabs.stories.ts | 87 +------------------ 1 file changed, 1 insertion(+), 86 deletions(-) diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index 983ec7ff33..d7f3be2670 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -221,30 +221,12 @@ export const ActiveTab: Story = { export const FullWidth: Story = { parameters: { layout: 'fullscreen', - docs: { - description: { - story: 'Full-width mode stretches the tabs container across the full screen width while keeping content aligned. Available in both modes.', - }, - }, - }, - args: { - variant: 'panels', - fullWidth: true }, + args: { fullWidth: true }, decorators: [story => html`
${story()}
`], }; export const Async: Story = { - parameters: { - docs: { - description: { - story: 'Tabs can be dynamically added or removed. This example shows panels mode with dynamic tab management.', - }, - }, - }, - args: { - variant: 'panels', - }, decorators: [ story => { let tabIndex = 0; @@ -299,70 +281,3 @@ export const Async: Story = { }, ], }; - -export const NavigationWithCurrent: Story = { - parameters: { - layout: 'fullscreen', - docs: { - description: { - story: 'Navigation mode with aria-current="page" for detecting active tab.', - }, - }, - }, - render: (args: Partial) => { - return html` - - - First page - - - Second page - - - Third page - - - `; - }, - args: { - variant: 'navigation', - }, -}; - -export const MixedMode: Story = { - parameters: { - layout: 'fullscreen', - docs: { - description: { - story: 'Mixed mode example that demonstrates error handling when both navigation and panel elements are present.', - }, - }, - }, - render: (args: Partial) => { - return html` - - - First page - - Second tab - Third tab - - -

This is the content of the second tab.

-
- -

This is the content of the third tab.

-
-
- `; - }, - args: { - variant: 'panels', - }, -}; \ No newline at end of file From acf912048d9c1819298be270c3f27931e20d0190 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Mon, 3 Nov 2025 17:16:17 +0100 Subject: [PATCH 076/127] reverted redundant changes --- .../documentation/src/stories/components/tabs/tabs.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index d7f3be2670..f748cef7c8 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -153,7 +153,7 @@ function renderTabs(args: PartialFirst tab Second tab Third tab - + This is the content of the first tab. By default it is shown initially. From 1891387316059129df1cf2b696cdb85895e70c3b Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Mon, 3 Nov 2025 17:27:25 +0100 Subject: [PATCH 077/127] fixed the docs --- .../src/stories/components/tabs/tabs.stories.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index f748cef7c8..a4f82da1a1 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -30,7 +30,9 @@ const meta: MetaComponent Date: Mon, 3 Nov 2025 17:32:14 +0100 Subject: [PATCH 078/127] fixed that tabs docs --- .../documentation/src/stories/components/tabs/tabs.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index a4f82da1a1..fc666d2a94 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -20,7 +20,7 @@ const meta: MetaComponent

If you attempt to mix both variants(anchors + panels), the component will throw an error.

', + description: 'Select between panels variant (content sections) or navigation variant (page navigation).

If you attempt to mix both variants(anchors + panels), the component will throw an error.

', control: 'radio', options: ['panels', 'navigation'], table: { From d752785bd1493957b9b6875a9522dfe00c9f7397 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Mon, 3 Nov 2025 17:47:58 +0100 Subject: [PATCH 079/127] fixed that tabs docs --- .../src/stories/components/tabs/tabs.stories.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index fc666d2a94..3f7721f196 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -20,7 +20,7 @@ const meta: MetaComponent

If you attempt to mix both variants(anchors + panels), the component will throw an error.

', + description: 'Select between panels variant (content sections) or navigation variant (page navigation).

If you attempt to mix both variants(anchors + panels), the component will throw an error.

', control: 'radio', options: ['panels', 'navigation'], table: { @@ -30,9 +30,8 @@ const meta: MetaComponent Date: Mon, 3 Nov 2025 17:50:02 +0100 Subject: [PATCH 080/127] fixed that tabs docs --- .../documentation/src/stories/components/tabs/tabs.stories.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index 3f7721f196..b562471932 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -36,7 +36,7 @@ const meta: MetaComponent Date: Mon, 3 Nov 2025 17:56:21 +0100 Subject: [PATCH 081/127] improved the docs --- packages/components/src/components.d.ts | 8 ++++++++ .../src/components/post-tabs/post-tabs.tsx | 19 ++++++++++++++++--- .../src/components/post-tabs/readme.md | 1 + 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 58c22c83e0..836dd2476e 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -499,6 +499,10 @@ export namespace Components { * @default false */ "fullWidth": boolean; + /** + * The accessible label for the tabs component for navigation variant. + */ + "label"?: string; /** * Shows the panel with the given name and selects its associated tab. In navigation mode, only updates the active tab state. Any other panel that was previously shown becomes hidden and its associated tab is unselected. */ @@ -1396,6 +1400,10 @@ declare namespace LocalJSX { * @default false */ "fullWidth"?: boolean; + /** + * The accessible label for the tabs component for navigation variant. + */ + "label"?: string; /** * An event emitted after the active tab changes, when the fade in transition of its associated panel is finished. The payload is the name of the newly active tab. */ diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 6805bd5ff4..1eb98c67a0 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -1,7 +1,7 @@ -import { Component, Element, Event, EventEmitter, h, Host, Method, Prop, State } from '@stencil/core'; +import { Component, Element, Event, EventEmitter, h, Host, Method, Prop, State, Watch } from '@stencil/core'; import { version } from '@root/package.json'; import { fadeIn, fadeOut } from '@/animations'; -import { componentOnReady } from '@/utils'; +import { componentOnReady, checkRequiredAndType } from '@/utils'; /** * @slot default - Slot for placing tab items. Each tab item should be a element. @@ -52,6 +52,18 @@ export class PostTabs { */ @Prop({ reflect: true }) fullWidth: boolean = false; + /** + * The accessible label for the tabs component for navigation variant. + */ + @Prop() readonly label?: string; + + @Watch('label') + validateLabel() { + if (this.isNavigationMode) { + checkRequiredAndType(this, 'label', 'string'); + } + } + /** * An event emitted after the active tab changes, when the fade in transition of its associated panel is finished. * The payload is the name of the newly active tab. @@ -64,6 +76,7 @@ export class PostTabs { this.isLoaded = true; this.enableTabs(); this.setupContentObserver(); + this.validateLabel(); if (this.isNavigationMode) { const activeTab = this.findActiveNavigationTab(); @@ -323,7 +336,7 @@ export class PostTabs { render() { const tabsRole = this.isNavigationMode ? undefined : 'tablist'; - const ariaLabel = this.isNavigationMode ? 'Tabs navigation' : undefined; + const ariaLabel = this.isNavigationMode ? this.label : undefined; const TabsContainer = this.isNavigationMode ? 'nav' : 'div'; return ( diff --git a/packages/components/src/components/post-tabs/readme.md b/packages/components/src/components/post-tabs/readme.md index e7d1da5e19..30f74226fa 100644 --- a/packages/components/src/components/post-tabs/readme.md +++ b/packages/components/src/components/post-tabs/readme.md @@ -11,6 +11,7 @@ | ----------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | | `activeTab` | `active-tab` | The name of the tab that is initially active. If not specified, it defaults to the first tab. **Changing this value after initialization has no effect.** | `string` | `undefined` | | `fullWidth` | `full-width` | When set to true, this property allows the tabs container to span the full width of the screen, from edge to edge. | `boolean` | `false` | +| `label` | `label` | The accessible label for the tabs component . Required when tabs are used for navigation. | `string` | `undefined` | ## Events From dae3f000150cc71a8b580198ca63a6803388669f Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Mon, 3 Nov 2025 17:56:31 +0100 Subject: [PATCH 082/127] improved the docs --- packages/components/src/components/post-tabs/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/components/post-tabs/readme.md b/packages/components/src/components/post-tabs/readme.md index 30f74226fa..5f6cb647e7 100644 --- a/packages/components/src/components/post-tabs/readme.md +++ b/packages/components/src/components/post-tabs/readme.md @@ -11,7 +11,7 @@ | ----------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | | `activeTab` | `active-tab` | The name of the tab that is initially active. If not specified, it defaults to the first tab. **Changing this value after initialization has no effect.** | `string` | `undefined` | | `fullWidth` | `full-width` | When set to true, this property allows the tabs container to span the full width of the screen, from edge to edge. | `boolean` | `false` | -| `label` | `label` | The accessible label for the tabs component . Required when tabs are used for navigation. | `string` | `undefined` | +| `label` | `label` | The accessible label for the tabs component for navigation variant. | `string` | `undefined` | ## Events From 6ca8015e399be9f11df531eaaf0b4856870a6ebd Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Mon, 3 Nov 2025 18:01:55 +0100 Subject: [PATCH 083/127] added the label --- .../stories/components/tabs/tabs.stories.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index b562471932..2451690425 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -47,6 +47,21 @@ const meta: MetaComponent ${unsafeHTML(args['slots-default'])}
@@ -104,6 +120,7 @@ function renderTabs(args: Partial First page @@ -202,6 +219,7 @@ export const NavigationVariant: Story = { }, args: { variant: 'navigation', + label: 'Page navigation', }, }; From cb1196f12175b1ba4f391ef9d8770ba857f05e02 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Mon, 3 Nov 2025 20:19:02 +0100 Subject: [PATCH 084/127] added aria-current --- packages/components/src/components.d.ts | 4 +-- .../post-tab-item/post-tab-item.tsx | 29 +++++++++++++++++-- .../src/components/post-tabs/post-tabs.tsx | 6 ++-- .../src/components/post-tabs/readme.md | 10 +++---- .../stories/components/tabs/tabs.stories.ts | 18 ++++++++---- 5 files changed, 50 insertions(+), 17 deletions(-) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 836dd2476e..80d0cd7e54 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -502,7 +502,7 @@ export namespace Components { /** * The accessible label for the tabs component for navigation variant. */ - "label"?: string; + "label": string; /** * Shows the panel with the given name and selects its associated tab. In navigation mode, only updates the active tab state. Any other panel that was previously shown becomes hidden and its associated tab is unselected. */ @@ -1403,7 +1403,7 @@ declare namespace LocalJSX { /** * The accessible label for the tabs component for navigation variant. */ - "label"?: string; + "label": string; /** * An event emitted after the active tab changes, when the fade in transition of its associated panel is finished. The payload is the name of the newly active tab. */ diff --git a/packages/components/src/components/post-tab-item/post-tab-item.tsx b/packages/components/src/components/post-tab-item/post-tab-item.tsx index 2572f4798e..4a7b3742dc 100644 --- a/packages/components/src/components/post-tab-item/post-tab-item.tsx +++ b/packages/components/src/components/post-tab-item/post-tab-item.tsx @@ -13,12 +13,13 @@ import { nanoid } from 'nanoid'; shadow: true, }) export class PostTabItem { - private mutationObserver = new MutationObserver(this.checkNavigationMode.bind(this)); + private mutationObserver = new MutationObserver(this.handleMutations.bind(this)); @Element() host: HTMLPostTabItemElement; @State() tabId: string; @State() isNavigationMode = false; + @State() hasAriaCurrent = false; /** * The name of the tab, used to associate it with a tab panel or identify the active tab in panel mode. @@ -34,6 +35,8 @@ export class PostTabItem { this.mutationObserver.observe(this.host, { childList: true, subtree: true, + attributes: true, + attributeFilter: ['aria-current'], }); } @@ -43,10 +46,18 @@ export class PostTabItem { componentDidLoad() { this.checkNavigationMode(); + this.checkAriaCurrent(); } disconnectedCallback() { - this.mutationObserver.disconnect(); + if (this.mutationObserver) { + this.mutationObserver.disconnect(); + } + } + + private handleMutations() { + this.checkNavigationMode(); + this.checkAriaCurrent(); } private checkNavigationMode() { @@ -54,8 +65,20 @@ export class PostTabItem { this.isNavigationMode = hasAnchor; } + private checkAriaCurrent() { + if (this.isNavigationMode) { + const anchor = this.host.querySelector('a'); + this.hasAriaCurrent = anchor?.getAttribute('aria-current') === 'page'; + } + } + render() { const isPanelMode = !this.isNavigationMode; + const classes = { + 'tab-title': true, + active: this.isNavigationMode && this.hasAriaCurrent, + }; + return ( diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 1eb98c67a0..662b057243 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -55,7 +55,7 @@ export class PostTabs { /** * The accessible label for the tabs component for navigation variant. */ - @Prop() readonly label?: string; + @Prop() readonly label!: string; @Watch('label') validateLabel() { @@ -100,7 +100,9 @@ export class PostTabs { } // Clean up content observer - this.contentObserver.disconnect(); + if (this.contentObserver) { + this.contentObserver.disconnect(); + } } private setupContentObserver() { diff --git a/packages/components/src/components/post-tabs/readme.md b/packages/components/src/components/post-tabs/readme.md index 5f6cb647e7..0e84c86d5b 100644 --- a/packages/components/src/components/post-tabs/readme.md +++ b/packages/components/src/components/post-tabs/readme.md @@ -7,11 +7,11 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ----------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | -| `activeTab` | `active-tab` | The name of the tab that is initially active. If not specified, it defaults to the first tab. **Changing this value after initialization has no effect.** | `string` | `undefined` | -| `fullWidth` | `full-width` | When set to true, this property allows the tabs container to span the full width of the screen, from edge to edge. | `boolean` | `false` | -| `label` | `label` | The accessible label for the tabs component for navigation variant. | `string` | `undefined` | +| Property | Attribute | Description | Type | Default | +| -------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | +| `activeTab` | `active-tab` | The name of the tab that is initially active. If not specified, it defaults to the first tab. **Changing this value after initialization has no effect.** | `string` | `undefined` | +| `fullWidth` | `full-width` | When set to true, this property allows the tabs container to span the full width of the screen, from edge to edge. | `boolean` | `false` | +| `label` _(required)_ | `label` | The accessible label for the tabs component for navigation variant. | `string` | `undefined` | ## Events diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index 2451690425..c0831b0c5e 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -1,5 +1,5 @@ import { StoryObj } from '@storybook/web-components-vite'; -import { html, nothing } from 'lit'; +import { html, nothing, TemplateResult } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { MetaComponent } from '@root/types'; @@ -16,11 +16,21 @@ const meta: MetaComponent TemplateResult) => html`${story()}`, + ], }, argTypes: { variant: { name: 'variant', - description: 'Select between panels variant (content sections) or navigation variant (page navigation).

If you attempt to mix both variants(anchors + panels), the component will throw an error.

', + description: 'Select between panels variant (content sections) or navigation variant (page navigation).

If you attempt (anchors + panels), the component will throw an error.

', control: 'radio', options: ['panels', 'navigation'], table: { @@ -57,9 +67,6 @@ const meta: MetaComponent Date: Wed, 5 Nov 2025 14:25:54 +0100 Subject: [PATCH 085/127] added missing labels for integration packages --- .../consumer-app/src/app/routes/home/home.component.html | 2 +- packages/nextjs-integration/src/app/ssr/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html index 662a839e23..9a5ae2f903 100644 --- a/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html +++ b/packages/components-angular/projects/consumer-app/src/app/routes/home/home.component.html @@ -101,7 +101,7 @@

Post Tabs - Panel Variant

Post Tabs - Navigation Variant

- + First diff --git a/packages/nextjs-integration/src/app/ssr/page.tsx b/packages/nextjs-integration/src/app/ssr/page.tsx index f05a184c1a..edc5381aec 100644 --- a/packages/nextjs-integration/src/app/ssr/page.tsx +++ b/packages/nextjs-integration/src/app/ssr/page.tsx @@ -164,7 +164,7 @@ export default function Home() {

Tabs - Navigation Variant

- + First From 231a3990107d1071f52d1b06a5d19038afc2104d Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 5 Nov 2025 15:42:06 +0100 Subject: [PATCH 086/127] refactored the e2e tests --- packages/components/cypress/e2e/tabs.cy.ts | 265 +++++++++++---------- 1 file changed, 138 insertions(+), 127 deletions(-) diff --git a/packages/components/cypress/e2e/tabs.cy.ts b/packages/components/cypress/e2e/tabs.cy.ts index e899ad04ba..c5831dc3e5 100644 --- a/packages/components/cypress/e2e/tabs.cy.ts +++ b/packages/components/cypress/e2e/tabs.cy.ts @@ -4,7 +4,7 @@ describe('tabs', () => { describe('default', () => { beforeEach(() => { cy.getComponent('tabs', TABS_ID); - cy.get('post-tab-item').as('tabItems'); + cy.get('post-tab-item').as('items'); }); it('should render', () => { @@ -12,19 +12,19 @@ describe('tabs', () => { }); it('should show three tab headers', () => { - cy.get('@tabItems').should('have.length', 3); + cy.get('@items').should('have.length', 3); }); it('should only show the first tab header as active', () => { - cy.get('post-tab-item.active').each(($header, index) => { - cy.wrap($header).should(index === 0 ? 'exist' : 'not.exist'); + cy.get('post-tab-item.active').each(($item, index) => { + cy.wrap($item).should(index === 0 ? 'exist' : 'not.exist'); }); }); it('should only show the tab panel associated with the first tab header', () => { cy.get('post-tab-panel:visible').as('panel'); cy.get('@panel').should('have.length', 1); - cy.get('@tabItems') + cy.get('@items') .first() .invoke('attr', 'name') .then(tabName => { @@ -33,31 +33,33 @@ describe('tabs', () => { }); it('should activate a clicked tab header and deactivate the tab header that was previously activated', () => { - cy.get('@tabItems').last().click(); + cy.get('@items').last().click(); - cy.get('@tabItems').first().should('not.have.class', 'active'); - cy.get('@tabItems').last().should('have.class', 'active'); + cy.get('@items').first().should('not.have.class', 'active'); + cy.get('@items').last().should('have.class', 'active'); }); it('should show the panel associated with a clicked tab header and hide the panel that was previously shown', () => { - cy.get('@tabItems').last().click(); + cy.get('@items').last().click(); - // Wait for transition to complete - cy.get('post-tab-panel:visible').should('have.length', 1); - - cy.get('@tabItems') + // wait for the fade out animation to complete + cy.wait(200); + + cy.get('post-tab-panel:visible').as('panel'); + cy.get('@panel').should('have.length', 1); + cy.get('@items') .last() .invoke('attr', 'name') .then(tabName => { - cy.get('post-tab-panel:visible').invoke('attr', 'for').should('equal', tabName); + cy.get('@panel').invoke('attr', 'for').should('equal', tabName); }); }); }); - describe('active panel', () => { + describe('active tab', () => { beforeEach(() => { cy.getComponent('tabs', TABS_ID, 'active-tab'); - cy.get('post-tab-item').as('tabItems'); + cy.get('post-tab-item').as('items'); cy.get('post-tab-panel:visible').as('panel'); }); @@ -70,15 +72,15 @@ describe('tabs', () => { }); }); - it('should show as active only the tab header associated with the requested active tab panel', () => { + it('should show as active only the tab item associated with the requested active tab', () => { cy.get('@tabs') .invoke('attr', 'active-tab') .then(activeTab => { - cy.get('@tabItems').each($header => { - cy.wrap($header) + cy.get('@items').each($item => { + cy.wrap($item) .invoke('attr', 'name') .then(tabName => { - cy.wrap($header.filter('.active')).should( + cy.wrap($item.filter('.active')).should( tabName === activeTab ? 'exist' : 'not.exist', ); }); @@ -90,12 +92,12 @@ describe('tabs', () => { describe('async', () => { beforeEach(() => { cy.getComponent('tabs', TABS_ID, 'async'); - cy.get('post-tab-item').as('tabItems'); + cy.get('post-tab-item').as('items'); }); it('should add a tab header', () => { cy.get('#add-tab').click(); - cy.get('@tabItems').should('have.length', 4); + cy.get('@items').should('have.length', 4); }); it('should still show the tab panel associated with the first tab header after adding new tab', () => { @@ -103,7 +105,7 @@ describe('tabs', () => { cy.get('post-tab-panel:visible').as('panel'); cy.get('@panel').should('have.length', 1); - cy.get('@tabItems') + cy.get('@items') .first() .invoke('attr', 'name') .then(tabName => { @@ -114,32 +116,35 @@ describe('tabs', () => { it('should activate the newly added tab header after clicking on it', () => { cy.get('#add-tab').click(); - cy.get('post-tab-item').as('tabItems'); - cy.get('@tabItems').last().click(); + cy.get('post-tab-item').as('items'); + cy.get('@items').last().click(); - cy.get('@tabItems').first().should('not.have.class', 'active'); - cy.get('@tabItems').last().should('have.class', 'active'); + cy.get('@items').first().should('not.have.class', 'active'); + cy.get('@items').last().should('have.class', 'active'); }); it('should display the tab panel associated with the newly added tab after clicking on it', () => { cy.get('#add-tab').click(); - cy.get('post-tab-item').last().as('new-tab'); - cy.get('@new-tab').click(); + cy.get('post-tab-item').last().as('new-item'); + cy.get('@new-item').click(); - cy.get('post-tab-panel:visible').should('have.length', 1); - - cy.get('@new-tab') + // wait for the fade out animation to complete + cy.wait(200); + + cy.get('post-tab-panel:visible').as('panel'); + cy.get('@panel').should('have.length', 1); + cy.get('@new-item') .invoke('attr', 'name') .then(tabName => { - cy.get('post-tab-panel:visible').invoke('attr', 'for').should('equal', tabName); + cy.get('@panel').invoke('attr', 'for').should('equal', tabName); }); }); it('should remove a tab header', () => { cy.get('.tab-title.active').then(() => { cy.get('#remove-active-tab').click(); - cy.get('@tabItems').should('have.length', 2); + cy.get('@items').should('have.length', 2); }); }); @@ -158,130 +163,136 @@ describe('tabs', () => { }); }); - describe('navigation mode', () => { + describe('navigation variant', () => { beforeEach(() => { - cy.getComponent('tabs', TABS_ID, 'navigation-variant'); - cy.get('post-tab-item').as('tabItems'); + cy.getComponent('tabs', TABS_ID, 'navigation'); + cy.get('post-tab-item').as('items'); }); - it('should render as navigation when tabs contain anchor elements', () => { + it('should render', () => { cy.get('@tabs').should('exist'); - cy.get('@tabItems').should('have.length', 3); - cy.get('@tabItems').each($item => { - cy.wrap($item).find('a').should('exist'); - }); }); - it('should not render tab panels in navigation mode', () => { - cy.get('post-tab-panel').should('not.exist'); - cy.get('@tabs').find('[part="content"]').should('not.exist'); + it('should show three tab items', () => { + cy.get('@items').should('have.length', 3); }); - it('should render the tabs container as nav element', () => { - cy.get('@tabs').find('nav[role="navigation"], nav').should('exist'); - }); + describe('semantic navigation markup', () => { + it('should render as navigation element', () => { + cy.get('@tabs') + .find('nav') + .should('exist'); + }); - it('should set proper ARIA attributes for navigation', () => { - cy.get('@tabs').find('nav').should('have.attr', 'aria-label', 'Tabs navigation'); - }); + it('should have aria-label on nav element for context', () => { + cy.get('@tabs') + .find('nav') + .should('have.attr', 'aria-label'); + }); - it('should support programmatic tab activation via show() method', () => { - cy.get('@tabs').then($tabs => { - const tabsElement = $tabs[0] as HTMLElement & { show: (tabName: string) => void }; - tabsElement.show('second'); + it('should have required label attribute on tabs component', () => { + cy.get('@tabs').should('have.attr', 'label'); }); - cy.get('@tabItems').eq(1).should('have.class', 'active'); - }); - it('should detect active tab based on aria-current="page"', () => { - cy.getComponent('tabs', TABS_ID, 'navigation-with-current'); - - cy.get('post-tab-item').eq(1).should('have.class', 'active'); - }); - }); + it('should contain anchor elements within tab items', () => { + cy.get('@items').each($item => { + cy.wrap($item).find('a').should('exist'); + }); + }); - describe('mode detection', () => { - it('should detect panels mode when no anchor elements are present', () => { - cy.getComponent('tabs', TABS_ID, 'default'); - cy.get('post-tabs').should('exist'); - cy.get('post-tab-panel').should('exist'); - cy.get('post-tabs').find('[part="content"]').should('exist'); + it('should mark the current page tab as active when anchor has aria-current="page"', () => { + cy.get('@items').each($item => { + const hasAriaCurrent = $item.find('a[aria-current="page"]').length > 0; + if (hasAriaCurrent) { + cy.wrap($item).should('have.class', 'active'); + } else { + cy.wrap($item).should('not.have.class', 'active'); + } + }); + }); }); - it('should detect navigation mode when anchor elements are present', () => { - cy.getComponent('tabs', TABS_ID, 'navigation-variant'); - cy.get('post-tabs').should('exist'); - cy.get('post-tab-panel').should('not.exist'); - cy.get('post-tabs').find('nav').should('exist'); - }); + describe('mode detection', () => { + it('should automatically enable navigation mode when is present inside post-tab-item', () => { + cy.get('@items').each($item => { + cy.wrap($item).should('have.attr', 'data-navigation-mode', 'true'); + }); + }); - it('should handle mixed mode usage', () => { - cy.getComponent('tabs', TABS_ID, 'mixed-mode'); - - cy.get('post-tabs').should('exist'); - cy.get('post-tab-item').should('exist'); + it('should not have tab panel elements in navigation mode', () => { + cy.get('post-tab-panel').should('not.exist'); + }); }); - }); -}); -describe('Accessibility', () => { - it('Has no detectable a11y violations on load for all variants', () => { - cy.getSnapshots('tabs'); + describe('tab item properties', () => { + it('should have name property on each tab item', () => { + cy.get('@items').each($item => { + cy.wrap($item).should('have.attr', 'name'); + }); + }); - cy.wait(200); - - cy.get('post-tabs').should('be.visible'); - cy.get('post-tab-item').should('exist'); + it('should not have role="tab" on tab items in navigation mode', () => { + cy.get('@items').each($item => { + cy.wrap($item).should('not.have.attr', 'role', 'tab'); + }); + }); - cy.checkA11y('#root-inner'); - }); + it('should not have tabindex on tab items in navigation mode', () => { + cy.get('@items').each($item => { + cy.wrap($item).should('not.have.attr', 'tabindex'); + }); + }); - describe('panels mode ARIA attributes', () => { - beforeEach(() => { - cy.getComponent('tabs', TABS_ID, 'default'); + it('should not have aria-selected attribute in navigation mode', () => { + cy.get('@items').each($item => { + cy.wrap($item).should('not.have.attr', 'aria-selected'); + }); + }); }); - it('should have proper ARIA attributes for panels mode', () => { - cy.get('post-tabs').find('[role="tablist"]').should('exist'); - cy.get('post-tab-item').should('have.attr', 'role', 'tab'); - cy.get('post-tab-item').should('have.attr', 'aria-selected'); - cy.get('post-tab-item').first().should('have.attr', 'aria-selected', 'true'); - cy.get('post-tab-item').not(':first').should('have.attr', 'aria-selected', 'false'); - }); + describe('anchor elements in light DOM', () => { + it('should render anchor elements in light DOM for consumer routing integration', () => { + cy.get('@items').each($item => { + cy.wrap($item).children('a').should('exist'); + }); + }); - it('should link tabs to panels with aria-controls and aria-labelledby', () => { - cy.get('post-tab-item').first().then($tab => { - const tabId = $tab.attr('id'); - const ariaControls = $tab.attr('aria-controls'); - - cy.get(`post-tab-panel[id="${ariaControls}"]`).should('exist'); - cy.get(`post-tab-panel[id="${ariaControls}"]`).should('have.attr', 'aria-labelledby', tabId); + it('should allow consumer routing via anchor href attributes', () => { + cy.get('@items').first().find('a').should('have.attr', 'href'); }); }); - it('should manage tabindex properly', () => { - cy.get('post-tab-item').first().should('have.attr', 'tabindex', '0'); - cy.get('post-tab-item').not(':first').should('have.attr', 'tabindex', '-1'); - - cy.get('post-tab-item').last().click(); - cy.get('post-tab-item').last().should('have.attr', 'tabindex', '0'); - cy.get('post-tab-item').not(':last').should('have.attr', 'tabindex', '-1'); - }); - }); + describe('active-tab property', () => { - describe('navigation mode ARIA attributes', () => { - beforeEach(() => { - cy.getComponent('tabs', TABS_ID, 'navigation-variant'); + it('should mark the tab item matching active-tab as active', () => { + cy.get('@tabs') + .invoke('attr', 'active-tab') + .then(activeTab => { + if (activeTab) { + cy.get(`post-tab-item[name="${activeTab}"]`).should('have.class', 'active'); + } + }); + }); }); - it('should have proper ARIA attributes for navigation mode', () => { - cy.get('post-tabs').find('nav').should('have.attr', 'aria-label', 'Tabs navigation'); - cy.get('post-tab-item').should('not.have.attr', 'role'); - cy.get('post-tab-item').should('not.have.attr', 'tabindex'); + describe('panels ignored in navigation mode', () => { + it('should not display content part in navigation mode', () => { + cy.get('@tabs') + .shadow() + .find('[part="content"]') + .should('not.exist'); + }); + + it('should ignore any post-tab-panel elements if present', () => { + cy.get('post-tab-panel').should('not.exist'); + }); }); + }); - it('should not have tablist role in navigation mode', () => { - cy.get('post-tabs').find('[role="tablist"]').should('not.exist'); + describe('Accessibility', () => { + it('Has no detectable a11y violations on load for all variants', () => { + cy.getSnapshots('tabs'); + cy.checkA11y('#root-inner'); }); }); -}); \ No newline at end of file +}); From 94921613ef57d9f55864f6d3acc29088b8892b54 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 5 Nov 2025 15:59:47 +0100 Subject: [PATCH 087/127] fixed formatting --- packages/components/cypress/e2e/tabs.cy.ts | 171 +++++++++++++++++---- 1 file changed, 140 insertions(+), 31 deletions(-) diff --git a/packages/components/cypress/e2e/tabs.cy.ts b/packages/components/cypress/e2e/tabs.cy.ts index c5831dc3e5..9e2b6acf63 100644 --- a/packages/components/cypress/e2e/tabs.cy.ts +++ b/packages/components/cypress/e2e/tabs.cy.ts @@ -177,20 +177,33 @@ describe('tabs', () => { cy.get('@items').should('have.length', 3); }); + it('should render as navigation when tabs contain anchor elements', () => { + cy.get('@items').each($item => { + cy.wrap($item).find('a').should('exist'); + }); + }); + + it('should not render tab panels in navigation mode', () => { + cy.get('post-tab-panel').should('not.exist'); + }); + + it('should not render content part in navigation mode', () => { + cy.get('@tabs') + .shadow() + .find('[part="content"]') + .should('not.exist'); + }); + describe('semantic navigation markup', () => { - it('should render as navigation element', () => { - cy.get('@tabs') - .find('nav') - .should('exist'); + it('should render the tabs container as nav element', () => { + cy.get('@tabs').find('nav').should('exist'); }); - it('should have aria-label on nav element for context', () => { - cy.get('@tabs') - .find('nav') - .should('have.attr', 'aria-label'); + it('should have aria-label on nav element', () => { + cy.get('@tabs').find('nav').should('have.attr', 'aria-label'); }); - it('should have required label attribute on tabs component', () => { + it('should have label attribute on tabs component', () => { cy.get('@tabs').should('have.attr', 'label'); }); @@ -200,16 +213,29 @@ describe('tabs', () => { }); }); - it('should mark the current page tab as active when anchor has aria-current="page"', () => { - cy.get('@items').each($item => { - const hasAriaCurrent = $item.find('a[aria-current="page"]').length > 0; - if (hasAriaCurrent) { - cy.wrap($item).should('have.class', 'active'); - } else { - cy.wrap($item).should('not.have.class', 'active'); - } - }); + it('should mark the current page tab as active when anchor has aria-current="page"', () => { + cy.get('@items').each($item => { + const hasAriaCurrent = $item.find('a[aria-current="page"]').length > 0; + if (hasAriaCurrent) { + cy.wrap($item).should('have.class', 'active'); + } else { + cy.wrap($item).should('not.have.class', 'active'); + } }); + }); + + it('should mark only one tab as active when anchor has aria-current="page"', () => { + let activeCount = 0; + cy.get('@items').each($item => { + const hasAriaCurrent = $item.find('a[aria-current="page"]').length > 0; + if (hasAriaCurrent) { + activeCount++; + cy.wrap($item).should('have.class', 'active'); + } + }).then(() => { + expect(activeCount).to.be.lte(1); // At most one active tab + }); + }); }); describe('mode detection', () => { @@ -253,6 +279,7 @@ describe('tabs', () => { describe('anchor elements in light DOM', () => { it('should render anchor elements in light DOM for consumer routing integration', () => { cy.get('@items').each($item => { + // Verify anchor is in light DOM (not in shadow DOM) cy.wrap($item).children('a').should('exist'); }); }); @@ -263,6 +290,15 @@ describe('tabs', () => { }); describe('active-tab property', () => { + it('should support programmatic tab activation via show() method', () => { + cy.get('@tabs').then($tabs => { + const tabsElement = $tabs[0] as HTMLElement & { show: (tabName: string) => Promise }; + if (typeof tabsElement.show === 'function') { + void tabsElement.show('second'); + cy.get('@items').eq(1).should('have.class', 'active'); + } + }); + }); it('should mark the tab item matching active-tab as active', () => { cy.get('@tabs') @@ -274,25 +310,98 @@ describe('tabs', () => { }); }); }); + }); - describe('panels ignored in navigation mode', () => { - it('should not display content part in navigation mode', () => { - cy.get('@tabs') - .shadow() - .find('[part="content"]') - .should('not.exist'); - }); + describe('mode detection', () => { + it('should detect panels mode when no anchor elements are present', () => { + cy.getComponent('tabs', TABS_ID, 'default'); + cy.get('post-tabs').should('exist'); + cy.get('post-tab-panel').should('exist'); + cy.get('post-tabs') + .shadow() + .find('[part="content"]') + .should('exist'); + }); - it('should ignore any post-tab-panel elements if present', () => { - cy.get('post-tab-panel').should('not.exist'); + it('should detect navigation mode when anchor elements are present', () => { + cy.getComponent('tabs', TABS_ID, 'navigation'); + cy.get('post-tabs').should('exist'); + cy.get('post-tab-panel').should('not.exist'); + cy.get('post-tabs').find('nav').should('exist'); + }); + }); +}); + +describe('Accessibility', () => { + it('Has no detectable a11y violations on load for all variants', () => { + cy.getSnapshots('tabs'); + + cy.wait(200); + + cy.get('post-tabs').should('be.visible'); + cy.get('post-tab-item').should('exist'); + + cy.checkA11y('#root-inner'); + }); + + describe('panels mode ARIA attributes', () => { + beforeEach(() => { + cy.getComponent('tabs', TABS_ID, 'default'); + }); + + it('should have proper ARIA attributes for panels mode', () => { + cy.get('post-tabs') + .shadow() + .find('[role="tablist"]') + .should('exist'); + cy.get('post-tab-item').should('have.attr', 'role', 'tab'); + cy.get('post-tab-item').should('have.attr', 'aria-selected'); + cy.get('post-tab-item').first().should('have.attr', 'aria-selected', 'true'); + cy.get('post-tab-item').not(':first').should('have.attr', 'aria-selected', 'false'); + }); + + it('should link tabs to panels with aria-controls and aria-labelledby', () => { + cy.get('post-tab-item').first().then($tab => { + const tabId = $tab.attr('id'); + const ariaControls = $tab.attr('aria-controls'); + + if (ariaControls) { + cy.get(`post-tab-panel[id="${ariaControls}"]`).should('exist'); + cy.get(`post-tab-panel[id="${ariaControls}"]`).should('have.attr', 'aria-labelledby', tabId); + } }); }); + + it('should manage tabindex properly', () => { + cy.get('post-tab-item').first().should('have.attr', 'tabindex', '0'); + cy.get('post-tab-item').not(':first').should('have.attr', 'tabindex', '-1'); + + cy.get('post-tab-item').last().click(); + cy.get('post-tab-item').last().should('have.attr', 'tabindex', '0'); + cy.get('post-tab-item').not(':last').should('have.attr', 'tabindex', '-1'); + }); }); - describe('Accessibility', () => { - it('Has no detectable a11y violations on load for all variants', () => { - cy.getSnapshots('tabs'); - cy.checkA11y('#root-inner'); + describe('navigation mode ARIA attributes', () => { + beforeEach(() => { + cy.getComponent('tabs', TABS_ID, 'navigation'); + }); + + it('should have proper ARIA attributes for navigation mode', () => { + cy.get('post-tabs').find('nav').should('have.attr', 'aria-label'); + cy.get('post-tab-item').should('not.have.attr', 'role'); + cy.get('post-tab-item').should('not.have.attr', 'tabindex'); + }); + + it('should not have tablist role in navigation mode', () => { + cy.get('post-tabs') + .shadow() + .find('[role="tablist"]') + .should('not.exist'); + }); + + it('should not have aria-selected on tab items in navigation mode', () => { + cy.get('post-tab-item').should('not.have.attr', 'aria-selected'); }); }); }); From e9ab30d105f4d03b1dc9afbb1d8f1a2ee2ccd5d0 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 5 Nov 2025 16:09:27 +0100 Subject: [PATCH 088/127] renamed tab header to tab item --- packages/components/cypress/e2e/tabs.cy.ts | 47 ++++++++++------------ 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/packages/components/cypress/e2e/tabs.cy.ts b/packages/components/cypress/e2e/tabs.cy.ts index 9e2b6acf63..9710783ae8 100644 --- a/packages/components/cypress/e2e/tabs.cy.ts +++ b/packages/components/cypress/e2e/tabs.cy.ts @@ -11,17 +11,17 @@ describe('tabs', () => { cy.get('@tabs').should('exist'); }); - it('should show three tab headers', () => { + it('should show three tab items', () => { cy.get('@items').should('have.length', 3); }); - it('should only show the first tab header as active', () => { + it('should only show the first tab item as active', () => { cy.get('post-tab-item.active').each(($item, index) => { cy.wrap($item).should(index === 0 ? 'exist' : 'not.exist'); }); }); - it('should only show the tab panel associated with the first tab header', () => { + it('should only show the tab panel associated with the first tab item', () => { cy.get('post-tab-panel:visible').as('panel'); cy.get('@panel').should('have.length', 1); cy.get('@items') @@ -32,14 +32,14 @@ describe('tabs', () => { }); }); - it('should activate a clicked tab header and deactivate the tab header that was previously activated', () => { + it('should activate a clicked tab item and deactivate the tab item that was previously activated', () => { cy.get('@items').last().click(); cy.get('@items').first().should('not.have.class', 'active'); cy.get('@items').last().should('have.class', 'active'); }); - it('should show the panel associated with a clicked tab header and hide the panel that was previously shown', () => { + it('should show the panel associated with a clicked tab item and hide the panel that was previously shown', () => { cy.get('@items').last().click(); // wait for the fade out animation to complete @@ -95,12 +95,12 @@ describe('tabs', () => { cy.get('post-tab-item').as('items'); }); - it('should add a tab header', () => { + it('should add a tab item', () => { cy.get('#add-tab').click(); cy.get('@items').should('have.length', 4); }); - it('should still show the tab panel associated with the first tab header after adding new tab', () => { + it('should still show the tab panel associated with the first tab item after adding new tab', () => { cy.get('#add-tab').click(); cy.get('post-tab-panel:visible').as('panel'); @@ -113,7 +113,7 @@ describe('tabs', () => { }); }); - it('should activate the newly added tab header after clicking on it', () => { + it('should activate the newly added tab item after clicking on it', () => { cy.get('#add-tab').click(); cy.get('post-tab-item').as('items'); @@ -141,14 +141,14 @@ describe('tabs', () => { }); }); - it('should remove a tab header', () => { + it('should remove a tab item', () => { cy.get('.tab-title.active').then(() => { cy.get('#remove-active-tab').click(); cy.get('@items').should('have.length', 2); }); }); - it('should still show an active tab header after removing the active tab', () => { + it('should still show an active tab item after removing the active tab', () => { cy.get('.tab-title.active').then(() => { cy.get('#remove-active-tab').click(); cy.get('.tab-title.active').should('exist'); @@ -213,16 +213,17 @@ describe('tabs', () => { }); }); - it('should mark the current page tab as active when anchor has aria-current="page"', () => { - cy.get('@items').each($item => { - const hasAriaCurrent = $item.find('a[aria-current="page"]').length > 0; - if (hasAriaCurrent) { - cy.wrap($item).should('have.class', 'active'); - } else { - cy.wrap($item).should('not.have.class', 'active'); - } - }); + it('should mark the current page tab as active when anchor has aria-current="page"', () => { + cy.get('@items').each($item => { + const hasAriaCurrent = $item.find('a[aria-current="page"]').length > 0; + if (hasAriaCurrent) { + cy.wrap($item).should('have.class', 'active'); + } else { + cy.wrap($item).should('not.have.class', 'active'); + } }); + }); + it('should mark only one tab as active when anchor has aria-current="page"', () => { let activeCount = 0; @@ -335,12 +336,6 @@ describe('tabs', () => { describe('Accessibility', () => { it('Has no detectable a11y violations on load for all variants', () => { cy.getSnapshots('tabs'); - - cy.wait(200); - - cy.get('post-tabs').should('be.visible'); - cy.get('post-tab-item').should('exist'); - cy.checkA11y('#root-inner'); }); @@ -404,4 +399,4 @@ describe('Accessibility', () => { cy.get('post-tab-item').should('not.have.attr', 'aria-selected'); }); }); -}); +}); \ No newline at end of file From a64eea72a994f180d01751efe8efc443ac241bab Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 5 Nov 2025 16:15:24 +0100 Subject: [PATCH 089/127] removed duplicate tests --- packages/components/cypress/e2e/tabs.cy.ts | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/packages/components/cypress/e2e/tabs.cy.ts b/packages/components/cypress/e2e/tabs.cy.ts index 9710783ae8..c608b44105 100644 --- a/packages/components/cypress/e2e/tabs.cy.ts +++ b/packages/components/cypress/e2e/tabs.cy.ts @@ -177,15 +177,7 @@ describe('tabs', () => { cy.get('@items').should('have.length', 3); }); - it('should render as navigation when tabs contain anchor elements', () => { - cy.get('@items').each($item => { - cy.wrap($item).find('a').should('exist'); - }); - }); - - it('should not render tab panels in navigation mode', () => { - cy.get('post-tab-panel').should('not.exist'); - }); + it('should not render content part in navigation mode', () => { cy.get('@tabs') @@ -239,17 +231,7 @@ describe('tabs', () => { }); }); - describe('mode detection', () => { - it('should automatically enable navigation mode when is present inside post-tab-item', () => { - cy.get('@items').each($item => { - cy.wrap($item).should('have.attr', 'data-navigation-mode', 'true'); - }); - }); - - it('should not have tab panel elements in navigation mode', () => { - cy.get('post-tab-panel').should('not.exist'); - }); - }); + describe('tab item properties', () => { it('should have name property on each tab item', () => { From 127bdf2d5ead02eb9c8589da96a014d809845c01 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 5 Nov 2025 16:18:03 +0100 Subject: [PATCH 090/127] fixed liting --- packages/components/cypress/e2e/tabs.cy.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/components/cypress/e2e/tabs.cy.ts b/packages/components/cypress/e2e/tabs.cy.ts index c608b44105..9de8d2adf9 100644 --- a/packages/components/cypress/e2e/tabs.cy.ts +++ b/packages/components/cypress/e2e/tabs.cy.ts @@ -205,16 +205,16 @@ describe('tabs', () => { }); }); - it('should mark the current page tab as active when anchor has aria-current="page"', () => { - cy.get('@items').each($item => { - const hasAriaCurrent = $item.find('a[aria-current="page"]').length > 0; - if (hasAriaCurrent) { - cy.wrap($item).should('have.class', 'active'); - } else { - cy.wrap($item).should('not.have.class', 'active'); - } + it('should mark the current page tab as active when anchor has aria-current="page"', () => { + cy.get('@items').each($item => { + const hasAriaCurrent = $item.find('a[aria-current="page"]').length > 0; + if (hasAriaCurrent) { + cy.wrap($item).should('have.class', 'active'); + } else { + cy.wrap($item).should('not.have.class', 'active'); + } + }); }); - }); it('should mark only one tab as active when anchor has aria-current="page"', () => { From 07f9257f6570179a9e71157f6c8c15f3d18b26b0 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 5 Nov 2025 16:36:03 +0100 Subject: [PATCH 091/127] removed duplicate tests --- packages/components/cypress/e2e/tabs.cy.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/components/cypress/e2e/tabs.cy.ts b/packages/components/cypress/e2e/tabs.cy.ts index 9de8d2adf9..57bd15f83f 100644 --- a/packages/components/cypress/e2e/tabs.cy.ts +++ b/packages/components/cypress/e2e/tabs.cy.ts @@ -251,12 +251,6 @@ describe('tabs', () => { cy.wrap($item).should('not.have.attr', 'tabindex'); }); }); - - it('should not have aria-selected attribute in navigation mode', () => { - cy.get('@items').each($item => { - cy.wrap($item).should('not.have.attr', 'aria-selected'); - }); - }); }); describe('anchor elements in light DOM', () => { @@ -376,9 +370,5 @@ describe('Accessibility', () => { .find('[role="tablist"]') .should('not.exist'); }); - - it('should not have aria-selected on tab items in navigation mode', () => { - cy.get('post-tab-item').should('not.have.attr', 'aria-selected'); - }); }); }); \ No newline at end of file From 5966df9ffc50fea8d2d6b2e3b40ea5a5df303457 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Thu, 6 Nov 2025 07:01:00 +0100 Subject: [PATCH 092/127] fixed aria-current --- .../post-tab-item/post-tab-item.tsx | 29 ++++++++++--------- .../src/components/post-tabs/post-tabs.tsx | 29 +++++++++++++++---- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/packages/components/src/components/post-tab-item/post-tab-item.tsx b/packages/components/src/components/post-tab-item/post-tab-item.tsx index 4a7b3742dc..e39d8abac1 100644 --- a/packages/components/src/components/post-tab-item/post-tab-item.tsx +++ b/packages/components/src/components/post-tab-item/post-tab-item.tsx @@ -19,7 +19,6 @@ export class PostTabItem { @State() tabId: string; @State() isNavigationMode = false; - @State() hasAriaCurrent = false; /** * The name of the tab, used to associate it with a tab panel or identify the active tab in panel mode. @@ -36,7 +35,7 @@ export class PostTabItem { childList: true, subtree: true, attributes: true, - attributeFilter: ['aria-current'], + attributeFilter: ['class'], }); } @@ -46,7 +45,7 @@ export class PostTabItem { componentDidLoad() { this.checkNavigationMode(); - this.checkAriaCurrent(); + this.syncAriaCurrent(); } disconnectedCallback() { @@ -57,7 +56,7 @@ export class PostTabItem { private handleMutations() { this.checkNavigationMode(); - this.checkAriaCurrent(); + this.syncAriaCurrent(); } private checkNavigationMode() { @@ -65,19 +64,23 @@ export class PostTabItem { this.isNavigationMode = hasAnchor; } - private checkAriaCurrent() { - if (this.isNavigationMode) { - const anchor = this.host.querySelector('a'); - this.hasAriaCurrent = anchor?.getAttribute('aria-current') === 'page'; + private syncAriaCurrent() { + if (!this.isNavigationMode) return; + + const anchor = this.host.querySelector('a'); + if (!anchor) return; + + const isActive = this.host.classList.contains('active'); + + if (isActive) { + anchor.setAttribute('aria-current', 'page'); + } else { + anchor.removeAttribute('aria-current'); } } render() { const isPanelMode = !this.isNavigationMode; - const classes = { - 'tab-title': true, - active: this.isNavigationMode && this.hasAriaCurrent, - }; return ( diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 662b057243..9fbfb99b1e 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -79,9 +79,16 @@ export class PostTabs { this.validateLabel(); if (this.isNavigationMode) { - const activeTab = this.findActiveNavigationTab(); - if (activeTab) { - void this.show(activeTab.name); + // In navigation mode, check for activeTab prop or find tab with aria-current + if (this.activeTab) { + void this.show(this.activeTab); + } else { + const activeTab = this.findActiveNavigationTab(); + if (activeTab) { + void this.show(activeTab.name); + } else if (this.tabs.length > 0) { + void this.show(this.tabs[0].name); + } } } else { const initiallyActiveTab = this.activeTab || this.tabs[0]?.name; @@ -139,6 +146,14 @@ export class PostTabs { // If mode changed, re-initialize if (previousMode !== this.isNavigationMode) { this.enableTabs(); + + // Re-initialize active tab after mode change + if (this.isNavigationMode) { + const activeTab = this.activeTab || this.findActiveNavigationTab()?.name || this.tabs[0]?.name; + if (activeTab) { + void this.show(activeTab); + } + } } } } @@ -271,23 +286,26 @@ export class PostTabs { } private activateTab(tab: HTMLPostTabItemElement) { + // Deactivate previous tab if (this.currentActiveTab) { this.currentActiveTab.setAttribute('aria-selected', 'false'); + this.currentActiveTab.classList.remove('active'); + if (!this.isNavigationMode) { this.currentActiveTab.setAttribute('tabindex', '-1'); } else { this.currentActiveTab.removeAttribute('tabindex'); } - this.currentActiveTab.classList.remove('active'); } tab.setAttribute('aria-selected', 'true'); + tab.classList.add('active'); + if (!this.isNavigationMode) { tab.setAttribute('tabindex', '0'); } else { tab.removeAttribute('tabindex'); } - tab.classList.add('active'); this.currentActiveTab = tab; } @@ -306,6 +324,7 @@ export class PostTabs { private showSelectedPanel() { const panel = this.getPanel(this.currentActiveTab.name); + if (!panel) return; panel.style.display = 'block'; // prevent the initially selected panel from fading in From 30a94ccd126302b3f3f88dade76ecc343aecde47 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Thu, 6 Nov 2025 07:16:41 +0100 Subject: [PATCH 093/127] fixed linting --- .../src/components/post-tabs/post-tabs.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 9fbfb99b1e..4c9f4812d4 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -288,23 +288,19 @@ export class PostTabs { private activateTab(tab: HTMLPostTabItemElement) { // Deactivate previous tab if (this.currentActiveTab) { - this.currentActiveTab.setAttribute('aria-selected', 'false'); this.currentActiveTab.classList.remove('active'); - + if (!this.isNavigationMode) { + this.currentActiveTab.setAttribute('aria-selected', 'false'); this.currentActiveTab.setAttribute('tabindex', '-1'); - } else { - this.currentActiveTab.removeAttribute('tabindex'); } } - tab.setAttribute('aria-selected', 'true'); tab.classList.add('active'); - + if (!this.isNavigationMode) { + tab.setAttribute('aria-selected', 'true'); tab.setAttribute('tabindex', '0'); - } else { - tab.removeAttribute('tabindex'); } this.currentActiveTab = tab; From 46f64e9217128f842eb95f914f0e174a1c2fd86c Mon Sep 17 00:00:00 2001 From: Alona Zherdetska <138328641+alionazherdetska@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:25:55 +0100 Subject: [PATCH 094/127] chore(components): added initial styles for `post-tabs` component in nav mode (#6593) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📄 Description This PR adds initial styles for for `post-tabs` component in nav mode. ## 🚀 Demo [Tabs docs ](https://preview-6593--swisspost-design-system-next.netlify.app/?path=/docs/introduction--docs)--- ## 🔮 Design review - [ ] Design review done - [ ] No design review needed ## 📝 Checklist - ✅ My code follows the style guidelines of this project - 🛠️ I have performed a self-review of my own code - 📄 I have made corresponding changes to the documentation - ⚠️ My changes generate no new warnings or errors - 🧪 I have added tests that prove my fix is effective or that my feature works - ✔️ New and existing unit tests pass locally with my changes --- .../post-tab-item/post-tab-item.tsx | 10 +- .../src/components/tabs/_tab-title.scss | 134 ++++++++++++------ 2 files changed, 97 insertions(+), 47 deletions(-) diff --git a/packages/components/src/components/post-tab-item/post-tab-item.tsx b/packages/components/src/components/post-tab-item/post-tab-item.tsx index e39d8abac1..3cb25bdf65 100644 --- a/packages/components/src/components/post-tab-item/post-tab-item.tsx +++ b/packages/components/src/components/post-tab-item/post-tab-item.tsx @@ -80,17 +80,15 @@ export class PostTabItem { } render() { - const isPanelMode = !this.isNavigationMode; - return ( diff --git a/packages/styles/src/components/tabs/_tab-title.scss b/packages/styles/src/components/tabs/_tab-title.scss index 48ac0a0d76..006e1e6c55 100644 --- a/packages/styles/src/components/tabs/_tab-title.scss +++ b/packages/styles/src/components/tabs/_tab-title.scss @@ -9,25 +9,20 @@ tokens.$default-map: utilities.$post-spacing; -.tab-title[role='tab'] { +.tab-title, +.nav-item a { display: inline-block; - cursor: pointer; position: relative; box-sizing: border-box; padding: nav.$nav-link-padding; transition: background-color 100ms; border-right: 1px solid transparent; border-left: 1px solid transparent; - outline-color: currentColor; opacity: 0.7; color: color.$black; text-decoration: none; background-color: transparent; - - &:focus { - background-color: unset; - color: color.$black; - } + cursor: pointer; &:hover { opacity: 1; @@ -55,34 +50,8 @@ tokens.$default-map: utilities.$post-spacing; } } - &.active { - z-index: 1; // Lift above the line and make focus visible all around - border-right-color: nav.$nav-tabs-border-color; - border-left-color: nav.$nav-tabs-border-color; - opacity: 1; - background-color: color.$white; - color: color.$black; - font-weight: 700; - - // Create a line that does not suffer from border corner mitering - &::before { - content: ''; - display: block; - position: absolute; - top: 0; - right: -1px; - left: -1px; - height: tokens.get('utility-gap-4'); - background-color: nav.$nav-tabs-link-active-border-color; - } - } - - // Tabs with dark backgrounds @include color-mx.on-dark-background() { - &:hover { - background-color: rgba(nav.$nav-tabs-link-active-bg, 0.2); - } - + &:hover, &:focus-visible { background-color: rgba(nav.$nav-tabs-link-active-bg, 0.2); } @@ -101,15 +70,98 @@ tokens.$default-map: utilities.$post-spacing; outline: tokens.get('utility-gap-2') solid Highlight; outline-offset: calc(tokens.get('utility-gap-4') * -1); } + } +} - &.active { - border-left-color: ButtonText; - border-right-color: ButtonText; - color: Highlight; +.nav-item { + display: inline-block; + position: relative; + box-sizing: border-box; + cursor: default; + + &:focus, + &:focus-visible { + outline: none; + box-shadow: none; + + &::after { + display: none; + } + } + + a { + color: inherit; + text-decoration: none; + display: block; + width: 100%; + height: 100%; + box-sizing: border-box; + position: relative; + outline: none; + cursor: pointer; + border-radius: 0; + + &:focus { + outline: none; + } + + &:active { + opacity: 1; + background-color: color.$black-alpha-60; + color: color.$white; + } + + &:focus-visible { + outline: none !important; + } + } + + @include color-mx.on-dark-background() { + a { + &:hover { + background-color: rgba(nav.$nav-tabs-link-active-bg, 0.2); + } - &::before { - background-color: Highlight; + &:focus-visible { + background-color: rgba(nav.$nav-tabs-link-active-bg, 0.2); } } } } + +.tab-title.active, +.nav-item.active a { + z-index: 1; + border-right-color: nav.$nav-tabs-border-color; + border-left-color: nav.$nav-tabs-border-color; + opacity: 1; + background-color: color.$white; + color: color.$black; + font-weight: 700; +} + +// Create a line that does not suffer from border corner mitering +.tab-title.active::before, +.nav-item.active a::before { + content: ''; + display: block; + position: absolute; + top: 0; + right: -1px; + left: -1px; + height: tokens.get('utility-gap-4'); + background-color: nav.$nav-tabs-link-active-border-color; +} + +@include utilities-mx.high-contrast-mode() { + .tab-title.active, + .nav-item.active a { + border-left-color: ButtonText; + border-right-color: ButtonText; + color: Highlight; + + &::before { + background-color: Highlight; + } + } +} From 9d56ea8f5154186e83ba35dc8d26b61e0efe47b5 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Fri, 7 Nov 2025 08:51:33 +0100 Subject: [PATCH 095/127] fixed the tabs --- packages/components/src/components.d.ts | 4 +- .../src/components/post-tabs/post-tabs.tsx | 44 ++++++++++--------- .../src/components/post-tabs/readme.md | 10 ++--- .../stories/components/tabs/tabs.stories.ts | 37 +++++++++++----- 4 files changed, 55 insertions(+), 40 deletions(-) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 80d0cd7e54..3ec5b8c5f7 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -491,7 +491,7 @@ export namespace Components { } interface PostTabs { /** - * The name of the tab that is initially active. If not specified, it defaults to the first tab. **Changing this value after initialization has no effect.** + * The name of the tab that is initially active. If not specified, it defaults to the first tab. **Panel mode**: Changing this value after initialization has no effect. **Navigation mode**: This should be updated by the routing framework to reflect the current page on each navigation. The component will automatically sync the active state when this prop changes. */ "activeTab"?: string; /** @@ -1392,7 +1392,7 @@ declare namespace LocalJSX { } interface PostTabs { /** - * The name of the tab that is initially active. If not specified, it defaults to the first tab. **Changing this value after initialization has no effect.** + * The name of the tab that is initially active. If not specified, it defaults to the first tab. **Panel mode**: Changing this value after initialization has no effect. **Navigation mode**: This should be updated by the routing framework to reflect the current page on each navigation. The component will automatically sync the active state when this prop changes. */ "activeTab"?: string; /** diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 4c9f4812d4..2522e82115 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -42,10 +42,21 @@ export class PostTabs { * The name of the tab that is initially active. * If not specified, it defaults to the first tab. * - * **Changing this value after initialization has no effect.** + * **Panel mode**: Changing this value after initialization has no effect. + * + * **Navigation mode**: This should be updated by the routing framework + * to reflect the current page on each navigation. The component will + * automatically sync the active state when this prop changes. */ @Prop() readonly activeTab?: string; + @Watch('activeTab') + handleActiveTabChange(newTab: string) { + if (this.isLoaded && this.isNavigationMode && newTab && newTab !== this.currentActiveTab?.name) { + void this.show(newTab); + } + } + /** * When set to true, this property allows the tabs container to span the * full width of the screen, from edge to edge. @@ -78,21 +89,14 @@ export class PostTabs { this.setupContentObserver(); this.validateLabel(); - if (this.isNavigationMode) { - // In navigation mode, check for activeTab prop or find tab with aria-current - if (this.activeTab) { - void this.show(this.activeTab); - } else { - const activeTab = this.findActiveNavigationTab(); - if (activeTab) { - void this.show(activeTab.name); - } else if (this.tabs.length > 0) { - void this.show(this.tabs[0].name); - } - } - } else { - const initiallyActiveTab = this.activeTab || this.tabs[0]?.name; - void this.show(initiallyActiveTab); + // Unified logic for both modes with priority order + const tabToActivate = + this.activeTab || + this.findActiveNavigationTab()?.name || + this.tabs[0]?.name; + + if (tabToActivate) { + void this.show(tabToActivate); } } @@ -148,11 +152,9 @@ export class PostTabs { this.enableTabs(); // Re-initialize active tab after mode change - if (this.isNavigationMode) { - const activeTab = this.activeTab || this.findActiveNavigationTab()?.name || this.tabs[0]?.name; - if (activeTab) { - void this.show(activeTab); - } + const activeTab = this.activeTab || this.findActiveNavigationTab()?.name || this.tabs[0]?.name; + if (activeTab) { + void this.show(activeTab); } } } diff --git a/packages/components/src/components/post-tabs/readme.md b/packages/components/src/components/post-tabs/readme.md index 0e84c86d5b..758d35c4b6 100644 --- a/packages/components/src/components/post-tabs/readme.md +++ b/packages/components/src/components/post-tabs/readme.md @@ -7,11 +7,11 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| -------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | -| `activeTab` | `active-tab` | The name of the tab that is initially active. If not specified, it defaults to the first tab. **Changing this value after initialization has no effect.** | `string` | `undefined` | -| `fullWidth` | `full-width` | When set to true, this property allows the tabs container to span the full width of the screen, from edge to edge. | `boolean` | `false` | -| `label` _(required)_ | `label` | The accessible label for the tabs component for navigation variant. | `string` | `undefined` | +| Property | Attribute | Description | Type | Default | +| -------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | +| `activeTab` | `active-tab` | The name of the tab that is initially active. If not specified, it defaults to the first tab. **Panel mode**: Changing this value after initialization has no effect. **Navigation mode**: This should be updated by the routing framework to reflect the current page on each navigation. The component will automatically sync the active state when this prop changes. | `string` | `undefined` | +| `fullWidth` | `full-width` | When set to true, this property allows the tabs container to span the full width of the screen, from edge to edge. | `boolean` | `false` | +| `label` _(required)_ | `label` | The accessible label for the tabs component for navigation variant. | `string` | `undefined` | ## Events diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index c0831b0c5e..48adf41a47 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -30,7 +30,7 @@ const meta: MetaComponent

If you attempt (anchors + panels), the component will throw an error.

', + description: 'Select between panels variant (content sections) or navigation variant (page navigation).

If you attempt (anchors + panels), the component will throw an error.

', control: 'radio', options: ['panels', 'navigation'], table: { @@ -38,17 +38,35 @@ const meta: MetaComponent; +type Story = StoryObj; -export const Default: Story = { -}; +export const Default: Story = {}; export const PanelsVariant: Story = { parameters: { From fdfc62bdcdced6f848408b4f456c2201ebfcf8ea Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Fri, 7 Nov 2025 09:04:55 +0100 Subject: [PATCH 096/127] updated the docs --- .../stories/components/tabs/tabs.stories.ts | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index 48adf41a47..7735f6f94a 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -4,7 +4,13 @@ import { ifDefined } from 'lit/directives/if-defined.js'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { MetaComponent } from '@root/types'; -const meta: MetaComponent = { +const meta: MetaComponent = { id: 'bb1291ca-4dbb-450c-a15f-596836d9f39e', title: 'Components/Tabs', tags: ['package:WebComponents', 'status:InProgress'], @@ -41,7 +47,7 @@ const meta: MetaComponent) { +function renderTabs(args: Partial) { const variant = args.variant || 'panels'; + // Map the variant-specific activeTab arg to the actual activeTab prop + const activeTab = variant === 'navigation' ? args.activeTabNavigation : args.activeTabPanels; + if (variant === 'navigation') { if (args['slots-default']) { return html` @@ -144,7 +158,7 @@ function renderTabs(args: Partial @@ -166,7 +180,7 @@ function renderTabs(args: Partial ${unsafeHTML(args['slots-default'])} @@ -177,7 +191,7 @@ function renderTabs(args: Partial First tab @@ -191,7 +205,7 @@ function renderTabs(args: Partial First tab @@ -212,7 +226,13 @@ function renderTabs(args: Partial; +type Story = StoryObj; export const Default: Story = {}; @@ -254,7 +274,7 @@ export const ActiveTab: Story = { }, args: { variant: 'panels', - activeTab: 'third', + activeTabPanels: 'third', }, }; From 07a6738c467850a78afdaaa27b984f0951c007cc Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Fri, 7 Nov 2025 09:12:36 +0100 Subject: [PATCH 097/127] removed redundant code --- .../src/stories/components/tabs/tabs.stories.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index 7735f6f94a..0c0297bc8a 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -22,16 +22,6 @@ const meta: MetaComponent TemplateResult) => html`${story()}`, - ], }, argTypes: { variant: { @@ -47,12 +37,13 @@ const meta: MetaComponent Date: Fri, 7 Nov 2025 09:14:38 +0100 Subject: [PATCH 098/127] fixed linting --- .../documentation/src/stories/components/tabs/tabs.stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index 0c0297bc8a..89090d2767 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -1,5 +1,5 @@ import { StoryObj } from '@storybook/web-components-vite'; -import { html, nothing, TemplateResult } from 'lit'; +import { html, nothing } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { MetaComponent } from '@root/types'; From 4245dda3767a8aef5bda9c9ad7b16c005d715155 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Fri, 7 Nov 2025 09:38:49 +0100 Subject: [PATCH 099/127] fixed code quality --- .../stories/components/tabs/tabs.stories.ts | 105 ++++++++++-------- 1 file changed, 59 insertions(+), 46 deletions(-) diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index 89090d2767..1c53ad22ca 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -123,74 +123,72 @@ const meta: MetaComponent) { - const variant = args.variant || 'panels'; - - // Map the variant-specific activeTab arg to the actual activeTab prop - const activeTab = variant === 'navigation' ? args.activeTabNavigation : args.activeTabPanels; - - if (variant === 'navigation') { - if (args['slots-default']) { - return html` - - ${unsafeHTML(args['slots-default'])} - - `; - } - +function renderNavigationVariant( + activeTab: string, + fullWidth: boolean, + label: string, + customSlots: string +): ReturnType { + if (customSlots) { return html` - -
First page - - - Second page - - - Third page - + ${unsafeHTML(customSlots)}
`; } - // Panels variant (default) - if (args['slots-default']) { - // Use custom slot content if provided (complete custom content) + return html` + + + First page + + + Second page + + + Third page + + + `; +} + +// Helper function to render tabs variant +function renderPanelsVariant( + activeTab: string | undefined, + fullWidth: boolean | undefined, + customSlots: string, + panelSlots: string +): ReturnType { + if (customSlots) { return html` - ${unsafeHTML(args['slots-default'])} + ${unsafeHTML(customSlots)} `; } - if (args['slots-panels']) { + if (panelSlots) { return html` First tab Second tab Third tab - ${unsafeHTML(args['slots-panels'])} + ${unsafeHTML(panelSlots)} `; } @@ -198,7 +196,7 @@ function renderTabs(args: Partial First tab Second tab @@ -217,6 +215,21 @@ function renderTabs(args: Partial) { + const variant = args.variant || 'panels'; + const activeTab = variant === 'navigation' ? args.activeTabNavigation : args.activeTabPanels; + + return variant === 'navigation' + ? renderNavigationVariant(activeTab, args.fullWidth, args.label, args['slots-default'] || '') + : renderPanelsVariant(activeTab, args.fullWidth, args['slots-default'] || '', args['slots-panels'] || ''); +} + // STORIES type Story = StoryObj Date: Fri, 7 Nov 2025 11:12:30 +0100 Subject: [PATCH 100/127] test files to prove aria-current is getting assigned --- .../src/app/ssr/contact/page.tsx | 11 + .../nextjs-integration/src/app/ssr/page.tsx | 328 ++++++++---------- .../src/app/ssr/products/page.tsx | 11 + 3 files changed, 170 insertions(+), 180 deletions(-) create mode 100644 packages/nextjs-integration/src/app/ssr/contact/page.tsx create mode 100644 packages/nextjs-integration/src/app/ssr/products/page.tsx diff --git a/packages/nextjs-integration/src/app/ssr/contact/page.tsx b/packages/nextjs-integration/src/app/ssr/contact/page.tsx new file mode 100644 index 0000000000..1164b1e5e6 --- /dev/null +++ b/packages/nextjs-integration/src/app/ssr/contact/page.tsx @@ -0,0 +1,11 @@ +'use client'; + +import dynamic from 'next/dynamic'; + +const CSRPage = dynamic(() => import('../page'), { + ssr: false, +}); + +export default function ProductsPage() { + return ; +} \ No newline at end of file diff --git a/packages/nextjs-integration/src/app/ssr/page.tsx b/packages/nextjs-integration/src/app/ssr/page.tsx index edc5381aec..3b77ad4b8b 100644 --- a/packages/nextjs-integration/src/app/ssr/page.tsx +++ b/packages/nextjs-integration/src/app/ssr/page.tsx @@ -1,193 +1,161 @@ -import { - PostAccordion, - PostAccordionItem, - PostAvatar, - PostBanner, - PostCardControl, - PostClosebutton, - PostCollapsible, - PostCollapsibleTrigger, - PostIcon, - PostLinkarea, - PostMenu, - PostMenuItem, - PostMenuTrigger, - PostPopover, - PostPopoverTrigger, - PostRating, - PostTabs, - PostTabItem, - PostTabPanel, - PostTooltipTrigger, - PostTooltip, -} from '@swisspost/design-system-components-react/server'; +'use client'; -export default function Home() { - return ( - <> -

Design System Components

-

- Lorem ipsum dolor sit amet consectetur adipisicing elit. Ea debitis ex rem minus! Ut - mollitia deserunt iure impedit. Enim, officia. Fugiat, cupiditate repellat? Excepturi est - iusto suscipit, omnis iste laboriosam! -

- -

Accordion

- - - Titulum 1 -
-

Contentus momentus vero siteos et accusam iretea et justo.

-
-
- - - Titulum 2 -
-

Contentus momentus vero siteos et accusam iretea et justo.

-
-
- - - Titulum 3 -
-

Contentus momentus vero siteos et accusam iretea et justo.

-
-
-
- -

Avatar

- - -

Banner

- -

Contentus momentus vero siteos et accusam iretea et justo.

-
+import { usePathname, useRouter } from 'next/navigation'; +import { PostTabs, PostTabItem } from '@swisspost/design-system-components-react'; +import { useEffect, useRef } from 'react'; -

Card Control

- +export default function Page() { + const pathname = usePathname(); + const router = useRouter(); + const tabsRef = useRef(null); -

Close Button

- Close button + // Map pathname to tab name + const getActiveTab = () => { + if (pathname === '/ssr/products') return 'products'; + if (pathname === '/ssr/contact') return 'contact'; + return 'home'; + }; -

Collapsible

- {/* The aria attributes need to be defined on the button already, otherwise nextjs will report a hydration error */} - - - - - -

- Contentus momentus vero siteos et accusam iretea et justo. -

-
- -

Icon

- - -

Linkarea

- -
-
-
Titulum
-

Contentus momentus vero siteos et accusam iretea et justo.

- - Ligilo teksto - - - Pli da ligo - -
-
-
+ // Update activeTab when route changes + useEffect(() => { + const activeTab = getActiveTab(); + if (tabsRef.current) { + tabsRef.current.setAttribute('active-tab', activeTab); + } + }, [pathname]); -

Menu

- {/* Throws Hydration Errors */} - - - - - - - - - Example 2 - -
Example 3
-
-
-
-

Popover

- - {/* The aria-expanded attribute need to be defined on the trigger already, otherwise nextjs will report a hydration error */} - - - +

Navigation Tabs Test

+

Testing navigation mode with Next.js routing

+ + -

Optional title

-

- A longer message that needs more time to read. Links are also - possible. -

-
- -

Rating

- - -

Tabs - Panel Variant

- - Unua langeto - Dua langeto - Tria langeto - - - Jen la enhavo de la unua langeto. Defaŭlte ĝi montriĝas komence. - - - Jen la enhavo de la dua langeto. Defaŭlte ĝi estas kaŝita komence. - - - Jen la enhavo de la tria langeto. Defaŭlte ĝi ankaŭ estas kaŝita komence. - - - -

Tabs - Navigation Variant

- - - First + + { + e.preventDefault(); + router.push('/ssr'); + }} + > + Home + - - Second + + { + e.preventDefault(); + router.push('/ssr/products'); + }} + > + Products + - - Third + + { + e.preventDefault(); + router.push('/ssr/contact'); + }} + > + Contact + -

Tag

- -

Tooltip

- - {/* The aria-describedby attribute need to be defined on the button already, otherwise we'll get a hydration error */} - - - - Hi there 👋 - - +
+

Current Route: {pathname}

+

Active Tab: {getActiveTab()}

+
+ + {/* Page Content */} +
+ {pathname === '/ssr' && ( + <> +

Home Page

+

Welcome to the home page. This is the default view.

+ + )} + + {pathname === '/ssr/products' && ( + <> +

Products Page

+

Browse our product catalog here.

+
    +
  • Product A
  • +
  • Product B
  • +
  • Product C
  • +
+ + )} + + {pathname === '/ssr/contact' && ( + <> +

Contact Page

+

Get in touch with us.

+
+
+ +
+
+ +
+ +
+ + )} +
+ + {/* Debug/Test Section */} +
+

Test Navigation

+

Use these buttons to test programmatic navigation:

+
+ + + +
+

+ 💡 Open browser console to see postChange events and aria-current updates +

+
+
); -} +} \ No newline at end of file diff --git a/packages/nextjs-integration/src/app/ssr/products/page.tsx b/packages/nextjs-integration/src/app/ssr/products/page.tsx new file mode 100644 index 0000000000..1164b1e5e6 --- /dev/null +++ b/packages/nextjs-integration/src/app/ssr/products/page.tsx @@ -0,0 +1,11 @@ +'use client'; + +import dynamic from 'next/dynamic'; + +const CSRPage = dynamic(() => import('../page'), { + ssr: false, +}); + +export default function ProductsPage() { + return ; +} \ No newline at end of file From c44a6ed3cceb66ce165f234989564514e4a18226 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 12 Nov 2025 06:25:19 +0100 Subject: [PATCH 101/127] Daily discussion --- packages/components/src/index.html | 149 +++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/packages/components/src/index.html b/packages/components/src/index.html index 2f35dcf714..abf746d5f2 100644 --- a/packages/components/src/index.html +++ b/packages/components/src/index.html @@ -198,5 +198,154 @@

Components package playground


+ +

Tabs Anchor Navigation

+ +
+

📌 The Problem

+
    +
  • ✅ URL changes to #second
  • +
  • ✅ Content scrolls
  • +
  • ❌ BUT tab styling still shows "First" is active
  • +
+

Developers see a mismatch - content shows second page, but styling shows first is selected

+
+ +
+

👇 Try It Now:

+ + First page + Second page + Third page + +
+ + +
+

📄 First Page Content

+

This is the content for the first page. When you click the "First page" tab, the URL should be #first and this section should show.

+

Notice: The tab styling correctly shows "First" is active ✅

+
+ + + + + + +
+ +
+

🔍 Why Does This Happen?

+

No one updates the activeTab property when the user clicks a tab link

+

Current flow:

+
+Developer uses tabs → User clicks link → Browser changes URL ✅ → +Content scrolls ✅ → Component doesn't know about the change ❌ → +Styling doesn't update ❌ +
+
+
+ +
+

✅ The Solution

+

Consumers handle routing via their framework or plain anchors:

+ +

📌 With Framework (React, Angular, Vue):

+
+// Framework handles route detection automatically
+// Component receives activeTab from router
+
+<post-tabs activeTab={currentRoute} 
+            label="Navigation tabs">
+  
+  <post-tab-item name="first">
+    <a href="#first">First page</a>
+  </post-tab-item>
+  
+  <post-tab-item name="second">
+    <a href="#second">Second page</a>
+  </post-tab-item>
+  
+  <post-tab-item name="third">
+    <a href="#third">Third page</a>
+  </post-tab-item>
+  
+</post-tabs>
+
+// That's it! Framework handles everything else.
+    
+ +

📌 Without Framework (Vanilla JS):

+
+HTML:
+─────────────────────────────────────────────────
+
+<post-tabs id="myTabs" active-tab="first">
+  <post-tab-item name="first">
+    <a href="#first">First page</a>
+  </post-tab-item>
+  <post-tab-item name="second">
+    <a href="#second">Second page</a>
+  </post-tab-item>
+</post-tabs>
+
+
+JAVASCRIPT:
+─────────────────────────────────────────────────
+
+const tabs = document.getElementById('myTabs');
+
+// Only needed if NOT using a framework router
+window.addEventListener('hashchange', () => {
+  const hash = window.location.hash.slice(1);
+  tabs.setAttribute('active-tab', hash);
+});
+    
+ +

+ ✅ Result: When activeTab prop changes, + component automatically updates the styling +

+
+
+ +
+

🎯 QUESTION

+

Should developers be responsible for updating activeTab when the URL changes or should component have this functionality "from the box" when using with plain anchors?

+
From bcef2c992ed1ac71b429c227572e79add3d3f13d Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Wed, 12 Nov 2025 12:39:34 +0100 Subject: [PATCH 102/127] refactored the tabs --- packages/components/src/components.d.ts | 12 +- .../post-tab-item/post-tab-item.tsx | 21 +-- .../src/components/post-tabs/post-tabs.tsx | 96 ++++++----- .../src/components/post-tabs/readme.md | 17 +- packages/components/src/index.html | 149 ------------------ .../stories/components/tabs/tabs.stories.ts | 59 ++++--- 6 files changed, 89 insertions(+), 265 deletions(-) diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 3ec5b8c5f7..886660975b 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -491,7 +491,7 @@ export namespace Components { } interface PostTabs { /** - * The name of the tab that is initially active. If not specified, it defaults to the first tab. **Panel mode**: Changing this value after initialization has no effect. **Navigation mode**: This should be updated by the routing framework to reflect the current page on each navigation. The component will automatically sync the active state when this prop changes. + * The name of the tab that is initially active. Changing this value after initialization has no effect. If not specified, defaults to the first tab. */ "activeTab"?: string; /** @@ -500,11 +500,11 @@ export namespace Components { */ "fullWidth": boolean; /** - * The accessible label for the tabs component for navigation variant. + * The accessible label for the tabs component in navigation mode. */ "label": string; /** - * Shows the panel with the given name and selects its associated tab. In navigation mode, only updates the active tab state. Any other panel that was previously shown becomes hidden and its associated tab is unselected. + * Shows the panel with the given name and selects its associated tab. Any other panel that was previously shown becomes hidden and its associated tab is unselected. */ "show": (tabName: string) => Promise; } @@ -1392,7 +1392,7 @@ declare namespace LocalJSX { } interface PostTabs { /** - * The name of the tab that is initially active. If not specified, it defaults to the first tab. **Panel mode**: Changing this value after initialization has no effect. **Navigation mode**: This should be updated by the routing framework to reflect the current page on each navigation. The component will automatically sync the active state when this prop changes. + * The name of the tab that is initially active. Changing this value after initialization has no effect. If not specified, defaults to the first tab. */ "activeTab"?: string; /** @@ -1401,11 +1401,11 @@ declare namespace LocalJSX { */ "fullWidth"?: boolean; /** - * The accessible label for the tabs component for navigation variant. + * The accessible label for the tabs component in navigation mode. */ "label": string; /** - * An event emitted after the active tab changes, when the fade in transition of its associated panel is finished. The payload is the name of the newly active tab. + * An event emitted after the active tab changes, when the fade in transition of its associated panel is finished. The payload is the name of the newly active tab. Only emitted in panel mode. */ "onPostChange"?: (event: PostTabsCustomEvent) => void; } diff --git a/packages/components/src/components/post-tab-item/post-tab-item.tsx b/packages/components/src/components/post-tab-item/post-tab-item.tsx index 3cb25bdf65..9663f2c026 100644 --- a/packages/components/src/components/post-tab-item/post-tab-item.tsx +++ b/packages/components/src/components/post-tab-item/post-tab-item.tsx @@ -34,8 +34,6 @@ export class PostTabItem { this.mutationObserver.observe(this.host, { childList: true, subtree: true, - attributes: true, - attributeFilter: ['class'], }); } @@ -45,7 +43,6 @@ export class PostTabItem { componentDidLoad() { this.checkNavigationMode(); - this.syncAriaCurrent(); } disconnectedCallback() { @@ -56,7 +53,6 @@ export class PostTabItem { private handleMutations() { this.checkNavigationMode(); - this.syncAriaCurrent(); } private checkNavigationMode() { @@ -64,21 +60,6 @@ export class PostTabItem { this.isNavigationMode = hasAnchor; } - private syncAriaCurrent() { - if (!this.isNavigationMode) return; - - const anchor = this.host.querySelector('a'); - if (!anchor) return; - - const isActive = this.host.classList.contains('active'); - - if (isActive) { - anchor.setAttribute('aria-current', 'page'); - } else { - anchor.removeAttribute('aria-current'); - } - } - render() { return ( ); } -} +} \ No newline at end of file diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 2522e82115..41e9ca6e38 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -40,23 +40,11 @@ export class PostTabs { /** * The name of the tab that is initially active. - * If not specified, it defaults to the first tab. - * - * **Panel mode**: Changing this value after initialization has no effect. - * - * **Navigation mode**: This should be updated by the routing framework - * to reflect the current page on each navigation. The component will - * automatically sync the active state when this prop changes. + * Changing this value after initialization has no effect. + * If not specified, defaults to the first tab. */ @Prop() readonly activeTab?: string; - @Watch('activeTab') - handleActiveTabChange(newTab: string) { - if (this.isLoaded && this.isNavigationMode && newTab && newTab !== this.currentActiveTab?.name) { - void this.show(newTab); - } - } - /** * When set to true, this property allows the tabs container to span the * full width of the screen, from edge to edge. @@ -64,7 +52,7 @@ export class PostTabs { @Prop({ reflect: true }) fullWidth: boolean = false; /** - * The accessible label for the tabs component for navigation variant. + * The accessible label for the tabs component in navigation mode. */ @Prop() readonly label!: string; @@ -78,6 +66,7 @@ export class PostTabs { /** * An event emitted after the active tab changes, when the fade in transition of its associated panel is finished. * The payload is the name of the newly active tab. + * Only emitted in panel mode. */ @Event() postChange: EventEmitter; @@ -89,14 +78,18 @@ export class PostTabs { this.setupContentObserver(); this.validateLabel(); - // Unified logic for both modes with priority order - const tabToActivate = - this.activeTab || - this.findActiveNavigationTab()?.name || - this.tabs[0]?.name; - - if (tabToActivate) { - void this.show(tabToActivate); + if (this.isNavigationMode) { + // In navigation mode, activate the tab with aria-current="page" + const activeTab = this.findActiveNavigationTab(); + if (activeTab) { + this.activateTab(activeTab); + } + } else { + // In panel mode, use activeTab prop or default to first tab + const tabToActivate = this.activeTab || this.tabs[0]?.name; + if (tabToActivate) { + void this.show(tabToActivate); + } } } @@ -110,7 +103,6 @@ export class PostTabs { this.hiding = null; } - // Clean up content observer if (this.contentObserver) { this.contentObserver.disconnect(); } @@ -118,10 +110,10 @@ export class PostTabs { private setupContentObserver() { const config: MutationObserverInit = { - childList: true, // Watch for child elements being added/removed - subtree: true, // Watch all descendants - attributes: true, // Watch for attribute changes - attributeFilter: ['data-navigation-mode'] // Only watch navigation mode changes + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['data-navigation-mode', 'aria-current'] }; this.contentObserver = new MutationObserver(this.handleContentChange.bind(this)); @@ -129,32 +121,45 @@ export class PostTabs { } private handleContentChange(mutations: MutationRecord[]) { - // Check if any mutations affect navigation mode const shouldRedetect = mutations.some(mutation => { - // Child nodes added/removed (new tab items or anchor elements) if (mutation.type === 'childList') { return mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0; } - // Navigation mode attribute changed if (mutation.type === 'attributes' && mutation.attributeName === 'data-navigation-mode') { return true; } return false; }); + // Handle aria-current changes in navigation mode + const ariaCurrentChanged = mutations.some( + mutation => mutation.type === 'attributes' && mutation.attributeName === 'aria-current' + ); + + if (ariaCurrentChanged && this.isNavigationMode) { + const activeTab = this.findActiveNavigationTab(); + if (activeTab && activeTab !== this.currentActiveTab) { + this.activateTab(activeTab); + } + } + if (shouldRedetect) { - // Re-detect mode and re-enable tabs if needed const previousMode = this.isNavigationMode; this.detectMode(); - // If mode changed, re-initialize if (previousMode !== this.isNavigationMode) { this.enableTabs(); - // Re-initialize active tab after mode change - const activeTab = this.activeTab || this.findActiveNavigationTab()?.name || this.tabs[0]?.name; - if (activeTab) { - void this.show(activeTab); + if (this.isNavigationMode) { + const activeTab = this.findActiveNavigationTab(); + if (activeTab) { + this.activateTab(activeTab); + } + } else { + const tabToActivate = this.activeTab || this.tabs[0]?.name; + if (tabToActivate) { + void this.show(tabToActivate); + } } } } @@ -184,7 +189,6 @@ export class PostTabs { /** * Shows the panel with the given name and selects its associated tab. - * In navigation mode, only updates the active tab state. * Any other panel that was previously shown becomes hidden and its associated tab is unselected. */ @Method() @@ -206,12 +210,6 @@ export class PostTabs { this.activateTab(newTab); - // In navigation mode, we don't need to handle panels - if (this.isNavigationMode) { - if (this.isLoaded) this.postChange.emit(this.currentActiveTab.name); - return; - } - // if a panel is currently being displayed, remove it from the view and complete the associated animation if (this.showing) { this.showing.effect['target'].style.display = 'none'; @@ -224,9 +222,7 @@ export class PostTabs { // wait for any hiding animation to complete before showing the selected tab if (this.hiding) await this.hiding.finished; - if (!this.isNavigationMode) { - this.showSelectedPanel(); - } + this.showSelectedPanel(); // wait for any display animation to complete for the returned promise to fully resolve if (this.showing) await this.showing.finished; @@ -246,18 +242,18 @@ export class PostTabs { } private enableTabs() { - // Prevent early call before detectMode() if (!this.isLoaded) return; - if (!this.tabs) return; this.tabs.forEach(async tab => { await componentOnReady(tab); + // In navigation mode, navigation is handled by the consumer's routing if (this.isNavigationMode) { return; } + // Panel mode: set up ARIA relationships and event handlers if (tab.getAttribute('aria-controls')) return; const tabPanel = this.getPanel(tab.name); @@ -373,4 +369,4 @@ export class PostTabs { ); } -} +} \ No newline at end of file diff --git a/packages/components/src/components/post-tabs/readme.md b/packages/components/src/components/post-tabs/readme.md index 758d35c4b6..5432b65823 100644 --- a/packages/components/src/components/post-tabs/readme.md +++ b/packages/components/src/components/post-tabs/readme.md @@ -7,18 +7,18 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| -------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | -| `activeTab` | `active-tab` | The name of the tab that is initially active. If not specified, it defaults to the first tab. **Panel mode**: Changing this value after initialization has no effect. **Navigation mode**: This should be updated by the routing framework to reflect the current page on each navigation. The component will automatically sync the active state when this prop changes. | `string` | `undefined` | -| `fullWidth` | `full-width` | When set to true, this property allows the tabs container to span the full width of the screen, from edge to edge. | `boolean` | `false` | -| `label` _(required)_ | `label` | The accessible label for the tabs component for navigation variant. | `string` | `undefined` | +| Property | Attribute | Description | Type | Default | +| -------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ----------- | +| `activeTab` | `active-tab` | The name of the tab that is initially active. Changing this value after initialization has no effect. If not specified, defaults to the first tab. | `string` | `undefined` | +| `fullWidth` | `full-width` | When set to true, this property allows the tabs container to span the full width of the screen, from edge to edge. | `boolean` | `false` | +| `label` _(required)_ | `label` | The accessible label for the tabs component in navigation mode. | `string` | `undefined` | ## Events -| Event | Description | Type | -| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| `postChange` | An event emitted after the active tab changes, when the fade in transition of its associated panel is finished. The payload is the name of the newly active tab. | `CustomEvent` | +| Event | Description | Type | +| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| `postChange` | An event emitted after the active tab changes, when the fade in transition of its associated panel is finished. The payload is the name of the newly active tab. Only emitted in panel mode. | `CustomEvent` | ## Methods @@ -26,7 +26,6 @@ ### `show(tabName: string) => Promise` Shows the panel with the given name and selects its associated tab. -In navigation mode, only updates the active tab state. Any other panel that was previously shown becomes hidden and its associated tab is unselected. #### Parameters diff --git a/packages/components/src/index.html b/packages/components/src/index.html index abf746d5f2..2f35dcf714 100644 --- a/packages/components/src/index.html +++ b/packages/components/src/index.html @@ -198,154 +198,5 @@

Components package playground


- -

Tabs Anchor Navigation

- -
-

📌 The Problem

-
    -
  • ✅ URL changes to #second
  • -
  • ✅ Content scrolls
  • -
  • ❌ BUT tab styling still shows "First" is active
  • -
-

Developers see a mismatch - content shows second page, but styling shows first is selected

-
- -
-

👇 Try It Now:

- - First page - Second page - Third page - -
- - -
-

📄 First Page Content

-

This is the content for the first page. When you click the "First page" tab, the URL should be #first and this section should show.

-

Notice: The tab styling correctly shows "First" is active ✅

-
- - - - - - -
- -
-

🔍 Why Does This Happen?

-

No one updates the activeTab property when the user clicks a tab link

-

Current flow:

-
-Developer uses tabs → User clicks link → Browser changes URL ✅ → -Content scrolls ✅ → Component doesn't know about the change ❌ → -Styling doesn't update ❌ -
-
-
- -
-

✅ The Solution

-

Consumers handle routing via their framework or plain anchors:

- -

📌 With Framework (React, Angular, Vue):

-
-// Framework handles route detection automatically
-// Component receives activeTab from router
-
-<post-tabs activeTab={currentRoute} 
-            label="Navigation tabs">
-  
-  <post-tab-item name="first">
-    <a href="#first">First page</a>
-  </post-tab-item>
-  
-  <post-tab-item name="second">
-    <a href="#second">Second page</a>
-  </post-tab-item>
-  
-  <post-tab-item name="third">
-    <a href="#third">Third page</a>
-  </post-tab-item>
-  
-</post-tabs>
-
-// That's it! Framework handles everything else.
-    
- -

📌 Without Framework (Vanilla JS):

-
-HTML:
-─────────────────────────────────────────────────
-
-<post-tabs id="myTabs" active-tab="first">
-  <post-tab-item name="first">
-    <a href="#first">First page</a>
-  </post-tab-item>
-  <post-tab-item name="second">
-    <a href="#second">Second page</a>
-  </post-tab-item>
-</post-tabs>
-
-
-JAVASCRIPT:
-─────────────────────────────────────────────────
-
-const tabs = document.getElementById('myTabs');
-
-// Only needed if NOT using a framework router
-window.addEventListener('hashchange', () => {
-  const hash = window.location.hash.slice(1);
-  tabs.setAttribute('active-tab', hash);
-});
-    
- -

- ✅ Result: When activeTab prop changes, - component automatically updates the styling -

-
-
- -
-

🎯 QUESTION

-

Should developers be responsible for updating activeTab when the URL changes or should component have this functionality "from the box" when using with plain anchors?

-
diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.ts b/packages/documentation/src/stories/components/tabs/tabs.stories.ts index 1c53ad22ca..12e06c9fd0 100644 --- a/packages/documentation/src/stories/components/tabs/tabs.stories.ts +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.ts @@ -7,7 +7,6 @@ import { MetaComponent } from '@root/types'; const meta: MetaComponent = { @@ -26,7 +25,7 @@ const meta: MetaComponent

If you attempt (anchors + panels), the component will throw an error.

', + description: 'Select between panels variant (content sections) or navigation variant (page navigation).

If you attempt to mix modes (anchors + panels), the component will throw an error.

', control: 'radio', options: ['panels', 'navigation'], table: { @@ -46,18 +45,6 @@ const meta: MetaComponent @@ -141,14 +125,14 @@ function renderNavigationVariant( `; } + // Default navigation example - first link is active return html` - First page + First page Second page @@ -218,23 +202,20 @@ function renderPanelsVariant( function renderTabs(args: Partial) { const variant = args.variant || 'panels'; - const activeTab = variant === 'navigation' ? args.activeTabNavigation : args.activeTabPanels; return variant === 'navigation' - ? renderNavigationVariant(activeTab, args.fullWidth, args.label, args['slots-default'] || '') - : renderPanelsVariant(activeTab, args.fullWidth, args['slots-default'] || '', args['slots-panels'] || ''); + ? renderNavigationVariant(args.fullWidth, args.label, args['slots-default'] || '') + : renderPanelsVariant(args.activeTabPanels, args.fullWidth, args['slots-default'] || '', args['slots-panels'] || ''); } // STORIES type Story = StoryObj; @@ -259,7 +240,13 @@ export const NavigationVariant: Story = { layout: 'fullscreen', docs: { description: { - story: 'Navigation variant is for page navigation. When tab items contain `` elements, the component renders as semantic navigation. Perfect for sub-navigation menus.', + story: `Navigation variant is for page navigation. When tab items contain \`\` elements, the component renders as semantic \`