diff --git a/.changeset/kind-buses-trade.md b/.changeset/kind-buses-trade.md new file mode 100644 index 0000000000..03a0351ecb --- /dev/null +++ b/.changeset/kind-buses-trade.md @@ -0,0 +1,6 @@ +--- +'@swisspost/design-system-documentation': minor +'@swisspost/design-system-styles': minor +--- + +Added styling support and documentation for the `` element. The dialog will replace the current modal and notification overlay components coming from ng-bootstrap. diff --git a/.changeset/popular-games-rush.md b/.changeset/popular-games-rush.md new file mode 100644 index 0000000000..e0299a924b --- /dev/null +++ b/.changeset/popular-games-rush.md @@ -0,0 +1,5 @@ +--- +'@swisspost/design-system-styles': patch +--- + +Deprecated the ng-bootstrap components Modal and Notification overlay in favor of the new Dialog component. The styles for these ng-bootstrap components will be removed in a future major version. diff --git a/packages/documentation/cypress/snapshots/components/dialog.snapshot.ts b/packages/documentation/cypress/snapshots/components/dialog.snapshot.ts new file mode 100644 index 0000000000..7ec2745610 --- /dev/null +++ b/packages/documentation/cypress/snapshots/components/dialog.snapshot.ts @@ -0,0 +1,7 @@ +describe('Dialog', () => { + it('default', () => { + cy.visit('/iframe.html?id=snapshots--dialog'); + cy.get('dialog[open]', { timeout: 30000 }).should('be.visible'); + cy.percySnapshot('Dialog', { widths: [1440] }); + }); +}); diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 0b491b6bc0..5054cbca61 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -25,7 +25,7 @@ "e2e:watch": "cypress open", "doctor": "storybook doctor", "snapshots": "percy exec -- cypress run --config-file ./cypress.snapshot.config.js --record --key 0995e768-43ec-42bd-a127-ff944a2ad8c9", - "lint": "eslint **/*.{js,ts,tsx,mdx}" + "lint": "eslint **/*.{ts,tsx,mdx}" }, "dependencies": { "@swisspost/design-system-components": "workspace:8.2.2", diff --git a/packages/documentation/src/stories/components/dialog/dialog.docs.mdx b/packages/documentation/src/stories/components/dialog/dialog.docs.mdx new file mode 100644 index 0000000000..91ab622432 --- /dev/null +++ b/packages/documentation/src/stories/components/dialog/dialog.docs.mdx @@ -0,0 +1,35 @@ +import { Canvas, Controls, Meta, Source } from '@storybook/blocks'; +import * as DialogStories from './dialog.stories'; +import JSFormData from './samples/js-form-data?raw'; +import StylesPackageImport from '@/shared/styles-package-import.mdx'; + + + +# Dialog + +

Communicate crucial information and request user action.

+ + +
+ +
+ + + +## Examples + +### Form dialog + + +#### Using form data + +Register a `submit` event listener on the form. In the event handler, you have access to all the form field values inside the dialog. The dialog box closes when the form gets submitted. + + + +### Custom content dialog +The dialog can also contain arbitrary content. + diff --git a/packages/documentation/src/stories/components/dialog/dialog.snapshot.stories.ts b/packages/documentation/src/stories/components/dialog/dialog.snapshot.stories.ts new file mode 100644 index 0000000000..9160ab7f2d --- /dev/null +++ b/packages/documentation/src/stories/components/dialog/dialog.snapshot.stories.ts @@ -0,0 +1,40 @@ +import meta, { Default } from './dialog.stories'; +import { html } from 'lit'; +import { bombArgs } from '@/utils'; +import type { Args, StoryContext, StoryObj } from '@storybook/web-components'; + +const { id, ...metaWithoutId } = meta; + +export default { + ...metaWithoutId, + title: 'Snapshots', +}; + +type Story = StoryObj; + +export const Dialog: Story = { + render: (_args: Args, context: StoryContext) => { + return html` + +
+ ${bombArgs({ + backgroundColor: ['bg-white', 'bg-primary'], + size: context.argTypes.size.options, + icon: ['none', '1034'], + closeButton: [true, false], + content: [ + 'Content', + 'Contentus momentus vero siteos et accusam iretea et justo. Contentus momentus vero siteos et accusam iretea et justo.', + ], + open: [true], + }).map((args: Args) => Default.render?.({ ...context.args, ...args }, context))} +
+ `; + }, +}; diff --git a/packages/documentation/src/stories/components/dialog/dialog.stories.ts b/packages/documentation/src/stories/components/dialog/dialog.stories.ts new file mode 100644 index 0000000000..24569321fe --- /dev/null +++ b/packages/documentation/src/stories/components/dialog/dialog.stories.ts @@ -0,0 +1,235 @@ +import { Args, Meta, StoryObj } from '@storybook/web-components'; +import { html, nothing } from 'lit-html'; + +const meta: Meta = { + id: '562eac2b-6dc1-4007-ba8e-4e981cef0cbc', + title: 'Components/Dialog', + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/design/xZ0IW0MJO0vnFicmrHiKaY/Components-Post?node-id=20215-22938&m=dev', + }, + }, + args: { + title: 'Dialog', + content: 'This is a dialog', + size: 'medium', + position: 'center', + icon: 'none', + backgroundColor: 'bg-white', + animation: 'pop-in', + closeButton: true, + open: false, + }, + argTypes: { + title: { + name: 'Title', + description: 'Optional title', + control: 'text', + table: { category: 'Content' }, + }, + content: { + name: 'Content', + description: 'Dialog text', + control: 'text', + table: { category: 'Content' }, + }, + size: { + name: 'Size', + description: 'Max width of the dialog.', + control: { + type: 'radio', + }, + options: ['small', 'medium', 'large'], + table: { category: 'Variant' }, + }, + position: { + name: 'Position', + description: 'Position of the dialog on the screen', + control: { + type: 'radio', + }, + options: ['top', 'center', 'bottom'], + table: { category: 'Variant' }, + }, + animation: { + name: 'Animation', + description: 'Choose an animation effect for showing and hidding the dialog.', + control: 'radio', + options: ['pop-in', 'slide-in', 'none'], + table: { category: 'Variant' }, + }, + icon: { + name: 'Icon', + description: 'Display an icon in the dialog.', + control: { + type: 'select', + labels: { + none: 'None', + 1034: '1034 (Info)', + 2104: '2104 (Danger)', + 2106: '2106 (Warning)', + 2105: '2105 (Success)', + }, + }, + options: ['none', '1034', '2105', '2104', '2106'], + table: { category: 'Content' }, + }, + backgroundColor: { + name: 'Background color', + description: 'The background color of the dialog field', + control: { + type: 'select', + }, + options: ['bg-white', 'bg-light', 'bg-primary'], + table: { category: 'Variant' }, + }, + closeButton: { + name: 'Close button', + description: 'Show a close button to dismiss the dialog', + control: 'boolean', + table: { category: 'Content' }, + }, + open: { + name: 'Default open', + description: 'Test property for snapshots', + control: 'boolean', + table: { disable: true }, + }, + }, + decorators: [ + story => + html`
+ ${story()} +
`, + ], +}; + +export default meta; + +const getHeader = (text: string) => { + return html`

${text}

`; +}; + +const getCloseButton = () => { + return html``; +}; + +const getControls = () => { + return html` + `; +}; + +const Template = { + render: (args: Args) => { + const header = getHeader(args.title); + const body = html`${args.content}`; + const controls = getControls(); + const postDialogIcon = + args.icon && args.icon !== 'none' + ? html`` + : nothing; + const postDialogCloseButton = args.closeButton ? getCloseButton() : nothing; + + // Don't declare default values or show empty containers + if (args.backgroundColor === 'bg-white') args.backgroundColor = nothing; + if (args.animation === 'pop-in') args.animation = nothing; + if (args.position === 'center') args.position = nothing; + if (args.size === 'medium') args.size = nothing; + + return html` + +
+ ${postDialogIcon} +

${header}

+
${body}
+
${controls}
+ ${postDialogCloseButton} +
+
+ `; + }, +}; + +const FormTemplate = { + ...Template, + render: (args: Args) => { + return html` + +
+

Form example

+
+
+ + +
+ Hintus textus elare volare cantare hendrerit in vulputate velit esse molestie + consequat, vel illum dolore eu feugiat nulla facilisis. +
+
+
+ +
+ + +
+
+
+ `; + }, +}; + +const CustomContentTemplate = { + ...Template, + render: () => { + return html` + +
+

Custom content

+

This is some other content, just placed inside the dialog.

+ +
+
+ `; + }, +}; + +type Story = StoryObj; + +export const Default: Story = { + ...Template, +}; + +export const Form: Story = { + ...FormTemplate, +}; + +export const Custom: Story = { + ...CustomContentTemplate, +}; diff --git a/packages/documentation/src/stories/components/dialog/samples/js-form-data.ts b/packages/documentation/src/stories/components/dialog/samples/js-form-data.ts new file mode 100644 index 0000000000..6cbb0e30e7 --- /dev/null +++ b/packages/documentation/src/stories/components/dialog/samples/js-form-data.ts @@ -0,0 +1,5 @@ +document.querySelector('#example-dialog-form')?.addEventListener('submit', event => { + if (!event.target) return; + const formData = Object.fromEntries(new FormData(event.target as HTMLFormElement)); // Object containing your form data + console.log(formData); +}); diff --git a/packages/documentation/src/stories/components/modal/modal.docs.mdx b/packages/documentation/src/stories/components/modal/modal.docs.mdx index eb7bb019bf..03a9af9868 100644 --- a/packages/documentation/src/stories/components/modal/modal.docs.mdx +++ b/packages/documentation/src/stories/components/modal/modal.docs.mdx @@ -24,6 +24,10 @@ import modalBlocking from './modal-blocking.sample?raw'; +
+ This component is deprecated in favor of the dialog component. +
+
  • Component Import diff --git a/packages/documentation/src/stories/components/modal/modal.stories.ts b/packages/documentation/src/stories/components/modal/modal.stories.ts index c896426233..d72e396777 100644 --- a/packages/documentation/src/stories/components/modal/modal.stories.ts +++ b/packages/documentation/src/stories/components/modal/modal.stories.ts @@ -4,7 +4,7 @@ import { MetaComponent } from '@root/types'; const meta: MetaComponent = { id: '9a512414-84c5-473c-a7c8-a434eda9578d', - title: 'Components/Modal', + title: 'Components/Modal (deprecated)', tags: ['package:Angular'], parameters: { badges: [], diff --git a/packages/documentation/src/stories/components/notification-overlay/notification-overlay.docs.mdx b/packages/documentation/src/stories/components/notification-overlay/notification-overlay.docs.mdx index db701ae4f6..910ea235e7 100644 --- a/packages/documentation/src/stories/components/notification-overlay/notification-overlay.docs.mdx +++ b/packages/documentation/src/stories/components/notification-overlay/notification-overlay.docs.mdx @@ -20,6 +20,10 @@ import basicExampleAngular from './notification-overlay.sample.ts?raw';

    Present the user with important information or a decision before continuing the workflow.

    +
    + This component is deprecated and will be removed in a future major version. +
    +
    • Component Import diff --git a/packages/documentation/src/stories/components/notification-overlay/notification-overlay.stories.ts b/packages/documentation/src/stories/components/notification-overlay/notification-overlay.stories.ts index 0e68676c5e..f9b275f9d5 100644 --- a/packages/documentation/src/stories/components/notification-overlay/notification-overlay.stories.ts +++ b/packages/documentation/src/stories/components/notification-overlay/notification-overlay.stories.ts @@ -3,7 +3,7 @@ import { MetaComponent } from '@root/types'; const meta: MetaComponent = { id: 'aab3f0df-08ca-4e33-90eb-77ffda6528db', - title: 'Components/Notification Overlay', + title: 'Components/Notification Overlay (deprecated)', tags: ['package:Angular'], parameters: { badges: [], diff --git a/packages/migrations/package.json b/packages/migrations/package.json index 0f22b119c0..6571111b88 100644 --- a/packages/migrations/package.json +++ b/packages/migrations/package.json @@ -18,7 +18,7 @@ "copy-files": "copyfiles -f LICENSE README.md package.json CONTRIBUTING.md CHANGELOG.md src/migrations.json dist", "build": "tsc -p tsconfig.json && pnpm copy-files", "clean": "rimraf dist", - "lint": "eslint **/*.{js,ts}" + "lint": "eslint **/*.ts" }, "dependencies": { "@angular-devkit/core": "=15.0.4", diff --git a/packages/styles/src/components/_index.scss b/packages/styles/src/components/_index.scss index f003b7e37f..e995bac5ba 100644 --- a/packages/styles/src/components/_index.scss +++ b/packages/styles/src/components/_index.scss @@ -19,6 +19,7 @@ @use 'form-select'; @use 'form-textarea'; @use 'datatable'; +@use 'dialog'; @use 'form-check'; @use 'forms'; @use 'grid'; diff --git a/packages/styles/src/components/dialog.scss b/packages/styles/src/components/dialog.scss new file mode 100644 index 0000000000..f3bf5211df --- /dev/null +++ b/packages/styles/src/components/dialog.scss @@ -0,0 +1,233 @@ +@use '../mixins/elevation'; +@use '../mixins/utilities'; +@use '../variables/spacing'; +@use '../variables/color'; +@use '../variables/animation'; + +@use './../themes/bootstrap/core' as *; + +dialog { + @include elevation.elevation('elevation-5'); + + padding: 0; + min-width: min(389px, 90vw); + max-width: 590px; + max-height: 90vh; + overflow: auto; + margin: revert; + overscroll-behavior: contain; + border: 2px solid var(--post-contrast-color); // Ensures good contrast when bg is dark against dark backdrop + + &::backdrop { + background-color: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(0); + } + + @include utilities.high-contrast-mode { + // Show the border in HCM + border-width: 2px; + + // Mark the backdrop as inactive in HCM + &::backdrop { + background-image: linear-gradient( + 135deg, + CanvasText 4.55%, + transparent 4.55%, + transparent 50%, + CanvasText 50%, + CanvasText 54.55%, + transparent 54.55%, + transparent 100% + ); + background-size: 22px 22px; + backdrop-filter: none; + background-color: transparent; + forced-color-adjust: none; + } + } + + // Sizes + // [small, medium (default), large] + &[data-size='small'] { + min-width: 296px; + max-width: 388px; + } + + &[data-size='large'] { + min-width: min(600px, 90vw); + max-width: 792px; + } + + // Positioning + // [top, center (default), bottom] + &[data-position='top'] { + top: 2rem; + bottom: auto; + } + + &[data-position='bottom'] { + top: auto; + bottom: 2rem; + } +} + +dialog > .dialog-grid { + margin: spacing.$size-regular spacing.$size-regular 0 spacing.$size-regular; + display: grid; + column-gap: spacing.$size-regular; + grid-template-columns: auto 1fr auto; + grid-template-areas: + 'icon header close-button' + 'icon body close-button' + 'controls controls controls'; + + // Propagate bg color to the controls + background-color: inherit; +} + +:where(.dialog-icon, .dialog-header, .dialog-body, .dialog-controls, .dialog-close):empty { + display: none; +} + +.dialog-grid > post-icon { + grid-area: icon; + display: block; + + width: spacing.$size-big; + height: spacing.$size-big; + + // Larger icon for bigger notification dialogs + dialog:not([size='small']) & { + @include media-breakpoint-up(rg) { + width: spacing.$size-small-huge; + height: spacing.$size-small-huge; + } + } +} + +.dialog-header { + grid-area: header; + margin-top: 0; +} + +.dialog-body { + grid-area: body; + margin-bottom: 0; + + > *:last-child { + margin-bottom: 0; + } +} + +.dialog-controls { + grid-area: controls; + position: sticky; + bottom: 0; + padding-block: spacing.$size-regular; + display: flex; + flex-wrap: wrap; + flex-direction: row-reverse; + gap: spacing.$size-regular; + background-color: inherit; + + button { + @include media-breakpoint-down(sm) { + width: 100%; + } + } +} + +.dialog-grid > .btn-close { + position: sticky; + top: 0; + grid-area: close-button; + width: spacing.$size-large; + height: spacing.$size-large; + min-height: 0; +} + +// Animations +// [slide-in, pop-in, none] +// Progressively enhanced with currently experimental @starting-style which allows to animate stuff appearing in the top layer +// https://developer.mozilla.org/en-US/docs/Web/CSS/@starting-style +dialog:not([data-animation='none']) { + --_dialog-transition-duration: var(--dialog-transition-duration, 0.5s); + + transform: scale(0.8); + opacity: 0; + transition-property: transform, opacity, overlay, display; + transition-behavior: allow-discrete; + transition-duration: var(--_dialog-transition-duration); + transition-timing-function: linear( + 0, + 0.007, + 0.029 2.2%, + 0.118 4.7%, + 0.625 14.4%, + 0.826 19%, + 0.902, + 0.962, + 1.008 26.1%, + 1.041 28.7%, + 1.064 32.1%, + 1.07 36%, + 1.061 40.5%, + 1.015 53.4%, + 0.999 61.6%, + 0.995 71.2%, + 1 + ); + + &::backdrop { + opacity: 0; + transition: + backdrop-filter var(--_dialog-transition-duration), + opacity var(--_dialog-transition-duration), + overlay var(--_dialog-transition-duration) allow-discrete, + display var(--_dialog-transition-duration) allow-discrete; + } + + &[open] { + transform: scale(1); + opacity: 1; + + @starting-style { + opacity: 0; + transform: scale(0.8); + } + + &::backdrop { + opacity: 1; + backdrop-filter: blur(10px); + + @starting-style { + opacity: 0; + backdrop-filter: blur(0); + } + + @media (forced-colors: active) { + backdrop-filter: none; + } + } + } + + &[data-animation='slide-in'] { + &[data-position='top'] { + --_dialog-slide-in-offset: -3rem; + } + + &[data-position='bottom'] { + --_dialog-slide-in-offset: 3rem; + } + + transform: translateY(calc(var(--_dialog-slide-in-offset))); + + &[open] { + transform: translateY(0); + + @starting-style { + transform: translateY(calc(var(--_dialog-slide-in-offset))); + } + } + } +} diff --git a/packages/styles/src/variables/_animation.scss b/packages/styles/src/variables/_animation.scss index c657c0e69d..48df5ee979 100644 --- a/packages/styles/src/variables/_animation.scss +++ b/packages/styles/src/variables/_animation.scss @@ -21,6 +21,25 @@ $transition-time-area-large: 500ms !default; $transition-easing-default: cubic-bezier(0.4, 0, 0.2, 1) !default; $transition-easing-decelerate: cubic-bezier(0, 0, 0.2, 1) !default; $transition-easing-accelerate: cubic-bezier(0.4, 0, 1, 1) !default; +$transition-easing-bump-in: linear( + 0, + 0.007, + 0.029 2.2%, + 0.118 4.7%, + 0.625 14.4%, + 0.826 19%, + 0.902, + 0.962, + 1.008 26.1%, + 1.041 28.7%, + 1.064 32.1%, + 1.07 36%, + 1.061 40.5%, + 1.015 53.4%, + 0.999 61.6%, + 0.995 71.2%, + 1 +); // Distances $transition-distance-xsmall: map.get(spacing.$post-sizes, 'micro') !default;