diff --git a/.changeset/forty-knives-love.md b/.changeset/forty-knives-love.md new file mode 100644 index 0000000000..f7b7e0188d --- /dev/null +++ b/.changeset/forty-knives-love.md @@ -0,0 +1,5 @@ +--- +'@swisspost/design-system-styles': patch +--- + +Added a workaround to display progress bar on input range and on webkit browsers without JavaScript. diff --git a/.changeset/silver-items-press.md b/.changeset/silver-items-press.md new file mode 100644 index 0000000000..b78410c2a2 --- /dev/null +++ b/.changeset/silver-items-press.md @@ -0,0 +1,5 @@ +--- +'@swisspost/design-system-styles': patch +--- + +Refactored the new focus style to only be visible when using keyboard for form elements. diff --git a/packages/demo/src/app/bootstrap/components/form-range/form-range-custom-demo/form-range-custom-demo.component.html b/packages/demo/src/app/bootstrap/components/form-range/form-range-custom-demo/form-range-custom-demo.component.html index f1a72d62e7..ebf4bd1ee9 100644 --- a/packages/demo/src/app/bootstrap/components/form-range/form-range-custom-demo/form-range-custom-demo.component.html +++ b/packages/demo/src/app/bootstrap/components/form-range/form-range-custom-demo/form-range-custom-demo.component.html @@ -1,18 +1,11 @@ - + @@ -21,7 +14,6 @@ { - this.updateStyleProperties(); - }); - if (this.control) { - this.controlSub = this.control.valueChanges.subscribe(() => { - this.updateStyleProperties(); - }); - } - // handle the changes of attributes values, note: can only be detected if setted by - // elem.setAttribute. - // elem.value = '42' will _not_ trigger the mutation observer - this.mutationObserver = new MutationObserver(mutationsList => { - for (let mutation of mutationsList) { - if ( - mutation.type === 'attributes' && - ['value', 'min', 'max'].indexOf(mutation.attributeName) >= 0 - ) { - this.updateStyleProperties(); - break; - } - } - }); - this.mutationObserver.observe(elem, { attributes: true }); - } - - ngOnDestroy() { - this.mutationObserver.disconnect(); - if (this.controlSub) { - this.controlSub.unsubscribe(); - } - } -} diff --git a/packages/demo/src/app/bootstrap/components/form-range/form-range-demo-page/form-range-demo-page.component.html b/packages/demo/src/app/bootstrap/components/form-range/form-range-demo-page/form-range-demo-page.component.html index ce5a000db6..ee89078958 100644 --- a/packages/demo/src/app/bootstrap/components/form-range/form-range-demo-page/form-range-demo-page.component.html +++ b/packages/demo/src/app/bootstrap/components/form-range/form-range-demo-page/form-range-demo-page.component.html @@ -4,19 +4,6 @@

Form range

-

Recommended form range

-
-

- It is recommended to add the - - FormRangeDirective - - to your project so that the range inputs behavior is complete. -

-
- Example range - + diff --git a/packages/demo/src/app/ng-bootstrap/components/datepicker/datepicker-demo-page/datepicker-demo-page.component.html b/packages/demo/src/app/ng-bootstrap/components/datepicker/datepicker-demo-page/datepicker-demo-page.component.html index 5d26e6047b..fa53d16d87 100644 --- a/packages/demo/src/app/ng-bootstrap/components/datepicker/datepicker-demo-page/datepicker-demo-page.component.html +++ b/packages/demo/src/app/ng-bootstrap/components/datepicker/datepicker-demo-page/datepicker-demo-page.component.html @@ -95,7 +95,6 @@

Simple datepicker

Datepicker with validation { it('Has no detectable a11y violations on load for all variants', () => { cy.checkA11y('#root-inner', { rules: { - 'color-contrast': { - enabled: false, - }, 'heading-order': { enabled: false, }, diff --git a/packages/documentation/src/stories/components/cards/product-card/product-card.stories.ts b/packages/documentation/src/stories/components/cards/product-card/product-card.stories.ts index 99f51dd051..0939309f3b 100644 --- a/packages/documentation/src/stories/components/cards/product-card/product-card.stories.ts +++ b/packages/documentation/src/stories/components/cards/product-card/product-card.stories.ts @@ -88,7 +88,7 @@ function getText(args: Args) { function renderProductCard(args: Args) { return html` -
+
${getTitle(args)} ${getText(args)} @@ -137,7 +137,7 @@ export const Multipart: Story = {
-
+

Preiswert

@@ -167,7 +167,7 @@ export const Multipart: Story = {
-
+
Sample Product

140 x 90 mm bis B5 (250 x 176 mm)

@@ -217,7 +217,7 @@ export const Multipart: Story = {
-
+
@@ -231,7 +231,7 @@ export const Multipart: Story = {
-
+

Schneller

@@ -266,7 +266,7 @@ export const Multipart: Story = {
-
+
Sample Product

140 x 90 mm bis B5 (250 x 176 mm)

@@ -304,7 +304,7 @@ export const Multipart: Story = {
-
+
diff --git a/packages/documentation/src/stories/components/forms/input/input.snapshot.stories.ts b/packages/documentation/src/stories/components/forms/input/input.snapshot.stories.ts index 8eb3453ce4..1b4d74221a 100644 --- a/packages/documentation/src/stories/components/forms/input/input.snapshot.stories.ts +++ b/packages/documentation/src/stories/components/forms/input/input.snapshot.stories.ts @@ -66,7 +66,7 @@ function renderInputSnapshot(_args: Args, context: StoryContext) { (context.args.type === 'text' || context.args.type === 'password')), ) .map((args: Args) => { - context.id = `a-${crypto.randomUUID()}`; + context.id = `${bg}-${crypto.randomUUID()}`; return html`
${meta.render?.({ ...context.args, ...args }, context)}
`; })}
diff --git a/packages/documentation/src/stories/components/forms/input/input.stories.ts b/packages/documentation/src/stories/components/forms/input/input.stories.ts index 732f001126..694e08502e 100644 --- a/packages/documentation/src/stories/components/forms/input/input.stories.ts +++ b/packages/documentation/src/stories/components/forms/input/input.stories.ts @@ -183,7 +183,7 @@ export default meta; type Story = StoryObj; function render(args: Args, context: StoryContext) { - const id = `ExampleTextarea_${context.name}`; + const id = context.id ?? `ExampleTextarea_${context.name}`; const classes = [ 'form-control', args.size, diff --git a/packages/documentation/src/stories/components/forms/radio/radio.snapshot.stories.ts b/packages/documentation/src/stories/components/forms/radio/radio.snapshot.stories.ts index 78965fb654..66012d5436 100644 --- a/packages/documentation/src/stories/components/forms/radio/radio.snapshot.stories.ts +++ b/packages/documentation/src/stories/components/forms/radio/radio.snapshot.stories.ts @@ -39,7 +39,7 @@ export const Radio: Story = { // remove disabled & validated examples .filter((args: Args) => !(args.disabled && args.validation !== 'null')) .map((args: Args) => { - context.id = `a-${crypto.randomUUID()}`; + context.id = `${bg}-${crypto.randomUUID()}`; return meta.render?.({ ...context.args, ...args }, context); })}
diff --git a/packages/documentation/src/stories/components/forms/radio/radio.stories.ts b/packages/documentation/src/stories/components/forms/radio/radio.stories.ts index d3e594e2bc..365299ed58 100644 --- a/packages/documentation/src/stories/components/forms/radio/radio.stories.ts +++ b/packages/documentation/src/stories/components/forms/radio/radio.stories.ts @@ -119,7 +119,7 @@ const meta: MetaComponent = { function render(args: Args, context: StoryContext) { const [_, updateArgs] = useArgs(); - const id = `${context.viewMode}_${context.name.replace(/\s/g, '-')}_ExampleRadio`; + const id = context.id ?? `${context.viewMode}_${context.name.replace(/\s/g, '-')}_ExampleRadio`; const classes = ['form-check-input', args.validation].filter(c => c && c !== 'null').join(' '); const groupClasses = ['form-check', args.size].filter(c => c && c !== 'null').join(' '); diff --git a/packages/documentation/src/stories/components/forms/slider/slider.snapshot.stories.ts b/packages/documentation/src/stories/components/forms/slider/slider.snapshot.stories.ts index 9053933684..1be11c3164 100644 --- a/packages/documentation/src/stories/components/forms/slider/slider.snapshot.stories.ts +++ b/packages/documentation/src/stories/components/forms/slider/slider.snapshot.stories.ts @@ -49,7 +49,7 @@ export const Slider: Story = { hiddenLabel: [true], }), ].map((args: Args) => { - context.id = `a-${crypto.randomUUID()}`; + context.id = `${bg}-${crypto.randomUUID()}`; return meta.render?.({ ...context.args, ...args }, context); })}
diff --git a/packages/documentation/src/stories/components/forms/slider/slider.stories.ts b/packages/documentation/src/stories/components/forms/slider/slider.stories.ts index a37f9bd2ef..85974e8ef5 100644 --- a/packages/documentation/src/stories/components/forms/slider/slider.stories.ts +++ b/packages/documentation/src/stories/components/forms/slider/slider.stories.ts @@ -165,7 +165,7 @@ type Story = StoryObj; function render(args: Args, context: StoryContext) { const [_, updateArgs] = useArgs(); - const id = `${context.viewMode}_${context.name.replace(/\s/g, '-')}_ExampleRange`; + const id = context.id ?? `${context.viewMode}_${context.name.replace(/\s/g, '-')}_ExampleRange`; const classes = ['form-range', args.validation].filter(c => c && c !== 'null').join(' '); const useAriaLabel = args.hiddenLabel; @@ -200,7 +200,9 @@ function render(args: Args, context: StoryContext) { if (args.showValue === 'text') { valueElement = html`

${args.value}

`; } else if (args.showValue === 'input') { - const inputId = `${context.viewMode}_${context.name.replace(/\s/g, '-')}_ExampleRangeInput`; + const inputId = context.id + ? `${context.id}_input` + : `${context.viewMode}_${context.name.replace(/\s/g, '-')}_ExampleRangeInput`; valueElement = [ html` `, diff --git a/packages/documentation/src/stories/components/forms/switch/switch.snapshot.stories.ts b/packages/documentation/src/stories/components/forms/switch/switch.snapshot.stories.ts index 5d34b3bc3b..b550dfdd84 100644 --- a/packages/documentation/src/stories/components/forms/switch/switch.snapshot.stories.ts +++ b/packages/documentation/src/stories/components/forms/switch/switch.snapshot.stories.ts @@ -16,29 +16,30 @@ export const Switch: Story = { render: (_args: Args, context: StoryContext) => { const longerText = 'Longa etikedo kiu plej versajne ne taugas sur unu linio kaj tial devas esti envolvita. Kaj nur por esti sur la sekura flanko, ni simple aldonu unu plian tre sencelan frazon ci tie. Vi neniam scias...'; - const templateVariants = bombArgs({ - labelPosition: ['before', 'after'], - label: ['Notifications', longerText], - hiddenLabel: [false], - checked: [false, true], - disabled: [false, true], - validation: ['null', 'is-valid', 'is-invalid'], - }) - .filter((args: Args) => !(args.labelPosition == 'before' && args.label === longerText)) - .map(args => ({ ...args, id: `a-${crypto.randomUUID()}` })) - .map( - (args: Args) => - html` -
- ${meta.render?.({ ...context.args, ...args }, { ...context, id: args.id })} -
- `, - ); + const templateVariants = (bg: string) => + bombArgs({ + labelPosition: ['before', 'after'], + label: ['Notifications', longerText], + hiddenLabel: [false], + checked: [false, true], + disabled: [false, true], + validation: ['null', 'is-valid', 'is-invalid'], + }) + .filter((args: Args) => !(args.labelPosition == 'before' && args.label === longerText)) + .map(args => ({ ...args, id: `${bg}-${crypto.randomUUID()}` })) + .map( + (args: Args) => + html` +
+ ${meta.render?.({ ...context.args, ...args }, { ...context, id: args.id })} +
+ `, + ); return html`
${['white', 'dark'].map( - bg => html`
${templateVariants}
`, + bg => html`
${templateVariants(bg)}
`, )}
`; diff --git a/packages/documentation/src/stories/components/forms/textarea/textarea.snapshot.stories.ts b/packages/documentation/src/stories/components/forms/textarea/textarea.snapshot.stories.ts index 41e7cea941..6c6e01806f 100644 --- a/packages/documentation/src/stories/components/forms/textarea/textarea.snapshot.stories.ts +++ b/packages/documentation/src/stories/components/forms/textarea/textarea.snapshot.stories.ts @@ -43,7 +43,7 @@ export const Textarea: Story = {

Sizes

${getCombinations('size', context.argTypes.size.options, combinations).map( (args: Args) => { - context.id = `a-${crypto.randomUUID()}`; + context.id = `${bg}-${crypto.randomUUID()}`; return html`
${args.title !== undefined && args.title @@ -62,7 +62,7 @@ export const Textarea: Story = { )}

Floating Label

${getCombinations('floatingLabel', [true], combinations).map((args: Args) => { - context.id = `a-${crypto.randomUUID()}`; + context.id = `${bg}-${crypto.randomUUID()}`; return html`
${meta.render?.({ ...context.args, ...args }, context)}
`; })}
diff --git a/packages/documentation/src/stories/components/forms/textarea/textarea.stories.ts b/packages/documentation/src/stories/components/forms/textarea/textarea.stories.ts index d3d1d02f35..8f5ac6ceee 100644 --- a/packages/documentation/src/stories/components/forms/textarea/textarea.stories.ts +++ b/packages/documentation/src/stories/components/forms/textarea/textarea.stories.ts @@ -154,7 +154,8 @@ export default meta; type Story = StoryObj; function renderTextarea(args: Args, context: StoryContext) { - const id = `${context.viewMode}_${context.story.replace(/\s/g, '-')}_ExampleTextarea`; + const id = + context.id ?? `${context.viewMode}_${context.story.replace(/\s/g, '-')}_ExampleTextarea`; const classes = mapClasses({ 'form-control': true, [args.size]: args.size && args.size !== 'null', diff --git a/packages/styles/src/components/form-range.scss b/packages/styles/src/components/form-range.scss index 0f0d21b90a..66f698fdf2 100644 --- a/packages/styles/src/components/form-range.scss +++ b/packages/styles/src/components/form-range.scss @@ -10,11 +10,24 @@ @use './../variables/components/forms'; @use './../mixins/utilities'; +$track-height: 4px; +$webkit-progress-height-adjustment: 2px; +$webkit-thumb-width: 32px; + +:has(> .form-range) { + @include utilities.focus-style(); +} + +@supports not selector(:has(> .form-range)) { + .form-range { + @include utilities.focus-style(); + } +} + .form-range { - // https://codepen.io/thebabydino/pen/goYYrN - --post-range: calc(var(--post-max) - var(--post-min)); - --post-ratio: calc((var(--post-val) - var(--post-min)) / var(--post-range)); - --post-sx: calc(0.5 * 1.5em + var(--post-ratio) * (100% - 1.5em)); + &::-webkit-slider-container { + overflow-x: clip; + } &::-moz-range-thumb { border-radius: 50%; @@ -22,26 +35,75 @@ cursor: pointer; } + &::-webkit-slider-runnable-track { + height: $track-height; + } + + &::-webkit-slider-thumb { + // Source: https://antvil.github.io/css-sos/sos/progress/ + clip-path: polygon( + 0 calc(50% - $track-height * 0.5), + 1px calc(50% - #{$track-height * 0.5 + 4px}), + 1px 0, + $webkit-thumb-width 0, + $webkit-thumb-width $webkit-thumb-width, + 1px $webkit-thumb-width, + 1px calc(50% + #{$track-height * 0.5 + 4px}), + 0 calc(50% + #{$track-height * 0.5}), + -100vw calc(50% + #{$track-height * 0.5}), + -100vw calc(50% - #{$track-height * 0.5}) + ); + } + + &::-moz-range-track, + &::-moz-range-progress { + height: $track-height; + } + &:not(:disabled, .disabled) { &::-webkit-slider-runnable-track { - background: linear-gradient(color.$black, color.$black) 0 / var(--post-sx) 100%; - background-repeat: no-repeat; background-color: color.$gray-20; } &::-moz-range-progress { background-color: color.$black; } + + &::-webkit-slider-thumb { + box-shadow: calc(-100vw - $webkit-thumb-width) 0 0 100vw color.$black; + } + + &:hover { + &::-webkit-slider-thumb { + border-width: 3px; + } + + &::-moz-range-thumb { + border-width: 3px; + } + } + + @include utilities.focus-style-custom('::-moz-range-thumb') { + box-shadow: none; // Remove default style + outline: none; + } } &:disabled, &.disabled { &::-webkit-slider-thumb { border-color: forms.$form-range-thumb-disabled-border-color; + border-style: dashed; + box-shadow: calc(-100vw - $webkit-thumb-width) 0 0 100vw color.$gray-40; } &::-moz-range-thumb { border-color: forms.$form-range-thumb-disabled-border-color; + border-style: dashed; + } + + &::-moz-range-progress { + background-color: color.$gray-40; } } @@ -57,10 +119,16 @@ // so, the "forced-color-adjust" property is necessary for "linear-gradient" to continue to work forced-color-adjust: none; + &::-webkit-slider-thumb { + box-shadow: calc(-100vw - $webkit-thumb-width) 0 0 100vw SelectedItem !important; + } + + &::-moz-range-progress { + background-color: SelectedItem !important; + } + &:not(:disabled, .disabled) { &::-webkit-slider-runnable-track { - background: linear-gradient(Highlight, Highlight) 0 / var(--post-sx) 100%; - background-repeat: no-repeat; background-color: ButtonBorder; } @@ -73,38 +141,22 @@ background-color: ButtonText; } - &::-moz-range-progress { - background-color: Highlight; - } - &::-moz-range-thumb { background-color: ButtonFace; border-color: ButtonText; } - - &:focus-visible { - &::-webkit-slider-thumb { - outline-offset: commons.$border-thick; - outline: commons.$border-thick solid Highlight; - } - - &::-moz-range-thumb { - outline-offset: commons.$border-thick; - outline: commons.$border-thick solid Highlight; - } - } } &:disabled, &.disabled { &::-webkit-slider-thumb { background-color: ButtonFace; - border-color: ButtonBorder; + border-color: GrayText; } &::-moz-range-thumb { background-color: ButtonFace; - border-color: ButtonBorder; + border-color: GrayText; } } } diff --git a/packages/styles/src/mixins/_utilities.scss b/packages/styles/src/mixins/_utilities.scss index 58fd54bb27..63d4363bc3 100644 --- a/packages/styles/src/mixins/_utilities.scss +++ b/packages/styles/src/mixins/_utilities.scss @@ -87,11 +87,16 @@ outline: none; } -@mixin focus-style($border-radius: commons.$border-radius, $vendor-prefix: '') { - &:is(:focus-visible, :focus-within, .pretend-focus)#{$vendor-prefix} { - outline-offset: spacing.$size-line; - outline: spacing.$size-line solid var(--post-focus-color); - border-radius: $border-radius; +@mixin focus-style($vendor-prefix: '') { + outline-style: none; + outline-offset: spacing.$size-line; + outline-width: spacing.$size-line; + outline-color: var(--post-focus-color); + + // :has(:focus-visible) mimic a focus-visible-within pseudo-class + &:is(:focus-visible, :has(:focus-visible), .pretend-focus)#{$vendor-prefix} { + outline-style: solid; + border-radius: commons.$border-radius !important; @include high-contrast-mode() { outline-color: Highlight; @@ -100,10 +105,33 @@ // In case rules need to be slightly adjusted @content; } + + // When a browser doesn't support :has, use focus-within as a fallback. This means that focus state is displayed on focus and not on focus-visible only (except some browsers like Safari). + @supports not selector(:has(:focus-visible)) { + &:is(:focus-visible, :focus-within, .pretend-focus)#{$vendor-prefix} { + outline-style: solid; + border-radius: commons.$border-radius !important; + + @include high-contrast-mode() { + outline-color: Highlight; + } + + // In case rules need to be slightly adjusted + @content; + } + } } @mixin focus-style-custom($vendor-prefix: '') { - &:is(:focus-visible, :focus-within, .pretend-focus)#{$vendor-prefix} { + // :has(:focus-visible) mimic a focus-visible-within pseudo-class + &:is(:focus-visible, :has(:focus-visible), .pretend-focus)#{$vendor-prefix} { @content; } + + // When a browser doesn't support :has, use focus-within as a fallback. This means that focus state is displayed on focus and not on focus-visible only (except some browsers like Safari). + @supports not selector(:has(:focus-visible)) { + &:is(:focus-visible, :focus-within, .pretend-focus)#{$vendor-prefix} { + @content; + } + } }