Skip to content
Merged
Show file tree
Hide file tree
Changes from 62 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
c113112
chore(post-popovercontainer): remove animate-pop-in class references
hugomslv May 30, 2025
0c9b513
chore(post-popovercontainer): remove animate-pop-in class references
hugomslv Jun 4, 2025
cb0e11f
Merge branch 'main' into 5463-refactor-post-popovercontainer-animatio…
hugomslv Jun 11, 2025
89fc041
Merge branch 'main' into 5463-refactor-post-popovercontainer-animatio…
hugomslv Jun 13, 2025
dd84738
Merge branch 'main' into 5463-refactor-post-popovercontainer-animatio…
hugomslv Jul 7, 2025
58ea300
fix PR comments
hugomslv Jul 7, 2025
4ee5b71
Merge branch '5463-refactor-post-popovercontainer-animation-to-use-we…
hugomslv Jul 7, 2025
d881d5e
Update post-popovercontainer.tsx
hugomslv Jul 7, 2025
764dbf6
Merge branch 'main' into 5463-refactor-post-popovercontainer-animatio…
hugomslv Aug 8, 2025
b0d1335
assign `role=menuitem` to focusable children
myrta2302 Aug 15, 2025
2839a4f
move the role="menu" back to host
myrta2302 Aug 15, 2025
b2209a2
trials
myrta2302 Aug 17, 2025
d40e5a7
Merge branch 'main' into 5463-refactor-post-popovercontainer-animatio…
hugomslv Aug 18, 2025
69db18c
remove old pop-in animation
hugomslv Aug 18, 2025
8cf3b89
Merge branch 'main' into 5463-refactor-post-popovercontainer-animatio…
hugomslv Aug 18, 2025
75f13b9
add roles on popovercontainer show
myrta2302 Aug 19, 2025
1ac1ba9
remove test component
myrta2302 Aug 19, 2025
ce4bd25
revert files
myrta2302 Aug 19, 2025
cd0ebfb
lint issue
myrta2302 Aug 19, 2025
6e62b74
Merge branch 'main' into 5837-bug-accessibility-issues-with-post-menu…
myrta2302 Aug 19, 2025
3d2f5d7
add changeset
myrta2302 Aug 19, 2025
0ae6785
update roles setup on language switcher and options to imrpove acce…
myrta2302 Aug 19, 2025
a079fba
Merge branch '5837-bug-accessibility-issues-with-post-menu-trigger-an…
myrta2302 Aug 19, 2025
eace006
improve breadcrumb accessibility
myrta2302 Aug 19, 2025
9926d50
Update whole-crews-dream.md
myrta2302 Aug 19, 2025
8489cb9
Update whole-crews-dream.md
myrta2302 Aug 19, 2025
8319dec
replace the language-option test with a language-switch test
myrta2302 Aug 19, 2025
55d3c32
Update whole-crews-dream.md
myrta2302 Aug 19, 2025
77e7d72
Merge branch 'main' into 5837-bug-accessibility-issues-with-post-menu…
myrta2302 Aug 20, 2025
2c6605c
language switch remove obsolete roles and aria-label
myrta2302 Aug 21, 2025
0e8db36
corrected accessibility error
myrta2302 Aug 21, 2025
2348d20
language-switch test update
myrta2302 Aug 21, 2025
82ab83b
Merge branch 'main' into 5837-bug-accessibility-issues-with-post-menu…
myrta2302 Aug 21, 2025
b8bd682
udpate language switch test
myrta2302 Aug 21, 2025
7af9341
Merge branch '5837-bug-accessibility-issues-with-post-menu-trigger-an…
myrta2302 Aug 21, 2025
e3be8d9
Merge branch 'main' into 5837-bug-accessibility-issues-with-post-menu…
myrta2302 Aug 25, 2025
58b680b
fix e2e test
myrta2302 Aug 25, 2025
62b7649
Merge branch 'main' into 5837-bug-accessibility-issues-with-post-menu…
myrta2302 Aug 25, 2025
fd02a6d
Merge branch 'main' into 5837-bug-accessibility-issues-with-post-menu…
myrta2302 Aug 25, 2025
bfed3d7
Merge branch 'main' into 5463-refactor-post-popovercontainer-animatio…
hugomslv Aug 26, 2025
1f6bf00
Update packages/components/src/animations/pop-in.ts
hugomslv Aug 26, 2025
0fea9db
Update packages/components/src/animations/pop-in.ts
hugomslv Aug 26, 2025
5682c4e
Merge branch 'main' into 5837-bug-accessibility-issues-with-post-menu…
myrta2302 Aug 26, 2025
abc902e
add caption prop to menu
myrta2302 Aug 28, 2025
eb8db3e
removed aria-label again as obsolete
myrta2302 Aug 28, 2025
61db02f
review comments update
myrta2302 Aug 28, 2025
2e0189c
Merge branch 'main' into 5837-bug-accessibility-issues-with-post-menu…
myrta2302 Aug 29, 2025
36376d6
Merge branch '5463-refactor-post-popovercontainer-animation-to-use-we…
hugomslv Sep 2, 2025
5edd2e7
Merge branch 'main' into 5463-refactor-post-popovercontainer-animatio…
hugomslv Sep 16, 2025
6c340c1
Merge branch 'main' into 5837-bug-accessibility-issues-with-post-menu…
myrta2302 Sep 26, 2025
bd604c2
add label prop for post-menu
myrta2302 Sep 26, 2025
f88d679
update first open check
myrta2302 Sep 26, 2025
756185e
update postToggle event payload
myrta2302 Sep 29, 2025
5c19472
Merge branch 'main' into 5837-bug-accessibility-issues-with-post-menu…
myrta2302 Sep 29, 2025
8b419ca
update popovercontainer events
myrta2302 Sep 30, 2025
4358936
Merge remote-tracking branch 'origin/5463-refactor-post-popovercontai…
myrta2302 Sep 30, 2025
dacf4a7
update events
myrta2302 Oct 1, 2025
40f76b9
restore label and aria set
myrta2302 Oct 1, 2025
a62c847
updates first
myrta2302 Oct 1, 2025
1405f7c
remove console.logs
myrta2302 Oct 1, 2025
4af08a5
remove obsolete role
myrta2302 Oct 2, 2025
058f6bf
add changeset vast-onions-return.md
myrta2302 Oct 2, 2025
18eed87
update <post-popovercontainer> event names
myrta2302 Oct 6, 2025
b35cd7b
Merge branch 'main' into 6341-update-post-menu-and-post-popovecontain…
myrta2302 Oct 6, 2025
ea7a6df
remove postHide payload
myrta2302 Oct 6, 2025
697390d
update components.d.ts
myrta2302 Oct 6, 2025
fe2f062
minor
myrta2302 Oct 6, 2025
f9f15ad
review comments update
myrta2302 Oct 20, 2025
c156b1c
Merge branch 'main' into 6341-update-post-menu-and-post-popovecontain…
myrta2302 Oct 20, 2025
8c3e657
remove console.logs
myrta2302 Oct 20, 2025
6ee9b22
restore eventFrom decorators
myrta2302 Oct 20, 2025
af7bb1a
Merge branch 'main' into 6341-update-post-menu-and-post-popovecontain…
myrta2302 Oct 21, 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
5 changes: 5 additions & 0 deletions .changeset/vast-onions-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@swisspost/design-system-components': patch
---

Updated `<post-popovercontainer>` to emit events: postBeforeShow, postAfterShow, postHide, postBeforeToggle, and postAfterToggle. Updated `<post-menu>` and `<post-tooltip-trigger>` to consume the `onPostAfterToggle` event emitted by `<post-popovercontainer>`.
9 changes: 9 additions & 0 deletions .changeset/whole-crews-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@swisspost/design-system-components': patch
---

Updated `<post-menu>` and `<post-menu-item>` to prevent accessibility roles from being announced before the menu content is visible, and to ensure correct focus behavior on menu items during keyboard navigation when NVDA is running.

Removed the obsolete `role="menuitem"` from `<post-language-switch>. `<post-language-option>` now assigns `role="listitem"` in case of `variant="list"`, to ensure a correct reference relationship.

Updated the `<post-breadcrumbs>` component by removing an `aria-label` set on the trigger wrapper `<div>`, which was causing an accessibility error. Also added `role="none"` to the wrapper div to reflect its presentational purpose.
64 changes: 0 additions & 64 deletions packages/components/cypress/e2e/language-option.cy.ts

This file was deleted.

179 changes: 179 additions & 0 deletions packages/components/cypress/e2e/language-switch.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
const LANGUAGE_SWITCH_ID = 'decbb10c-2b39-4f47-b67d-337d8111a3ae';
const LANGUAGE_OPTION_ID = '3753ab83-a659-47b5-a2f2-ac452ec97916';

describe('post-language-switch', () => {
describe('list variant', () => {
beforeEach(() => {
cy.getComponent('language-switch', LANGUAGE_SWITCH_ID);
});

it('should render', () => {
cy.get('@language-switch').should('exist');
});

it('should have three language option items', () => {
cy.get('@language-switch').find('post-language-option').as('language-options');
cy.get('@language-options').should('have.length', 3);
});

it('should render as a list', () => {
cy.get('@language-switch').find('.post-language-switch-list').should('exist');
});

it('should not render dropdown elements', () => {
cy.get('@language-switch').find('post-menu-trigger').should('not.exist');
cy.get('@language-switch').find('post-menu').should('not.exist');
});

it('should have the correct ARIA attributes', () => {
cy.get('@language-switch')
.find('[role="list"]')
.should('have.attr', 'aria-label', 'Change the language')
.and('have.attr', 'aria-describedby');

cy.get('@language-switch')
.find('[aria-describedby]')
.should('contain.text', 'The currently selected language is English.');
});

it('should correctly set the active language option on click', () => {
cy.get('@language-switch')
.find('post-language-option[code="en"]')
.should('have.attr', 'active');
cy.get('@language-switch')
.find('post-language-option[code="fr"]')
.should('have.attr', 'active', 'false');
cy.get('@language-switch')
.find('post-language-option[code="de"]')
.should('have.attr', 'active', 'false');

cy.get('@language-switch').find('post-language-option[code="de"]').click();

cy.get('@language-switch')
.find('post-language-option[code="de"]')
.should('have.attr', 'active');
cy.get('@language-switch')
.find('post-language-option[code="en"]')
.should('not.have.attr', 'active', 'false');
cy.get('@language-switch')
.find('post-language-option[code="fr"]')
.should('not.have.attr', 'active', 'false');
});
});

describe('menu variant', () => {
beforeEach(() => {
cy.getComponent('language-switch', LANGUAGE_SWITCH_ID);
cy.get('@language-switch').invoke('prop', 'variant', 'menu');
cy.get('@language-switch').find('post-menu-trigger').as('trigger');
});

it('should render as a dropdown menu', () => {
cy.get('@language-switch').find('post-menu-trigger').should('exist');
cy.get('@language-switch').find('post-menu').should('exist');
});

it('should not render list elements', () => {
cy.get('@language-switch').find('[role="list"]').should('not.exist');
});

it('should display the active language in the trigger button', () => {
cy.get('@trigger').should('contain.text', 'en');
});

it('should show the menu on trigger click', () => {
cy.get('@trigger').find('button').click();
cy.get('@language-switch').find('post-language-option button').should('be.visible');
});

it('should correctly switch language and hide menu on option click', () => {
cy.get('@trigger').find('button').click();
cy.get('@language-switch').find('post-language-option[code="de"]').click();

cy.get('@trigger').should('contain.text', 'de');
cy.get('@language-switch').find('post-language-option').should('not.be.visible');
});

it('should have correct ARIA roles', () => {
cy.get('@trigger').find('button').click();
cy.get('@language-switch').find('post-menu').should('have.attr', 'role', 'menu');
cy.get('@language-switch')
.find('post-language-option')
.find('button[role="menuitem"]')
.should('have.length', 2);
});
});

describe('language-option', () => {
describe('button', () => {
beforeEach(() => {
cy.getComponent('language-option', LANGUAGE_OPTION_ID);
cy.get('@language-option').find('button').as('button');
});

it('should render', () => {
cy.get('@language-option').should('exist');
});

it('should not render an anchor', () => {
cy.get('@language-option').find('a').should('not.exist');
});

it('should render a button with correct properties', () => {
cy.get('@button')
.should('exist')
.and('have.attr', 'aria-current', 'true')
.and('have.attr', 'lang', 'en');
});

it('should emit postChange event when clicked', () => {
cy.get('@language-option').then($languageOption => {
$languageOption.on('postChange', cy.spy().as('postChangeSpy'));
});
cy.get('@button').click({ force: true });
cy.get('@postChangeSpy').should('have.been.called');
});
});

describe('anchor', () => {
beforeEach(() => {
cy.getComponent('language-option', LANGUAGE_OPTION_ID, 'anchor');
cy.get('@language-option').find('a').as('anchor');
});

it('should render', () => {
cy.get('@language-option').should('exist');
});

it('should not render a button', () => {
cy.get('@language-option').find('button').should('not.exist');
});

it('should render an anchor', () => {
cy.get('@anchor')
.should('exist')
.and('have.attr', 'aria-current', 'page')
.and('have.attr', 'href', 'https://www.post.ch/en')
.and('have.attr', 'hrefLang', 'en')
.and('have.attr', 'lang', 'en');
});
});
});

describe('Accessibility', () => {
beforeEach(() => {
cy.getComponent('language-switch', LANGUAGE_SWITCH_ID);
cy.get('@language-switch').invoke('prop', 'variant', 'menu');
cy.get('@language-switch').find('post-menu-trigger').as('trigger');
cy.get('@language-switch').find('post-language-option').as('language-options');
});

it('Has no detectable a11y violations for all variants', () => {
cy.get('post-language-switch').as('languageSwitch');
cy.get('@languageSwitch').invoke('prop', 'variant', 'menu');
cy.checkA11y('#root-inner');
cy.get('@languageSwitch').invoke('prop', 'variant', 'list');
cy.checkA11y('#root-inner');
});
});
});
18 changes: 18 additions & 0 deletions packages/components/src/animations/pop-in.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const duration = 250;
const easing = 'ease-out';

export function popIn(el: Element) {
if (!el) return;

return el.animate(
[
{ transform: 'scale(0.9)', opacity: 0 },
{ transform: 'scale(1)', opacity: 1 },
],
{
duration,
easing,
fill: 'forwards',
},
);
}
34 changes: 31 additions & 3 deletions packages/components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,10 @@ export namespace Components {
* Hides the popover menu and restores focus to the previously focused element.
*/
"hide": () => Promise<void>;
/**
* Required label providing an accessible name for the menu.
*/
"label"?: string;
/**
* Defines the position of the menu relative to its trigger. Menus are automatically flipped to the opposite side if there is not enough available space and are shifted towards the viewport if they would overlap edge boundaries. For supported values and behavior details, see the [Floating UI placement documentation](https://floating-ui.com/docs/computePosition#placement).
* @default 'bottom'
Expand Down Expand Up @@ -802,7 +806,11 @@ declare global {
new (): HTMLPostPopoverElement;
};
interface HTMLPostPopovercontainerElementEventMap {
"postToggle": boolean;
"postBeforeShow": { first?: boolean };
"postAfterShow": { first?: boolean };
"postHide": { first?: boolean };
"postBeforeToggle": { willOpen: boolean; first?: boolean };
"postAfterToggle": { isOpen: boolean; first?: boolean };
}
interface HTMLPostPopovercontainerElement extends Components.PostPopovercontainer, HTMLStencilElement {
addEventListener<K extends keyof HTMLPostPopovercontainerElementEventMap>(type: K, listener: (this: HTMLPostPopovercontainerElement, ev: PostPopovercontainerCustomEvent<HTMLPostPopovercontainerElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
Expand Down Expand Up @@ -1196,6 +1204,10 @@ declare namespace LocalJSX {
"for": string;
}
interface PostMenu {
/**
* Required label providing an accessible name for the menu.
*/
"label"?: string;
/**
* Emits when the menu is shown or hidden. The event payload is a boolean: `true` when the menu was opened, `false` when it was closed.
*/
Expand Down Expand Up @@ -1252,9 +1264,25 @@ declare namespace LocalJSX {
*/
"manualClose"?: boolean;
/**
* Fires whenever the popovercontainer gets shown or hidden, passing the new state in event.details as a boolean
* Fires whenever the popovercontainer is shown, passing in event.detail a `first` boolean, which is true if it is shown for the first time.
*/
"onPostAfterShow"?: (event: PostPopovercontainerCustomEvent<{ first?: boolean }>) => void;
/**
* Fires whenever the popovercontainer gets shown or hidden, passing in event.detail an object containing two booleans: `isOpen`, which is true if the popovercontainer was opened and false if it was closed, and `first`, which is true if it was opened for the first time.
*/
"onPostAfterToggle"?: (event: PostPopovercontainerCustomEvent<{ isOpen: boolean; first?: boolean }>) => void;
/**
* Fires whenever the popovercontainer is about to be shown, passing in event.detail a `first` boolean, which is true if it is to be shown for the first time.
*/
"onPostBeforeShow"?: (event: PostPopovercontainerCustomEvent<{ first?: boolean }>) => void;
/**
* Fires whenever the popovercontainer is about to be shown or hidden, passing in event.detail an object containing two booleans: `willOpen`, which is true if the popovercontainer is about to be opened and false if it is about to be closed, and `first`, which is true if it is to be opened for the first time.
*/
"onPostBeforeToggle"?: (event: PostPopovercontainerCustomEvent<{ willOpen: boolean; first?: boolean }>) => void;
/**
* Fires whenever the popovercontainer is hidden, passing in event.detail a `first` boolean, which is true if it is hidden for the first time.
*/
"onPostToggle"?: (event: PostPopovercontainerCustomEvent<boolean>) => void;
"onPostHide"?: (event: PostPopovercontainerCustomEvent<{ first?: boolean }>) => void;
/**
* Defines the placement of the popovercontainer according to the floating-ui options available at https://floating-ui.com/docs/computePosition#placement. Popovercontainers are automatically flipped to the opposite side if there is not enough available space and are shifted towards the viewport if they would overlap edge boundaries.
* @default 'top'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,8 @@ export class PostBreadcrumbs {
{/* Conditionally render concatenated menu or individual breadcrumb items */}
{this.isConcatenated ? (
<div
role="none"
class="menu-trigger-wrapper"
aria-label="More breadcrumbs"
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,16 @@ export class PostLanguageOption {

render() {
const lang = this.code.toLowerCase();
const role = this.variant === 'menu' ? 'menuitem' : undefined;
const emitOnKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
this.emitChange();
}
};

return (
<Host data-version={version}>
<Host data-version={version} role={this.variant === 'list' ? 'listitem' : undefined}>
{this.url ? (
<a
role={role}
aria-current={this.active ? 'page' : undefined}
href={this.url}
hrefLang={lang}
Expand All @@ -143,7 +141,6 @@ export class PostLanguageOption {
</a>
) : (
<button
role={role}
aria-current={this.active ? 'true' : undefined}
lang={lang}
onClick={() => this.emitChange()}
Expand Down
Loading
Loading