Skip to content
Draft
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
66 changes: 65 additions & 1 deletion packages/dev/s2-docs/pages/react-aria/Menu.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,52 @@ import {Button} from 'vanilla-starter/Button';
</MenuTrigger>
```

### Asynchronous loading

Use [renderEmptyState](#empty-state) to display a loading indicator during initial load. To enable infinite scrolling, render a `<MenuLoadMoreItem>` at the end of the menu. Use whatever data fetching library you prefer — this example uses `useAsyncList` from `react-stately`.

```tsx render
"use client";
import {MenuTrigger, Menu, MenuItem, MenuLoadMoreItem} from 'vanilla-starter/Menu';
import {Button} from 'vanilla-starter/Button';
import {Collection} from 'react-aria-components/Collection';
import {useAsyncList} from 'react-aria-components/useAsyncList';

interface Character {
name: string;
birth_year: string;
}

function AsyncMenuExample() {
let list = useAsyncList<Character>({
async load({signal, cursor}) {
if (cursor) cursor = cursor.replace(/^http:\/\//i, 'https://');
let res = await fetch(cursor || 'https://swapi.py4e.com/api/people/', {signal});
let json = await res.json();
return {items: json.results, cursor: json.next};
}
});

return (
<MenuTrigger>
<Button>Select Character</Button>
<Menu
aria-label="Characters"
renderEmptyState={() => list.loadingState === 'loading' ? 'Loading…' : 'No characters found'}>
<Collection items={list.items}>
{(item) => <MenuItem id={item.name}>{item.name}</MenuItem>}
</Collection>
{/*- begin highlight -*/}
<MenuLoadMoreItem
onLoadMore={list.loadMore}
isLoading={list.loadingState === 'loadingMore'} />
{/*- end highlight -*/}
</Menu>
</MenuTrigger>
);
}
```

### Links

Use the `href` prop on a `<MenuItem>` to create a link.
Expand Down Expand Up @@ -313,6 +359,19 @@ By default, links are rendered as an `<a>` element. Use the `render` prop to int
} />
```

### Empty state

```tsx render hideImports
"use client";
import {Menu} from 'vanilla-starter/Menu';

<Menu
aria-label="Actions"
renderEmptyState={() => 'No actions available.'}>
{[]}
</Menu>
```

### Autocomplete

Popovers can include additional components as siblings of a menu. This example uses an [Autocomplete](Autocomplete) with a [SearchField](SearchField) to let the user filter the items.
Expand Down Expand Up @@ -559,7 +618,7 @@ import {Button} from 'react-aria-components/Button';

<Anatomy />

```tsx links={{MenuTrigger: '#menutrigger', Button: 'Button', Popover: 'Popover', Menu: '#menu', MenuItem: '#menuitem', Separator: 'Separator', MenuSection: '#menusection', SubmenuTrigger: '#submenutrigger', SelectionIndicator: 'selection#animated-selectionindicator'}}
```tsx links={{MenuTrigger: '#menutrigger', Button: 'Button', Popover: 'Popover', Menu: '#menu', MenuItem: '#menuitem', Separator: 'Separator', MenuSection: '#menusection', SubmenuTrigger: '#submenutrigger', SelectionIndicator: 'selection#animated-selectionindicator', MenuLoadMoreItem: '#menuloadmoreitem'}}
<MenuTrigger>
<Button />
<Popover>
Expand All @@ -581,6 +640,7 @@ import {Button} from 'react-aria-components/Button';
<Menu />
</Popover>
</SubmenuTrigger>
<MenuLoadMoreItem />
</Menu>
</Popover>
</MenuTrigger>
Expand All @@ -605,3 +665,7 @@ import {Button} from 'react-aria-components/Button';
### SubmenuTrigger

<PropTable component={docs.exports.SubmenuTrigger} links={docs.links} showDescription />

### MenuLoadMoreItem

<PropTable component={docs.exports.MenuLoadMoreItem} links={docs.links} showDescription />
2 changes: 2 additions & 0 deletions packages/react-aria-components/exports/Menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import 'client-only';
export {
Menu,
MenuItem,
MenuLoadMoreItem,
MenuTrigger,
MenuSection,
MenuContext,
Expand All @@ -29,6 +30,7 @@ export type {
MenuProps,
MenuItemProps,
MenuItemRenderProps,
MenuLoadMoreItemProps,
MenuTriggerProps,
SubmenuTriggerProps,
MenuSectionProps
Expand Down
2 changes: 2 additions & 0 deletions packages/react-aria-components/exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export {
export {
Menu,
MenuItem,
MenuLoadMoreItem,
MenuTrigger,
MenuSection,
MenuContext,
Expand Down Expand Up @@ -406,6 +407,7 @@ export type {
MenuProps,
MenuItemProps,
MenuItemRenderProps,
MenuLoadMoreItemProps,
MenuTriggerProps,
SubmenuTriggerProps,
MenuSectionProps
Expand Down
2 changes: 1 addition & 1 deletion packages/react-aria-components/src/GridList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -827,7 +827,7 @@ export interface GridListLoadMoreItemProps
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
* element.
*
* @default 'react-aria-GridListLoadMoreItem'
* @default 'react-aria-GridListLoadingIndicator'
*/
className?: string;
/**
Expand Down
2 changes: 1 addition & 1 deletion packages/react-aria-components/src/ListBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ export interface ListBoxLoadMoreItemProps
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
* element.
*
* @default 'react-aria-ListBoxLoadMoreItem'
* @default 'react-aria-ListBoxLoadingIndicator'
*/
className?: string;
/**
Expand Down
95 changes: 94 additions & 1 deletion packages/react-aria-components/src/Menu.tsx

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basically ListBox's implementation

Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
BaseCollection,
CollectionNode,
ItemNode,
LoaderNode,
SectionNode
} from 'react-aria/private/collections/BaseCollection';
import {
Expand All @@ -40,6 +41,7 @@ import {
Provider,
RenderProps,
SlotProps,
StyleProps,
StyleRenderProps,
useContextProps,
useRenderProps,
Expand Down Expand Up @@ -79,7 +81,12 @@ import {
} from '@react-types/shared';
import {FocusScope} from 'react-aria/FocusScope';
import {HeaderContext} from './Header';
import {inertValue} from 'react-aria/private/utils/inertValue';
import {KeyboardContext} from './Keyboard';
import {
LoadMoreSentinelProps,
useLoadMoreSentinel
} from 'react-aria/private/utils/useLoadMoreSentinel';
import {mergeProps} from 'react-aria/mergeProps';
import {MultipleSelectionState} from 'react-stately/useMultipleSelectionState';
import {Node} from '@react-types/shared';
Expand Down Expand Up @@ -281,7 +288,7 @@ export interface MenuProps<T>
* @default 'react-aria-Menu'
*/
className?: ClassNameOrFunction<MenuRenderProps>;
/** Provides content to display when there are no items in the list. */
/** Provides content to display when there are no items in the menu. */
renderEmptyState?: () => ReactNode;
/** Whether the menu should close when the menu item is selected. */
shouldCloseOnSelect?: boolean;
Expand Down Expand Up @@ -631,3 +638,89 @@ export const MenuItem = /*#__PURE__*/ createLeafComponent(ItemNode, function Men
</ElementType>
);
});

export interface MenuLoadMoreItemProps
extends
Omit<LoadMoreSentinelProps, 'collection' | 'direction'>,
StyleProps,
DOMRenderProps<'div', undefined>,
GlobalDOMAttributes<HTMLDivElement> {
/**
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
* element.
*
* @default 'react-aria-MenuLoadingIndicator'
*/
className?: string;
/**
* The load more spinner to render when loading additional items.
*/
children?: ReactNode;
/**
* Whether or not the loading spinner should be rendered or not.
*/
isLoading?: boolean;
}

export const MenuLoadMoreItem = createLeafComponent(
LoaderNode,
function MenuLoadingIndicator(
props: MenuLoadMoreItemProps,
ref: ForwardedRef<HTMLDivElement>,
item: Node<object>
) {
let state = useContext(MenuStateContext)!;
let {isLoading, onLoadMore, scrollOffset, ...otherProps} = props;

let sentinelRef = useRef<HTMLDivElement>(null);
let memoedLoadMoreProps = useMemo(
() => ({
onLoadMore,
collection: state?.collection,
sentinelRef,
scrollOffset
}),
[onLoadMore, scrollOffset, state?.collection]
);
useLoadMoreSentinel(memoedLoadMoreProps, sentinelRef);
let renderProps = useRenderProps({
...otherProps,
id: undefined,
children: item.rendered,
defaultClassName: 'react-aria-MenuLoadingIndicator',
values: undefined
});

let itemProps = {
// For Android talkback
tabIndex: -1
};

return (
<>
{/* Always render the sentinel. For now onus is on the user for styling when using flex + gap (this would introduce a gap even though it doesn't take room) */}
{/* @ts-ignore - compatibility with React < 19 */}
<div style={{position: 'relative', width: 0, height: 0}} inert={inertValue(true)}>
<div
data-testid="loadMoreSentinel"
ref={sentinelRef}
style={{position: 'absolute', height: 1, width: 1}}
/>
</div>
{isLoading && renderProps.children && (
<>
{/* oxlint-disable jsx-a11y/role-has-required-aria-props -- loader row is not selectable */}
<dom.div
{...mergeProps(filterDOMProps(props, {global: true}), itemProps)}
{...renderProps}
role="menuitem"
ref={ref as ForwardedRef<HTMLDivElement>}>
{renderProps.children}
</dom.div>
{/* oxlint-enable jsx-a11y/role-has-required-aria-props */}
</>
)}
</>
);
}
);
2 changes: 1 addition & 1 deletion packages/react-aria-components/src/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2342,7 +2342,7 @@ export interface TableLoadMoreItemProps
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
* element.
*
* @default 'react-aria-TableLoadMoreItem'
* @default 'react-aria-TableLoadingIndicator'
*/
className?: string;
/**
Expand Down
3 changes: 2 additions & 1 deletion packages/react-aria-components/src/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1075,7 +1075,7 @@ export interface TreeLoadMoreItemProps
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
* element. A function may be provided to compute the class based on component state.
*
* @default 'react-aria-TreeLoadMoreItem'
* @default 'react-aria-TreeLoader'
*/
className?: ClassNameOrFunction<TreeLoadMoreItemRenderProps>;
/**
Expand Down Expand Up @@ -1124,6 +1124,7 @@ export const TreeLoadMoreItem = createLeafComponent(LoaderNode, function TreeLoa
...otherProps,
id: undefined,
children: item.rendered,
// TODO: this isn't consistent with other components
defaultClassName: 'react-aria-TreeLoader',
values: {
level
Expand Down
Loading