diff --git a/packages/menu/demo/menu.stories.tsx b/packages/menu/demo/menu.stories.tsx index 76723ddab..df42391c0 100644 --- a/packages/menu/demo/menu.stories.tsx +++ b/packages/menu/demo/menu.stories.tsx @@ -63,6 +63,54 @@ export const Controlled: Story = { }, args: { isExpanded: false, + restoreFocus: true, + focusedValue: 'plant-01', + selectedItems: [{ value: 'Cherry', type: 'checkbox' }] + } +}; + +export const ControlledManagedFocus: Story = { + render: function Render(args) { + const updateArgs = useArgs()[1]; + const triggerRef = React.useRef(null); + + return ( + { + // eslint-disable-next-line no-console + console.log('onChange:', _args); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { type, isExpanded, ...rest } = _args; + const { selectedItems } = rest; + const nextArgs: typeof rest & { isExpanded?: boolean } = rest; + + const lastItem = selectedItems?.[selectedItems.length - 1]; + const isNonCheckboxItem = !selectedItems || lastItem?.type !== 'checkbox'; + + if (isExpanded !== undefined && isNonCheckboxItem) { + nextArgs.isExpanded = isExpanded; + } + + if (!args.restoreFocus && nextArgs.isExpanded === false && triggerRef.current) { + triggerRef.current.focus(); + } + updateArgs(nextArgs); + }} + /> + ); + }, + name: 'Controlled + Managed Focus', + argTypes: { + defaultFocusedValue: { control: false }, + defaultExpanded: { control: false }, + focusedValue: { control: { type: 'text' } } + }, + args: { + isExpanded: false, + restoreFocus: false, focusedValue: 'plant-01', selectedItems: [{ value: 'Cherry', type: 'checkbox' }] } diff --git a/packages/menu/demo/stories/MenuStory.tsx b/packages/menu/demo/stories/MenuStory.tsx index a60b8ecf8..788782ee1 100644 --- a/packages/menu/demo/stories/MenuStory.tsx +++ b/packages/menu/demo/stories/MenuStory.tsx @@ -91,60 +91,71 @@ const Component = ({ const selectedValues = selection.map(item => item.value); return ( -
- - -
    - {items.map((item: MenuItem) => { - if ('items' in item) { - return ( -
  • - - -
      - {item.items.map(groupItem => ( - +
      +
      + + +
        + {items.map((item: MenuItem) => { + if ('items' in item) { + return ( +
      • + + - ))} -
      - - ); - } - - if ('separator' in item) { - return ( -
    • - ); - } - - return ( - - ); - })} -
    +
      + {item.items.map(groupItem => ( + + ))} +
    +
  • + ); + } + + if ('separator' in item) { + return ( +
  • + ); + } + + return ( + + ); + })} +
+
+
+ +
+ ); }; @@ -167,16 +178,16 @@ interface IArgs extends MenuContainerProps { as: 'hook' | 'container'; } -export const MenuStory: StoryFn = ({ as, ...props }) => { - const triggerRef = useRef(null); +export const MenuStory: StoryFn = ({ as, triggerRef, ...props }) => { + const _triggerRef = useRef(null); const menuRef = useRef(null); switch (as) { case 'container': - return ; + return ; case 'hook': default: - return ; + return ; } }; diff --git a/packages/menu/src/MenuContainer.tsx b/packages/menu/src/MenuContainer.tsx index e002548e7..600876d14 100644 --- a/packages/menu/src/MenuContainer.tsx +++ b/packages/menu/src/MenuContainer.tsx @@ -29,5 +29,12 @@ MenuContainer.propTypes = { defaultExpanded: PropTypes.bool, selectedItems: PropTypes.arrayOf(PropTypes.any), focusedValue: PropTypes.oneOfType([PropTypes.string]), - defaultFocusedValue: PropTypes.oneOfType([PropTypes.string]) + defaultFocusedValue: PropTypes.oneOfType([PropTypes.string]), + restoreFocus: PropTypes.bool +}; + +MenuContainer.defaultProps = { + defaultExpanded: false, + restoreFocus: true, + rtl: false }; diff --git a/packages/menu/src/types.ts b/packages/menu/src/types.ts index 201de2e1d..7e70a6570 100644 --- a/packages/menu/src/types.ts +++ b/packages/menu/src/types.ts @@ -66,6 +66,8 @@ export interface IUseMenuProps { focusedValue?: string | null; /** Determines focused value on menu initialization */ defaultFocusedValue?: string; + /** Returns focus to the trigger when the menu is collapsed */ + restoreFocus?: boolean; /** Sets the selected values in a controlled menu */ selectedItems?: ISelectedItem[]; /** diff --git a/packages/menu/src/useMenu.ts b/packages/menu/src/useMenu.ts index bd1e170a4..913025554 100644 --- a/packages/menu/src/useMenu.ts +++ b/packages/menu/src/useMenu.ts @@ -12,7 +12,6 @@ import React, { useEffect, useMemo, useReducer, - useRef, useState } from 'react'; import { useSelection } from '@zendeskgarden/container-selection'; @@ -49,6 +48,7 @@ export const useMenu = undefined, isExpanded, defaultExpanded = false, + restoreFocus = true, selectedItems, focusedValue, defaultFocusedValue @@ -94,8 +94,6 @@ export const useMenu = (false); - const focusTriggerRef = useRef(false); - const [state, dispatch] = useReducer(stateReducer, { focusedValue: focusedValue || defaultFocusedValue, isExpanded: isExpanded || defaultExpanded, @@ -135,6 +133,15 @@ export const useMenu = { + if (!skip && restoreFocus && triggerRef.current) { + triggerRef.current.focus(); + } + }, + [triggerRef, restoreFocus] + ); + const closeMenu = useCallback( (changeType: string) => { dispatch({ @@ -281,13 +288,22 @@ export const useMenu = { @@ -395,6 +420,8 @@ export const useMenu = { if (state.focusOnOpen && menuVisible && controlledFocusedValue && controlledIsExpanded) { @@ -614,13 +632,7 @@ export const useMenu =