Skip to content

Commit a2146aa

Browse files
LFDanLureidbarber
andauthored
Submenu support (#4976)
* adding stories and chevron * planning * fix sections in items * scuffed experimentation with getting sub menu to render using existing ContextualHelpTrigger and ListBox to see what is needed for the sub menu * notes from api review * reset state of some files and add tentative SubMenuTrigger implementation SubMenuTrigger hasnt been tested, just grabs stuff from MenuTrigger and ContextualHelpTrigger that I deemed suitable * update static story and dynamic story for testing * fix dynamic story * update sub menu interactions add support for opening submenu via Enter/Space/ArrowRight and closing submenu via ArrowLeft. Also adds the proper autoFocus behavior based on the interaction that opened the submenu * fix onOpen undef and test onBlurWithin misc cleanup as well * use useMenuTrigger to get proper submenutrigger id and aria-labelledby for submenu * add onAction inheritence between submenus and test aria-controls * fix crash * explored alt dynamic render function, update rest of the existing stories * making all menus close on menu action trigger * making sub menu item ArrowLeft not close all menus * fix user provided onClose not firing and ContextualHelpTrigger isUnavailable=false user provided onClose wasnt firing because we were always sourcing it from the MenuTriggers context onClose. ContextualHelpTrigger also needed additional logic to not render a sub menu if isUnavailable is false * add complex and align/placement stories * fix lint and circular dependency * add onSubMenuClose instead of having a isSubMenu bool and passing the whole trigger state to useMenu this makes the api a bit nicer * add first batch of tests * prevent SubMenuTrigger items from being selected current approach has the SubMenuTrigger adding a prop to the Item let selectionManager know that it should be non-selectable. Other approaches would involve additional changes to useSelectableItem to disable press props but keep keyboard navigation * make Escape close all menus without triggering user provided onClose * Add stories + tests for user provided handlers and make onCloseAllMenus a private prop dont really feel like users will need to pass in something for onCloseAllMenus so made it a hidden prop. Also moved it into useMenu so it doesnt matter if the user is focused on the menu or a menu item when hitting Esc * Adding selection and onOpenChange tests also made onSubMenuClose a private prop * fixng tests and lint * fix last failing tests * fix mutated submenu trigger keys * fix tests * use menuProps from useMenuTrigger for submenu props and restore new useMenu props * tentative workaround to close all menus when clicking outside * fix tests didnt actually need to let propagation continue * add onAction to stories and test for aria-controls * Removing several MenuTrigger props from SubMenu as per discussion we decided that things like isOpen/positioning/etc can be added later if need be * make submenu trigger use menuitem role and adding additional tests submenu triggers cant be checked so they shouldnt have a menuitemcheckbox or menuitemradio role * finally fix sub menu width the div in the menu that we portal the next submenu to needs to be wide enough that the submenu items can render as wide as they can be. Technically the width applied should be (submenu max width + parent menu width) * fix infinite repositioning loop the portal container div for submenus needed to fit exactly within the screen width in both directions so the sub menu size would resolve properly in the case where we need to flip the positioning * rough refactor progress * fix ArrowRight closing too many menus and preventing close when focusing submenutrigger still closes too many submenus if you hover a submenu trigger and then hit arrow right... * fix aria attribute propagation to submenu trigger * save state * move stuff into hooks * stopgap lint and test fixes * attempting to close submenu whenever focus moves out of its active scope * fixing submenu close behavior when moving focus from trigger or into lower menu * adding/fixing tests for new submenu hover behavior focus now doesnt move to menu if opened via hover * Update ContextualHelpTrigger to work with useSubMenuTrigger * fix tabbing in ContextualHelpTrigger refactor * fixing tabbing behavior between submenu/trigger and retaining menu item focus on hover fixes break in behavior where tabbing from level 3 submenu was closing all submenus. Also fixes it so that a user hovering a level 1 submenutrigger keeps focus on the trigger even after all the remaining submenus close * updating tests for userEvent library upgrade * clearing expandedKeyStack when user clicks underlay and comment cleanup * adding test for tab/shift tabbing * updating menu tree state expanded keys on submenu open/close needed a way for a menu to know that it has a open submenu so that it can set aria-hidden on itself, otherwise Safari VO had a hard time shifting focus to the newly focused submenu item. Also helpful for knowing when to set contain on a menu. Alternative would be having useMenu accept menuTreeState and initialize a level and look up in the expandedKeysStack if it owns a open submenu, open to discussion if that is the way we wanna go * fix test test was failing because it mimick run flow in browser. We needed to have useSelectableCollections focus call trigger the blur on the submenutrigger and update expandedKeys to modify the containment of the submenu before the keydown makes it to the keydown handlers of FocusScope * inital progress with Tray submenu * adding rough styling and removing popover specific logic from tray * fix lint * update context name and fix ContextualHelpTrigger behavior when focus is moved to the subdialog trigger * adding tray tests and fixing submenu overflow * Fix tray autofocus behavior with touch and add remaining tests * replacing submenu aria-labelledby with aria-label due to parent menu being aria-hidden follows the approach shown in https://www.w3.org/WAI/ARIA/apg/patterns/menubar/examples/menubar-navigation/. Aria-hidden on the parent menu is needed to force Safari VO to move to newly opened submenus * move submenu styles to css files * fixing submenu cross offset * fix tests * fix ContextualHelpTrigger dialog offset and hover behavior when entered via a open submenu this unfortunately breaks another interaction case but to handle later * update labeling so it uses hook * fix tests * partial cleanup and fix build * Revert "update labeling so it uses hook" This reverts commit 66ce969. * Revert "fix tests" This reverts commit 2c6921a. * converting hooks to UNSTABLE and other cleanup * adding sample chromatic test with interactions * add tray tests * fix chromatic tray interactions * Making ContextualHelpTrigger tray modal a tray instead * Make SubMenu Tray a dialog with heading * partial fix for stacking drop shadow issue * Adding useKeyboard to useMenu and updating tests * Expanding useMenuItem types and fixing Esc and keyup behavior * merge useMenuState into useMenuTriggerState * Stop updating tree state expanded keys in favor of returning level from submenu state and looking up key in expandedKeyStack from discussion, we wanted to get rid of the reliance on tree state * Making it so a menu trigger that is set as the selectedKey doesnt actually get autofocused IMO we should only be moving focus to truely selectable options in useSelectableCollection, previously was autofocusing lvl 2 Item 3 in https://reactspectrum.blob.core.windows.net/reactspectrum/82c23dbc122a23d05f2ba3e3968d45d36d82fc2b/storybook/index.html?path=/story/menutrigger-submenu--sub-menu-selection&providerSwitcher-express=false * forgot to remove the state combination * fix styles for ContextualHelpTrigger tray experience * reaname SubMenu -> Submenu * fix git casing * Submenus: prevent pointer events on menu if moving towards submenu (#5082) * initial conversion * initialize safe area submenu hook * lint * lint peerDeps * disable if submenu isn't open * add comment for angle calculation * cleanup direction check * remove menuRef * update lastProcessedTime * fix style props merge * early return for touch and pen pointerType * add timeout * fire pointerenter * add padding to top/bottom angles * allow 2 invalid movements * add padding as const * use pointerover to trigger underlying item * up the timeout to 1s * cleanup from merge * store direction as ref and rename * type * bail early when pointer reaches submenu * clear timeout if bailing early * move up cleartimeout * fix test * add skip * fix ts strict * add interaction modality check * remove skip * revert mock * attempt to fix contextualhelp * fix ref * make submenu ref optional * fix logic and allow disabling * attempt to fix refs for unavailable menu item * fix from merge conflict * conditionally render overlay to avoid dup refs * add missing textValue * merge * fix from merge * fix safe pointer movement for submenus --------- Co-authored-by: Daniel Lu <[email protected]> * rename subMenu -> submenu * fix types * update useSafelyMouseToSubmenu to set menu style * remove dup style * fix tests * lint * fix ts * code review * fixing submenu tray header positioning and getting rid of non-issue todos * improve submenu safe area experience * add story with many items * fix dup key * partial progress on getting rid of onExit has changes that makes tab close all submenus and move focus to prev/next element. Still need to get tabbing working in contextualhelp dialog, ideally have it contain focus but still allow user to freely hover another menu item without FocusScope highjacking focus back to the subdialog trigger * sharing TrayHeaderWrapper with SubMenu and ContextualHelpTrigger also made an attempt to have the ContextualHelpTrigger contain focus, but that breaks the case where the user hovers the sub menu item in the root menu aka focus gets moved back to the subdialog trigger instead of the menu item the user hovered * don't measure for safe area when outside of menu * typo * removing contains on ContextualHelpDialog the contains interfered with the hover focus that a user could perform on the root menu items when the subdialog was open so we are omitting for now * clean up more todos * fix menu overflow behavior and rendering for an available ContextualHelpTrigger * initial docs * add dynamic example to docs * adding prop table for SubmenuTrigger and additional copy * fix mobile w/ keyboard focus behavior when closing submenu * fixing tray experience on desktop with small screens when emulating a small screen with desktop mode active in dev tools, hovering a unavailable menu item was opening it and clicking on a submenu trigger didnt open it * review comments * addressing review comments makes submenu level 0th indexed and getting rid of rafs by just moving hidden trays out of view * getting rid of weird double border * Fix SubMenu tray scrolling if there are many items * get rid of erroneous border and background color and fix tray height * restore drop shadow * make contextual help dialog full width * fixing tests * fix scrollbars appearing when closing all submenus comment for posterity: the popover container needs to be as wide as the viewport and offset to be flush with the viewport so that the popover positioning w/ flipping works. Also required to allow the menu width to grow to fit its contents but still be constrained by the max height. We remove the div when the root menu is closed so that scroll bars dont flash on to the screen because the leftOffset calculation isnt perfect due to a transform applied on left/right aligned menus that throws off the getBoundingClientRect calc * forgot to save * fix tests * fix focus being lost to body when using hidden dismiss button in Safari manually moves focus to the parent menu when MacOS Safari Voiceover user triggers the hidden dismiss button. to discuss with team if this belongs in aria * remove transition from submenu and rename onDismiss for clarity brings submenu appearance more inline with native submenus and rename onDismiss prop so people know it is specifically for the dismiss button interaction * fix keyboard scrolling for long menus and tabindex keep it so disabled menu items dont have a tabindex still * merge overlay and popover props * unwrap ref in ContextualHelpTrigger * fix event propagation when in contextual help or on submenutrigger fixes case where arrowLeft was propagating through contextualhelp dialogs which we only want to have be closed via ESC. Also fixes event propagation for default cases for submenu triggers so that we can call it in useMenuItem useKeyboard. Now we still properly close only the submenu trigger menu when focus is on the trigger and the submenu is open * forgot to remove comment * fixes from test session updates propName to type as per review, fixes VO dismiss button with contextual help and reintroduces the transitions back to submenu since there were some weird transforms that cause visible jerks in position when closing the submenus without the visibility transition. * remove extraneous flush sync now that we completely leave the menu on Tab * omit selectableitem props from submenutrigger items instead of informing selection manager directly that the submenu item is a non selectable item, opt for change in useMenuItem instead since it feels a bit too specific to include in selection manager. Still need onFocus though so we still update the tracked key when user clicks on a submenu trigger in a Tray * update submenu unmount animation removed the transform for submenus and replaced it with a offset calculated for Popover instead so that we dont get the weird jerk animation when the submenu unmounts * fix case where submenus dont properly open/close when rapidly opening/closing the menu with keyboard if the user presses left/right rapidly enough, the submenu doesnt actually unmount due to being in the middle of a transition animation, meaning focus wasnt being handled by useSelectableCollection focus useEffect which only run on mount. This fixes that by running focus and openSubmenu in those cases * fix tests * fix alignment * clearing timeout and adding comment for offset * clear autoclose timeout if user moves mouse before it fires --------- Co-authored-by: Reid Barber <[email protected]>
1 parent ce963f7 commit a2146aa

32 files changed

+3102
-231
lines changed

packages/@adobe/spectrum-css-temp/components/contextualhelp/index.css

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
--spectrum-dialog-padding-x: var(--spectrum-contextualhelp-dialog-padding);
3535
--spectrum-dialog-padding-y: var(--spectrum-contextualhelp-dialog-padding);
3636

37+
&.react-spectrum-ContextualHelp-dialog--isMobile {
38+
width: 100%;
39+
}
3740
.react-spectrum-ContextualHelp-content {
3841
margin-top: var(--spectrum-global-dimension-static-size-100);
3942
}

packages/@adobe/spectrum-css-temp/components/menu/index.css

+57-5
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ governing permissions and limitations under the License.
4040
overflow: hidden;
4141
max-height: 100%;
4242
display: inline-flex;
43+
flex-direction: column;
4344
border-radius: var(--spectrum-alias-border-radius-regular);
4445
border-style: solid;
4546
border-width: var(--spectrum-popover-border-size);
@@ -50,6 +51,11 @@ governing permissions and limitations under the License.
5051
}
5152

5253
.spectrum-Menu {
54+
/*
55+
Menu border radius + 1 to match XD designs for submenu offset. No calc use so
56+
getComputedStyle + getPropertyValue actually returns a pixel value rather than the calc string.
57+
*/
58+
--spectrum-submenu-offset-distance: var(--spectrum-global-dimension-size-65);
5359
text-align: start;
5460
display: block;
5561

@@ -186,17 +192,17 @@ governing permissions and limitations under the License.
186192

187193
.spectrum-Menu-itemGrid {
188194
display: grid;
189-
grid-template-columns: calc(var(--spectrum-selectlist-option-padding) - var(--spectrum-selectlist-border-size-key-focus)) auto 1fr auto auto auto var(--spectrum-selectlist-option-padding);
195+
grid-template-columns: calc(var(--spectrum-selectlist-option-padding) - var(--spectrum-selectlist-border-size-key-focus)) auto 1fr auto auto auto auto var(--spectrum-selectlist-option-padding);
190196
/*
191197
Renamed from padding-y to padding-height to fix docs issue where fallback var replaced this value
192198
(due to old spectrum-css postcss-custom-properties-custom-mapping plugin).
193199
*/
194200
grid-template-rows: var(--spectrum-selectlist-option-padding-height) 1fr auto var(--spectrum-selectlist-option-padding-height);
195201
grid-template-areas:
196-
". . . . . . ."
197-
". icon text checkmark end keyboard ."
198-
". icon description checkmark end keyboard ."
199-
". . . . . . .";
202+
". . . . . . . ."
203+
". icon text checkmark end keyboard chevron ."
204+
". icon description checkmark end keyboard chevron ."
205+
". . . . . . . .";
200206
}
201207

202208
.spectrum-Menu-item.is-selectable {
@@ -238,3 +244,49 @@ governing permissions and limitations under the License.
238244
font-family: var(--spectrum-font-family-base);
239245
unicode-bidi: plaintext;
240246
}
247+
248+
.spectrum-Menu-chevron {
249+
margin-inline-end: calc(var(--spectrum-global-dimension-size-50) * -1);
250+
grid-area: chevron;
251+
justify-self: end;
252+
align-self: flex-start;
253+
padding-inline-start: var(--spectrum-global-dimension-size-300);
254+
}
255+
256+
.spectrum-Menu-wrapper {
257+
overflow: auto;
258+
259+
.spectrum-Submenu-wrapper {
260+
display: flex;
261+
flex-direction: column;
262+
overflow: auto;
263+
}
264+
265+
&.spectrum-Menu-wrapper--isMobile {
266+
width: 100%;
267+
border: none;
268+
background-color: unset;
269+
270+
&.is-expanded {
271+
position: absolute;
272+
left: 100%;
273+
height: 0;
274+
border: 0;
275+
}
276+
277+
.spectrum-Submenu-headingWrapper {
278+
display: flex;
279+
margin: 4px 4px 0px 4px;
280+
281+
.spectrum-Submenu-heading {
282+
display: flex;
283+
margin-top: 7px;
284+
margin-bottom: 0px;
285+
margin-inline-start: 4px;
286+
font-weight: var(--spectrum-selectlist-heading-text-font-weight);
287+
font-size: var(--spectrum-dialog-title-text-size);
288+
line-height: var(--spectrum-dialog-title-text-line-height);
289+
}
290+
}
291+
}
292+
}

packages/@adobe/spectrum-css-temp/components/menu/skin.css

+17
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ governing permissions and limitations under the License.
3131
background-color: var(--spectrum-selectlist-background-color);
3232
}
3333

34+
.spectrum-Submenu-headingWrapper {
35+
--spectrum-heading-subtitle3-text-color: var(--spectrum-global-color-gray-900);
36+
}
37+
3438
.spectrum-Menu-item {
3539
background-color: var(--spectrum-selectlist-option-background-color);
3640
color: var(--spectrum-selectlist-option-text-color);
@@ -83,6 +87,19 @@ governing permissions and limitations under the License.
8387
.spectrum-Menu-description {
8488
color: var(--spectrum-global-color-gray-700);
8589
}
90+
91+
.spectrum-Submenu-popover {
92+
filter: unset;
93+
-webkit-filter: unset;
94+
transition: opacity var(--spectrum-global-animation-duration-100) ease-in-out,
95+
visibility 0ms linear var(--spectrum-global-animation-duration-100);
96+
transform: none;
97+
}
98+
99+
.spectrum-Submenu-heading {
100+
color: var(--spectrum-heading-subtitle3-text-color);
101+
}
102+
86103
@media (forced-colors: active) {
87104
.spectrum-Menu-divider {
88105
background-color: CanvasText;

packages/@adobe/spectrum-css-temp/components/overlay/index.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ governing permissions and limitations under the License.
2424

2525
.spectrum-overlay--open {
2626
visibility: visible;
27-
/* In Edge (pre chromium), a stacking context is formed for opacity less then 1, and then its removed for 1.
27+
/* In Edge (pre chromium), a stacking context is formed for opacity less then 1, and then its removed for 1.
2828
It causes a rendering flicker that is visible when css transition is applied. */
2929
opacity: 0.9999;
3030

packages/@react-aria/menu/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ export {useMenuTrigger} from './useMenuTrigger';
1414
export {useMenu} from './useMenu';
1515
export {useMenuItem} from './useMenuItem';
1616
export {useMenuSection} from './useMenuSection';
17+
export {UNSTABLE_useSubmenuTrigger} from './useSubmenuTrigger';
1718

1819
export type {AriaMenuProps} from '@react-types/menu';
1920
export type {AriaMenuTriggerProps, MenuTriggerAria} from './useMenuTrigger';
2021
export type {AriaMenuOptions, MenuAria} from './useMenu';
2122
export type {AriaMenuItemProps, MenuItemAria} from './useMenuItem';
2223
export type {AriaMenuSectionProps, MenuSectionAria} from './useMenuSection';
24+
export type {AriaSubmenuTriggerProps, SubmenuTriggerAria} from './useSubmenuTrigger';

packages/@react-aria/menu/src/useMenu.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import {AriaMenuProps} from '@react-types/menu';
14-
import {DOMAttributes, Key, KeyboardDelegate} from '@react-types/shared';
14+
import {DOMAttributes, Key, KeyboardDelegate, KeyboardEvents} from '@react-types/shared';
1515
import {filterDOMProps, mergeProps} from '@react-aria/utils';
1616
import {RefObject} from 'react';
1717
import {TreeState} from '@react-stately/tree';
@@ -22,7 +22,7 @@ export interface MenuAria {
2222
menuProps: DOMAttributes
2323
}
2424

25-
export interface AriaMenuOptions<T> extends Omit<AriaMenuProps<T>, 'children'> {
25+
export interface AriaMenuOptions<T> extends Omit<AriaMenuProps<T>, 'children'>, KeyboardEvents {
2626
/** Whether the menu uses virtual scrolling. */
2727
isVirtualized?: boolean,
2828

@@ -49,6 +49,8 @@ export const menuData = new WeakMap<TreeState<unknown>, MenuData>();
4949
export function useMenu<T>(props: AriaMenuOptions<T>, state: TreeState<T>, ref: RefObject<HTMLElement>): MenuAria {
5050
let {
5151
shouldFocusWrap = true,
52+
onKeyDown,
53+
onKeyUp,
5254
...otherProps
5355
} = props;
5456

@@ -73,10 +75,8 @@ export function useMenu<T>(props: AriaMenuOptions<T>, state: TreeState<T>, ref:
7375
});
7476

7577
return {
76-
menuProps: mergeProps(domProps, {
78+
menuProps: mergeProps(domProps, {onKeyDown, onKeyUp}, {
7779
role: 'menu',
78-
// this forces AT to move their cursors into any open sub dialogs, the sub dialogs contain hidden close buttons in order to come back to this level of the menu
79-
'aria-hidden': state.expandedKeys.size > 0 ? true : undefined,
8080
...listProps,
8181
onKeyDown: (e) => {
8282
// don't clear the menu selected keys if the user is presses escape since escape closes the menu

0 commit comments

Comments
 (0)