From 71e02b0a6e9485ce32cee08dc036752e719f7018 Mon Sep 17 00:00:00 2001 From: alionazherdetska Date: Tue, 30 Sep 2025 11:11:50 +0200 Subject: [PATCH 01/54] 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 02/54] 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 03/54] 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 04/54] 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 05/54] 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 06/54] 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 07/54] 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 08/54] 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 09/54] 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 10/54] 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 11/54] 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 12/54] 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 13/54] 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 14/54] 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 15/54] 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 `