From 65bb519f2fedc1735b793a26baec4c06890f914f Mon Sep 17 00:00:00 2001 From: Myrta Sakellariou Date: Mon, 18 Aug 2025 10:44:34 +0200 Subject: [PATCH 01/29] add for-label story and demo target component --- packages/documentation/.storybook/preview.ts | 2 + .../src/demo-components/demo-button.ts | 43 +++++++ .../src/demo-components/demo-target.ts | 81 ++++++++++++ .../src/demo-components/index.ts | 2 + .../for/for.docs.mdx | 62 +++++++++ .../for/for.stories.ts | 120 ++++++++++++++++++ .../documentation/types/demo-components.ts | 8 ++ packages/documentation/types/index.ts | 1 + 8 files changed, 319 insertions(+) create mode 100644 packages/documentation/src/demo-components/demo-button.ts create mode 100644 packages/documentation/src/demo-components/demo-target.ts create mode 100644 packages/documentation/src/demo-components/index.ts create mode 100644 packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/for/for.docs.mdx create mode 100644 packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/for/for.stories.ts create mode 100644 packages/documentation/types/demo-components.ts 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..cf7bb9c731 --- /dev/null +++ b/packages/documentation/src/demo-components/demo-button.ts @@ -0,0 +1,43 @@ +export class DemoButton extends HTMLElement { + static get observedAttributes() { + return ['aria-labelledby-id', 'aria-describedby-id']; + } + + private ariaLabelledbyId?: string; + private ariaDescribedbyId?: string; + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { + this.render(); + } + + attributeChangedCallback(name: string, _oldValue: string, newValue: string) { + if (name === 'aria-labelledby-id') this.ariaLabelledbyId = newValue; + if (name === 'aria-describedby-id') this.ariaDescribedbyId = newValue; + this.render(); + } + + private render() { + if (!this.shadowRoot) return; + this.shadowRoot.innerHTML = ` +
+ +
+ +
+
+ `; + } +} + +customElements.define('demo-button', DemoButton); diff --git a/packages/documentation/src/demo-components/demo-target.ts b/packages/documentation/src/demo-components/demo-target.ts new file mode 100644 index 0000000000..cd89d7cdff --- /dev/null +++ b/packages/documentation/src/demo-components/demo-target.ts @@ -0,0 +1,81 @@ +export class DemoTarget extends HTMLElement { + static get observedAttributes() { + return ['workaround', 'aria-labelledby-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 === 'aria-labelledby-id') this.ariaLabelledbyId = newValue; + if (name === 'target-version') this.targetVersion = newValue as '1' | '2' | '3'; + + this.render(); + } + + private setupAriaLabelledBy() { + if (!this.internalEl) return; + + if (this.targetVersion === '1') { + const labelEl = document.querySelector(`[for="${this.ariaLabelledbyId}"]`); + this.internalEl.ariaLabelledByElements = + this.workaround === 'ariaLabelledByElements' && labelEl ? [labelEl] : []; + } else if (this.targetVersion === '2') { + if (this.slotEl && this.workaround === 'ariaLabelledByElements') { + const assignedElements = this.slotEl.assignedElements({ flatten: true }); + const labelElement = assignedElements.find(el => el.tagName === 'LABEL'); + this.internalEl.ariaLabelledByElements = labelElement ? [labelElement] : []; + } else { + this.internalEl.ariaLabelledByElements = []; + } + } else if (this.targetVersion === '3') { + // Target3 doesn't have an input; just make div focusable + // this.setAttribute('tabindex', '0'); + // this.setAttribute('role', 'textbox'); + } + } + + private render() { + if (!this.shadowRoot) return; + + if (this.targetVersion === '2') { + this.shadowRoot.innerHTML = ` + + + `; + this.slotEl = this.shadowRoot.querySelector('slot[name="label-slot"]') as HTMLSlotElement; + this.internalEl = this.shadowRoot.querySelector('#internal') as HTMLElement; + } else if (this.targetVersion === '3') { + this.shadowRoot.innerHTML = ` + + + + `; + this.internalEl = this.shadowRoot.querySelector('#internal') as HTMLElement; + } else { + // default to target1 + this.shadowRoot.innerHTML = ` + + + `; + this.internalEl = this.shadowRoot.querySelector('#internal') as HTMLElement; + } + + this.setupAriaLabelledBy(); + } +} + +customElements.define('demo-target', DemoTarget); diff --git a/packages/documentation/src/demo-components/index.ts b/packages/documentation/src/demo-components/index.ts new file mode 100644 index 0000000000..b104b2ee97 --- /dev/null +++ b/packages/documentation/src/demo-components/index.ts @@ -0,0 +1,2 @@ +import './demo-button'; +import './demo-target'; diff --git a/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/for/for.docs.mdx b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/for/for.docs.mdx new file mode 100644 index 0000000000..33be19dea3 --- /dev/null +++ b/packages/documentation/src/stories/accessibility-practices/foundational-structure-and-semantics/reference-crossing-the-shadowdom/for/for.docs.mdx @@ -0,0 +1,62 @@ +import { Meta, Canvas, Source, Controls } from '@storybook/addon-docs/blocks'; +import * as CCR from './for.stories'; + + + +# for + +## + +
The native HTML `for`/`id` relationship is a fundamental way to connect a `