Skip to content
Merged
Show file tree
Hide file tree
Changes from 80 commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
4c54a2f
initialize RAC submenu
reidbarber Jan 3, 2024
75edf1f
use cached children
reidbarber Jan 11, 2024
dbc2cf8
cleanup
reidbarber Jan 11, 2024
8bdd954
use one useRenderProps
reidbarber Jan 11, 2024
5d7c9a6
types
reidbarber Jan 11, 2024
da103f0
cleanup
reidbarber Jan 11, 2024
0100aa5
fix trigger ref
reidbarber Jan 11, 2024
bd95e34
typescript
reidbarber Jan 11, 2024
2e6b265
add chevron style to submenu trigger
reidbarber Jan 11, 2024
c3cb412
types
reidbarber Jan 11, 2024
a61d762
cleanup
reidbarber Jan 11, 2024
acc9144
pass submenu ref
reidbarber Jan 11, 2024
afb26c0
Merge remote-tracking branch 'origin/main' into rac-submenu-2
reidbarber Jan 11, 2024
f7bbc67
add submenu story to menu story file
reidbarber Jan 11, 2024
9537ac5
fix nested case
reidbarber Jan 12, 2024
5eeaefe
cleanup
reidbarber Jan 12, 2024
e41c580
add nested story
reidbarber Jan 12, 2024
0fab5de
fix trigger attribute
reidbarber Jan 12, 2024
74c94e5
add tests
reidbarber Jan 12, 2024
93eae69
update jsdoc
reidbarber Jan 12, 2024
0ac5950
add docs
reidbarber Jan 12, 2024
531f880
update storybook styles
reidbarber Jan 12, 2024
0353aad
Merge branch 'main' into rac-submenu-2
reidbarber Jan 12, 2024
78bb709
fix docs types
reidbarber Jan 12, 2024
46ee9ca
Merge branch 'rac-submenu-2' of https://github.com/adobe/react-spectr…
reidbarber Jan 12, 2024
444a051
cleanup tests
reidbarber Jan 12, 2024
268619e
move imports in docs closer to example
reidbarber Jan 12, 2024
e7c1207
typescript
reidbarber Jan 12, 2024
a11f588
add example to homepage
reidbarber Jan 12, 2024
a7405f9
close all submenus if underlay is clicked
reidbarber Jan 12, 2024
10862de
revert autoformatting from headwind
reidbarber Jan 16, 2024
5193143
add to small example, fix imports and labels
reidbarber Jan 16, 2024
2461c4c
render via portal
reidbarber Jan 17, 2024
9f215a2
add render props and data attributes
reidbarber Jan 17, 2024
774a524
add || undefined
reidbarber Jan 17, 2024
4a6064d
add many items example
reidbarber Jan 17, 2024
2f1c070
add keys for storybook
reidbarber Jan 17, 2024
ff34be5
imrove styles on docs page
reidbarber Jan 17, 2024
7c45618
improve homepage styles
reidbarber Jan 17, 2024
de49d82
move chevron into item wrapper
reidbarber Jan 17, 2024
ffcbbd4
Merge remote-tracking branch 'origin/main' into rac-submenu-2
reidbarber Jan 17, 2024
e00dd27
ts lint
reidbarber Jan 17, 2024
d6509a2
lint
reidbarber Jan 17, 2024
d8fdc04
story style update
reidbarber Jan 17, 2024
087315b
style isSubmenuOpen states
reidbarber Jan 18, 2024
2613335
add dynamic example to dcs
reidbarber Jan 19, 2024
c52c8c5
don't allow submenu trigger to be a link
reidbarber Jan 19, 2024
e9777b6
clarify submenutrigger children order in types
reidbarber Jan 19, 2024
5aee52b
use proper hover state from useHover
reidbarber Jan 19, 2024
6cd4870
update tests
reidbarber Jan 19, 2024
db7f438
update docs to clarify children
reidbarber Jan 19, 2024
ed2fb5c
remove top -5 offset
reidbarber Jan 22, 2024
292fac0
add disabled submenu example
reidbarber Jan 22, 2024
05c89e3
fix iOS VO submenu closing issue
reidbarber Jan 22, 2024
14f5584
add Select story with many items
reidbarber Jan 22, 2024
5db5718
move docs chevron styles from inline to CSS snippet
reidbarber Jan 23, 2024
8e1f11c
export SubmenuTrigger directly without forwardRef
reidbarber Jan 23, 2024
63ee502
update render props
reidbarber Jan 23, 2024
dcea397
fix indentation
reidbarber Jan 23, 2024
544f5e1
add has-submenu to tailwind plugin
reidbarber Jan 23, 2024
d136d0a
remove offset
reidbarber Jan 23, 2024
b15ba52
Merge remote-tracking branch 'origin/main' into rac-submenu-2
reidbarber Jan 23, 2024
e2df1d2
Merge branch 'main' into rac-submenu-2
reidbarber Jan 24, 2024
c4bc063
fix homepage example styles
reidbarber Jan 25, 2024
e9fd987
fix docs example dark mode style
reidbarber Jan 25, 2024
4b06860
focus trigger on escape
reidbarber Jan 26, 2024
85c38bf
add test for focus menu trigger after submenu closes via escape
reidbarber Jan 26, 2024
d4d3058
Merge remote-tracking branch 'origin/main' into rac-submenu-2
reidbarber Jan 29, 2024
161a84a
add alpha tag to docs
reidbarber Jan 30, 2024
66e5b97
add JSDoc
reidbarber Jan 31, 2024
3dfab71
fix restoring focus to menu trigger after nested submenu closed via E…
reidbarber Jan 31, 2024
51e34f0
lint
reidbarber Jan 31, 2024
e208c99
Merge branch 'main' into rac-submenu-2
reidbarber Jan 31, 2024
4389c5c
Render popover outside menu item div
devongovett Feb 1, 2024
f85a0b8
support a custom delay prop in hook and RAC
reidbarber Feb 1, 2024
77c5cab
remove from homepage and tailwind starter for now
reidbarber Feb 2, 2024
7c9ac79
Merge branch 'main' into rac-submenu-2
LFDanLu Feb 2, 2024
7174f6d
revert auto-formatting
reidbarber Feb 2, 2024
b8ab586
Merge remote-tracking branch 'origin/main' into rac-submenu-2
reidbarber Feb 2, 2024
31df572
Merge branch 'rac-submenu-2' of https://github.com/adobe/react-spectr…
reidbarber Feb 2, 2024
efc1c46
Merge branch 'main' into rac-submenu-2
reidbarber Feb 5, 2024
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
11 changes: 8 additions & 3 deletions packages/@react-aria/menu/src/useSubmenuTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ export interface AriaSubmenuTriggerProps {
/** Ref of the menu that contains the submenu trigger. */
parentMenuRef: RefObject<HTMLElement>,
/** Ref of the submenu opened by the submenu trigger. */
submenuRef: RefObject<HTMLElement>
submenuRef: RefObject<HTMLElement>,
/**
* The delay time in milliseconds for the submenu to appear after hovering over the trigger.
* @default 200
*/
delay?: number
}

interface SubmenuTriggerProps extends AriaMenuItemProps {
Expand Down Expand Up @@ -59,7 +64,7 @@ export interface SubmenuTriggerAria<T> {
* @param ref - Ref to the submenu trigger element.
*/
export function UNSTABLE_useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, state: SubmenuTriggerState, ref: RefObject<FocusableElement>): SubmenuTriggerAria<T> {
let {parentMenuRef, submenuRef, type = 'menu', isDisabled, node} = props;
let {parentMenuRef, submenuRef, type = 'menu', isDisabled, node, delay = 200} = props;
let submenuTriggerId = useId();
let overlayId = useId();
let {direction} = useLocale();
Expand Down Expand Up @@ -188,7 +193,7 @@ export function UNSTABLE_useSubmenuTrigger<T>(props: AriaSubmenuTriggerProps, st
if (!openTimeout.current) {
openTimeout.current = setTimeout(() => {
onSubmenuOpen();
}, 200);
}, delay);
}
} else if (!isHovered) {
cancelOpenTimeout();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,10 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
if (!isVirtualized) {
scrollIntoView(scrollRef.current, element);
}
scrollIntoViewport(element, {containingElement: ref.current});
// Avoid scroll in iOS VO, since it may cause overlay to close (i.e. RAC submenu)
if (modality !== 'virtual') {
scrollIntoViewport(element, {containingElement: ref.current});
}
}
}

Expand Down
140 changes: 137 additions & 3 deletions packages/react-aria-components/docs/Menu.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {Layout} from '@react-spectrum/docs';
export default Layout;

import docs from 'docs:react-aria-components';
import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable} from '@react-spectrum/docs';
import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable, VersionBadge} from '@react-spectrum/docs';
import styles from '@react-spectrum/docs/src/docs.css';
import packageData from 'react-aria-components/package.json';
import Anatomy from './MenuAnatomy.svg';
Expand All @@ -38,7 +38,7 @@ type: component

<HeaderInfo
packageData={packageData}
componentNames={['MenuTrigger', 'Menu']}
componentNames={['MenuTrigger', 'Menu', 'SubmenuTrigger']}
sourceData={[
{type: 'W3C', url: 'https://www.w3.org/WAI/ARIA/apg/patterns/menu/'}
]} />
Expand Down Expand Up @@ -226,7 +226,18 @@ function MyMenuButton<T extends object>({label, children, ...props}: MyMenuButto
}

function MyItem(props: MenuItemProps) {
return <MenuItem {...props} className={({isFocused, isSelected}) => `my-item ${isFocused ? 'focused' : ''}`} />
return (
<MenuItem {...props} className={({isFocused, isSelected, isOpen}) => `my-item ${isFocused ? 'focused' : ''} ${isOpen ? 'open' : ''}`}>
{({hasSubmenu}) => (
<>
{props.children}
{hasSubmenu && (
<svg className="chevron" viewBox="0 0 24 24"><path d="m9 18 6-6-6-6" /></svg>
)}
</>
)}
</MenuItem>
);
}

<MyMenuButton label="Edit">
Expand Down Expand Up @@ -254,6 +265,23 @@ function MyItem(props: MenuItemProps) {
background: #e70073;
color: white;
}
&.open:not(.focused) {
background: rgba(192, 192, 192, 0.3);
color: var(--text-color);
}
.chevron {
width: 20;
height: 20;
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2;
position: absolute;
right: 0;
top: 0;
height: 100%;
}
}

@media (forced-colors: active) {
Expand Down Expand Up @@ -658,12 +686,106 @@ function Example() {
}
```

## Submenus <VersionBadge version="alpha" style={{marginLeft: 4, verticalAlign: 'bottom'}} />

Submenus can be created by wrapping an item and a submenu in a `SubmenuTrigger`. The `SubmenuTrigger` accepts exactly two children: the first child should be the `MenuItem` which triggers opening of the submenu, and second child should be the `Popover` containing the submenu.

### Static

```tsx example
import {Menu, Popover, SubmenuTrigger} from 'react-aria-components';

<MyMenuButton label="Actions">
<MyItem>Cut</MyItem>
<MyItem>Copy</MyItem>
<MyItem>Delete</MyItem>
<SubmenuTrigger>
<MyItem aria-label="Share">Share</MyItem>
<Popover>
<Menu>
<MyItem>SMS</MyItem>
<MyItem>Twitter</MyItem>
<SubmenuTrigger>
<MyItem aria-label="Email">Email</MyItem>
<Popover>
<Menu>
<MyItem>Work</MyItem>
<MyItem>Personal</MyItem>
</Menu>
</Popover>
</SubmenuTrigger>
</Menu>
</Popover>
</SubmenuTrigger>
</MyMenuButton>
```

### Dynamic

You can define a recursive function to render the nested menu items dynamically.

```tsx example
import {Menu, Popover, SubmenuTrigger} from 'react-aria-components';

let items = [
{id: 'cut', name: 'Cut'},
{id: 'copy', name: 'Copy'},
{id: 'delete', name: 'Delete'},
{id: 'share', name: 'Share', children: [
{id: 'sms', name: 'SMS'},
{id: 'twitter', name: 'Twitter'},
{id: 'email', name: 'Email', children: [
{id: 'work', name: 'Work'},
{id: 'personal', name: 'Personal'},
]}
]}
];

<MyMenuButton label="Actions" items={items}>
{function renderSubmenu(item) {
if (item.children) {
return (
<SubmenuTrigger>
<MyItem key={item.name} aria-label={item.name}>{item.name}</MyItem>
<Popover>
<Menu items={item.children}>
{(item) => renderSubmenu(item)}
</Menu>
</Popover>
</SubmenuTrigger>
);
} else {
return <MyItem key={item.name}>{item.name}</MyItem>;
}
}}
</MyMenuButton>
```

<details>
<summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show CSS</summary>

```css
.react-aria-Popover[data-trigger=SubmenuTrigger][data-placement="right"] {
margin-left: -5px;
}

.react-aria-Popover[data-trigger=SubmenuTrigger][data-placement="left"] {
margin-right: -5px;
}
```

</details>

## Props

### MenuTrigger

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

### SubmenuTrigger

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

### Button

A `<Button>` accepts its contents as `children`. Other props such as `onPress` and `isDisabled` will be set by the `MenuTrigger`.
Expand Down Expand Up @@ -812,6 +934,18 @@ Within a MenuTrigger, the popover will have the `data-trigger="MenuTrigger"` att
}
```

Within a SubmenuTrigger, the popover will have the `data-trigger="SubmenuTrigger"` attribute, which can be used to define submenu-specific styles.

```css render=false
.react-aria-Popover[data-trigger=SubmenuTrigger][data-placement="right"] {
transform: translateX(-5px);
}

.react-aria-Popover[data-trigger=SubmenuTrigger][data-placement="left"] {
transform: translateX(5px);
}
```

### Menu

A `Menu` can be targeted with the `.react-aria-Menu` CSS selector, or by overriding with a custom `className`.
Expand Down
25 changes: 25 additions & 0 deletions packages/react-aria-components/example/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ html {
.menu, .group {
padding: 0;
list-style: none;
overflow-y: auto;
max-height: inherit;
}

.item {
Expand Down Expand Up @@ -43,11 +45,20 @@ html {
}
}

.item[data-disabled] {
opacity: 0.4;
}

.item.focused {
background: gray;
color: white;
}

.item.open:not(.focused) {
background: lightslategray;
color: white;
}

.item.item.hovered {
background: lightsalmon;
color: white;
Expand All @@ -58,6 +69,20 @@ html {
color: white;
}

.item[data-has-submenu]::after {
content: '›';
content: '›' / '';
justify-self: end;
}

.popover[data-trigger=SubmenuTrigger][data-placement="right"] {
margin-left: -8px;
}

.popover[data-trigger=SubmenuTrigger][data-placement="left"] {
margin-right: -8px;
}

.wrapper {
display: flex;
flex-direction: column;
Expand Down
2 changes: 2 additions & 0 deletions packages/react-aria-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@
"@internationalized/string": "^3.2.0",
"@react-aria/focus": "^3.16.0",
"@react-aria/interactions": "^3.20.1",
"@react-aria/menu": "^3.12.0",
"@react-aria/toolbar": "3.0.0-beta.1",
"@react-aria/utils": "^3.23.0",
"@react-stately/menu": "^3.6.0",
"@react-stately/table": "^3.11.4",
"@react-stately/utils": "^3.9.0",
"@react-types/form": "^3.7.1",
Expand Down
Loading