diff --git a/.changeset/yellow-gifts-sit.md b/.changeset/yellow-gifts-sit.md new file mode 100644 index 0000000000..6ad9f49882 --- /dev/null +++ b/.changeset/yellow-gifts-sit.md @@ -0,0 +1,6 @@ +--- +'@swisspost/design-system-documentation': minor +'@swisspost/design-system-components': minor +--- + +Added the `post-language-switch` component that enables users to change the language of a page. diff --git a/packages/components/package.json b/packages/components/package.json index db4866c06a..2068ff5214 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -40,10 +40,11 @@ "dependencies": { "@floating-ui/dom": "1.6.8", "@oddbird/popover-polyfill": "0.3.7", - "@swisspost/design-system-styles": "workspace:9.0.0-next.8", "@swisspost/design-system-icons": "workspace:9.0.0-next.8", + "@swisspost/design-system-styles": "workspace:9.0.0-next.8", "ally.js": "1.4.1", - "long-press-event": "2.5.0" + "long-press-event": "2.5.0", + "nanoid": "5.0.9" }, "devDependencies": { "@percy/cli": "1.29.1", diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index d1a32e53ee..9acbc7fda6 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -7,9 +7,11 @@ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { HeadingLevel } from "./types/index"; import { BannerType } from "./components/post-banner/banner-types"; +import { SwitchVariant } from "./components/post-language-switch/switch-variants"; import { Placement } from "@floating-ui/dom"; export { HeadingLevel } from "./types/index"; export { BannerType } from "./components/post-banner/banner-types"; +export { SwitchVariant } from "./components/post-language-switch/switch-variants"; export { Placement } from "@floating-ui/dom"; export namespace Components { interface PostAccordion { @@ -216,6 +218,10 @@ 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". */ @@ -228,6 +234,28 @@ export namespace Components { * The URL used for the href attribute of the internal anchor. This field is optional; if not provided, a button will be used internally instead of an anchor. */ "url": string; + /** + * The variant of the post-language-switch parent (dynamically set by the parent) + */ + "variant"?: SwitchVariant | null; + } + interface PostLanguageSwitch { + /** + * A title for the list of language options + */ + "caption": string; + /** + * 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) + */ + "variant": SwitchVariant; } interface PostList { /** @@ -617,6 +645,12 @@ declare global { prototype: HTMLPostLanguageOptionElement; new (): HTMLPostLanguageOptionElement; }; + interface HTMLPostLanguageSwitchElement extends Components.PostLanguageSwitch, HTMLStencilElement { + } + var HTMLPostLanguageSwitchElement: { + prototype: HTMLPostLanguageSwitchElement; + new (): HTMLPostLanguageSwitchElement; + }; interface HTMLPostListElement extends Components.PostList, HTMLStencilElement { } var HTMLPostListElement: { @@ -806,6 +840,7 @@ declare global { "post-header": HTMLPostHeaderElement; "post-icon": HTMLPostIconElement; "post-language-option": HTMLPostLanguageOptionElement; + "post-language-switch": HTMLPostLanguageSwitchElement; "post-list": HTMLPostListElement; "post-list-item": HTMLPostListItemElement; "post-logo": HTMLPostLogoElement; @@ -1011,6 +1046,10 @@ 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". */ @@ -1023,6 +1062,28 @@ declare namespace LocalJSX { * The URL used for the href attribute of the internal anchor. This field is optional; if not provided, a button will be used internally instead of an anchor. */ "url"?: string; + /** + * The variant of the post-language-switch parent (dynamically set by the parent) + */ + "variant"?: SwitchVariant | null; + } + interface PostLanguageSwitch { + /** + * A title for the list of language options + */ + "caption"?: string; + /** + * 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) + */ + "variant"?: SwitchVariant; } interface PostList { /** @@ -1202,6 +1263,7 @@ declare namespace LocalJSX { "post-header": PostHeader; "post-icon": PostIcon; "post-language-option": PostLanguageOption; + "post-language-switch": PostLanguageSwitch; "post-list": PostList; "post-list-item": PostListItem; "post-logo": PostLogo; @@ -1245,6 +1307,7 @@ declare module "@stencil/core" { */ "post-icon": LocalJSX.PostIcon & JSXBase.HTMLAttributes; "post-language-option": LocalJSX.PostLanguageOption & JSXBase.HTMLAttributes; + "post-language-switch": LocalJSX.PostLanguageSwitch & JSXBase.HTMLAttributes; "post-list": LocalJSX.PostList & JSXBase.HTMLAttributes; "post-list-item": LocalJSX.PostListItem & JSXBase.HTMLAttributes; "post-logo": LocalJSX.PostLogo & JSXBase.HTMLAttributes; diff --git a/packages/components/src/components/post-header/post-header.tsx b/packages/components/src/components/post-header/post-header.tsx index 0dc1a9a233..c8a62dfbce 100644 --- a/packages/components/src/components/post-header/post-header.tsx +++ b/packages/components/src/components/post-header/post-header.tsx @@ -1,6 +1,9 @@ import { Component, h, Host, State, Element, Listen } from '@stencil/core'; import { throttle } from 'throttle-debounce'; import { version } from '@root/package.json'; +import { SwitchVariant } from '@/components'; + +type DEVICE_SIZE = 'mobile' | 'tablet' | 'desktop' | null; @Component({ tag: 'post-header', @@ -9,7 +12,7 @@ import { version } from '@root/package.json'; }) export class PostHeader { @Element() host: HTMLPostHeaderElement; - @State() device: 'mobile' | 'tablet' | 'desktop' = null; + @State() device: DEVICE_SIZE = null; @State() mobileMenuExtended: boolean = false; private scrollParent = null; @@ -67,17 +70,33 @@ export class PostHeader { } private handleResize() { + const previousDevice = this.device; + let newDevice: DEVICE_SIZE; const width = window?.innerWidth; + if (width >= 1024) { - this.device = 'desktop'; + newDevice = 'desktop'; this.mobileMenuExtended = false; // Close any open mobile menu } else if (width >= 600) { - this.device = 'tablet'; + newDevice = 'tablet'; } else { - this.device = 'mobile'; + newDevice = 'mobile'; + } + + // Apply only on change for doing work only when necessary + if (newDevice !== previousDevice) { + this.device = newDevice; + window.requestAnimationFrame(() => { + this.switchLanguageSwitchMode(); + }); } } + private switchLanguageSwitchMode() { + const variant: SwitchVariant = this.device === 'desktop' ? 'dropdown' : 'list'; + this.host.querySelector('post-language-switch')?.setAttribute('variant', variant); + } + private handleMobileMenuToggle() { this.mobileMenuExtended = !this.mobileMenuExtended; } diff --git a/packages/components/src/components/post-icon/readme.md b/packages/components/src/components/post-icon/readme.md index 8b1c4de7d4..b5967432b0 100644 --- a/packages/components/src/components/post-icon/readme.md +++ b/packages/components/src/components/post-icon/readme.md @@ -28,6 +28,7 @@ some content - [post-breadcrumb-item](../post-breadcrumb-item) - [post-card-control](../post-card-control) - [post-closebutton](../post-closebutton) + - [post-language-switch](../post-language-switch) - [post-rating](../post-rating) - [post-tag](../post-tag) @@ -40,6 +41,7 @@ graph TD; post-breadcrumb-item --> post-icon post-card-control --> post-icon post-closebutton --> post-icon + post-language-switch --> post-icon post-rating --> post-icon post-tag --> post-icon style post-icon fill:#f9f,stroke:#333,stroke-width:4px diff --git a/packages/components/src/components/post-language-option/post-language-option.scss b/packages/components/src/components/post-language-option/post-language-option.scss index 6d81397276..314839d3d8 100644 --- a/packages/components/src/components/post-language-option/post-language-option.scss +++ b/packages/components/src/components/post-language-option/post-language-option.scss @@ -23,3 +23,42 @@ a { width: 100%; padding: var(--post-language-option-padding); } + +.post-language-option-list { + @include post.focus-style; + border-radius: 2px; + width: 40px; + height: 40px; + + &[aria-current='true'], + &[aria-current='page'] { + background-color: #050400; + color: #fff; + } +} + +.post-language-option-dropdown { + padding-block: 13px; + padding-inline: 24px; + box-sizing: border-box; + position: relative; + + &[aria-current='true'], + &[aria-current='page'] { + &::after { + content: ''; + left: -2px; + height: 3px; + background-color: #504f4b; + width: calc(100% + 4px); + display: block; + position: absolute; + bottom: 3px; + } + + &:focus::after { + width: calc(100% - 5px); + left: 2px; + } + } +} diff --git a/packages/components/src/components/post-language-option/post-language-option.tsx b/packages/components/src/components/post-language-option/post-language-option.tsx index d7233c6905..6e8bd502a1 100644 --- a/packages/components/src/components/post-language-option/post-language-option.tsx +++ b/packages/components/src/components/post-language-option/post-language-option.tsx @@ -11,6 +11,7 @@ import { } from '@stencil/core'; import { checkEmptyOrType, checkType } from '@/utils'; import { version } from '@root/package.json'; +import { SwitchVariant } from '../post-language-switch/switch-variants'; /** * @slot default - Slot for placing the content inside the anchor or button. @@ -51,6 +52,11 @@ export class PostLanguageOption { ); } + /** + * The variant of the post-language-switch parent (dynamically set by the parent) + */ + @Prop() variant?: SwitchVariant | null; + /** * The full name of the language. For example, "Deutsch". */ @@ -122,6 +128,7 @@ export class PostLanguageOption { {this.url ? ( ) : ( + + +
+ +
+
+
+ ); + } + + render() { + return this.variant === 'list' ? this.renderList() : this.renderDropdown(); + } +} diff --git a/packages/components/src/components/post-language-switch/readme.md b/packages/components/src/components/post-language-switch/readme.md new file mode 100644 index 0000000000..c2079be7d3 --- /dev/null +++ b/packages/components/src/components/post-language-switch/readme.md @@ -0,0 +1,36 @@ +# post-language-switch + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ----------- | +| `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'` | + + +## Dependencies + +### Depends on + +- [post-menu-trigger](../post-menu-trigger) +- [post-icon](../post-icon) +- [post-menu](../post-menu) + +### Graph +```mermaid +graph TD; + post-language-switch --> post-menu-trigger + post-language-switch --> post-icon + post-language-switch --> post-menu + post-menu --> post-popovercontainer + style post-language-switch fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/components/src/components/post-language-switch/switch-variants.ts b/packages/components/src/components/post-language-switch/switch-variants.ts new file mode 100644 index 0000000000..2473e44f8f --- /dev/null +++ b/packages/components/src/components/post-language-switch/switch-variants.ts @@ -0,0 +1,3 @@ +export const SWITCH_VARIANTS = ['list', 'dropdown'] as const; + +export type SwitchVariant = (typeof SWITCH_VARIANTS)[number]; diff --git a/packages/components/src/components/post-menu-trigger/post-menu-trigger.tsx b/packages/components/src/components/post-menu-trigger/post-menu-trigger.tsx index 1846eaf361..c13a921134 100644 --- a/packages/components/src/components/post-menu-trigger/post-menu-trigger.tsx +++ b/packages/components/src/components/post-menu-trigger/post-menu-trigger.tsx @@ -21,7 +21,7 @@ export class PostMenuTrigger { @State() ariaExpanded: boolean = false; /** - * Reference to the slotted button within the trigger, if present. + * Reference to the slotted button within the trigger, if present. * Used to manage click and key events for menu control. */ private slottedButton: HTMLButtonElement | null = null; @@ -62,8 +62,20 @@ export class PostMenuTrigger { componentDidLoad() { this.root = getRoot(this.host); this.validateControlFor(); - + this.slottedButton = this.host.querySelector('button'); + + // Check if the slottedButton is within a web component + if (!this.slottedButton) { + const webComponent = this.host.querySelector('.menu-trigger-webc'); + if (webComponent.shadowRoot) { + const slottedButton = webComponent.shadowRoot.querySelector('button'); + if (slottedButton) { + this.slottedButton = slottedButton; + } + } + } + if (this.slottedButton) { this.slottedButton.setAttribute('aria-haspopup', 'menu'); this.slottedButton.addEventListener('click', () => { diff --git a/packages/components/src/components/post-menu-trigger/readme.md b/packages/components/src/components/post-menu-trigger/readme.md index 5be3ab3109..5198f2ceaf 100644 --- a/packages/components/src/components/post-menu-trigger/readme.md +++ b/packages/components/src/components/post-menu-trigger/readme.md @@ -12,6 +12,19 @@ | `for` _(required)_ | `for` | ID of the menu element that this trigger is linked to. Used to open and close the specified menu. | `string` | `undefined` | +## Dependencies + +### Used by + + - [post-language-switch](../post-language-switch) + +### Graph +```mermaid +graph TD; + post-language-switch --> post-menu-trigger + style post-menu-trigger fill:#f9f,stroke:#333,stroke-width:4px +``` + ---------------------------------------------- *Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/components/src/components/post-menu/readme.md b/packages/components/src/components/post-menu/readme.md index f700878d4c..2372e47c18 100644 --- a/packages/components/src/components/post-menu/readme.md +++ b/packages/components/src/components/post-menu/readme.md @@ -66,6 +66,10 @@ Type: `Promise` ## Dependencies +### Used by + + - [post-language-switch](../post-language-switch) + ### Depends on - [post-popovercontainer](../post-popovercontainer) @@ -74,6 +78,7 @@ Type: `Promise` ```mermaid graph TD; post-menu --> post-popovercontainer + post-language-switch --> post-menu style post-menu fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/components/src/index.html b/packages/components/src/index.html index ca9c2ba3e2..172bd137c5 100644 --- a/packages/components/src/index.html +++ b/packages/components/src/index.html @@ -17,40 +17,41 @@ + Homepage - -
    + + + - + + = Menu - + + + DE + FR + IT + EN + +

    Application title

    -
      + + + +