diff --git a/packages/components/src/components/post-popover-trigger/readme.md b/packages/components/src/components/post-popover-trigger/readme.md index c3dc54d5da..9ee7b43b64 100644 --- a/packages/components/src/components/post-popover-trigger/readme.md +++ b/packages/components/src/components/post-popover-trigger/readme.md @@ -1,12 +1,78 @@ -# post-popover-trigger +# post-popover +# post-popover-trigger - ## Properties +| Property | Attribute | Description | Type | Default | +| --------------------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| `arrow` | `arrow` | Show a little indicator arrow | `boolean` | `true` | +| `closeButtonCaption` _(required)_ | `close-button-caption` | Define the caption of the close button for assistive technology | `string` | `undefined` | +| `placement` | `placement` | Defines the position of the popover relative to its trigger. Popovers are automatically flipped to the opposite side if there is not enough available space and are shifted towards the viewport if they would overlap edge boundaries. For supported values and behavior details, see the [Floating UI placement documentation](https://floating-ui.com/docs/computePosition#placement). | `"bottom" \| "bottom-end" \| "bottom-start" \| "left" \| "left-end" \| "left-start" \| "right" \| "right-end" \| "right-start" \| "top" \| "top-end" \| "top-start"` | `'top'` | + + +## Methods + +### `hide() => Promise` + +Programmatically hide this popover + +#### Returns + +Type: `Promise` + + + +### `show(target: HTMLElement) => Promise` + +Programmatically display the popover + +#### Parameters + +| Name | Type | Description | +| -------- | ------------- | ---------------------------------------------------------------------------- | +| `target` | `HTMLElement` | An element with [data-popover-target="id"] where the popover should be shown | + +#### Returns + +Type: `Promise` + + + +### `toggle(target: HTMLElement, force?: boolean) => Promise` + +Toggle popover display + +#### Parameters + +| Name | Type | Description | +| -------- | ------------- | ---------------------------------------------------------------------------------- | +| `target` | `HTMLElement` | An element with [data-popover-target="id"] where the popover should be anchored to | +| `force` | `boolean` | Pass true to always show or false to always hide | + +#### Returns + +Type: `Promise` + + + + +## Dependencies + +### Depends on + +- [post-popovercontainer](../post-popovercontainer) + +### Graph +```mermaid +graph TD; + post-popover --> post-popovercontainer + style post-popover fill:#f9f,stroke:#333,stroke-width:4px +``` + | Property | Attribute | Description | Type | Default | | ------------------ | --------- | --------------------------------------------------------------------------------------------- | -------- | ----------- | | `for` _(required)_ | `for` | ID of the popover element that this trigger is linked to. Used to open and close the popover. | `string` | `undefined` | diff --git a/packages/documentation/.storybook/preview.ts b/packages/documentation/.storybook/preview.ts index 52d91c8f1d..1a1b29d779 100644 --- a/packages/documentation/.storybook/preview.ts +++ b/packages/documentation/.storybook/preview.ts @@ -19,6 +19,8 @@ import './styles/preview.scss'; import { SyntaxHighlighter } from 'storybook/internal/components'; import scss from 'react-syntax-highlighter/dist/esm/languages/prism/scss'; +import '../src/demo-components'; + SyntaxHighlighter.registerLanguage('scss', scss); export const SourceDarkScheme = true; diff --git a/packages/documentation/src/demo-components/demo-button.ts b/packages/documentation/src/demo-components/demo-button.ts new file mode 100644 index 0000000000..49c0c0f133 --- /dev/null +++ b/packages/documentation/src/demo-components/demo-button.ts @@ -0,0 +1,83 @@ +export class DemoButton extends HTMLElement { + static get observedAttributes() { + return ['button-version', 'workaround', 'arialabelledby-id', 'ariadescribedby-id']; + } + private buttonVersion?: '1' | '2' | '3' | '4'; + private workaround?: string; + private internalButton?: HTMLElement; + private arialabelledbyId?: string; + private ariadescribedbyId?: string; + private slotEl?: HTMLSlotElement; + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.render(); + } + + attributeChangedCallback(name: string, _oldValue: string, newValue: string) { + if (name === 'workaround') this.workaround = newValue; + if (name === 'arialabelledby-id') this.arialabelledbyId = newValue; + if (name === 'ariadescribedby-id') this.ariadescribedbyId = newValue; + if (name === 'button-version') this.buttonVersion = newValue as '1' | '2' | '3'; + this.render(); + } + + private setupAria() { + const isLabelled = this.workaround === 'ariaLabelledByElements'; + const isDescribed = this.workaround === 'ariaDescribedByElements'; + + if (!this.internalButton || (!isLabelled && !isDescribed)) { + return; + } + + let elementToLink: Element | null = null; + + if (this.buttonVersion === '1' || this.buttonVersion === '3') { + const id = isLabelled ? this.arialabelledbyId : this.ariadescribedbyId; + if (id) { + elementToLink = document.querySelector(`#${id}`); + } + } else if (this.buttonVersion === '2' || this.buttonVersion === '4') { + if (this.slotEl) { + const assignedElements = this.slotEl.assignedElements({ flatten: true }); + elementToLink = assignedElements.find(el => el.tagName === 'SPAN') || null; + } + } + + const ariaPropertyName = isLabelled ? 'ariaLabelledByElements' : 'ariaDescribedByElements'; + this.internalButton[ariaPropertyName] = elementToLink ? [elementToLink] : []; + } + + private render() { + if (!this.shadowRoot) return; + + if (this.buttonVersion) { + this.shadowRoot.innerHTML = ` + +
+
+ `; + } else { + this.shadowRoot.innerHTML = ` + My Text +
+ +
+ `; + } + + this.internalButton = this.shadowRoot.querySelector('div[role="button"]') as HTMLElement; + this.slotEl = this.shadowRoot.querySelector('slot[name="aria-slot"]') as HTMLSlotElement; + this.setupAria(); + } +} + +customElements.define('demo-button', DemoButton); diff --git a/packages/documentation/src/demo-components/demo-input.ts b/packages/documentation/src/demo-components/demo-input.ts new file mode 100644 index 0000000000..149681dbeb --- /dev/null +++ b/packages/documentation/src/demo-components/demo-input.ts @@ -0,0 +1,90 @@ +export class DemoInput extends HTMLElement { + static get observedAttributes() { + return ['workaround', 'arialabelledby-id', 'target-version']; + } + + private workaround?: string; + private arialabelledbyId?: string; + private targetVersion?: '1' | '2' | '3'; + private internalEl?: HTMLElement; + private slotEl?: HTMLSlotElement; + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.render(); + } + + attributeChangedCallback(name: string, _oldValue: string, newValue: string) { + if (name === 'workaround') this.workaround = newValue; + if (name === 'arialabelledby-id') this.arialabelledbyId = newValue; + if (name === 'target-version') this.targetVersion = newValue as '1' | '2' | '3'; + + this.render(); + } + + private setupAriaLabelledBy() { + if (!this.internalEl) { + return; + } + + let elementToLink: Element | null = null; + const isLabelledByWorkaround = this.workaround === 'ariaLabelledByElements'; + + if (this.targetVersion === '1') { + if (isLabelledByWorkaround && this.arialabelledbyId) { + elementToLink = document.querySelector(`[for="${this.arialabelledbyId}"]`); + } + } else if (this.targetVersion === '2') { + if (isLabelledByWorkaround && this.slotEl) { + const assignedElements = this.slotEl.assignedElements({ flatten: true }); + elementToLink = assignedElements.find(el => el.tagName === 'LABEL') || null; + } + } else if (this.targetVersion === '3') { + if (this.arialabelledbyId) { + elementToLink = document.querySelector(`#${this.arialabelledbyId}`); + } + } + + this.internalEl.ariaLabelledByElements = elementToLink ? [elementToLink] : []; + } + + private render() { + if (!this.shadowRoot) return; + //Version #2 + if (this.targetVersion === '2') { + this.shadowRoot.innerHTML = ` + + + `; + this.slotEl = this.shadowRoot.querySelector('slot[name="aria-slot"]') as HTMLSlotElement; + this.internalEl = this.shadowRoot.querySelector('#internal') as HTMLElement; + } else if (this.targetVersion === '3') { + // Version #3 + this.shadowRoot.innerHTML = ` + + `; + this.internalEl = this.shadowRoot.querySelector('#internal') as HTMLElement; + } else if (this.targetVersion === '1') { + // Version #1 + this.shadowRoot.innerHTML = ` + + + `; + this.internalEl = this.shadowRoot.querySelector('#internal') as HTMLElement; + } else { + // Version default + this.shadowRoot.innerHTML = ` + + + `; + } + + this.setupAriaLabelledBy(); + } +} + +customElements.define('demo-input', DemoInput); diff --git a/packages/documentation/src/demo-components/demo-label.ts b/packages/documentation/src/demo-components/demo-label.ts new file mode 100644 index 0000000000..1a03cc20f0 --- /dev/null +++ b/packages/documentation/src/demo-components/demo-label.ts @@ -0,0 +1,31 @@ +export class DemoLabel extends HTMLElement { + static get observedAttributes() { + return ['for']; + } + + private for!: string; + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.render(); + } + + attributeChangedCallback(name: string, _oldValue: string, newValue: string) { + if (name === 'for') this.for = newValue; + + this.render(); + } + + private render() { + if (!this.shadowRoot) return; + this.shadowRoot.innerHTML = ` + + `; + } +} + +customElements.define('demo-label', DemoLabel); diff --git a/packages/documentation/src/demo-components/demo-list-item-group.ts b/packages/documentation/src/demo-components/demo-list-item-group.ts new file mode 100644 index 0000000000..14718735c7 --- /dev/null +++ b/packages/documentation/src/demo-components/demo-list-item-group.ts @@ -0,0 +1,47 @@ +export class DemoListItemGroup extends HTMLElement { + static get observedAttributes() { + return ['list-group-version']; + } + + private listGroupVersion?: number; + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + attributeChangedCallback(name: string, _oldValue: number, newValue: number) { + if (name === 'list-group-version') this.listGroupVersion = newValue; + + this.render(); + } + + connectedCallback() { + this.render(); + } + + private render() { + if (!this.shadowRoot) return; + if (this.listGroupVersion == 1) { + this.shadowRoot.innerHTML = ` + + `; + } else if (this.listGroupVersion == 2) { + this.shadowRoot.innerHTML = ` +
item 1
+
item 2
+
item 3
`; + } else if (this.listGroupVersion == 3) { + this.shadowRoot.innerHTML = ` +
item 1
+
item 2
+
item 3
`; + } else if (this.listGroupVersion == 4) { + this.shadowRoot.innerHTML = ` + + `; + } + } +} + +customElements.define('demo-list-item-group', DemoListItemGroup); diff --git a/packages/documentation/src/demo-components/demo-list-item.ts b/packages/documentation/src/demo-components/demo-list-item.ts new file mode 100644 index 0000000000..ff976ce8c4 --- /dev/null +++ b/packages/documentation/src/demo-components/demo-list-item.ts @@ -0,0 +1,20 @@ +export class DemoListItem extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.render(); + } + + private render() { + if (!this.shadowRoot) return; + this.shadowRoot.innerHTML = ` +
+ +
`; + } +} + +customElements.define('demo-list-item', DemoListItem); diff --git a/packages/documentation/src/demo-components/demo-list.ts b/packages/documentation/src/demo-components/demo-list.ts new file mode 100644 index 0000000000..503d71aa99 --- /dev/null +++ b/packages/documentation/src/demo-components/demo-list.ts @@ -0,0 +1,54 @@ +export class DemoList extends HTMLElement { + static get observedAttributes() { + return ['list-version']; + } + + private listVersion?: number; + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.render(); + } + + attributeChangedCallback(name: string, _oldValue: number, newValue: number) { + if (name === 'list-version') this.listVersion = newValue; + + this.render(); + } + + private render() { + if (!this.shadowRoot) return; + if (this.listVersion == 0) { + this.shadowRoot.innerHTML = ` +
+
+ `; + } else if (this.listVersion == 1) { + this.shadowRoot.innerHTML = ` + + `; + } else if (this.listVersion == 2) { + this.shadowRoot.innerHTML = ` +
+
item 1
+
item 2
+
item 3
+
+ `; + } else if (this.listVersion == 3) { + this.shadowRoot.innerHTML = ` +
+ +
+ `; + } else if (this.listVersion == 4) { + this.shadowRoot.innerHTML = ``; + } + } +} + +customElements.define('demo-list', DemoList); diff --git a/packages/documentation/src/demo-components/demo-span.ts b/packages/documentation/src/demo-components/demo-span.ts new file mode 100644 index 0000000000..9629a62455 --- /dev/null +++ b/packages/documentation/src/demo-components/demo-span.ts @@ -0,0 +1,34 @@ +export class DemoSpan extends HTMLElement { + static get observedAttributes() { + return ['spanId', 'content']; + } + + private spanId!: string; + private content: string = 'My text'; + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.render(); + } + + attributeChangedCallback(name: string, _oldValue: string, newValue: string) { + if (name === 'id') this.spanId = newValue; + if (name === 'content') this.content = newValue; + + this.render(); + } + + private render() { + if (!this.shadowRoot) return; + + this.shadowRoot.innerHTML = ` + ${this.content} + `; + } +} + +customElements.define('demo-span', DemoSpan); diff --git a/packages/documentation/src/demo-components/index.ts b/packages/documentation/src/demo-components/index.ts new file mode 100644 index 0000000000..18a3ef5194 --- /dev/null +++ b/packages/documentation/src/demo-components/index.ts @@ -0,0 +1,7 @@ +import './demo-button'; +import './demo-input'; +import './demo-label'; +import './demo-list'; +import './demo-list-item'; +import './demo-list-item-group'; +import './demo-span'; diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/aria-describedby.docs.mdx b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/aria-describedby.docs.mdx new file mode 100644 index 0000000000..3eab51ad8b --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/aria-describedby.docs.mdx @@ -0,0 +1,171 @@ +import { Meta, Canvas, Controls, Source } from '@storybook/addon-docs/blocks'; +import * as CCR from './aria-describedby.stories'; +import DefaultExample from './examples/defaultExample.sample.html?raw'; +import DefaultExample2 from './examples/defaultExample2.sample.html?raw'; +import AriaDescribedbyElementsPropertyHTML from './examples/ariadescribedbyElementsProperty.sample.html?raw'; +import AriaDescribedbyElementsProperty from './examples/ariadescribedbyElementsProperty.sample.js?raw'; +import NonWorkingExampleIIa from './examples/nonWorkingExample-IIa.sample.html?raw'; +import NonWorkingExampleIIb from './examples/nonWorkingExample-IIb.sample.html?raw'; +import WorkingExampleIIa from './examples/workingExample-IIa.sample.html?raw'; +import WorkingExampleIIb from './examples/workingExample-IIb.sample.html?raw'; +import NonWorkingExampleIII from './examples/nonWorkingExample-III.sample.html?raw'; +import WorkingExampleIII from './examples/workingExample-III.sample.html?raw'; +import NonWorkingExampleIV from './examples/nonWorkingExample-IV.sample.html?raw'; +import WorkingExampleIV from './examples/workingExample-IV.sample.html?raw'; + + + +# aria-describedby + +
+ In the Light DOM, `aria-describedby` is used to give an element a description by referencing the + ID of another element or multiple elements that contain the desired text. +
+

+ This is useful when a simple text label isn't enough or when the description needs to be combined + from different pieces of text. +

+ +### I. Referencing Within the Same DOM Tree + +• Both elements in Light DOM ✔️ + + + +• Both elements in same Shadow DOM ✔️ + + +

+ + The above relationships are valid because in both cases the target element and its description + are part of the same tree. + +

+ +

+ Below is a working example of an `aria-describedby`/`id` relationship within the shadowDom of a + custom button component. You can test it with NVDA to confirm the correct description + announcement. +

+ + + +### II. Referencing From Outside a Shadow DOM Into That Shadow DOM + +• Light DOM → Shadow DOM ❌ + + + +• Slotted Content → Shadow DOM ❌ + + + +

+ The above examples do not work across shadow DOM boundaries when setting the aria-describedby + attribute directly. +

+

+ + Attribute element references can only point to elements in the same DOM or shadow DOM, because + IDs are only valid within their own scope. + +

+ +#### Workaround: `Element:ariaDescribedByElements` + +

+ `ariaDescribedByElements` is a property of the Element interface that returns an array of elements + describing the element. It provides direct references to these elements rather than just their ID + strings. + + 🔗ariaDescribedByElements + +

+ +
Basic usage of `ariaDescribedByElements`
+ +

+ Instead of adding the `aria-describedby` attribute on the element, we can set it using the + `ariaDescribedByElements` property. +

+ +

+ The following examples demonstrate how the `ariaDescribedByElements` property can be used to + establish relationships across the shadow DOM. +

+ +

+ • Light DOM → Shadow DOM Example ✔️ +

+

+ In this case a description element in the light DOM is directly assigned to a target element inside a + custom component’s shadow DOM using `ariaDescribedByElements`. +

+ + +

Use the control below to toggle the `ariaDescribedByElements` workaround implementation.

+ +
+ +
+ +

+ • Slotted Content → Shadow DOM Example ✔️ +

+

+ In this case a description element provided via a light DOM slot is automatically assigned to a target + element inside a custom component’s shadow DOM using `ariaDescribedByElements`. +

+ +

Use the control below to toggle the `ariaDescribedByElements` workaround implementation.

+ +
+ +
+ +⚠️ In the above scenarios, the relationship is correctly announced by NVDA, passes accessibility checks on Axe, WAVE and NVDA but partially fails on Siteimprove and Accessibility Insights tools. + +### III. Referencing From Inside a Shadow DOM Out to the Light DOM + +

+ • Shadow DOM → Light DOM (or Shadow DOM → Slotted Content)❌ +

+ +

+ Referencing an element in the light DOM from within a shadow DOM using the standard + `aria-describedby` is not possible. The `ariaDescribedByElements` property neither works in this + case because, for it to work, the description element must be in the same DOM or in a parent DOM of the + target element. +

+ + +##### Working Solution: Set the `id`on the description component host + +

+ The only option here is to set the `id` directly on the host of the description component. In + this case all the visible text inside this component will be assigned as the Accessible Description of the target element. +

+ + + +### IV. Referencing From One Shadow DOM To Another Shadow DOM + +Shadow DOM → Other Shadow DOM ❌ + + +

+ This relationship is not valid because both `id` and `aria-describedby` attributes are scoped only within + their individual shadowDoms. +

+ +##### Working Solution + +A working solution for this case combines approaches II and III: setting the description component’s `id` directly on the host element (III), and using `Element.ariaDescribedByElements` to assign the description to the target component. + + + + +⚠️ In the above scenario, the relationship is correctly announced by NVDA, passes accessibility checks on Axe, WAVE and NVDA but partially fails on Siteimprove and Accessibility Insights tools. diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/aria-describedby.stories.ts b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/aria-describedby.stories.ts new file mode 100644 index 0000000000..b2b2f2664b --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/aria-describedby.stories.ts @@ -0,0 +1,96 @@ +import { StoryObj, Args } from '@storybook/web-components-vite'; +import { MetaExtended } from '@root/types'; +import { html } from 'lit'; +import './aria-describedby.styles.scss'; + +const meta: MetaExtended = { + id: '76ade552-2c03-4d6d-9dce-28daa3405910', + title: + 'Accessibility Practices/Foundational Structure And Semantics/Reference Crossing The Shadowdom/aria-describedby', + parameters: { + badges: [], + }, + + decorators: [story => html`
${story()}
`], +}; + +export default meta; + +type Story = StoryObj; + +// Case: Standard Light DOM to Light DOM +export const ExampleHTML: Story = { + render: () => html` +
+ +
+ My Description + `, +}; + +// Case: Referencing from Shadow DOM (Host Attribute) to Light DOM (Element) workaround setting programmatically the relevant Element property +export const Example2: Story = { + argTypes: { + workaround: { + name: 'Workaround', + control: { + type: 'radio', + }, + options: ['none', 'ariaDescribedByElements'], + }, + }, + args: { + workaround: 'none', + }, + render: (args: Args) => html` + + My Description + `, +}; + +// Case: Referencing from Shadow DOM (Host Attribute) to Slotted Content (Element) workaround setting programmatically the relevant Element property +export const Example3: Story = { + argTypes: { + workaround: { + name: 'Workaround', + control: { + type: 'radio', + }, + options: ['none', 'ariaDescribedByElements'], + }, + }, + args: { + workaround: 'none', + }, + render: (args: Args) => html` + My Description + `, +}; + +// Case: Standard Light DOM to Shadow DOM workaround +export const Example4: Story = { + render: () => html` +
+ +
+ + `, +}; + +// Case: Shadow DOM to other Shadow Dom workaround +export const Example5: Story = { + render: () => html` + + + `, +}; diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/aria-describedby.styles.scss b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/aria-describedby.styles.scss new file mode 100644 index 0000000000..bf859e8590 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/aria-describedby.styles.scss @@ -0,0 +1,22 @@ +::part(button) { + display: inline-flex; + position: relative; + align-items: center; + justify-content: center; + max-width: 100%; + overflow: hidden; + transition: opacity 250ms, border-color 250ms, background-color 250ms, color 250ms; + border-width: 2px; + border-style: solid; + border-radius: 100px; + font-weight: bold; + width: 48px; + min-height: 40px; + color: white; + background: black; + + &:hover { + background-color: #504f4b; + border-color: #504f4b; + } +} diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/ariadescribedbyElementsProperty.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/ariadescribedbyElementsProperty.sample.html new file mode 100644 index 0000000000..64a1f8f77e --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/ariadescribedbyElementsProperty.sample.html @@ -0,0 +1,3 @@ + + + diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/ariadescribedbyElementsProperty.sample.js b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/ariadescribedbyElementsProperty.sample.js new file mode 100644 index 0000000000..ca1650819a --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/ariadescribedbyElementsProperty.sample.js @@ -0,0 +1,8 @@ +// Get the element that will serve as the description +const description = document.getElementById('descriptionId'); + +// Get the element that needs an accessible description +const element = document.getElementById('elementId'); + +// Assign one or more description elements directly to the target element `ariaDescribedByElements` property +element.ariaDescribedByElements = [description]; diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/defaultExample.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/defaultExample.sample.html new file mode 100644 index 0000000000..8c6f22e30b --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/defaultExample.sample.html @@ -0,0 +1,5 @@ +
+ +
+ +My Description diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/defaultExample2.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/defaultExample2.sample.html new file mode 100644 index 0000000000..0ce4ef870d --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/defaultExample2.sample.html @@ -0,0 +1,7 @@ + + ⏷ #shadow-root (open) +
+ +
+ My Description +
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/nonWorkingExample-III.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/nonWorkingExample-III.sample.html new file mode 100644 index 0000000000..8518f0469a --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/nonWorkingExample-III.sample.html @@ -0,0 +1,8 @@ + + ⏷ #shadow-root (open) + My Description + + +
+ +
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/nonWorkingExample-IIa.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/nonWorkingExample-IIa.sample.html new file mode 100644 index 0000000000..d5d65462ca --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/nonWorkingExample-IIa.sample.html @@ -0,0 +1,8 @@ + + ⏷ #shadow-root (open) +
+ +
+
+ +My Description diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/nonWorkingExample-IIb.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/nonWorkingExample-IIb.sample.html new file mode 100644 index 0000000000..7cb30ae36a --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/nonWorkingExample-IIb.sample.html @@ -0,0 +1,8 @@ + + ⏷ #shadow-root (open) +
+ +
+ + My Description +
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/nonWorkingExample-IV.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/nonWorkingExample-IV.sample.html new file mode 100644 index 0000000000..107d099356 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/nonWorkingExample-IV.sample.html @@ -0,0 +1,12 @@ + + ⏷ #shadow-root (open) +
+ +
+
+ + + ⏷ #shadow-root (open) + My Description + + diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workaround-IIa.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workaround-IIa.sample.html new file mode 100644 index 0000000000..e5c60ad61e --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workaround-IIa.sample.html @@ -0,0 +1,9 @@ + + ⏷ #shadow-root (open) +
+ +
+
+ +My Description + diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workaround-IIb.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workaround-IIb.sample.html new file mode 100644 index 0000000000..d5d65462ca --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workaround-IIb.sample.html @@ -0,0 +1,8 @@ + + ⏷ #shadow-root (open) +
+ +
+
+ +My Description diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workingExample-III.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workingExample-III.sample.html new file mode 100644 index 0000000000..14d2527003 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workingExample-III.sample.html @@ -0,0 +1,8 @@ +
+ +
+ + + ⏷ #shadow-root (open) + My Description + diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workingExample-IIa.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workingExample-IIa.sample.html new file mode 100644 index 0000000000..aefe9337c5 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workingExample-IIa.sample.html @@ -0,0 +1,16 @@ +My Description + + + ⏷ #shadow-root (open) +
+ + +
+
+ + diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workingExample-IIb.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workingExample-IIb.sample.html new file mode 100644 index 0000000000..a3caff1abb --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workingExample-IIb.sample.html @@ -0,0 +1,19 @@ + + ⏷ #shadow-root (open) + +
+ + +
+ + My Description +
+ + diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workingExample-IV.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workingExample-IV.sample.html new file mode 100644 index 0000000000..80c0a1b3e6 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-describedby/examples/workingExample-IV.sample.html @@ -0,0 +1,19 @@ + + ⏷ #shadow-root (open) +
+ +
+
+ + + ⏷ #shadow-root (open) + My Description + + + + diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/aria-labelledby.docs.mdx b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/aria-labelledby.docs.mdx new file mode 100644 index 0000000000..d4eaaead45 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/aria-labelledby.docs.mdx @@ -0,0 +1,173 @@ +import { Meta, Canvas, Controls, Source } from '@storybook/addon-docs/blocks'; +import * as CCR from './aria-labelledby.stories'; +import DefaultExample from './examples/defaultExample.sample.html?raw'; +import DefaultExample2 from './examples/defaultExample2.sample.html?raw'; +import ArialabelledbyElementsPropertyHTML from './examples/arialabelledbyElementsProperty.sample.html?raw'; +import ArialabelledbyElementsProperty from './examples/arialabelledbyElementsProperty.sample.js?raw'; +import NonWorkingExampleIIa from './examples/nonWorkingExample-IIa.sample.html?raw'; +import NonWorkingExampleIIb from './examples/nonWorkingExample-IIb.sample.html?raw'; +import WorkingExampleIIa from './examples/workingExample-IIa.sample.html?raw'; +import WorkingExampleIIb from './examples/workingExample-IIb.sample.html?raw'; +import NonWorkingExampleIII from './examples/nonWorkingExample-III.sample.html?raw'; +import WorkingExampleIII from './examples/workingExample-III.sample.html?raw'; +import NonWorkingExampleIV from './examples/nonWorkingExample-IV.sample.html?raw'; +import WorkingExampleIV from './examples/workingExample-IV.sample.html?raw'; + + + +# aria-labelledby + +
+ In the Light DOM, the `aria-labelledby` attribute assigns an accessible name to an element by + referencing the ID of one or more elements that provide the descriptive text. +
+

+ This is particularly useful when a simple text label isn’t directly linked to the element, or when + the accessible name needs to be composed from multiple pieces of text. +

+### I. Referencing Within the Same DOM Tree + +• Both elements in Light DOM ✔️ + + + +• Both elements in same Shadow DOM ✔️ + + +

+ + The above relationships are valid because in both cases the label and its associated target + element are part of the same tree. + +

+ +

+ Below is a working example of an `aria-labelledby`/`id` relationship within the shadowDom of a + custom button component. You can test it with NVDA to confirm the correct label + announcement. +

+ + + +### II. Referencing From Outside a Shadow DOM Into That Shadow DOM + +• Light DOM → Shadow DOM ❌ + + + +• Slotted Content → Shadow DOM ❌ + + + +

+ The above examples do not work across shadow DOM boundaries when setting the aria-labelledby + attribute directly. +

+

+ + Attribute element references can only point to elements in the same DOM or shadow DOM, because + IDs are only valid within their own scope. + +

+ +#### Workaround: `Element:ariaLabelledByElements` + +

+ `ariaLabelledByElements` is a property of the Element interface that returns an array of elements + labeling the element. It provides direct references to these elements rather than just their ID + strings. + + 🔗ariaLabelledByElements + +

+ +
Basic usage of `ariaLabelledByElements`
+ +

+ Instead of adding the `aria-labelledby` attribute on the element, we can set it using the + `ariaLabelledByElements` property. +

+ +

+ The following examples demonstrate how the `ariaLabelledByElements` property can be used to + establish relationships across the shadow DOM. +

+ +

+ • Light DOM → Shadow DOM Example ✔️ +

+

+ In this case a label element in the light DOM is directly assigned to a target element inside a + custom component’s shadow DOM using `ariaLabelledByElements`. +

+ + +

Use the control below to toggle the `ariaLabelledByElements` workaround implementation.

+ +
+ +
+ +

+ • Slotted Content → Shadow DOM Example ✔️ +

+

+ In this case a label element provided via a light DOM slot is automatically assigned to a target + element inside a custom component’s shadow DOM using `ariaLabelledByElements`. +

+ +

Use the control below to toggle the `ariaLabelledByElements` workaround implementation.

+ +
+ +
+ +⚠️ In the above scenarios, the relationship is correctly announced by NVDA, passes accessibility checks on Axe, WAVE and NVDA but partially fails on Siteimprove and Accessibility Insights tools. + +### III. Referencing From Inside a Shadow DOM Out to the Light DOM + +

+ • Shadow DOM → Light DOM (or Shadow DOM → Slotted Content)❌ +

+ +

+ Referencing an element in the light DOM from within a shadow DOM using the standard + `aria-labelledby` is not possible. The `ariaLabelledByElements` property neither works in this + case because, for it to work, the label element must be in the same DOM or in a parent DOM of the + target element. +

+ + +##### Working Solution: Set the `id`on the label component host + +

+ The only option here is to set the `id` directly on the host of the labeling component. In + this case{' '} + + all the visible text inside the referencing component will be assigned as the Accessible Name + {' '} + of the target element. +

+ + + +### IV. Referencing From One Shadow DOM To Another Shadow DOM + +Shadow DOM → Other Shadow DOM ❌ + + +

+ This relationship is not valid because both `id` and `aria-labelledby` attributes are scoped only within + their individual shadowDoms. +

+##### Working Solution + +A working solution for this case combines approaches II and III: setting the label component’s `id` directly on the host element (III), and using `Element.ariaLabelledByElements` to assign the label to the target component. + + + + +⚠️ In the above scenario, the relationship is correctly announced by NVDA, passes accessibility checks on Axe, WAVE and NVDA but partially fails on Siteimprove and Accessibility Insights tools. diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/aria-labelledby.stories.ts b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/aria-labelledby.stories.ts new file mode 100644 index 0000000000..0cace5fc4d --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/aria-labelledby.stories.ts @@ -0,0 +1,92 @@ +import { StoryObj, Args } from '@storybook/web-components-vite'; +import { MetaExtended } from '@root/types'; +import { html } from 'lit'; +import './aria-labelledby.styles.scss'; + +const meta: MetaExtended = { + id: '76ade552-2c03-4d6d-9dce-28daa3405678', + title: + 'Accessibility Practices/Foundational Structure And Semantics/Reference Crossing The Shadowdom/aria-labelledby', + parameters: { + badges: [], + }, + + decorators: [story => html`
${story()}
`], +}; + +export default meta; + +type Story = StoryObj; + +// Case: Standard Light DOM to Light DOM +export const ExampleHTML: Story = { + render: () => html` `, +}; + +// Case: Referencing from Shadow DOM (Host Attribute) to Light DOM (Element) workaround setting programmatically the relevant Element property +export const Example2: Story = { + argTypes: { + workaround: { + name: 'Workaround', + description: 'Toggles the workaround solution', + control: { + type: 'radio', + }, + options: ['none', 'ariaLabelledByElements'], + }, + }, + args: { + workaround: 'none', + }, + render: (args: Args) => html` + My Text + + `, +}; + +// Case: Referencing from Shadow DOM (Host Attribute) to Slotted Content (Element) workaround setting programmatically the relevant Element property +export const Example3: Story = { + argTypes: { + workaround: { + name: 'Workaround', + control: { + type: 'radio', + }, + options: ['none', 'ariaLabelledByElements'], + }, + }, + args: { + workaround: 'none', + }, + render: (args: Args) => html` + My Text + `, +}; + +// Case: Standard Light DOM to Shadow DOM workaround +export const Example4: Story = { + render: () => html` + My Text +
+ +
+ `, +}; + +// Case: Shadow DOM to other Shadow Dom workaround +export const Example5: Story = { + render: () => html` + My Text + + `, +}; diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/aria-labelledby.styles.scss b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/aria-labelledby.styles.scss new file mode 100644 index 0000000000..bf859e8590 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/aria-labelledby.styles.scss @@ -0,0 +1,22 @@ +::part(button) { + display: inline-flex; + position: relative; + align-items: center; + justify-content: center; + max-width: 100%; + overflow: hidden; + transition: opacity 250ms, border-color 250ms, background-color 250ms, color 250ms; + border-width: 2px; + border-style: solid; + border-radius: 100px; + font-weight: bold; + width: 48px; + min-height: 40px; + color: white; + background: black; + + &:hover { + background-color: #504f4b; + border-color: #504f4b; + } +} diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/arialabelledbyElementsProperty.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/arialabelledbyElementsProperty.sample.html new file mode 100644 index 0000000000..7160a12432 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/arialabelledbyElementsProperty.sample.html @@ -0,0 +1,3 @@ + + + diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/arialabelledbyElementsProperty.sample.js b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/arialabelledbyElementsProperty.sample.js new file mode 100644 index 0000000000..38a1bf7836 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/arialabelledbyElementsProperty.sample.js @@ -0,0 +1,8 @@ +// Get the element that will serve as the label +const label = document.getElementById('labelId'); + +// Get the element that needs an accessible name +const element = document.getElementById('elementId'); + +// Assign one or more label elements directly to the target element `ariaLabelledByElements` property +element.ariaLabelledByElements = [label]; diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/defaultExample.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/defaultExample.sample.html new file mode 100644 index 0000000000..0a309addfa --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/defaultExample.sample.html @@ -0,0 +1,5 @@ +My Text + +
+ +
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/defaultExample2.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/defaultExample2.sample.html new file mode 100644 index 0000000000..b465534e19 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/defaultExample2.sample.html @@ -0,0 +1,7 @@ + + ⏷ #shadow-root (open) + My Text +
+ +
+
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/nonWorkingExample-III.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/nonWorkingExample-III.sample.html new file mode 100644 index 0000000000..0ad5d47931 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/nonWorkingExample-III.sample.html @@ -0,0 +1,8 @@ + + ⏷ #shadow-root (open) + My Text + + +
+ +
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/nonWorkingExample-IIa.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/nonWorkingExample-IIa.sample.html new file mode 100644 index 0000000000..388de8933f --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/nonWorkingExample-IIa.sample.html @@ -0,0 +1,8 @@ +My Text + + + ⏷ #shadow-root (open) +
+ +
+
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/nonWorkingExample-IIb.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/nonWorkingExample-IIb.sample.html new file mode 100644 index 0000000000..997d9faf11 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/nonWorkingExample-IIb.sample.html @@ -0,0 +1,8 @@ + + ⏷ #shadow-root (open) + +
+ +
+ My Text +
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/nonWorkingExample-IV.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/nonWorkingExample-IV.sample.html new file mode 100644 index 0000000000..05b27ce5bf --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/nonWorkingExample-IV.sample.html @@ -0,0 +1,11 @@ + + ⏷ #shadow-root (open) + My Text + + + + ⏷ #shadow-root (open) +
+ +
+
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workaround-IIa.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workaround-IIa.sample.html new file mode 100644 index 0000000000..388de8933f --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workaround-IIa.sample.html @@ -0,0 +1,8 @@ +My Text + + + ⏷ #shadow-root (open) +
+ +
+
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workaround-IIb.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workaround-IIb.sample.html new file mode 100644 index 0000000000..388de8933f --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workaround-IIb.sample.html @@ -0,0 +1,8 @@ +My Text + + + ⏷ #shadow-root (open) +
+ +
+
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workingExample-III.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workingExample-III.sample.html new file mode 100644 index 0000000000..429fdc2626 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workingExample-III.sample.html @@ -0,0 +1,8 @@ + + ⏷ #shadow-root (open) + My Text + + +
+ +
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workingExample-IIa.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workingExample-IIa.sample.html new file mode 100644 index 0000000000..9f2d63843c --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workingExample-IIa.sample.html @@ -0,0 +1,16 @@ +My Text + + + ⏷ #shadow-root (open) +
+ + +
+
+ + diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workingExample-IIb.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workingExample-IIb.sample.html new file mode 100644 index 0000000000..2ae81bd671 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workingExample-IIb.sample.html @@ -0,0 +1,19 @@ + + ⏷ #shadow-root (open) + +
+ + +
+ + My Text +
+ + diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workingExample-IV.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workingExample-IV.sample.html new file mode 100644 index 0000000000..e2c1013216 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-labelledby/examples/workingExample-IV.sample.html @@ -0,0 +1,18 @@ + + ⏷ #shadow-root (open) + My Text + + + + ⏷ #shadow-root (open) +
+ +
+
+ + diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/aria-role-list.docs.mdx b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/aria-role-list.docs.mdx new file mode 100644 index 0000000000..a746187144 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/aria-role-list.docs.mdx @@ -0,0 +1,137 @@ +import { Meta, Canvas, Source } from '@storybook/addon-docs/blocks'; +import * as CCR from './aria-role-list.stories'; + +import DefaultExample from './examples/defaultExample.sample.html?raw'; +import DefaultExample2 from './examples/defaultExample2.sample.html?raw'; +import DefaultExample3 from './examples/defaultExample3.sample.html?raw'; +import WorkingExampleIIa from './examples/workingExample-IIa.sample.html?raw'; +import WorkingExampleIIb from './examples/workingExample-IIb.sample.html?raw'; +import WorkingExampleIIIa from './examples/workingExample-IIIa.sample.html?raw'; +import WorkingExampleIIIb from './examples/workingExample-IIIb.sample.html?raw'; +import WorkingExampleIVa from './examples/workingExample-IVa.sample.html?raw'; +import WorkingExampleIVb from './examples/workingExample-IVb.sample.html?raw'; +import WorkingExampleIVc from './examples/workingExample-IVc.sample.html?raw'; + + + +# role="list" + +
+ The `role="list"` tells assistive technologies that an element represents a list, which is + expected to contain related items identified with `role="listitem"`. +
+ +### I. Referencing Within the Same DOM Tree + +• Both `list` parent and `listitem` children in Light DOM ✔️ + + + + + +

+ This represents the default structure, where a `role="list"` element contains direct children with + `role="listitem"`. +

+ +• Both `list` parent and `listitem` children in the same Shadow DOM ✔️ + + + + + +

+ Same as above, but contained within a custom component’s Shadow DOM. The `role="list"` still has + direct `role="listitem"` children, so the structure remains valid. +

+ +• `list` role on the host and slotted `listitem` children ✔️ + + + + + +

+ Similarly, in the case of a component with `role="list"` set on the host and slotted `listitems`, the + relationship is preserved correctly. +

+ +### II. Parent in the Light DOM-children Into The Shadow DOM + +• Light DOM → Shadow DOM ✔️ + + + + + +

+ In the case a component with `role="list"` in the host with `listitem` elements within its Shadow + DOM, the relationship is valid. +

+ +• Slotted Parent → Shadow DOM ❌ + + + +

+ ⚠️ The above structure does not comply with accessibility standards, as `listitems` must be + children of an element with `role="list"`. The relationship fails not because of the Shadow DOM + boundary, but due to the incorrect structure itself. +

+ +### III. Parent in the Shadow DOM - Children Out to the Light DOM + +• Parent in the Shadow DOM → Children in the Light DOM ❌ + + + +

+ ⚠️ The above structure does not comply with accessibility standards, as `listitems` must be + children of an element with `role="list"`. The relationship fails not because of the Shadow DOM + boundary, but due to the incorrect structure itself. +

+ +• Parent in the Shadow DOM → Slotted Children ✔️ + + + + + +

+ When a component applies `role="list"` to an internal element and its children with `role="listitem"` are slotted into that element, the structure still works correctly and preserves a valid `list/listitem` relationship. +

+ +### IV. Referencing From One Shadow DOM To Another Shadow shadowDOM + +• Parent in the Shadow DOM → Children grouped in a nested Component ✔️ + + + + + +

+ This structure works correctly: the parent `role="list"` maintains a valid relationship with its children, even when the `listitem` elements are grouped inside a nested component. +

+ +• Parent in the Shadow DOM → slotted Children as individual Components ✔️ + + + + +

+ In this case the `listitem` elements are slotted and encapsulated within individual custom components. This structure also forms a valid `list-listitem` relationship. +

+ +• Parent in the Shadow DOM → nested Grouping Component -> nested Children individual components ✔️ + + + + + +

+ In the case of a `role="list"` parent containing a grouping component, with individual `listitem` children nested inside their own components, the relationship is still correctly preserved across two Shadow DOM boundaries. +

+ + +## Conclusion: Shadow DOM and List Semantics +

When the markup is properly structured such that elements with `role="listitem"` exist within a parent element having `role="list"`, the presence of Shadow DOM does not break the semantic `list/listitem` relationship—even across multiple component boundaries. The `listitems` do not necessarily have to be direct children of the `role="list"` element for the relationship to be preserved, as the accessibility tree will bridge through the encapsulation when the markup is correct.

diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/aria-role-list.stories.ts b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/aria-role-list.stories.ts new file mode 100644 index 0000000000..7fd94c044c --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/aria-role-list.stories.ts @@ -0,0 +1,111 @@ +import { StoryObj } from '@storybook/web-components-vite'; +import { MetaExtended } from '@root/types'; +import { html } from 'lit'; + +const meta: MetaExtended = { + id: '76ade552-2c03-4d6d-9dce-28daa3405112', + title: + 'Accessibility Practices/Foundational Structure And Semantics/Reference Crossing The Shadowdom/aria-role-list', + parameters: { + badges: [], + }, + + decorators: [story => html`
${story()}
`], +}; + +export default meta; + +type Story = StoryObj; + +// Case: Standard Light DOM to Light DOM +export const ExampleHTML: Story = { + render: () => html` +
+
item 1
+
item 2
+
item 3
+
+ `, +}; + +// Case: Light Dom to child components with Slotted content +export const Example1a: Story = { + render: () => html` +
item 1
+
item 2
+
item 3
+
`, +}; + +// Case: Parent in the Light - children with Slotted content same component +export const Example1b: Story = { + render: () => html` + +
item 1
+
item 2
+
item 3
+
+ `, +}; + +// Case: Parent in the Light (Component host) -> Children in the ShadowDOM +export const Example2a: Story = { + render: () => + html` + + `, +}; + +// Case: Parent in Light (slotted content) - Children in the ShadowDOM +export const Example2b: Story = { + render: () => + html` +
+
`, +}; + +// Case: Parent in the Shadow - Children in the Light +export const Example3a: Story = { + render: () => html` + +
item 1
+
item 2
+
item 3
+ `, +}; + +// Case: Parent in the Shadow - Children Slotted +export const Example3b: Story = { + render: () => html` + +
item 1
+
item 2
+
item 3
+
+ `, +}; + +// Case: Referencing from Shadow DOM to embedded Shadow DOM +export const Example4a: Story = { + render: () => html` `, +}; + +// Case: Referencing from Shadow DOM with slotted children in different embedded shadow DOMs +export const Example4b: Story = { + render: () => html` + item 1 + item 2 + item 3 + `, +}; + +// Case: Parent in the Shadow > custom-component > childrent components +export const Example4c: Story = { + render: () => html` + + item 1 + item 2 + item 3 + + `, +}; diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/defaultExample.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/defaultExample.sample.html new file mode 100644 index 0000000000..3d98ef8e29 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/defaultExample.sample.html @@ -0,0 +1,5 @@ +
+
item 1
+
item 2
+
item 3
+
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/defaultExample2.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/defaultExample2.sample.html new file mode 100644 index 0000000000..4ab8ba42ec --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/defaultExample2.sample.html @@ -0,0 +1,8 @@ + + ⏷ #shadow-root (open) +
+
item 1
+
item 2
+
item 3
+
+
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/defaultExample3.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/defaultExample3.sample.html new file mode 100644 index 0000000000..a0ae2e60b8 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/defaultExample3.sample.html @@ -0,0 +1,7 @@ + + ⏷ #shadow-root (open) + +
item 1
+
item 2
+
item 3
+
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IIIa.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IIIa.sample.html new file mode 100644 index 0000000000..670820c5bf --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IIIa.sample.html @@ -0,0 +1,7 @@ + + ⏷ #shadow-root (open) +
test
+
+
item 1
+
item 2
+
item 3
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IIIb.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IIIb.sample.html new file mode 100644 index 0000000000..be5ccd5d35 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IIIb.sample.html @@ -0,0 +1,13 @@ + + ⏷ #shadow-root (open) +
+ +
+ + +
item 1
+
item 2
+
item 3
+
+ + diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IIa.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IIa.sample.html new file mode 100644 index 0000000000..8e9e8883d5 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IIa.sample.html @@ -0,0 +1,6 @@ + + ⏷ #shadow-root (open) +
item 1
+
item 2
+
item 3
+
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IIb.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IIb.sample.html new file mode 100644 index 0000000000..9e95d4a2ac --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IIb.sample.html @@ -0,0 +1,10 @@ + + ⏷ #shadow-root (open) + +
item 1
+
item 2
+
item 3
+ + +
+
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IVa.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IVa.sample.html new file mode 100644 index 0000000000..937da1cef0 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IVa.sample.html @@ -0,0 +1,11 @@ + + ⏷ #shadow-root (open) +
+ + ⏷ #shadow-root (open) +
item 1
+
item 2
+
item 3
+
+
+
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IVb.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IVb.sample.html new file mode 100644 index 0000000000..a565b687b5 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IVb.sample.html @@ -0,0 +1,20 @@ + + ⏷ #shadow-root (open) +
+ +
+ + + + ⏷ #shadow-root (open) +
item 1
+
+ + ⏷ #shadow-root (open) +
item 2
+
+ + ⏷ #shadow-root (open) +
item 3
+
+
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IVc.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IVc.sample.html new file mode 100644 index 0000000000..d2950b875f --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/aria-role-list/examples/workingExample-IVc.sample.html @@ -0,0 +1,20 @@ + + ⏷ #shadow-root (open) +
+ + ⏷ #shadow-root (open) + + ⏷ #shadow-root (open) +
item 1
+
+ + ⏷ #shadow-root (open) +
item 2
+
+ + ⏷ #shadow-root (open) +
item 3
+
+
+
+
diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/for/examples/arialabelledbyElementsProperty.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/for/examples/arialabelledbyElementsProperty.sample.html new file mode 100644 index 0000000000..b061f1763b --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/for/examples/arialabelledbyElementsProperty.sample.html @@ -0,0 +1,3 @@ + + + diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/for/examples/arialabelledbyElementsProperty.sample.js b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/for/examples/arialabelledbyElementsProperty.sample.js new file mode 100644 index 0000000000..bb66e1f7a4 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/for/examples/arialabelledbyElementsProperty.sample.js @@ -0,0 +1,8 @@ +// Get the element that will serve as the label +const label = document.getElementById('labelId'); + +// Get the input that needs a label +const input = document.getElementById('inputId'); + +// Assign one or more label elements directly to the target element `ariaLabelledByElements` property +input.ariaLabelledByElements = [label]; diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/for/examples/defaultExample.sample.html b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/for/examples/defaultExample.sample.html new file mode 100644 index 0000000000..d73fcbd078 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/for/examples/defaultExample.sample.html @@ -0,0 +1,3 @@ +