Skip to content

Commit

Permalink
MenuCloseOnClickContext
Browse files Browse the repository at this point in the history
  • Loading branch information
r100-stack committed Feb 28, 2025
1 parent a2b9df2 commit 2ac45b6
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 69 deletions.
20 changes: 20 additions & 0 deletions apps/react-workshop/src/Tile.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,26 @@ export const AllProps = () => {
<MenuItem key={2} onClick={() => console.log('clicked item 2')}>
Item 2
</MenuItem>
<MenuItem
key={3}
onClick={() => console.log('clicked item 3')}
subMenuItems={[
<MenuItem
key={1}
onClick={() => console.log('clicked sub item 1')}
>
Sub Item 1
</MenuItem>,
<MenuItem
key={2}
onClick={() => console.log('clicked sub item 2')}
>
Sub Item 2
</MenuItem>,
]}
>
Item 3
</MenuItem>
</Tile.MoreOptions>
<Tile.Metadata>
<SvgTag />
Expand Down
106 changes: 62 additions & 44 deletions packages/itwinui-react/src/core/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,51 +243,51 @@ export const Menu = React.forwardRef((props, ref) => {
() => undefined,
);

const popoverGetItemProps: PopoverGetItemProps = ({
focusableItemIndex,
userProps,
}) => {
return getItemProps({
...userProps,
// Roving tabIndex
tabIndex:
activeIndex != null &&
activeIndex >= 0 &&
focusableItemIndex != null &&
focusableItemIndex >= 0 &&
activeIndex === focusableItemIndex
? 0
: -1,
onFocus: mergeEventHandlers(userProps?.onFocus, () => {
// Set hasFocusedNodeInSubmenu in a microtask to ensure the submenu stays open reliably.
// E.g. Even when hovering into it rapidly.
queueMicrotask(() => {
setHasFocusedNodeInSubmenu(true);
});
tree?.events.emit('onNodeFocused', {
nodeId: nodeId,
parentId: parentId,
});
}),
const popoverGetItemProps: PopoverGetItemProps = React.useCallback(
({ focusableItemIndex, userProps }) => {
return getItemProps({
...userProps,
// Roving tabIndex
tabIndex:
activeIndex != null &&
activeIndex >= 0 &&
focusableItemIndex != null &&
focusableItemIndex >= 0 &&
activeIndex === focusableItemIndex
? 0
: -1,
onFocus: mergeEventHandlers(userProps?.onFocus, () => {
// Set hasFocusedNodeInSubmenu in a microtask to ensure the submenu stays open reliably.
// E.g. Even when hovering into it rapidly.
queueMicrotask(() => {
setHasFocusedNodeInSubmenu(true);
});
tree?.events.emit('onNodeFocused', {
nodeId: nodeId,
parentId: parentId,
});
}),

// useListNavigation sets focusItemOnHover to false, since it doesn't work for us.
// Thus, we need to manually emulate the "focus on hover" behavior.
onMouseEnter: mergeEventHandlers(userProps?.onMouseEnter, (event) => {
// Updating the activeIndex will result in useListNavigation focusing the item.
if (focusableItemIndex != null && focusableItemIndex >= 0) {
setActiveIndex(focusableItemIndex);
}
// useListNavigation sets focusItemOnHover to false, since it doesn't work for us.
// Thus, we need to manually emulate the "focus on hover" behavior.
onMouseEnter: mergeEventHandlers(userProps?.onMouseEnter, (event) => {
// Updating the activeIndex will result in useListNavigation focusing the item.
if (focusableItemIndex != null && focusableItemIndex >= 0) {
setActiveIndex(focusableItemIndex);
}

// However, useListNavigation only focuses the item when the activeIndex changes.
// So, if we re-hover the parent MenuItem of an open submenu, the activeIndex won't change,
// and thus the hovered MenuItem won't be focused.
// As a result, we need to explicitly focus the item manually.
if (event.target === event.currentTarget) {
event.currentTarget.focus();
}
}),
});
};
// However, useListNavigation only focuses the item when the activeIndex changes.
// So, if we re-hover the parent MenuItem of an open submenu, the activeIndex won't change,
// and thus the hovered MenuItem won't be focused.
// As a result, we need to explicitly focus the item manually.
if (event.target === event.currentTarget) {
event.currentTarget.focus();
}
}),
});
},
[activeIndex, getItemProps, nodeId, parentId, tree?.events],
);

const reference = cloneElementWithRef(trigger, (triggerChild) =>
getReferenceProps(
Expand Down Expand Up @@ -319,7 +319,12 @@ export const Menu = React.forwardRef((props, ref) => {

return (
<>
<MenuContext.Provider value={{ popoverGetItemProps, focusableElements }}>
<MenuContext.Provider
value={React.useMemo(
() => ({ popoverGetItemProps, focusableElements, close }),
[close, focusableElements, popoverGetItemProps],
)}
>
<PopoverOpenContext.Provider value={popover.open}>
{reference}
</PopoverOpenContext.Provider>
Expand Down Expand Up @@ -353,10 +358,23 @@ type PopoverGetItemProps = ({
>[0];
}) => ReturnType<ReturnType<typeof useInteractions>['getItemProps']>;

// ----------------------------------------------------------------------------

export const MenuContext = React.createContext<
| {
popoverGetItemProps: PopoverGetItemProps;
focusableElements: HTMLElement[];
close?: () => void;
}
| undefined
>(undefined);

/**
* @private
* Wraps around a `Menu` or a component that contains a `Menu`.
*
* If `true`, closes the menu when any descendant `MenuItem` is clicked.
*/
export const MenuCloseOnClickContext = React.createContext<boolean | undefined>(
undefined,
);
12 changes: 7 additions & 5 deletions packages/itwinui-react/src/core/Menu/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import {
useWarningLogger,
} from '../../utils/index.js';
import type { PolymorphicForwardRefComponent } from '../../utils/index.js';
import { Menu, MenuContext } from './Menu.js';
import { Menu, MenuCloseOnClickContext, MenuContext } from './Menu.js';
import { ListItem } from '../List/ListItem.js';
import type { ListItemOwnProps } from '../List/ListItem.js';
import cx from 'classnames';
import { TileMoreOptionsContext } from '../Tile/Tile.js';

export type MenuItemProps = {
/**
Expand Down Expand Up @@ -112,7 +111,8 @@ export const MenuItem = React.forwardRef((props, forwardedRef) => {
}

const parentMenu = React.useContext(MenuContext);
const tileMoreOptionsContext = React.useContext(TileMoreOptionsContext);
const menuContext = React.useContext(MenuContext);
const menuCloseOnClickContext = React.useContext(MenuCloseOnClickContext);

const menuItemRef = React.useRef<HTMLElement>(null);
const submenuId = useId();
Expand All @@ -136,8 +136,10 @@ export const MenuItem = React.forwardRef((props, forwardedRef) => {
return;
}

// If a MenuItem is within a Tile.MoreOptions, should close the menu when the item is clicked
tileMoreOptionsContext?.close?.();
// If MenuCloseOnClickContext's value = true, should close the menu when the item is clicked
if (menuCloseOnClickContext) {
menuContext?.close?.();
}

onClickProp?.(value);
};
Expand Down
23 changes: 3 additions & 20 deletions packages/itwinui-react/src/core/Tile/Tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { DropdownMenu } from '../DropdownMenu/DropdownMenu.js';
import { IconButton } from '../Buttons/IconButton.js';
import { ProgressRadial } from '../ProgressIndicators/ProgressRadial.js';
import { LinkAction } from '../LinkAction/LinkAction.js';
import { MenuCloseOnClickContext } from '../Menu/Menu.js';

const TileContext = React.createContext<
| {
Expand Down Expand Up @@ -65,17 +66,6 @@ if (process.env.NODE_ENV === 'development') {
TileContext.displayName = 'TileContext';
}

/**
* To close nested Tile.MoreOptions menu when descendant MenuItem is clicked.
* https://github.com/iTwin/iTwinUI/issues/2451
*/
export const TileMoreOptionsContext = React.createContext<
| {
close: (() => void) | undefined;
}
| undefined
>(undefined);

// ----------------------------------------------------------------------------
// Main Tile component

Expand Down Expand Up @@ -394,14 +384,8 @@ const TileMoreOptions = React.forwardRef((props, forwardedRef) => {
const { className, children = [], buttonProps, ...rest } = props;
const [isMenuVisible, setIsMenuVisible] = React.useState(false);

const close = React.useCallback(() => {
setIsMenuVisible(false);
}, []);

return (
<TileMoreOptionsContext.Provider
value={React.useMemo(() => ({ close }), [close])}
>
<MenuCloseOnClickContext.Provider value={true}>
<Box
className={cx(
'iui-tile-more-options',
Expand All @@ -414,7 +398,6 @@ const TileMoreOptions = React.forwardRef((props, forwardedRef) => {
{...rest}
>
<DropdownMenu
visible={isMenuVisible}
onVisibleChange={setIsMenuVisible}
menuItems={children as React.ReactElement<any>[]}
>
Expand All @@ -428,7 +411,7 @@ const TileMoreOptions = React.forwardRef((props, forwardedRef) => {
</IconButton>
</DropdownMenu>
</Box>
</TileMoreOptionsContext.Provider>
</MenuCloseOnClickContext.Provider>
);
}) as PolymorphicForwardRefComponent<'div', TileMoreOptionsOwnProps>;
if (process.env.NODE_ENV === 'development') {
Expand Down

0 comments on commit 2ac45b6

Please sign in to comment.