Behavior
Accessibility contract
ariaLabelledByandariaDescribedByare already-composed, space-separated id strings. Attach them verbatim to the focusable element; do not join or parse them.Labelreceivesidfor the label element id. Apply it to the element that wraps the visible label text so controls can reference it viaaria-labelledby.- When
idis provided, set it on the primary focusable element. For composite widgets, choose the element that receives keyboard focus. ariaDescribedByreferences the renderedHelpandErrorsids. Keep those elements in the DOM when you render them. Legal and flyover content is not included inariaDescribedByby default, so ensure it remains accessible in your layout.- For
TabContainer, follow the WAI-ARIA tab pattern:role="tablist"on the container,role="tab"on each tab withid={buttonId},role="tabpanel"on each panel withid={panelId}, and wirearia-controls/aria-labelledby. - For custom select or multiselect widgets, follow standard combobox/listbox roles and keyboard interactions (Arrow keys, Enter/Space to select, Escape to close) when you are not using native inputs.
Controlled-value contract
All inputs are controlled; callbacks receive values, not DOM events.
- TextInput/TextArea: pass the raw string; empty string stays empty string.
- NumberInput/SpinnerInput/SliderInput: parse to
numberorundefined. Useundefinedwhen the field is empty or invalid; do not pass strings. - DateInput/DateTimeInput/TimeInput: treat the value as an opaque string and return it as entered. Do not normalize, format, or shift timezones.
- Select/Radio:
selectedOption = undefinedmeans no selection. CallonChange(token | undefined)for changes. - CheckboxList/MultiSelect: treat
selectedOptions[].tokenas the selected set. CallonSelectoronDeselectonce per user action and do not reorder the provided selections.
Disabled behavior
- When
disabledis true, render the UI as disabled and suppress all callbacks. - Native inputs: use the
disabledattribute. - Custom widgets: set
aria-disabled="true", remove from the tab order (tabIndex={-1}), and ignore pointer/keyboard events. - Disabled options should remain visible and announced as disabled.
- If an add/remove action is provided with
canAdd={false}orcanRemove={false}, render it disabled rather than hiding it.
Options and custom options lifecycle
- Tokens are stable for a given option or selection; it is safe to use them as React keys.
SelectedOptionItem.labelmay not match the current options list (legacy or custom values). Render it as provided.- The renderer may include disabled legacy options in
optionsto keep stored answers visible. Treat them as normal options, but disabled. specifyOtherOptionis an extra option row. When the user selects it, the renderer enters a custom-entry state and providescustomOptionForm.customOptionFormis present only while custom entry is active. Render it near the options list or in place of it; use itssubmitandcancelactions to finish or return to the list.isLoadingcan be true while options fetch. The renderer may also renderOptionsLoadingin the question scaffold; handle both without duplicating spinners.
Repeating items contract
AnswerListrenders one or moreAnswerScaffoldentries; whenonAddis provided it should render add-answer controls.AnswerScaffold.onRemoveis provided for repeating questions; render a remove action next to the control and disable it whencanRemoveis false.AnswerScaffold.errorsis provided for per-answer validation; render it near the answer content (it may render nothing).GroupListrenders a list of group instances (GroupScaffold) and can show an add control whenonAddis provided.GroupScaffoldshould render a remove action whenonRemoveis provided; usecanRemoveto disable.
