Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/www/src/content/docs/components/accordion/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`
1 change: 1 addition & 0 deletions apps/www/src/content/docs/components/collapsible/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`
20 changes: 20 additions & 0 deletions packages/react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 —
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<BasicCombobox multiple value={value} />);
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();
Expand Down
8 changes: 4 additions & 4 deletions packages/react/components/combobox/combobox-root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface ComboboxContextValue<Value = string> {
hasItems: boolean;
inputId: string;
inputContainerRef: RefObject<HTMLDivElement | null>;
value: Value | Value[] | null | undefined;
value: Value | readonly Value[] | null | undefined;
getLabel: (value: Value) => string;
}

Expand Down Expand Up @@ -58,8 +58,8 @@ export interface SingleComboboxProps<Value = string, Item = Value>
export interface MultipleComboboxProps<Value = string, Item = Value>
extends BaseComboboxRootProps<Value, Item> {
multiple: true;
value?: Value[];
defaultValue?: Value[];
value?: readonly Value[];
defaultValue?: readonly Value[];
onValueChange?: (value: Value[]) => void;
}

Expand Down Expand Up @@ -95,7 +95,7 @@ export const ComboboxRoot = <Value extends unknown | unknown[], Item = Value>({
state: 'inputValue'
});
const [computedValue, setValueUnwrapped] = useControlled<
Value | Value[] | null
Value | readonly Value[] | null
>({
controlled: providedValue,
default: defaultValue ?? null,
Expand Down
25 changes: 25 additions & 0 deletions packages/react/components/select/__tests__/select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Select multiple value={value}>
<Select.Trigger>{TRIGGER_TEXT}</Select.Trigger>
<Select.Content>
{FRUIT_OPTIONS.map(option => (
<Select.Item key={option.value} value={option.value}>
{option.label}
</Select.Item>
))}
</Select.Content>
</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']);
});
});

describe('Keyboard Navigation', () => {
Expand Down
8 changes: 4 additions & 4 deletions packages/react/components/select/select-root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Loading