-
Notifications
You must be signed in to change notification settings - Fork 21
feat(component): add anchor navigation functionality to <post-tab> component
#6350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
π¦ Changeset detectedLatest commit: a49862c The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Related Previews |
packages/components/src/components/post-tab-item/post-tab-item.tsx
Outdated
Show resolved
Hide resolved
β¦.tsx Co-authored-by: AlizΓ© Debray <33580481+alizedebray@users.noreply.github.com>
β¦spost/design-system into feat/tabs-anchor-navigation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous Subnavigation component had styles. Don't we want the anchor navigation to also have styles? Since it's sharing the post-tabs component, I'd have expected it to have the same styles
| role={isPanelMode ? 'tab' : undefined} | ||
| data-version={version} | ||
| data-navigation-mode={this.isNavigationMode.toString()} | ||
| aria-selected={isPanelMode ? 'false' : undefined} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In anchor mode, the active link should have aria-current="page"
|
|
||
| render() { | ||
| const tabsRole = this.isNavigationMode ? undefined : 'tablist'; | ||
| const ariaLabel = this.isNavigationMode ? 'Tabs navigation' : undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aria-label should be localized so that should be added as a prop
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR refactors the post-tabs component to support two distinct usage modes: panels variant (for tabbed content sections) and navigation variant (for anchor-based page navigation). The refactoring includes renaming post-tab-header to post-tab-item, updating property names for consistency, and implementing automatic mode detection based on the presence of anchor elements.
- Renamed
post-tab-headercomponent topost-tab-itemwith updated properties - Changed property names across components for better clarity (
activePanelβactiveTab,panelβname,nameβfor) - Added automatic mode detection and dual-mode support (panels vs navigation)
Reviewed Changes
Copilot reviewed 26 out of 28 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/components/src/components/post-tabs/post-tabs.tsx | Core refactoring with mode detection logic and navigation support |
| packages/components/src/components/post-tab-item/post-tab-item.tsx | New component replacing post-tab-header with navigation mode detection |
| packages/components/src/components/post-tab-panel/post-tab-panel.tsx | Updated property from name to for and added slot attribute |
| packages/documentation/src/stories/components/tabs/tabs.stories.ts | Updated stories to demonstrate both variants with new API |
| packages/nextjs-integration/src/app/ssr/page.tsx | Updated integration example with new component names and properties |
| packages/components/cypress/e2e/tabs.cy.ts | Comprehensive test updates covering both modes and accessibility |
| Various documentation files | Updated all documentation references from old to new API |
| } | ||
|
|
||
| // Clean up content observer | ||
| this.contentObserver.disconnect(); |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The contentObserver may be undefined if setupContentObserver() throws an error or if disconnectedCallback() is called before componentDidLoad() completes. Add a null check before calling disconnect().
| this.contentObserver.disconnect(); | |
| if (this.contentObserver) { | |
| this.contentObserver.disconnect(); | |
| } |
| private enableTabs() { | ||
| // Prevent early call before detectMode() | ||
| if (!this.isLoaded) return; |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The guard if (!this.isLoaded) causes enableTabs() to exit early when called from componentDidLoad() at line 65, before this.isLoaded = true is set at line 64. This means tabs won't be enabled on initial load. Move this.isLoaded = true before calling enableTabs() in componentDidLoad().
| if (this.isNavigationMode) { | ||
| if (this.isLoaded) this.postChange.emit(this.currentActiveTab.name); | ||
| return; | ||
| } |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In navigation mode, if this.isLoaded is false, the event won't be emitted. However, the method still returns, which means navigation tabs activated during initialization won't trigger the postChange event. This is inconsistent with panels mode behavior at line 202. Consider removing the if (this.isLoaded) check or handling both modes consistently.
| shadow: true, | ||
| }) | ||
| export class PostTabItem { | ||
| private mutationObserver = new MutationObserver(this.checkNavigationMode.bind(this)); |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MutationObserver callback binding is created during field initialization before the component instance is fully constructed. Consider initializing the observer in connectedCallback() to ensure proper binding context: this.mutationObserver = new MutationObserver(this.checkNavigationMode.bind(this));
| name: 'active-panel', | ||
| variant: { | ||
| name: 'variant', | ||
| description: 'Select between panels variant (content sections) or navigation variant (page navigation). <post-banner data-size="sm"><p>If you attempt to mix both variants(anchors + panels), the component will throw an error.</p></post-banner>', |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing space in 'variants(anchors' - should be 'variants (anchors'.
| description: 'Select between panels variant (content sections) or navigation variant (page navigation). <post-banner data-size="sm"><p>If you attempt to mix both variants(anchors + panels), the component will throw an error.</p></post-banner>', | |
| description: 'Select between panels variant (content sections) or navigation variant (page navigation). <post-banner data-size="sm"><p>If you attempt to mix both variants (anchors + panels), the component will throw an error.</p></post-banner>', |
|
|
||
| activeItem?.remove(); | ||
| activePanel?.remove(); |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The find() method returns undefined if no active item is found, but the type annotation declares it as HTMLPostTabItemElement | undefined. The subsequent check at line 272 guards against this, but the logic appears flawed - find() should use a proper predicate. The current implementation always returns the first item because the arrow function doesn't return the result of classList.contains('active').
| }); | ||
|
|
||
| it('should render the tabs container as nav element', () => { | ||
| cy.get('@tabs').find('nav[role="navigation"], nav').should('exist'); |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The selector nav[role=\"navigation\"], nav is redundant since <nav> elements have an implicit ARIA role of 'navigation'. The test should either check for just nav or verify that the role is properly set/omitted. Consider simplifying to cy.get('@tabs').find('nav').should('exist').
| cy.get('@tabs').find('nav[role="navigation"], nav').should('exist'); | |
| cy.get('@tabs').find('nav').should('exist'); |
| @@ -1,4 +1,4 @@ | |||
| import * as Components from '@swisspost/design-system-components/dist'; | |||
| import * as Components from '@swisspost/design-system-components/loader'; | |||
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The import path changed from dist to loader. This appears to be an unrelated change to the tabs refactoring. Ensure this import path change is intentional and doesn't break the component loading mechanism.
| import * as Components from '@swisspost/design-system-components/loader'; | |
| import * as Components from '@swisspost/design-system-components/dist'; |
|



π Description
Ticket: #5940
Breaking Changes
post-tab-headercomponent topost-tab-itempanelβnameinpost-tab-itemnameβforinpost-tab-panelactivePanelβactiveTabinpost-tabspanelsslotNew Features
<nav>element with proper ARIA attributesTesting
Documentation
Animation:
Note:
The controls table still shows both CSS Shadow Parts for both variants because Storybook automatically extracts these from the component's JSDoc
partannotations at build time, preventing any dynamic filtering based on the selected variant control.Unlike slots, which are manually defined in the stories configuration and support conditional visibility using the
ifproperty, CSS Shadow Parts are auto-generated from the component source code.π Preview Link
Tabs component
π Checklist