diff --git a/.changeset/calm-rabbits-pull.md b/.changeset/calm-rabbits-pull.md new file mode 100644 index 0000000000..36ee70ea3d --- /dev/null +++ b/.changeset/calm-rabbits-pull.md @@ -0,0 +1,6 @@ +--- +'@swisspost/design-system-components': minor +'@swisspost/design-system-styles': minor +--- + +Added composable footer component. diff --git a/.changeset/gold-news-repair.md b/.changeset/gold-news-repair.md new file mode 100644 index 0000000000..f10c73d5a3 --- /dev/null +++ b/.changeset/gold-news-repair.md @@ -0,0 +1,5 @@ +--- +'@swisspost/design-system-components': minor +--- + +Added the `post-megadropdown` component. diff --git a/.changeset/gorgeous-needles-run.md b/.changeset/gorgeous-needles-run.md new file mode 100644 index 0000000000..732850bd1a --- /dev/null +++ b/.changeset/gorgeous-needles-run.md @@ -0,0 +1,5 @@ +--- +'@swisspost/design-system-components': patch +--- + +Added a fixed slot `post-list-item` on the `post-list-item` host element, so it is no longer necessary to add it manually. diff --git a/.changeset/heavy-eyes-live.md b/.changeset/heavy-eyes-live.md new file mode 100644 index 0000000000..e2d5076e51 --- /dev/null +++ b/.changeset/heavy-eyes-live.md @@ -0,0 +1,5 @@ +--- +'@swisspost/design-system-components': patch +--- + +Fixed the used `headingLevel` in the `post-accorddion-item` component. The component now uses the value from its closest `post-accorddion` parent component, if this is specified and falls back to `h2` if not specified. diff --git a/.changeset/hungry-penguins-turn.md b/.changeset/hungry-penguins-turn.md new file mode 100644 index 0000000000..f18caa33d0 --- /dev/null +++ b/.changeset/hungry-penguins-turn.md @@ -0,0 +1,5 @@ +--- +'@swisspost/design-system-styles': patch +--- + +Fixed the `btn-icon` styles, so icons within can no longer be rendered too small, because of the inline-padding on the button. diff --git a/.changeset/nervous-lizards-laugh.md b/.changeset/nervous-lizards-laugh.md new file mode 100644 index 0000000000..944f941bc9 --- /dev/null +++ b/.changeset/nervous-lizards-laugh.md @@ -0,0 +1,5 @@ +--- +'@swisspost/design-system-styles': minor +--- + +Removed outdated portal-specific styles, including subnavigation-related rules. diff --git a/.changeset/new-cougars-count.md b/.changeset/new-cougars-count.md new file mode 100644 index 0000000000..a06891fb5e --- /dev/null +++ b/.changeset/new-cougars-count.md @@ -0,0 +1,5 @@ +--- +'@swisspost/design-system-components': minor +--- + +Added the parts `button` and `body` in the `post-accordion-item` component, so one can override styles from the outside. diff --git a/.changeset/nice-cycles-provide.md b/.changeset/nice-cycles-provide.md new file mode 100644 index 0000000000..a87368195c --- /dev/null +++ b/.changeset/nice-cycles-provide.md @@ -0,0 +1,5 @@ +--- +'@swisspost/design-system-components': patch +--- + +Updated the `post-togglebutton` to function like a real button, including support for keyboard navigation and proper focus styles. diff --git a/.changeset/nine-baboons-rule.md b/.changeset/nine-baboons-rule.md new file mode 100644 index 0000000000..ecc4c1652d --- /dev/null +++ b/.changeset/nine-baboons-rule.md @@ -0,0 +1,5 @@ +--- +'@swisspost/design-system-styles': patch +--- + +Fixed the appstore-badge styles to get rid of the inline gap below. diff --git a/.changeset/old-spiders-travel.md b/.changeset/old-spiders-travel.md new file mode 100644 index 0000000000..7f4fc4731a --- /dev/null +++ b/.changeset/old-spiders-travel.md @@ -0,0 +1,5 @@ +--- +'@swisspost/design-system-styles': minor +--- + +Added the possibility to define a `$child-selector` parameter with our list mixins, so they can be used also with custom elements. diff --git a/.changeset/real-gorillas-behave.md b/.changeset/real-gorillas-behave.md new file mode 100644 index 0000000000..e90fc39826 --- /dev/null +++ b/.changeset/real-gorillas-behave.md @@ -0,0 +1,6 @@ +--- +'@swisspost/design-system-documentation': minor +'@swisspost/design-system-components': minor +--- + +Added the `post-breadcrumb` component to provide a standalone breadcrumb navigation solution. diff --git a/.changeset/tender-laws-confess.md b/.changeset/tender-laws-confess.md new file mode 100644 index 0000000000..49878a3ecc --- /dev/null +++ b/.changeset/tender-laws-confess.md @@ -0,0 +1,6 @@ +--- +'@swisspost/design-system-documentation': minor +'@swisspost/design-system-components': minor +--- + +Added the css parts `button` and `body` in the `post-accorddion-item` component. diff --git a/packages/components/cypress/e2e/footer.cy.ts b/packages/components/cypress/e2e/footer.cy.ts new file mode 100644 index 0000000000..77c7719d2b --- /dev/null +++ b/packages/components/cypress/e2e/footer.cy.ts @@ -0,0 +1,54 @@ +const FOOTER_ID = 'd97528b3-a9ef-4201-bf28-9caf6e8997dc'; + +describe('Footer', () => { + describe('Structure & Props', () => { + beforeEach(() => { + cy.getComponent('footer', FOOTER_ID); + cy.get('@footer').find('> footer h2.visually-hidden').as('label'); + }); + + it('should render', () => { + cy.get('@footer').should('exist'); + }); + + it('should set label text according to "label" prop', () => { + cy.get('@label').should('have.text', 'Footer label'); + }); + + it('should render the post-accorddion on mobile', () => { + cy.viewport('iphone-3'); + cy.get('@footer').find('post-accorddion').as('accorddion'); + + cy.get('@accorddion').should('exist'); + }); + + it('should have accorddion-items with slotted elements on mobile', () => { + cy.viewport('iphone-3'); + cy.get('@footer').find('post-accorddion').as('accorddion'); + cy.get('@accorddion').find('post-accordion-item').as('accordionItems'); + + cy.get('@accordionItems').should('have.length', 4); + cy.get('@accordionItems') + .find('slot[name="header"]') + .each($slot => { + const headerSlot = $slot.get(0) as HTMLSlotElement; + + expect(headerSlot.assignedElements().length).to.be.greaterThan(0); + }); + cy.get('@accordionItems') + .find('slot:not([name])') + .each($slot => { + const slotDefault = $slot.get(0) as HTMLSlotElement; + + expect(slotDefault.assignedElements().length).to.be.greaterThan(0); + }); + }); + }); + + describe('Accessibility', () => { + it('Has no detectable a11y violations', () => { + cy.getSnapshots('footer'); + cy.checkA11y('#root-inner'); + }); + }); +}); diff --git a/packages/components/src/animations/slide.ts b/packages/components/src/animations/slide.ts index 2bd21d7cd4..7ab26f7392 100644 --- a/packages/components/src/animations/slide.ts +++ b/packages/components/src/animations/slide.ts @@ -2,10 +2,10 @@ const easing: string = 'ease'; const duration: number = 500; const fill: FillMode = 'forwards'; -export const slideUp = (el: HTMLElement, translateSize: string = '8rem'): Animation => { +export const slideUp = (el: HTMLElement, translateSize: string = '100%'): Animation => { return el.animate( [ - { transform: `translateY(-${translateSize})` }, // Starting position (no translation) + { transform: `translateY(${translateSize})` }, // Starting position (no translation) { transform: 'translateY(0)' }, // End position ], { @@ -16,11 +16,11 @@ export const slideUp = (el: HTMLElement, translateSize: string = '8rem'): Animat ); }; -export const slideDown = (el: HTMLElement, translateSize: string = '8rem'): Animation => { +export const slideDown = (el: HTMLElement, translateSize: string = '100%'): Animation => { return el.animate( [ { transform: 'translateY(0)' }, // Starting position (no translation) - { transform: `translateY(-${translateSize})` }, // End position + { transform: `translateY(${translateSize})` }, // End position ], { duration: duration, diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index fc3cb9961f..76172f1f38 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -97,6 +97,16 @@ export namespace Components { */ "type": BannerType; } + interface PostBreadcrumb { + /** + * The text label for the home breadcrumb item. + */ + "homeText": string; + /** + * The URL for the home breadcrumb item. + */ + "homeUrl": string; + } interface PostBreadcrumbItem { /** * The optional URL to which the breadcrumb item will link. @@ -174,6 +184,12 @@ export namespace Components { */ "update": () => Promise; } + interface PostFooter { + /** + * The label to add to the footer (visually hidden). + */ + "label": string; + } interface PostHeader { /** * Toggles the mobile navigation. @@ -222,10 +238,6 @@ export namespace Components { * The ISO 639 language code, formatted according to [RFC 5646 (also known as BCP 47)](https://datatracker.ietf.org/doc/html/rfc5646). For example, "de". */ "code": string; - /** - * Used on parent component (post-language-switch) to detect elements that are manually added - */ - "generated": boolean; /** * The full name of the language. For example, "Deutsch". */ @@ -252,10 +264,6 @@ export namespace Components { * A descriptive text for the list of language options */ "description": string; - /** - * The name of the language switch, which will be used on the dropdown as an ID - */ - "name": string; /** * Variant that determines the rendering of the language switch either as a list (used on mobile in the header) or a dropdown (used on desktop in the header) */ @@ -283,25 +291,20 @@ export namespace Components { } interface PostMegadropdown { /** - * Hide megadropdown - * @returns boolean + * Displays the popover dropdown + * @param target - The HTML element relative to which the popover dropdown should be displayed. */ - "hide": () => Promise; - /** - * Show megadropdown - * @param element HTMLElement - * @returns boolean - */ - "show": (element: HTMLElement) => Promise; + "show": (target: HTMLElement) => Promise; /** - * Toggle megadropdown - * @param element HTMLElement - * @param force boolean - * @returns boolean + * Toggles the dropdown visibility based on its current state. */ - "toggle": (element: HTMLElement, force?: boolean) => Promise; + "toggle": (target: HTMLElement) => Promise; } interface PostMegadropdownTrigger { + /** + * ID of the mega dropdown element that this trigger is linked to. Used to open and close the specified mega dropdown. + */ + "for": string; } interface PostMenu { /** @@ -499,9 +502,9 @@ export interface PostMainnavigationCustomEvent extends CustomEvent { detail: T; target: HTMLPostMainnavigationElement; } -export interface PostMegadropdownTriggerCustomEvent extends CustomEvent { +export interface PostMegadropdownCustomEvent extends CustomEvent { detail: T; - target: HTMLPostMegadropdownTriggerElement; + target: HTMLPostMegadropdownElement; } export interface PostMenuCustomEvent extends CustomEvent { detail: T; @@ -561,6 +564,12 @@ declare global { prototype: HTMLPostBannerElement; new (): HTMLPostBannerElement; }; + interface HTMLPostBreadcrumbElement extends Components.PostBreadcrumb, HTMLStencilElement { + } + var HTMLPostBreadcrumbElement: { + prototype: HTMLPostBreadcrumbElement; + new (): HTMLPostBreadcrumbElement; + }; interface HTMLPostBreadcrumbItemElement extends Components.PostBreadcrumbItem, HTMLStencilElement { } var HTMLPostBreadcrumbItemElement: { @@ -617,6 +626,12 @@ declare global { prototype: HTMLPostCollapsibleTriggerElement; new (): HTMLPostCollapsibleTriggerElement; }; + interface HTMLPostFooterElement extends Components.PostFooter, HTMLStencilElement { + } + var HTMLPostFooterElement: { + prototype: HTMLPostFooterElement; + new (): HTMLPostFooterElement; + }; interface HTMLPostHeaderElement extends Components.PostHeader, HTMLStencilElement { } var HTMLPostHeaderElement: { @@ -690,25 +705,25 @@ declare global { prototype: HTMLPostMainnavigationElement; new (): HTMLPostMainnavigationElement; }; - interface HTMLPostMegadropdownElement extends Components.PostMegadropdown, HTMLStencilElement { - } - var HTMLPostMegadropdownElement: { - prototype: HTMLPostMegadropdownElement; - new (): HTMLPostMegadropdownElement; - }; - interface HTMLPostMegadropdownTriggerElementEventMap { - "postToggle": any; + interface HTMLPostMegadropdownElementEventMap { + "postToggleMegadropdown": boolean; } - interface HTMLPostMegadropdownTriggerElement extends Components.PostMegadropdownTrigger, HTMLStencilElement { - addEventListener(type: K, listener: (this: HTMLPostMegadropdownTriggerElement, ev: PostMegadropdownTriggerCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + interface HTMLPostMegadropdownElement extends Components.PostMegadropdown, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLPostMegadropdownElement, ev: PostMegadropdownCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLPostMegadropdownTriggerElement, ev: PostMegadropdownTriggerCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLPostMegadropdownElement, ev: PostMegadropdownCustomEvent) => any, options?: boolean | EventListenerOptions): void; removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; } + var HTMLPostMegadropdownElement: { + prototype: HTMLPostMegadropdownElement; + new (): HTMLPostMegadropdownElement; + }; + interface HTMLPostMegadropdownTriggerElement extends Components.PostMegadropdownTrigger, HTMLStencilElement { + } var HTMLPostMegadropdownTriggerElement: { prototype: HTMLPostMegadropdownTriggerElement; new (): HTMLPostMegadropdownTriggerElement; @@ -836,11 +851,13 @@ declare global { "post-avatar": HTMLPostAvatarElement; "post-back-to-top": HTMLPostBackToTopElement; "post-banner": HTMLPostBannerElement; + "post-breadcrumb": HTMLPostBreadcrumbElement; "post-breadcrumb-item": HTMLPostBreadcrumbItemElement; "post-card-control": HTMLPostCardControlElement; "post-closebutton": HTMLPostClosebuttonElement; "post-collapsible": HTMLPostCollapsibleElement; "post-collapsible-trigger": HTMLPostCollapsibleTriggerElement; + "post-footer": HTMLPostFooterElement; "post-header": HTMLPostHeaderElement; "post-icon": HTMLPostIconElement; "post-language-option": HTMLPostLanguageOptionElement; @@ -933,6 +950,16 @@ declare namespace LocalJSX { */ "type"?: BannerType; } + interface PostBreadcrumb { + /** + * The text label for the home breadcrumb item. + */ + "homeText"?: string; + /** + * The URL for the home breadcrumb item. + */ + "homeUrl"?: string; + } interface PostBreadcrumbItem { /** * The optional URL to which the breadcrumb item will link. @@ -1006,6 +1033,12 @@ declare namespace LocalJSX { */ "for"?: string; } + interface PostFooter { + /** + * The label to add to the footer (visually hidden). + */ + "label": string; + } interface PostHeader { } /** @@ -1050,10 +1083,6 @@ declare namespace LocalJSX { * The ISO 639 language code, formatted according to [RFC 5646 (also known as BCP 47)](https://datatracker.ietf.org/doc/html/rfc5646). For example, "de". */ "code": string; - /** - * Used on parent component (post-language-switch) to detect elements that are manually added - */ - "generated"?: boolean; /** * The full name of the language. For example, "Deutsch". */ @@ -1080,10 +1109,6 @@ declare namespace LocalJSX { * A descriptive text for the list of language options */ "description"?: string; - /** - * The name of the language switch, which will be used on the dropdown as an ID - */ - "name"?: string; /** * Variant that determines the rendering of the language switch either as a list (used on mobile in the header) or a dropdown (used on desktop in the header) */ @@ -1114,12 +1139,16 @@ declare namespace LocalJSX { "onPostToggle"?: (event: PostMainnavigationCustomEvent) => void; } interface PostMegadropdown { + /** + * Emits when the dropdown is shown or hidden. The event payload is a boolean: `true` when the dropdown was opened, `false` when it was closed. + */ + "onPostToggleMegadropdown"?: (event: PostMegadropdownCustomEvent) => void; } interface PostMegadropdownTrigger { /** - * Emits after each toggle + * ID of the mega dropdown element that this trigger is linked to. Used to open and close the specified mega dropdown. */ - "onPostToggle"?: (event: PostMegadropdownTriggerCustomEvent) => void; + "for": string; } interface PostMenu { /** @@ -1259,11 +1288,13 @@ declare namespace LocalJSX { "post-avatar": PostAvatar; "post-back-to-top": PostBackToTop; "post-banner": PostBanner; + "post-breadcrumb": PostBreadcrumb; "post-breadcrumb-item": PostBreadcrumbItem; "post-card-control": PostCardControl; "post-closebutton": PostClosebutton; "post-collapsible": PostCollapsible; "post-collapsible-trigger": PostCollapsibleTrigger; + "post-footer": PostFooter; "post-header": PostHeader; "post-icon": PostIcon; "post-language-option": PostLanguageOption; @@ -1297,6 +1328,7 @@ declare module "@stencil/core" { "post-avatar": LocalJSX.PostAvatar & JSXBase.HTMLAttributes; "post-back-to-top": LocalJSX.PostBackToTop & JSXBase.HTMLAttributes; "post-banner": LocalJSX.PostBanner & JSXBase.HTMLAttributes; + "post-breadcrumb": LocalJSX.PostBreadcrumb & JSXBase.HTMLAttributes; "post-breadcrumb-item": LocalJSX.PostBreadcrumbItem & JSXBase.HTMLAttributes; /** * @class PostCardControl - representing a stencil component @@ -1305,6 +1337,7 @@ declare module "@stencil/core" { "post-closebutton": LocalJSX.PostClosebutton & JSXBase.HTMLAttributes; "post-collapsible": LocalJSX.PostCollapsible & JSXBase.HTMLAttributes; "post-collapsible-trigger": LocalJSX.PostCollapsibleTrigger & JSXBase.HTMLAttributes; + "post-footer": LocalJSX.PostFooter & JSXBase.HTMLAttributes; "post-header": LocalJSX.PostHeader & JSXBase.HTMLAttributes; /** * @class PostIcon - representing a stencil component diff --git a/packages/components/src/components/post-accordion-item/post-accordion-item.tsx b/packages/components/src/components/post-accordion-item/post-accordion-item.tsx index 7421822a10..235d9ce6e1 100644 --- a/packages/components/src/components/post-accordion-item/post-accordion-item.tsx +++ b/packages/components/src/components/post-accordion-item/post-accordion-item.tsx @@ -4,6 +4,8 @@ import { HEADING_LEVELS, HeadingLevel } from '@/types'; import { checkEmptyOrOneOf } from '@/utils'; /** + * @part button - The pseudo-element, used to override styles on the components internal header `button` element. + * @part body - The pseudo-element, used to override styles on the components internal `body` element. * @slot logo - Slot for the placing a logo before the header. * @slot header - Slot for placing custom content within the accordion item's header. * @slot default - Slot for placing content within the accordion item's body. @@ -79,14 +81,19 @@ export class PostAccordionItem { } render() { - const HeadingTag = `h${this.headingLevel ?? 2}`; + const headingLevel = this.host.closest('post-accorddion')?.getAttribute('heading-level'); + const HeadingTag = `h${headingLevel ?? this.headingLevel ?? 2}`; return (
- + + + {visibleItems.map((item) => ( + { + if (e.key === 'Enter' || e.key === ' ') { + const linkElement = (e.currentTarget as HTMLElement).querySelector('a'); + linkElement?.click(); + e.preventDefault(); + } + }} + > + {item.url ? {item.text} : {item.text}} + + ))} + +
+ + ) : ( + visibleItems.map((item) => ( + + {item.text} + + )) + )} + + {this.lastItem && ( + + {this.lastItem.text} + + )} + + + {/* Hidden items for width calculation */} +
+ + {this.homeText} + + + {this.breadcrumbItems.map((item) => ( + + {item.text} + + ))} +
+ +
+ ); + } +} diff --git a/packages/components/src/components/post-breadcrumb/readme.md b/packages/components/src/components/post-breadcrumb/readme.md new file mode 100644 index 0000000000..916ff74906 --- /dev/null +++ b/packages/components/src/components/post-breadcrumb/readme.md @@ -0,0 +1,41 @@ +# post-breadcrumbs-new + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ---------- | ----------- | -------------------------------------------- | -------- | ----------- | +| `homeText` | `home-text` | The text label for the home breadcrumb item. | `string` | `'Home'` | +| `homeUrl` | `home-url` | The URL for the home breadcrumb item. | `string` | `undefined` | + + +## Dependencies + +### Depends on + +- [post-icon](../post-icon) +- [post-menu-trigger](../post-menu-trigger) +- [post-menu](../post-menu) +- [post-menu-item](../post-menu-item) +- [post-breadcrumb-item](../post-breadcrumb-item) + +### Graph +```mermaid +graph TD; + post-breadcrumb --> post-icon + post-breadcrumb --> post-menu-trigger + post-breadcrumb --> post-menu + post-breadcrumb --> post-menu-item + post-breadcrumb --> post-breadcrumb-item + post-menu --> post-popovercontainer + post-breadcrumb-item --> post-icon + style post-breadcrumb fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/components/src/components/post-closebutton/post-closebutton.tsx b/packages/components/src/components/post-closebutton/post-closebutton.tsx index 68d13ef51a..0d447ab844 100644 --- a/packages/components/src/components/post-closebutton/post-closebutton.tsx +++ b/packages/components/src/components/post-closebutton/post-closebutton.tsx @@ -15,7 +15,7 @@ export class PostClosebutton { return ( diff --git a/packages/components/src/components/post-language-switch/readme.md b/packages/components/src/components/post-language-switch/readme.md index c2079be7d3..8beb610dfa 100644 --- a/packages/components/src/components/post-language-switch/readme.md +++ b/packages/components/src/components/post-language-switch/readme.md @@ -9,7 +9,6 @@ | ------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ----------- | | `caption` | `caption` | A title for the list of language options | `string` | `undefined` | | `description` | `description` | A descriptive text for the list of language options | `string` | `undefined` | -| `name` | `name` | The name of the language switch, which will be used on the dropdown as an ID | `string` | `undefined` | | `variant` | `variant` | Variant that determines the rendering of the language switch either as a list (used on mobile in the header) or a dropdown (used on desktop in the header) | `"dropdown" \| "list"` | `'list'` | diff --git a/packages/components/src/components/post-list-item/post-list-item.tsx b/packages/components/src/components/post-list-item/post-list-item.tsx index fdb5c6b012..14707488ae 100644 --- a/packages/components/src/components/post-list-item/post-list-item.tsx +++ b/packages/components/src/components/post-list-item/post-list-item.tsx @@ -18,7 +18,7 @@ export class PostListItem { render() { return ( - + ); diff --git a/packages/components/src/components/post-mainnavigation/post-mainnavigation.scss b/packages/components/src/components/post-mainnavigation/post-mainnavigation.scss index 7ac5a14022..9e767ffa25 100644 --- a/packages/components/src/components/post-mainnavigation/post-mainnavigation.scss +++ b/packages/components/src/components/post-mainnavigation/post-mainnavigation.scss @@ -1,29 +1,29 @@ @use '@swisspost/design-system-styles/mixins/button'; @use '@swisspost/design-system-styles/mixins/icons'; @use '@swisspost/design-system-styles/mixins/media'; -@use '@swisspost/design-system-styles/mixins/utilities'; -@use '@swisspost/design-system-styles/functions/icons' as icon-fn; @use '@swisspost/design-system-styles/functions/tokens'; @use '@swisspost/design-system-styles/tokens/elements'; @use '@swisspost/design-system-styles/variables/animation'; +@use '@swisspost/design-system-styles/components/header/mixins' as header-mx; $nav-height: var(--post-core-dimension-56); post-mainnavigation { // reset links and buttons post-list-item { - a { + > a { text-decoration: none; border-radius: 0; } - button { + post-megadropdown-trigger button { @include button.reset-button; text-align: start; } - a, - button { + > a, + > button, + post-megadropdown-trigger button { display: flex; align-items: center; justify-content: space-between; @@ -51,7 +51,7 @@ post-mainnavigation { post-list { margin-inline: var(--post-core-dimension-4); - > [role="list"] { + > [role='list'] { flex-direction: row; max-width: 100vw; transition: transform animation.$transition-base-timing; @@ -59,8 +59,9 @@ post-mainnavigation { } post-list-item { - a, - button { + > a, + > button, + post-megadropdown-trigger button { padding-inline: var(--post-core-dimension-12); height: $nav-height; gap: var(--post-core-dimension-4); @@ -79,7 +80,8 @@ post-mainnavigation { } // styles specific to for the mega-dropdown buttons - button { + > button, + post-megadropdown-trigger button { padding-inline-end: var(--post-core-dimension-12); transition: border-block-end-color animation.$transition-base-timing; @@ -97,40 +99,22 @@ post-mainnavigation { } } - [slot="back-button"] { + [slot='back-button'] { display: none; } } // tablet/mobile styles @include media.max(lg) { - post-list > [role="list"] { + post-list > [role='list'] { transform: none !important; } post-list-item { - a, - button { - width: 100%; - height: var(--post-core-dimension-48); - padding-inline-end: var(--post-core-dimension-6); - gap: var(--post-core-dimension-16); - border-block: var(--post-core-dimension-1) solid transparent; - border-block-end-color: currentColor; - font-weight: var(--post-core-font-weight-700); - - &:hover, - &.selected { - border-block-width: var(--post-core-dimension-3); - } - - &:hover::after { - content: ''; - display: block; - @include icons.icon(3020); - width: var(--post-core-dimension-24); - height: var(--post-core-dimension-24); - } + > a, + > button, + post-megadropdown-trigger button { + @include header-mx.mobile-header-interactive; } } } diff --git a/packages/components/src/components/post-megadropdown-trigger/post-megadropdown-trigger.scss b/packages/components/src/components/post-megadropdown-trigger/post-megadropdown-trigger.scss index 48bb062ea7..33bbd29e3e 100644 --- a/packages/components/src/components/post-megadropdown-trigger/post-megadropdown-trigger.scss +++ b/packages/components/src/components/post-megadropdown-trigger/post-megadropdown-trigger.scss @@ -1,3 +1,3 @@ -:host { - display: inline-block; +post-megadropdown-trigger { + width: 100%; } diff --git a/packages/components/src/components/post-megadropdown-trigger/post-megadropdown-trigger.tsx b/packages/components/src/components/post-megadropdown-trigger/post-megadropdown-trigger.tsx index 246c9d2c95..a9d5e32f23 100644 --- a/packages/components/src/components/post-megadropdown-trigger/post-megadropdown-trigger.tsx +++ b/packages/components/src/components/post-megadropdown-trigger/post-megadropdown-trigger.tsx @@ -1,27 +1,75 @@ -import { Component, Event, EventEmitter, Host, State, h } from '@stencil/core'; +import { Component, Element, Prop, h, Host, State, Watch } from '@stencil/core'; +import { version } from '@root/package.json'; +import { checkType } from '@/utils'; @Component({ tag: 'post-megadropdown-trigger', - shadow: true, - styleUrl: './post-megadropdown-trigger.scss', + styleUrl: 'post-megadropdown-trigger.scss', + shadow: false, }) export class PostMegadropdownTrigger { /** - * Emits after each toggle + * ID of the mega dropdown element that this trigger is linked to. Used to open and close the specified mega dropdown. */ - @Event() postToggle: EventEmitter; + @Prop() for!: string; - @State() toggled = false; + @Element() host: HTMLPostMegadropdownTriggerElement; - private handleClick() { - this.toggled = !this.toggled; - this.postToggle.emit(); + /** + * Manages the accessibility attribute `aria-expanded` to indicate whether the associated mega dropdown is expanded or collapsed. + */ + @State() ariaExpanded: boolean = false; + + /** + * Reference to the slotted button within the trigger, if present. + * Used to manage click and key events for mega dropdown control. + */ + private slottedButton: HTMLButtonElement | null = null; + + /** + * Watch for changes to the `for` property to validate its type and ensure it is a string. + * @param forValue - The new value of the `for` property. + */ + @Watch('for') + validateControlFor(forValue = this.for) { + checkType(forValue, 'string', 'The "for" property is required and should be a string.'); + } + + private get megadropdown(): HTMLPostMegadropdownElement | null { + const ref = document.getElementById(this.for); + return ref && ref.localName === 'post-megadropdown' + ? (ref as HTMLPostMegadropdownElement) + : null; + } + + private handleToggle() { + if (this.megadropdown && this.slottedButton) { + this.ariaExpanded = !this.ariaExpanded; + this.slottedButton.setAttribute('aria-expanded', this.ariaExpanded.toString()); + this.megadropdown.toggle(this.host); + } else { + console.warn(`No post-megadropdown found with ID: ${this.for}`); + } + } + + componentDidLoad() { + this.validateControlFor(); + + this.slottedButton = this.host.querySelector('button'); + if (this.slottedButton) { + this.slottedButton.setAttribute('aria-haspopup', 'menu'); + this.slottedButton.addEventListener('click', () => { + this.handleToggle(); + }); + } else { + console.warn('No button found within post-megadropdown-trigger'); + } } render() { return ( - - diff --git a/packages/components/src/components/post-megadropdown-trigger/readme.md b/packages/components/src/components/post-megadropdown-trigger/readme.md index 870d32aabc..9488d51aa9 100644 --- a/packages/components/src/components/post-megadropdown-trigger/readme.md +++ b/packages/components/src/components/post-megadropdown-trigger/readme.md @@ -5,11 +5,11 @@ -## Events +## Properties -| Event | Description | Type | -| ------------ | ----------------------- | ------------------ | -| `postToggle` | Emits after each toggle | `CustomEvent` | +| Property | Attribute | Description | Type | Default | +| ------------------ | --------- | ------------------------------------------------------------------------------------------------------------------- | -------- | ----------- | +| `for` _(required)_ | `for` | ID of the mega dropdown element that this trigger is linked to. Used to open and close the specified mega dropdown. | `string` | `undefined` | ---------------------------------------------- diff --git a/packages/components/src/components/post-megadropdown/post-megadropdown.scss b/packages/components/src/components/post-megadropdown/post-megadropdown.scss index ffc7413fc4..ba5e0d7465 100644 --- a/packages/components/src/components/post-megadropdown/post-megadropdown.scss +++ b/packages/components/src/components/post-megadropdown/post-megadropdown.scss @@ -1,6 +1,24 @@ @use '@swisspost/design-system-styles/mixins/media'; @use '@swisspost/design-system-styles/variables/color'; +@keyframes slide-in { + 0% { + transform: translateX(100%); + } + 100% { + transform: translateX(0%); + } +} + +@keyframes slide-out { + from { + transform: translateX(0%); + } + to { + transform: translateX(100%); + } +} + *, ::before, ::after { @@ -8,64 +26,175 @@ } post-popovercontainer { + --post-global-header-height: 72px; + --post-main-header-height: 56px; + --post-header-height: calc(var(--post-global-header-height) + var(--post-main-header-height)); + background-color: #fafafa; + border: none; + border-top: 1px solid #e1e0dc; width: 100%; - border-bottom: 1px solid black; + border-radius: 0; + box-shadow: 0 8px 6px rgba(0, 0, 0, 16%); @include media.max(lg) { - // Overrides floating-ui inline styles on mobile + --post-global-header-height: 64px; position: absolute; - top: var(--header-height) !important; + top: var(--post-header-height) !important; bottom: 0; left: 0; width: 100%; height: auto; + + &.slide-in { + animation: slide-in; + animation-duration: 350ms; + animation-fill-mode: forwards; + } + + &.slide-out { + animation: slide-out; + animation-duration: 350ms; + animation-fill-mode: forwards; + } } } .megadropdown { - display: flex; - gap: 1rem; - padding: 1.5rem; - border-radius: 0 0 3px 3px; + padding: 31px 40px 40px; @include media.max(lg) { - flex-direction: column; + padding: 16px 32px 24px; + } + + .megadropdown-content { + gap: 1.5rem; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(15vw, 100%), 1fr)); + grid-auto-rows: 1fr auto; + grid-auto-flow: dense; + + @include media.max(lg) { + grid-template-columns: repeat(auto-fit, minmax(min(35vw, 100%), 1fr)); + } + } + + h2 { + display: none; + margin-top: 0; + margin-bottom: 8px; + font-weight: 900; + + a { + text-decoration: none; + font-weight: inherit; + border-bottom: 0; + border-top: 0; + } + + @include media.max(lg) { + display: block; + font-size: 20px; + } + } + + // Unset styles added by parent main navigation + a:hover { + border-block-width: 0; + + &::after { + content: unset; + } } post-list { - display: flex; - flex-direction: column; - gap: 0.5rem; + display: grid; + grid-row: span 2; + grid-template-rows: subgrid; + row-gap: 0px; + + > [role='list'] { + flex-direction: unset; + display: unset; + } + + > * { + width: 100%; + } + + .list-title { + display: flex; + flex-direction: column-reverse; + height: 100%; + + h3 { + font-size: 20px; + border-bottom: 1px solid #050400; + padding: 15px 12px 14px 0; + display: block; + font-weight: 900; + + a { + text-decoration: none; + width: 100%; + font-size: inherit; + display: inline-block; + font-weight: inherit; + height: unset; + padding: 0; + border: 0; + } + + @include media.max(lg) { + font-size: 16px; + padding: 12px 12px 11px 0; + } + } + } } post-list-item { - border-bottom: 1px solid gray; + border-bottom: 1px solid #050400; > a { - padding-block: 0.25rem; + padding: 13px 12px 12px 0; display: block; text-decoration: none; + width: 100%; + border-bottom: 0; + height: auto; + border-top: 0; + + @include media.max(lg) { + padding: 12px 12px 11px 0; + } } } -} -.back-button { - display: none; + .back-button { + display: none; + margin-bottom: 2rem; + + .btn { + width: auto; + } - @include media.max(lg) { - display: block; + post-icon { + transform: rotate(180deg); + } + + @include media.max(lg) { + display: block; + } } -} -post-closebutton { - margin-left: auto; + .close-button { + position: absolute; + top: 1rem; + right: 1rem; - @include media.max(lg) { - display: none; + @include media.max(lg) { + display: none; + } } } - -h3 { - font-size: 20px; -} diff --git a/packages/components/src/components/post-megadropdown/post-megadropdown.tsx b/packages/components/src/components/post-megadropdown/post-megadropdown.tsx index 3709c59e5a..03d0ff5db9 100644 --- a/packages/components/src/components/post-megadropdown/post-megadropdown.tsx +++ b/packages/components/src/components/post-megadropdown/post-megadropdown.tsx @@ -1,56 +1,105 @@ -import { Component, Host, Method, h } from '@stencil/core'; +import { Component, Element, Event, EventEmitter, h, Host, Method, State } from '@stencil/core'; @Component({ tag: 'post-megadropdown', + styleUrl: 'post-megadropdown.scss', shadow: false, - styleUrl: './post-megadropdown.scss', }) export class PostMegadropdown { private popoverRef: HTMLPostPopovercontainerElement; + @Element() host: HTMLPostMegadropdownElement; + /** - * Show megadropdown - * @param element HTMLElement - * @returns boolean + * Holds the current visibility state of the dropdown. + * This state is internally managed to track whether the dropdown is open (`true`) or closed (`false`), + * and updates automatically when the dropdown is toggled. */ - @Method() - async show(element: HTMLElement) { - return this.popoverRef.show(element); + @State() isVisible: boolean = false; + + @State() animationClass: string | null = null; + + /** + * Emits when the dropdown is shown or hidden. + * The event payload is a boolean: `true` when the dropdown was opened, `false` when it was closed. + **/ + @Event() postToggleMegadropdown: EventEmitter; + + componentDidLoad() { + this.popoverRef.addEventListener('postToggle', (event: CustomEvent) => { + this.isVisible = event.detail; + this.postToggleMegadropdown.emit(this.isVisible); + }); + + this.popoverRef.addEventListener('animationend', () => { + if (this.animationClass === 'slide-out') { + this.hide(); + } + }); } /** - * Hide megadropdown - * @returns boolean + * Toggles the dropdown visibility based on its current state. */ @Method() - async hide() { - return this.popoverRef.hide(); + async toggle(target: HTMLElement) { + this.isVisible ? this.hide() : await this.show(target); } /** - * Toggle megadropdown - * @param element HTMLElement - * @param force boolean - * @returns boolean + * Displays the popover dropdown + * + * @param target - The HTML element relative to which the popover dropdown should be displayed. */ @Method() - async toggle(element: HTMLElement, force?: boolean) { - return this.popoverRef.toggle(element, force ?? undefined); + async show(target: HTMLElement) { + if (this.popoverRef) { + await this.popoverRef.show(target); + this.animationClass = 'slide-in'; + } else { + console.error('show: popoverRef is null or undefined'); + } + } + + /** + * Hides the popover dropdown + */ + private hide() { + if (this.popoverRef) { + this.popoverRef.hide(); + } else { + console.error('hide: popoverRef is null or undefined'); + } } private handleBackButtonClick() { - this.hide(); + this.animationClass = 'slide-out'; + } + + private handleCloseButtonClick() { + this.popoverRef.hide(); } render() { return ( - - (this.popoverRef = el)}> + + (this.popoverRef = el)} + >
this.handleBackButtonClick()} class="back-button">
- +
this.handleCloseButtonClick()} class="close-button"> + +
+ +
+ +
diff --git a/packages/components/src/components/post-megadropdown/readme.md b/packages/components/src/components/post-megadropdown/readme.md index 532147ae8e..368d1a787f 100644 --- a/packages/components/src/components/post-megadropdown/readme.md +++ b/packages/components/src/components/post-megadropdown/readme.md @@ -5,50 +5,46 @@ -## Methods - -### `hide() => Promise` +## Events -Hide megadropdown +| Event | Description | Type | +| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | +| `postToggleMegadropdown` | Emits when the dropdown is shown or hidden. The event payload is a boolean: `true` when the dropdown was opened, `false` when it was closed. | `CustomEvent` | -#### Returns - -Type: `Promise` -boolean +## Methods -### `show(element: HTMLElement) => Promise` +### `show(target: HTMLElement) => Promise` -Show megadropdown +Displays the popover dropdown #### Parameters -| Name | Type | Description | -| --------- | ------------- | ----------- | -| `element` | `HTMLElement` | HTMLElement | +| Name | Type | Description | +| -------- | ------------- | ------------------------------------------------------------------------------ | +| `target` | `HTMLElement` | - The HTML element relative to which the popover dropdown should be displayed. | #### Returns Type: `Promise` -boolean -### `toggle(element: HTMLElement, force?: boolean) => Promise` -Toggle megadropdown +### `toggle(target: HTMLElement) => Promise` + +Toggles the dropdown visibility based on its current state. #### Parameters -| Name | Type | Description | -| --------- | ------------- | ----------- | -| `element` | `HTMLElement` | HTMLElement | -| `force` | `boolean` | boolean | +| Name | Type | Description | +| -------- | ------------- | ----------- | +| `target` | `HTMLElement` | | #### Returns -Type: `Promise` +Type: `Promise` + -boolean ## Dependencies diff --git a/packages/components/src/components/post-menu-item/readme.md b/packages/components/src/components/post-menu-item/readme.md index d6c88bc316..eeeef359b7 100644 --- a/packages/components/src/components/post-menu-item/readme.md +++ b/packages/components/src/components/post-menu-item/readme.md @@ -5,6 +5,19 @@ +## Dependencies + +### Used by + + - [post-breadcrumb](../post-breadcrumb) + +### Graph +```mermaid +graph TD; + post-breadcrumb --> post-menu-item + style post-menu-item fill:#f9f,stroke:#333,stroke-width:4px +``` + ---------------------------------------------- *Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/components/src/components/post-menu-trigger/readme.md b/packages/components/src/components/post-menu-trigger/readme.md index 5198f2ceaf..d8af67b195 100644 --- a/packages/components/src/components/post-menu-trigger/readme.md +++ b/packages/components/src/components/post-menu-trigger/readme.md @@ -16,11 +16,13 @@ ### Used by + - [post-breadcrumb](../post-breadcrumb) - [post-language-switch](../post-language-switch) ### Graph ```mermaid graph TD; + post-breadcrumb --> post-menu-trigger post-language-switch --> post-menu-trigger style post-menu-trigger fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/components/src/components/post-menu/post-menu.tsx b/packages/components/src/components/post-menu/post-menu.tsx index dd5c90f3bf..865a24a729 100644 --- a/packages/components/src/components/post-menu/post-menu.tsx +++ b/packages/components/src/components/post-menu/post-menu.tsx @@ -2,6 +2,7 @@ import { Component, Element, Event, EventEmitter, h, Host, Method, Prop, State } import { Placement } from '@floating-ui/dom'; import { version } from '@root/package.json'; import { isFocusable } from '@/utils/is-focusable'; +import { getRoot } from '@/utils'; @Component({ tag: 'post-menu', @@ -20,7 +21,7 @@ export class PostMenu { TAB: 'Tab', HOME: 'Home', END: 'End', - ESCAPE: 'Escape' + ESCAPE: 'Escape', }; @Element() host: HTMLPostMenuElement; @@ -45,7 +46,10 @@ export class PostMenu { **/ @Event() toggleMenu: EventEmitter; + private root?: Document | ShadowRoot; + connectedCallback() { + this.root = getRoot(this.host); this.host.addEventListener('keydown', this.handleKeyDown); this.host.addEventListener('click', this.handleClick); } @@ -79,7 +83,7 @@ export class PostMenu { async show(target: HTMLElement) { if (this.popoverRef) { await this.popoverRef.show(target); - this.lastFocusedElement = document.activeElement as HTMLElement; + this.lastFocusedElement = this.root.activeElement as HTMLElement; // Use root's activeElement const menuItems = this.getSlottedItems(); if (menuItems.length > 0) { @@ -131,7 +135,7 @@ export class PostMenu { return; } - const currentFocusedElement = document.activeElement as HTMLElement; + const currentFocusedElement = this.root.activeElement as HTMLElement; // Use root's activeElement let currentIndex = menuItems.findIndex(el => el === currentFocusedElement); switch (e.key) { @@ -185,7 +189,7 @@ export class PostMenu { return ( (this.popoverRef = e)}> -
+
diff --git a/packages/components/src/components/post-menu/readme.md b/packages/components/src/components/post-menu/readme.md index 2372e47c18..3361253421 100644 --- a/packages/components/src/components/post-menu/readme.md +++ b/packages/components/src/components/post-menu/readme.md @@ -64,10 +64,18 @@ Type: `Promise` +## Shadow Parts + +| Part | Description | +| --------------------- | ----------- | +| `"popover-container"` | | + + ## Dependencies ### Used by + - [post-breadcrumb](../post-breadcrumb) - [post-language-switch](../post-language-switch) ### Depends on @@ -78,6 +86,7 @@ Type: `Promise` ```mermaid graph TD; post-menu --> post-popovercontainer + post-breadcrumb --> post-menu post-language-switch --> post-menu style post-menu fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/components/src/components/post-togglebutton/post-togglebutton.scss b/packages/components/src/components/post-togglebutton/post-togglebutton.scss index b73f35a5b6..88ee2219af 100644 --- a/packages/components/src/components/post-togglebutton/post-togglebutton.scss +++ b/packages/components/src/components/post-togglebutton/post-togglebutton.scss @@ -1,5 +1,20 @@ +@use '@swisspost/design-system-styles/mixins/utilities'; +@use '@swisspost/design-system-styles/functions/tokens'; +@use '@swisspost/design-system-styles/tokens/helpers'; + :host { cursor: pointer; + outline-offset: tokens.get('focus-outline-offset', helpers.$post-focus) !important; + outline: tokens.get('focus-outline-color', helpers.$post-focus) none + tokens.get('focus-outline-width', helpers.$post-focus) !important; +} + +:host(:focus-visible) { + outline-style: tokens.get('focus-border-style', helpers.$post-focus) !important; + + @include utilities.high-contrast-mode() { + outline-color: Highlight !important; + } } :host([aria-pressed="true"]) { diff --git a/packages/components/src/components/post-togglebutton/post-togglebutton.tsx b/packages/components/src/components/post-togglebutton/post-togglebutton.tsx index b34c4f8282..b0eb329d9d 100644 --- a/packages/components/src/components/post-togglebutton/post-togglebutton.tsx +++ b/packages/components/src/components/post-togglebutton/post-togglebutton.tsx @@ -1,4 +1,4 @@ -import { Component, Host, h, Prop, Watch } from '@stencil/core'; +import { Component, Host, h, Prop, Watch, Element } from '@stencil/core'; import { version } from '@root/package.json'; import { checkType } from '@/utils'; @@ -12,6 +12,8 @@ import { checkType } from '@/utils'; shadow: true, }) export class PostTogglebutton { + @Element() host: HTMLPostTogglebuttonElement; + /** * If `true`, the button is in the "on" state, otherwise it is in the "off" state. */ @@ -28,6 +30,10 @@ export class PostTogglebutton { componentWillLoad() { this.validateToggled(); + + // add event listener to not override listener that might be set on the host + this.host.addEventListener('click', () => this.handleClick()); + this.host.addEventListener('keydown', (e: KeyboardEvent) => this.handleKeydown(e)); } private handleClick = () => { @@ -35,8 +41,10 @@ export class PostTogglebutton { }; private handleKeydown = (event: KeyboardEvent) => { - if (event.key === 'Enter') { - this.toggled = !this.toggled; + // perform a click when enter or spaced are pressed to mimic the button behavior + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); // prevents the page from scrolling when space is pressed + this.host.click(); } }; @@ -44,12 +52,10 @@ export class PostTogglebutton { return ( diff --git a/packages/components/src/index.html b/packages/components/src/index.html index dfa6592340..60605cbfb7 100644 --- a/packages/components/src/index.html +++ b/packages/components/src/index.html @@ -22,12 +22,16 @@ - = Menu + + Menu + + + Application title @@ -66,10 +80,14 @@

Main Navigation

- Briefe - - -

Briefe title

+ Briefe + + + Schliessen +

Briefe title

Briefe senden

Briefe Schweiz @@ -84,14 +102,17 @@

Schritt für Schritt

Waren Ausland Express und Kurier
- Schliessen
- Pakete - - -

Pakete title

+ Pakete + + + Schliessen +

Pakete title

Pakete senden

Pakete Schweiz @@ -106,7 +127,6 @@

Schritt für Schritt

Waren Ausland Express und Kurier
- Schliessen
diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 8dd02245f9..ee02397ab8 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -3,33 +3,35 @@ export { Components, JSX } from './components'; // Export every single component so it gets included in the dist output export { PostAccordion } from './components/post-accordion/post-accordion'; export { PostAccordionItem } from './components/post-accordion-item/post-accordion-item'; +export { PostAvatar } from './components/post-avatar/post-avatar'; export { PostBackToTop } from './components/post-back-to-top/post-back-to-top'; export { PostBanner } from './components/post-banner/post-banner'; +export { PostBreadcrumb } from './components/post-breadcrumb/post-breadcrumb'; export { PostBreadcrumbItem } from './components/post-breadcrumb-item/post-breadcrumb-item'; -export { PostAvatar } from './components/post-avatar/post-avatar'; export { PostCardControl } from './components/post-card-control/post-card-control'; export { PostClosebutton } from './components/post-closebutton/post-closebutton'; export { PostCollapsible } from './components/post-collapsible/post-collapsible'; export { PostCollapsibleTrigger } from './components/post-collapsible-trigger/post-collapsible-trigger'; +export { PostFooter } from './components/post-footer/post-footer'; +export { PostHeader } from './components/post-header/post-header'; export { PostIcon } from './components/post-icon/post-icon'; export { PostLanguageOption } from './components/post-language-option/post-language-option'; export { PostLanguageSwitch } from './components/post-language-switch/post-language-switch'; +export { PostList } from './components/post-list/post-list'; +export { PostListItem } from './components/post-list-item/post-list-item'; export { PostLogo } from './components/post-logo/post-logo'; +export { PostMainnavigation } from './components/post-mainnavigation/post-mainnavigation'; +export { PostMegadropdown } from './components/post-megadropdown/post-megadropdown'; +export { PostMegadropdownTrigger } from './components/post-megadropdown-trigger/post-megadropdown-trigger'; export { PostMenu } from './components/post-menu/post-menu'; -export { PostMenuTrigger } from './components/post-menu-trigger/post-menu-trigger'; export { PostMenuItem } from './components/post-menu-item/post-menu-item'; +export { PostMenuTrigger } from './components/post-menu-trigger/post-menu-trigger'; export { PostPopover } from './components/post-popover/post-popover'; export { PostPopovercontainer } from './components/post-popovercontainer/post-popovercontainer'; export { PostRating } from './components/post-rating/post-rating'; export { PostTabs } from './components/post-tabs/post-tabs'; export { PostTabHeader } from './components/post-tab-header/post-tab-header'; export { PostTabPanel } from './components/post-tab-panel/post-tab-panel'; -export { PostTooltip } from './components/post-tooltip/post-tooltip'; export { PostTag } from './components/post-tag/post-tag'; export { PostTogglebutton } from './components/post-togglebutton/post-togglebutton'; -export { PostList } from './components/post-list/post-list'; -export { PostListItem } from './components/post-list-item/post-list-item'; -export { PostHeader } from './components/post-header/post-header'; -export { PostMainnavigation } from './components/post-mainnavigation/post-mainnavigation'; -export { PostMegadropdown } from './components/post-megadropdown/post-megadropdown'; -export { PostMegadropdownTrigger } from './components/post-megadropdown-trigger/post-megadropdown-trigger'; +export { PostTooltip } from './components/post-tooltip/post-tooltip'; diff --git a/packages/components/src/utils/breakpoints.ts b/packages/components/src/utils/breakpoints.ts new file mode 100644 index 0000000000..01e5066434 --- /dev/null +++ b/packages/components/src/utils/breakpoints.ts @@ -0,0 +1,61 @@ +type MapItem = { + minWidth: number; + key: string; + name: string; +}; +type ListenerType = 'key' | 'name'; + +export class Breakpoint { + private readonly breakpointMap: MapItem[]; + private readonly current = { + key: '', + name: '', + }; + + constructor() { + if (!this.breakpointMap) { + const styles = getComputedStyle(document.documentElement); + const keys = styles.getPropertyValue('--post-breakpoint-keys').split(', '); + const names = styles.getPropertyValue('--post-breakpoint-names').split(', '); + + this.breakpointMap = styles + .getPropertyValue('--post-breakpoint-widths') + .split(', ') + .map((width, i) => { + return { + minWidth: Number(width), + key: keys[i], + name: names[i], + }; + }) + .reverse(); + + window.addEventListener('resize', () => this.updateHandler(), { passive: true }); + } + } + + private updateHandler(emitEvents = true) { + const calculated = this.breakpointMap.find(({ minWidth }) => innerWidth >= minWidth); + + if (this.current.key !== calculated.key) { + this.current.key = calculated.key; + if (emitEvents) this.dispatchEvent('key'); + } + + if (this.current.name !== calculated.name) { + this.current.name = calculated.name; + if (emitEvents) this.dispatchEvent('name'); + } + } + + private dispatchEvent(type: ListenerType) { + window.dispatchEvent(new CustomEvent(`postBreakpoint:${type}`, { detail: this.current[type] })); + } + + public get(type: ListenerType) { + this.updateHandler(false); + return this.current[type]; + } +} + +export const breakpoint = new Breakpoint(); diff --git a/packages/components/src/utils/tests/index.spec.ts b/packages/components/src/utils/tests/index.spec.ts new file mode 100644 index 0000000000..91b04ea5b8 --- /dev/null +++ b/packages/components/src/utils/tests/index.spec.ts @@ -0,0 +1,16 @@ +import * as fs from 'fs'; + +const file = fs.readFileSync('./src/utils/index.ts').toString(); +const exportModuleMatches = file.matchAll(/^export \* from '(?:\.\/)?(.*)';$/gm); +const exportModuleNames = [...exportModuleMatches].map(m => m[1]); +const exportModuleExceptionNames = ['breakpoint']; + +describe('packages/components/src/utils/index.ts', () => { + exportModuleExceptionNames.forEach(moduleName => { + const exportModuleName = exportModuleNames.find(m => m === moduleName); + + it(`should NOT export utility "${moduleName}"`, () => { + expect(exportModuleName).toBeUndefined(); + }); + }); +}); diff --git a/packages/documentation/.storybook/manager.ts b/packages/documentation/.storybook/manager.ts index c52ce3e90d..ec30bb63de 100644 --- a/packages/documentation/.storybook/manager.ts +++ b/packages/documentation/.storybook/manager.ts @@ -17,6 +17,7 @@ addons.setConfig({ 'layout', 'raw-components', 'components', + 'modules', 'utilities', 'templates', 'guidelines', diff --git a/packages/documentation/.storybook/preview.ts b/packages/documentation/.storybook/preview.ts index e10fe379cf..c5ff54491d 100644 --- a/packages/documentation/.storybook/preview.ts +++ b/packages/documentation/.storybook/preview.ts @@ -54,6 +54,10 @@ const preview: Preview = { // Category - Components 'Components', + // Category - Modules + 'Modules', + ['Header', 'Footer'], + // Category - Utilities 'Utilities', diff --git a/packages/documentation/cypress/snapshots/components/breadcrumb.snapshot.ts b/packages/documentation/cypress/snapshots/components/breadcrumb.snapshot.ts new file mode 100644 index 0000000000..d8757cd4dd --- /dev/null +++ b/packages/documentation/cypress/snapshots/components/breadcrumb.snapshot.ts @@ -0,0 +1,7 @@ +describe('Breadcrumb', () => { + it('default', () => { + cy.visit('/iframe.html?id=snapshots--breadcrumb'); + cy.get('post-breadcrumb.hydrated', { timeout: 30000 }).should('be.visible'); + cy.percySnapshot('Breadcrumb', { widths: [400] }); + }); +}); diff --git a/packages/documentation/cypress/snapshots/modules/footer.snapshot.ts b/packages/documentation/cypress/snapshots/modules/footer.snapshot.ts new file mode 100644 index 0000000000..634b8645c0 --- /dev/null +++ b/packages/documentation/cypress/snapshots/modules/footer.snapshot.ts @@ -0,0 +1,7 @@ +describe('Footer', () => { + it('default', () => { + cy.visit('/iframe.html?id=snapshots--footer'); + cy.get('post-footer[data-hydrated]', { timeout: 30000 }).should('be.visible'); + cy.percySnapshot('Footer', { widths: [320, 600, 1024, 1440] }); + }); +}); diff --git a/packages/documentation/src/stories/components/accordion/accordion-item.stories.ts b/packages/documentation/src/stories/components/accordion/accordion-item.stories.ts index b1063223b5..879ded9536 100644 --- a/packages/documentation/src/stories/components/accordion/accordion-item.stories.ts +++ b/packages/documentation/src/stories/components/accordion/accordion-item.stories.ts @@ -12,15 +12,33 @@ const meta: Meta = { }, }, argTypes: { - collapsed: { + 'collapsed': { control: false, // disable the control since it is not usable on the story }, - headingLevel: { + 'headingLevel': { name: 'heading-level', control: false, // disable the control since it is not usable on the story table: { - disable:true, - } + disable: true, + }, + }, + 'css-shadow-parts-button': { + name: 'button', + table: { + type: { + summary: 'css selector', + detail: '::part(button) { ... }', + }, + }, + }, + 'css-shadow-parts-body': { + name: 'body', + table: { + type: { + summary: 'css selector', + detail: '::part(body) { ... }', + }, + }, }, }, }; diff --git a/packages/documentation/src/stories/components/breadcrumb/breadcrumb.mdx b/packages/documentation/src/stories/components/breadcrumb/breadcrumb.mdx new file mode 100644 index 0000000000..3e546a981a --- /dev/null +++ b/packages/documentation/src/stories/components/breadcrumb/breadcrumb.mdx @@ -0,0 +1,29 @@ +import { Meta, Canvas, Controls } from '@storybook/blocks'; +import * as BreadcrumbStories from './breadcrumb.stories'; + + + +# Post Breadcrumb + +
+ The breadcrumb is a secondary navigation pattern that helps users understand the hierarchy among levels and navigate back through them. +
+ +The `` is a container for `` components, used to display a navigational trail, showing the user's location within the app and enabling quick access to parent pages. + +## `` + + + + +## Concatenated Breadcrumb + +When space is constrained, the breadcrumb concatenates middle items into a dropdown menu. + + + + +## `` + + + diff --git a/packages/documentation/src/stories/components/breadcrumb/breadcrumb.snapshot.stories.ts b/packages/documentation/src/stories/components/breadcrumb/breadcrumb.snapshot.stories.ts new file mode 100644 index 0000000000..666932dc92 --- /dev/null +++ b/packages/documentation/src/stories/components/breadcrumb/breadcrumb.snapshot.stories.ts @@ -0,0 +1,45 @@ +import { Args, StoryContext, StoryObj } from '@storybook/web-components'; +import meta, { Default, Concatenated } from './breadcrumb.stories'; +import { html } from 'lit'; +import { schemes } from '@/shared/snapshots/schemes'; + +const { id, ...metaWithoutId } = meta; + +export default { + ...metaWithoutId, + title: 'Snapshots', +}; + +type Story = StoryObj; + +export const BreadcrumbSnapshots: Story = { + render: (_args: Args, context: StoryContext) => { + const scenarios = [ + { label: 'Default', story: Default.render?.(context.args, context) || html`

Error rendering Default

` }, + { label: 'Concatenated', story: Concatenated.render?.(context.args, context) || html`

Error rendering Concatenated

` }, + { + label: 'Long Text', + story: html` + + This is a very long breadcrumb item + Another long breadcrumb item + Yet another long item that tests wrapping behavior + + `, + }, + ]; + + return schemes(() => html` +
+ ${scenarios.map( + (scenario) => html` +
+

${scenario.label}

+ ${scenario.story} +
+ ` + )} +
+ `); + }, +}; diff --git a/packages/documentation/src/stories/components/breadcrumb/breadcrumb.stories.ts b/packages/documentation/src/stories/components/breadcrumb/breadcrumb.stories.ts new file mode 100644 index 0000000000..b218e22640 --- /dev/null +++ b/packages/documentation/src/stories/components/breadcrumb/breadcrumb.stories.ts @@ -0,0 +1,91 @@ +import type { Args, StoryObj } from '@storybook/web-components'; +import { html } from 'lit'; +import { MetaComponent } from '@root/types'; + +const meta: MetaComponent = { + id: 'b7db7391-f893-4b1e-a125-b30c6f0b028b', + title: 'Components/Breadcrumb', + tags: ['package:WebComponents'], + parameters: { + badges: [], + design: { + type: 'figma', + url: 'https://www.figma.com/design/JIT5AdGYqv6bDRpfBPV8XR/Foundations-%26-Components-Next-Level?node-id=1787-20607&node-type=instance&m=dev', + }, + }, + args: { + homeUrl: '/', + homeText: 'Home', + }, + argTypes: { + homeUrl: { + name: 'Home URL', + description: 'URL for the home breadcrumb link.', + control: { type: 'text' }, + table: { category: 'Props' }, + }, + homeText: { + name: 'Home Text', + description: 'Text for the home breadcrumb link.', + control: { type: 'text' }, + table: { category: 'Props' }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args: Args) => html` + + Section 1 + Section 2 + Section 3 + + `, +}; + +export const Concatenated: Story = { + render: (args: Args) => html` + + Section 1 + Section 2 + Section 3 + Section 4 + Section 5 + Section 6 + Section 7 + Section 8 + Section 9 + Section 10 + + `, +}; + +export const BreadcrumbItem: Story = { + render: (args: Args) => html` + ${args.content} + `, + args: { + url: '/section1', + content: 'Section 1', + }, + argTypes: { + url: { + name: 'URL', + description: 'The URL of the breadcrumb item.', + control: { type: 'text' }, + table: { category: 'Props' }, + }, + content: { + name: 'Content', + description: 'The visible label of the breadcrumb item.', + control: { type: 'text' }, + table: { category: 'Props' }, + }, + homeUrl: { table: { disable: true } }, + homeText: { table: { disable: true } }, + }, +}; diff --git a/packages/documentation/src/stories/components/header/language-switch/language-switch.docs.mdx b/packages/documentation/src/stories/components/language-switch/language-switch.docs.mdx similarity index 100% rename from packages/documentation/src/stories/components/header/language-switch/language-switch.docs.mdx rename to packages/documentation/src/stories/components/language-switch/language-switch.docs.mdx diff --git a/packages/documentation/src/stories/components/header/language-switch/language-switch.snapshot.stories.ts b/packages/documentation/src/stories/components/language-switch/language-switch.snapshot.stories.ts similarity index 100% rename from packages/documentation/src/stories/components/header/language-switch/language-switch.snapshot.stories.ts rename to packages/documentation/src/stories/components/language-switch/language-switch.snapshot.stories.ts diff --git a/packages/documentation/src/stories/components/header/language-switch/language-switch.stories.ts b/packages/documentation/src/stories/components/language-switch/language-switch.stories.ts similarity index 98% rename from packages/documentation/src/stories/components/header/language-switch/language-switch.stories.ts rename to packages/documentation/src/stories/components/language-switch/language-switch.stories.ts index 013f6d01d9..e3cb30133e 100644 --- a/packages/documentation/src/stories/components/header/language-switch/language-switch.stories.ts +++ b/packages/documentation/src/stories/components/language-switch/language-switch.stories.ts @@ -4,7 +4,7 @@ import { MetaComponent } from '@root/types'; const meta: MetaComponent = { id: 'decbb10c-2b39-4f47-b67d-337d8111a3ae', - title: 'Components/Header/Language Switch', + title: 'Components/Language Switch', tags: ['package:WebComponents'], component: 'post-language-switch', render: renderLanguageSwitch, diff --git a/packages/documentation/src/stories/modules/footer/footer.docs.mdx b/packages/documentation/src/stories/modules/footer/footer.docs.mdx new file mode 100644 index 0000000000..733a68b17a --- /dev/null +++ b/packages/documentation/src/stories/modules/footer/footer.docs.mdx @@ -0,0 +1,15 @@ +import { Canvas, Controls, Meta } from '@storybook/blocks'; +import * as FooterStories from './footer.stories'; + + + +# Composible Footer + +

The composible footer component for your Swiss Post page.

+ +Internally the `` component has a fix structure, but it offers a number of slots, which can be used to add your very own content, so it ultimatly fulfills everyones requirements. + + + + + \ No newline at end of file diff --git a/packages/documentation/src/stories/modules/footer/footer.snapshot.stories.ts b/packages/documentation/src/stories/modules/footer/footer.snapshot.stories.ts new file mode 100644 index 0000000000..358e7cfdd8 --- /dev/null +++ b/packages/documentation/src/stories/modules/footer/footer.snapshot.stories.ts @@ -0,0 +1,20 @@ +import { Args, StoryContext, StoryObj } from '@storybook/web-components'; +import { html } from 'lit'; +import { schemes } from '@/shared/snapshots/schemes'; + +import meta from './footer.stories'; + +const { id, ...metaWithoutId } = meta; + +export default { + ...metaWithoutId, + title: 'Snapshots', +}; + +type Story = StoryObj; + +export const Footer: Story = { + render: (args: Args, context: StoryContext) => { + return schemes(() => html` ${meta.render?.(args, context)} `); + }, +}; diff --git a/packages/documentation/src/stories/modules/footer/footer.stories.ts b/packages/documentation/src/stories/modules/footer/footer.stories.ts new file mode 100644 index 0000000000..fcd3ca4c6f --- /dev/null +++ b/packages/documentation/src/stories/modules/footer/footer.stories.ts @@ -0,0 +1,174 @@ +import { Args, StoryObj } from '@storybook/web-components'; +import { html } from 'lit'; +import { MetaComponent } from '@root/types'; + +const GRID_CELLS = [1, 2, 3, 4]; +const LINKS_PER_CELL = [6, 8, 8, 5]; + +const meta: MetaComponent = { + id: 'd97528b3-a9ef-4201-bf28-9caf6e8997dc', + title: 'Modules/Footer', + component: 'post-footer', + tags: ['package:WebComponents'], + parameters: { + layout: 'fullscreen', + badges: [], + design: { + type: 'figma', + url: 'https://www.figma.com/design/JIT5AdGYqv6bDRpfBPV8XR/Foundations-%26-Components-Next-Level?node-id=1009-25200&node-type=frame&t=Fmc9LEl8mpYnfRid-0', + }, + }, + render, + args: { + label: 'Footer label', + }, + argTypes: {}, +}; + +export default meta; + +type Story = StoryObj; + +function render(args: Args) { + return html` + ${GRID_CELLS.map( + cell => html` + Title ${cell} + +

Title ${cell}

+ ${Array.from(Array(LINKS_PER_CELL[cell - 1]).keys()).map( + item => html` + + Text link ${item + 1} + + `, + )} +
+ `, + )} + + + +

Follow us

+ + + + Facebook + + + + + + Instagram + + + + + + Youtube + + + + + + Snapchat + + + + + + Titter X + + + + + + Linkedin + + + + + + Xing + + + + + + E-Mail + + +
+ + + +

Download app

+ + + Google Play Store badge + Download the App on Google Play + + + + + Apple App Store badge + Download the App on the Apple Store + + +
+ + + +

Die schweizerische Post AG

+ + PostAuto + + + PostFinance + +
+ + + +

Meta

+ + Accessibility + + + General Terms and Conditions + + + Data protection and disclaimer + + + Publication details + + + + +
+ + © Copyright 2024 by Swiss Post Ltd. + All rights reserved. +
`; +} + +export const Default: Story = {}; diff --git a/packages/documentation/src/stories/components/header/components/header.markup.ts b/packages/documentation/src/stories/modules/header/components/header.markup.ts similarity index 70% rename from packages/documentation/src/stories/components/header/components/header.markup.ts rename to packages/documentation/src/stories/modules/header/components/header.markup.ts index 5c890e2916..d6bf86c4f4 100644 --- a/packages/documentation/src/stories/components/header/components/header.markup.ts +++ b/packages/documentation/src/stories/modules/header/components/header.markup.ts @@ -6,12 +6,16 @@ export default html` - = Menu + + Menu + + + @@ -49,10 +63,14 @@ export default html` - Briefe - - -

Briefe title

+ Briefe + + + Schliessen +

Briefe title

Briefe senden

Briefe Schweiz @@ -67,14 +85,17 @@ export default html` Waren Ausland Express und Kurier
- Schliessen
- Pakete - - -

Pakete title

+ Pakete + + + Schliessen +

Pakete title

Pakete senden

Pakete Schweiz @@ -89,7 +110,6 @@ export default html` Waren Ausland Express und Kurier
- Schliessen
diff --git a/packages/documentation/src/stories/components/header/header.docs.mdx b/packages/documentation/src/stories/modules/header/header.docs.mdx similarity index 100% rename from packages/documentation/src/stories/components/header/header.docs.mdx rename to packages/documentation/src/stories/modules/header/header.docs.mdx diff --git a/packages/documentation/src/stories/components/header/header.stories.ts b/packages/documentation/src/stories/modules/header/header.stories.ts similarity index 95% rename from packages/documentation/src/stories/components/header/header.stories.ts rename to packages/documentation/src/stories/modules/header/header.stories.ts index 9b2cc7b912..06546b6b31 100644 --- a/packages/documentation/src/stories/components/header/header.stories.ts +++ b/packages/documentation/src/stories/modules/header/header.stories.ts @@ -4,7 +4,7 @@ import HeaderMarkup from './components/header.markup'; const meta: MetaComponent = { id: 'header', - title: 'Components/Header', + title: 'Modules/Header', tags: ['package:HTML'], parameters: { layout: 'fullscreen', diff --git a/packages/documentation/src/stories/components/breadcrumbs/breadcrumbs.docs.mdx b/packages/documentation/src/stories/raw-components/breadcrumbs/breadcrumbs.docs.mdx similarity index 100% rename from packages/documentation/src/stories/components/breadcrumbs/breadcrumbs.docs.mdx rename to packages/documentation/src/stories/raw-components/breadcrumbs/breadcrumbs.docs.mdx diff --git a/packages/documentation/src/stories/components/breadcrumbs/breadcrumbs.stories.ts b/packages/documentation/src/stories/raw-components/breadcrumbs/breadcrumbs.stories.ts similarity index 96% rename from packages/documentation/src/stories/components/breadcrumbs/breadcrumbs.stories.ts rename to packages/documentation/src/stories/raw-components/breadcrumbs/breadcrumbs.stories.ts index 60dcfad632..96fcb2f2de 100644 --- a/packages/documentation/src/stories/components/breadcrumbs/breadcrumbs.stories.ts +++ b/packages/documentation/src/stories/raw-components/breadcrumbs/breadcrumbs.stories.ts @@ -7,7 +7,7 @@ import { MetaComponent } from '@root/types'; const meta: MetaComponent = { id: '4347e5bf-8bf2-4f44-9075-9faaa53591ed', - title: 'Components/Breadcrumbs', + title: 'Raw Components/Internet Header/Breadcrumbs', component: 'swisspost-internet-breadcrumbs', tags: ['package:InternetHeader'], render: renderInternetBreadcrumbs, diff --git a/packages/documentation/src/stories/components/breadcrumbs/custom-items.ts b/packages/documentation/src/stories/raw-components/breadcrumbs/custom-items.ts similarity index 100% rename from packages/documentation/src/stories/components/breadcrumbs/custom-items.ts rename to packages/documentation/src/stories/raw-components/breadcrumbs/custom-items.ts diff --git a/packages/documentation/src/stories/components/footer/custom-footer-config.ts b/packages/documentation/src/stories/raw-components/footer/custom-footer-config.ts similarity index 100% rename from packages/documentation/src/stories/components/footer/custom-footer-config.ts rename to packages/documentation/src/stories/raw-components/footer/custom-footer-config.ts diff --git a/packages/documentation/src/stories/components/footer/footer.docs.mdx b/packages/documentation/src/stories/raw-components/footer/footer.docs.mdx similarity index 100% rename from packages/documentation/src/stories/components/footer/footer.docs.mdx rename to packages/documentation/src/stories/raw-components/footer/footer.docs.mdx diff --git a/packages/documentation/src/stories/components/footer/footer.stories.ts b/packages/documentation/src/stories/raw-components/footer/footer.stories.ts similarity index 97% rename from packages/documentation/src/stories/components/footer/footer.stories.ts rename to packages/documentation/src/stories/raw-components/footer/footer.stories.ts index 2c0dbec051..85fd8c1677 100644 --- a/packages/documentation/src/stories/components/footer/footer.stories.ts +++ b/packages/documentation/src/stories/raw-components/footer/footer.stories.ts @@ -6,7 +6,7 @@ import { MetaComponent } from '@root/types'; const meta: MetaComponent = { id: '27fc009d-3eec-43a9-b3a2-55531e721817', - title: 'Components/Footer', + title: 'Raw Components/Internet Header/Footer', component: 'swisspost-internet-footer', tags: ['package:InternetHeader'], render: renderInternetFooter, diff --git a/packages/documentation/src/stories/components/internet-header/config/custom-config.ts b/packages/documentation/src/stories/raw-components/internet-header/config/custom-config.ts similarity index 100% rename from packages/documentation/src/stories/components/internet-header/config/custom-config.ts rename to packages/documentation/src/stories/raw-components/internet-header/config/custom-config.ts diff --git a/packages/documentation/src/stories/components/internet-header/config/language-switch-overrides.ts b/packages/documentation/src/stories/raw-components/internet-header/config/language-switch-overrides.ts similarity index 100% rename from packages/documentation/src/stories/components/internet-header/config/language-switch-overrides.ts rename to packages/documentation/src/stories/raw-components/internet-header/config/language-switch-overrides.ts diff --git a/packages/documentation/src/stories/components/internet-header/config/os-flyout-overrides.ts b/packages/documentation/src/stories/raw-components/internet-header/config/os-flyout-overrides.ts similarity index 100% rename from packages/documentation/src/stories/components/internet-header/config/os-flyout-overrides.ts rename to packages/documentation/src/stories/raw-components/internet-header/config/os-flyout-overrides.ts diff --git a/packages/documentation/src/stories/components/internet-header/header.docs.mdx b/packages/documentation/src/stories/raw-components/internet-header/header.docs.mdx similarity index 100% rename from packages/documentation/src/stories/components/internet-header/header.docs.mdx rename to packages/documentation/src/stories/raw-components/internet-header/header.docs.mdx diff --git a/packages/documentation/src/stories/components/internet-header/header.scss b/packages/documentation/src/stories/raw-components/internet-header/header.scss similarity index 100% rename from packages/documentation/src/stories/components/internet-header/header.scss rename to packages/documentation/src/stories/raw-components/internet-header/header.scss diff --git a/packages/documentation/src/stories/components/internet-header/header.stories.ts b/packages/documentation/src/stories/raw-components/internet-header/header.stories.ts similarity index 99% rename from packages/documentation/src/stories/components/internet-header/header.stories.ts rename to packages/documentation/src/stories/raw-components/internet-header/header.stories.ts index d6ccd59d79..a0200a7ae3 100644 --- a/packages/documentation/src/stories/components/internet-header/header.stories.ts +++ b/packages/documentation/src/stories/raw-components/internet-header/header.stories.ts @@ -8,7 +8,7 @@ import { MetaComponent } from '@root/types'; const meta: MetaComponent = { id: 'ebb11274-091b-4cb7-9a3f-3e0451c9a865', - title: 'Components/Internet Header', + title: 'Raw Components/Internet Header', tags: ['package:InternetHeader'], component: 'swisspost-internet-header', parameters: { diff --git a/packages/styles/src/components/_index.scss b/packages/styles/src/components/_index.scss index 2f9f104eca..51cda318d0 100644 --- a/packages/styles/src/components/_index.scss +++ b/packages/styles/src/components/_index.scss @@ -1,5 +1,7 @@ @forward './../variables/options'; +@use 'breakpoints'; +@use 'globals'; @use 'appstore-badge'; @use 'avatar'; @use 'badge'; @@ -27,6 +29,7 @@ @use 'switch'; @use 'form-hint'; @use 'form-input'; +@use 'header'; @use 'icon-button'; @use 'icon-close-button'; @use 'lead'; diff --git a/packages/styles/src/components/appstore-badge.scss b/packages/styles/src/components/appstore-badge.scss index 0c4005bb9d..6c76601ba7 100644 --- a/packages/styles/src/components/appstore-badge.scss +++ b/packages/styles/src/components/appstore-badge.scss @@ -7,9 +7,12 @@ tokens.$default-map: components.$post-app-store-badge; .app-store-badge { display: inline-flex; border-radius: tokens.get('app-store-border-radius'); + height: tokens.get('app-store-height'); + vertical-align: text-bottom; img { display: block; - height: tokens.get('app-store-height'); + width: auto; + height: 100%; } } diff --git a/packages/styles/src/components/breakpoints.scss b/packages/styles/src/components/breakpoints.scss new file mode 100644 index 0000000000..3e36eafb96 --- /dev/null +++ b/packages/styles/src/components/breakpoints.scss @@ -0,0 +1,17 @@ +@use 'sass:list'; +@use 'sass:map'; +@use 'sass:math'; +@use '../variables/breakpoints'; + +:root { + $breakpoint-list: (); + + @each $key, $value in breakpoints.$grid-breakpoints { + $unitless-value: math.div($value, $value * 0 + 1); + $breakpoint-list: list.append($breakpoint-list, $unitless-value, comma); + } + + --post-breakpoint-widths: #{$breakpoint-list}; + --post-breakpoint-keys: #{map.keys(breakpoints.$grid-breakpoints)}; + --post-breakpoint-names: #{map.values(breakpoints.$grid-breakpoints-key-name-map)}; +} diff --git a/packages/styles/src/components/globals/_index.scss b/packages/styles/src/components/globals/_index.scss new file mode 100644 index 0000000000..99119af291 --- /dev/null +++ b/packages/styles/src/components/globals/_index.scss @@ -0,0 +1,3 @@ +@forward './../../variables/options'; + +@use 'post-footer'; diff --git a/packages/styles/src/components/globals/post-footer.scss b/packages/styles/src/components/globals/post-footer.scss new file mode 100644 index 0000000000..087415d8ab --- /dev/null +++ b/packages/styles/src/components/globals/post-footer.scss @@ -0,0 +1,92 @@ +@use '../../variables/color'; +@use '../../mixins/media'; +@use '../../mixins/list'; + +post-footer { + // mobile + --post-footer-grid-list-title-display: none; + --post-footer-grid-list-title-gap: 0; + --post-footer-grid-list-item-gap: 8px; + + --post-footer-socialmedia-list-item-gap: 8px; + --post-footer-app-list-item-gap: 8px; + --post-footer-businesssector-list-item-gap: 8px; + --post-footer-meta-list-item-gap: 8px; + + // tablet sm + @include media.min(sm) { + --post-footer-grid-list-title-display: block; + --post-footer-grid-list-title-gap: 8px; + + --post-footer-socialmedia-list-item-gap: 16px; + --post-footer-businesssector-list-item-gap: 24px; + --post-footer-meta-list-item-gap: 16px; + } + + // desktop lg + @include media.min(lg) { + --post-footer-meta-list-item-gap: 24px; + } + + :is(h3, .h3) { + margin: 0; + font-size: inherit; + } + + a { + &:not(.btn-icon, .app-store-badge) { + display: block; + text-decoration: none; + } + } + + post-list { + &[slot|='grid'] { + :is(h3, .h3) { + display: var(--post-footer-grid-list-title-display); + margin-block-end: var(--post-footer-grid-list-title-gap); + } + + > [role='list'] { + @include list.list-bullet($child-selector: 'post-list-item'); + margin-block: 0; + padding-inline-start: 0; + list-style: none; + + > post-list-item { + &::before { + display: none; + } + + ~ post-list-item { + margin-block-start: var(--post-footer-grid-list-item-gap); + } + } + } + } + + &:is([slot='socialmedia'], [slot='app'], [slot='businesssectors'], [slot='meta']) { + > [role='list'] { + @include list.list-inline($child-selector: 'post-list-item'); + margin: 0; + } + } + + &[slot='socialmedia'] > [role='list'] { + gap: var(--post-footer-socialmedia-list-item-gap); + } + + &[slot='app'] > [role='list'] { + gap: var(--post-footer-app-list-item-gap); + } + + &[slot='businesssectors'] > [role='list'] { + gap: var(--post-footer-businesssector-list-item-gap); + } + + &[slot='meta'] > [role='list'] { + row-gap: 0; + column-gap: var(--post-footer-meta-list-item-gap); + } + } +} diff --git a/packages/styles/src/components/header/_mixins.scss b/packages/styles/src/components/header/_mixins.scss new file mode 100644 index 0000000000..c033808a6c --- /dev/null +++ b/packages/styles/src/components/header/_mixins.scss @@ -0,0 +1,34 @@ +@use '../../mixins/icons'; + +@mixin subsidiary-header-interactive() { + text-decoration: none; + display: inline-flex; + align-items: center; + gap: var(--post-core-dimension-6); + border-radius: var(--post-core-dimension-24); + font-size: var(--post-core-font-size-16); + padding: var(--post-core-dimension-3) var(--post-core-dimension-10); +} + +@mixin mobile-header-interactive() { + width: 100%; + height: var(--post-core-dimension-48); + padding-inline-end: var(--post-core-dimension-6); + gap: var(--post-core-dimension-16); + border-block: var(--post-core-dimension-1) solid transparent; + border-block-end-color: currentColor; + font-weight: var(--post-core-font-weight-700); + + &:hover, + &.selected { + border-block-width: var(--post-core-dimension-3); + } + + &:hover::after { + content: ''; + display: block; + @include icons.icon(3020); + width: var(--post-core-dimension-24); + height: var(--post-core-dimension-24); + } +} diff --git a/packages/styles/src/components/header/index.scss b/packages/styles/src/components/header/index.scss new file mode 100644 index 0000000000..674e266c8e --- /dev/null +++ b/packages/styles/src/components/header/index.scss @@ -0,0 +1,60 @@ +@use '../../mixins/media'; +@use '../../mixins/utilities'; + +@use 'mixins' as *; + +post-header { + ul[slot="meta-navigation"] { + gap: var(--post-core-dimension-4); + + @include media.max(lg) { + flex-direction: column; + + a, + button { + justify-content: space-between; + border-radius: 0; + @include mobile-header-interactive; + + @include utilities.focus-style-custom { + border-radius: var(--post-core-dimension-4); + } + } + } + } + + a, + post-togglebutton { + &:not(post-mainnavigation *) { + @include subsidiary-header-interactive; + + @include media.min(sm) { + post-icon { + height: var(--post-core-dimension-22); + width: var(--post-core-dimension-22); + } + } + + @include media.max(sm) { + padding: var(--post-core-dimension-8); + + post-icon { + height: var(--post-core-dimension-24); + width: var(--post-core-dimension-24); + } + + .visually-hidden-sm { + @include utilities.visuallyhidden; + } + } + } + } + + a.selected, + post-togglebutton[aria-pressed='true'] { + &:not(post-mainnavigation *) { + color: var(--post-core-color-brand-white); + background: var(--post-core-color-sandgrey-100); + } + } +} diff --git a/packages/styles/src/components/icon-close-button.scss b/packages/styles/src/components/icon-close-button.scss index c8eb34d175..8cf04ff0c1 100644 --- a/packages/styles/src/components/icon-close-button.scss +++ b/packages/styles/src/components/icon-close-button.scss @@ -16,6 +16,7 @@ tokens.$default-map: components.$post-close; color: tokens.get('close-enabled-fg'); > post-icon { + min-width: tokens.get('close-icon-size'); width: tokens.get('close-icon-size'); height: tokens.get('close-icon-size'); } diff --git a/packages/styles/src/index.scss b/packages/styles/src/index.scss index 97fa256ba6..2f92769c20 100644 --- a/packages/styles/src/index.scss +++ b/packages/styles/src/index.scss @@ -4,6 +4,3 @@ @use './utilities'; @use './elements'; @use './components'; - -// Portal specific styles -@use './layouts/portal'; diff --git a/packages/styles/src/layouts/portal/_index.scss b/packages/styles/src/layouts/portal/_index.scss deleted file mode 100644 index c067e7178e..0000000000 --- a/packages/styles/src/layouts/portal/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@use './subnavigation'; diff --git a/packages/styles/src/layouts/portal/_subnavigation.scss b/packages/styles/src/layouts/portal/_subnavigation.scss deleted file mode 100644 index 538ea92584..0000000000 --- a/packages/styles/src/layouts/portal/_subnavigation.scss +++ /dev/null @@ -1,5 +0,0 @@ -.subnavigation-list { - width: 100%; - margin: 0; - padding: 0; -} diff --git a/packages/styles/src/mixins/_icon-button.scss b/packages/styles/src/mixins/_icon-button.scss index ec62913afc..78a3544d8a 100644 --- a/packages/styles/src/mixins/_icon-button.scss +++ b/packages/styles/src/mixins/_icon-button.scss @@ -13,12 +13,15 @@ tokens.$default-map: components.$post-icon-button; 'lg': 'large', ); $actual-size: map.get($size-map, $size); - min-width: tokens.get('icon-button-#{$actual-size}-outer'); + padding: 0; width: tokens.get('icon-button-#{$actual-size}-outer'); height: tokens.get('icon-button-#{$actual-size}-outer'); + min-height: 0; + vertical-align: text-bottom; > post-icon { - min-width: tokens.get('icon-button-#{$actual-size}-icon'); + display: block; + width: tokens.get('icon-button-#{$actual-size}-icon'); height: tokens.get('icon-button-#{$actual-size}-icon'); } } diff --git a/packages/styles/src/mixins/list.scss b/packages/styles/src/mixins/_list.scss similarity index 86% rename from packages/styles/src/mixins/list.scss rename to packages/styles/src/mixins/_list.scss index c654fb92e1..03a136a240 100644 --- a/packages/styles/src/mixins/list.scss +++ b/packages/styles/src/mixins/_list.scss @@ -1,7 +1,7 @@ @use '../functions/tokens'; @use '../tokens/elements'; -@mixin list-bullet() { +@mixin list-bullet($child-selector: 'li') { list-style: none; margin-block: tokens.get('list-bullet-margin-block', elements.$post-listbullet); padding-inline-start: calc( @@ -11,12 +11,12 @@ )} ); - > li { + > #{$child-selector} { margin: 0; padding-inline: 0; padding-block: tokens.get('list-bullet-item-text-padding-block', elements.$post-listbullet); - ~ li { + ~ #{$child-selector} { margin-block-start: tokens.get('list-bullet-item-gap-block', elements.$post-listbullet); } @@ -42,7 +42,7 @@ } } -@mixin list-number() { +@mixin list-number($child-selector: 'li') { margin-block: tokens.get('list-number-item-gap-block', elements.$post-listnumber); padding-inline-end: tokens.get('list-number-item-gap-block', elements.$post-listnumber); padding-inline-start: calc( @@ -50,7 +50,7 @@ tokens.get('list-number-item-icon-gap-inline', elements.$post-listnumber) ); - > li { + > #{$child-selector} { margin-block-end: tokens.get('list-number-margin-block', elements.$post-listnumber); padding-inline-start: tokens.get('list-number-item-icon-gap-inline', elements.$post-listnumber); padding-block: tokens.get('list-number-item-text-padding-block', elements.$post-listnumber); @@ -61,8 +61,8 @@ } } -@mixin list-inline() { - @include list-unstyled() { +@mixin list-inline($child-selector: 'li') { + @include list-unstyled($child-selector) { display: flex; flex-wrap: wrap; row-gap: tokens.get('list-bullet-item-gap-block', elements.$post-listbullet); @@ -73,14 +73,14 @@ } } -@mixin list-revert() { +@mixin list-revert($child-selector: 'li') { all: revert; @content; - > li { + > #{$child-selector} { all: revert; - ~ li { + ~ #{$child-selector} { all: revert; } @@ -90,17 +90,17 @@ } } -@mixin list-unstyled() { +@mixin list-unstyled($child-selector: 'li') { all: unset; display: block; list-style: none; @content; - > li { + > #{$child-selector} { all: unset; display: list-item; - ~ li { + ~ #{$child-selector} { all: unset; display: list-item; } diff --git a/packages/styles/src/variables/_breakpoints.scss b/packages/styles/src/variables/_breakpoints.scss index b78ff9ea19..06fa9bf964 100644 --- a/packages/styles/src/variables/_breakpoints.scss +++ b/packages/styles/src/variables/_breakpoints.scss @@ -5,3 +5,11 @@ $grid-breakpoints: ( lg: 1024px, xl: 1280px, ) !default; + +$grid-breakpoints-key-name-map: ( + xs: 'mobile', + sm: 'tablet', + md: 'tablet', + lg: 'desktop', + xl: 'desktop', +); diff --git a/packages/styles/tests/layouts/portal/index.test.scss b/packages/styles/tests/layouts/portal/index.test.scss deleted file mode 100644 index 477d1c3bf2..0000000000 --- a/packages/styles/tests/layouts/portal/index.test.scss +++ /dev/null @@ -1 +0,0 @@ -@use 'src/layouts/portal'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 147b78436b..5031b6aa97 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15803,7 +15803,7 @@ snapshots: axios@1.7.7: dependencies: - follow-redirects: 1.15.6(debug@4.3.7) + follow-redirects: 1.15.6(debug@4.3.6) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -18513,7 +18513,7 @@ snapshots: http-proxy-middleware@2.0.6(@types/express@4.17.21): dependencies: '@types/http-proxy': 1.17.15 - http-proxy: 1.18.1(debug@4.3.7) + http-proxy: 1.18.1 is-glob: 4.0.3 is-plain-obj: 3.0.0 micromatch: 4.0.8 @@ -18533,6 +18533,14 @@ snapshots: transitivePeerDependencies: - supports-color + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.6(debug@4.3.6) + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + http-proxy@1.18.1(debug@4.3.7): dependencies: eventemitter3: 4.0.7 @@ -18548,7 +18556,7 @@ snapshots: corser: 2.0.1 he: 1.2.0 html-encoding-sniffer: 3.0.0 - http-proxy: 1.18.1(debug@4.3.7) + http-proxy: 1.18.1 mime: 1.6.0 minimist: 1.2.8 opener: 1.5.2 @@ -19684,7 +19692,7 @@ snapshots: dom-serialize: 2.2.1 glob: 7.2.3 graceful-fs: 4.2.11 - http-proxy: 1.18.1(debug@4.3.7) + http-proxy: 1.18.1 isbinaryfile: 4.0.10 lodash: 4.17.21 log4js: 6.9.1