Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cdfafa6
add component popover trigger
myrta2302 Sep 10, 2025
62ed4c5
add functionality and focus management
myrta2302 Sep 11, 2025
8c216b1
revert files
myrta2302 Sep 11, 2025
372b494
revert file
myrta2302 Sep 11, 2025
81c4ae6
revert file
myrta2302 Sep 11, 2025
ac1c470
Merge branch 'main' into 6087-web-component-func-popover-trigger
myrta2302 Sep 11, 2025
ea844ff
update description
myrta2302 Sep 11, 2025
dfef7fc
check if eventTarget exists
myrta2302 Sep 11, 2025
92f601d
update popover e2e test
myrta2302 Sep 12, 2025
a5dcbeb
update popovercontainer e2e2 test
myrta2302 Sep 12, 2025
9c8927d
updated documentation and added no trigger console msg
myrta2302 Sep 12, 2025
ba7e526
add angular test
myrta2302 Sep 18, 2025
21337c3
update tests
myrta2302 Sep 19, 2025
5627456
revert file
myrta2302 Sep 19, 2025
aaf4e2f
minor post-tooltip
myrta2302 Sep 19, 2025
daf42fd
update tests
myrta2302 Sep 19, 2025
35d716d
typo
myrta2302 Sep 19, 2025
b3c3cf0
update tests to remove escape check
myrta2302 Sep 25, 2025
dda1b48
fix code smell
myrta2302 Sep 25, 2025
c1eb08f
Merge branch 'main' into 6087-web-component-func-popover-trigger
myrta2302 Sep 25, 2025
35b400a
created changeset
myrta2302 Sep 25, 2025
705a1c3
Merge branch '6087-web-component-func-popover-trigger' of https://git…
myrta2302 Sep 25, 2025
710c1e5
Merge branch 'main' into 6087-web-component-func-popover-trigger
myrta2302 Oct 7, 2025
c37aef4
e2e error update
myrta2302 Oct 7, 2025
b9c6188
fixture update
myrta2302 Oct 7, 2025
937194d
merge conflict update
myrta2302 Oct 7, 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
7 changes: 7 additions & 0 deletions .changeset/forty-jokes-sin.md
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this file but I see two remaining usage of data-popover-target, one in the angular consumer app, and one in the nextjs-integration package.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@swisspost/design-system-components-angular-workspace': patch
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This package is private so there is no need to add it to the changeset

'@swisspost/design-system-documentation': patch
'@swisspost/design-system-components': patch
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'@swisspost/design-system-components': patch
'@swisspost/design-system-components': major

This is a breaking change

---

Introduced `<post-popover-trigger>` web component to replace the previous `data-popover-target` implementation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already a test file for the popover in the components package. How come you've added one here?
I feel like it'd be clearer to have all popover tests within the same place.

Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
describe('Popover', () => {
beforeEach(() => {
cy.visit('/popover');
cy.get('post-popover[data-hydrated]').as('popover');
cy.get('post-popovercontainer[data-hydrated]').as('popovercontainer');
cy.get('#popoverContent').as('popoverContent');
cy.get('post-popover-trigger[data-hydrated][for="popover-one"]')
.children()
.first()
.as('trigger');
});

it('should contain an HTML element inside the trigger, not just plain text', () => {
cy.get('post-popover-trigger[data-hydrated][for="popover-one"]')
.children()
.should('have.length.at.least', 1);
});
it('should show up on click', () => {
cy.get('@popoverContent').should('not.be.visible');
cy.get('@trigger').should('have.attr', 'aria-expanded', 'false');
cy.get('@trigger').click();
cy.get('@popover').should('be.visible');
cy.get('@trigger').should('have.attr', 'aria-expanded', 'true');
// Void click light dismiss does not work in cypress for closing
});

it('should show up when clicking on a nested element inside the trigger', () => {
// Modify trigger by adding a nested span
cy.get('@trigger').then($trigger => {
const originalText = $trigger.text();
$trigger.html(`<span class="nested-element">${originalText}</span>`);
});

cy.get('@popoverContent').should('not.be.visible');
cy.get('@trigger').should('have.attr', 'aria-expanded', 'false');
cy.get('.nested-element').click();
cy.get('@popoverContent').should('be.visible');
cy.get('@trigger').should('have.attr', 'aria-expanded', 'true');
cy.get('.btn-close').click();
cy.get('@popoverContent').should('not.be.visible');
cy.get('@trigger').should('have.attr', 'aria-expanded', 'false');
});

it('should show up when clicking on a deeply nested element inside the trigger', () => {
// Set up a trigger with a deeply nested structure
cy.get('@trigger').then($trigger => {
const originalText = $trigger.text();
$trigger.html(`
<div class="level-1">
<div class="level-2">
<span class="level-3">${originalText}</span>
</div>
</div>
`);
});

cy.get('@popoverContent').should('not.be.visible');
cy.get('.level-3').click();
cy.get('@popoverContent').should('be.visible');
cy.get('@trigger').should('have.attr', 'aria-expanded', 'true');
});

it('should close on X click', () => {
cy.get('@trigger').click();
cy.get('@popoverContent').should('be.visible');
cy.get('.btn-close').click();
cy.get('@popoverContent').should('not.be.visible');
});

it('should open on enter and close on escape', () => {
cy.get('@popoverContent').should('not.be.visible');
cy.get('@trigger').focus().type('{enter}');
cy.get('@popoverContent').should('be.visible');
});

it('should open and close with the API', () => {
Promise.all([cy.get('@trigger'), cy.get('@popoverContent')])
.then(([$trigger, $popover]: [JQuery<HTMLButtonElement>, JQuery<HTMLPostPopoverElement>]) => [
$trigger.get(0),
$popover.get(0),
])
.then(([trigger, popover]: [HTMLButtonElement, HTMLPostPopoverElement]) => {
cy.get('@popoverContent').should('not.be.visible');
popover.show(trigger);
cy.get('@popoverContent').should('be.visible');
popover.hide();
cy.get('@popoverContent').should('not.be.visible');
popover.toggle(trigger);
cy.get('@popoverContent').should('be.visible');
popover.toggle(trigger);
cy.get('@popoverContent').should('not.be.visible');
});

it('should switch position', () => {
cy.get('post-popover').invoke('attr', 'placement', 'top');
cy.get('@popoverContent').should('not.be.visible');

Promise.all([cy.get('@trigger'), cy.get('@popover')])
.then(
([$trigger, $popover]: [JQuery<HTMLButtonElement>, JQuery<HTMLPostPopoverElement>]) => [
$trigger.get(0),
$popover.get(0),
],
)
.then(([trigger, popover]: [HTMLButtonElement, HTMLPostPopoverElement]) => {
const t = trigger.getBoundingClientRect();
const p = popover.getBoundingClientRect();
expect(t.top < p.top);
});
});
});
});
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 { PopoverComponent } from './routes/popover/popover.component';

const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ title: 'Home', path: 'home', component: HomeComponent },
{ title: 'Card-Control', path: 'card-control', component: CardControlComponent },
{ title: 'Popover', path: 'popover', component: PopoverComponent },
];

@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 { PopoverComponent } from './routes/popover/popover.component';

@NgModule({
imports: [
Expand All @@ -15,6 +16,7 @@ import { CardControlComponent } from './routes/card-control/card-control.compone
AppRoutingModule,
FormsModule,
CardControlComponent,
PopoverComponent,
],
declarations: [AppComponent],
providers: [providePostComponents()],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<h2>Popover</h2>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All web components are documented within the home.component.html page, the popover is already there and just needs to have trigger replaced with the new post-popover-trigger.


<post-popover-trigger for="popover-one">
<button class="btn btn-secondary">Popover Trigger</button>
</post-popover-trigger>
<post-popover
class="palette palette-accent"
id="popover-one"
placement="top"
close-button-caption="Close"
arrow=""
><div id="popoverContent">
<h2 class="h6">Optional title</h2>
<span class="mb-0">
A longer message that needs more time to read. <a href="#">Links</a> are also possible.</span
>
</div>
</post-popover>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { PostComponentsModule } from 'components';

@Component({
selector: 'popover-page',
templateUrl: './popover.component.html',
imports: [CommonModule, ReactiveFormsModule, PostComponentsModule],
})
export class PopoverComponent {}
18 changes: 13 additions & 5 deletions packages/components/cypress/e2e/popover.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@ describe('popover', { baseUrl: null, includeShadowDom: true }, () => {
cy.get('post-popover[data-hydrated]');

// Aria-expanded is set by the web component, therefore it's a good measure to indicate the component is ready
cy.get('[data-popover-target="popover-one"][aria-expanded]').as('trigger');
cy.get('post-popover-trigger[data-hydrated][for="popover-one"]')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to also test:

  • the aria-expanded does switch to true when clicked
  • the first element within the popover gets focused when opened

.children()
.first()
.as('trigger');
cy.get('#testtext').as('popover');
});

it('should contain an HTML element inside the trigger, not just plain text', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you could more specifically check that there is focusable content within the trigger

cy.get('post-popover-trigger[data-hydrated][for="popover-one"]')
.children()
.should('have.length.at.least', 1);
});

it('should show up on click', () => {
cy.get('@popover').should('not.be.visible');
cy.get('@trigger').should('have.attr', 'aria-expanded', 'false');
Expand Down Expand Up @@ -63,12 +72,10 @@ describe('popover', { baseUrl: null, includeShadowDom: true }, () => {
cy.get('@popover').should('not.be.visible');
});

it('should open and close on enter', () => {
it('should open on enter', () => {
cy.get('@popover').should('not.be.visible');
cy.get('@trigger').focus().type('{enter}');
cy.get('@popover').should('be.visible');
cy.get('@trigger').type('{enter}');
cy.get('@popover').should('not.be.visible');
});

it('should open and close with the API', () => {
Expand Down Expand Up @@ -119,7 +126,8 @@ describe('popover', { baseUrl: null, includeShadowDom: true }, () => {
cy.get('post-popover[data-hydrated]');

// Aria-expanded is set by the web component, therefore it's a good measure to indicate the component is ready
cy.get('[data-popover-target="popover-one"][aria-expanded]').as('trigger');

cy.get('post-popover-trigger[data-hydrated][for="popover-one"]').as('trigger');

cy.injectAxe();
});
Expand Down
12 changes: 10 additions & 2 deletions packages/components/cypress/e2e/popovercontainer.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ describe('popovercontainer', { baseUrl: null, includeShadowDom: true }, () => {
const selector = isPopoverSupported() ? ':popover-open' : '.\\:popover-open';

beforeEach(() => {
// There is no dedicated docs page for the popovercontainer
cy.visit('./cypress/fixtures/post-popover.test.html');
cy.get('[data-popover-target="popover-one"][aria-expanded]').as('trigger');

// Ensure the component is hydrated, which is necessary to ensure the component is ready for interaction
cy.get('post-popover[data-hydrated]');

// Aria-expanded is set by the web component, therefore it's a good measure to indicate the component is ready
cy.get('post-popover-trigger[data-hydrated][for="popover-one"]')
.children()
.first()
.as('trigger');

cy.get('#testtext').as('container');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
<script src="../../dist/post-components/post-components.esm.js" type="module"></script>
</head>
<body>
<button data-popover-target="popover-one">toggle</button>
<post-popover-trigger for="popover-one">
<button type="button" class="btn btn-secondary">Popover Trigger</button>
</post-popover-trigger>
<post-popover id="popover-one" close-button-caption="Close Popover">
<p id="testtext">This is a test</p>
</post-popover>
Expand Down
29 changes: 25 additions & 4 deletions packages/components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,16 +372,22 @@ export namespace Components {
"placement"?: Placement;
/**
* Programmatically display the popover
* @param target An element with [data-popover-target="id"] where the popover should be shown
* @param target A <post-popover-trigger> component that controls the popover
*/
"show": (target: HTMLElement) => Promise<void>;
/**
* Toggle popover display
* @param target An element with [data-popover-target="id"] where the popover should be anchored to
* @param target A <post-popover-trigger> component that controls the popover
* @param force Pass true to always show or false to always hide
*/
"toggle": (target: HTMLElement, force?: boolean) => Promise<void>;
}
interface PostPopoverTrigger {
/**
* ID of the popover element that this trigger is linked to. Used to open and close the popover.
*/
"for": string;
}
interface PostPopovercontainer {
/**
* Animation style
Expand Down Expand Up @@ -418,12 +424,12 @@ export namespace Components {
"safeSpace"?: 'triangle' | 'trapezoid';
/**
* Programmatically display the popovercontainer
* @param target An element with [data-popover-target="id"] where the popovercontainer should be shown
* @param target A <post-popover-trigger> component that controls the popover
*/
"show": (target: HTMLElement) => Promise<void>;
/**
* Toggle popovercontainer display
* @param target An element with [data-popover-target="id"] where the popovercontainer should be shown
* @param target A <post-popover-trigger> component that controls the popover
* @param force Pass true to always show or false to always hide
*/
"toggle": (target: HTMLElement, force?: boolean) => Promise<boolean>;
Expand Down Expand Up @@ -805,6 +811,12 @@ declare global {
prototype: HTMLPostPopoverElement;
new (): HTMLPostPopoverElement;
};
interface HTMLPostPopoverTriggerElement extends Components.PostPopoverTrigger, HTMLStencilElement {
}
var HTMLPostPopoverTriggerElement: {
prototype: HTMLPostPopoverTriggerElement;
new (): HTMLPostPopoverTriggerElement;
};
interface HTMLPostPopovercontainerElementEventMap {
"postToggle": { isOpen: boolean; first?: boolean };
}
Expand Down Expand Up @@ -915,6 +927,7 @@ declare global {
"post-menu-item": HTMLPostMenuItemElement;
"post-menu-trigger": HTMLPostMenuTriggerElement;
"post-popover": HTMLPostPopoverElement;
"post-popover-trigger": HTMLPostPopoverTriggerElement;
"post-popovercontainer": HTMLPostPopovercontainerElement;
"post-rating": HTMLPostRatingElement;
"post-tab-header": HTMLPostTabHeaderElement;
Expand Down Expand Up @@ -1238,6 +1251,12 @@ declare namespace LocalJSX {
*/
"placement"?: Placement;
}
interface PostPopoverTrigger {
/**
* ID of the popover element that this trigger is linked to. Used to open and close the popover.
*/
"for": string;
}
interface PostPopovercontainer {
/**
* Animation style
Expand Down Expand Up @@ -1397,6 +1416,7 @@ declare namespace LocalJSX {
"post-menu-item": PostMenuItem;
"post-menu-trigger": PostMenuTrigger;
"post-popover": PostPopover;
"post-popover-trigger": PostPopoverTrigger;
"post-popovercontainer": PostPopovercontainer;
"post-rating": PostRating;
"post-tab-header": PostTabHeader;
Expand Down Expand Up @@ -1444,6 +1464,7 @@ declare module "@stencil/core" {
"post-menu-item": LocalJSX.PostMenuItem & JSXBase.HTMLAttributes<HTMLPostMenuItemElement>;
"post-menu-trigger": LocalJSX.PostMenuTrigger & JSXBase.HTMLAttributes<HTMLPostMenuTriggerElement>;
"post-popover": LocalJSX.PostPopover & JSXBase.HTMLAttributes<HTMLPostPopoverElement>;
"post-popover-trigger": LocalJSX.PostPopoverTrigger & JSXBase.HTMLAttributes<HTMLPostPopoverTriggerElement>;
"post-popovercontainer": LocalJSX.PostPopovercontainer & JSXBase.HTMLAttributes<HTMLPostPopovercontainerElement>;
"post-rating": LocalJSX.PostRating & JSXBase.HTMLAttributes<HTMLPostRatingElement>;
"post-tab-header": LocalJSX.PostTabHeader & JSXBase.HTMLAttributes<HTMLPostTabHeaderElement>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:host {
cursor: pointer;
}
Loading
Loading