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,