From e763fb53506a49487567312ae7aa8e4d6a5627a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 05:30:07 +0000 Subject: [PATCH 1/2] fix(fields): deliver aria-invalid on 20 registered field widgets after validation failure (#3318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the #3222/#3306 pattern per widget: spread toDomProps(props) onto the widget's real focusable control (never a non-focusable wrapper, never a non-DOM Radix Root), then an explicit aria-invalid={!!error} after the spread so the widget's own computation from the published error slot wins. Delivered types (removed from the NOT_YET_DELIVERED ratchet ledger): multiselect, radio, checkboxes, tags, lookup, master_detail, user, owner, file, image, location, object, color, rating, code, avatar, address, geolocation, qrcode, object-ref. Placement notes: - radio: onto the Radix RadioGroup Root, a real
— the ARIA-designated carrier of aria-invalid for a set of radios. - multiselect / checkboxes / rating: onto every focusable option control; the wrapper div carries id/aria-describedby but never aria-invalid, and never `name` (only DOM-legal on form controls — the #3291 leak class). - lookup family: onto the shared trigger button (both picker branches). - object-ref / recipient-picker: Combobox now forwards standard button attributes to its focusable role="combobox" trigger (the #3306 SelectTrigger reasoning), so widgets finally have an element to deliver to. - file / image / avatar: onto the focusable upload control (dropzone / upload button) — the keyboard path to the hidden file input. Still on the ledger, each with its blocker documented in the ledger comment: formula, summary, auto_number, vector, grid, slider, signature, filter-condition, recipient-picker (the last two now deliver in their editable states but render a gate hint with no focusable control in the sweep's fresh-form state). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NVPjPzmmAJ2Ngtvgg5MSRa --- packages/components/src/custom/combobox.tsx | 17 ++++++- .../widget-aria-invalid-registry-e2e.test.tsx | 51 ++++++++++--------- packages/fields/src/widgets/AddressField.tsx | 15 +++++- packages/fields/src/widgets/AvatarField.tsx | 9 +++- .../fields/src/widgets/CheckboxesField.tsx | 18 +++++++ packages/fields/src/widgets/CodeField.tsx | 9 +++- packages/fields/src/widgets/ColorField.tsx | 11 +++- packages/fields/src/widgets/FileField.tsx | 14 ++++- .../src/widgets/FilterConditionField.tsx | 11 ++++ .../fields/src/widgets/GeolocationField.tsx | 10 +++- packages/fields/src/widgets/ImageField.tsx | 9 +++- packages/fields/src/widgets/LocationField.tsx | 9 +++- packages/fields/src/widgets/LookupField.tsx | 14 ++++- .../fields/src/widgets/MultiSelectField.tsx | 17 +++++++ packages/fields/src/widgets/ObjectField.tsx | 23 ++++++--- .../fields/src/widgets/ObjectRefField.tsx | 10 ++++ packages/fields/src/widgets/QRCodeField.tsx | 9 +++- packages/fields/src/widgets/RadioField.tsx | 11 ++++ packages/fields/src/widgets/RatingField.tsx | 19 ++++++- .../src/widgets/RecipientPickerField.tsx | 18 +++++++ packages/fields/src/widgets/TagsField.tsx | 12 ++++- 21 files changed, 269 insertions(+), 47 deletions(-) diff --git a/packages/components/src/custom/combobox.tsx b/packages/components/src/custom/combobox.tsx index a1e73a2ee8..21714333a4 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 3785239a5d..1be68a4766 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" />