From 7b456d801576b29300ea2ff64b13dfe9bfb097aa Mon Sep 17 00:00:00 2001 From: "hedwig.doets" Date: Mon, 8 Jun 2026 09:02:14 +0200 Subject: [PATCH 1/2] feat: openspec for connecting validation ids to input widgets --- .../.openspec.yaml | 2 + .../design.md | 145 ++++++++++++++++++ .../proposal.md | 29 ++++ .../specs/validation-id-enforcement/spec.md | 74 +++++++++ .../tasks.md | 77 ++++++++++ 5 files changed, 327 insertions(+) create mode 100644 openspec/changes/enforce-validation-id-requirement/.openspec.yaml create mode 100644 openspec/changes/enforce-validation-id-requirement/design.md create mode 100644 openspec/changes/enforce-validation-id-requirement/proposal.md create mode 100644 openspec/changes/enforce-validation-id-requirement/specs/validation-id-enforcement/spec.md create mode 100644 openspec/changes/enforce-validation-id-requirement/tasks.md diff --git a/openspec/changes/enforce-validation-id-requirement/.openspec.yaml b/openspec/changes/enforce-validation-id-requirement/.openspec.yaml new file mode 100644 index 0000000000..c53ef21aaa --- /dev/null +++ b/openspec/changes/enforce-validation-id-requirement/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-05 diff --git a/openspec/changes/enforce-validation-id-requirement/design.md b/openspec/changes/enforce-validation-id-requirement/design.md new file mode 100644 index 0000000000..f7de42f029 --- /dev/null +++ b/openspec/changes/enforce-validation-id-requirement/design.md @@ -0,0 +1,145 @@ +## Context + +ValidationAlert is a shared component in `@mendix/widget-plugin-component-kit` used by 5+ widgets to display validation messages. Currently, the `id` prop is optional, leading to inconsistent ARIA implementation. Some widgets (combobox, checkbox-radio-selection) properly connect validation via `aria-describedby`, while others (slider, range-slider, rich-text) render ValidationAlert without IDs, breaking screen reader accessibility. + +WCAG 2.2 AA requires form inputs to be programmatically associated with their error messages. Without IDs, screen readers cannot announce validation errors when users focus invalid fields. + +## Goals / Non-Goals + +**Goals:** + +- Make ValidationAlert ID prop required at compile time (TypeScript) +- Update all affected widgets to provide proper IDs and connect them via ARIA attributes +- Establish consistent validation ID naming convention across widgets +- Maintain backward compatibility for widgets already using IDs correctly + +**Non-Goals:** + +- Runtime enforcement or validation checking (TypeScript-only enforcement) +- Automatic ID generation (widgets must explicitly provide IDs) +- Changes to ValidationAlert visual styling or behavior +- Enforcement for non-widget code using ValidationAlert + +## Decisions + +### Decision 1: Required prop via TypeScript interface + +**Choice:** Change `ValidationAlertProps.id` from `id?: string` to `id: string` (remove optional modifier) + +**Rationale:** TypeScript provides compile-time enforcement without runtime overhead. Widgets that don't provide IDs will fail to compile, preventing the issue from reaching production. + +**Alternatives considered:** + +- Runtime error on missing ID: Rejected - breaks production apps immediately, too risky for breaking change +- Console warning only: Rejected - easy to miss, doesn't prevent deployment +- ESLint rule: Rejected - not as reliable as type system, requires separate tooling + +### Decision 2: Centralized ID helper utility + +**Choice:** Export `getValidationErrorId(inputId?: string): string | undefined` from `@mendix/widget-plugin-component-kit`. Returns `${inputId}-validation-message` or undefined. + +**Rationale:** + +- Consistent naming convention across all widgets +- Already partially exists in combobox (proven pattern) +- Type-safe (returns undefined when input is undefined) +- Simple implementation, easy to test + +**Alternatives considered:** + +- Each widget generates own IDs: Rejected - inconsistent conventions +- UUID-based IDs: Rejected - not human-readable, harder to debug +- Component-level auto-generation: Rejected - breaks explicit connection requirement + +### Decision 3: Widget migration strategy + +**Choice:** Update widgets in dependency order: + +1. `widget-plugin-component-kit` - update ValidationAlert interface + add helper +2. Widgets already using IDs (combobox, checkbox-radio-selection) - switch to helper function +3. Widgets without IDs (slider, range-slider, rich-text) - add ID prop + ARIA attributes + +**Rationale:** Minimizes breaking changes by updating the shared component first, then migrating widgets incrementally. Widgets already using IDs get easy refactor to helper function before breaking change lands. + +**Alternatives considered:** + +- All-at-once update: Rejected - risky, hard to review +- Gradual deprecation: Rejected - leaves accessibility broken for too long + +## Risks / Trade-offs + +### Risk: Breaking change impacts external consumers + +If teams outside web-widgets use ValidationAlert directly, their builds will break. + +**Mitigation:** + +- Search codebase for ValidationAlert usage before merging +- Add migration notes to CHANGELOG +- Consider major version bump for widget-plugin-component-kit +- Provide clear error message: "ValidationAlert requires 'id' prop for ARIA accessibility" + +### Risk: Inconsistent input IDs across widgets + +Some widgets may not have stable input IDs, making validation ID generation unreliable. + +**Mitigation:** + +- All Mendix widgets already receive `id` prop from platform (widget instance ID) +- Document requirement that input elements use `{props.id}` as their ID +- Helper function handles undefined gracefully (returns undefined, no error) + +### Trade-off: Required prop vs gradual adoption + +Making ID required forces immediate action from all widgets. This is intentional - accessibility bugs should not be optional to fix. + +**Accepted:** Short-term friction for long-term correctness is the right choice for accessibility requirements. + +## Migration Plan + +### Phase 1: Update shared component (widget-plugin-component-kit) + +1. Add `getValidationErrorId` helper to exports +2. Change `id?: string` to `id: string` in ValidationAlertProps +3. Update package version (consider semver MAJOR) +4. Run tests for Alert component + +### Phase 2: Update widgets with existing IDs (non-breaking) + +1. combobox-web: Switch to `getValidationErrorId` helper +2. checkbox-radio-selection-web: Switch to helper +3. Verify ARIA connections still work +4. Run widget-specific tests + +### Phase 3: Fix widgets without IDs (now broken by Phase 1) + +1. slider-web: Add `id={getValidationErrorId(props.id)}` to ValidationAlert, add `aria-invalid` and `aria-describedby` to slider handle +2. range-slider-web: Consolidate to single ValidationAlert with `id={getValidationErrorId(props.id)}`, both handles reference same validation message via `aria-describedby` +3. rich-text-web: Add ID and ARIA attributes to editor wrapper +4. Run E2E accessibility tests for each widget + +### Phase 4: Documentation + +1. Update frontend-guidelines.md with validation connection requirements +2. Add example to widget template/scaffold +3. Document helper function in widget-plugin-component-kit README + +### Rollback Strategy + +If critical issues found after merge: + +1. Revert widget-plugin-component-kit to make `id` optional again +2. Keep helper function (non-breaking) +3. Keep widget fixes (they work with optional ID) +4. Re-evaluate approach + +## Open Questions + +1. Should we add E2E tests that verify ARIA connections in each widget's test suite? + - Leaning yes - ensures validation stays connected across refactors + +2. Should getValidationErrorId be widget-specific or centralized? + - Decision: Centralized (as proposed) for consistency + +3. Do we need TypeScript tests to verify the type error occurs when ID is missing? + - Leaning no - TypeScript compilation itself is the test diff --git a/openspec/changes/enforce-validation-id-requirement/proposal.md b/openspec/changes/enforce-validation-id-requirement/proposal.md new file mode 100644 index 0000000000..080306664a --- /dev/null +++ b/openspec/changes/enforce-validation-id-requirement/proposal.md @@ -0,0 +1,29 @@ +## Why + +ValidationAlert components are currently used without IDs in several widgets, preventing proper ARIA connection between form controls and validation messages. This breaks accessibility for screen reader users who cannot determine which field has an error or read the validation message. Making the ID required ensures all validation messages are properly connected via `aria-describedby` and `aria-invalid` attributes. + +## What Changes + +- **BREAKING**: Make `ValidationAlert` component's `id` prop required in TypeScript interface +- Add development-mode runtime warning when ValidationAlert is rendered without proper ARIA connection +- Update all widgets using ValidationAlert to provide IDs: slider-web, range-slider-web, rich-text-web +- Add validation connection helper utilities for consistent ID generation +- Update widget development guidelines to document validation ID requirements + +## Capabilities + +### New Capabilities + +- `validation-id-enforcement`: Type system and runtime checks ensuring all validation messages have IDs and can be properly connected to form controls via ARIA attributes + +### Modified Capabilities + + + +## Impact + +- **Breaking change** for ValidationAlert component API (id prop becomes required) +- Affects 5+ widgets currently using ValidationAlert (slider-web, range-slider-web, rich-text-web, combobox-web, checkbox-radio-selection-web) +- Improves WCAG 2.2 AA compliance across all widgets with validation +- Requires coordinated updates across multiple widget packages +- No runtime behavior changes for end users - purely accessibility improvements diff --git a/openspec/changes/enforce-validation-id-requirement/specs/validation-id-enforcement/spec.md b/openspec/changes/enforce-validation-id-requirement/specs/validation-id-enforcement/spec.md new file mode 100644 index 0000000000..6d802cd660 --- /dev/null +++ b/openspec/changes/enforce-validation-id-requirement/specs/validation-id-enforcement/spec.md @@ -0,0 +1,74 @@ +## ADDED Requirements + +### Requirement: ValidationAlert requires ID prop + +The ValidationAlert component SHALL require an `id` prop in its TypeScript interface. The TypeScript compiler MUST error when ValidationAlert is used without an `id` prop. + +#### Scenario: Component used without ID fails type checking + +- **WHEN** developer writes `Error message` without id prop +- **THEN** TypeScript compiler produces error "Property 'id' is missing in type" + +#### Scenario: Component used with ID passes type checking + +- **WHEN** developer writes `Error message` +- **THEN** TypeScript compiler accepts the code without errors + +### Requirement: ValidationAlert renders with ID attribute + +The ValidationAlert component SHALL render its `id` prop as the `id` attribute on the alert DOM element. The rendered element MUST have the exact ID value provided. + +#### Scenario: ID prop is rendered as DOM attribute + +- **WHEN** ValidationAlert is rendered with `id="my-field-error"` +- **THEN** the rendered alert element has attribute `id="my-field-error"` + +#### Scenario: ID is accessible to ARIA references + +- **WHEN** input has `aria-describedby="my-field-error"` and ValidationAlert has `id="my-field-error"` +- **THEN** screen readers announce the validation message when the input is focused + +### Requirement: Input elements connect to validation + +Form input components that display validation MUST set both `aria-invalid` and `aria-describedby` attributes when validation exists. The `aria-describedby` value SHALL reference the ValidationAlert's ID. + +#### Scenario: Input with validation sets ARIA attributes + +- **WHEN** input component has validation message present +- **WHEN** ValidationAlert is rendered with `id="input-123-error"` +- **THEN** input element has `aria-invalid="true"` +- **THEN** input element has `aria-describedby="input-123-error"` + +#### Scenario: Input without validation clears ARIA attributes + +- **WHEN** input component has no validation message +- **THEN** input element does not have `aria-invalid` attribute +- **THEN** input element does not have `aria-describedby` attribute + +#### Scenario: Multi-handle widget shares validation message + +- **WHEN** range slider has two handles (lower and upper bound) +- **WHEN** either attribute has validation message +- **WHEN** ValidationAlert is rendered with `id="widget-123-validation-message"` +- **THEN** both handle elements have `aria-describedby="widget-123-validation-message"` +- **THEN** both handle elements have `aria-invalid="true"` + +### Requirement: Validation ID helper utility + +A helper function SHALL be provided to generate consistent validation IDs from input IDs. The function SHALL append "-validation-message" suffix to the input ID. The function SHALL handle undefined input IDs gracefully. + +#### Scenario: Generate validation ID from input ID + +- **WHEN** helper function called with `inputId="myInput"` +- **THEN** function returns "myInput-validation-message" + +#### Scenario: Handle undefined input ID + +- **WHEN** helper function called with `inputId=undefined` +- **THEN** function returns `undefined` + +#### Scenario: Consistent naming convention + +- **WHEN** multiple widgets use the helper function with their input IDs +- **THEN** all validation IDs follow the same "-validation-message" suffix pattern +- **THEN** ARIA connections can be validated consistently across widgets diff --git a/openspec/changes/enforce-validation-id-requirement/tasks.md b/openspec/changes/enforce-validation-id-requirement/tasks.md new file mode 100644 index 0000000000..5a2d465561 --- /dev/null +++ b/openspec/changes/enforce-validation-id-requirement/tasks.md @@ -0,0 +1,77 @@ +## 1. Update Shared Component (widget-plugin-component-kit) + +- [ ] 1.1 Export getValidationErrorId helper function in Alert.tsx (takes inputId?: string, returns "${inputId}-validation-message" or undefined) +- [ ] 1.2 Add unit tests for getValidationErrorId helper (test normal ID, undefined ID, empty string) +- [ ] 1.3 Change ValidationAlertProps interface: id?: string → id: string (make required) +- [ ] 1.4 Verify existing Alert tests still pass with required ID +- [ ] 1.5 Update widget-plugin-component-kit package version (consider semver implications) + +## 2. Update Combobox Widget (Already Has IDs) + +- [ ] 2.1 Import getValidationErrorId from @mendix/widget-plugin-component-kit +- [ ] 2.2 Replace manual ID generation with getValidationErrorId(options.inputId) in SingleSelection +- [ ] 2.3 Replace manual ID generation with getValidationErrorId(options.inputId) in MultiSelection +- [ ] 2.4 Verify aria-describedby and aria-invalid still work correctly +- [ ] 2.5 Run combobox unit tests to ensure no regressions +- [ ] 2.6 Run combobox E2E tests to verify validation display + +## 3. Update Checkbox-Radio-Selection Widget (Already Has IDs) + +- [ ] 3.1 Import getValidationErrorId from @mendix/widget-plugin-component-kit +- [ ] 3.2 Replace manual ID generation with getValidationErrorId in CheckboxSelection component +- [ ] 3.3 Replace manual ID generation with getValidationErrorId in RadioSelection component +- [ ] 3.4 Verify aria-describedby and aria-invalid still work correctly +- [ ] 3.5 Run checkbox-radio-selection unit tests to ensure no regressions +- [ ] 3.6 Run checkbox-radio-selection E2E tests to verify validation display + +## 4. Fix Slider Widget (Missing IDs) + +- [ ] 4.1 Import getValidationErrorId from @mendix/widget-plugin-component-kit +- [ ] 4.2 Update Slider.tsx: pass id={getValidationErrorId(props.id)} to ValidationAlert +- [ ] 4.3 Add aria-invalid attribute to slider handle when validation exists +- [ ] 4.4 Add aria-describedby={getValidationErrorId(props.id)} to slider handle when validation exists +- [ ] 4.5 Write unit tests verifying ARIA attributes are set when validation exists +- [ ] 4.6 Write unit tests verifying ARIA attributes are removed when validation clears +- [ ] 4.7 Run slider widget tests to ensure no regressions +- [ ] 4.8 Manually test slider validation in test project + +## 5. Fix Range Slider Widget (Missing IDs - Consolidate Validations) + +- [ ] 5.1 Import getValidationErrorId from @mendix/widget-plugin-component-kit +- [ ] 5.2 Determine which validation to display (lowerBoundAttribute.validation || upperBoundAttribute.validation) +- [ ] 5.3 Replace two ValidationAlert components with single ValidationAlert using id={getValidationErrorId(props.id)} +- [ ] 5.4 Add aria-invalid to both slider handles when any validation exists +- [ ] 5.5 Add aria-describedby={getValidationErrorId(props.id)} to both slider handles when validation exists +- [ ] 5.6 Write unit tests verifying both handles reference same validation message ID +- [ ] 5.7 Write unit tests verifying ARIA attributes are set when either bound has validation +- [ ] 5.8 Run range slider widget tests to ensure no regressions +- [ ] 5.9 Manually test range slider validation in test project + +## 6. Fix Rich Text Widget (Missing IDs) + +- [ ] 6.1 Import getValidationErrorId from @mendix/widget-plugin-component-kit +- [ ] 6.2 Update RichText.tsx: pass id={getValidationErrorId(props.id)} to ValidationAlert +- [ ] 6.3 Identify the rich text editor's input element/container +- [ ] 6.4 Add aria-invalid attribute to editor when stringAttribute.validation exists +- [ ] 6.5 Add aria-describedby={getValidationErrorId(props.id)} to editor when validation exists +- [ ] 6.6 Write unit tests verifying ARIA attributes on editor element +- [ ] 6.7 Run rich text widget tests to ensure no regressions +- [ ] 6.8 Manually test rich text validation in test project + +## 7. Integration Testing + +- [ ] 7.1 Build all affected widgets (combobox, checkbox-radio-selection, slider, range-slider, rich-text) +- [ ] 7.2 Test in Studio Pro project: verify validation messages appear visually +- [ ] 7.3 Test with screen reader (VoiceOver/NVDA): verify validation messages are announced on focus +- [ ] 7.4 Verify development console warnings appear for any remaining unconnected validations +- [ ] 7.5 Verify no console warnings in production build +- [ ] 7.6 Run full monorepo test suite to check for unexpected breakages + +## 8. Documentation + +- [ ] 8.1 Update docs/requirements/frontend-guidelines.md: add section on validation ARIA requirements +- [ ] 8.2 Document getValidationErrorId helper in widget-plugin-component-kit README +- [ ] 8.3 Add example of proper ValidationAlert usage with ID and ARIA connection +- [ ] 8.4 Update CHANGELOG.md for widget-plugin-component-kit (BREAKING: ValidationAlert id prop now required) +- [ ] 8.5 Update CHANGELOG.md for each affected widget (accessibility: connect validation to inputs via ARIA) +- [ ] 8.6 Consider adding validation connection example to widget template/scaffold From 1cc3e9bb7ab3a1161e815d7fa65ce61d3df492ff Mon Sep 17 00:00:00 2001 From: "hedwig.doets" Date: Thu, 18 Jun 2026 15:49:29 +0200 Subject: [PATCH 2/2] fix(combobox-web): hide decorative icons and prevent empty menu exposure --- .../combobox-validation-a11y/.openspec.yaml | 2 + .../combobox-validation-a11y/design.md | 109 ++++++++++++++++ .../combobox-validation-a11y/proposal.md | 30 +++++ .../specs/decorative-icon-hiding/spec.md | 40 ++++++ .../specs/empty-group-prevention/spec.md | 50 ++++++++ .../changes/combobox-validation-a11y/tasks.md | 44 +++++++ .../combobox-web/e2e/Combobox.spec.js | 17 +++ .../src/__tests__/SingleSelection.spec.tsx | 121 ++++++++++++++++++ .../MultiSelection.spec.tsx.snap | 96 ++------------ .../SingleSelection.spec.tsx.snap | 11 +- .../StaticSelection.spec.tsx.snap | 11 +- .../combobox-web/src/assets/icons.tsx | 4 +- .../src/components/ComboboxMenuWrapper.tsx | 49 ++++--- 13 files changed, 454 insertions(+), 130 deletions(-) create mode 100644 openspec/changes/combobox-validation-a11y/.openspec.yaml create mode 100644 openspec/changes/combobox-validation-a11y/design.md create mode 100644 openspec/changes/combobox-validation-a11y/proposal.md create mode 100644 openspec/changes/combobox-validation-a11y/specs/decorative-icon-hiding/spec.md create mode 100644 openspec/changes/combobox-validation-a11y/specs/empty-group-prevention/spec.md create mode 100644 openspec/changes/combobox-validation-a11y/tasks.md diff --git a/openspec/changes/combobox-validation-a11y/.openspec.yaml b/openspec/changes/combobox-validation-a11y/.openspec.yaml new file mode 100644 index 0000000000..a903f7fe18 --- /dev/null +++ b/openspec/changes/combobox-validation-a11y/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-16 diff --git a/openspec/changes/combobox-validation-a11y/design.md b/openspec/changes/combobox-validation-a11y/design.md new file mode 100644 index 0000000000..a4e1b99238 --- /dev/null +++ b/openspec/changes/combobox-validation-a11y/design.md @@ -0,0 +1,109 @@ +## Context + +The combobox widget currently has two accessibility issues affecting screen reader users: + +1. **Decorative icons announced as "image"**: The DownArrow and ClearButton SVG icons are exposed to assistive technologies, causing screen readers to announce them as "image" elements. These icons are purely decorative - they duplicate information already conveyed through the combobox role, button roles, and ARIA labels. + +2. **Empty menu structure exposed**: When the combobox has no options, the `