From b41db4bfb4f17ce80dde7e21f31ba7c10267e9a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aliz=C3=A9=20Debray?= <33580481+alizedebray@users.noreply.github.com> Date: Thu, 24 Aug 2023 10:42:58 +0200 Subject: [PATCH 1/4] feat(components): add post-tabs component (#1181) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Loïc Fürhoff <12294151+imagoiq@users.noreply.github.com> Co-authored-by: Philipp Gfeller <1659006+gfellerph@users.noreply.github.com> --- .changeset/shy-cooks-arrive.md | 7 + .../components/cypress/e2e/collapsible.cy.ts | 11 +- packages/components/cypress/e2e/tabs.cy.ts | 144 ++++++++++++ .../components/cypress/support/commands.ts | 7 +- .../components/cypress/support/index.d.ts | 2 +- packages/components/src/animations/fade.ts | 13 ++ packages/components/src/animations/index.ts | 1 + packages/components/src/components.d.ts | 75 ++++++ .../post-tab-header/post-tab-header.scss | 5 + .../post-tab-header/post-tab-header.tsx | 46 ++++ .../src/components/post-tab-header/readme.md | 17 ++ .../post-tab-panel/post-tab-panel.scss | 3 + .../post-tab-panel/post-tab-panel.tsx | 37 +++ .../src/components/post-tab-panel/readme.md | 17 ++ .../src/components/post-tabs/post-tabs.scss | 5 + .../src/components/post-tabs/post-tabs.tsx | 181 +++++++++++++++ .../src/components/post-tabs/readme.md | 38 ++++ .../snapshots/components/tabs.snapshot.ts | 6 + .../src/stories/components/tabs/tabs.docs.mdx | 43 ++++ .../components/tabs/tabs.snapshot.stories.tsx | 35 +++ .../stories/components/tabs/tabs.stories.tsx | 93 ++++++++ packages/styles/src/components/tabs.scss | 214 ------------------ .../src/components/tabs/_tab-title.scss | 152 +++++++++++++ .../src/components/tabs/_tabs-wrapper.scss | 69 ++++++ .../styles/src/components/tabs/index.scss | 2 + 25 files changed, 999 insertions(+), 224 deletions(-) create mode 100644 .changeset/shy-cooks-arrive.md create mode 100644 packages/components/cypress/e2e/tabs.cy.ts create mode 100644 packages/components/src/animations/fade.ts create mode 100644 packages/components/src/animations/index.ts create mode 100644 packages/components/src/components/post-tab-header/post-tab-header.scss create mode 100644 packages/components/src/components/post-tab-header/post-tab-header.tsx create mode 100644 packages/components/src/components/post-tab-header/readme.md create mode 100644 packages/components/src/components/post-tab-panel/post-tab-panel.scss create mode 100644 packages/components/src/components/post-tab-panel/post-tab-panel.tsx create mode 100644 packages/components/src/components/post-tab-panel/readme.md create mode 100644 packages/components/src/components/post-tabs/post-tabs.scss create mode 100644 packages/components/src/components/post-tabs/post-tabs.tsx create mode 100644 packages/components/src/components/post-tabs/readme.md create mode 100644 packages/documentation/cypress/snapshots/components/tabs.snapshot.ts create mode 100644 packages/documentation/src/stories/components/tabs/tabs.docs.mdx create mode 100644 packages/documentation/src/stories/components/tabs/tabs.snapshot.stories.tsx create mode 100644 packages/documentation/src/stories/components/tabs/tabs.stories.tsx delete mode 100644 packages/styles/src/components/tabs.scss create mode 100644 packages/styles/src/components/tabs/_tab-title.scss create mode 100644 packages/styles/src/components/tabs/_tabs-wrapper.scss create mode 100644 packages/styles/src/components/tabs/index.scss diff --git a/.changeset/shy-cooks-arrive.md b/.changeset/shy-cooks-arrive.md new file mode 100644 index 0000000000..fce43c8ac7 --- /dev/null +++ b/.changeset/shy-cooks-arrive.md @@ -0,0 +1,7 @@ +--- +'@swisspost/design-system-documentation': minor +'@swisspost/design-system-components': minor +'@swisspost/design-system-styles': patch +--- + +Added a new post-tabs component. diff --git a/packages/components/cypress/e2e/collapsible.cy.ts b/packages/components/cypress/e2e/collapsible.cy.ts index d9afc16986..4714d372e4 100644 --- a/packages/components/cypress/e2e/collapsible.cy.ts +++ b/packages/components/cypress/e2e/collapsible.cy.ts @@ -1,7 +1,8 @@ describe('collapsible', () => { describe('default', () => { beforeEach(() => { - cy.registerCollapsibleFrom('/iframe.html?id=components-collapsible--default'); + cy.getComponent('collapsible'); + cy.get('@collapsible').find('.collapse').as('collapse'); cy.get('@collapsible').find('.accordion-header').as('header'); cy.get('@collapsible').find('.accordion-body').as('body'); }); @@ -62,9 +63,8 @@ describe('collapsible', () => { describe('initially collapsed', () => { beforeEach(() => { - cy.registerCollapsibleFrom( - '/iframe.html?id=components-collapsible--initially-collapsed', - ); + cy.getComponent('collapsible', 'initially-collapsed'); + cy.get('@collapsible').find('.collapse').as('collapse'); cy.get('@collapsible').find('.accordion-header').as('header'); }); @@ -89,7 +89,8 @@ describe('collapsible', () => { describe('custom trigger', () => { beforeEach(() => { - cy.registerCollapsibleFrom('/iframe.html?id=components-collapsible--custom-trigger'); + cy.getComponent('collapsible', 'custom-trigger'); + cy.get('@collapsible').find('.collapse').as('collapse'); cy.get('[aria-controls="collapsible-example--custom-trigger"]').as('controls'); cy.get('@controls').contains('Toggle').as('toggle'); cy.get('@controls').contains('Show').as('show'); diff --git a/packages/components/cypress/e2e/tabs.cy.ts b/packages/components/cypress/e2e/tabs.cy.ts new file mode 100644 index 0000000000..6f8c2904e2 --- /dev/null +++ b/packages/components/cypress/e2e/tabs.cy.ts @@ -0,0 +1,144 @@ +describe('tabs', () => { + describe('default', () => { + beforeEach(() => { + cy.getComponent('tabs'); + cy.get('post-tab-header').as('headers'); + }); + + it('should render', () => { + cy.get('@tabs').should('exist'); + }); + + it('should show three tab headers', () => { + cy.get('@headers').should('have.length', 3); + }); + + it('should only show the first tab header as active', () => { + cy.get('@headers').each(($header, index) => { + cy.wrap($header).find('.active').should(index === 0 ? 'exist' : 'not.exist'); + }); + }); + + it('should only show the tab panel associated with the first tab header', () => { + cy.get('post-tab-panel:visible').as('panel'); + cy.get('@panel').should('have.length', 1); + cy.get('@headers').first().invoke('attr', 'panel').then(panel => { + cy.get('@panel').invoke('attr', 'name').should('equal', panel); + }); + }); + + it('should activate a clicked tab header and deactivate the tab header that was previously activated', () => { + cy.get('@headers').last().click(); + + cy.get('@headers').first().find('.active').should('not.exist'); + cy.get('@headers').last().find('.active').should('exist'); + }); + + it('should show the panel associated with a clicked tab header and hide the panel that was previously shown', () => { + cy.get('@headers').last().click(); + + // wait for the fade out animation to complete + cy.wait(200); + + cy.get('post-tab-panel:visible').as('panel'); + cy.get('@panel').should('have.length', 1); + cy.get('@headers').last().invoke('attr', 'panel').then(panel => { + cy.get('@panel').invoke('attr', 'name').should('equal', panel); + }); + }); + }); + + describe('active panel', () => { + beforeEach(() => { + cy.getComponent('tabs', 'active-panel'); + cy.get('post-tab-header').as('headers'); + cy.get('post-tab-panel:visible').as('panel'); + }); + + it('should only show the requested active tab panel', () => { + cy.get('@panel').should('have.length', 1); + cy.get('@tabs').invoke('attr', 'active-panel').then(activePanel => { + cy.get('@panel').invoke('attr', 'name').should('equal', activePanel); + }); + }); + + it('should show as active only the tab header associated with the requested active tab panel', () => { + cy.get('@tabs').invoke('attr', 'active-panel').then(activePanel => { + cy.get('@headers').each($header => { + cy.wrap($header).invoke('attr', 'panel').then(panel => { + cy.wrap($header).find('.active').should(panel === activePanel ? 'exist' : 'not.exist'); + }); + }); + }); + }); + }); + + describe('async', () => { + beforeEach(() => { + cy.getComponent('tabs', 'async'); + cy.get('post-tab-header').as('headers'); + }); + + it('should add a tab header', () => { + cy.get('#add-tab').click(); + cy.get('@headers').should('have.length', 4); + }); + + it('should still show the tab panel associated with the first tab header after adding new tab', () => { + cy.get('#add-tab').click(); + + cy.get('post-tab-panel:visible').as('panel'); + cy.get('@panel').should('have.length', 1); + cy.get('@headers').first().invoke('attr', 'panel').then(panel => { + cy.get('@panel').invoke('attr', 'name').should('equal', panel); + }); + }); + + 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('@headers').last().click(); + + cy.get('@headers').first().find('.active').should('not.exist'); + cy.get('@headers').last().find('.active').should('exist'); + }); + + 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('@new-panel').click(); + + // wait for the fade out animation to complete + cy.wait(200); + + cy.get('post-tab-panel:visible').as('panel'); + cy.get('@panel').should('have.length', 1); + cy.get('@new-panel').invoke('attr', 'panel').then(panel => { + cy.get('@panel').invoke('attr', 'name').should('equal', panel); + }); + }); + + it('should remove a tab header', () => { + cy.get('.tab-title.active').then(() => { + cy.get('#remove-active-tab').click(); + cy.get('@headers').should('have.length', 2); + }); + }); + + it('should still show an active tab header after removing the active tab', () => { + cy.get('.tab-title.active').then(() => { + cy.get('#remove-active-tab').click(); + cy.get('.tab-title.active').should('exist'); + }); + }); + + it('should still show a tab panel after removing the active tab', () => { + cy.get('.tab-title.active').then(() => { + cy.get('#remove-active-tab').click(); + cy.get('post-tab-panel:visible').should('exist'); + }); + }); + }); +}); diff --git a/packages/components/cypress/support/commands.ts b/packages/components/cypress/support/commands.ts index 4afbcb2507..86c6d51182 100644 --- a/packages/components/cypress/support/commands.ts +++ b/packages/components/cypress/support/commands.ts @@ -48,10 +48,9 @@ export const isInViewport = function (_chai: Chai.ChaiStatic) { chai.use(isInViewport); -Cypress.Commands.add('registerCollapsibleFrom', (url: string) => { - cy.visit(url); - cy.get('post-collapsible').as('collapsible'); - cy.get('@collapsible').find('.collapse').as('collapse'); +Cypress.Commands.add('getComponent', (component: string, story = 'default') => { + cy.visit(`/iframe.html?id=components-${component}--${story}`); + cy.get(`post-${component}`).as(component); }); Cypress.Commands.add('checkVisibility', (visibility: 'visible' | 'hidden') => { diff --git a/packages/components/cypress/support/index.d.ts b/packages/components/cypress/support/index.d.ts index 564e9357f2..eb62b61ef7 100644 --- a/packages/components/cypress/support/index.d.ts +++ b/packages/components/cypress/support/index.d.ts @@ -1,7 +1,7 @@ declare global { namespace Cypress { interface Chainable { - registerCollapsibleFrom(url: string): Chainable; + getComponent(component: string, story?: string): Chainable; checkVisibility(visibility: 'visible' | 'hidden'): Chainable; checkAriaExpanded(isExpanded: 'true' | 'false'): Chainable; } diff --git a/packages/components/src/animations/fade.ts b/packages/components/src/animations/fade.ts new file mode 100644 index 0000000000..1a3a94c534 --- /dev/null +++ b/packages/components/src/animations/fade.ts @@ -0,0 +1,13 @@ +const fadeDuration = 200; +const fadedOutKeyFrame = {opacity: '0'}; +const fadedInKeyFrame = {opacity: '1'}; + +export const fadeIn = (el: Element): Animation => el.animate( + [ fadedOutKeyFrame, fadedInKeyFrame ], + { duration: fadeDuration } +); + +export const fadeOut = (el: Element): Animation => el.animate( + [ fadedInKeyFrame, fadedOutKeyFrame ], + { duration: fadeDuration } +); diff --git a/packages/components/src/animations/index.ts b/packages/components/src/animations/index.ts new file mode 100644 index 0000000000..e3e450c263 --- /dev/null +++ b/packages/components/src/animations/index.ts @@ -0,0 +1 @@ +export * from './fade'; diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index f4cbbb38d2..b9d13c000e 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -53,6 +53,32 @@ export namespace Components { */ "scale"?: number | null; } + interface PostTabHeader { + /** + * The name of the panel controlled by the tab header. + */ + "panel": HTMLPostTabPanelElement['name']; + } + interface PostTabPanel { + /** + * The name of the panel, used to associate it with a tab header. + */ + "name": 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.** + */ + "activePanel": HTMLPostTabPanelElement['name']; + /** + * 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. + */ + "show": (panelName: string) => Promise; + } +} +export interface PostTabsCustomEvent extends CustomEvent { + detail: T; + target: HTMLPostTabsElement; } declare global { interface HTMLPostCollapsibleElement extends Components.PostCollapsible, HTMLStencilElement { @@ -70,9 +96,30 @@ declare global { prototype: HTMLPostIconElement; new (): HTMLPostIconElement; }; + interface HTMLPostTabHeaderElement extends Components.PostTabHeader, HTMLStencilElement { + } + var HTMLPostTabHeaderElement: { + prototype: HTMLPostTabHeaderElement; + new (): HTMLPostTabHeaderElement; + }; + interface HTMLPostTabPanelElement extends Components.PostTabPanel, HTMLStencilElement { + } + var HTMLPostTabPanelElement: { + prototype: HTMLPostTabPanelElement; + new (): HTMLPostTabPanelElement; + }; + interface HTMLPostTabsElement extends Components.PostTabs, HTMLStencilElement { + } + var HTMLPostTabsElement: { + prototype: HTMLPostTabsElement; + new (): HTMLPostTabsElement; + }; interface HTMLElementTagNameMap { "post-collapsible": HTMLPostCollapsibleElement; "post-icon": HTMLPostIconElement; + "post-tab-header": HTMLPostTabHeaderElement; + "post-tab-panel": HTMLPostTabPanelElement; + "post-tabs": HTMLPostTabsElement; } } declare namespace LocalJSX { @@ -119,9 +166,34 @@ declare namespace LocalJSX { */ "scale"?: number | null; } + interface PostTabHeader { + /** + * The name of the panel controlled by the tab header. + */ + "panel"?: HTMLPostTabPanelElement['name']; + } + interface PostTabPanel { + /** + * The name of the panel, used to associate it with a tab header. + */ + "name"?: 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.** + */ + "activePanel"?: HTMLPostTabPanelElement['name']; + /** + * 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. + */ + "onTabChange"?: (event: PostTabsCustomEvent) => void; + } interface IntrinsicElements { "post-collapsible": PostCollapsible; "post-icon": PostIcon; + "post-tab-header": PostTabHeader; + "post-tab-panel": PostTabPanel; + "post-tabs": PostTabs; } } export { LocalJSX as JSX }; @@ -133,6 +205,9 @@ declare module "@stencil/core" { * @class PostIcon - representing a stencil component */ "post-icon": LocalJSX.PostIcon & JSXBase.HTMLAttributes; + "post-tab-header": LocalJSX.PostTabHeader & JSXBase.HTMLAttributes; + "post-tab-panel": LocalJSX.PostTabPanel & JSXBase.HTMLAttributes; + "post-tabs": LocalJSX.PostTabs & JSXBase.HTMLAttributes; } } } diff --git a/packages/components/src/components/post-tab-header/post-tab-header.scss b/packages/components/src/components/post-tab-header/post-tab-header.scss new file mode 100644 index 0000000000..a370b985f4 --- /dev/null +++ b/packages/components/src/components/post-tab-header/post-tab-header.scss @@ -0,0 +1,5 @@ +@use '@swisspost/design-system-styles/components/tabs/tab-title'; + +:host { + display: block; +} 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 new file mode 100644 index 0000000000..8475221952 --- /dev/null +++ b/packages/components/src/components/post-tab-header/post-tab-header.tsx @@ -0,0 +1,46 @@ +import { Component, Element, h, Host, Prop, State, Watch } from '@stencil/core'; +import { version } from '../../../package.json'; +import { checkNonEmpty } from '../../utils'; + +@Component({ + tag: 'post-tab-header', + styleUrl: 'post-tab-header.scss', + shadow: true, +}) +export class PostTabHeader { + @Element() host: HTMLPostTabHeaderElement; + + @State() tabId: string; + + /** + * The name of the panel controlled by the tab header. + */ + @Prop() readonly panel: HTMLPostTabPanelElement['name']; + + @Watch('panel') + validateFor(newValue: HTMLPostTabPanelElement['name']) { + checkNonEmpty(newValue, 'The "panel" prop is required for the post-tab-header.'); + } + + componentWillLoad() { + this.tabId = `tab-${this.host.id || crypto.randomUUID()}`; + } + + render() { + return ( + + + + ); + } +} diff --git a/packages/components/src/components/post-tab-header/readme.md b/packages/components/src/components/post-tab-header/readme.md new file mode 100644 index 0000000000..a76762d11a --- /dev/null +++ b/packages/components/src/components/post-tab-header/readme.md @@ -0,0 +1,17 @@ +# post-tab-header + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| -------- | --------- | --------------------------------------------------- | -------- | ----------- | +| `panel` | `panel` | The name of the panel controlled by the tab header. | `string` | `undefined` | + + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/components/src/components/post-tab-panel/post-tab-panel.scss b/packages/components/src/components/post-tab-panel/post-tab-panel.scss new file mode 100644 index 0000000000..cc36f78bfa --- /dev/null +++ b/packages/components/src/components/post-tab-panel/post-tab-panel.scss @@ -0,0 +1,3 @@ +:host { + display: none; +} 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 new file mode 100644 index 0000000000..20aa54163c --- /dev/null +++ b/packages/components/src/components/post-tab-panel/post-tab-panel.tsx @@ -0,0 +1,37 @@ +import { Component, Element, h, Host, Prop, State } from '@stencil/core'; +import { version } from '../../../package.json'; + +@Component({ + tag: 'post-tab-panel', + styleUrl: 'post-tab-panel.scss', + shadow: true, +}) +export class PostTabPanel { + @Element() host: HTMLPostTabPanelElement; + + @State() panelId: string; + + /** + * The name of the panel, used to associate it with a tab header. + */ + @Prop() readonly name: string; + + componentWillLoad() { + // get the id set on the host element or use a random id by default + this.panelId = `panel-${this.host.id || crypto.randomUUID()}`; + } + + render() { + return ( + +
+ +
+
+ ); + } +} diff --git a/packages/components/src/components/post-tab-panel/readme.md b/packages/components/src/components/post-tab-panel/readme.md new file mode 100644 index 0000000000..aa1d1d00be --- /dev/null +++ b/packages/components/src/components/post-tab-panel/readme.md @@ -0,0 +1,17 @@ +# post-tab-panel + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| -------- | --------- | -------------------------------------------------------------- | -------- | ----------- | +| `name` | `name` | The name of the panel, used to associate it with a tab header. | `string` | `undefined` | + + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/components/src/components/post-tabs/post-tabs.scss b/packages/components/src/components/post-tabs/post-tabs.scss new file mode 100644 index 0000000000..deaae1dcea --- /dev/null +++ b/packages/components/src/components/post-tabs/post-tabs.scss @@ -0,0 +1,5 @@ +@use '@swisspost/design-system-styles/components/tabs/tabs-wrapper'; + +:host { + display: block; +} diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx new file mode 100644 index 0000000000..368357edf4 --- /dev/null +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -0,0 +1,181 @@ +import { Component, Host, h, Element, Method, Event, EventEmitter, Prop } from '@stencil/core'; +import { version } from '../../../package.json'; +import { fadeIn, fadeOut } from '../../animations'; + +@Component({ + tag: 'post-tabs', + styleUrl: 'post-tabs.scss', + shadow: true, +}) +export class PostTabs { + private activeTab: HTMLPostTabHeaderElement; + private showing: Animation; + private hiding: Animation; + private isLoaded = false; + + private get tabs(): NodeListOf { + return this.host.querySelectorAll('post-tab-header'); + } + + @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. + * + * **Changing this value after initialization has no effect.** + */ + @Prop() readonly activePanel: HTMLPostTabPanelElement['name']; + + /** + * 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. + */ + @Event() tabChange: EventEmitter; + + componentDidLoad() { + this.moveMisplacedTabs(); + this.enableTabs(); + + const initiallyActivePanel = this.activePanel || this.tabs.item(0).panel; + this.show(initiallyActivePanel); + + this.isLoaded = true; + } + + /** + * 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. + */ + @Method() + async show(panelName: string) { + // do nothing if the tab is already active + if (panelName === this.activeTab?.panel) { + return; + } + + const previousTab = this.activeTab; + const newTab : HTMLPostTabHeaderElement = this.host.querySelector(`post-tab-header[panel=${panelName}]`); + this.activateTab(newTab); + + // 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'; + this.showing.finish(); + } + + // hide the currently visible panel only if no other animation is running + if (previousTab && !this.showing && !this.hiding) { + this.hidePanel(previousTab.panel); + } + + // wait for any hiding animation to complete before showing the selected tab + if (this.hiding) { + await this.hiding.finished; + } + + this.showSelectedPanel(); + + // wait for any display animation to complete for the returned promise to fully resolve + if (this.showing) { + await this.showing.finished; + } + + this.tabChange.emit(this.activeTab.panel); + } + private moveMisplacedTabs() { + if (!this.tabs) return; + + this.tabs.forEach(tab => { + if (tab.getAttribute('slot') === 'tabs') return; + tab.setAttribute('slot', 'tabs'); + }); + } + + private enableTabs() { + if (!this.tabs) return; + + this.tabs.forEach(async tab => { + await tab.componentOnReady(); + + const tabTitle = tab.shadowRoot.querySelector('.tab-title'); + + // if the tab has an "aria-controls" attribute it was already linked to its panel: do nothing + if (tabTitle.getAttribute('aria-controls')) return; + + const tabPanel = this.getPanel(tab.panel).shadowRoot.querySelector('.tab-pane'); + tabTitle.setAttribute('aria-controls', tabPanel.id); + tabPanel.setAttribute('aria-labelledby', tabTitle.id); + + tab.addEventListener('click', e => { + e.preventDefault(); + this.show(tab.panel); + }); + }); + + // if the currently active tab was removed from the DOM then select the first one + if (this.activeTab && !this.activeTab.isConnected) { + this.show(this.tabs.item(0).panel); + } + } + + private activateTab(tab: HTMLPostTabHeaderElement) { + if (this.activeTab) { + const tabTitle = this.activeTab.shadowRoot.querySelector('.tab-title'); + tabTitle.setAttribute('aria-selected', 'false'); + tabTitle.classList.remove('active'); + } + + const tabTitle = tab.shadowRoot.querySelector('.tab-title'); + tabTitle.setAttribute('aria-selected', 'true'); + tabTitle.classList.add('active'); + + this.activeTab = tab; + } + + private hidePanel(panelName: HTMLPostTabPanelElement['name']) { + const previousPanel = this.getPanel(panelName); + + if (!previousPanel) return; + + this.hiding = fadeOut(previousPanel); + this.hiding.onfinish = () => { + previousPanel.style.display = 'none'; + this.hiding = null; + }; + } + + private showSelectedPanel() { + const panel = this.getPanel(this.activeTab.panel); + panel.style.display = 'block'; + + // prevent the initially selected panel from fading in + if (!this.isLoaded) return; + + this.showing = fadeIn(panel); + this.showing.onfinish = () => { + this.showing = null; + }; + } + + private getPanel(name: string): HTMLPostTabPanelElement { + return this.host.querySelector( + `post-tab-panel[name=${name}]` + ); + } + + render() { + return ( + +
+ +
+
+ this.moveMisplacedTabs()} /> +
+
+ ); + } +} diff --git a/packages/components/src/components/post-tabs/readme.md b/packages/components/src/components/post-tabs/readme.md new file mode 100644 index 0000000000..2633280a3a --- /dev/null +++ b/packages/components/src/components/post-tabs/readme.md @@ -0,0 +1,38 @@ +# post-tabs + + + + + + +## 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` | + + +## Events + +| Event | Description | Type | +| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| `tabChange` | 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` | + + +## Methods + +### `show(panelName: string) => Promise` + +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. + +#### Returns + +Type: `Promise` + + + + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/documentation/cypress/snapshots/components/tabs.snapshot.ts b/packages/documentation/cypress/snapshots/components/tabs.snapshot.ts new file mode 100644 index 0000000000..ea5cdb5f50 --- /dev/null +++ b/packages/documentation/cypress/snapshots/components/tabs.snapshot.ts @@ -0,0 +1,6 @@ +describe('Tabs', () => { + it('default', () => { + cy.visit('./iframe.html?id=snapshots--tabs'); + cy.percySnapshot('Tabs', { widths: [1440] }); + }); +}); diff --git a/packages/documentation/src/stories/components/tabs/tabs.docs.mdx b/packages/documentation/src/stories/components/tabs/tabs.docs.mdx new file mode 100644 index 0000000000..45da50aac0 --- /dev/null +++ b/packages/documentation/src/stories/components/tabs/tabs.docs.mdx @@ -0,0 +1,43 @@ +import { Canvas, Controls, Meta } from '@storybook/blocks'; +import * as TabStories from './tabs.stories'; + + + +# Tabs +
+ Organize content across different sections that are shown one at a time. +
+ + + + + +## Examples + +### Initially Active Tab + +The initial selection is set to the first tab by default, displaying its associated panel. +To have a different panel displayed initially, +use the `active-panel` property and assign it the name of the desired panel. + + + +### Tab Changes + +Use the `tabChange` event to run a function anytime a new tab gets activate. + +```typescript +const tabs = document.querySelector('post-tabs') as HTMLPostTabsElement; +tabs.addEventListener('tabChange', (panelName: string) => { + alert(`The panel named ${panelName} is now displayed.`); +}); +``` + +### Custom Trigger + +To trigger a tab change, use the `show()` method specifying the name of the panel that you want to show. + +```typescript +const tabs = document.querySelector('post-tabs') as HTMLPostTabsElement; +tabs.show('myPanel'); +``` diff --git a/packages/documentation/src/stories/components/tabs/tabs.snapshot.stories.tsx b/packages/documentation/src/stories/components/tabs/tabs.snapshot.stories.tsx new file mode 100644 index 0000000000..d3add8aec2 --- /dev/null +++ b/packages/documentation/src/stories/components/tabs/tabs.snapshot.stories.tsx @@ -0,0 +1,35 @@ +import type { Args, StoryContext, StoryObj } from '@storybook/web-components'; +import meta from './tabs.stories'; +import { html } from 'lit'; +import { bombArgs } from '../../../utils/bombArgs'; + +export default { + ...meta, + title: 'Snapshots', +}; + +type Story = StoryObj; + +export const Tabs: Story = { + render: (_args: HTMLPostTabsElement, context: StoryContext) => { + return html` +
+ ${['bg-white', 'bg-dark'].map(bg => html` +
+ ${bombArgs({ + activePanel: [undefined, 'tria'], + }) + .map((args: Args) => + meta.render?.( + { ...context.args, ...args }, + context, + ) + )} +
+ `)} +
+ `; + }, +}; diff --git a/packages/documentation/src/stories/components/tabs/tabs.stories.tsx b/packages/documentation/src/stories/components/tabs/tabs.stories.tsx new file mode 100644 index 0000000000..be69196cd4 --- /dev/null +++ b/packages/documentation/src/stories/components/tabs/tabs.stories.tsx @@ -0,0 +1,93 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import { html } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { BADGE } from '../../../../.storybook/constants'; + +const meta: Meta = { + title: 'Components/Tabs', + component: 'post-tabs', + render: renderTabs, + parameters: { + badge: [BADGE.BETA, BADGE.NEEDS_REVISION], + }, + argTypes: { + activePanel: { + name: 'active-panel', + control: 'select', + options: ['unua', 'dua', 'tria'] + } + } +}; + +export default meta; + +function renderTabs(args: Partial) { + return html` + + 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. + + `; +} + +// STORIES +type Story = StoryObj; + +export const Default: Story = {}; + +export const ActivePanel: Story = { + args: { + activePanel: 'tria' + } +}; + +export const Async: Story = { + decorators: [ + story => { + let tabIndex = 0; + const addTab = () => { + const tabs = document.querySelector('post-tabs'); + + tabIndex++; + const newTab = ` + Nova langeto ${tabIndex} + Jen la enhavo de la nova langeto ${tabIndex}. + `; + + tabs?.insertAdjacentHTML('beforeend', newTab); + }; + + const removeActiveTab = () => { + const headers: NodeListOf = document.querySelectorAll('post-tab-header'); + + const activeHeader: HTMLPostTabHeaderElement | undefined = Array.from(headers).find(header => header.shadowRoot?.querySelector('.active')); + activeHeader?.remove(); + + const activePanel: HTMLPostTabPanelElement | null = document.querySelector(`post-tab-panel[name=${activeHeader?.panel}]`); + activePanel?.remove(); + + console.log(headers, activeHeader, activePanel); + }; + + return html` + ${story()} +
+
+ + +
+ `; + } + ] +}; diff --git a/packages/styles/src/components/tabs.scss b/packages/styles/src/components/tabs.scss deleted file mode 100644 index cf94fd5303..0000000000 --- a/packages/styles/src/components/tabs.scss +++ /dev/null @@ -1,214 +0,0 @@ -@forward './../variables/options'; - -@use './../lic/bootstrap-license'; -@use './../themes/bootstrap/core' as *; -@use './../themes/bootstrap/nav' as n; - -@use './../variables/components/nav'; -@use './../variables/spacing'; -@use './../variables/commons'; -@use './../variables/color'; -@use './../mixins/color' as color-mx; -@use './../mixins/utilities'; - -.tabs-wrapper { - position: relative; - padding-top: spacing.$spacer; - border: 0; - background-color: color.$gray-background-light; - - // Create a line that lies below the active but above the passive elements - // This way hover works smoothly with the background color - &::after { - content: ''; - position: absolute; - bottom: 0; - width: 100%; - height: 1px; - background-color: nav.$nav-tabs-border-color; - } - - // Small hack to save a lot of code and provide a lot of flexibility to tabs coloring - .tabs { - background-color: transparent !important; - } - - @include utilities.high-contrast-mode() { - &::after { - background-color: ButtonBorder; - } - } -} - -.tabs { - flex-wrap: nowrap; - overflow-x: auto; - - /* prevent scroll chaining on x scroll */ - overscroll-behavior-x: contain; - white-space: nowrap; - - // Scrolling fix, make the tabs scroll a little more so the right most tab is not - // flush with the window border - &::after { - content: ''; - display: block; - flex: 1 0 auto; - width: nav.$nav-link-padding-x; - } - - .tab-title { - position: relative; - box-sizing: border-box; - padding: nav.$nav-link-padding; - transition: background-color 100ms; - border-right: 1px solid transparent; - border-left: 1px solid transparent; - outline-color: currentColor; - opacity: 0.7; - color: var(--post-contrast-color); - - &:focus { - opacity: 0.7; - background-color: unset; - color: var(--post-contrast-color); - } - - &:hover { - opacity: 1; - background-color: nav.$nav-tabs-link-active-bg; - color: var(--post-contrast-color); - } - - // same styles as focus, can't use placeholder here because focus-visible can't be described outside of the support condition - &:focus-visible { - outline: transparent; - opacity: 1; - background-color: nav.$nav-tabs-link-active-bg; - color: var(--post-contrast-color); - box-shadow: none; - - &::after { - content: ''; - display: block; - position: absolute; - top: $nav-tabs-focus-box-shadow-width; - right: $nav-tabs-focus-box-shadow-width - 1px; - bottom: 0; - left: $nav-tabs-focus-box-shadow-width - 1px; - box-shadow: nav.$nav-tabs-focus-box-shadow; - } - } - - &.active { - z-index: 1; // Lift above the line and make focus visible all around - border-right-color: nav.$nav-tabs-border-color; - border-left-color: nav.$nav-tabs-border-color; - opacity: 1; - background-color: nav.$nav-tabs-link-active-bg; - color: var(--post-contrast-color); - font-weight: 700; - - // Create a line that does not suffer from border corner mitering - &::before { - content: ''; - display: block; - position: absolute; - top: 0; - right: -1px; - left: -1px; - height: spacing.$size-micro; - background-color: nav.$nav-tabs-link-active-border-color; - } - } - - @include utilities.high-contrast-mode() { - opacity: 1; - border-left-color: Canvas; - border-right-color: Canvas; - color: LinkText; - - &:hover, - &:focus, - &:focus-within, - &:focus-visible { - outline: spacing.$size-line solid Highlight; - outline-offset: -(spacing.$size-micro); - } - - &.active { - border-left-color: ButtonText; - border-right-color: ButtonText; - color: Highlight; - - &::before { - background-color: Highlight; - } - } - } - } - - // Tabs with dark backgrounds - @include color-mx.on-dark-background(true) { - .tab-title { - &:hover { - background-color: rgba(nav.$nav-tabs-link-active-bg, 0.2); - } - - &:focus-visible { - background-color: rgba(nav.$nav-tabs-link-active-bg, 0.2); - } - } - } -} - -.tab-content { - padding-top: spacing.$spacer; - padding-bottom: spacing.$spacer; -} - -// Careful, this generates a lot of code -@each $key, $color in color.$background-colors { - .bg-#{$key} { - .tabs-wrapper { - background-color: $color; - } - - .tab-title { - background-color: $color; - - &.active { - background-color: $color; - } - } - - .nav-item { - background-color: $color; - } - } -} - -[class*='bg-'] .tab-title.active { - &:focus { - &::after { - background-color: rgba(255, 255, 255, 0.2); - } - } - - // write supports for selectors this way: https://stackoverflow.com/a/62666132 - // need to include * otherwise it throws build error - @supports #{'\selector(*:focus-visible)'} { - &:focus { - &::after { - background-color: unset; - } - } - - // same styles as focus, can't use placeholder here because focus-visible can't be described outside of the support condition - &:focus-visible { - &::after { - background-color: rgba(255, 255, 255, 0.2); - } - } - } -} diff --git a/packages/styles/src/components/tabs/_tab-title.scss b/packages/styles/src/components/tabs/_tab-title.scss new file mode 100644 index 0000000000..31cfc4f0d7 --- /dev/null +++ b/packages/styles/src/components/tabs/_tab-title.scss @@ -0,0 +1,152 @@ +@forward './../../variables/options'; + +@use './../../mixins/color' as color-mx; +@use './../../mixins/utilities' as utilities-mx; +@use './../../variables/color'; +@use './../../variables/spacing'; +@use './../../variables/components/nav'; + +.tab-title { + display: inline-block; + position: relative; + box-sizing: border-box; + padding: nav.$nav-link-padding; + transition: background-color 100ms; + border-right: 1px solid transparent; + border-left: 1px solid transparent; + outline-color: currentColor; + opacity: 0.7; + color: var(--post-contrast-color); + text-decoration: none; + + &:focus { + background-color: unset; + color: var(--post-contrast-color); + } + + &:hover { + opacity: 1; + background-color: nav.$nav-tabs-link-active-bg; + color: var(--post-contrast-color); + } + + // same styles as focus, can't use placeholder here because focus-visible can't be described outside of the support condition + &:focus-visible { + outline: transparent; + opacity: 1; + background-color: nav.$nav-tabs-link-active-bg; + color: var(--post-contrast-color); + box-shadow: none; + + &::after { + content: ''; + display: block; + position: absolute; + top: nav.$nav-tabs-focus-box-shadow-width; + right: nav.$nav-tabs-focus-box-shadow-width - 1px; + bottom: 0; + left: nav.$nav-tabs-focus-box-shadow-width - 1px; + box-shadow: nav.$nav-tabs-focus-box-shadow; + } + } + + &.active { + z-index: 1; // Lift above the line and make focus visible all around + border-right-color: nav.$nav-tabs-border-color; + border-left-color: nav.$nav-tabs-border-color; + opacity: 1; + background-color: nav.$nav-tabs-link-active-bg; + color: var(--post-contrast-color); + font-weight: 700; + + // Create a line that does not suffer from border corner mitering + &::before { + content: ''; + display: block; + position: absolute; + top: 0; + right: -1px; + left: -1px; + height: spacing.$size-micro; + background-color: nav.$nav-tabs-link-active-border-color; + } + } + + // Tabs with dark backgrounds + @include color-mx.on-dark-background() { + &:hover { + background-color: rgba(nav.$nav-tabs-link-active-bg, 0.2); + } + + &:focus-visible { + background-color: rgba(nav.$nav-tabs-link-active-bg, 0.2); + } + } + + @include utilities-mx.high-contrast-mode() { + opacity: 1; + border-left-color: Canvas; + border-right-color: Canvas; + color: LinkText; + + &:hover, + &:focus, + &:focus-within, + &:focus-visible { + outline: spacing.$size-line solid Highlight; + outline-offset: -(spacing.$size-micro); + } + + &.active { + border-left-color: ButtonText; + border-right-color: ButtonText; + color: Highlight; + + &::before { + background-color: Highlight; + } + } + } +} + +// Careful, this generates a lot of code +@each $key, $color in color.$background-colors { + .bg-#{$key} { + .tab-title { + background-color: $color; + + &.active { + background-color: $color; + } + } + + .nav-item { + background-color: $color; + } + } +} + +[class*='bg-'] .tab-title.active { + &:focus { + &::after { + background-color: rgba(255, 255, 255, 0.2); + } + } + + // write supports for selectors this way: https://stackoverflow.com/a/62666132 + // need to include * otherwise it throws build error + @supports #{'\selector(*:focus-visible)'} { + &:focus { + &::after { + background-color: unset; + } + } + + // same styles as focus, can't use placeholder here because focus-visible can't be described outside of the support condition + &:focus-visible { + &::after { + background-color: rgba(255, 255, 255, 0.2); + } + } + } +} diff --git a/packages/styles/src/components/tabs/_tabs-wrapper.scss b/packages/styles/src/components/tabs/_tabs-wrapper.scss new file mode 100644 index 0000000000..7aa9b43c20 --- /dev/null +++ b/packages/styles/src/components/tabs/_tabs-wrapper.scss @@ -0,0 +1,69 @@ +@forward './../../variables/options'; + +@use './../../mixins/utilities' as utilities-mx; +@use './../../variables/color'; +@use './../../variables/spacing'; +@use './../../variables/components/nav'; + +.tabs-wrapper { + position: relative; + padding-top: spacing.$spacer; + border: 0; + background-color: color.$gray-background-light; + + // Create a line that lies below the active but above the passive elements + // This way hover works smoothly with the background color + &::after { + content: ''; + position: absolute; + bottom: 0; + width: 100%; + height: 1px; + background-color: nav.$nav-tabs-border-color; + } + + // Small hack to save a lot of code and provide a lot of flexibility to tabs coloring + .tabs { + background-color: transparent !important; + } + + @include utilities-mx.high-contrast-mode() { + &::after { + background-color: ButtonBorder; + } + } +} + +.tabs { + @include utilities-mx.reset-list; + display: flex; + flex-wrap: nowrap; + overflow-x: auto; + + /* prevent scroll chaining on x scroll */ + overscroll-behavior-x: contain; + white-space: nowrap; + + // Scrolling fix to make the tabs scroll a little more so the right most tab is not + // flush with the window border + &::after { + content: ''; + display: block; + flex: 1 0 auto; + width: nav.$nav-link-padding-x; + } +} + +.tab-content { + padding-top: spacing.$spacer; + padding-bottom: spacing.$spacer; +} + +// Careful, this generates a lot of code +@each $key, $color in color.$background-colors { + .bg-#{$key} { + .tabs-wrapper { + background-color: $color; + } + } +} diff --git a/packages/styles/src/components/tabs/index.scss b/packages/styles/src/components/tabs/index.scss new file mode 100644 index 0000000000..e862a037e9 --- /dev/null +++ b/packages/styles/src/components/tabs/index.scss @@ -0,0 +1,2 @@ +@use 'tabs-wrapper'; +@use 'tab-title'; From 16f046229420ba775e8a482db7e470b6dc1fa422 Mon Sep 17 00:00:00 2001 From: Philipp Gfeller <1659006+gfellerph@users.noreply.github.com> Date: Thu, 24 Aug 2023 10:53:53 +0200 Subject: [PATCH 2/4] chore: mark old issues and pull requests as stale (#1870) --- .github/workflows/stale.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/stale.yaml diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml new file mode 100644 index 0000000000..6999f78749 --- /dev/null +++ b/.github/workflows/stale.yaml @@ -0,0 +1,19 @@ +name: 'Mark stale issues' +permissions: + issues: write +on: + workflow_dispatch: + schedule: + - cron: '9 22 * * *' # the job will run every day at 22:09 + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v8 + with: + days-before-stale: 60 + days-before-close: -1 + stale-issue-label: 'stale' + exempt-milestones: 'next' + operations-per-run: 100 From 41d3b0cd2142e5dbddd59b433824f71a93b30e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20F=C3=BCrhoff?= <12294151+imagoiq@users.noreply.github.com> Date: Thu, 24 Aug 2023 11:42:31 +0200 Subject: [PATCH 3/4] fix(internet-header): compatibility with storybook v7 for e2e (#1857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alizé Debray <33580481+alizedebray@users.noreply.github.com> --- .../header/post-internet-header.stories.tsx | 4 +- .../.storybook/cypress-storybook/client.js | 21 +++++ .../.storybook/cypress-storybook/common.js | 85 +++++++++++++++++++ .../.storybook/preview-head.html | 3 +- packages/documentation/.storybook/preview.ts | 12 ++- packages/documentation/package.json | 1 + .../components/internet-header.stories.tsx | 14 +-- .../internet-header/cypress/e2e/header.cy.ts | 6 +- .../cypress/e2e/language-detection.cy.ts | 4 +- .../cypress/e2e/language-switch.cy.ts | 4 +- .../internet-header/cypress/e2e/login.cy.ts | 10 +-- .../cypress/e2e/main-navigation.cy.ts | 7 +- .../cypress/e2e/meta-navigation.cy.ts | 6 +- .../cypress/e2e/os-flyout.cy.ts | 2 +- .../internet-header/cypress/e2e/search.cy.ts | 35 ++++---- .../cypress/e2e/skiplinks.cy.ts | 2 +- .../cypress/e2e/stickyness.cy.ts | 3 +- .../cypress/support/prepare-story.ts | 12 +-- pnpm-lock.yaml | 72 ++++------------ 19 files changed, 190 insertions(+), 113 deletions(-) create mode 100644 packages/documentation/.storybook/cypress-storybook/client.js create mode 100644 packages/documentation/.storybook/cypress-storybook/common.js diff --git a/packages/documentation-v6/src/stories/internet-header/components/header/post-internet-header.stories.tsx b/packages/documentation-v6/src/stories/internet-header/components/header/post-internet-header.stories.tsx index 85633f1c6b..0ef7682b94 100644 --- a/packages/documentation-v6/src/stories/internet-header/components/header/post-internet-header.stories.tsx +++ b/packages/documentation-v6/src/stories/internet-header/components/header/post-internet-header.stories.tsx @@ -1,4 +1,4 @@ -import { Meta, Args, Story } from '@storybook/react'; +import { Args, Meta, Story } from '@storybook/react'; import { BADGE } from '@geometricpanda/storybook-addon-badges'; import { filterArgs } from '../../../utilities/filterArgs'; import docsPage from './internet-header.docs.mdx'; @@ -288,7 +288,7 @@ const Template = (args: Args) => {
-

Design System Internet Header

+

Swiss Post Internet Header

diff --git a/packages/documentation/.storybook/cypress-storybook/client.js b/packages/documentation/.storybook/cypress-storybook/client.js new file mode 100644 index 0000000000..921e6022ea --- /dev/null +++ b/packages/documentation/.storybook/cypress-storybook/client.js @@ -0,0 +1,21 @@ +// Source: https://github.com/NicholasBoll/cypress-storybook + +import { addons } from '@storybook/preview-api'; +import { FORCE_RE_RENDER } from '@storybook/core-events'; +import { changeKnob, setCurrentStory } from './common'; + +window.__setCurrentStory = function (categorization, story) { + setCurrentStory(categorization, story); + forceReRender(); +}; + +window.__changeKnob = function (changedKnob) { + changeKnob(changedKnob); + + // force story to rerender with updated knob + forceReRender(); +}; + +function forceReRender() { + addons.getChannel().emit(FORCE_RE_RENDER); +} diff --git a/packages/documentation/.storybook/cypress-storybook/common.js b/packages/documentation/.storybook/cypress-storybook/common.js new file mode 100644 index 0000000000..e60ac5e3fb --- /dev/null +++ b/packages/documentation/.storybook/cypress-storybook/common.js @@ -0,0 +1,85 @@ +// Source: https://github.com/NicholasBoll/cypress-storybook + +import { addons } from '@storybook/preview-api'; +import Events from '@storybook/core-events'; + +// Collect actions emitted by storybook/addon-actions +window.__actions = {}; +// Track current story Id - this means `loadStory` must be used for some commands to work properly +window.__storyId = null; + +/** + * Remove punctuation and illegal characters from a story ID. + * + * See https://gist.github.com/davidjrice/9d2af51100e41c6c4b4a + */ +function sanitize(string) { + return ( + string + .toLowerCase() + // eslint-disable-next-line no-useless-escape + .replace(/[ ’–—―′¿'`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '-') + .replace(/-+/g, '-') + .replace(/^-+/, '') + .replace(/-+$/, '') + ); +} + +function sanitizeSafe(string, part) { + const sanitized = sanitize(string); + if (sanitized === '') { + throw new Error('Invalid ' + part + " '" + string + "', must include alphanumeric characters"); + } + return sanitized; +} + +/** + * Generate a storybook ID from a component/kind and story name. + * This is a copy from https://github.com/storybookjs/csf/blob/next/src/index.ts + * to be able to support storybook 6.x since they moved toId from storybook/router to storybook/csf + */ +export function toId(kind, name) { + return sanitizeSafe(kind, 'kind') + '--' + sanitizeSafe(name, 'name'); +} + +function resetKnobs() { + addons.getChannel().emit('storybookjs/knobs/reset'); +} + +export function setCurrentStory(categorization, story) { + resetKnobs(); + resetActions(); + window.__storyId = toId(categorization, story); + addons.getChannel().emit(Events.SET_CURRENT_STORY, { + storyId: window.__storyId, + }); + addons.getChannel().emit('storybookjs/knobs/reset'); +} + +export function changeKnob(changedKnob) { + addons.getChannel().emit('storybookjs/knobs/change', changedKnob); +} + +export function changeArg(updatedArgs) { + console.log(updatedArgs); + addons.getChannel().emit(Events.UPDATE_STORY_ARGS, { + storyId: window.__storyId, + updatedArgs: updatedArgs, + }); +} + +addons.getChannel().addListener('storybook/actions/action-event', function (action) { + if (window.Cypress) { + if (!window.__actions[action.data.name]) { + window.__actions[action.data.name] = window.Cypress.sinon.spy(); + } + + window.__actions[action.data.name](action.data.args); + } +}); + +export function resetActions() { + window.__actions = {}; +} + +window.__changeArg = changeArg; diff --git a/packages/documentation/.storybook/preview-head.html b/packages/documentation/.storybook/preview-head.html index 70375e27c8..6b39f036b6 100644 --- a/packages/documentation/.storybook/preview-head.html +++ b/packages/documentation/.storybook/preview-head.html @@ -10,7 +10,8 @@ + \ No newline at end of file + diff --git a/packages/documentation/.storybook/preview.ts b/packages/documentation/.storybook/preview.ts index fc2657f068..c861a10e94 100644 --- a/packages/documentation/.storybook/preview.ts +++ b/packages/documentation/.storybook/preview.ts @@ -1,6 +1,6 @@ import type { Preview } from '@storybook/web-components'; - -import { extractArgTypes, extractComponentDescription } from '@pxtrn/storybook-addon-docs-stencil'; +import './cypress-storybook/client'; +import { extractArgTypes } from '@pxtrn/storybook-addon-docs-stencil'; import { format } from 'prettier'; import DocsLayout from './blocks/layout'; import { badgesConfig, prettierOptions, resetComponents } from './helpers'; @@ -25,7 +25,13 @@ const preview: Preview = { ['Typography', 'Color', 'Layout', 'Elevation', 'Accessibility'], 'Components', 'Internet Header', - ['Getting Started', 'Migration Guide', 'Header Component', 'Breadcrumbs Component', 'Footer Component'], + [ + 'Getting Started', + 'Migration Guide', + 'Header Component', + 'Breadcrumbs Component', + 'Footer Component', + ], 'Intranet Header', ['Getting Started'], 'Icons', diff --git a/packages/documentation/package.json b/packages/documentation/package.json index cb14b47c2d..726762bb55 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -45,6 +45,7 @@ "@storybook/addons": "7.2.3", "@storybook/blocks": "7.2.3", "@storybook/components": "7.2.3", + "@storybook/core-events": "7.3.2", "@storybook/manager-api": "7.2.3", "@storybook/preview-api": "7.2.3", "@storybook/testing-library": "0.2.0", diff --git a/packages/documentation/src/stories/internet-header/components/internet-header.stories.tsx b/packages/documentation/src/stories/internet-header/components/internet-header.stories.tsx index d2b80ec41b..3f1d2b2e92 100644 --- a/packages/documentation/src/stories/internet-header/components/internet-header.stories.tsx +++ b/packages/documentation/src/stories/internet-header/components/internet-header.stories.tsx @@ -15,11 +15,12 @@ const meta: Meta = { }, badges: [BADGE.STABLE], controls: { - exclude: ['config-proxy'] + exclude: ['config-proxy'], }, }, args: { - 'project': 'test' + project: 'test', + language: 'de', }, argTypes: { activeRoute: { @@ -64,12 +65,12 @@ function mockPage(story: any) {
${story()}
- +

Swiss Post Internet Header

- +
`; } @@ -86,7 +87,7 @@ function renderInternetHeader(args: HTMLSwisspostInternetHeaderElement) { type Story = StoryObj; export const Default: Story = { - decorators: [ mockPage ], + decorators: [mockPage], parameters: { docs: { story: { @@ -98,7 +99,7 @@ export const Default: Story = { }; export const FullWidth: Story = { - decorators: [ mockPage ], + decorators: [mockPage], args: { fullWidth: true, }, @@ -192,6 +193,7 @@ export const CustomNavigation: Story = { }; export const CustomOnlineServiceFlyout: Story = { + decorators: [mockPage], args: { osFlyoutOverrides: { title: 'Custom OS Flyout', diff --git a/packages/internet-header/cypress/e2e/header.cy.ts b/packages/internet-header/cypress/e2e/header.cy.ts index adeb87f697..07ea692d15 100644 --- a/packages/internet-header/cypress/e2e/header.cy.ts +++ b/packages/internet-header/cypress/e2e/header.cy.ts @@ -2,7 +2,7 @@ import { prepare } from '../support/prepare-story'; describe('header', () => { beforeEach(() => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); }); context('initial state', () => { @@ -10,8 +10,8 @@ describe('header', () => { cy.get('swisspost-internet-header').should('have.class', 'hydrated'); }); - it(`has title 'Design System Internet Header'`, () => { - cy.get('h1').should('contain.text', 'Design System Internet Header'); + it(`has title 'Swiss Post Internet Header'`, () => { + cy.get('h1').should('contain.text', 'Swiss Post Internet Header'); }); it(`has nav item 'Briefe versenden' selected`, () => { diff --git a/packages/internet-header/cypress/e2e/language-detection.cy.ts b/packages/internet-header/cypress/e2e/language-detection.cy.ts index 9693162d56..1bfbfce648 100644 --- a/packages/internet-header/cypress/e2e/language-detection.cy.ts +++ b/packages/internet-header/cypress/e2e/language-detection.cy.ts @@ -3,7 +3,7 @@ import { prepare } from '../support/prepare-story'; describe('language detection from storybook', () => { it('should not render the header without languages in the config', () => { - prepare('Internet Header/Components/Header', 'Default', {}); + prepare('Internet Header/Header Component', 'Default', {}); cy.get('swisspost-internet-header').should('exist'); cy.get('.post-internet-header').should('not.exist'); }); @@ -15,7 +15,7 @@ describe('language detection from storybook', () => { delete customConfig.fr; delete customConfig.en; - prepare('Internet Header/Components/Header', 'Default', customConfig); + prepare('Internet Header/Header Component', 'Default', customConfig); cy.get('swisspost-internet-header') .shadow() diff --git a/packages/internet-header/cypress/e2e/language-switch.cy.ts b/packages/internet-header/cypress/e2e/language-switch.cy.ts index 4ca5325b60..a1a40ba17a 100644 --- a/packages/internet-header/cypress/e2e/language-switch.cy.ts +++ b/packages/internet-header/cypress/e2e/language-switch.cy.ts @@ -11,7 +11,7 @@ describe('language-switch', () => { beforeEach(() => { cy.viewport(1024, 800); - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); }); describe('meta menu', () => { @@ -144,7 +144,7 @@ describe('language-switch', () => { url: '', }; config.en!.header.navLang = [navLangEntry]; - prepare('Internet Header/Components/Header', 'Default', config); + prepare('Internet Header/Header Component', 'Default', config); cy.get('#post-language-switch-desktop').should('not.exist'); }); diff --git a/packages/internet-header/cypress/e2e/login.cy.ts b/packages/internet-header/cypress/e2e/login.cy.ts index d239516ad4..588c912fec 100644 --- a/packages/internet-header/cypress/e2e/login.cy.ts +++ b/packages/internet-header/cypress/e2e/login.cy.ts @@ -6,7 +6,7 @@ describe('login', () => { describe('args', () => { describe('login: true', () => { it(`adds login control`, () => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); cy.changeArg('login', true); cy.get('post-klp-login-widget').should('exist').and('be.visible'); }); @@ -14,7 +14,7 @@ describe('login', () => { describe('login: false', () => { it(`removes login control`, () => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); cy.changeArg('login', false); cy.get('post-klp-login-widget').should('not.exist'); }); @@ -31,7 +31,7 @@ describe('login', () => { config.de!.header.loginWidgetOptions = undefined; // Intercept the request to the config API and return a static response - prepare('Internet Header/Components/Header', 'Default', config); + prepare('Internet Header/Header Component', 'Default', config); // Assert the header is hydrated cy.get('swisspost-internet-header').should('have.class', 'hydrated'); @@ -48,7 +48,7 @@ describe('login', () => { let config: IPortalConfig = JSON.parse(JSON.stringify(testConfiguration)); config.de!.header.showJobsLoginWidget = true; config.de!.header.isLoginWidgetHidden = false; - prepare('Internet Header/Components/Header', 'Default', config); + prepare('Internet Header/Header Component', 'Default', config); console.warn(config.de?.header.loginWidgetOptions); cy.get('swisspost-internet-header').should('have.class', 'hydrated'); cy.get('a.login-button').should('exist').and('be.visible'); @@ -61,7 +61,7 @@ describe('login', () => { let config: IPortalConfig = JSON.parse(JSON.stringify(testConfiguration)); config.de!.header.showJobsLoginWidget = false; config.de!.header.isLoginWidgetHidden = false; - prepare('Internet Header/Components/Header', 'Default', config); + prepare('Internet Header/Header Component', 'Default', config); cy.get('swisspost-internet-header').should('have.class', 'hydrated'); cy.get('.klp-widget-anonymous').should('exist'); }); diff --git a/packages/internet-header/cypress/e2e/main-navigation.cy.ts b/packages/internet-header/cypress/e2e/main-navigation.cy.ts index 3c08d51440..97b7ae0f8d 100644 --- a/packages/internet-header/cypress/e2e/main-navigation.cy.ts +++ b/packages/internet-header/cypress/e2e/main-navigation.cy.ts @@ -3,7 +3,7 @@ import { prepare } from '../support/prepare-story'; describe('main-navigation', () => { beforeEach(() => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); }); it('should not have any highlight when active route is false', async () => { @@ -27,7 +27,7 @@ describe('main-navigation', () => { it('should have an active route when config defines an active route', () => { const activeConfig = JSON.parse(JSON.stringify(testConfiguration)); activeConfig.de.header.navMain[0].isActive = true; - prepare('Internet Header/Components/Header', 'Default', activeConfig); + prepare('Internet Header/Header Component', 'Default', activeConfig); cy.get('swisspost-internet-header') .shadow() .find('.flyout-link.active, .main-link.active') @@ -46,7 +46,8 @@ describe('main-navigation', () => { }); it('Changes active link also in custom config nav links', () => { - prepare('Internet Header/Components/Header', 'Custom Navigation'); + prepare('Internet Header/Header Component', 'Custom Navigation'); + cy.changeArg('language', 'en'); cy.changeArg('active-route', 'https://maps.google.com'); cy.get('swisspost-internet-header') .shadow() diff --git a/packages/internet-header/cypress/e2e/meta-navigation.cy.ts b/packages/internet-header/cypress/e2e/meta-navigation.cy.ts index 25eb948bee..83b088c0e7 100644 --- a/packages/internet-header/cypress/e2e/meta-navigation.cy.ts +++ b/packages/internet-header/cypress/e2e/meta-navigation.cy.ts @@ -4,7 +4,7 @@ import { prepare } from '../support/prepare-story'; describe('meta-navigation', () => { beforeEach(() => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); cy.viewport(1024, Cypress.config('viewportHeight')); }); @@ -95,7 +95,7 @@ describe('meta-navigation', () => { // Clear meta navigation config config.de!.header.navMeta = undefined; - prepare('Internet Header/Components/Header', 'Default', config); + prepare('Internet Header/Header Component', 'Default', config); // Assert the header is hydrated cy.get('swisspost-internet-header').should('have.class', 'hydrated'); @@ -120,7 +120,7 @@ describe('meta-navigation', () => { }, ]; - prepare('Internet Header/Components/Header', 'Default', config); + prepare('Internet Header/Header Component', 'Default', config); // Assert the header is hydrated cy.get('swisspost-internet-header').should('have.class', 'hydrated'); diff --git a/packages/internet-header/cypress/e2e/os-flyout.cy.ts b/packages/internet-header/cypress/e2e/os-flyout.cy.ts index 390b27f77e..c31d28037a 100644 --- a/packages/internet-header/cypress/e2e/os-flyout.cy.ts +++ b/packages/internet-header/cypress/e2e/os-flyout.cy.ts @@ -2,7 +2,7 @@ import { prepare } from '../support/prepare-story'; describe('os-flyout', () => { beforeEach(() => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); }); it('should customize the os flyout title', () => { const title = 'Test OS Flyout'; diff --git a/packages/internet-header/cypress/e2e/search.cy.ts b/packages/internet-header/cypress/e2e/search.cy.ts index c9c5dfb99a..eaf58cdec3 100644 --- a/packages/internet-header/cypress/e2e/search.cy.ts +++ b/packages/internet-header/cypress/e2e/search.cy.ts @@ -4,7 +4,8 @@ import mockStaoCacheTypes from '../fixtures/internet-header/staocache-types.json import { copyConfig, prepare } from '../support/prepare-story'; describe('search', () => { - const searchButton = '#post-internet-header-search-button'; + const searchButton = '#post-internet-header-search-button[aria-expanded=false]'; + const closeButton = '#post-internet-header-search-button[aria-expanded=true]'; beforeEach(() => { cy.intercept('/rest/search/v2/querySuggest?**', mockCoveoSuggestions).as('coveoSuggestions'); @@ -21,7 +22,7 @@ describe('search', () => { describe('config', () => { describe('default', () => { it('Search button should not be displayed if the config does not provide a search config', () => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); cy.changeArg('search', false); cy.get('swisspost-internet-header').should('exist'); cy.get('post-search').should('not.exist'); @@ -32,14 +33,14 @@ describe('search', () => { describe('args', () => { describe('search: true', () => { it(`adds search control`, () => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); cy.get('post-search').should('exist').and('be.visible'); }); }); describe('search: false', () => { it(`removes search control`, () => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); cy.changeArg('search', false); cy.get('post-search').should('not.exist'); }); @@ -47,7 +48,7 @@ describe('search', () => { describe('Search button should be hidden if header.search = false is set during runtime', () => { it(`change during runtime`, () => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); cy.changeArg('search', false); cy.get('post-search').should('not.exist'); cy.changeArg('search', true); @@ -59,17 +60,17 @@ describe('search', () => { describe('open & close', () => { describe('open search', () => { it('search should open on search button click', () => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); cy.changeArg('search', true); cy.get(searchButton).click({ force: true }); cy.get('.flyout').should('exist').should('have.class', 'open'); }); it('Coveo suggestions should be loaded when search is opened', () => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); cy.changeArg('search', true); cy.get(searchButton).click(); - cy.get('#searchBox').type('s'); + cy.get('#searchBox').type('s', { force: true }); cy.get('.suggestions').should('exist'); cy.get('.suggestions li use[href="#pi-search"]').should('have.length', 3); }); @@ -77,9 +78,9 @@ describe('search', () => { it('Coveo suggestions should be turned off with isCustomSuggestionHidden', () => { const config = copyConfig(); config.de!.header.search.isCustomSuggestionHidden = true; - prepare('Internet Header/Components/Header', 'Default', config); + prepare('Internet Header/Header Component', 'Default', config); cy.get(searchButton).click(); - cy.get('#searchBox').type('s'); + cy.get('#searchBox').type('s', { force: true }); cy.get('.suggestions').should('exist'); cy.get('.suggestions li use[href="#pi-search"]').should('have.length', 0); }); @@ -87,7 +88,7 @@ describe('search', () => { describe('close search', () => { it('Search should close on esc key click', () => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); cy.changeArg('search', true); cy.get(searchButton).click(); cy.get('#searchBox') @@ -98,11 +99,11 @@ describe('search', () => { }); it('Search should close on search button click', () => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); cy.changeArg('search', true); cy.get(searchButton).click({ force: true }); cy.get('.flyout').should('exist'); - cy.get(searchButton).click({ force: true }); + cy.get(closeButton).click({ force: true }); cy.get('.flyout').should('not.have.class', 'open'); }); }); @@ -116,7 +117,7 @@ describe('search', () => { ); }); it('Search should redirect to search page on enter if search input has a value', () => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); cy.changeArg('search', true); cy.get(searchButton).click({ force: true }); // workaround type error disabled with this https://github.com/cypress-io/cypress/issues/5827 @@ -128,7 +129,7 @@ describe('search', () => { }); it('With suggestions and focus on the search input, pressing arrow down should focus the first suggestion', () => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); let value: string; cy.changeArg('search', true); cy.get(searchButton).click(); @@ -143,7 +144,7 @@ describe('search', () => { }); it('Should render geolocation results', () => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); cy.changeArg('search', true); cy.get(searchButton).click(); cy.get('#searchBox').click(); @@ -153,7 +154,7 @@ describe('search', () => { }); it('redirects to the correct search page', () => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); cy.changeArg('search', true); cy.get(searchButton).click(); cy.get('#searchBox').click().type('{downArrow}', { force: true }); diff --git a/packages/internet-header/cypress/e2e/skiplinks.cy.ts b/packages/internet-header/cypress/e2e/skiplinks.cy.ts index 477c90ffcb..068769bc5b 100644 --- a/packages/internet-header/cypress/e2e/skiplinks.cy.ts +++ b/packages/internet-header/cypress/e2e/skiplinks.cy.ts @@ -2,7 +2,7 @@ import { prepare } from '../support/prepare-story'; describe('skiplinks', () => { beforeEach(() => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); }); it(`adds and removes skiplinks control`, () => { diff --git a/packages/internet-header/cypress/e2e/stickyness.cy.ts b/packages/internet-header/cypress/e2e/stickyness.cy.ts index 3d3d9a9949..cf1d0f88d0 100644 --- a/packages/internet-header/cypress/e2e/stickyness.cy.ts +++ b/packages/internet-header/cypress/e2e/stickyness.cy.ts @@ -2,7 +2,7 @@ import { prepare } from '../support/prepare-story'; describe('stickyness', () => { beforeEach(() => { - prepare('Internet Header/Components/Header', 'Default'); + prepare('Internet Header/Header Component', 'Default'); }); it('should not show header when scrolling when stickyness is none', () => { @@ -21,6 +21,7 @@ describe('stickyness', () => { cy.get('swisspost-internet-header').should('not.be.inViewport'); cy.get('post-meta-navigation').should('not.be.inViewport'); cy.scrollTo('center'); + cy.scrollTo(0, -20); cy.get('post-main-navigation').should('be.inViewport'); cy.get('post-meta-navigation').should('not.be.inViewport'); cy.scrollTo('top'); diff --git a/packages/internet-header/cypress/support/prepare-story.ts b/packages/internet-header/cypress/support/prepare-story.ts index 7cb6d8f679..c7b0dfd3ba 100644 --- a/packages/internet-header/cypress/support/prepare-story.ts +++ b/packages/internet-header/cypress/support/prepare-story.ts @@ -14,15 +14,11 @@ export const prepare = ( config: Object = testConfiguration, ) => { installInterceptors(config); - cy.visitStorybook({ - onBeforeLoad(win: { navigator: any }) { - // Set default browser language explicitly to English - Object.defineProperty(win.navigator, 'language', { - value: 'en', - }); - }, - }); + cy.visitStorybook(); + cy.get('[class=sb-nopreview_main]', { timeout: 30000 }).should('be.visible'); // Wait until vite is ready (initial loading is longer) cy.loadStory(storyTitle, storyName); + cy.get('[id=storybook-root]', { timeout: 30000 }).should('be.visible'); // Ensure that we have a storybook component loaded, before going further + cy.changeArg('language', 'de'); }; export const copyConfig = (): IPortalConfig => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23ab31702b..34cd9a6a72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -493,7 +493,7 @@ importers: devDependencies: '@geometricpanda/storybook-addon-badges': specifier: 1.1.1 - version: 1.1.1(@storybook/addons@7.2.3)(@storybook/api@6.5.16)(@storybook/components@7.2.3)(@storybook/core-events@6.5.16)(@storybook/theming@7.2.3)(react-dom@18.2.0)(react@18.2.0) + version: 1.1.1(@storybook/addons@7.2.3)(@storybook/api@6.5.16)(@storybook/components@7.2.3)(@storybook/core-events@7.3.2)(@storybook/theming@7.2.3)(react-dom@18.2.0)(react@18.2.0) '@open-wc/lit-helpers': specifier: 0.6.0 version: 0.6.0(lit@2.8.0) @@ -527,6 +527,9 @@ importers: '@storybook/components': specifier: 7.2.3 version: 7.2.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) + '@storybook/core-events': + specifier: 7.3.2 + version: 7.3.2 '@storybook/manager-api': specifier: 7.2.3 version: 7.2.3(react-dom@18.2.0)(react@18.2.0) @@ -1802,7 +1805,7 @@ packages: resolution: {integrity: sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.22.5 + '@babel/types': 7.22.10 '@jridgewell/gen-mapping': 0.3.3 '@jridgewell/trace-mapping': 0.3.18 jsesc: 2.5.2 @@ -2174,7 +2177,7 @@ packages: dependencies: '@babel/template': 7.22.5 '@babel/traverse': 7.22.8 - '@babel/types': 7.22.5 + '@babel/types': 7.22.10 transitivePeerDependencies: - supports-color @@ -2201,14 +2204,13 @@ packages: hasBin: true dependencies: '@babel/types': 7.22.10 - dev: true /@babel/parser@7.22.7: resolution: {integrity: sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.22.5 + '@babel/types': 7.22.10 /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.5(@babel/core@7.20.12): resolution: {integrity: sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==} @@ -4261,8 +4263,8 @@ packages: '@babel/helper-function-name': 7.22.5 '@babel/helper-hoist-variables': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.22.7 - '@babel/types': 7.22.5 + '@babel/parser': 7.22.10 + '@babel/types': 7.22.10 debug: 4.3.4(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: @@ -4618,8 +4620,6 @@ packages: /@esbuild/android-arm64@0.18.11: resolution: {integrity: sha512-snieiq75Z1z5LJX9cduSAjUr7vEI1OdlzFPMw0HH5YI7qQHDd3qs+WZoMrWYDsfRJSq36lIA6mfZBkvL46KoIw==} engines: {node: '>=12'} - cpu: [arm64] - os: [android] requiresBuild: true dev: true optional: true @@ -4645,8 +4645,6 @@ packages: /@esbuild/android-arm@0.18.11: resolution: {integrity: sha512-q4qlUf5ucwbUJZXF5tEQ8LF7y0Nk4P58hOsGk3ucY0oCwgQqAnqXVbUuahCddVHfrxmpyewRpiTHwVHIETYu7Q==} engines: {node: '>=12'} - cpu: [arm] - os: [android] requiresBuild: true dev: true optional: true @@ -4672,8 +4670,6 @@ packages: /@esbuild/android-x64@0.18.11: resolution: {integrity: sha512-iPuoxQEV34+hTF6FT7om+Qwziv1U519lEOvekXO9zaMMlT9+XneAhKL32DW3H7okrCOBQ44BMihE8dclbZtTuw==} engines: {node: '>=12'} - cpu: [x64] - os: [android] requiresBuild: true dev: true optional: true @@ -4699,8 +4695,6 @@ packages: /@esbuild/darwin-arm64@0.18.11: resolution: {integrity: sha512-Gm0QkI3k402OpfMKyQEEMG0RuW2LQsSmI6OeO4El2ojJMoF5NLYb3qMIjvbG/lbMeLOGiW6ooU8xqc+S0fgz2w==} engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] requiresBuild: true dev: true optional: true @@ -4726,8 +4720,6 @@ packages: /@esbuild/darwin-x64@0.18.11: resolution: {integrity: sha512-N15Vzy0YNHu6cfyDOjiyfJlRJCB/ngKOAvoBf1qybG3eOq0SL2Lutzz9N7DYUbb7Q23XtHPn6lMDF6uWbGv9Fw==} engines: {node: '>=12'} - cpu: [x64] - os: [darwin] requiresBuild: true dev: true optional: true @@ -4753,8 +4745,6 @@ packages: /@esbuild/freebsd-arm64@0.18.11: resolution: {integrity: sha512-atEyuq6a3omEY5qAh5jIORWk8MzFnCpSTUruBgeyN9jZq1K/QI9uke0ATi3MHu4L8c59CnIi4+1jDKMuqmR71A==} engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] requiresBuild: true dev: true optional: true @@ -4780,8 +4770,6 @@ packages: /@esbuild/freebsd-x64@0.18.11: resolution: {integrity: sha512-XtuPrEfBj/YYYnAAB7KcorzzpGTvOr/dTtXPGesRfmflqhA4LMF0Gh/n5+a9JBzPuJ+CGk17CA++Hmr1F/gI0Q==} engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] requiresBuild: true dev: true optional: true @@ -4807,8 +4795,6 @@ packages: /@esbuild/linux-arm64@0.18.11: resolution: {integrity: sha512-c6Vh2WS9VFKxKZ2TvJdA7gdy0n6eSy+yunBvv4aqNCEhSWVor1TU43wNRp2YLO9Vng2G+W94aRz+ILDSwAiYog==} engines: {node: '>=12'} - cpu: [arm64] - os: [linux] requiresBuild: true dev: true optional: true @@ -4834,8 +4820,6 @@ packages: /@esbuild/linux-arm@0.18.11: resolution: {integrity: sha512-Idipz+Taso/toi2ETugShXjQ3S59b6m62KmLHkJlSq/cBejixmIydqrtM2XTvNCywFl3VC7SreSf6NV0i6sRyg==} engines: {node: '>=12'} - cpu: [arm] - os: [linux] requiresBuild: true dev: true optional: true @@ -4861,8 +4845,6 @@ packages: /@esbuild/linux-ia32@0.18.11: resolution: {integrity: sha512-S3hkIF6KUqRh9n1Q0dSyYcWmcVa9Cg+mSoZEfFuzoYXXsk6196qndrM+ZiHNwpZKi3XOXpShZZ+9dfN5ykqjjw==} engines: {node: '>=12'} - cpu: [ia32] - os: [linux] requiresBuild: true dev: true optional: true @@ -4888,8 +4870,6 @@ packages: /@esbuild/linux-loong64@0.18.11: resolution: {integrity: sha512-MRESANOoObQINBA+RMZW+Z0TJWpibtE7cPFnahzyQHDCA9X9LOmGh68MVimZlM9J8n5Ia8lU773te6O3ILW8kw==} engines: {node: '>=12'} - cpu: [loong64] - os: [linux] requiresBuild: true dev: true optional: true @@ -4915,8 +4895,6 @@ packages: /@esbuild/linux-mips64el@0.18.11: resolution: {integrity: sha512-qVyPIZrXNMOLYegtD1u8EBccCrBVshxMrn5MkuFc3mEVsw7CCQHaqZ4jm9hbn4gWY95XFnb7i4SsT3eflxZsUg==} engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] requiresBuild: true dev: true optional: true @@ -4942,8 +4920,6 @@ packages: /@esbuild/linux-ppc64@0.18.11: resolution: {integrity: sha512-T3yd8vJXfPirZaUOoA9D2ZjxZX4Gr3QuC3GztBJA6PklLotc/7sXTOuuRkhE9W/5JvJP/K9b99ayPNAD+R+4qQ==} engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] requiresBuild: true dev: true optional: true @@ -4969,8 +4945,6 @@ packages: /@esbuild/linux-riscv64@0.18.11: resolution: {integrity: sha512-evUoRPWiwuFk++snjH9e2cAjF5VVSTj+Dnf+rkO/Q20tRqv+644279TZlPK8nUGunjPAtQRCj1jQkDAvL6rm2w==} engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] requiresBuild: true dev: true optional: true @@ -4996,8 +4970,6 @@ packages: /@esbuild/linux-s390x@0.18.11: resolution: {integrity: sha512-/SlRJ15XR6i93gRWquRxYCfhTeC5PdqEapKoLbX63PLCmAkXZHY2uQm2l9bN0oPHBsOw2IswRZctMYS0MijFcg==} engines: {node: '>=12'} - cpu: [s390x] - os: [linux] requiresBuild: true dev: true optional: true @@ -5023,8 +4995,6 @@ packages: /@esbuild/linux-x64@0.18.11: resolution: {integrity: sha512-xcncej+wF16WEmIwPtCHi0qmx1FweBqgsRtEL1mSHLFR6/mb3GEZfLQnx+pUDfRDEM4DQF8dpXIW7eDOZl1IbA==} engines: {node: '>=12'} - cpu: [x64] - os: [linux] requiresBuild: true dev: true optional: true @@ -5050,8 +5020,6 @@ packages: /@esbuild/netbsd-x64@0.18.11: resolution: {integrity: sha512-aSjMHj/F7BuS1CptSXNg6S3M4F3bLp5wfFPIJM+Km2NfIVfFKhdmfHF9frhiCLIGVzDziggqWll0B+9AUbud/Q==} engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] requiresBuild: true dev: true optional: true @@ -5077,8 +5045,6 @@ packages: /@esbuild/openbsd-x64@0.18.11: resolution: {integrity: sha512-tNBq+6XIBZtht0xJGv7IBB5XaSyvYPCm1PxJ33zLQONdZoLVM0bgGqUrXnJyiEguD9LU4AHiu+GCXy/Hm9LsdQ==} engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] requiresBuild: true dev: true optional: true @@ -5104,8 +5070,6 @@ packages: /@esbuild/sunos-x64@0.18.11: resolution: {integrity: sha512-kxfbDOrH4dHuAAOhr7D7EqaYf+W45LsAOOhAet99EyuxxQmjbk8M9N4ezHcEiCYPaiW8Dj3K26Z2V17Gt6p3ng==} engines: {node: '>=12'} - cpu: [x64] - os: [sunos] requiresBuild: true dev: true optional: true @@ -5131,8 +5095,6 @@ packages: /@esbuild/win32-arm64@0.18.11: resolution: {integrity: sha512-Sh0dDRyk1Xi348idbal7lZyfSkjhJsdFeuC13zqdipsvMetlGiFQNdO+Yfp6f6B4FbyQm7qsk16yaZk25LChzg==} engines: {node: '>=12'} - cpu: [arm64] - os: [win32] requiresBuild: true dev: true optional: true @@ -5158,8 +5120,6 @@ packages: /@esbuild/win32-ia32@0.18.11: resolution: {integrity: sha512-o9JUIKF1j0rqJTFbIoF4bXj6rvrTZYOrfRcGyL0Vm5uJ/j5CkBD/51tpdxe9lXEDouhRgdr/BYzUrDOvrWwJpg==} engines: {node: '>=12'} - cpu: [ia32] - os: [win32] requiresBuild: true dev: true optional: true @@ -5185,8 +5145,6 @@ packages: /@esbuild/win32-x64@0.18.11: resolution: {integrity: sha512-rQI4cjLHd2hGsM1LqgDI7oOCYbQ6IBOVsX9ejuRMSze0GqXUG2ekwiKkiBU1pRGSeCqFFHxTrcEydB2Hyoz9CA==} engines: {node: '>=12'} - cpu: [x64] - os: [win32] requiresBuild: true dev: true optional: true @@ -5313,7 +5271,7 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@geometricpanda/storybook-addon-badges@1.1.1(@storybook/addons@7.2.3)(@storybook/api@6.5.16)(@storybook/components@7.2.3)(@storybook/core-events@6.5.16)(@storybook/theming@7.2.3)(react-dom@18.2.0)(react@18.2.0): + /@geometricpanda/storybook-addon-badges@1.1.1(@storybook/addons@7.2.3)(@storybook/api@6.5.16)(@storybook/components@7.2.3)(@storybook/core-events@7.3.2)(@storybook/theming@7.2.3)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-oruMaqUTq8kceJCQhoCHg4SJcCx/34j9t0g9P0krLH7cTgZQJVtmFyvtO9tMtOIU+FQUvizL+cDUt54TU5pIyQ==} peerDependencies: '@storybook/addons': ^6.5.8 @@ -5332,7 +5290,7 @@ packages: '@storybook/addons': 7.2.3(react-dom@18.2.0)(react@18.2.0) '@storybook/api': 6.5.16(react-dom@18.2.0)(react@18.2.0) '@storybook/components': 7.2.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 6.5.16 + '@storybook/core-events': 7.3.2 '@storybook/theming': 7.2.3(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -8586,6 +8544,10 @@ packages: resolution: {integrity: sha512-WWpdORiEvOl3/71xFghfEwid7ptgm9U6OxoJm8hU9e5xNuj80k2B+t4sv/iVnz872UuI5xXJqamzCqGVTblPlg==} dev: true + /@storybook/core-events@7.3.2: + resolution: {integrity: sha512-DCrM3s+sxLKS8vl0zB+1tZEtcl5XQTOGl46XgRRV/SIBabFbsC0l5pQPswWkTUsIqdREtiT0YUHcXB1+YDyFvA==} + dev: true + /@storybook/core-server@6.5.16(react-dom@18.2.0)(react@18.2.0)(typescript@4.9.5): resolution: {integrity: sha512-/3NPfmNyply395Dm0zaVZ8P9aruwO+tPx4D6/jpw8aqrRSwvAMndPMpoMCm0NXcpSm5rdX+Je4S3JW6JcggFkA==} peerDependencies: @@ -19417,7 +19379,7 @@ packages: '@babel/generator': 7.22.9 '@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.22.10) '@babel/plugin-syntax-typescript': 7.21.4(@babel/core@7.22.10) - '@babel/types': 7.22.5 + '@babel/types': 7.22.10 '@jest/expect-utils': 29.6.2 '@jest/transform': 29.6.2 '@jest/types': 29.6.1 @@ -25188,7 +25150,7 @@ packages: '@storybook/addons': 7.2.3(react-dom@18.2.0)(react@18.2.0) '@storybook/api': 7.0.12(react-dom@18.2.0)(react@18.2.0) '@storybook/components': 7.2.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0) - '@storybook/core-events': 7.2.3 + '@storybook/core-events': 7.3.2 '@storybook/global': 5.0.0 '@storybook/theming': 7.2.3(react-dom@18.2.0)(react@18.2.0) fast-deep-equal: 3.1.3 From 61a47e82c9b9e30322eeecd0b707e06a56e181d6 Mon Sep 17 00:00:00 2001 From: Philipp Gfeller <1659006+gfellerph@users.noreply.github.com> Date: Thu, 24 Aug 2023 11:55:57 +0200 Subject: [PATCH 4/4] chore: update codeowners again (#1871) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Loïc Fürhoff <12294151+imagoiq@users.noreply.github.com> --- .github/CODEOWNERS | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7a38d9c762..f48b6c1f68 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,6 +5,8 @@ # # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners -* @gfellerph @oliverschuerch @alizedebray @imagoiq +# Maintainers have to sign off PRs to main as required in branch protection rules +* @swisspost/design-system-maintainers -/packages/documentation @b1aserlu @davidritter-dotcom +# For the duration of the practical year +/packages/documentation @swisspost/design-system-maintainers @b1aserlu @davidritter-dotcom