diff --git a/.changeset/fast-bats-poke.md b/.changeset/fast-bats-poke.md new file mode 100644 index 0000000000..d9c7be75ce --- /dev/null +++ b/.changeset/fast-bats-poke.md @@ -0,0 +1,6 @@ +--- +'@swisspost/design-system-documentation': minor +'@swisspost/design-system-components': minor +--- + +Created the `post-list` and `post-list-item` components. diff --git a/packages/components/cypress/e2e/list.cy.ts b/packages/components/cypress/e2e/list.cy.ts new file mode 100644 index 0000000000..0bf19446f9 --- /dev/null +++ b/packages/components/cypress/e2e/list.cy.ts @@ -0,0 +1,60 @@ +describe('PostList Component', { baseUrl: null, includeShadowDom: false }, () => { + beforeEach(() => { + // Visit the page where the component is rendered + cy.visit('./cypress/fixtures/post-list.test.html'); + }); + + it('should render the post-list component', () => { + // Check if the post-list component is rendered + cy.get('post-list').should('exist'); + }); + + it('should have an id for the first div in post-list', () => { + // Ensure the first div inside post-list has an id attribute + cy.get('post-list') + .find('div') + .first() + .should('have.attr', 'id') + .and('not.be.empty') + .then($div => { + const id = $div['id']; + cy.log(`First div ID: ${id}`); + }); + }); + + it('should throw an error if the title is missing', () => { + // Check for the mandatory title accessibility error if no title is provided + cy.on('uncaught:exception', err => { + expect(err.message).to.include( + 'Please provide a title to the list component. Title is mandatory for accessibility purposes.', + ); + return false; + }); + cy.get('post-list').within(() => { + cy.get('[slot="post-list-item"]').first().invoke('remove'); + }); + }); + + it('should hide the title when title-hidden is set', () => { + // Set the `title-hidden` property and check if the title is hidden + cy.get('post-list').invoke('attr', 'title-hidden', true); + cy.get('post-list div').first().should('have.class', 'visually-hidden'); + }); + + it('should render horizontally when the horizontal attribute is set', () => { + // Set the `horizontal` property and verify if the list has a horizontal layout + cy.get('post-list').invoke('attr', 'horizontal', true); + cy.get('post-list').should('have.attr', 'horizontal', 'true'); + }); + + it('should ensure post-list-item components have the correct slot and role', () => { + // Verify that the `post-list-item` components have the correct slot and role attributes + cy.get('post-list').within(() => { + cy.get('post-list-item').each($el => { + cy.wrap($el) + .should('have.attr', 'slot', 'post-list-item') + .and('have.attr', 'role', 'listitem'); + }); + }); + }); +}); diff --git a/packages/components/cypress/fixtures/post-list.test.html b/packages/components/cypress/fixtures/post-list.test.html new file mode 100644 index 0000000000..bd2da7fc62 --- /dev/null +++ b/packages/components/cypress/fixtures/post-list.test.html @@ -0,0 +1,19 @@ + + + + + + Document + + + + +

TITLE

+ Title 1 + Title 2 + Title 3 + Title 4 + Title 5 +
+ + diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 5b08fc08e8..882e95de01 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -217,6 +217,18 @@ export namespace Components { */ "url": string; } + interface PostList { + /** + * The list can become horizontal by setting `horizontal="true"` or just `horizontal` + */ + "horizontal": boolean; + /** + * If `true`, the list title will be hidden. Otherwise, it will be displayed.` + */ + "titleHidden": boolean; + } + interface PostListItem { + } interface PostLogo { /** * The URL to which the user is redirected upon clicking the logo. @@ -500,6 +512,18 @@ declare global { prototype: HTMLPostLanguageOptionElement; new (): HTMLPostLanguageOptionElement; }; + interface HTMLPostListElement extends Components.PostList, HTMLStencilElement { + } + var HTMLPostListElement: { + prototype: HTMLPostListElement; + new (): HTMLPostListElement; + }; + interface HTMLPostListItemElement extends Components.PostListItem, HTMLStencilElement { + } + var HTMLPostListItemElement: { + prototype: HTMLPostListItemElement; + new (): HTMLPostListItemElement; + }; interface HTMLPostLogoElement extends Components.PostLogo, HTMLStencilElement { } var HTMLPostLogoElement: { @@ -598,6 +622,8 @@ declare global { "post-collapsible-trigger": HTMLPostCollapsibleTriggerElement; "post-icon": HTMLPostIconElement; "post-language-option": HTMLPostLanguageOptionElement; + "post-list": HTMLPostListElement; + "post-list-item": HTMLPostListItemElement; "post-logo": HTMLPostLogoElement; "post-popover": HTMLPostPopoverElement; "post-popovercontainer": HTMLPostPopovercontainerElement; @@ -795,6 +821,18 @@ declare namespace LocalJSX { */ "url"?: string; } + interface PostList { + /** + * The list can become horizontal by setting `horizontal="true"` or just `horizontal` + */ + "horizontal"?: boolean; + /** + * If `true`, the list title will be hidden. Otherwise, it will be displayed.` + */ + "titleHidden"?: boolean; + } + interface PostListItem { + } interface PostLogo { /** * The URL to which the user is redirected upon clicking the logo. @@ -919,6 +957,8 @@ declare namespace LocalJSX { "post-collapsible-trigger": PostCollapsibleTrigger; "post-icon": PostIcon; "post-language-option": PostLanguageOption; + "post-list": PostList; + "post-list-item": PostListItem; "post-logo": PostLogo; "post-popover": PostPopover; "post-popovercontainer": PostPopovercontainer; @@ -949,6 +989,8 @@ declare module "@stencil/core" { */ "post-icon": LocalJSX.PostIcon & JSXBase.HTMLAttributes; "post-language-option": LocalJSX.PostLanguageOption & JSXBase.HTMLAttributes; + "post-list": LocalJSX.PostList & JSXBase.HTMLAttributes; + "post-list-item": LocalJSX.PostListItem & JSXBase.HTMLAttributes; "post-logo": LocalJSX.PostLogo & JSXBase.HTMLAttributes; "post-popover": LocalJSX.PostPopover & JSXBase.HTMLAttributes; "post-popovercontainer": LocalJSX.PostPopovercontainer & JSXBase.HTMLAttributes; diff --git a/packages/components/src/components/post-list-item/post-list-item.scss b/packages/components/src/components/post-list-item/post-list-item.scss new file mode 100644 index 0000000000..79ff2d5269 --- /dev/null +++ b/packages/components/src/components/post-list-item/post-list-item.scss @@ -0,0 +1,3 @@ +:host { + display: flex; +} 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 new file mode 100644 index 0000000000..fdb5c6b012 --- /dev/null +++ b/packages/components/src/components/post-list-item/post-list-item.tsx @@ -0,0 +1,26 @@ +import { Component, Element, Host, h } from '@stencil/core'; + +/** + * @slot default- Slot for placing the content of the list item. + */ + +@Component({ + tag: 'post-list-item', + styleUrl: 'post-list-item.scss', + shadow: true, +}) +export class PostListItem { + @Element() host: HTMLPostListItemElement; + + connectedCallback() { + this.host.setAttribute('slot', 'post-list-item'); + } + + render() { + return ( + + + + ); + } +} diff --git a/packages/components/src/components/post-list-item/readme.md b/packages/components/src/components/post-list-item/readme.md new file mode 100644 index 0000000000..5031db9cd5 --- /dev/null +++ b/packages/components/src/components/post-list-item/readme.md @@ -0,0 +1,17 @@ +# post-list-item + + + + + + +## Slots + +| Slot | Description | +| ----------------------------------------------------------- | ----------- | +| `"default- Slot for placing the content of the list item."` | | + + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/components/src/components/post-list/post-list.scss b/packages/components/src/components/post-list/post-list.scss new file mode 100644 index 0000000000..d492599b77 --- /dev/null +++ b/packages/components/src/components/post-list/post-list.scss @@ -0,0 +1,27 @@ +@use '@swisspost/design-system-styles/core' as post; + +post-list { + display: flex; + gap: var(--post-list-title-gap); + flex-direction: column; + align-items: flex-start; + + & > div[role='list'] { + flex-direction: column; + display: flex; + gap: var(--post-list-item-gap); + } + + &[horizontal]:not([horizontal='false']) { + flex-direction: row; + align-items: baseline; + & > div[role='list'] { + flex-direction: row; + align-items: center; + } + } + + > .list-title.visually-hidden { + @include post.visually-hidden(); + } +} diff --git a/packages/components/src/components/post-list/post-list.tsx b/packages/components/src/components/post-list/post-list.tsx new file mode 100644 index 0000000000..d919db8288 --- /dev/null +++ b/packages/components/src/components/post-list/post-list.tsx @@ -0,0 +1,69 @@ +import { Component, Element, Prop, Host, State, h } from '@stencil/core'; +import { version } from '@root/package.json'; + +/** + * @slot default - Slot for placing the list title. + * @slot post-list-item - Slot for placing post-list-item components. + */ + +@Component({ + tag: 'post-list', + styleUrl: 'post-list.scss', + shadow: false, +}) +export class PostList { + @Element() host: HTMLPostListElement; + + /** + * The unique title of the list that is also referenced in the labelledby + */ + @State() uuid: string; + + /** + * If `true`, the list title will be hidden. Otherwise, it will be displayed.` + */ + @Prop() readonly titleHidden: boolean = false; + + /** + * The list can become horizontal by setting `horizontal="true"` or just `horizontal` + */ + @Prop() readonly horizontal: boolean = false; + + titleEl: HTMLElement; + + componentWillLoad() { + /** + * Get the id set on the host element or use a random id by default + */ + this.uuid = `list-${crypto.randomUUID()}`; + } + + componentDidLoad() { + this.checkTitle(); + } + + private checkTitle() { + if (!this.titleEl.innerText) { + throw new Error( + 'Please provide a title to the list component. Title is mandatory for accessibility purposes.', + ); + } + } + + render() { + return ( + +
(this.titleEl = el)} + id={this.uuid} + class={`list-title${this.titleHidden ? ' visually-hidden' : ''}`} + > + +
+
+ +
+
+ ); + } +} diff --git a/packages/components/src/components/post-list/readme.md b/packages/components/src/components/post-list/readme.md new file mode 100644 index 0000000000..faf4bd36b5 --- /dev/null +++ b/packages/components/src/components/post-list/readme.md @@ -0,0 +1,26 @@ +# post-list + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ------------- | -------------- | ---------------------------------------------------------------------------------- | --------- | ------- | +| `horizontal` | `horizontal` | The list can become horizontal by setting `horizontal="true"` or just `horizontal` | `boolean` | `false` | +| `titleHidden` | `title-hidden` | If `true`, the list title will be hidden. Otherwise, it will be displayed.` | `boolean` | `false` | + + +## Slots + +| Slot | Description | +| ------------------ | ------------------------------------------- | +| `"default"` | Slot for placing the list title. | +| `"post-list-item"` | Slot for placing post-list-item components. | + + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index b2c2fe0350..85a4eb32ff 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -19,3 +19,5 @@ 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 { PostList } from './components/post-list/post-list'; +export { PostListItem } from './components/post-list-item/post-list-item'; diff --git a/packages/documentation/cypress/snapshots/components/list.snapshot.ts b/packages/documentation/cypress/snapshots/components/list.snapshot.ts new file mode 100644 index 0000000000..c279b69d45 --- /dev/null +++ b/packages/documentation/cypress/snapshots/components/list.snapshot.ts @@ -0,0 +1,7 @@ +describe('List', () => { + it('default', () => { + cy.visit('/iframe.html?id=snapshots--list'); + cy.get('post-list', { timeout: 30000 }).should('be.visible'); + cy.percySnapshot('List', { widths: [1440] }); + }); +}); diff --git a/packages/documentation/src/stories/components/list/list.docs.mdx b/packages/documentation/src/stories/components/list/list.docs.mdx new file mode 100644 index 0000000000..28b696738e --- /dev/null +++ b/packages/documentation/src/stories/components/list/list.docs.mdx @@ -0,0 +1,53 @@ +import { Canvas, Controls, Meta } from '@storybook/blocks'; +import * as ListStories from './list.stories'; + + + +
+ # List + + +
+ +The `` is a container for `` components. + + + +## `` + + + +## Installation + +The `` element is part of the `@swisspost/design-system-components` package. +For more information, read the [getting started with components guide](/?path=/docs/edfb619b-fda1-4570-bf25-20830303d483--docs). + +## Examples + +

Horizontal List

+

Set the `horizontal` property to `true`to make the list horizontal.

+ + +

Hidden Title

+

The title can be visually hidden but should always be present for accessibility reasons.

+

Set the `title-hidden` property to `true`to visually hide the title.

+ + + +

List Styling

+ +--post-list-title-gap +

+ Define the gap between the title and the list items using the `--post-list-title-gap` property ( + e.g., `--post-list-title-gap: 2rem;`). +

+ +--post-list-item-gap +

+ Define the gap of the list items using the `--post-list-item-gap` property (e.g. + `--post-list-item-gap: 1rem 0.5rem;`). +

+ + diff --git a/packages/documentation/src/stories/components/list/list.snapshot.stories.ts b/packages/documentation/src/stories/components/list/list.snapshot.stories.ts new file mode 100644 index 0000000000..67e1efc59b --- /dev/null +++ b/packages/documentation/src/stories/components/list/list.snapshot.stories.ts @@ -0,0 +1,52 @@ +import type { Args, StoryContext, StoryObj, StoryFn } from '@storybook/web-components'; +import meta from './list.stories'; +import { html } from 'lit'; +import { bombArgs } from '@/utils'; + +const { id, ...metaWithoutId } = meta; + +export default { + ...metaWithoutId, + title: 'Snapshots', +}; + +type Story = StoryObj; + +export const PostList: Story = { + render: () => { + return html` +
+ ${bombArgs({ + titleHidden: [false, true], + horizontal: [false, true], + itemGap: ['1rem', '2rem', '5rem'], // Variations for item gap + headingGap: ['1rem', '5rem', '10rem'], // Variations for heading gap + }) + .filter((args: Args) => !(args.titleHidden && args.headingGap !== '1rem')) + .map((args: Args) => { + return html` + +

List Title

+ Item 1 + Item 2 + Item 3 +
+ `; + })} +
+ `; + }, + decorators: [ + (story: StoryFn, context: StoryContext) => { + const storyTemplate = html`
${story(context.args, context)}
`; + return storyTemplate; + }, + ], +}; diff --git a/packages/documentation/src/stories/components/list/list.stories.ts b/packages/documentation/src/stories/components/list/list.stories.ts new file mode 100644 index 0000000000..76000871b2 --- /dev/null +++ b/packages/documentation/src/stories/components/list/list.stories.ts @@ -0,0 +1,90 @@ +import { StoryObj } from '@storybook/web-components'; +import { html, nothing } from 'lit'; +import { MetaComponent } from '@root/types'; + +const meta: MetaComponent = { + id: '18566ca9-4502-4053-89f0-db5fdf6f906e', + title: 'Components/List', + tags: ['package:WebComponents'], + component: 'post-list', + parameters: { + badges: [], + design: { + type: 'figma', + url: 'https://www.figma.com/design/JIT5AdGYqv6bDRpfBPV8XR/Foundations-%26-Components-Next-Level?node-id=558-7013&t=tiW8c2NHXI0mcJa5-1', + }, + }, + args: { + horizontal: false, + titleHidden: false, + listItemGap: null, + listTitleGap: null, + }, + argTypes: { + listItemGap: { + name: '--post-list-item-gap', + description: 'Defines the gap between list items.', + control: { type: 'number', min: 0, max: 10, step: 1 }, + table: { + category: 'Styling', + }, + }, + listTitleGap: { + name: '--post-list-title-gap', + description: 'Defines the gap between the title and the list items.', + control: { type: 'number', min: 0, max: 10, step: 1 }, + table: { + category: 'Styling', + }, + }, + }, + render: args => + html` +

Title

+ List Item 1 + List Item 2 + List Item 3 +
`, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const PostListHorizontal: Story = { + render: () => + html` +

Title

+ List Item 1 + List Item 2 + List Item 3 +
`, +}; + +export const PostListNoTitle: Story = { + render: () => + html` +

Title

+ List Item 1 + List Item 2 + List Item 3 +
`, +}; + +export const PostListStyling: Story = { + render: () => + html` +

Title

+ List Item 1 + List Item 2 + List Item 3 +
`, +};