From 6bea41a614fa1baf7b1a4b2c98e57b99fc5b2140 Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Tue, 3 Nov 2026 18:49:56 +0530 Subject: [PATCH 1/6] fix: [combobox, select] accept readonly arrays for multiple values Base UI 1.9.0 accepts readonly arrays for multiple values. The wrappers now accept them for value and defaultValue too. onValueChange still receives a mutable array, so existing handlers keep their types. --- .../combobox/__tests__/combobox.test.tsx | 7 ++++++ .../components/combobox/combobox-root.tsx | 8 +++--- .../select/__tests__/select.test.tsx | 25 +++++++++++++++++++ .../react/components/select/select-root.tsx | 8 +++--- 4 files changed, 40 insertions(+), 8 deletions(-) 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, From 4087324465fb46fa59c6b7f26c7d12f7e0ed2f47 Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Tue, 3 Nov 2026 18:49:58 +0530 Subject: [PATCH 2/6] chore: [accordion, collapsible] describe disabled triggers in the docs Base UI 1.9.0 puts the native disabled attribute on these triggers, so a disabled trigger leaves the Tab order. The docs did not say so. --- apps/www/src/content/docs/components/accordion/index.mdx | 1 + apps/www/src/content/docs/components/collapsible/index.mdx | 1 + 2 files changed, 2 insertions(+) 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` From c2a5d544a83c0607c62df218d41b85125754d2b5 Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Tue, 3 Nov 2026 18:49:58 +0530 Subject: [PATCH 3/6] chore: [changelog] note the base ui 1.9.0 changes Lists the changes from Base UI 1.9.0 that an app can see, including the ones from the upgrade itself, which has no entry. --- packages/react/CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) 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 — From 7bce69a15704eb0aac3d5e9f97acdd9b22e7e39b Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Thu, 5 Nov 2026 10:53:41 +0530 Subject: [PATCH 4/6] refactor: [combobox, select] trim readonly array changes The combobox context keeps its mutable value type, since only the public props need to accept readonly arrays. The select test checks the checked state of the first option. --- packages/react/components/combobox/combobox-root.tsx | 2 +- .../react/components/select/__tests__/select.test.tsx | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/react/components/combobox/combobox-root.tsx b/packages/react/components/combobox/combobox-root.tsx index cc40f1986..bc405a5c8 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 | readonly Value[] | null | undefined; + value: Value | Value[] | null | undefined; getLabel: (value: Value) => string; } diff --git a/packages/react/components/select/__tests__/select.test.tsx b/packages/react/components/select/__tests__/select.test.tsx index ab45b93bc..bc0c67f97 100644 --- a/packages/react/components/select/__tests__/select.test.tsx +++ b/packages/react/components/select/__tests__/select.test.tsx @@ -308,13 +308,10 @@ describe('Select', () => { ); 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']); + expect( + within(screen.getAllByRole('option')[0]).getByRole('checkbox') + ).toHaveAttribute('aria-checked', 'true'); }); }); From 4c22f16a6cd32f10066016929c8e2dea3c804eff Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Thu, 5 Nov 2026 10:53:44 +0530 Subject: [PATCH 5/6] chore: [changelog] shorten the base ui 1.9.0 entry The submenu bullet names the input types that toggle the submenu and the focus guard case. --- packages/react/CHANGELOG.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 63c0922fc..35eef4e3f 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -4,8 +4,7 @@ ### 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. +Apsara uses `@base-ui/react` 1.9.0 and `@base-ui/utils` 0.5.0. #### Changes @@ -13,9 +12,9 @@ changes below are the ones an app can see. `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. +- In `Menu`, `ContextMenu` and `Menubar`, pressing the trigger of an open + submenu with touch or the keyboard closes it, and a submenu closes when + focus moves back to its trigger 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. From aa3be44dcaac3445c135cee663ecbcefacee9416 Mon Sep 17 00:00:00 2001 From: Shreyag02 Date: Thu, 5 Nov 2026 10:56:33 +0530 Subject: [PATCH 6/6] chore: [combobox] show readonly arrays in the docs props value and defaultValue accept readonly arrays for multiple selection. --- apps/www/src/content/docs/components/combobox/props.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/www/src/content/docs/components/combobox/props.ts b/apps/www/src/content/docs/components/combobox/props.ts index 406ed533f..c81c0ebba 100644 --- a/apps/www/src/content/docs/components/combobox/props.ts +++ b/apps/www/src/content/docs/components/combobox/props.ts @@ -7,15 +7,15 @@ export interface ComboboxRootProps { /** The controlled value of the combobox. * For single selection: string - * For multiple selection: string[] + * For multiple selection: readonly string[] */ - value?: string | string[]; + value?: string | readonly string[]; /** The default value of the combobox (uncontrolled). * For single selection: string - * For multiple selection: string[] + * For multiple selection: readonly string[] */ - defaultValue?: string | string[]; + defaultValue?: string | readonly string[]; /** Callback fired when the value changes. * For single selection: (value: string) => void