From d450f49708ddcb03ff690e3b9bd887a496b0d4cb Mon Sep 17 00:00:00 2001 From: Mario Castigliano Date: Mon, 2 Dec 2024 16:45:27 +0100 Subject: [PATCH] refactor(sbb-loading-indicator): split variants into two components and add missing sizes (#3211) BREAKING CHANGE: The `sbb-loading-indicator` component no longer supports the `circle` variant, to achieve this look use `sbb-loading-indicator-circle` instead. For any other case where it is used in its `window` variant just remove the `variant` property as it is no longer needed. --- src/elements/button/common/common-stories.ts | 7 +- src/elements/loading-indicator-circle.ts | 1 + ...ing-indicator-circle.snapshot.spec.snap.js | 41 ++++ .../loading-indicator-circle.scss | 92 ++++++++ .../loading-indicator-circle.snapshot.spec.ts | 28 +++ .../loading-indicator-circle.spec.ts | 15 ++ .../loading-indicator-circle.ssr.spec.ts | 23 ++ .../loading-indicator-circle.stories.ts | 107 +++++++++ .../loading-indicator-circle.ts | 38 ++++ .../loading-indicator-circle.visual.spec.ts | 31 +++ .../loading-indicator-circle/readme.md | 35 +++ .../loading-indicator.snapshot.spec.snap.js | 1 - .../loading-indicator/loading-indicator.scss | 214 +++++------------- .../loading-indicator.snapshot.spec.ts | 21 +- .../loading-indicator.stories.ts | 86 +------ .../loading-indicator/loading-indicator.ts | 27 +-- .../loading-indicator.visual.spec.ts | 30 +-- src/elements/loading-indicator/readme.md | 39 +--- 18 files changed, 507 insertions(+), 329 deletions(-) create mode 100644 src/elements/loading-indicator-circle.ts create mode 100644 src/elements/loading-indicator-circle/__snapshots__/loading-indicator-circle.snapshot.spec.snap.js create mode 100644 src/elements/loading-indicator-circle/loading-indicator-circle.scss create mode 100644 src/elements/loading-indicator-circle/loading-indicator-circle.snapshot.spec.ts create mode 100644 src/elements/loading-indicator-circle/loading-indicator-circle.spec.ts create mode 100644 src/elements/loading-indicator-circle/loading-indicator-circle.ssr.spec.ts create mode 100644 src/elements/loading-indicator-circle/loading-indicator-circle.stories.ts create mode 100644 src/elements/loading-indicator-circle/loading-indicator-circle.ts create mode 100644 src/elements/loading-indicator-circle/loading-indicator-circle.visual.spec.ts create mode 100644 src/elements/loading-indicator-circle/readme.md diff --git a/src/elements/button/common/common-stories.ts b/src/elements/button/common/common-stories.ts index d4cce7c369..710b0b1ddc 100644 --- a/src/elements/button/common/common-stories.ts +++ b/src/elements/button/common/common-stories.ts @@ -14,7 +14,7 @@ import { html, unsafeStatic } from 'lit/static-html.js'; import { sbbSpread } from '../../../storybook/helpers/spread.js'; import '../../icon.js'; -import '../../loading-indicator.js'; +import '../../loading-indicator-circle.js'; /* eslint-disable lit/binding-positions, @typescript-eslint/naming-convention */ const Template = ({ tag, text, ...args }: Args): TemplateResult => html` @@ -38,10 +38,9 @@ const IconSlotTemplate = ({ const LoadingIndicatorTemplate = ({ tag, text, ...args }: Args): TemplateResult => html` <${unsafeStatic(tag)} ${sbbSpread(args)}> - + > ${text} `; diff --git a/src/elements/loading-indicator-circle.ts b/src/elements/loading-indicator-circle.ts new file mode 100644 index 0000000000..a4ac3af4f9 --- /dev/null +++ b/src/elements/loading-indicator-circle.ts @@ -0,0 +1 @@ +export * from './loading-indicator-circle/loading-indicator-circle.js'; diff --git a/src/elements/loading-indicator-circle/__snapshots__/loading-indicator-circle.snapshot.spec.snap.js b/src/elements/loading-indicator-circle/__snapshots__/loading-indicator-circle.snapshot.spec.snap.js new file mode 100644 index 0000000000..428306b2f8 --- /dev/null +++ b/src/elements/loading-indicator-circle/__snapshots__/loading-indicator-circle.snapshot.spec.snap.js @@ -0,0 +1,41 @@ +/* @web/test-runner snapshot v1 */ +export const snapshots = {}; + +snapshots["sbb-loading-indicator-circle renders with variant `circle` DOM"] = +` + +`; +/* end snapshot sbb-loading-indicator-circle renders with variant `circle` DOM */ + +snapshots["sbb-loading-indicator-circle renders with variant `circle` Shadow DOM"] = +` + + + +`; +/* end snapshot sbb-loading-indicator-circle renders with variant `circle` Shadow DOM */ + +snapshots["sbb-loading-indicator-circle renders with variant `circle` A11y tree Chrome"] = +`

+ { + "role": "WebArea", + "name": "" +} +

+`; +/* end snapshot sbb-loading-indicator-circle renders with variant `circle` A11y tree Chrome */ + +snapshots["sbb-loading-indicator-circle renders with variant `circle` A11y tree Firefox"] = +`

+ { + "role": "document", + "name": "" +} +

+`; +/* end snapshot sbb-loading-indicator-circle renders with variant `circle` A11y tree Firefox */ + diff --git a/src/elements/loading-indicator-circle/loading-indicator-circle.scss b/src/elements/loading-indicator-circle/loading-indicator-circle.scss new file mode 100644 index 0000000000..0f4f57929a --- /dev/null +++ b/src/elements/loading-indicator-circle/loading-indicator-circle.scss @@ -0,0 +1,92 @@ +@use '../core/styles' as sbb; + +// Box-sizing rules contained in typography are not traversing Shadow DOM boundaries. We need to include box-sizing mixin in every component. +@include sbb.box-sizing; + +:host { + display: inline-block; + line-height: 0; + + --sbb-loading-indicator-circle-color: var(--sbb-color-red); + --sbb-loading-indicator-circle-padding: #{sbb.px-to-rem-build(2)}; + --sbb-loading-indicator-circle-duration: var(--sbb-disable-animation-zero-duration, 1.5s); + --sbb-loading-indicator-circle-background-color: var(--sbb-color-white); + --sbb-loading-indicator-circle-background: conic-gradient( + from 90deg, + var(--sbb-loading-indicator-circle-background-color), + var(--sbb-loading-indicator-circle-color) + ); + --sbb-loading-indicator-circle-animated-width: 0.1875em; + --sbb-loading-indicator-circle-animated-height: 0.1875em; + --sbb-loading-indicator-circle-animated-border-radius: 50%; + + @include sbb.if-forced-colors { + --sbb-loading-indicator-circle-color: CanvasText !important; + --sbb-loading-indicator-circle-animated-width: 50%; + --sbb-loading-indicator-circle-animated-height: 100%; + --sbb-loading-indicator-circle-animated-border-radius: 0; + --sbb-loading-indicator-circle-background: transparent; + } +} + +:host([color='smoke']) { + --sbb-loading-indicator-circle-color: var(--sbb-color-smoke); +} + +:host([color='white']) { + --sbb-loading-indicator-circle-color: var(--sbb-color-white); +} + +:host([color='white']) { + --sbb-loading-indicator-circle-background-color: var(--sbb-color-iron); +} + +.sbb-loading-indicator { + display: inline-flex; + padding: var(--sbb-loading-indicator-circle-padding); + vertical-align: middle; + line-height: 1; +} + +.sbb-loading-indicator__animated-element { + width: 1.25em; + height: 1.25em; + display: inline-block; + color: transparent; + position: relative; + margin: 0 auto; + overflow: hidden; + border-radius: 50%; + background: var(--sbb-loading-indicator-circle-background); + mask: radial-gradient( + circle 0.4375em, + #0000 98%, + var(--sbb-loading-indicator-circle-background-color) + ); + animation: rotation var(--sbb-loading-indicator-circle-duration) infinite linear; + + // Rounded start of strong color part + &::after { + content: ''; + width: var(--sbb-loading-indicator-circle-animated-width); + height: var(--sbb-loading-indicator-circle-animated-height); + background: var(--sbb-loading-indicator-circle-color); + border-radius: var(--sbb-loading-indicator-circle-animated-border-radius); + position: absolute; + top: 50%; + right: 0; + translate: 0 -50%; + overflow: hidden; + margin: auto; + } +} + +@keyframes rotation { + from { + rotate: 0deg; + } + + to { + rotate: 359deg; + } +} diff --git a/src/elements/loading-indicator-circle/loading-indicator-circle.snapshot.spec.ts b/src/elements/loading-indicator-circle/loading-indicator-circle.snapshot.spec.ts new file mode 100644 index 0000000000..06363dbe57 --- /dev/null +++ b/src/elements/loading-indicator-circle/loading-indicator-circle.snapshot.spec.ts @@ -0,0 +1,28 @@ +import { expect } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { fixture, testA11yTreeSnapshot } from '../core/testing/private.js'; + +import type { SbbLoadingIndicatorCircleElement } from './loading-indicator-circle.js'; + +import './loading-indicator-circle.js'; + +describe(`sbb-loading-indicator-circle`, () => { + let element: SbbLoadingIndicatorCircleElement; + + describe('renders with variant `circle`', () => { + beforeEach(async () => { + element = await fixture(html``); + }); + + it('DOM', async () => { + await expect(element).dom.to.be.equalSnapshot(); + }); + + it('Shadow DOM', async () => { + await expect(element).shadowDom.to.be.equalSnapshot(); + }); + + testA11yTreeSnapshot(); + }); +}); diff --git a/src/elements/loading-indicator-circle/loading-indicator-circle.spec.ts b/src/elements/loading-indicator-circle/loading-indicator-circle.spec.ts new file mode 100644 index 0000000000..f064c0b074 --- /dev/null +++ b/src/elements/loading-indicator-circle/loading-indicator-circle.spec.ts @@ -0,0 +1,15 @@ +import { assert } from '@open-wc/testing'; +import { html } from 'lit/static-html.js'; + +import { fixture } from '../core/testing/private.js'; + +import { SbbLoadingIndicatorCircleElement } from './loading-indicator-circle.js'; + +describe(`sbb-loading-indicator-circle`, () => { + let element: SbbLoadingIndicatorCircleElement; + + it('renders', async () => { + element = await fixture(html``); + assert.instanceOf(element, SbbLoadingIndicatorCircleElement); + }); +}); diff --git a/src/elements/loading-indicator-circle/loading-indicator-circle.ssr.spec.ts b/src/elements/loading-indicator-circle/loading-indicator-circle.ssr.spec.ts new file mode 100644 index 0000000000..2c58b2be4b --- /dev/null +++ b/src/elements/loading-indicator-circle/loading-indicator-circle.ssr.spec.ts @@ -0,0 +1,23 @@ +import { assert } from '@open-wc/testing'; +import { html } from 'lit'; + +import { ssrHydratedFixture } from '../core/testing/private.js'; + +import { SbbLoadingIndicatorCircleElement } from './loading-indicator-circle.js'; + +describe(`sbb-loading-indicator-circle ssr`, () => { + let root: SbbLoadingIndicatorCircleElement; + + beforeEach(async () => { + root = await ssrHydratedFixture( + html``, + { + modules: ['./loading-indicator-circle.js'], + }, + ); + }); + + it('renders', () => { + assert.instanceOf(root, SbbLoadingIndicatorCircleElement); + }); +}); diff --git a/src/elements/loading-indicator-circle/loading-indicator-circle.stories.ts b/src/elements/loading-indicator-circle/loading-indicator-circle.stories.ts new file mode 100644 index 0000000000..acabba3db6 --- /dev/null +++ b/src/elements/loading-indicator-circle/loading-indicator-circle.stories.ts @@ -0,0 +1,107 @@ +import type { InputType, StoryContext } from '@storybook/types'; +import type { Args, ArgTypes, Meta, StoryObj } from '@storybook/web-components'; +import type { TemplateResult } from 'lit'; +import { html } from 'lit'; + +import { sbbSpread } from '../../storybook/helpers/spread.js'; + +import type { SbbLoadingIndicatorCircleElement } from './loading-indicator-circle.js'; +import readme from './readme.md?raw'; + +import './loading-indicator-circle.js'; +import '../button/button.js'; +import '../title.js'; +import '../card.js'; + +const createLoadingIndicator = (event: Event): void => { + const loader: SbbLoadingIndicatorCircleElement = document.createElement( + 'sbb-loading-indicator-circle', + ); + const container = (event.currentTarget as HTMLElement).parentElement!.querySelector( + '.loader-container', + )!; + loader.setAttribute('aria-label', 'Loading, please wait'); + container.append(loader); + setTimeout(() => { + const p = document.createElement('p'); + p.textContent = "Loading complete. Here's your data: ..."; + container.append(p); + loader.remove(); + }, 5000); +}; + +const TemplateAccessibility = (): TemplateResult => html` + + Turn on your screen-reader and click the button to make the loading indicator appear. + +
+ createLoadingIndicator(event)}> Show loader +
+`; + +const Template = (args: Args): TemplateResult => html` +

+ Inline loading + indicator +

+ + Adaptive to + font size + +`; + +const color: InputType = { + control: { + type: 'inline-radio', + }, + options: ['default', 'smoke', 'white'], +}; + +const defaultArgTypes: ArgTypes = { + color, +}; + +const defaultArgs: Args = { + color: color.options![0], +}; + +export const Default: StoryObj = { + render: Template, + argTypes: defaultArgTypes, + args: { ...defaultArgs }, +}; + +export const Accessibility: StoryObj = { + render: TemplateAccessibility, + argTypes: defaultArgTypes, + args: { ...defaultArgs }, +}; + +const meta: Meta = { + decorators: [ + (story, context) => { + if (context.args.color === 'white') { + return html`
+ ${story()} +
`; + } + return story(); + }, + ], + parameters: { + backgroundColor: (context: StoryContext) => + context.args.color === 'white' ? 'var(--sbb-color-iron)' : 'var(--sbb-color-white)', + docs: { + extractComponentDescription: () => readme, + }, + }, + title: 'elements/sbb-loading-indicator-circle', +}; + +export default meta; diff --git a/src/elements/loading-indicator-circle/loading-indicator-circle.ts b/src/elements/loading-indicator-circle/loading-indicator-circle.ts new file mode 100644 index 0000000000..74d0fdfef4 --- /dev/null +++ b/src/elements/loading-indicator-circle/loading-indicator-circle.ts @@ -0,0 +1,38 @@ +import type { CSSResultGroup, TemplateResult } from 'lit'; +import { html, LitElement } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; + +import { hostAttributes } from '../core/decorators.js'; + +import style from './loading-indicator-circle.scss?lit&inline'; + +/** + * It displays a circle loading indicator. + */ +export +@customElement('sbb-loading-indicator-circle') +@hostAttributes({ + role: 'progressbar', + 'aria-busy': 'true', +}) +class SbbLoadingIndicatorCircleElement extends LitElement { + public static override styles: CSSResultGroup = style; + + /** Color variant. */ + @property({ reflect: true }) public accessor color: 'default' | 'smoke' | 'white' = 'default'; + + protected override render(): TemplateResult { + return html` + + + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'sbb-loading-indicator-circle': SbbLoadingIndicatorCircleElement; + } +} diff --git a/src/elements/loading-indicator-circle/loading-indicator-circle.visual.spec.ts b/src/elements/loading-indicator-circle/loading-indicator-circle.visual.spec.ts new file mode 100644 index 0000000000..fdab987aa8 --- /dev/null +++ b/src/elements/loading-indicator-circle/loading-indicator-circle.visual.spec.ts @@ -0,0 +1,31 @@ +import { html, nothing } from 'lit'; + +import { describeEach, describeViewports, visualDiffDefault } from '../core/testing/private.js'; + +import './loading-indicator-circle.js'; + +describe(`sbb-loading-indicator-circle`, () => { + const cases = { + color: ['default', 'smoke', 'white'], + size: ['s', 'l'], + }; + + describeViewports({ viewports: ['zero'] }, () => { + describeEach(cases, ({ color, size }) => { + it( + '', + visualDiffDefault.with(async (setup) => { + await setup.withFixture( + html` + + `, + { backgroundColor: color === 'white' ? 'var(--sbb-color-charcoal)' : undefined }, + ); + }), + ); + }); + }); +}); diff --git a/src/elements/loading-indicator-circle/readme.md b/src/elements/loading-indicator-circle/readme.md new file mode 100644 index 0000000000..eb04dbd753 --- /dev/null +++ b/src/elements/loading-indicator-circle/readme.md @@ -0,0 +1,35 @@ +The `sbb-loading-indicator-circle` is a component which can be used to indicate progress status +or an ongoing activity which require some time to complete. + +```html + +``` + +It can be slotted in other components (e.g. `sbb-button`) in the icon slot. + +```html + + + Button + +``` + +## Accessibility + +If the `sbb-loading-indicator-circle` should be announced by screen-readers, use an element with the correct aria attributes +(`aria-live` set to `polite` or `assertive`, and possibly `aria-atomic` and `aria-relevant`) +and then append the `sbb-loading-indicator` on it after giving it the correct `aria-label`. + +```html +
+ +
+``` + + + +## Properties + +| Name | Attribute | Privacy | Type | Default | Description | +| ------- | --------- | ------- | --------------------------------- | ----------- | -------------- | +| `color` | `color` | public | `'default' \| 'smoke' \| 'white'` | `'default'` | Color variant. | diff --git a/src/elements/loading-indicator/__snapshots__/loading-indicator.snapshot.spec.snap.js b/src/elements/loading-indicator/__snapshots__/loading-indicator.snapshot.spec.snap.js index ecdee2654d..f458ae6d14 100644 --- a/src/elements/loading-indicator/__snapshots__/loading-indicator.snapshot.spec.snap.js +++ b/src/elements/loading-indicator/__snapshots__/loading-indicator.snapshot.spec.snap.js @@ -7,7 +7,6 @@ snapshots["sbb-loading-indicator renders with variant `window` DOM"] = color="default" role="progressbar" size="s" - variant="window" > `; diff --git a/src/elements/loading-indicator/loading-indicator.scss b/src/elements/loading-indicator/loading-indicator.scss index 03f2c0651a..0675cb57b2 100644 --- a/src/elements/loading-indicator/loading-indicator.scss +++ b/src/elements/loading-indicator/loading-indicator.scss @@ -5,6 +5,29 @@ :host { --sbb-loading-indicator-color: var(--sbb-color-red); + --sbb-loading-indicator-padding: 0; + --sbb-loading-indicator-duration: var( + --sbb-disable-animation-zero-duration, + var(--sbb-animation-duration-6x) + ); + --sbb-loading-indicator-window-element-rotation: 55.24deg; + --_sbb-loading-indicator-window-first-span-width: calc( + var(--sbb-loading-indicator-window-height) * 0.58 + ); + + // Size of the gap between the windows + --_sbb-loading-indicator-window-unit: calc( + var(--_sbb-loading-indicator-window-first-span-width) / 5.5 + ); + + // The spans must move one rectangle plus one gap per animation cycle + --_sbb-loading-indicator-window-element-animation-speed: calc( + var(--_sbb-loading-indicator-window-unit) * 6.5 + ); + + // Defaults to S + --sbb-loading-indicator-window-height: #{sbb.px-to-rem-build(18)}; + --sbb-loading-indicator-window-element-width: #{sbb.px-to-rem-build(55)}; display: inline-block; line-height: 0; @@ -18,166 +41,71 @@ --sbb-loading-indicator-color: var(--sbb-color-white); } -:host([variant='circle']) { - --sbb-loading-indicator-padding: var(--sbb-border-width-2x); - --sbb-loading-indicator-duration: var(--sbb-disable-animation-zero-duration, 1.5s); - --sbb-loading-indicator-background-color: var(--sbb-color-white); - --sbb-loading-indicator-circle-background: conic-gradient( - from 90deg, - var(--sbb-loading-indicator-background-color), - var(--sbb-loading-indicator-color) - ); - --sbb-loading-indicator-circle-animated-width: 0.1875em; - --sbb-loading-indicator-circle-animated-height: 0.1875em; - --sbb-loading-indicator-circle-animated-border-radius: 50%; -} - -:host([color='white'][variant='circle']) { - --sbb-loading-indicator-background-color: var(--sbb-color-iron); -} - -:host([variant='circle']) .sbb-loading-indicator { - display: inline-flex; - height: auto; - width: auto; - padding-inline: var(--sbb-loading-indicator-padding); - padding-block: var(--sbb-loading-indicator-padding); - vertical-align: middle; - line-height: 1; -} - -:host([variant='circle']) .sbb-loading-indicator__animated-element { - width: 1.25em; - height: 1.25em; - display: inline-block; - color: transparent; - position: relative; - margin: 0 auto; - overflow: hidden; - border-radius: 50%; - background: var(--sbb-loading-indicator-circle-background); - // stylelint-disable-next-line property-no-vendor-prefix - -webkit-mask: radial-gradient( - circle 0.4375em, - #0000 98%, - var(--sbb-loading-indicator-background-color) - ); - mask: radial-gradient(circle 0.4375em, #0000 98%, var(--sbb-loading-indicator-background-color)); - animation: rotation var(--sbb-loading-indicator-duration) infinite linear; - - &::after { - content: ''; - width: var(--sbb-loading-indicator-circle-animated-width); - height: var(--sbb-loading-indicator-circle-animated-height); - background: var(--sbb-loading-indicator-color); - border-radius: var(--sbb-loading-indicator-circle-animated-border-radius); - position: absolute; - top: 50%; - right: 0; - transform: translateY(-50%); - overflow: hidden; - margin: auto; - @include sbb.if-forced-colors { - --sbb-loading-indicator-color: CanvasText; - --sbb-loading-indicator-circle-animated-width: 50%; - --sbb-loading-indicator-circle-animated-height: 100%; - --sbb-loading-indicator-circle-animated-border-radius: 0; - } - } - - @include sbb.if-forced-colors { - --sbb-loading-indicator-circle-background: transparent; - } -} - -:host([color='white'][variant='circle']) .sbb-loading-indicator__animated-element::after { - @include sbb.if-forced-colors { - --sbb-loading-indicator-color: var(--sbb-color-white); - } +:host([size='l']) { + --sbb-loading-indicator-window-height: #{sbb.px-to-rem-build(32)}; + --sbb-loading-indicator-window-element-width: #{sbb.px-to-rem-build(100)}; } -:host([variant='window']) { - --sbb-loading-indicator-padding: 0; - --sbb-loading-indicator-duration: var( - --sbb-disable-animation-zero-duration, - var(--sbb-animation-duration-6x) - ); +:host([size='xl']) { + --sbb-loading-indicator-window-height: #{sbb.px-to-rem-build(51)}; + --sbb-loading-indicator-window-element-width: #{sbb.px-to-rem-build(140)}; } -:host([variant='window'][size='s']) { - --sbb-loading-indicator-window-height: #{sbb.px-to-rem-build(26.66666)}; - --sbb-loading-indicator-window-padding-block-start: #{sbb.px-to-rem-build(10.66666)}; - --sbb-loading-indicator-window-element-width: #{sbb.px-to-rem-build(55.46666)}; - --sbb-loading-indicator-window-element-height: #{sbb.px-to-rem-build(5.33333)}; - --sbb-loading-indicator-window-element-perspective: #{sbb.px-to-rem-build(96)}; - --sbb-loading-indicator-window-element-animation-name: loading-container-small; - --sbb-loading-indicator-window-last-span-width: #{sbb.px-to-rem-build(8.53333)}; - --sbb-loading-indicator-window-last-span-height: #{sbb.px-to-rem-build(5.33333)}; - --sbb-loading-indicator-window-last-span-margin: #{sbb.px-to-rem-build(3.2)}; - --sbb-loading-indicator-window-last-span-transform: translate3d( - #{sbb.px-to-rem-build(-1.6)}, - 0, - 0 - ); +:host([size='xxl']) { + --sbb-loading-indicator-window-height: #{sbb.px-to-rem-build(98)}; + --sbb-loading-indicator-window-element-width: #{sbb.px-to-rem-build(250)}; } -:host([variant='window'][size='l']) { - --sbb-loading-indicator-window-height: #{sbb.px-to-rem-build(48)}; - --sbb-loading-indicator-window-padding-block-start: #{sbb.px-to-rem-build(19.2)}; - --sbb-loading-indicator-window-element-width: #{sbb.px-to-rem-build(92.7969)}; - --sbb-loading-indicator-window-element-height: #{sbb.px-to-rem-build(9.59375)}; - --sbb-loading-indicator-window-element-perspective: #{sbb.px-to-rem-build(128)}; - --sbb-loading-indicator-window-element-animation-name: loading-container-large; - --sbb-loading-indicator-window-last-span-width: #{sbb.px-to-rem-build(16)}; - --sbb-loading-indicator-window-last-span-height: #{sbb.px-to-rem-build(9.59375)}; - --sbb-loading-indicator-window-last-span-margin: #{sbb.px-to-rem-build(3.2)}; +:host([size='xxxl']) { + --sbb-loading-indicator-window-height: #{sbb.px-to-rem-build(147)}; + --sbb-loading-indicator-window-element-width: #{sbb.px-to-rem-build(360)}; } -:host([variant='window']) span { +span { display: inline-block; } -:host([variant='window']) .sbb-loading-indicator { +.sbb-loading-indicator { display: flex; height: var(--sbb-loading-indicator-window-height); - padding-block-start: var(--sbb-loading-indicator-window-padding-block-start); + align-items: center; + + @include sbb.zero-width-space; } -:host([variant='window']) .sbb-loading-indicator__animated-element { +.sbb-loading-indicator__animated-element { + position: relative; + justify-content: center; + display: flex; margin: 0 auto; transform-origin: center; - transform: translate3d(-2em, 0, 0); + translate: 25%; backface-visibility: hidden; - transform-style: preserve-3d; width: var(--sbb-loading-indicator-window-element-width); - height: var(--sbb-loading-indicator-window-element-height); - perspective: var(--sbb-loading-indicator-window-element-perspective); + perspective: var(--sbb-loading-indicator-window-height); } -:host([variant='window']) .sbb-loading-indicator__animated-element > span { +.sbb-loading-indicator__animated-element > span { position: relative; - transform: rotateY(50deg) translateZ(1em); - transform-origin: right; + align-self: center; + transform-origin: left; + rotate: y var(--sbb-loading-indicator-window-element-rotation); backface-visibility: hidden; } -:host([variant='window']) .sbb-loading-indicator__animated-element > span > span { +.sbb-loading-indicator__animated-element > span > span { position: relative; display: flex; - animation-name: var(--sbb-loading-indicator-window-element-animation-name); - animation-timing-function: linear; - animation-iteration-count: infinite; - animation-duration: var(--sbb-loading-indicator-duration); + animation: loading-container var(--sbb-loading-indicator-duration) linear infinite; } -:host([variant='window']) .sbb-loading-indicator__animated-element > span > span > span { +.sbb-loading-indicator__animated-element > span > span > span { background: var(--sbb-loading-indicator-color); backface-visibility: hidden; - outline: var(--sbb-border-width-1x) solid rgb(0 0 0 / 0%); - width: var(--sbb-loading-indicator-window-last-span-width); - height: var(--sbb-loading-indicator-window-last-span-height); - margin-right: var(--sbb-loading-indicator-window-last-span-margin); - transform: var(--sbb-loading-indicator-window-last-span-transform); + outline: var(--sbb-border-width-1x) solid transparent; + width: var(--_sbb-loading-indicator-window-first-span-width); + height: var(--sbb-loading-indicator-window-height); + margin-inline-end: var(--_sbb-loading-indicator-window-unit); &:nth-child(1) { animation: loading-rectangle1 var(--sbb-loading-indicator-duration) linear infinite; @@ -200,27 +128,17 @@ } &:last-child { - margin-right: 0; + margin-inline-end: 0; } } -@keyframes loading-container-small { +@keyframes loading-container { 0% { - transform: translateX(0.73333em); + translate: var(--_sbb-loading-indicator-window-element-animation-speed); } 100% { - transform: translateX(0); - } -} - -@keyframes loading-container-large { - 0% { - transform: translateX(1.2em); - } - - 100% { - transform: translateX(0); + translate: 0; } } @@ -273,13 +191,3 @@ opacity: 0.25; } } - -@keyframes rotation { - from { - transform: rotate(0deg); - } - - to { - transform: rotate(359deg); - } -} diff --git a/src/elements/loading-indicator/loading-indicator.snapshot.spec.ts b/src/elements/loading-indicator/loading-indicator.snapshot.spec.ts index 68f295a3c1..d3d283fa32 100644 --- a/src/elements/loading-indicator/loading-indicator.snapshot.spec.ts +++ b/src/elements/loading-indicator/loading-indicator.snapshot.spec.ts @@ -4,6 +4,7 @@ import { html } from 'lit/static-html.js'; import { fixture, testA11yTreeSnapshot } from '../core/testing/private.js'; import type { SbbLoadingIndicatorElement } from './loading-indicator.js'; + import './loading-indicator.js'; describe(`sbb-loading-indicator`, () => { @@ -11,9 +12,7 @@ describe(`sbb-loading-indicator`, () => { describe('renders with variant `window`', () => { beforeEach(async () => { - element = await fixture( - html``, - ); + element = await fixture(html``); }); it('DOM', async () => { @@ -26,20 +25,4 @@ describe(`sbb-loading-indicator`, () => { testA11yTreeSnapshot(); }); - - describe('renders with variant `circle`', () => { - beforeEach(async () => { - element = await fixture( - html``, - ); - }); - - it('DOM', async () => { - await expect(element).dom.to.be.equalSnapshot(); - }); - - it('Shadow DOM', async () => { - await expect(element).shadowDom.to.be.equalSnapshot(); - }); - }); }); diff --git a/src/elements/loading-indicator/loading-indicator.stories.ts b/src/elements/loading-indicator/loading-indicator.stories.ts index 259fd8b19f..aead89e1c7 100644 --- a/src/elements/loading-indicator/loading-indicator.stories.ts +++ b/src/elements/loading-indicator/loading-indicator.stories.ts @@ -1,5 +1,5 @@ import type { InputType, StoryContext } from '@storybook/types'; -import type { Meta, StoryObj, ArgTypes, Args } from '@storybook/web-components'; +import type { Args, ArgTypes, Meta, StoryObj } from '@storybook/web-components'; import type { TemplateResult } from 'lit'; import { html } from 'lit'; @@ -10,7 +10,6 @@ import readme from './readme.md?raw'; import './loading-indicator.js'; import '../button/button.js'; -import '../title.js'; import '../card.js'; const createLoadingIndicator = (event: Event, args: Args): void => { @@ -20,7 +19,6 @@ const createLoadingIndicator = (event: Event, args: Args): void => { )!; loader.setAttribute('aria-label', 'Loading, please wait'); loader.size = args['size']; - loader.variant = args['variant']; container.append(loader); setTimeout(() => { const p = document.createElement('p'); @@ -38,32 +36,22 @@ const TemplateAccessibility = (args: Args): TemplateResult => html` createLoadingIndicator(event, args)}> Show loader -
+
`; const Template = (args: Args): TemplateResult => html` `; -const CircleTemplate = (args: Args): TemplateResult => html` -

Inline loading indicator

- - Adaptive to font size - -`; - -const variant: InputType = { - control: { - type: 'select', - }, - options: ['window', 'circle'], -}; - const size: InputType = { control: { type: 'inline-radio', }, - options: ['s', 'l'], + options: ['s', 'l', 'xl', 'xxl', 'xxxl'], }; const color: InputType = { @@ -74,79 +62,21 @@ const color: InputType = { }; const defaultArgTypes: ArgTypes = { - variant, size, color, }; const defaultArgs: Args = { - variant: variant.options![0], size: size.options![0], color: color.options![0], }; -export const WindowSmallDefault: StoryObj = { +export const Default: StoryObj = { render: Template, argTypes: defaultArgTypes, args: { ...defaultArgs }, }; -export const WindowSmallSmoke: StoryObj = { - render: Template, - argTypes: defaultArgTypes, - args: { ...defaultArgs, color: color.options![1] }, -}; - -export const WindowSmallWhite: StoryObj = { - render: Template, - argTypes: defaultArgTypes, - args: { ...defaultArgs, color: color.options![2] }, -}; - -export const WindowLargeDefault: StoryObj = { - render: Template, - argTypes: defaultArgTypes, - args: { ...defaultArgs, size: size.options![1] }, -}; - -export const WindowLargeSmoke: StoryObj = { - render: Template, - argTypes: defaultArgTypes, - args: { ...defaultArgs, color: color.options![1], size: size.options![1] }, -}; - -export const WindowLargeWhite: StoryObj = { - render: Template, - argTypes: defaultArgTypes, - args: { ...defaultArgs, color: color.options![2], size: size.options![1] }, -}; - -export const CircleDefault: StoryObj = { - render: CircleTemplate, - argTypes: defaultArgTypes, - args: { ...defaultArgs, variant: variant.options![1] }, -}; - -export const CircleSmoke: StoryObj = { - render: CircleTemplate, - argTypes: defaultArgTypes, - args: { ...defaultArgs, color: color.options![1], variant: variant.options![1] }, -}; - -export const CircleWhite: StoryObj = { - render: CircleTemplate, - argTypes: defaultArgTypes, - args: { ...defaultArgs, color: color.options![2], variant: variant.options![1] }, - decorators: [ - (story) => - html`
- ${story()} -
`, - ], -}; - export const Accessibility: StoryObj = { render: TemplateAccessibility, argTypes: defaultArgTypes, diff --git a/src/elements/loading-indicator/loading-indicator.ts b/src/elements/loading-indicator/loading-indicator.ts index a65ec48dc2..90fe192514 100644 --- a/src/elements/loading-indicator/loading-indicator.ts +++ b/src/elements/loading-indicator/loading-indicator.ts @@ -1,5 +1,5 @@ import type { CSSResultGroup, TemplateResult } from 'lit'; -import { html, LitElement, nothing } from 'lit'; +import { html, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { hostAttributes } from '../core/decorators.js'; @@ -18,11 +18,8 @@ export class SbbLoadingIndicatorElement extends LitElement { public static override styles: CSSResultGroup = style; - /** Variant of the loading indicator; `circle` is meant to be used inline, while `window` as overlay. */ - @property({ reflect: true }) public accessor variant: 'window' | 'circle' = 'window'; - /** Size variant, either s or m. */ - @property({ reflect: true }) public accessor size: 's' | 'l' = 's'; + @property({ reflect: true }) public accessor size: 's' | 'l' | 'xl' | 'xxl' | 'xxxl' = 's'; /** Color variant. */ @property({ reflect: true }) public accessor color: 'default' | 'smoke' | 'white' = 'default'; @@ -31,17 +28,15 @@ class SbbLoadingIndicatorElement extends LitElement { return html` - ${this.variant === 'window' - ? html` - - - - - - - - ` - : nothing} + + + + + + + + + `; diff --git a/src/elements/loading-indicator/loading-indicator.visual.spec.ts b/src/elements/loading-indicator/loading-indicator.visual.spec.ts index 239d3d775b..1be4e570db 100644 --- a/src/elements/loading-indicator/loading-indicator.visual.spec.ts +++ b/src/elements/loading-indicator/loading-indicator.visual.spec.ts @@ -1,4 +1,4 @@ -import { html, nothing } from 'lit'; +import { html } from 'lit'; import { describeEach, describeViewports, visualDiffDefault } from '../core/testing/private.js'; @@ -7,38 +7,16 @@ import './loading-indicator.js'; describe(`sbb-loading-indicator`, () => { const cases = { color: ['default', 'smoke', 'white'], - size: ['s', 'l'], + size: ['s', 'l', 'xl', 'xxl', 'xxxl'], }; describeViewports({ viewports: ['zero'] }, () => { describeEach(cases, ({ color, size }) => { it( - `variant=window`, + '', visualDiffDefault.with(async (setup) => { await setup.withFixture( - html` - - `, - { backgroundColor: color === 'white' ? 'var(--sbb-color-charcoal)' : undefined }, - ); - }), - ); - - it( - `variant=circle`, - visualDiffDefault.with(async (setup) => { - await setup.withFixture( - html` - - `, + html``, { backgroundColor: color === 'white' ? 'var(--sbb-color-charcoal)' : undefined }, ); }), diff --git a/src/elements/loading-indicator/readme.md b/src/elements/loading-indicator/readme.md index 8b5ffb28c5..1d2cb4b853 100644 --- a/src/elements/loading-indicator/readme.md +++ b/src/elements/loading-indicator/readme.md @@ -1,32 +1,12 @@ The `sbb-loading-indicator` is a component which can be used to indicate progress status or an ongoing activity which require some time to complete. -### Variants - -The component has two different variants. - -In `window` mode, the component completely covers the parent element, preventing interaction with it. - -```html - -``` - -While the `circle` mode can be used inline within another component (e.g. button); -in this case the component adjusts its size to the parent font size. - -```html - - - Click me - -``` - ### Style -In `window` mode it's possible to define the `size` of the component, choosing between `s` (default) and `l`. +It's possible to define the `size` of the component, choosing between `s` (default), `l`, `xl`, `xxl`, and `xxxl`. ```html - + ``` ## Accessibility @@ -37,11 +17,7 @@ and then append the `sbb-loading-indicator` on it after giving it the correct `a ```html
- +
``` @@ -49,8 +25,7 @@ and then append the `sbb-loading-indicator` on it after giving it the correct `a ## Properties -| Name | Attribute | Privacy | Type | Default | Description | -| --------- | --------- | ------- | --------------------------------- | ----------- | ------------------------------------------------------------------------------------------------- | -| `color` | `color` | public | `'default' \| 'smoke' \| 'white'` | `'default'` | Color variant. | -| `size` | `size` | public | `'s' \| 'l'` | `'s'` | Size variant, either s or m. | -| `variant` | `variant` | public | `'window' \| 'circle'` | `'window'` | Variant of the loading indicator; `circle` is meant to be used inline, while `window` as overlay. | +| Name | Attribute | Privacy | Type | Default | Description | +| ------- | --------- | ------- | --------------------------------------- | ----------- | ---------------------------- | +| `color` | `color` | public | `'default' \| 'smoke' \| 'white'` | `'default'` | Color variant. | +| `size` | `size` | public | `'s' \| 'l' \| 'xl' \| 'xxl' \| 'xxxl'` | `'s'` | Size variant, either s or m. |