Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
71e02b0
feat(component): implement `anchor-navigation` for `<post-tabs>`
alionazherdetska Sep 30, 2025
fa1c06d
improved the render
alionazherdetska Sep 30, 2025
20cc211
changed the logic for assigning aria current
alionazherdetska Sep 30, 2025
0944082
small changes
alionazherdetska Sep 30, 2025
ba0fa35
changed the docs
alionazherdetska Sep 30, 2025
cc36657
fixed the `Enter` button
alionazherdetska Sep 30, 2025
a51883b
reverted some redundant changes
alionazherdetska Sep 30, 2025
428f9bc
changed some PostTabHeader to PostTabItem
alionazherdetska Sep 30, 2025
ec120a5
some adjustments
alionazherdetska Sep 30, 2025
f279029
removed redundant code
alionazherdetska Sep 30, 2025
10aa194
removed redundant code
alionazherdetska Sep 30, 2025
0817546
removed comments
alionazherdetska Sep 30, 2025
2d1daa3
removed comments
alionazherdetska Sep 30, 2025
d2e814d
changed the naming in the test files
alionazherdetska Sep 30, 2025
2a468b0
added initial docs
alionazherdetska Oct 1, 2025
8283f36
adjusted the docs
alionazherdetska Oct 1, 2025
93ca97a
small changes
alionazherdetska Oct 1, 2025
8435960
added e2e tests
alionazherdetska Oct 1, 2025
8a80ffe
removed redundant test
alionazherdetska Oct 1, 2025
b0d7a46
changed the docs
alionazherdetska Oct 1, 2025
8cb61d0
changed the docs
alionazherdetska Oct 1, 2025
6fe0429
changed the name slots
alionazherdetska Oct 1, 2025
b50cef5
updated the docs
alionazherdetska Oct 1, 2025
0aa648a
chnaged the docs
alionazherdetska Oct 1, 2025
c479813
changed the names of panels
alionazherdetska Oct 1, 2025
6625782
removed redundant code
alionazherdetska Oct 1, 2025
8724a4d
adjusted the tests
alionazherdetska Oct 1, 2025
d017089
adjusted the tabs
alionazherdetska Oct 1, 2025
2bc088e
removed redundant code
alionazherdetska Oct 1, 2025
0a9fad4
removed redundant code
alionazherdetska Oct 1, 2025
d3e2878
fixed e2e tests
alionazherdetska Oct 1, 2025
56c570e
fixed linting
alionazherdetska Oct 1, 2025
282820b
fixed the stories
alionazherdetska Oct 1, 2025
900434e
code style editing
alionazherdetska Oct 6, 2025
4c07f68
shifted the data-attribute assigning
alionazherdetska Oct 7, 2025
c2373ae
cleaned up animation
alionazherdetska Oct 7, 2025
bfe512f
implemednted the mutation observer
alionazherdetska Oct 7, 2025
93014cf
fixed `e2e` tests
alionazherdetska Oct 7, 2025
e72df63
renamed all instances of post-tab-header to post-tab-item
alionazherdetska Oct 7, 2025
ef1dede
added angular e2e tests
alionazherdetska Oct 7, 2025
17ca32c
removed redundant comments
alionazherdetska Oct 7, 2025
a4c0c65
reverted the changes to tabs.stories
alionazherdetska Oct 7, 2025
a096e7d
removed redundant code
alionazherdetska Oct 7, 2025
7e3d4ef
fixed the tests
alionazherdetska Oct 8, 2025
d9e1540
added a changeset
alionazherdetska Oct 8, 2025
2f8bdaf
added a changeset
alionazherdetska Oct 8, 2025
1eac65d
fixed the changeset
alionazherdetska Oct 8, 2025
863ea26
fixed the changeset
alionazherdetska Oct 8, 2025
9c14124
Merge branch 'main' into feat/tabs-anchor-navigation
alionazherdetska Oct 8, 2025
1210ef5
fixed linting
alionazherdetska Oct 8, 2025
d396888
removed unused var
alionazherdetska Oct 8, 2025
970c0c2
fixed angular tests
alionazherdetska Oct 8, 2025
f82a15a
clean up
alionazherdetska Oct 8, 2025
94de5d9
fixed tests
alionazherdetska Oct 8, 2025
b3a046b
chnaged the docs
alionazherdetska Oct 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .changeset/tabs-anchor-navigation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
"@swisspost/design-system-components": major
"@swisspost/design-system-components-react": major
"@swisspost/design-system-components-angular": major
---

Refactored `<post-tabs>` component:
- Renamed `post-tab-header` component to `post-tab-item`
- Renamed `panel` property to `name` in `post-tab-item` component
- Renamed `name` property to `for` in `post-tab-panel` component
- Renamed `activePanel` property to `activeTab` in `post-tabs` component
- Changed slot structure: `post-tab-item` elements now use the default slot and `post-tab-panel` elements use the `panels` slot

BEFORE:

```html
<post-tabs active-panel="first">
<post-tab-header panel="first">First tab</post-tab-header>
<post-tab-header panel="second">Second tab</post-tab-header>
<post-tab-header panel="third">Third tab</post-tab-header>

<post-tab-panel name="first">
This is the content of the first tab.
</post-tab-panel>
<post-tab-panel name="second">
This is the content of the second tab.
</post-tab-panel>
<post-tab-panel name="third">
This is the content of the third tab.
</post-tab-panel>
</post-tabs>
```

AFTER:

```html
<post-tabs active-tab="first">
<post-tab-item name="first">First tab</post-tab-item>
<post-tab-item name="second">Second tab</post-tab-item>
<post-tab-item name="third">Third tab</post-tab-item>

<post-tab-panel for="first" slot="panels">
This is the content of the first tab.
</post-tab-panel>
<post-tab-panel for="second" slot="panels">
This is the content of the second tab.
</post-tab-panel>
<post-tab-panel for="third" slot="panels">
This is the content of the third tab.
</post-tab-panel>
</post-tabs>
```
6 changes: 6 additions & 0 deletions .changeset/tabs-navigation-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@swisspost/design-system-components": minor
"@swisspost/design-system-documentation": minor
---

Added navigation variant to the `post-tabs` component, enabling anchor-based navigation. The component now automatically detects whether `post-tab-item` elements contain anchor links and switches between panels and navigation variants accordingly.
1 change: 1 addition & 0 deletions packages/components-angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"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",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as Components from '@swisspost/design-system-components/dist';
import * as Components from '@swisspost/design-system-components/loader';

const COMPONENT_TAG_NAMES = Object.keys(Components)
.filter(c => /^Post([A-Z][a-z]+)+$/.test(c))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
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');
});
});

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]);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@

// When a command from ./commands is ready to use, import with `import './commands'` syntax
import './commands';
import 'cypress-axe';
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ 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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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: [
Expand All @@ -15,6 +16,7 @@ import { CardControlComponent } from './routes/card-control/card-control.compone
AppRoutingModule,
FormsModule,
CardControlComponent,
TabsComponent,
],
declarations: [AppComponent],
providers: [providePostComponents()],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,17 @@ <h2>Post Rating</h2>
<div class="my-24">
<h2>Post Tabs</h2>
<post-tabs>
<post-tab-header panel="unua">Unua langeto</post-tab-header>
<post-tab-header panel="dua">Dua langeto</post-tab-header>
<post-tab-header panel="tria">Tria langeto</post-tab-header>
<post-tab-item name="unua">Unua langeto</post-tab-item>
<post-tab-item name="dua">Dua langeto</post-tab-item>
<post-tab-item name="tria">Tria langeto</post-tab-item>

<post-tab-panel name="unua">
<post-tab-panel for="unua">
Jen la enhavo de la unua langeto. Defaŭlte ĝi montriĝas komence.
</post-tab-panel>
<post-tab-panel name="dua">
<post-tab-panel for="dua">
Jen la enhavo de la dua langeto. Defaŭlte ĝi estas kaŝita komence.
</post-tab-panel>
<post-tab-panel name="tria">
<post-tab-panel for="tria">
Jen la enhavo de la tria langeto. Defaŭlte ĝi ankaŭ estas kaŝita komence.
</post-tab-panel>
</post-tabs>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
PostPopovercontainer,
PostRating,
PostTabs,
PostTabHeader,
PostTabItem,
PostTabPanel,
PostTooltipTrigger,
} from 'components';
Expand All @@ -36,7 +36,7 @@ import {
PostPopovercontainer,
PostRating,
PostTabs,
PostTabHeader,
PostTabItem,
PostTabPanel,
PostTooltipTrigger,
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<h2>Tabs - Panel Variant</h2>
<post-tabs>
<post-tab-item name="first">First</post-tab-item>
<post-tab-item name="second">Second</post-tab-item>
<post-tab-item name="third">Third</post-tab-item>
<post-tab-panel for="first">
<p>Content of first tab</p>
</post-tab-panel>
<post-tab-panel for="second">
<p>Content of second tab</p>
</post-tab-panel>
<post-tab-panel for="third">
<p>Content of third tab</p>
</post-tab-panel>
</post-tabs>

<h2>Tabs - Navigation Variant</h2>
<post-tabs>
<post-tab-item name="nav-first">
<a href="#first">First</a>
</post-tab-item>
<post-tab-item name="nav-second">
<a href="#second">Second</a>
</post-tab-item>
<post-tab-item name="nav-third">
<a href="#third">Third</a>
</post-tab-item>
</post-tabs>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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 {}
Loading
Loading