diff --git a/.changeset/widget-aria-invalid-delivery-batch.md b/.changeset/widget-aria-invalid-delivery-batch.md new file mode 100644 index 000000000..4a5c9c729 --- /dev/null +++ b/.changeset/widget-aria-invalid-delivery-batch.md @@ -0,0 +1,33 @@ +--- +"@object-ui/fields": patch +"@object-ui/components": patch +--- + +20 more registered field widgets now announce a failed validation to assistive +tech: `multiselect`, `radio`, `checkboxes`, `tags`, `lookup`, `master_detail`, +`user`, `owner`, `file`, `image`, `location`, `object`, `color`, `rating`, +`code`, `avatar`, `address`, `geolocation`, `qrcode` and `object-ref` carry +`aria-invalid="true"` on their real focusable control after a validation +failure, where before the red message rendered while a screen reader was told +nothing (objectui#3318, the registry-wide gap objectui#3306's sweep measured). + +Each widget follows the objectui#3222/#3306 pattern: the `toDomProps(props)` +whitelist spread goes onto the control the user actually focuses — the input, +the lookup trigger button, the radiogroup (`role="radiogroup"` is the +ARIA-designated carrier for a set of radios), every chip/checkbox/star of the +composite option widgets, the upload dropzone/button — followed by an explicit +`aria-invalid={!!error}` computed from the published `error` slot. Wrapper +`
`s never carry the state, and `name` is withheld from non-form-control +elements (the objectui#3291 leak class). + +`Combobox` (`@object-ui/components`) now accepts standard button attributes +and forwards them to its focusable `role="combobox"` trigger, giving +combobox-based widgets an element to deliver `aria-invalid` / +`aria-describedby` to — the same seam objectui#3306 opened on +`SelectTrigger`. + +Nine types remain on the objectui#3318 ratchet ledger with their blockers +documented there (`formula`/`summary`/`auto_number`/`vector` render no +focusable control; `grid`, `slider`, `signature` need component-level design; +`filter-condition`/`recipient-picker` deliver in their editable states but +render a dependency-gate hint with no control in a fresh form). diff --git a/packages/components/src/custom/combobox.tsx b/packages/components/src/custom/combobox.tsx index a1e73a2ee..21714333a 100644 --- a/packages/components/src/custom/combobox.tsx +++ b/packages/components/src/custom/combobox.tsx @@ -32,7 +32,18 @@ export interface ComboboxOption { label: string } -export interface ComboboxProps { +/** + * Beyond its own named props, the combobox accepts standard button attributes + * (`id`, `aria-*`, `data-*`, handlers, …) and forwards them to the trigger — + * the focusable `role="combobox"` button a user and their screen reader + * actually interact with. Without this seam a field widget rendering a + * Combobox had no element to deliver `aria-invalid` / `aria-describedby` to + * after a validation failure (objectui#3318; the same reason #3306 routes the + * select's pass-through onto `SelectTrigger`). `value` / `onChange` are + * omitted: this component's own `value` / `onValueChange` contract owns them. + */ +export interface ComboboxProps + extends Omit, "value" | "onChange"> { options: ComboboxOption[] value?: string onValueChange?: (value: string) => void @@ -52,6 +63,7 @@ export function Combobox({ emptyText = "No option found.", className, disabled, + ...triggerProps }: ComboboxProps) { const [open, setOpen] = React.useState(false) @@ -59,6 +71,9 @@ export function Combobox({
@@ -79,6 +89,7 @@ export function AddressField({ value, onChange, field, readonly, ...props }: Fie onChange={(e) => handleFieldChange('state', e.target.value)} placeholder="CA" disabled={readonly || props.disabled} + aria-invalid={!!error} /> @@ -93,6 +104,7 @@ export function AddressField({ value, onChange, field, readonly, ...props }: Fie onChange={(e) => handleFieldChange('zipCode', e.target.value)} placeholder="94102" disabled={readonly || props.disabled} + aria-invalid={!!error} /> @@ -105,6 +117,7 @@ export function AddressField({ value, onChange, field, readonly, ...props }: Fie onChange={(e) => handleFieldChange('country', e.target.value)} placeholder="United States" disabled={readonly || props.disabled} + aria-invalid={!!error} /> diff --git a/packages/fields/src/widgets/AvatarField.tsx b/packages/fields/src/widgets/AvatarField.tsx index 3785239a5..1be68a476 100644 --- a/packages/fields/src/widgets/AvatarField.tsx +++ b/packages/fields/src/widgets/AvatarField.tsx @@ -2,12 +2,13 @@ import React from 'react'; import { Avatar, AvatarFallback, AvatarImage, Button } from '@object-ui/components'; import { Upload, X } from 'lucide-react'; import { FieldWidgetComponentProps } from './types'; +import { toDomProps } from './toDomProps'; /** * Avatar field widget - provides an avatar/profile picture uploader * Supports image URLs or file uploads */ -export function AvatarField({ value, onChange, field, readonly, ...props }: FieldWidgetComponentProps) { +export function AvatarField({ value, onChange, field, readonly, error, ...props }: FieldWidgetComponentProps) { const [isHovered, setIsHovered] = React.useState(false); const fileInputRef = React.useRef(null); @@ -92,11 +93,17 @@ export function AvatarField({ value, onChange, field, readonly, ...props }: Fiel className="hidden" />