From 7ff65524aa9d849ce1a0afd816ff11dc15eadfed Mon Sep 17 00:00:00 2001 From: Tommaso Menga Date: Mon, 2 Dec 2024 11:10:09 +0100 Subject: [PATCH] fix(sbb-teaser, sbb-teaser-product): allow screen readers to navigate the content (#3250) Co-authored-by: Jeremias Peier --- .../core/base-elements/link-base-element.ts | 6 +- .../common/teaser-product-common.scss | 15 +++-- .../common/teaser-product-common.ts | 22 ++++--- ...easer-product-static.snapshot.spec.snap.js | 28 +++++---- .../teaser-product.snapshot.spec.snap.js | 62 +++++++++++++------ .../teaser-product/teaser-product.scss | 44 +++++++++---- .../teaser-product/teaser-product.stories.ts | 6 +- .../teaser-product/teaser-product.ts | 18 +++++- .../teaser.snapshot.spec.snap.js | 59 ++++++++++++------ src/elements/teaser/readme.md | 2 - src/elements/teaser/teaser.scss | 21 +++++-- src/elements/teaser/teaser.ts | 16 ++++- src/elements/teaser/teaser.visual.spec.ts | 17 +++++ 13 files changed, 222 insertions(+), 94 deletions(-) diff --git a/src/elements/core/base-elements/link-base-element.ts b/src/elements/core/base-elements/link-base-element.ts index 2c87979d1c..406e360b6c 100644 --- a/src/elements/core/base-elements/link-base-element.ts +++ b/src/elements/core/base-elements/link-base-element.ts @@ -73,6 +73,10 @@ abstract class SbbLinkBaseElement extends SbbActionBaseElement { /** Default render method for link-like components. Can be overridden if the LinkRenderVariables are not needed. */ protected override render(): TemplateResult { + return this.renderLink(this.renderTemplate()); + } + + protected renderLink(renderContent: TemplateResult): TemplateResult { return html` - ${this.renderTemplate()} + ${renderContent} ${!!this.href && this.target === '_blank' ? html`. ${i18nTargetOpensInNewWindow[this.language.current]} - - - - - - - - +
+
+
+ + + +
+ +
+
+
`; } } diff --git a/src/elements/teaser-product/teaser-product-static/__snapshots__/teaser-product-static.snapshot.spec.snap.js b/src/elements/teaser-product/teaser-product-static/__snapshots__/teaser-product-static.snapshot.spec.snap.js index 21154bd051..eae0b1d4d2 100644 --- a/src/elements/teaser-product/teaser-product-static/__snapshots__/teaser-product-static.snapshot.spec.snap.js +++ b/src/elements/teaser-product/teaser-product-static/__snapshots__/teaser-product-static.snapshot.spec.snap.js @@ -28,20 +28,22 @@ snapshots["sbb-teaser-product-static renders DOM"] = snapshots["sbb-teaser-product-static renders Shadow DOM"] = ` - - - - - - - +
+
+ - - - - - - +
+
+ + + + +
+ + +
+
+
`; /* end snapshot sbb-teaser-product-static renders Shadow DOM */ diff --git a/src/elements/teaser-product/teaser-product/__snapshots__/teaser-product.snapshot.spec.snap.js b/src/elements/teaser-product/teaser-product/__snapshots__/teaser-product.snapshot.spec.snap.js index b850117190..09cd442e4c 100644 --- a/src/elements/teaser-product/teaser-product/__snapshots__/teaser-product.snapshot.spec.snap.js +++ b/src/elements/teaser-product/teaser-product/__snapshots__/teaser-product.snapshot.spec.snap.js @@ -29,25 +29,31 @@ snapshots["sbb-teaser-product renders DOM"] = /* end snapshot sbb-teaser-product renders DOM */ snapshots["sbb-teaser-product renders Shadow DOM"] = -`
- - - - - - - - - - - +`
+ + + + +
+
+ - - - +
+
+ + + + +
+ + +
+
+
+
`; /* end snapshot sbb-teaser-product renders Shadow DOM */ @@ -59,8 +65,16 @@ snapshots["sbb-teaser-product renders A11y tree Firefox"] = "children": [ { "role": "link", - "name": "Content Footnote", + "name": "", "value": "https://www.sbb.ch/" + }, + { + "role": "text leaf", + "name": "Content" + }, + { + "role": "text leaf", + "name": "Footnote" } ] } @@ -76,7 +90,15 @@ snapshots["sbb-teaser-product renders A11y tree Chrome"] = "children": [ { "role": "link", - "name": "Content Footnote" + "name": "" + }, + { + "role": "text", + "name": "Content" + }, + { + "role": "text", + "name": "Footnote" } ] } diff --git a/src/elements/teaser-product/teaser-product/teaser-product.scss b/src/elements/teaser-product/teaser-product/teaser-product.scss index 13e52cc285..a13a47623c 100644 --- a/src/elements/teaser-product/teaser-product/teaser-product.scss +++ b/src/elements/teaser-product/teaser-product/teaser-product.scss @@ -8,6 +8,13 @@ ); --sbb-teaser-product-animation-easing: var(--sbb-animation-easing); --sbb-teaser-product-border-radius: var(--sbb-border-radius-4x); + + // Simulate link color optically + @include sbb.if-forced-colors { + --sbb-title-text-color-normal-override: LinkText !important; + --sbb-teaser-product-content-color: LinkText !important; + --sbb-teaser-product-footer-color: LinkText !important; + } } :host(:hover) { @@ -16,14 +23,20 @@ } } -.sbb-teaser-product { - &:focus-visible { - :host(:not([data-focus-origin='mouse'], [data-focus-origin='touch'])) & { - @include sbb.focus-outline; +::slotted(:is(img, sbb-image)) { + will-change: filter; + transition-property: filter; + transition-duration: var(--sbb-teaser-product-animation-duration); + transition-timing-function: var(--sbb-animation-easing); + filter: brightness(var(--sbb-teaser-product-brightness, 1)); +} - border-radius: var(--sbb-teaser-product-border-radius); - } - } +.sbb-teaser-product__wrapper { + position: relative; +} + +.sbb-teaser-product__root { + pointer-events: none; @include sbb.if-forced-colors { &::after { @@ -34,10 +47,15 @@ } } -::slotted(:is(img, sbb-image)) { - will-change: filter; - transition-property: filter; - transition-duration: var(--sbb-teaser-product-animation-duration); - transition-timing-function: var(--sbb-animation-easing); - filter: brightness(var(--sbb-teaser-product-brightness, 1)); +.sbb-teaser-product { + position: absolute; + inset: 0; + + &:focus-visible { + :host(:not([data-focus-origin='mouse'], [data-focus-origin='touch'])) & { + @include sbb.focus-outline; + + border-radius: var(--sbb-teaser-product-border-radius); + } + } } diff --git a/src/elements/teaser-product/teaser-product/teaser-product.stories.ts b/src/elements/teaser-product/teaser-product/teaser-product.stories.ts index b17659f9c1..2f02fd0eb6 100644 --- a/src/elements/teaser-product/teaser-product/teaser-product.stories.ts +++ b/src/elements/teaser-product/teaser-product/teaser-product.stories.ts @@ -8,13 +8,13 @@ import type { StoryContext, StoryObj, } from '@storybook/web-components'; -import { nothing, type TemplateResult } from 'lit'; -import { html } from 'lit'; +import { html, nothing, type TemplateResult } from 'lit'; import { sbbSpread } from '../../../storybook/helpers/spread.js'; import sampleImages from '../../core/images.js'; import readme from './readme.md?raw'; + import './teaser-product.js'; import '../../button/button-static.js'; import '../../image.js'; @@ -78,7 +78,7 @@ const defaultArgs: Args = { withFooter: true, slottedImg: false, href: 'https://www.sbb.ch', - 'accessibility-label': undefined, + 'accessibility-label': 'Benefit from up to 70% discount, Follow the link to benefit.', }; const content = (): TemplateResult => html` diff --git a/src/elements/teaser-product/teaser-product/teaser-product.ts b/src/elements/teaser-product/teaser-product/teaser-product.ts index 5d6cd26ac8..89e0397715 100644 --- a/src/elements/teaser-product/teaser-product/teaser-product.ts +++ b/src/elements/teaser-product/teaser-product/teaser-product.ts @@ -1,11 +1,14 @@ -import type { CSSResultGroup } from 'lit'; +import type { CSSResultGroup, TemplateResult } from 'lit'; import { customElement } from 'lit/decorators.js'; +import { html } from 'lit/static-html.js'; import { SbbLinkBaseElement } from '../../core/base-elements.js'; import { SbbTeaserProductCommonElementMixin, teaserProductCommonStyle } from '../common.js'; import style from './teaser-product.scss?lit&inline'; +import '../../screen-reader-only.js'; + /** * Displays a text and a footnote, combined with an image, to tease a product * @@ -19,6 +22,19 @@ export @customElement('sbb-teaser-product') class SbbTeaserProductElement extends SbbTeaserProductCommonElementMixin(SbbLinkBaseElement) { public static override styles: CSSResultGroup = [teaserProductCommonStyle, style]; + + protected override render(): TemplateResult { + // We render the content outside the anchor tag to allow screen readers to navigate through it + return html` +
+ ${this.renderLink( + // For SEO we add the accessibility hidden as hidden content of the link + html`${this.accessibilityLabel}`, + )} + ${this.renderTemplate()} +
+ `; + } } declare global { diff --git a/src/elements/teaser/__snapshots__/teaser.snapshot.spec.snap.js b/src/elements/teaser/__snapshots__/teaser.snapshot.spec.snap.js index 4de19d15bd..14f4dcf7ad 100644 --- a/src/elements/teaser/__snapshots__/teaser.snapshot.spec.snap.js +++ b/src/elements/teaser/__snapshots__/teaser.snapshot.spec.snap.js @@ -14,11 +14,16 @@ snapshots["sbb-teaser renders after centered DOM"] = /* end snapshot sbb-teaser renders after centered DOM */ snapshots["sbb-teaser renders after centered Shadow DOM"] = -` +`
+ + + SBB teaser + + @@ -49,7 +54,7 @@ snapshots["sbb-teaser renders after centered Shadow DOM"] = - +
`; /* end snapshot sbb-teaser renders after centered Shadow DOM */ @@ -67,11 +72,16 @@ snapshots["sbb-teaser renders after with title level set DOM"] = /* end snapshot sbb-teaser renders after with title level set DOM */ snapshots["sbb-teaser renders after with title level set Shadow DOM"] = -` +`
+ + + SBB teaser + + @@ -102,7 +112,7 @@ snapshots["sbb-teaser renders after with title level set Shadow DOM"] = - +
`; /* end snapshot sbb-teaser renders after with title level set Shadow DOM */ @@ -132,11 +142,16 @@ snapshots["sbb-teaser renders below with projected content DOM"] = /* end snapshot sbb-teaser renders below with projected content DOM */ snapshots["sbb-teaser renders below with projected content Shadow DOM"] = -` +`
+ + + SBB teaser + + @@ -167,7 +182,7 @@ snapshots["sbb-teaser renders below with projected content Shadow DOM"] = - +
`; /* end snapshot sbb-teaser renders below with projected content Shadow DOM */ @@ -177,6 +192,10 @@ snapshots["sbb-teaser renders after centered A11y tree Chrome"] = "role": "WebArea", "name": "", "children": [ + { + "role": "text", + "name": "​" + }, { "role": "link", "name": "SBB teaser" @@ -193,6 +212,10 @@ snapshots["sbb-teaser renders after centered A11y tree Firefox"] = "role": "document", "name": "", "children": [ + { + "role": "statictext", + "name": "​" + }, { "role": "link", "name": "SBB teaser", diff --git a/src/elements/teaser/readme.md b/src/elements/teaser/readme.md index 6555ef08e1..ce417533f1 100644 --- a/src/elements/teaser/readme.md +++ b/src/elements/teaser/readme.md @@ -65,8 +65,6 @@ It's important to set the `accessibilityLabel` on the ``, which desc The description text is wrapped into an `

` element to guarantee the semantic meaning. -Avoid slotting block elements (e.g. `

`) as this violates semantic rules and can have negative effects on screen readers. - ## Properties diff --git a/src/elements/teaser/teaser.scss b/src/elements/teaser/teaser.scss index e91e849229..031a973714 100644 --- a/src/elements/teaser/teaser.scss +++ b/src/elements/teaser/teaser.scss @@ -22,6 +22,11 @@ --sbb-disable-animation-zero-time, var(--sbb-animation-duration-4x) ); + + @include sbb.if-forced-colors { + --sbb-teaser-description-color: LinkText; + --sbb-title-text-color-normal-override: LinkText !important; + } } :host([alignment='after']) { @@ -35,11 +40,18 @@ --sbb-teaser-width: 100%; } -.sbb-teaser { +.sbb-teaser__wrapper { display: flex; - text-decoration: none; + position: relative; + cursor: pointer; @include sbb.zero-width-space; +} + +.sbb-teaser { + text-decoration: none; + position: absolute; + inset: 0; // Hide focus outline when focus origin is mouse or touch. This is being used as a workaround in various components. :host(:not([data-focus-origin='mouse'], [data-focus-origin='touch'])) &:focus-visible { @@ -66,6 +78,7 @@ gap: var(--sbb-teaser-gap); max-width: 100%; width: 100%; + pointer-events: none; } .sbb-teaser__text { @@ -81,7 +94,7 @@ transition: var(--sbb-teaser-animation-duration) var(--sbb-animation-easing); @include sbb.hover-mq($hover: true) { - .sbb-teaser:hover & { + .sbb-teaser__wrapper:hover & { transform: scale(var(--sbb-teaser-scale-hover)); --sbb-teaser-brightness: var(--sbb-teaser-brightness-hover); @@ -105,7 +118,7 @@ transition: var(--sbb-teaser-animation-duration) var(--sbb-animation-easing); @include sbb.hover-mq($hover: true) { - .sbb-teaser:hover & { + .sbb-teaser__wrapper:hover & { @include sbb.shadow-level-9-hard; } } diff --git a/src/elements/teaser/teaser.ts b/src/elements/teaser/teaser.ts index b93d5c716a..4959e5c6d7 100644 --- a/src/elements/teaser/teaser.ts +++ b/src/elements/teaser/teaser.ts @@ -9,6 +9,7 @@ import type { SbbTitleLevel } from '../title.js'; import style from './teaser.scss?lit&inline'; import '../chip.js'; +import '../screen-reader-only.js'; import '../title.js'; /** @@ -42,7 +43,20 @@ class SbbTeaserElement extends SbbLinkBaseElement { @property({ attribute: 'chip-content', reflect: true, converter: omitEmptyConverter }) public accessor chipContent: string = ''; - protected override renderTemplate(): TemplateResult { + protected override render(): TemplateResult { + // We render the content outside the anchor tag to allow screen readers to navigate through it + return html` +
+ ${this.renderLink( + // For SEO we add the accessibility hidden as hidden content of the link + html`${this.accessibilityLabel}`, + )} + ${this.renderContent()} +
+ `; + } + + protected renderContent(): TemplateResult { return html` diff --git a/src/elements/teaser/teaser.visual.spec.ts b/src/elements/teaser/teaser.visual.spec.ts index 34101c0059..842128c86a 100644 --- a/src/elements/teaser/teaser.visual.spec.ts +++ b/src/elements/teaser/teaser.visual.spec.ts @@ -165,6 +165,23 @@ describe(`sbb-teaser`, () => { await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); }), ); + + it( + 'forcedColors=true', + visualDiffDefault.with(async (setup) => { + await setup.withFixture( + html` + + + This is a paragraph + + `, + { forcedColors: true }, + ); + + await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); + }), + ); }); } });