fix(a11y): add id={property.name} to all property editors for label association#1508
fix(a11y): add id={property.name} to all property editors for label association#1508
Conversation
GerardasB
left a comment
There was a problem hiding this comment.
Property editor elements are (usually) created by EditorContainer, how do you plan to pass down the itemId prop?
Additionally, itemId prop documentation states, that it adds data-item-id, not data-testid:
indeed i misread, i thought it added data-testid not data-item-id, will add a new attribute instead like you suggested |
|
How about usage? Property editor elements are (usually) created by |
Ya I looked more into it and the change actually needs to be made in itwinjs-core first. I think you saw the discussion there, I'll wait to see from our QA team if they're fine with using something other than the data-testid and will update this PR accordingly |
hey given that theyre not keen on adding data-testid support in itwinjs-core, can we instead dynamically define the data-testid by using the label for example as part of the data-testid? |
|
Still waiting on the usage part, i.e. what APIs/components are you using and what you are trying to achieve? Is this needed for testing only? |
|
Seems like inputs are labelled correctly when |
Associate each new editor's interactive element (Input, Select, Checkbox,
ToggleSwitch) with the property name via the HTML id attribute. This
ensures that the existing <label htmlFor={propertyName}> in
ComponentGenerator correctly targets the editor, enabling:
- getByLabelText() queries in tests
- Accessible label/input association when toolSettingsNewEditors is enabled
Resolves the a11y gap Gerardas pointed out: inputs were correctly labelled
with toolSettingsNewEditors disabled (legacy EditorContainer sets id) but
not when it was enabled (new editors omitted the id).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds 5 new tests across two files:
PropertyRecordEditor.test.tsx — integration:
- sets id={property.name} on the input (new editor system)
- allows getByLabelText for text properties (new editor system)
- sets id={property.name} on the select for enum properties
- guard: legacy editor system path still renders EditorContainer
EditorRenderer.test.tsx — unit:
- forwards id prop to the rendered editor element
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three stories under 'Components/New Property Editors': - All editor types (new system): shows Text, Numeric, Enum, Boolean, and Toggle editors each paired with a <label htmlFor>. Clicking any label focuses its input — verifying the a11y fix works. - Arc Drawing — ToolSettings use case: two enum selects (Arc Type, Draw Method) plus a numeric field, all uniquely addressable by label. This is the concrete scenario that motivated the fix. - Legacy vs New — label association: side-by-side comparison of the same enum editor rendered with the legacy EditorContainer (label click doesn't focus) vs the new system (label click focuses). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Shows every legacy editor type (string, number, enum, boolean, toggle) in a table annotating what selector you can use in tests: string → id = property.name → getByLabelText / #name ✅ unique number → no id or data-testid on the <input> ⚠ not targetable enum → data-testid='components-select-editor' ⚠ not unique boolean → data-testid='components-checkbox-editor' ⚠ not unique toggle → data-testid='components-toggle-editor' ⚠ not unique Also renders two enum selects side-by-side to demonstrate the collision that motivated the a11y fix (both share the same data-testid). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
number typename with no specific editor name falls back to
BasicPropertyEditor → TextEditor, which sets id={property.name}.
So id-based targeting works for number just like string.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirrors what TextEditor already does. Each editor now sets
id={propertyRecord.property.name} on its native element
(<Select>, <Checkbox>, <ToggleSwitch>) alongside the existing
data-testid, enabling label association and getByLabelText queries.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ypes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TextareaEditor, IconEditor, EnumButtonGroupEditor, DateTimeEditor,
SliderEditor all now expose id={propertyRecord.property.name} on
their root/interactive element.
For popup-based editors (Textarea, DateTime, Slider) the id is
placed on the outer container div — the always-visible trigger —
since the popup content may not be in the DOM when closed.
CustomNumberEditor was already fixed in a prior commit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ImageCheckBoxProps lacked an id field (CommonProps only has itemId). Added id?: string to the interface and thread it to the native <input type="checkbox"> element, consistent with all other legacy editors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…pers
TextArea, Color, CustomNumber, EnumButtonGroup, NumericInput, Slider
were all missing id in their props destructuring and didn't forward it
to their primary element. Enum already forwarded id via {...props}.
Popup-based editors (TextArea, Color, Slider) place id on their
trigger <Button>/<IconButton> — the always-visible element.
Input-based editors (CustomNumber, NumericInput) place id on the <Input>.
EnumButtonGroup places id on the <ButtonGroup> container.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
id threads from EditorProps → DateTimeEditor/DateEditor → DateInput → <Button> (the popup trigger, always visible in the DOM). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- common/api/core-react.api.md: updated to include id?: string added to ImageCheckBoxProps - .vscode/cSpell.json: add 'beforeunload' (pre-existing CHANGELOG word) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| } | ||
| data-testid="components-checkbox-editor" | ||
| data-testid={this.props.itemId ?? "components-checkbox-editor"} | ||
| id={this.props.propertyRecord?.property.name} |
There was a problem hiding this comment.
The id properties must be unique in a document.
Looks like the same property record (with the same property name, which results to duplicate ids) might be rendered multiple times e.g. in a property grid. @saskliutas
There was a problem hiding this comment.
Double checked PropertyRecords we created for iModel element property grid and it looks like we should create unique names inside that single property grid. But there might be custom property grids or I guess multiple iModel property grids present that might result in duplicating ids.
So I think we should think about how to ensure unique ids in document scope.
There was a problem hiding this comment.
Any idea how we can tackle this?

Summary
All property editor components (both legacy and new editor system) now set
id={propertyRecord.property.name}on their primary interactive element. This enables proper<label htmlFor>association and makes editors uniquely targetable in tests viagetByLabelText.Problem
The legacy editors relied on a shared, hardcoded
data-testid(e.g."components-select-editor"). When multiple editors of the same type appeared on the same page — e.g. two enum selects in a ToolSettings panel — they were indistinguishable. The new editor system had noidat all.Changes
New editor system (
editorSystem="new")id?: stringtoEditorPropsinTypes.tsid={propertyRecord.property.name}fromPropertyRecordEditor→CommittingEditor→EditorRenderer→ each concrete editorTextEditor,NumericEditor,EnumEditor,BooleanEditor,ToggleEditor,FallbackEditor,DateEditor,DateTimeEditor(viaDateInput)TextArea,Color,CustomNumber,EnumButtonGroup,NumericInput,SliderLegacy editor system
EnumEditor→idon<Select>BooleanEditor→idon<Checkbox>ToggleEditor→idon<ToggleSwitch>ImageCheckBoxEditor→idon<input type="checkbox">(required addingidprop toImageCheckBoxPropsin core-react)NumericInputEditor→idon<input>viaNumericEditorTextareaEditor,DateTimeEditor,SliderEditor→idon outer container div (popup-based; interactive content is not always in DOM)EnumButtonGroupEditor→idon container<div>IconEditor→idon<IconPickerButton>TextEditorandCustomNumberEditoralready hadidsetTesting
PropertyRecordEditor.test.tsxcovering label association viagetByLabelTextEditorRenderer.test.tsxverifyingidforwarding