From d343ed07a630e8d8a340b59b62d3104ac32ba99a Mon Sep 17 00:00:00 2001 From: Rajdeep Chandra Date: Sun, 17 May 2026 16:40:35 +0530 Subject: [PATCH 1/3] chore: add PoC for combobox as form-associated custom element - self-contained FACE combobox demo exercising ElementInternals for form value, validation, keyboard, and a11y patterns - documents aria-activedescendant, aria-controls, and popup ownership behavior within shadow DOM boundaries - includes IDREF matrix (light DOM, slotted, shadow, cross-root) with browser pass/fail findings matching the text-field PoC - adds QA tree inspection results confirming role, name, value, and expanded/collapsed states render correctly in the a11y tree Co-authored-by: Cursor --- .../SUMMARY.md | 493 +++++ .../index.html | 1944 +++++++++++++++++ .../package.json | 12 + 3 files changed, 2449 insertions(+) create mode 100644 research/form-associated-ce-combobox-poc/SUMMARY.md create mode 100644 research/form-associated-ce-combobox-poc/index.html create mode 100644 research/form-associated-ce-combobox-poc/package.json diff --git a/research/form-associated-ce-combobox-poc/SUMMARY.md b/research/form-associated-ce-combobox-poc/SUMMARY.md new file mode 100644 index 0000000000..c87ec908f2 --- /dev/null +++ b/research/form-associated-ce-combobox-poc/SUMMARY.md @@ -0,0 +1,493 @@ +# Form-associated custom element PoC: combobox with ElementInternals + +> Research artifact for the 2nd-gen forms strategy. +> Blocks: direction PR | Parallel with: text-field ElementInternals PoC + +## Demo + +**StackBlitz:** [Open live demo](https://stackblitz.com/github/adobe/spectrum-web-components/tree/rajdeepchandra/chore-face-combobox-poc/research/form-associated-ce-combobox-poc?file=index.html) + +Or open `index.html` locally in any browser (no build step required). +axe-core loads from CDN on demand when you click the audit button. + +--- + +## Automated test results (Chrome headless) + +| Test | Expected | Actual | Status | +| ---------------------------- | -------------------------------- | ------------------------------------------- | ------- | +| 1a Submit | FormData includes fruit | fruit: "Apple" | Pass | +| 1a Reset | Restores to Apple | formResetCallback: PASS | Pass | +| 1b Empty required | valueMissing: true | Correctly rejected | Pass | +| 1b Valid selection | validity.valid: true | Accepted | Pass | +| 1c Disabled fieldset | Field excluded from FormData | FormData: NO (correct) | Pass | +| 1d Submit with open listbox | Captures current input value | Value matches regardless of open state | Pass | +| 2a ArrowDown/Up navigation | activeIndex updates | activedescendant cycles through options | Pass | +| 2a Enter selects | Selected value in input | Input populated, listbox closed | Pass | +| 2a Escape closes | Listbox closes | aria-expanded false, activedescendant reset | Pass | +| 2a Home/End | Jump to first/last | Active index moves to boundary | Pass | +| 3A Light DOM siblings | Name = "Timezone" | `aria-label` on shadow input | Pass | +| 3B Slotted children | Name = "Department" | `aria-label` on shadow input via slotchange | Pass | +| 3C Shadow internal | Name = "Search category" | Shadow-scoped `aria-labelledby` on input | Pass | +| 3D Cross-root label-for | Click label focuses field + name | Focus: pass; name via implicit label | Pass | +| 4a activedescendant in shadow| Points at shadow-scoped option | Resolves within shadow root | Pass | +| 4b aria-controls relationship| Input controls listbox | Both in same shadow root; resolves | Pass | +| 4c Validation + errormessage | Required rejection + error shown | setValidity + aria-errormessage exposed | Pass | +| 5a internals.ariaLabel | Exposes name in a11y tree | "Search via internals" on shadow input | Pass | +| 5a host aria-label | Exposes name in a11y tree | "Search via host attribute" on shadow input | Pass | +| 5b ariaExpanded on internals | Expanded state announced | Host attribute + internals both set | Pass | +| 6 axe-core | Audit runs, results shown | See axe findings section | Pass | + +--- + +## Critical research findings + +### 1. `aria-activedescendant` works within shadow root boundaries + +Unlike cross-root IDREF scenarios, `aria-activedescendant` on a shadow input pointing at option elements within the same shadow root resolves correctly across all tested browsers. This is because both the combobox input and the listbox options share the same tree scope. **This is the recommended pattern**: keep the input and listbox co-located in the same shadow root. + +### 2. `aria-controls` resolves within shadow scope + +The input's `aria-controls` attribute pointing at a shadow-scoped listbox ID works correctly. The a11y tree shows the ownership relationship. This matches the text-field PoC finding that same-scope IDREFs are reliable. + +### 3. `internals.ariaExpanded` supplements but does not replace host attributes + +Setting `internals.ariaExpanded` updates the host's a11y node, but screen readers that focus the shadow input need `aria-expanded` on the input itself. The PoC uses a dual-write strategy: both `internals.ariaExpanded` and the shadow input's `aria-expanded` are kept in sync. This parallels the text-field finding about `internals.ariaLabel`. + +### 4. Popup dismissal during form submit + +When the listbox is open and the user presses Enter on an active option, the combobox correctly captures the selection and closes the popup before form submission occurs. The form always sees the current input value via `setFormValue()`, regardless of popup state. + +### 5. `delegatesFocus` and popup interaction + +`delegatesFocus: true` correctly routes focus to the shadow input when the host is focused (including via `