Skip to content
Open
9 changes: 1 addition & 8 deletions apps/www/src/components/linear-menu-demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,14 +207,6 @@ export default function LinearMenuDemo() {
const renderMenu = (items: MenuItem[], query: string) => {
const filteredItems = filterMenuItems(items, query);

if (searchQuery && filteredItems.length === 0) {
return (
<div>
<Text>No results</Text>
</div>
);
}

return filteredItems.map((item, index) => {
switch (item.type) {
case 'group':
Expand Down Expand Up @@ -274,6 +266,7 @@ export default function LinearMenuDemo() {
<Menu.Trigger render={<Button />}>Actions</Menu.Trigger>
<Menu.Content searchPlaceholder='Search'>
{renderMenu(menuData, searchQuery)}
<Menu.EmptyState>No results</Menu.EmptyState>
</Menu.Content>
</Menu>
</Flex>
Expand Down
24 changes: 24 additions & 0 deletions apps/www/src/content/docs/(overview)/upgrading.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ One section per release, newest first, with only the changes that need action
from you. The full record of every release, features and fixes included, is
on [GitHub releases](https://github.com/raystack/apsara/releases).

## Unreleased: menu search uses Base UI filtering

Search in `Menu`, `ContextMenu` and `Menubar` uses Base UI's menu filtering.
Props, slots and exported types do not change.

### 1. Update tests that query roles

In a searchable menu, items keep their menu roles: `menuitem`,
`menuitemcheckbox` and `menuitemradio`. They used to be `option`. The trigger
has `aria-haspopup="dialog"`, the popup is a `dialog`, and the input is a
`searchbox`. Update queries such as `getAllByRole('option')` to
`getAllByRole('menuitem')`.

### 2. Check items that have a `value`

An item with a `value` matches its `value` and its `children` text, not the
text in its icons. A `RadioItem` matches by its text, not its `value`.

### 3. Render `EmptyState` as a direct child of `Content`

In a searchable menu, `EmptyState` shows only when no item matches. Render it
as a direct child of `Content`, or inside a fragment there, so it sits next to
the list instead of inside it.

## Unreleased: date filters compare whole days

`DataView`, `DataTable` and `FilterChip` read and compare dates as calendar
Expand Down
1 change: 1 addition & 0 deletions apps/www/src/content/docs/components/context-menu/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export const autocompleteDemo = {
<Text>Right click here</Text>
</ContextMenu.Trigger>
<ContextMenu.Content searchPlaceholder="Search">
<ContextMenu.EmptyState>No results found</ContextMenu.EmptyState>
<ContextMenu.Group>
<ContextMenu.Label>Heading</ContextMenu.Label>
<ContextMenu.Item>Assign member...</ContextMenu.Item>
Expand Down
25 changes: 14 additions & 11 deletions apps/www/src/content/docs/components/context-menu/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Use `ContextMenu.Submenu`, `ContextMenu.SubmenuTrigger`, and `ContextMenu.Submen

Pass `autocomplete` to the ContextMenu root. Each menu manages its own autocomplete state.

- **`autocompleteMode="auto"`** is the default. Items filter as the user types, matched against each item's `value` prop or its children text.
- **`autocompleteMode="auto"`** is the default. Items filter as the user types. Each item matches by its text and, when set, its `value` prop. A `ContextMenu.RadioItem` matches by its text only, because its `value` is the selected value. Matching ignores case and accents.
- **Submenus** opt in separately: pass `autocomplete` to `ContextMenu.Submenu`.

<Demo data={autocompleteDemo} />
Expand Down Expand Up @@ -107,7 +107,7 @@ The container that holds the menu items. When autocomplete is enabled, renders a

Individual clickable options within the menu. Built on top of [Base UI ContextMenu.Item](https://base-ui.com/react/components/context-menu).

Renders as a `role='option'` when used in an autocomplete menu. By default, the item's `children` text content is used for matching, which can be overridden by passing a `value` prop.
In a searchable menu, the item's text is used for matching, including text in `leadingIcon` and `trailingIcon`. Pass a `value` prop to also match other text, such as a keyword. With a `value`, text in the icons no longer matches.

<auto-type-table path="./props.ts" name="ContextMenuItemProps" />

Expand All @@ -125,7 +125,7 @@ Groups related radio items and holds the selected value.

### RadioItem

A menu item that works like a radio button in a `ContextMenu.RadioGroup`. Renders a dot before the label when checked. The `value` prop is also used for search matching when it is a string.
A menu item that works like a radio button in a `ContextMenu.RadioGroup`. Renders a dot before the label when checked.

<auto-type-table path="./props.ts" name="ContextMenuRadioItemProps" />

Expand All @@ -137,13 +137,13 @@ A menu item that renders an `<a>` element. Pass a router link to `render` to use

### Group

A way to group related menu items together. When filtering is active, the group label is hidden.
A way to group related menu items together. When filtering is active, a group with no matching items is hidden.

<auto-type-table path="./props.ts" name="ContextMenuGroupProps" />

### Label

Renders a label in a menu group. This component should be wrapped with ContextMenu.Group so the `aria-labelledby` is correctly set on the group element. Hidden when filtering is active.
Renders a label in a menu group. This component should be wrapped with ContextMenu.Group so the `aria-labelledby` is correctly set on the group element.

<auto-type-table path="./props.ts" name="ContextMenuLabelProps" />

Expand All @@ -155,7 +155,7 @@ Visual divider between menu items or groups. Hidden when filtering is active.

### EmptyState

Placeholder content when there are no menu items to display.
Placeholder content when there are no menu items to display. In a searchable menu, it shows only when no item matches the search, and screen readers announce it. Render it as a direct child of `ContextMenu.Content`.

<auto-type-table path="./props.ts" name="ContextMenuEmptyStateProps" />

Expand All @@ -171,7 +171,7 @@ Supports its own `autocomplete` prop to enable search functionality within the s

The trigger item for a submenu. Renders with a trailing chevron icon by default. Accepts `leadingIcon` and `trailingIcon` props.

When inside a searchable parent menu, the `value` prop can be used for autocomplete matching.
When inside a searchable parent menu, the trigger matches by its text and, when set, its `value` prop. The root search does not search inside submenus.

<auto-type-table path="./props.ts" name="ContextMenuSubTriggerProps" />

Expand Down Expand Up @@ -217,10 +217,13 @@ Every rendered part carries a stable `data-slot` attribute for [styling and test
- Arrow keys move through items, Enter activates the highlighted item, and
ArrowRight/ArrowLeft open and close submenus.
- With `autocomplete`, the popup becomes a `role="dialog"` containing a search
input, items render as `role="option"`, and focus stays in the input while
arrow keys move the highlight, and Escape first closes an open submenu.
input (`role="searchbox"`, labelled by `searchPlaceholder`) and a
`role="menu"` list. Items keep their menu roles. Focus stays in the input
while arrow keys move the highlight. Escape in a submenu closes it and
returns focus to the parent input.
- `ContextMenu.Label` must be wrapped in `ContextMenu.Group` so the group is
labelled via `aria-labelledby`.
- Checkbox items use `role="menuitemcheckbox"` and radio items use
`role="menuitemradio"`, both with `aria-checked`. In a searchable menu they
render as `role="option"` and keep `aria-checked`.
`role="menuitemradio"`, both with `aria-checked`.
- In a searchable menu, `ContextMenu.EmptyState` is a `role="status"` live
region.
14 changes: 7 additions & 7 deletions apps/www/src/content/docs/components/context-menu/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export interface ContextMenuTriggerProps {
}

export interface ContextMenuContentProps {
/** Placeholder text for the autocomplete search input
/** Placeholder text and accessible name for the autocomplete search input
* @default "Search..."
*/
searchPlaceholder?: string;
Expand Down Expand Up @@ -102,7 +102,7 @@ export interface ContextMenuItemProps {
/** Whether the item is disabled */
disabled?: boolean;

/** Value of the item used for autocomplete matching. If not provided, `children` text content is used. */
/** Extra text to match in a searchable menu, along with the `children` text. */
value?: string;

/** Additional CSS class names */
Expand Down Expand Up @@ -153,7 +153,7 @@ export interface ContextMenuCheckboxItemProps {
/** Whether the item is disabled */
disabled?: boolean;

/** Value of the item used for autocomplete matching. If not provided, `children` text content is used. */
/** Extra text to match in a searchable menu, along with the `children` text. */
value?: string;

/** Additional CSS class names */
Expand Down Expand Up @@ -183,7 +183,7 @@ export interface ContextMenuRadioGroupProps {
}

export interface ContextMenuRadioItemProps {
/** Value of the radio item. Also used for autocomplete matching when it is a string. */
/** Value of the radio item. */
value: unknown;

/** Whether to close the menu when the item is clicked
Expand Down Expand Up @@ -222,7 +222,7 @@ export interface ContextMenuLinkItemProps {
/** Icon element to display after item text */
trailingIcon?: ReactNode;

/** Value of the item used for autocomplete matching. If not provided, `children` text content is used. */
/** Extra text to match in a searchable menu, along with the `children` text. */
value?: string;

/** Additional CSS class names */
Expand Down Expand Up @@ -283,12 +283,12 @@ export interface ContextMenuSubTriggerProps {
/** Icon element to display after trigger text. Defaults to a chevron right icon. */
trailingIcon?: ReactNode;

/** Value used for autocomplete matching when inside a searchable parent menu */
/** Extra text to match in a searchable parent menu, along with the `children` text. */
value?: string;
}

export interface ContextMenuSubContentProps {
/** Placeholder text for the autocomplete search input
/** Placeholder text and accessible name for the autocomplete search input
* @default "Search..."
*/
searchPlaceholder?: string;
Expand Down
6 changes: 2 additions & 4 deletions apps/www/src/content/docs/components/menu/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export const autocompleteDemo = {
Default Autocomplete
</Menu.Trigger>
<Menu.Content searchPlaceholder="Search">
<Menu.EmptyState>No results found</Menu.EmptyState>
<Menu.Group>
<Menu.Label>Heading</Menu.Label>
<Menu.Item>Assign member...</Menu.Item>
Expand Down Expand Up @@ -274,10 +275,6 @@ export const linearDemo = {
const renderMenu = (items: MenuItem[], query: string) => {
const filteredItems = filterMenuItems(items, query);

if (searchQuery && filteredItems.length === 0) {
return <div className="no-results">No results</div>;
}

return filteredItems.map((item, index) => {
switch (item.type) {
case "group":
Expand Down Expand Up @@ -335,6 +332,7 @@ export const linearDemo = {
</Menu.Trigger>
<Menu.Content searchPlaceholder="Search">
{renderMenu(menuData, searchQuery)}
<Menu.EmptyState>No results</Menu.EmptyState>
</Menu.Content>
</Menu>
);
Expand Down
32 changes: 17 additions & 15 deletions apps/www/src/content/docs/components/menu/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ To use a router link, pass it to `render`. The item keeps its layout and icons.

Pass `autocomplete` to the Menu root. Each menu manages its own autocomplete state.

- **`autocompleteMode="auto"`** is the default. Items filter as the user types, matched against each item's `value` prop or its children text.
- **`autocompleteMode="auto"`** is the default. Items filter as the user types. Each item matches by its text and, when set, its `value` prop. A `Menu.RadioItem` matches by its text only, because its `value` is the selected value. Matching ignores case and accents.
- **`autocompleteMode="manual"`** leaves filtering to you. Do it in `onInputValueChange`.
- **Submenus** opt in separately: pass `autocomplete` to `Menu.Submenu`.

Expand Down Expand Up @@ -141,7 +141,7 @@ The container that holds the menu items. When autocomplete is enabled, renders a

Individual clickable options within the menu. Built on top of [Base UI Menu.Item](https://base-ui.com/react/components/menu).

Renders as a `role='option'` when used in an autocomplete menu. By default, the item's `children` text content is used for matching, which can be overridden by passing a `value` prop.
In a searchable menu, the item's text is used for matching, including text in `leadingIcon` and `trailingIcon`. Pass a `value` prop to also match other text, such as a keyword. With a `value`, text in the icons no longer matches.

<auto-type-table path="./props.ts" name="MenuItemProps" />

Expand All @@ -159,7 +159,7 @@ Groups related radio items and holds the selected value.

### RadioItem

A menu item that works like a radio button in a `Menu.RadioGroup`. Renders a dot before the label when checked. The `value` prop is also used for search matching when it is a string.
A menu item that works like a radio button in a `Menu.RadioGroup`. Renders a dot before the label when checked.

<auto-type-table path="./props.ts" name="MenuRadioItemProps" />

Expand All @@ -171,13 +171,13 @@ A menu item that renders an `<a>` element. Built on [Base UI Menu.LinkItem](http

### Group

A way to group related menu items together. When filtering is active, the group label is hidden.
A way to group related menu items together. When filtering is active, a group with no matching items is hidden.

<auto-type-table path="./props.ts" name="MenuGroupProps" />

### Label

Renders a label in a menu group. This component should be wrapped with Menu.Group so the `aria-labelledby` is correctly set on the group element. Hidden when filtering is active.
Renders a label in a menu group. This component should be wrapped with Menu.Group so the `aria-labelledby` is correctly set on the group element.

<auto-type-table path="./props.ts" name="MenuLabelProps" />

Expand All @@ -189,7 +189,7 @@ Visual divider between menu items or groups. Hidden when filtering is active.

### EmptyState

Placeholder content when there are no menu items to display.
Placeholder content when there are no menu items to display. In a searchable menu, it shows only when no item matches the search, and screen readers announce it. Render it as a direct child of `Menu.Content`.

<auto-type-table path="./props.ts" name="MenuEmptyStateProps" />

Expand All @@ -205,7 +205,7 @@ Supports its own `autocomplete` prop to enable search functionality within the s

The trigger item for a submenu. Renders with a trailing chevron icon by default. Accepts `leadingIcon` and `trailingIcon` props.

When inside a searchable parent menu, the `value` prop can be used for autocomplete matching.
When inside a searchable parent menu, the trigger matches by its text and, when set, its `value` prop. The root search does not search inside submenus.

<auto-type-table path="./props.ts" name="MenuSubTriggerProps" />

Expand Down Expand Up @@ -250,14 +250,16 @@ Every rendered part carries a stable `data-slot` attribute for [styling and test
- Arrow keys move through items, Enter activates the highlighted item, Escape
closes the menu and returns focus to the trigger, and ArrowRight/ArrowLeft
open and close submenus.
- With `autocomplete`, the popup becomes a `role="dialog"` containing a search
input, items render as `role="option"`, and focus stays in the input while
arrow keys move the highlight, and ArrowRight or Enter opens a highlighted
submenu, and Escape first closes an open submenu.
- With `autocomplete`, the trigger has `aria-haspopup="dialog"` and the popup
becomes a `role="dialog"` containing a search input (`role="searchbox"`,
labelled by `searchPlaceholder`) and a `role="menu"` list. Items keep their
menu roles. Focus stays in the input while arrow keys move the highlight.
ArrowRight or Enter opens a highlighted submenu and moves focus to its first
item, or to its own search input when it is searchable. Escape closes the
submenu and returns focus to the parent input.
- `Menu.Label` must be wrapped in `Menu.Group` so the group is labelled via
`aria-labelledby`.
- `Menu.CheckboxItem` uses `role="menuitemcheckbox"` and `Menu.RadioItem` uses
`role="menuitemradio"`, both with `aria-checked`. In a searchable menu they
render as `role="option"` and keep `aria-checked`.
- `Menu.LinkItem` renders an `<a>` with `role="menuitem"`, or `role="option"`
in a searchable menu.
`role="menuitemradio"`, both with `aria-checked`.
- `Menu.LinkItem` renders an `<a>` with `role="menuitem"`.
- In a searchable menu, `Menu.EmptyState` is a `role="status"` live region.
14 changes: 7 additions & 7 deletions apps/www/src/content/docs/components/menu/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export interface MenuTriggerProps {
}

export interface MenuContentProps {
/** Placeholder text for the autocomplete search input
/** Placeholder text and accessible name for the autocomplete search input
* @default "Search..."
*/
searchPlaceholder?: string;
Expand Down Expand Up @@ -104,7 +104,7 @@ export interface MenuItemProps {
/** Whether the item is disabled */
disabled?: boolean;

/** Value of the item used for autocomplete matching. If not provided, `children` text content is used. */
/** Extra text to match in a searchable menu, along with the `children` text. */
value?: string;

/** Additional CSS class names */
Expand Down Expand Up @@ -140,7 +140,7 @@ export interface MenuCheckboxItemProps {
/** Whether the item is disabled */
disabled?: boolean;

/** Value of the item used for autocomplete matching. If not provided, `children` text content is used. */
/** Extra text to match in a searchable menu, along with the `children` text. */
value?: string;

/** Additional CSS class names */
Expand Down Expand Up @@ -170,7 +170,7 @@ export interface MenuRadioGroupProps {
}

export interface MenuRadioItemProps {
/** Value of the radio item. Also used for autocomplete matching when it is a string. */
/** Value of the radio item. */
value: unknown;

/** Whether to close the menu when the item is clicked
Expand Down Expand Up @@ -209,7 +209,7 @@ export interface MenuLinkItemProps {
/** Icon element to display after item text */
trailingIcon?: ReactNode;

/** Value of the item used for autocomplete matching. If not provided, `children` text content is used. */
/** Extra text to match in a searchable menu, along with the `children` text. */
value?: string;

/** Additional CSS class names */
Expand Down Expand Up @@ -285,12 +285,12 @@ export interface MenuSubTriggerProps {
/** Icon element to display after trigger text. Defaults to a chevron right icon. */
trailingIcon?: ReactNode;

/** Value used for autocomplete matching when inside a searchable parent menu */
/** Extra text to match in a searchable parent menu, along with the `children` text. */
value?: string;
}

export interface MenuSubContentProps {
/** Placeholder text for the autocomplete search input
/** Placeholder text and accessible name for the autocomplete search input
* @default "Search..."
*/
searchPlaceholder?: string;
Expand Down
1 change: 1 addition & 0 deletions apps/www/src/content/docs/components/menubar/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export const autocompleteDemo = {
<Menu autocomplete>
<Menu.Trigger>Actions</Menu.Trigger>
<Menu.Content searchPlaceholder="Search actions...">
<Menu.EmptyState>No results found</Menu.EmptyState>
<Menu.Group>
<Menu.Label>General</Menu.Label>
<Menu.Item>Assign member...</Menu.Item>
Expand Down
Loading