From a1e002767c65de991c9c84d8d57c5b47b0fe6dc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aliz=C3=A9=20Debray?= Date: Fri, 26 Jul 2024 14:51:47 +0200 Subject: [PATCH] feat(components): add a post-logo component --- .changeset/swift-geckos-film.md | 8 +++ packages/components/src/components.d.ts | 21 +++++++ .../src/components/post-logo/post-logo.scss | 19 ++++++ .../src/components/post-logo/post-logo.tsx | 62 +++++++++++++++++++ .../src/components/post-logo/readme.md | 24 +++++++ packages/components/src/index.ts | 1 + .../src/utils/property-checkers/check-url.ts | 9 +++ .../src/utils/property-checkers/index.ts | 3 + .../property-checkers/tests/check-url.spec.ts | 27 ++++++++ .../src/stories/components/logo/logo.docs.mdx | 28 +++++++++ .../components/logo/logo.snapshot.stories.ts | 43 +++++++++++++ .../stories/components/logo/logo.stories.ts | 46 ++++++++++++++ pnpm-lock.yaml | 20 +++--- 13 files changed, 301 insertions(+), 10 deletions(-) create mode 100644 .changeset/swift-geckos-film.md create mode 100644 packages/components/src/components/post-logo/post-logo.scss create mode 100644 packages/components/src/components/post-logo/post-logo.tsx create mode 100644 packages/components/src/components/post-logo/readme.md create mode 100644 packages/components/src/utils/property-checkers/check-url.ts create mode 100644 packages/components/src/utils/property-checkers/tests/check-url.spec.ts create mode 100644 packages/documentation/src/stories/components/logo/logo.docs.mdx create mode 100644 packages/documentation/src/stories/components/logo/logo.snapshot.stories.ts create mode 100644 packages/documentation/src/stories/components/logo/logo.stories.ts diff --git a/.changeset/swift-geckos-film.md b/.changeset/swift-geckos-film.md new file mode 100644 index 0000000000..c759d3dd70 --- /dev/null +++ b/.changeset/swift-geckos-film.md @@ -0,0 +1,8 @@ +--- +'@swisspost/design-system-documentation': minor +'@swisspost/design-system-components': minor +'@swisspost/design-system-components-angular': minor +'@swisspost/design-system-components-react': minor +--- + +Added the post-logo component, which enables displaying the Post's logo either as a clickable link or as a simple image. diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 78cc568037..11a2b05f4e 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -177,6 +177,12 @@ export namespace Components { */ "scale"?: number | null; } + interface PostLogo { + /** + * The URL to which the user is redirected upon clicking the logo. + */ + "url": string | URL; + } interface PostPopover { /** * Show a little indicator arrow @@ -422,6 +428,12 @@ declare global { prototype: HTMLPostIconElement; new (): HTMLPostIconElement; }; + interface HTMLPostLogoElement extends Components.PostLogo, HTMLStencilElement { + } + var HTMLPostLogoElement: { + prototype: HTMLPostLogoElement; + new (): HTMLPostLogoElement; + }; interface HTMLPostPopoverElement extends Components.PostPopover, HTMLStencilElement { } var HTMLPostPopoverElement: { @@ -512,6 +524,7 @@ declare global { "post-collapsible": HTMLPostCollapsibleElement; "post-collapsible-trigger": HTMLPostCollapsibleTriggerElement; "post-icon": HTMLPostIconElement; + "post-logo": HTMLPostLogoElement; "post-popover": HTMLPostPopoverElement; "post-popovercontainer": HTMLPostPopovercontainerElement; "post-rating": HTMLPostRatingElement; @@ -668,6 +681,12 @@ declare namespace LocalJSX { */ "scale"?: number | null; } + interface PostLogo { + /** + * The URL to which the user is redirected upon clicking the logo. + */ + "url"?: string | URL; + } interface PostPopover { /** * Show a little indicator arrow @@ -780,6 +799,7 @@ declare namespace LocalJSX { "post-collapsible": PostCollapsible; "post-collapsible-trigger": PostCollapsibleTrigger; "post-icon": PostIcon; + "post-logo": PostLogo; "post-popover": PostPopover; "post-popovercontainer": PostPopovercontainer; "post-rating": PostRating; @@ -807,6 +827,7 @@ declare module "@stencil/core" { * @class PostIcon - representing a stencil component */ "post-icon": LocalJSX.PostIcon & JSXBase.HTMLAttributes; + "post-logo": LocalJSX.PostLogo & JSXBase.HTMLAttributes; "post-popover": LocalJSX.PostPopover & JSXBase.HTMLAttributes; "post-popovercontainer": LocalJSX.PostPopovercontainer & JSXBase.HTMLAttributes; "post-rating": LocalJSX.PostRating & JSXBase.HTMLAttributes; diff --git a/packages/components/src/components/post-logo/post-logo.scss b/packages/components/src/components/post-logo/post-logo.scss new file mode 100644 index 0000000000..290ad8befe --- /dev/null +++ b/packages/components/src/components/post-logo/post-logo.scss @@ -0,0 +1,19 @@ +@use '@swisspost/design-system-styles/variables/color'; +@use '@swisspost/design-system-styles/mixins/utilities'; + +post-logo { + display: inline-block; + height: 100%; + + .logo, + svg { + display: block; + height: 100%; + aspect-ratio: 1/1; + } + + .description { + @include utilities.visuallyhidden; + } +} + diff --git a/packages/components/src/components/post-logo/post-logo.tsx b/packages/components/src/components/post-logo/post-logo.tsx new file mode 100644 index 0000000000..a787b433e7 --- /dev/null +++ b/packages/components/src/components/post-logo/post-logo.tsx @@ -0,0 +1,62 @@ +import { Component, Host, h, Prop, Watch, Element } from '@stencil/core'; +import { checkEmptyOrUrl } from '@/utils'; +import { version } from '@root/package.json'; + +/** + * @slot default - Slot for placing hidden descriptive text. If `url` is set, this text will serve as the accessible name of the link; otherwise, it will be used as the title of the SVG. + */ +@Component({ + tag: 'post-logo', + styleUrl: 'post-logo.scss', +}) +export class PostLogo { + @Element() host: HTMLPostLogoElement; + + /** + * The URL to which the user is redirected upon clicking the logo. + */ + @Prop() url: string | URL; + + @Watch('url') + validateUrl() { + checkEmptyOrUrl(this.url, 'The "url" property of the post-logo is invalid'); + } + + connectedCallback() { + this.validateUrl(); + this.checkDescription(); + } + + private checkDescription() { + if (!this.host.textContent) { + console.warn( + 'Be sure to add descriptive text in the post-logo to ensure good accessibility of the component.', + ); + } + } + + render() { + const logoLink = this.url && (typeof this.url === 'string' ? this.url : this.url.href); + const LogoTag = logoLink ? 'a' : 'span'; + + return ( + + + + ); + } +} diff --git a/packages/components/src/components/post-logo/readme.md b/packages/components/src/components/post-logo/readme.md new file mode 100644 index 0000000000..47d5ff4589 --- /dev/null +++ b/packages/components/src/components/post-logo/readme.md @@ -0,0 +1,24 @@ +# post-logo + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| -------- | --------- | --------------------------------------------------------------- | --------------- | ----------- | +| `url` | `url` | The URL to which the user is redirected upon clicking the logo. | `URL \| string` | `undefined` | + + +## Slots + +| Slot | Description | +| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `"default"` | Slot for placing hidden descriptive text. If `url` is set, this text will serve as the accessible name of the link; otherwise, it will be used as the title of the SVG. | + + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 0130213e14..110cc5402c 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -8,6 +8,7 @@ export { PostCardControl } from './components/post-card-control/post-card-contro export { PostCollapsible } from './components/post-collapsible/post-collapsible'; export { PostCollapsibleTrigger } from './components/post-collapsible-trigger/post-collapsible-trigger'; export { PostIcon } from './components/post-icon/post-icon'; +export { PostLogo } from './components/post-logo/post-logo'; export { PostPopover } from './components/post-popover/post-popover'; export { PostPopovercontainer } from './components/post-popovercontainer/post-popovercontainer'; export { PostRating } from './components/post-rating/post-rating'; diff --git a/packages/components/src/utils/property-checkers/check-url.ts b/packages/components/src/utils/property-checkers/check-url.ts new file mode 100644 index 0000000000..946f7844b6 --- /dev/null +++ b/packages/components/src/utils/property-checkers/check-url.ts @@ -0,0 +1,9 @@ +export function checkUrl(value: unknown, error: string) { + if (typeof value !== 'string' && !(value instanceof URL)) throw new Error(error); + + try { + new URL(value); + } catch (e) { + throw new Error(error); + } +} diff --git a/packages/components/src/utils/property-checkers/index.ts b/packages/components/src/utils/property-checkers/index.ts index bd37acd8a7..728b5fd5d5 100644 --- a/packages/components/src/utils/property-checkers/index.ts +++ b/packages/components/src/utils/property-checkers/index.ts @@ -2,12 +2,15 @@ import { emptyOr } from './empty-or'; import { checkOneOf } from './check-one-of'; import { checkPattern } from './check-pattern'; import { checkType } from './check-type'; +import { checkUrl } from './check-url'; export const checkEmptyOrOneOf = emptyOr(checkOneOf); export const checkEmptyOrPattern = emptyOr(checkPattern); export const checkEmptyOrType = emptyOr(checkType); +export const checkEmptyOrUrl = emptyOr(checkUrl); export * from './check-non-empty'; export * from './check-one-of'; export * from './check-pattern'; export * from './check-type'; +export * from './check-url'; diff --git a/packages/components/src/utils/property-checkers/tests/check-url.spec.ts b/packages/components/src/utils/property-checkers/tests/check-url.spec.ts new file mode 100644 index 0000000000..4fabb884a2 --- /dev/null +++ b/packages/components/src/utils/property-checkers/tests/check-url.spec.ts @@ -0,0 +1,27 @@ +import { checkUrl } from '../check-url'; + +describe('checkUrl', () => { + const errorMessage = 'Invalid URL'; + + test('should not throw an error if the value is an URL string or an URL object', () => { + ['https://www.example.com', new URL('https://www.example.com')].forEach(validUrl => { + expect(() => checkUrl(validUrl, errorMessage)).not.toThrow(); + }); + }); + + test('should throw an error if the value is not an URL string or an URL object', () => { + [ + '', + 'invalid-url', + 123, + true, + null, + undefined, + ['https://www.example.com'], + { url: 'https://www.example.com' }, + () => 'https://www.example.com', + ].forEach(invalidUrl => { + expect(() => checkUrl(invalidUrl, errorMessage)).toThrow(errorMessage); + }); + }); +}); diff --git a/packages/documentation/src/stories/components/logo/logo.docs.mdx b/packages/documentation/src/stories/components/logo/logo.docs.mdx new file mode 100644 index 0000000000..8d72fb11d4 --- /dev/null +++ b/packages/documentation/src/stories/components/logo/logo.docs.mdx @@ -0,0 +1,28 @@ +import { Canvas, Controls, Meta } from '@storybook/blocks'; +import * as logoStories from './logo.stories'; + + + +# Post Logo + +

Display the Post logo as a clickable link or as a simple image.

+ + +
+ +
+ +## 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 + +### Link + +You can use the logo as a link by setting its `url` property. +Ensure that the descriptive text clearly indicates the destination the user will be redirected to after clicking the logo. + + diff --git a/packages/documentation/src/stories/components/logo/logo.snapshot.stories.ts b/packages/documentation/src/stories/components/logo/logo.snapshot.stories.ts new file mode 100644 index 0000000000..2764332545 --- /dev/null +++ b/packages/documentation/src/stories/components/logo/logo.snapshot.stories.ts @@ -0,0 +1,43 @@ +import type { StoryContext, StoryObj } from '@storybook/web-components'; +import meta from './logo.stories'; +import { html } from 'lit'; + +const { id, ...metaWithoutId } = meta; + +export default { + ...metaWithoutId, + title: 'Snapshots', +}; + +type Story = StoryObj; + +export const PostLogo: Story = { + render: (_args: Partial, context: StoryContext) => { + return html` +
+
+

Images

+
+
+

Links

+
+
+ + ${['big', 'huge', 'giant'].map( + height => html` +
+ ${['', 'https://www.post.ch'].map(url => + ['white', 'dark'].map( + bg => html` +
+ ${meta.render?.({ ...context.args, url }, context)} +
+ `, + ), + )} +
+ `, + )} + `; + }, +}; diff --git a/packages/documentation/src/stories/components/logo/logo.stories.ts b/packages/documentation/src/stories/components/logo/logo.stories.ts new file mode 100644 index 0000000000..7365085869 --- /dev/null +++ b/packages/documentation/src/stories/components/logo/logo.stories.ts @@ -0,0 +1,46 @@ +import { StoryObj } from '@storybook/web-components'; +import { html } from 'lit'; +import { MetaComponent } from '@root/types'; +import { ifDefined } from 'lit/directives/if-defined.js'; + +const meta: MetaComponent = { + id: '73066e1c-0720-4a9b-8f81-a29d4250872a', + title: 'Components/Post Logo', + tags: ['package:WebComponents'], + component: 'post-logo', + render: renderLogo(), + parameters: { + design: {}, + }, + argTypes: { + url: { + control: { + type: 'text', + }, + }, + }, +}; + +export default meta; + +// RENDERER +function renderLogo(pageTitle = '[page title]') { + return (args: Partial) => { + const url = args.url || undefined; + const description = `Logo of the Post${url ? `, To ${pageTitle}` : ''}`; + + return html` ${description} `; + }; +} + +// STORIES +type Story = StoryObj; + +export const Default: Story = {}; + +export const Link: Story = { + args: { + url: 'https://www.post.ch/en', + }, + render: renderLogo('the homepage'), +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e97a5461b..9814c1f9a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11116,11 +11116,11 @@ snapshots: '@babel/preset-env': 7.24.7(@babel/core@7.24.7) '@babel/runtime': 7.24.7 '@discoveryjs/json-ext': 0.5.7 - '@ngtools/webpack': 18.1.1(@angular/compiler-cli@18.1.1(@angular/compiler@18.1.1(@angular/core@18.1.1(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.4.5))(typescript@5.4.5)(webpack@5.92.1) + '@ngtools/webpack': 18.1.1(@angular/compiler-cli@18.1.1(@angular/compiler@18.1.1(@angular/core@18.1.1(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.4.5))(typescript@5.4.5)(webpack@5.92.1(esbuild@0.21.5)) '@vitejs/plugin-basic-ssl': 1.1.0(vite@5.3.2(@types/node@20.12.7)(less@4.2.0)(sass@1.77.6)(terser@5.29.2)) ansi-colors: 4.1.3 autoprefixer: 10.4.19(postcss@8.4.38) - babel-loader: 9.1.3(@babel/core@7.24.7)(webpack@5.92.1) + babel-loader: 9.1.3(@babel/core@7.24.7)(webpack@5.92.1(esbuild@0.21.5)) browserslist: 4.23.0 copy-webpack-plugin: 12.0.2(webpack@5.92.1(esbuild@0.21.5)) critters: 0.0.24 @@ -11145,7 +11145,7 @@ snapshots: picomatch: 4.0.2 piscina: 4.6.1 postcss: 8.4.38 - postcss-loader: 8.1.1(postcss@8.4.38)(typescript@5.4.5)(webpack@5.92.1) + postcss-loader: 8.1.1(postcss@8.4.38)(typescript@5.4.5)(webpack@5.92.1(esbuild@0.21.5)) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.77.6 @@ -11209,11 +11209,11 @@ snapshots: '@babel/preset-env': 7.24.7(@babel/core@7.24.7) '@babel/runtime': 7.24.7 '@discoveryjs/json-ext': 0.5.7 - '@ngtools/webpack': 18.1.1(@angular/compiler-cli@18.1.1(@angular/compiler@18.1.1(@angular/core@18.1.1(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.4.5))(typescript@5.4.5)(webpack@5.92.1) + '@ngtools/webpack': 18.1.1(@angular/compiler-cli@18.1.1(@angular/compiler@18.1.1(@angular/core@18.1.1(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.4.5))(typescript@5.4.5)(webpack@5.92.1(esbuild@0.21.5)) '@vitejs/plugin-basic-ssl': 1.1.0(vite@5.3.2(@types/node@20.14.11)(less@4.2.0)(sass@1.77.6)(terser@5.29.2)) ansi-colors: 4.1.3 autoprefixer: 10.4.19(postcss@8.4.38) - babel-loader: 9.1.3(@babel/core@7.24.7)(webpack@5.92.1) + babel-loader: 9.1.3(@babel/core@7.24.7)(webpack@5.92.1(esbuild@0.21.5)) browserslist: 4.23.0 copy-webpack-plugin: 12.0.2(webpack@5.92.1(esbuild@0.21.5)) critters: 0.0.24 @@ -11238,7 +11238,7 @@ snapshots: picomatch: 4.0.2 piscina: 4.6.1 postcss: 8.4.38 - postcss-loader: 8.1.1(postcss@8.4.38)(typescript@5.4.5)(webpack@5.92.1) + postcss-loader: 8.1.1(postcss@8.4.38)(typescript@5.4.5)(webpack@5.92.1(esbuild@0.21.5)) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.77.6 @@ -11306,7 +11306,7 @@ snapshots: '@vitejs/plugin-basic-ssl': 1.1.0(vite@5.3.2(@types/node@20.14.11)(less@4.2.0)(sass@1.77.6)(terser@5.29.2)) ansi-colors: 4.1.3 autoprefixer: 10.4.19(postcss@8.4.38) - babel-loader: 9.1.3(@babel/core@7.24.7)(webpack@5.92.1) + babel-loader: 9.1.3(@babel/core@7.24.7)(webpack@5.92.1(esbuild@0.21.5)) browserslist: 4.23.0 copy-webpack-plugin: 12.0.2(webpack@5.92.1(esbuild@0.21.5)) critters: 0.0.24 @@ -14387,7 +14387,7 @@ snapshots: rxjs: 7.8.1 tslib: 2.6.3 - '@ngtools/webpack@18.1.1(@angular/compiler-cli@18.1.1(@angular/compiler@18.1.1(@angular/core@18.1.1(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.4.5))(typescript@5.4.5)(webpack@5.92.1)': + '@ngtools/webpack@18.1.1(@angular/compiler-cli@18.1.1(@angular/compiler@18.1.1(@angular/core@18.1.1(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.4.5))(typescript@5.4.5)(webpack@5.92.1(esbuild@0.21.5))': dependencies: '@angular/compiler-cli': 18.1.1(@angular/compiler@18.1.1(@angular/core@18.1.1(rxjs@7.8.1)(zone.js@0.14.8)))(typescript@5.4.5) typescript: 5.4.5 @@ -16636,7 +16636,7 @@ snapshots: transitivePeerDependencies: - supports-color - babel-loader@9.1.3(@babel/core@7.24.7)(webpack@5.92.1): + babel-loader@9.1.3(@babel/core@7.24.7)(webpack@5.92.1(esbuild@0.21.5)): dependencies: '@babel/core': 7.24.7 find-cache-dir: 4.0.0 @@ -22334,7 +22334,7 @@ snapshots: jiti: 1.21.6 postcss: 8.4.39 - postcss-loader@8.1.1(postcss@8.4.38)(typescript@5.4.5)(webpack@5.92.1): + postcss-loader@8.1.1(postcss@8.4.38)(typescript@5.4.5)(webpack@5.92.1(esbuild@0.21.5)): dependencies: cosmiconfig: 9.0.0(typescript@5.4.5) jiti: 1.21.6