diff --git a/apps/www/src/content/docs/components/accordion/index.mdx b/apps/www/src/content/docs/components/accordion/index.mdx index db12ac4fb..53cae5139 100644 --- a/apps/www/src/content/docs/components/accordion/index.mdx +++ b/apps/www/src/content/docs/components/accordion/index.mdx @@ -107,4 +107,5 @@ Every rendered part carries a stable `data-slot` attribute for [styling and test - Trigger elements use `aria-expanded` to indicate open/closed state - Supports keyboard navigation with Enter and Space to toggle items - Content panels use `aria-controls` and `aria-labelledby` for association +- A disabled item's trigger has the native `disabled` attribute, so it is left out of the Tab order - Respects motion preferences: icon rotation and panel expand/collapse motion is enabled only when `prefers-reduced-motion: no-preference` diff --git a/apps/www/src/content/docs/components/collapsible/index.mdx b/apps/www/src/content/docs/components/collapsible/index.mdx index 694af2e68..009a11e2f 100644 --- a/apps/www/src/content/docs/components/collapsible/index.mdx +++ b/apps/www/src/content/docs/components/collapsible/index.mdx @@ -85,4 +85,5 @@ Every rendered part carries a stable `data-slot` attribute for [styling and test - The Trigger uses `aria-expanded` to indicate the open/closed state of the panel. - Supports keyboard interaction with Enter and Space to toggle the panel. - The Panel is automatically associated with the Trigger via `aria-controls`. +- A disabled Trigger has the native `disabled` attribute, so it is left out of the Tab order. - Respects motion preferences: panel expand/collapse motion is enabled only when `prefers-reduced-motion: no-preference` diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index fa5ec7f72..63c0922fc 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -2,6 +2,26 @@ ## Unreleased +### Base UI 1.9.0 + +Apsara uses `@base-ui/react` 1.9.0 and `@base-ui/utils` 0.5.0. The +changes below are the ones an app can see. + +#### Changes + +- A disabled `Accordion` or `Collapsible` trigger has the native + `disabled` attribute. It is dimmed and left out of the Tab order. +- A multiple `Combobox` or `Select` accepts a readonly array for `value` + and `defaultValue`. `onValueChange` still receives a mutable array. +- In `Menu`, `ContextMenu` and `Menubar`, activating the trigger of an + open submenu closes it, and a submenu closes when focus leaves it + through its focus guard. +- The `ScrollArea` viewport has no `role="presentation"`. +- `Checkbox`, `Radio` and `Switch` take their accessible name from + `aria-label` when they also have an associated label. +- A `Dialog`, `AlertDialog` or `Drawer` opened from code through a + handle is not linked to a trigger that did not open it. + ### Icons — lucide replaces @radix-ui/react-icons (BREAKING) Apsara names every icon with a stable key that does not name a library — diff --git a/packages/react/components/combobox/__tests__/combobox.test.tsx b/packages/react/components/combobox/__tests__/combobox.test.tsx index be67e1770..4f6f53d35 100644 --- a/packages/react/components/combobox/__tests__/combobox.test.tsx +++ b/packages/react/components/combobox/__tests__/combobox.test.tsx @@ -605,6 +605,13 @@ describe('Combobox chips', () => { expect(screen.getByText('banana')).toBeInTheDocument(); }); + it('accepts a readonly array as the value', () => { + const value = ['apple', 'banana'] as const; + render(); + expect(screen.getByText('apple')).toBeInTheDocument(); + expect(screen.getByText('banana')).toBeInTheDocument(); + }); + it('removes the last chip on Backspace in an empty input', async () => { const user = userEvent.setup(); const handleValueChange = vi.fn(); diff --git a/packages/react/components/combobox/combobox-root.tsx b/packages/react/components/combobox/combobox-root.tsx index 3c22bca55..cc40f1986 100644 --- a/packages/react/components/combobox/combobox-root.tsx +++ b/packages/react/components/combobox/combobox-root.tsx @@ -19,7 +19,7 @@ interface ComboboxContextValue { hasItems: boolean; inputId: string; inputContainerRef: RefObject; - value: Value | Value[] | null | undefined; + value: Value | readonly Value[] | null | undefined; getLabel: (value: Value) => string; } @@ -58,8 +58,8 @@ export interface SingleComboboxProps export interface MultipleComboboxProps extends BaseComboboxRootProps { multiple: true; - value?: Value[]; - defaultValue?: Value[]; + value?: readonly Value[]; + defaultValue?: readonly Value[]; onValueChange?: (value: Value[]) => void; } @@ -95,7 +95,7 @@ export const ComboboxRoot = ({ state: 'inputValue' }); const [computedValue, setValueUnwrapped] = useControlled< - Value | Value[] | null + Value | readonly Value[] | null >({ controlled: providedValue, default: defaultValue ?? null, diff --git a/packages/react/components/select/__tests__/select.test.tsx b/packages/react/components/select/__tests__/select.test.tsx index 82647b7bd..ab45b93bc 100644 --- a/packages/react/components/select/__tests__/select.test.tsx +++ b/packages/react/components/select/__tests__/select.test.tsx @@ -291,6 +291,31 @@ describe('Select', () => { await flushMicrotasks(); expect(handleValueChange).toHaveBeenCalledTimes(1); }); + + it('accepts a readonly array as the value', async () => { + const value = ['apple', 'banana'] as const; + const user = userEvent.setup(); + render( + + ); + await openSelect(user); + await flushMicrotasks(); + + const checked = screen + .getAllByRole('option') + .map(option => within(option).getByRole('checkbox')) + .map(checkbox => checkbox.getAttribute('aria-checked')); + expect(checked).toEqual(['true', 'true', 'false', 'false', 'false']); + }); }); describe('Keyboard Navigation', () => { diff --git a/packages/react/components/select/select-root.tsx b/packages/react/components/select/select-root.tsx index 5a0321ecf..5d78591f5 100644 --- a/packages/react/components/select/select-root.tsx +++ b/packages/react/components/select/select-root.tsx @@ -25,7 +25,7 @@ interface CommonProps { } interface SelectContextValue extends CommonProps { - value?: string | string[]; + value?: string | readonly string[]; registerItem: (item: ItemType) => void; unregisterItem: (value: string) => void; multiple: boolean; @@ -79,9 +79,9 @@ export interface SingleSelectProps extends BaseSelectProps { export interface MultipleSelectProps extends BaseSelectProps { multiple: true; - value?: string[]; + value?: readonly string[]; onValueChange?: (value: string[]) => void; - defaultValue?: string[]; + defaultValue?: readonly string[]; } export type SelectRootProps = SingleSelectProps | MultipleSelectProps; @@ -112,7 +112,7 @@ export const SelectRoot = (props: SelectRootProps) => { const resolvedRequired = required ?? fieldContext?.required; const [computedValue, setValueUnwrapped] = useControlled< - string | string[] | undefined + string | readonly string[] | undefined >({ controlled: providedValue, default: defaultValue,