From ae7fb8b8457e67341520cda3f282680e5c34d711 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Fri, 24 Jul 2026 15:38:09 -0300 Subject: [PATCH 1/4] feat(forms): add SelectField for labelled, accessible selects Adds a SelectField component: the design-system counterpart to InputGroup for a react-select control. It wires the label, inline error, and aria relationships off a single id (htmlFor -> inputId, aria-invalid, aria-errormessage), which the InputGroup `component` slot does not do. First cut only. No call sites migrated yet; the error-state styling and react-select aria verification are follow-ups tracked in SELECT_FIELD_MIGRATION.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/SELECT_FIELD_MIGRATION.md | 72 +++++++++++++++ .../components/SelectField.stories.tsx | 68 ++++++++++++++ .../web/components/base/forms/SelectField.tsx | 90 +++++++++++++++++++ 3 files changed, 230 insertions(+) create mode 100644 frontend/SELECT_FIELD_MIGRATION.md create mode 100644 frontend/documentation/components/SelectField.stories.tsx create mode 100644 frontend/web/components/base/forms/SelectField.tsx diff --git a/frontend/SELECT_FIELD_MIGRATION.md b/frontend/SELECT_FIELD_MIGRATION.md new file mode 100644 index 000000000000..22c6d5fb2feb --- /dev/null +++ b/frontend/SELECT_FIELD_MIGRATION.md @@ -0,0 +1,72 @@ +# Removing the `InputGroup` `component` prop + +Plan to retire `` in favour of purpose-built, accessible field components. Contributes to #7364 (unified form field standardisation). + +## Why + +`InputGroup`'s value is that it wires a labelled field's accessibility for you: `htmlFor` / `id` on the label and control, `aria-invalid`, and `aria-describedby` to the error, all off one id. + +The `component` prop bypasses all of that. When you pass `component={ + + + ) +} + +export default SelectField From d8cd50e4cd9b50aa51d0c514c6853cd9b2d3e3b8 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Fri, 24 Jul 2026 15:44:29 -0300 Subject: [PATCH 2/4] docs(forms): note the Select E2E fork-drop as a sequenced follow-up Records the decision to drop the Select E2E (TestCafe-era) fork in its own Select-only PR, after the SelectField call-site migration. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/SELECT_FIELD_MIGRATION.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frontend/SELECT_FIELD_MIGRATION.md b/frontend/SELECT_FIELD_MIGRATION.md index 22c6d5fb2feb..fb468d05d637 100644 --- a/frontend/SELECT_FIELD_MIGRATION.md +++ b/frontend/SELECT_FIELD_MIGRATION.md @@ -70,3 +70,17 @@ Counts are a starting map, not a per-site read. A few `component=` matches in th - `SelectField` is a first cut. The error-state styling and the react-select aria verification (step 2) are deliberately follow-ups so this PR stays reviewable. - The base `Select` (`web/project/project-components.js`) is untyped JS and spreads props into react-select, which is why `SelectField` types its own surface off react-select's `Props` plus the base's custom extras (`size`, `autoSelect`). + +## Related follow-up: drop the Select E2E fork (separate PR, after SelectField) + +The base `Select` swaps to a bespoke ` + ` DOM when `global.E2E` is true. This is TestCafe-era scaffolding; E2E is fully Playwright now, and Playwright can drive real react-select. The fork is the source of the "Select behaves differently under E2E" bugs (e.g. the fork keys its input off `id`, while react-select uses `inputId`). + +Kept out of the `component`-prop migration on purpose. Sequenced **after** the Select call sites move to `SelectField`, as its own Select-only PR: + +1. Add `data-test` to the real (non-E2E) `Select` container so tests can target it (react-select does not render arbitrary `data-test`). +2. Add a Playwright `selectOption(selectTestId, label)` helper: click the container to open the menu, click `.react-select__option` by text. +3. Migrate the sites that use the fork's `*-option-N` ids: `invite-test.pw.ts`, `roles-test.pw.ts`, and the `select-segment-option-${i}` helper in `e2e-helpers.playwright.ts`. +4. Remove the `E2E ?` branch from `Select`. +5. Run the affected specs (`invite`, `roles`, segment) to confirm. Requires Docker + API on localhost:8000. + +Scope is `Select` only. The other E2E forks (`toast`, `ValueEditor`, `FeatureAction`, `ActionItem`, the config/organisation stores) are out of scope. From 1d8ee58922e024586be2d201ce774201d792b5b0 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Fri, 24 Jul 2026 15:46:03 -0300 Subject: [PATCH 3/4] docs(forms): prefer a11y/role selectors for the Select E2E follow-up Target selects by role and accessible name rather than data-test, so the E2E suite doubles as an accessibility regression guard. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/SELECT_FIELD_MIGRATION.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/frontend/SELECT_FIELD_MIGRATION.md b/frontend/SELECT_FIELD_MIGRATION.md index fb468d05d637..e4ffd29e7f57 100644 --- a/frontend/SELECT_FIELD_MIGRATION.md +++ b/frontend/SELECT_FIELD_MIGRATION.md @@ -75,12 +75,21 @@ Counts are a starting map, not a per-site read. A few `component=` matches in th The base `Select` swaps to a bespoke ` + ` DOM when `global.E2E` is true. This is TestCafe-era scaffolding; E2E is fully Playwright now, and Playwright can drive real react-select. The fork is the source of the "Select behaves differently under E2E" bugs (e.g. the fork keys its input off `id`, while react-select uses `inputId`). -Kept out of the `component`-prop migration on purpose. Sequenced **after** the Select call sites move to `SelectField`, as its own Select-only PR: +Kept out of the `component`-prop migration on purpose. Sequenced **after** the Select call sites move to `SelectField`, as its own Select-only PR. -1. Add `data-test` to the real (non-E2E) `Select` container so tests can target it (react-select does not render arbitrary `data-test`). -2. Add a Playwright `selectOption(selectTestId, label)` helper: click the container to open the menu, click `.react-select__option` by text. -3. Migrate the sites that use the fork's `*-option-N` ids: `invite-test.pw.ts`, `roles-test.pw.ts`, and the `select-segment-option-${i}` helper in `e2e-helpers.playwright.ts`. -4. Remove the `E2E ?` branch from `Select`. -5. Run the affected specs (`invite`, `roles`, segment) to confirm. Requires Docker + API on localhost:8000. +Prefer accessibility selectors over `data-test`. This is the payoff of `SelectField`: a select with a proper label gets an accessible name, so Playwright can target it by role, with no bespoke test id. The sequencing matters here, once the target selects render through `SelectField` (with a `title`), the role selectors work. + +This also makes the E2E suite a standing accessibility guard: a role/name selector only resolves if the component is genuinely accessible, so if a select later loses its label the test fails loudly instead of the accessibility silently rotting. `data-test` selectors give no such signal. + +1. Add a Playwright `selectOption(name, optionLabel)` helper using role/name selectors: + ```ts + await page.getByRole('combobox', { name }).click() // open the menu + await page.getByRole('option', { name: optionLabel }).click() + ``` +2. Migrate the sites that use the fork's `*-option-N` ids to the helper: `invite-test.pw.ts`, `roles-test.pw.ts`, and the `select-segment-option-${i}` helper in `e2e-helpers.playwright.ts`. +3. Remove the `E2E ?` branch from `Select`. +4. Run the affected specs (`invite`, `roles`, segment) to confirm. Requires Docker + API on localhost:8000. + +Fallback: a select without a visible label (inline filters, etc.) has no accessible name. Give it an `aria-label` (via `SelectField`) so it stays role-targetable, rather than reintroducing a `data-test`. Scope is `Select` only. The other E2E forks (`toast`, `ValueEditor`, `FeatureAction`, `ActionItem`, the config/organisation stores) are out of scope. From 502632c0fc3f39e24f80b7e379e89477d9256126 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Mon, 27 Jul 2026 13:57:22 -0300 Subject: [PATCH 4/4] chore(forms): keep the SelectField migration plan as a local working doc Co-Authored-By: Claude Fable 5 --- frontend/SELECT_FIELD_MIGRATION.md | 95 ------------------------------ 1 file changed, 95 deletions(-) delete mode 100644 frontend/SELECT_FIELD_MIGRATION.md diff --git a/frontend/SELECT_FIELD_MIGRATION.md b/frontend/SELECT_FIELD_MIGRATION.md deleted file mode 100644 index e4ffd29e7f57..000000000000 --- a/frontend/SELECT_FIELD_MIGRATION.md +++ /dev/null @@ -1,95 +0,0 @@ -# Removing the `InputGroup` `component` prop - -Plan to retire `` in favour of purpose-built, accessible field components. Contributes to #7364 (unified form field standardisation). - -## Why - -`InputGroup`'s value is that it wires a labelled field's accessibility for you: `htmlFor` / `id` on the label and control, `aria-invalid`, and `aria-describedby` to the error, all off one id. - -The `component` prop bypasses all of that. When you pass `component={ + ` DOM when `global.E2E` is true. This is TestCafe-era scaffolding; E2E is fully Playwright now, and Playwright can drive real react-select. The fork is the source of the "Select behaves differently under E2E" bugs (e.g. the fork keys its input off `id`, while react-select uses `inputId`). - -Kept out of the `component`-prop migration on purpose. Sequenced **after** the Select call sites move to `SelectField`, as its own Select-only PR. - -Prefer accessibility selectors over `data-test`. This is the payoff of `SelectField`: a select with a proper label gets an accessible name, so Playwright can target it by role, with no bespoke test id. The sequencing matters here, once the target selects render through `SelectField` (with a `title`), the role selectors work. - -This also makes the E2E suite a standing accessibility guard: a role/name selector only resolves if the component is genuinely accessible, so if a select later loses its label the test fails loudly instead of the accessibility silently rotting. `data-test` selectors give no such signal. - -1. Add a Playwright `selectOption(name, optionLabel)` helper using role/name selectors: - ```ts - await page.getByRole('combobox', { name }).click() // open the menu - await page.getByRole('option', { name: optionLabel }).click() - ``` -2. Migrate the sites that use the fork's `*-option-N` ids to the helper: `invite-test.pw.ts`, `roles-test.pw.ts`, and the `select-segment-option-${i}` helper in `e2e-helpers.playwright.ts`. -3. Remove the `E2E ?` branch from `Select`. -4. Run the affected specs (`invite`, `roles`, segment) to confirm. Requires Docker + API on localhost:8000. - -Fallback: a select without a visible label (inline filters, etc.) has no accessible name. Give it an `aria-label` (via `SelectField`) so it stays role-targetable, rather than reintroducing a `data-test`. - -Scope is `Select` only. The other E2E forks (`toast`, `ValueEditor`, `FeatureAction`, `ActionItem`, the config/organisation stores) are out of scope.