@elliemae/ds-menu-button 3.80.0-next.51 → 3.80.0-next.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/cjs/config/useSplitInherithedProps.js +13 -2
  2. package/dist/cjs/config/useSplitInherithedProps.js.map +2 -2
  3. package/dist/cjs/parts/DSFlyoutMenu/DSFlyoutMenu.js +25 -2
  4. package/dist/cjs/parts/DSFlyoutMenu/DSFlyoutMenu.js.map +2 -2
  5. package/dist/cjs/parts/DSMenuBehaviouralContextProvider/config/useFocusTracker.js +4 -1
  6. package/dist/cjs/parts/DSMenuBehaviouralContextProvider/config/useFocusTracker.js.map +2 -2
  7. package/dist/cjs/parts/DSMenuBehaviouralContextProvider/config/useGlobalEvents.js +4 -3
  8. package/dist/cjs/parts/DSMenuBehaviouralContextProvider/config/useGlobalEvents.js.map +2 -2
  9. package/dist/cjs/parts/DSMenuBehaviouralContextProvider/config/useMenuItemEventsHandlers.js +27 -10
  10. package/dist/cjs/parts/DSMenuBehaviouralContextProvider/config/useMenuItemEventsHandlers.js.map +2 -2
  11. package/dist/cjs/parts/DSMenuBehaviouralContextProvider/config/useMenuOpenStatus.js +14 -8
  12. package/dist/cjs/parts/DSMenuBehaviouralContextProvider/config/useMenuOpenStatus.js.map +2 -2
  13. package/dist/cjs/parts/DSOpinionatedButton/config/useTriggerEventsHandlers.js +19 -9
  14. package/dist/cjs/parts/DSOpinionatedButton/config/useTriggerEventsHandlers.js.map +2 -2
  15. package/dist/cjs/react-desc-prop-types.js +9 -2
  16. package/dist/cjs/react-desc-prop-types.js.map +2 -2
  17. package/dist/esm/config/useSplitInherithedProps.js +13 -2
  18. package/dist/esm/config/useSplitInherithedProps.js.map +2 -2
  19. package/dist/esm/parts/DSFlyoutMenu/DSFlyoutMenu.js +25 -2
  20. package/dist/esm/parts/DSFlyoutMenu/DSFlyoutMenu.js.map +2 -2
  21. package/dist/esm/parts/DSMenuBehaviouralContextProvider/config/useFocusTracker.js +4 -1
  22. package/dist/esm/parts/DSMenuBehaviouralContextProvider/config/useFocusTracker.js.map +2 -2
  23. package/dist/esm/parts/DSMenuBehaviouralContextProvider/config/useGlobalEvents.js +4 -3
  24. package/dist/esm/parts/DSMenuBehaviouralContextProvider/config/useGlobalEvents.js.map +2 -2
  25. package/dist/esm/parts/DSMenuBehaviouralContextProvider/config/useMenuItemEventsHandlers.js +27 -10
  26. package/dist/esm/parts/DSMenuBehaviouralContextProvider/config/useMenuItemEventsHandlers.js.map +2 -2
  27. package/dist/esm/parts/DSMenuBehaviouralContextProvider/config/useMenuOpenStatus.js +14 -8
  28. package/dist/esm/parts/DSMenuBehaviouralContextProvider/config/useMenuOpenStatus.js.map +2 -2
  29. package/dist/esm/parts/DSOpinionatedButton/config/useTriggerEventsHandlers.js +19 -9
  30. package/dist/esm/parts/DSOpinionatedButton/config/useTriggerEventsHandlers.js.map +2 -2
  31. package/dist/esm/react-desc-prop-types.js +9 -2
  32. package/dist/esm/react-desc-prop-types.js.map +2 -2
  33. package/dist/types/config/useSplitInherithedProps.d.ts +3 -2
  34. package/dist/types/parts/DSMenuBehaviouralContextProvider/config/useFocusTracker.d.ts +3 -1
  35. package/dist/types/parts/DSMenuBehaviouralContextProvider/config/useMenuItemEventsHandlers.d.ts +1 -0
  36. package/dist/types/parts/DSMenuBehaviouralContextProvider/config/useMenuOpenStatus.d.ts +12 -4
  37. package/dist/types/react-desc-prop-types.d.ts +13 -2
  38. package/package.json +12 -12
@@ -46,6 +46,7 @@ const useSplitInherithedProps = ({
46
46
  onActivateItem,
47
47
  onOpen,
48
48
  onClose,
49
+ a11yDangerouslyConsumerOwnedMenuOpenState,
49
50
  maxHeight,
50
51
  // this is not shared with anything at all, it's instead a property of the "root" node, each node (with a submenu) can have its own maxHeight
51
52
  ...buttonInheritedProps
@@ -65,6 +66,14 @@ const useSplitInherithedProps = ({
65
66
  }
66
67
  return [];
67
68
  }, [selectedItems, treeRootNode]);
69
+ const onCloseAndDismissal = React2.useCallback(
70
+ (metainfo) => {
71
+ if (onClose) onClose(metainfo);
72
+ if (metainfo.reason === "outside-click" || metainfo.reason === "escape")
73
+ onClickOutside(metainfo.event);
74
+ },
75
+ [onClose, onClickOutside]
76
+ );
68
77
  const menuSpecificProps = React2.useMemo(
69
78
  () => ({
70
79
  options,
@@ -100,7 +109,8 @@ const useSplitInherithedProps = ({
100
109
  onItemSelected,
101
110
  onActivateItem,
102
111
  onOpen,
103
- onClose
112
+ onClose: onCloseAndDismissal,
113
+ a11yDangerouslyConsumerOwnedMenuOpenState
104
114
  },
105
115
  opinionatedButtonProps: {
106
116
  ...buttonInheritedProps,
@@ -116,7 +126,8 @@ const useSplitInherithedProps = ({
116
126
  onItemSelected,
117
127
  onActivateItem,
118
128
  onOpen,
119
- onClose,
129
+ onCloseAndDismissal,
130
+ a11yDangerouslyConsumerOwnedMenuOpenState,
120
131
  buttonInheritedProps,
121
132
  innerRefSnatchingNode,
122
133
  menuSpecificProps
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../scripts/build/transpile/react-shim.js", "../../../src/config/useSplitInherithedProps.ts"],
4
- "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* *******************************************************\n * From official source definition\n *\n * Menu Button Pattern\n * About This Pattern:\n * A menu button is a button\n * that opens a menu\n * (as described in the Menu and Menubar Pattern).\n *\n * Since the menu button is a button, it inherits all the props from the Button component.\n * Since the menu button MUST have a menu, it also have a set of specific props used to handle the menu.\n * Because we build with atomic composition in mind, the \"logic layer\" is separated and has a yet another set of props.\n *\n * This is effectively an OOP \"extension\" of the Button component,\n * so it has the same props,\n * BUT also a few more to handle the specific behavior of the menu.\n *\n * this hooks is meant to take all the DSMenuButtonT.InternalProps with default already merged\n * and spit out menuSpecificProps & buttonInheritedProps\n ******************************************************* */\nimport type {} from '@elliemae/ds-system';\nimport type {} from '@elliemae/pui-theme';\nimport React from 'react';\nimport { type DSMenuButtonT } from '../react-desc-prop-types.js';\nimport { isObjectAMenuNode, isSelectionableNode } from '../utils/nodesTypeguardsAndGetters.js';\nimport { resolveRef } from '../utils/resolveRef.js';\n\n/**\n * Converts selected options to a map of selected nodes.\n * @param {DSMenuButtonT.SelectionableMenuNodes[]} selectedItems - The selected options, as provided by the user (either items or nodes).\n * @param {DSMenuButtonT.MenuNode} treeRootNode - The root node of the menu tree.\n * @returns {DSMenuButtonT.SelectionableMenuNodes[]} - The map of selected nodes.\n * @throws Will throw an error if a selected option is not found in the tree.\n */\nconst convertSelectedOptionsToNodes = (\n selectedItems: DSMenuButtonT.SelectionableMenuNodes[] | DSMenuButtonT.SelectionableMenuItemInterface[],\n treeRootNode: DSMenuButtonT.MenuNode,\n): DSMenuButtonT.SelectionableMenuNodes[] => {\n const convertedSelection: DSMenuButtonT.SelectionableMenuNodes[] = [];\n\n selectedItems.forEach((selectedItem) => {\n const needsConversion = !isObjectAMenuNode(selectedItem);\n if (!needsConversion) {\n convertedSelection.push(selectedItem);\n return;\n }\n\n const { dsId } = selectedItem;\n const selectedNode = treeRootNode.findNode((node: DSMenuButtonT.MenuNode) => node.dsId === dsId);\n if (!selectedNode) {\n throw new Error(`Selected option with dsId ${dsId} not found in the menu tree.`);\n }\n if (!isSelectionableNode(selectedNode)) {\n // eslint-disable-next-line no-console\n console.warn(`Selected option with dsId ${dsId} is not a selectionable node and will be ignored.`);\n return;\n }\n convertedSelection.push(selectedNode);\n });\n\n return convertedSelection;\n};\n\ntype UseSplitInherithedPropsConfig = {\n propsWithDefault: DSMenuButtonT.InternalProps;\n focusableNodes: DSMenuButtonT.PseudoFocusableMenuNodes[];\n treeRootNode: DSMenuButtonT.MenuNode;\n};\nexport const useSplitInherithedProps = ({\n propsWithDefault,\n focusableNodes,\n treeRootNode,\n}: UseSplitInherithedPropsConfig) => {\n const buttonDOMNodeRef = React.useRef<HTMLElement | null>(null);\n // when this component has been wrote\n // =============================================================================\n // MENU BUTTON props are:\n // Required\n // optionsTree, menuSpecificProps\n // Default (we already have a value because props for this hook are already merged with default props)\n // onClickOutside, onOptionClick, openedSubmenus, onSubmenuToggle, isLoading, isSkeleton, selectedOptions\n // Optional (may or may not be present)\n // ItemRenderer\n // =============================================================================\n // Props for the DSMenuBehaviouralContextProvider\n // =============================================================================\n // Required\n // selectedItems, onItemSelected, onActivateItem,\n // Optional\n // onDisplayedSubmenuChange, onOpen, onClose\n\n const {\n options,\n onClickOutside,\n onOptionClick,\n openedSubmenus,\n onSubmenuToggle,\n isLoading,\n isSkeleton,\n ItemRenderer,\n floatingConfig,\n innerRef,\n selectedItems,\n onDisplayedSubmenuChange,\n onItemSelected,\n onActivateItem,\n onOpen,\n onClose,\n maxHeight, // this is not shared with anything at all, it's instead a property of the \"root\" node, each node (with a submenu) can have its own maxHeight\n ...buttonInheritedProps\n } = propsWithDefault;\n\n // the button is allowed to receive innerRef, but we also need to invoke the setButtonDOMNode to store the button node\n // so we create a functional ref to do both\n const innerRefSnatchingNode: Required<DSMenuButtonT.Props>['innerRef'] = React.useCallback(\n (node: HTMLButtonElement) => {\n buttonDOMNodeRef.current = node;\n if (innerRef) {\n resolveRef(innerRef, node);\n }\n },\n [innerRef],\n );\n // in the WIDGET API, the user can provide selectedItems as an array of \"items\" or an array of \"nodes\"\n // the component needs the \"nodes\" to handle the logic and rendering & accessibility\n // if the user provides \"items\", we convert them to \"nodes\" here\n const selectedNodesMap = React.useMemo(() => {\n if (selectedItems) {\n return convertSelectedOptionsToNodes(selectedItems, treeRootNode);\n }\n return [];\n }, [selectedItems, treeRootNode]);\n\n const menuSpecificProps = React.useMemo(\n () => ({\n options,\n onClickOutside,\n onOptionClick,\n openedSubmenus,\n onSubmenuToggle,\n isLoading,\n isSkeleton,\n ItemRenderer,\n ...(floatingConfig !== undefined ? { floatingConfig } : {}),\n }),\n [\n options,\n onClickOutside,\n onOptionClick,\n openedSubmenus,\n onSubmenuToggle,\n isLoading,\n isSkeleton,\n ItemRenderer,\n floatingConfig,\n ],\n );\n\n return React.useMemo(\n () => ({\n menuBehaviouralLayerProps: {\n buttonDOMNodeRef,\n selectedNodes: selectedNodesMap,\n focusableNodes,\n optionsTree: treeRootNode,\n onDisplayedSubmenuChange,\n onItemSelected,\n onActivateItem,\n onOpen,\n onClose,\n },\n opinionatedButtonProps: {\n ...(buttonInheritedProps as DSMenuButtonT.ButtonInheritedProps),\n innerRef: innerRefSnatchingNode,\n menuSpecificProps,\n },\n }),\n [\n selectedNodesMap,\n focusableNodes,\n treeRootNode,\n onDisplayedSubmenuChange,\n onItemSelected,\n onActivateItem,\n onOpen,\n onClose,\n buttonInheritedProps,\n innerRefSnatchingNode,\n menuSpecificProps,\n ],\n );\n};\n"],
5
- "mappings": "AAAA,YAAY,WAAW;ACsBvB,OAAOA,YAAW;AAElB,SAAS,mBAAmB,2BAA2B;AACvD,SAAS,kBAAkB;AAS3B,MAAM,gCAAgC,CACpC,eACA,iBAC2C;AAC3C,QAAM,qBAA6D,CAAC;AAEpE,gBAAc,QAAQ,CAAC,iBAAiB;AACtC,UAAM,kBAAkB,CAAC,kBAAkB,YAAY;AACvD,QAAI,CAAC,iBAAiB;AACpB,yBAAmB,KAAK,YAAY;AACpC;AAAA,IACF;AAEA,UAAM,EAAE,KAAK,IAAI;AACjB,UAAM,eAAe,aAAa,SAAS,CAAC,SAAiC,KAAK,SAAS,IAAI;AAC/F,QAAI,CAAC,cAAc;AACjB,YAAM,IAAI,MAAM,6BAA6B,IAAI,8BAA8B;AAAA,IACjF;AACA,QAAI,CAAC,oBAAoB,YAAY,GAAG;AAEtC,cAAQ,KAAK,6BAA6B,IAAI,mDAAmD;AACjG;AAAA,IACF;AACA,uBAAmB,KAAK,YAAY;AAAA,EACtC,CAAC;AAED,SAAO;AACT;AAOO,MAAM,0BAA0B,CAAC;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AACF,MAAqC;AACnC,QAAM,mBAAmBA,OAAM,OAA2B,IAAI;AAkB9D,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AAIJ,QAAM,wBAAmEA,OAAM;AAAA,IAC7E,CAAC,SAA4B;AAC3B,uBAAiB,UAAU;AAC3B,UAAI,UAAU;AACZ,mBAAW,UAAU,IAAI;AAAA,MAC3B;AAAA,IACF;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AAIA,QAAM,mBAAmBA,OAAM,QAAQ,MAAM;AAC3C,QAAI,eAAe;AACjB,aAAO,8BAA8B,eAAe,YAAY;AAAA,IAClE;AACA,WAAO,CAAC;AAAA,EACV,GAAG,CAAC,eAAe,YAAY,CAAC;AAEhC,QAAM,oBAAoBA,OAAM;AAAA,IAC9B,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,GAAI,mBAAmB,SAAY,EAAE,eAAe,IAAI,CAAC;AAAA,IAC3D;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,SAAOA,OAAM;AAAA,IACX,OAAO;AAAA,MACL,2BAA2B;AAAA,QACzB;AAAA,QACA,eAAe;AAAA,QACf;AAAA,QACA,aAAa;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACA,wBAAwB;AAAA,QACtB,GAAI;AAAA,QACJ,UAAU;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAAA,IACA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* *******************************************************\n * From official source definition\n *\n * Menu Button Pattern\n * About This Pattern:\n * A menu button is a button\n * that opens a menu\n * (as described in the Menu and Menubar Pattern).\n *\n * Since the menu button is a button, it inherits all the props from the Button component.\n * Since the menu button MUST have a menu, it also have a set of specific props used to handle the menu.\n * Because we build with atomic composition in mind, the \"logic layer\" is separated and has a yet another set of props.\n *\n * This is effectively an OOP \"extension\" of the Button component,\n * so it has the same props,\n * BUT also a few more to handle the specific behavior of the menu.\n *\n * this hooks is meant to take all the DSMenuButtonT.InternalProps with default already merged\n * and spit out menuSpecificProps & buttonInheritedProps\n ******************************************************* */\nimport type {} from '@elliemae/ds-system';\nimport type {} from '@elliemae/pui-theme';\nimport React from 'react';\nimport { type DSMenuButtonT } from '../react-desc-prop-types.js';\nimport { isObjectAMenuNode, isSelectionableNode } from '../utils/nodesTypeguardsAndGetters.js';\nimport { resolveRef } from '../utils/resolveRef.js';\n\n/**\n * Converts selected options to a map of selected nodes.\n * @param {DSMenuButtonT.SelectionableMenuNodes[]} selectedItems - The selected options, as provided by the user (either items or nodes).\n * @param {DSMenuButtonT.MenuNode} treeRootNode - The root node of the menu tree.\n * @returns {DSMenuButtonT.SelectionableMenuNodes[]} - The map of selected nodes.\n * @throws Will throw an error if a selected option is not found in the tree.\n */\nconst convertSelectedOptionsToNodes = (\n selectedItems: DSMenuButtonT.SelectionableMenuNodes[] | DSMenuButtonT.SelectionableMenuItemInterface[],\n treeRootNode: DSMenuButtonT.MenuNode,\n): DSMenuButtonT.SelectionableMenuNodes[] => {\n const convertedSelection: DSMenuButtonT.SelectionableMenuNodes[] = [];\n\n selectedItems.forEach((selectedItem) => {\n const needsConversion = !isObjectAMenuNode(selectedItem);\n if (!needsConversion) {\n convertedSelection.push(selectedItem);\n return;\n }\n\n const { dsId } = selectedItem;\n const selectedNode = treeRootNode.findNode((node: DSMenuButtonT.MenuNode) => node.dsId === dsId);\n if (!selectedNode) {\n throw new Error(`Selected option with dsId ${dsId} not found in the menu tree.`);\n }\n if (!isSelectionableNode(selectedNode)) {\n // eslint-disable-next-line no-console\n console.warn(`Selected option with dsId ${dsId} is not a selectionable node and will be ignored.`);\n return;\n }\n convertedSelection.push(selectedNode);\n });\n\n return convertedSelection;\n};\n\ntype UseSplitInherithedPropsConfig = {\n propsWithDefault: DSMenuButtonT.InternalProps;\n focusableNodes: DSMenuButtonT.PseudoFocusableMenuNodes[];\n treeRootNode: DSMenuButtonT.MenuNode;\n};\nexport const useSplitInherithedProps = ({\n propsWithDefault,\n focusableNodes,\n treeRootNode,\n}: UseSplitInherithedPropsConfig) => {\n const buttonDOMNodeRef = React.useRef<HTMLElement | null>(null);\n // when this component has been wrote\n // =============================================================================\n // MENU BUTTON props are:\n // Required\n // optionsTree, menuSpecificProps\n // Default (we already have a value because props for this hook are already merged with default props)\n // onClickOutside, onOptionClick, openedSubmenus, onSubmenuToggle, isLoading, isSkeleton, selectedOptions\n // Optional (may or may not be present)\n // ItemRenderer\n // =============================================================================\n // Props for the DSMenuBehaviouralContextProvider\n // =============================================================================\n // Required\n // selectedItems, onItemSelected, onActivateItem,\n // Optional\n // onDisplayedSubmenuChange, onOpen, onClose\n\n const {\n options,\n onClickOutside,\n onOptionClick,\n openedSubmenus,\n onSubmenuToggle,\n isLoading,\n isSkeleton,\n ItemRenderer,\n floatingConfig,\n innerRef,\n selectedItems,\n onDisplayedSubmenuChange,\n onItemSelected,\n onActivateItem,\n onOpen,\n onClose,\n a11yDangerouslyConsumerOwnedMenuOpenState,\n maxHeight, // this is not shared with anything at all, it's instead a property of the \"root\" node, each node (with a submenu) can have its own maxHeight\n ...buttonInheritedProps\n } = propsWithDefault;\n\n // the button is allowed to receive innerRef, but we also need to invoke the setButtonDOMNode to store the button node\n // so we create a functional ref to do both\n const innerRefSnatchingNode: Required<DSMenuButtonT.Props>['innerRef'] = React.useCallback(\n (node: HTMLButtonElement) => {\n buttonDOMNodeRef.current = node;\n if (innerRef) {\n resolveRef(innerRef, node);\n }\n },\n [innerRef],\n );\n // in the WIDGET API, the user can provide selectedItems as an array of \"items\" or an array of \"nodes\"\n // the component needs the \"nodes\" to handle the logic and rendering & accessibility\n // if the user provides \"items\", we convert them to \"nodes\" here\n const selectedNodesMap = React.useMemo(() => {\n if (selectedItems) {\n return convertSelectedOptionsToNodes(selectedItems, treeRootNode);\n }\n return [];\n }, [selectedItems, treeRootNode]);\n\n // `onClickOutside` is the dismissal channel \u2014 a pointer interaction outside the menu, or the Escape key \u2014\n // it is a subset of the close intents, so it is derived from `onClose`'s `reason` instead of a second code path\n const onCloseAndDismissal = React.useCallback<Required<DSMenuButtonT.MenuBehaviouralLayerOptionalProps>['onClose']>(\n (metainfo) => {\n if (onClose) onClose(metainfo);\n if (metainfo.reason === 'outside-click' || metainfo.reason === 'escape')\n onClickOutside(metainfo.event as Parameters<typeof onClickOutside>[0]);\n },\n [onClose, onClickOutside],\n );\n\n const menuSpecificProps = React.useMemo(\n () => ({\n options,\n onClickOutside,\n onOptionClick,\n openedSubmenus,\n onSubmenuToggle,\n isLoading,\n isSkeleton,\n ItemRenderer,\n ...(floatingConfig !== undefined ? { floatingConfig } : {}),\n }),\n [\n options,\n onClickOutside,\n onOptionClick,\n openedSubmenus,\n onSubmenuToggle,\n isLoading,\n isSkeleton,\n ItemRenderer,\n floatingConfig,\n ],\n );\n\n return React.useMemo(\n () => ({\n menuBehaviouralLayerProps: {\n buttonDOMNodeRef,\n selectedNodes: selectedNodesMap,\n focusableNodes,\n optionsTree: treeRootNode,\n onDisplayedSubmenuChange,\n onItemSelected,\n onActivateItem,\n onOpen,\n onClose: onCloseAndDismissal,\n a11yDangerouslyConsumerOwnedMenuOpenState,\n },\n opinionatedButtonProps: {\n ...(buttonInheritedProps as DSMenuButtonT.ButtonInheritedProps),\n innerRef: innerRefSnatchingNode,\n menuSpecificProps,\n },\n }),\n [\n selectedNodesMap,\n focusableNodes,\n treeRootNode,\n onDisplayedSubmenuChange,\n onItemSelected,\n onActivateItem,\n onOpen,\n onCloseAndDismissal,\n a11yDangerouslyConsumerOwnedMenuOpenState,\n buttonInheritedProps,\n innerRefSnatchingNode,\n menuSpecificProps,\n ],\n );\n};\n"],
5
+ "mappings": "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",
6
6
  "names": ["React"]
7
7
  }
@@ -40,8 +40,14 @@ const DSFlyoutMenu = (props) => {
40
40
  } = React2.useContext(MenuButtonContextProvider);
41
41
  const {
42
42
  instanceUid,
43
- globalEventsHelpers: { mainMenuRef, handleSubmenusRefChange }
43
+ globalEventsHelpers: { mainMenuRef, handleSubmenusRefChange },
44
+ openIntentPendingRef,
45
+ menuItemEventsHandlers: { handleMenuTabKeyDownCapture },
46
+ focusRegion,
47
+ trackFocusFirstChildItem,
48
+ propsWithDefault: { focusableNodes, a11yDangerouslyConsumerOwnedMenuOpenState }
44
49
  } = React2.useContext(MenuBehaviouralContextProviderContext);
50
+ const lastMountedRootNodeRef = React2.useRef(null);
45
51
  const [didAnimationEnd, setDidAnimationEnd] = React2.useState(false);
46
52
  const { setFloatingRef, floatingStyles, floatingContext, ItemRenderer, isMenuOpen, itemNode } = propsWithDefault;
47
53
  const ariaPositionsByDsId = React2.useMemo(
@@ -53,11 +59,27 @@ const DSFlyoutMenu = (props) => {
53
59
  (node) => {
54
60
  if (isRootNode(itemNode)) {
55
61
  mainMenuRef.current = node;
62
+ if (node && node !== lastMountedRootNodeRef.current) {
63
+ lastMountedRootNodeRef.current = node;
64
+ const wasIntentArmed = openIntentPendingRef.current;
65
+ openIntentPendingRef.current = false;
66
+ if (a11yDangerouslyConsumerOwnedMenuOpenState !== void 0 && !wasIntentArmed && !focusRegion.startsWith("item-") && focusableNodes.length > 0)
67
+ trackFocusFirstChildItem(itemNode);
68
+ }
56
69
  return;
57
70
  }
58
71
  handleSubmenusRefChange(node, itemNode.dsId);
59
72
  },
60
- [handleSubmenusRefChange, itemNode, mainMenuRef]
73
+ [
74
+ a11yDangerouslyConsumerOwnedMenuOpenState,
75
+ focusableNodes,
76
+ focusRegion,
77
+ handleSubmenusRefChange,
78
+ itemNode,
79
+ mainMenuRef,
80
+ openIntentPendingRef,
81
+ trackFocusFirstChildItem
82
+ ]
61
83
  );
62
84
  const handleAnimationStartTrigger = React2.useCallback(() => {
63
85
  setDidAnimationEnd(false);
@@ -81,6 +103,7 @@ const DSFlyoutMenu = (props) => {
81
103
  StyledMenu,
82
104
  {
83
105
  role: "menu",
106
+ onKeyDownCapture: isRootNode(itemNode) ? handleMenuTabKeyDownCapture : void 0,
84
107
  innerRef: handleRefChange,
85
108
  $maxHeight: itemNode.plainItem.maxHeight ?? menuSpecificDefaultProps.maxHeight,
86
109
  getOwnerProps,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../scripts/build/transpile/react-shim.js", "../../../../src/parts/DSFlyoutMenu/DSFlyoutMenu.tsx"],
4
- "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "import { FloatingWrapper } from '@elliemae/ds-floating-context';\nimport { Grid } from '@elliemae/ds-grid';\nimport { describe } from '@elliemae/ds-props-helpers';\nimport { styled, xStyledCommonProps } from '@elliemae/ds-system';\n\nimport React from 'react';\nimport { type DSMenuButtonT, menuSpecificDefaultProps } from '../../react-desc-prop-types.js';\nimport { MenuBehaviouralContextProviderContext } from '../DSMenuBehaviouralContextProvider/MenuBehaviouralContextProviderCTX.js';\nimport { DSMenuItemRendererFactory } from '../DSMenuItemRendererFactory/index.js';\nimport { useFlyoutMenu } from './config/useFlyoutMenu.js';\nimport { DSFlyoutMenuName, FLYOUT_MENU_SLOTS } from './constants/index.js';\nimport { DSFlyoutMenuPropTypesSchema, type DSFlyoutMenuT } from './react-desc-prop-types.js';\nimport { getAriaSetSizeAndPosInSetBySiblingList, isRootNode } from '../../utils/nodesTypeguardsAndGetters.js';\nimport { MenuButtonContextProvider } from '../../MenuButtonContextProviderCTX.js';\nconst StyledWrapper = styled(Grid, {\n name: DSFlyoutMenuName,\n slot: FLYOUT_MENU_SLOTS.ROOT,\n})`\n min-width: 1px;\n background-color: white;\n padding: ${({ theme }) => theme.space.xxxs} 0;\n ${xStyledCommonProps}\n`;\n\nconst StyledFloatingWrapper = styled(FloatingWrapper, {\n name: DSFlyoutMenuName,\n slot: FLYOUT_MENU_SLOTS.FLOATING_WRAPPER,\n})``;\n\nconst StyledMenu = styled('div', { name: DSFlyoutMenuName, slot: FLYOUT_MENU_SLOTS.LIST_WRAPPER })<{\n $maxHeight: string | number;\n}>`\n max-height: ${({ $maxHeight }) => (typeof $maxHeight === 'number' ? `${$maxHeight}px` : $maxHeight)};\n overflow-y: auto;\n padding: 0;\n margin: 0;\n`;\n\nconst NoComponentPlaceholder = () => null;\n\nconst DSFlyoutMenu: React.ComponentType<DSFlyoutMenuT.Props> = (props) => {\n const { propsWithDefault, xstyledProps } = useFlyoutMenu(props);\n const {\n ownerProps: { getOwnerProps, getOwnerPropsArguments },\n } = React.useContext(MenuButtonContextProvider);\n const {\n instanceUid,\n globalEventsHelpers: { mainMenuRef, handleSubmenusRefChange },\n } = React.useContext(MenuBehaviouralContextProviderContext);\n const [didAnimationEnd, setDidAnimationEnd] = React.useState(false);\n\n const { setFloatingRef, floatingStyles, floatingContext, ItemRenderer, isMenuOpen, itemNode } = propsWithDefault;\n const ariaPositionsByDsId = React.useMemo(\n // DSTree doesn't support children having a polymorphic type, so we need to cast it manually (same as below)\n () => getAriaSetSizeAndPosInSetBySiblingList(itemNode.children as DSMenuButtonT.MenuNode[]),\n [itemNode.children],\n );\n const handleRefChange = React.useCallback(\n (node: HTMLDivElement) => {\n if (isRootNode(itemNode)) {\n mainMenuRef.current = node;\n return;\n }\n handleSubmenusRefChange(node, itemNode.dsId);\n },\n [handleSubmenusRefChange, itemNode, mainMenuRef],\n );\n\n const handleAnimationStartTrigger = React.useCallback(() => {\n setDidAnimationEnd(false);\n }, []);\n const handleAnimationEnd = React.useCallback(() => {\n setDidAnimationEnd(true);\n }, []);\n\n return (\n <StyledFloatingWrapper\n innerRef={setFloatingRef}\n floatingStyles={floatingStyles}\n isOpen={isMenuOpen}\n context={floatingContext}\n onAnimationStartTriggered={handleAnimationStartTrigger}\n onAnimationEnd={handleAnimationEnd}\n getOwnerProps={getOwnerProps}\n getOwnerPropsArguments={getOwnerPropsArguments}\n // Adding a data-testid for legacy e2e tests\n data-testid=\"ds-floating-wrapper-root\"\n >\n <StyledWrapper getOwnerProps={getOwnerProps} getOwnerPropsArguments={getOwnerPropsArguments} {...xstyledProps}>\n <StyledMenu\n role=\"menu\"\n innerRef={handleRefChange}\n $maxHeight={itemNode.plainItem.maxHeight ?? menuSpecificDefaultProps.maxHeight}\n getOwnerProps={getOwnerProps}\n getOwnerPropsArguments={getOwnerPropsArguments}\n >\n {itemNode.children.map((optionNode) => {\n const ariaPosition = ariaPositionsByDsId.get(optionNode.dsId);\n return (\n <DSMenuItemRendererFactory\n key={`flyout-menu-item-${optionNode.dsId}-${instanceUid}`}\n // DSTree doesn't support children having a polymorphic type, so we need to cast it manually\n itemNode={optionNode as DSMenuButtonT.MenuNode}\n ItemRenderer={ItemRenderer}\n // we are passing the DSFlyoutMenu component as a prop to the DSMenuItemRendererFactory\n // this solves the circular-ish dependency\n // the conditional `didAnimationEnd` solves the visual artefact of nested FloatingWrapper wrongly positioned due to animation\n FlyoutMenuCircularDepInject={didAnimationEnd ? DSFlyoutMenu : NoComponentPlaceholder}\n aria-posinset={ariaPosition?.ariaPosInSet}\n aria-setsize={ariaPosition?.ariaSetSize}\n />\n );\n })}\n </StyledMenu>\n </StyledWrapper>\n </StyledFloatingWrapper>\n );\n};\n\nDSFlyoutMenu.displayName = DSFlyoutMenuName;\nconst DSFlyoutMenuWithSchema = describe(DSFlyoutMenu);\nDSFlyoutMenuWithSchema.propTypes = DSFlyoutMenuPropTypesSchema;\n\nexport { DSFlyoutMenu, DSFlyoutMenuWithSchema };\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "import { FloatingWrapper } from '@elliemae/ds-floating-context';\nimport { Grid } from '@elliemae/ds-grid';\nimport { describe } from '@elliemae/ds-props-helpers';\nimport { styled, xStyledCommonProps } from '@elliemae/ds-system';\n\nimport React from 'react';\nimport { type DSMenuButtonT, menuSpecificDefaultProps } from '../../react-desc-prop-types.js';\nimport { MenuBehaviouralContextProviderContext } from '../DSMenuBehaviouralContextProvider/MenuBehaviouralContextProviderCTX.js';\nimport { DSMenuItemRendererFactory } from '../DSMenuItemRendererFactory/index.js';\nimport { useFlyoutMenu } from './config/useFlyoutMenu.js';\nimport { DSFlyoutMenuName, FLYOUT_MENU_SLOTS } from './constants/index.js';\nimport { DSFlyoutMenuPropTypesSchema, type DSFlyoutMenuT } from './react-desc-prop-types.js';\nimport { getAriaSetSizeAndPosInSetBySiblingList, isRootNode } from '../../utils/nodesTypeguardsAndGetters.js';\nimport { MenuButtonContextProvider } from '../../MenuButtonContextProviderCTX.js';\nconst StyledWrapper = styled(Grid, {\n name: DSFlyoutMenuName,\n slot: FLYOUT_MENU_SLOTS.ROOT,\n})`\n min-width: 1px;\n background-color: white;\n padding: ${({ theme }) => theme.space.xxxs} 0;\n ${xStyledCommonProps}\n`;\n\nconst StyledFloatingWrapper = styled(FloatingWrapper, {\n name: DSFlyoutMenuName,\n slot: FLYOUT_MENU_SLOTS.FLOATING_WRAPPER,\n})``;\n\nconst StyledMenu = styled('div', { name: DSFlyoutMenuName, slot: FLYOUT_MENU_SLOTS.LIST_WRAPPER })<{\n $maxHeight: string | number;\n}>`\n max-height: ${({ $maxHeight }) => (typeof $maxHeight === 'number' ? `${$maxHeight}px` : $maxHeight)};\n overflow-y: auto;\n padding: 0;\n margin: 0;\n`;\n\nconst NoComponentPlaceholder = () => null;\n\nconst DSFlyoutMenu: React.ComponentType<DSFlyoutMenuT.Props> = (props) => {\n const { propsWithDefault, xstyledProps } = useFlyoutMenu(props);\n const {\n ownerProps: { getOwnerProps, getOwnerPropsArguments },\n } = React.useContext(MenuButtonContextProvider);\n const {\n instanceUid,\n globalEventsHelpers: { mainMenuRef, handleSubmenusRefChange },\n openIntentPendingRef,\n menuItemEventsHandlers: { handleMenuTabKeyDownCapture },\n focusRegion,\n trackFocusFirstChildItem,\n propsWithDefault: { focusableNodes, a11yDangerouslyConsumerOwnedMenuOpenState },\n } = React.useContext(MenuBehaviouralContextProviderContext);\n const lastMountedRootNodeRef = React.useRef<HTMLDivElement | null>(null);\n const [didAnimationEnd, setDidAnimationEnd] = React.useState(false);\n\n const { setFloatingRef, floatingStyles, floatingContext, ItemRenderer, isMenuOpen, itemNode } = propsWithDefault;\n const ariaPositionsByDsId = React.useMemo(\n // DSTree doesn't support children having a polymorphic type, so we need to cast it manually (same as below)\n () => getAriaSetSizeAndPosInSetBySiblingList(itemNode.children as DSMenuButtonT.MenuNode[]),\n [itemNode.children],\n );\n const handleRefChange = React.useCallback(\n (node: HTMLDivElement) => {\n if (isRootNode(itemNode)) {\n mainMenuRef.current = node;\n // The menu just mounted (open). When the consumer owns the open state, a menu can be opened by something that\n // never went through the widget's own intents (an external button, an effect...), so nobody declared where the\n // focus goes. Declare the default intent, like every open does: \"the first item\" \u2014 the items focus\n // themselves from their own ref callbacks, as they do for every other focus intent.\n // - only on a real mount: the ref re-attaches the SAME node whenever the options change while open\n // - never when an open intent already armed the tracker, nor when focus is already on an item\n if (node && node !== lastMountedRootNodeRef.current) {\n lastMountedRootNodeRef.current = node;\n const wasIntentArmed = openIntentPendingRef.current;\n // eslint-disable-next-line no-param-reassign\n openIntentPendingRef.current = false;\n if (\n a11yDangerouslyConsumerOwnedMenuOpenState !== undefined &&\n !wasIntentArmed &&\n !focusRegion.startsWith('item-') &&\n focusableNodes.length > 0\n )\n trackFocusFirstChildItem(itemNode as unknown as DSMenuButtonT.PseudoFocusableMenuNodes);\n }\n return;\n }\n handleSubmenusRefChange(node, itemNode.dsId);\n },\n [\n a11yDangerouslyConsumerOwnedMenuOpenState,\n focusableNodes,\n focusRegion,\n handleSubmenusRefChange,\n itemNode,\n mainMenuRef,\n openIntentPendingRef,\n trackFocusFirstChildItem,\n ],\n );\n\n const handleAnimationStartTrigger = React.useCallback(() => {\n setDidAnimationEnd(false);\n }, []);\n const handleAnimationEnd = React.useCallback(() => {\n setDidAnimationEnd(true);\n }, []);\n\n return (\n <StyledFloatingWrapper\n innerRef={setFloatingRef}\n floatingStyles={floatingStyles}\n isOpen={isMenuOpen}\n context={floatingContext}\n onAnimationStartTriggered={handleAnimationStartTrigger}\n onAnimationEnd={handleAnimationEnd}\n getOwnerProps={getOwnerProps}\n getOwnerPropsArguments={getOwnerPropsArguments}\n // Adding a data-testid for legacy e2e tests\n data-testid=\"ds-floating-wrapper-root\"\n >\n <StyledWrapper getOwnerProps={getOwnerProps} getOwnerPropsArguments={getOwnerPropsArguments} {...xstyledProps}>\n <StyledMenu\n role=\"menu\"\n // Tab is handled in the capture phase of the ROOT menu only: nested submenus are React descendants of it (their\n // events reach it too), attaching it to them as well would run the handler twice per keypress\n onKeyDownCapture={isRootNode(itemNode) ? handleMenuTabKeyDownCapture : undefined}\n innerRef={handleRefChange}\n $maxHeight={itemNode.plainItem.maxHeight ?? menuSpecificDefaultProps.maxHeight}\n getOwnerProps={getOwnerProps}\n getOwnerPropsArguments={getOwnerPropsArguments}\n >\n {itemNode.children.map((optionNode) => {\n const ariaPosition = ariaPositionsByDsId.get(optionNode.dsId);\n return (\n <DSMenuItemRendererFactory\n key={`flyout-menu-item-${optionNode.dsId}-${instanceUid}`}\n // DSTree doesn't support children having a polymorphic type, so we need to cast it manually\n itemNode={optionNode as DSMenuButtonT.MenuNode}\n ItemRenderer={ItemRenderer}\n // we are passing the DSFlyoutMenu component as a prop to the DSMenuItemRendererFactory\n // this solves the circular-ish dependency\n // the conditional `didAnimationEnd` solves the visual artefact of nested FloatingWrapper wrongly positioned due to animation\n FlyoutMenuCircularDepInject={didAnimationEnd ? DSFlyoutMenu : NoComponentPlaceholder}\n aria-posinset={ariaPosition?.ariaPosInSet}\n aria-setsize={ariaPosition?.ariaSetSize}\n />\n );\n })}\n </StyledMenu>\n </StyledWrapper>\n </StyledFloatingWrapper>\n );\n};\n\nDSFlyoutMenu.displayName = DSFlyoutMenuName;\nconst DSFlyoutMenuWithSchema = describe(DSFlyoutMenu);\nDSFlyoutMenuWithSchema.propTypes = DSFlyoutMenuPropTypesSchema;\n\nexport { DSFlyoutMenu, DSFlyoutMenuWithSchema };\n"],
5
+ "mappings": "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",
6
6
  "names": ["React"]
7
7
  }
@@ -12,6 +12,7 @@ import { MENU_FOCUS_REGIONS } from "../constants/index.js";
12
12
  import { getFocusableSiblingsList } from "../utils/nodeGettersByCriterias.js";
13
13
  const useFocusTracker = () => {
14
14
  const shouldFocusImmediately = React2.useRef(false);
15
+ const shouldRefocusTrigger = React2.useRef(true);
15
16
  const [focusRegion, setFocusRegion] = React2.useState("");
16
17
  const focusedRegionPerformanceHelper = React2.useRef("");
17
18
  const preventBlurReset = React2.useRef(false);
@@ -25,6 +26,7 @@ const useFocusTracker = () => {
25
26
  focusedRegionPerformanceHelper.current = newFocusRegion;
26
27
  focusedElementItemNode.current = focusNode;
27
28
  shouldFocusImmediately.current = flags?.shouldFocusImmediately ?? false;
29
+ shouldRefocusTrigger.current = flags?.refocusTrigger ?? true;
28
30
  return focusNode;
29
31
  },
30
32
  50,
@@ -33,7 +35,7 @@ const useFocusTracker = () => {
33
35
  []
34
36
  );
35
37
  const trackFocusTrigger = React2.useCallback(
36
- (options = { shouldFocusImmediately: false }) => racingConditionDebounceTrackFocus(MENU_FOCUS_REGIONS.TRIGGER, null, options),
38
+ (options = {}) => racingConditionDebounceTrackFocus(MENU_FOCUS_REGIONS.TRIGGER, null, options),
37
39
  [racingConditionDebounceTrackFocus]
38
40
  );
39
41
  const trackFocusNode = React2.useCallback(
@@ -140,6 +142,7 @@ const useFocusTracker = () => {
140
142
  focusRegion,
141
143
  focusedElementItemNode,
142
144
  shouldFocusImmediately,
145
+ shouldRefocusTrigger,
143
146
  trackFocusTrigger,
144
147
  trackFocusNode,
145
148
  trackFocusRegionReset,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../scripts/build/transpile/react-shim.js", "../../../../../src/parts/DSMenuBehaviouralContextProvider/config/useFocusTracker.ts"],
4
- "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* eslint-disable no-console */\n/* eslint-disable max-lines */\n/* eslint-disable max-statements */\n/* eslint-disable complexity */\n/* eslint-disable prettier/prettier */\nimport { throttle } from 'lodash-es';\nimport React from 'react';\nimport type { DSMenuButtonT } from '../../../react-desc-prop-types.js';\nimport {\n isFocusableNode,\n isGroup,\n isMenuNodeAllowedToHaveChildren,\n isRootNode,\n} from '../../../utils/nodesTypeguardsAndGetters.js';\nimport { TREE_STRUCTURE_ERRORS } from '../constants/Errors.js';\nimport { MENU_FOCUS_REGIONS } from '../constants/index.js';\nimport { getFocusableSiblingsList } from '../utils/nodeGettersByCriterias.js';\n\ntype MenuFocusRegionsValues = (typeof MENU_FOCUS_REGIONS)[keyof typeof MENU_FOCUS_REGIONS];\n// if MenuFocusRegionsValues may be a string or a function that returns a string, we want only the resolved strings\ntype ValidRegionsValues<T extends MenuFocusRegionsValues = MenuFocusRegionsValues> = T extends string\n ? T\n : // eslint-disable-next-line @typescript-eslint/no-explicit-any\n T extends (...args: any) => any\n ? ReturnType<T>\n : never;\n\nexport const useFocusTracker = () => {\n const shouldFocusImmediately = React.useRef(false);\n const [focusRegion, setFocusRegion] = React.useState<ValidRegionsValues>('');\n // we want to keep the focus region trackers as stable as possible to avoid unnecessary re-renders\n // there is no need to change the trackers reference when the focus region changes,\n // since changing the focus region is always triggered by a final user interaction (so after reacts reconciliation)\n const focusedRegionPerformanceHelper = React.useRef('') as React.MutableRefObject<ValidRegionsValues>;\n const preventBlurReset = React.useRef(false);\n\n const focusedElementItemNode = React.useRef(\n null,\n ) as React.MutableRefObject<DSMenuButtonT.PseudoFocusableMenuNodes | null>;\n\n // typescript with debounce doesn't work well, so we need to disable the exhaustive deps rule here\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const racingConditionDebounceTrackFocus = React.useCallback(\n throttle(\n (\n newFocusRegion: ValidRegionsValues,\n focusNode: DSMenuButtonT.PseudoFocusableMenuNodes | null,\n flags?: { shouldFocusImmediately?: boolean },\n ) => {\n setFocusRegion(newFocusRegion);\n focusedRegionPerformanceHelper.current = newFocusRegion;\n focusedElementItemNode.current = focusNode;\n\n shouldFocusImmediately.current = flags?.shouldFocusImmediately ?? false;\n\n return focusNode;\n },\n 50,\n { leading: true, trailing: true },\n ),\n [],\n );\n\n const trackFocusTrigger = React.useCallback(\n (options = { shouldFocusImmediately: false }) =>\n racingConditionDebounceTrackFocus(MENU_FOCUS_REGIONS.TRIGGER, null, options),\n [racingConditionDebounceTrackFocus],\n );\n\n const trackFocusNode = React.useCallback(\n (nodeToFocus: DSMenuButtonT.PseudoFocusableMenuNodes) => {\n const newFocusRegion = MENU_FOCUS_REGIONS.ITEM_BY_DSID(nodeToFocus.dsId);\n racingConditionDebounceTrackFocus(newFocusRegion, nodeToFocus);\n },\n [racingConditionDebounceTrackFocus],\n );\n\n const trackFocusRegionReset = React.useCallback(\n () => racingConditionDebounceTrackFocus(MENU_FOCUS_REGIONS.RESET, null),\n [racingConditionDebounceTrackFocus],\n );\n\n const trackFocusFirstChildItem = React.useCallback(\n (itemNode: DSMenuButtonT.PseudoFocusableMenuNodes) => {\n if (!isMenuNodeAllowedToHaveChildren(itemNode)) {\n console.log('focus first child item > itemNode', itemNode);\n throw TREE_STRUCTURE_ERRORS.NODE_CANNOT_HAVE_CHILDREN;\n }\n if (itemNode.children.length === 0) {\n console.log(itemNode);\n throw new Error('No children found in the item node');\n }\n const focusableChildrenNodes = getFocusableSiblingsList(itemNode.children[0]);\n if (focusableChildrenNodes.length === 0) {\n console.log('focus first child item > itemNode', itemNode);\n throw new Error('No focusable nodes found in the children of the item node');\n }\n\n const newFocusedNode = focusableChildrenNodes[0];\n return racingConditionDebounceTrackFocus(MENU_FOCUS_REGIONS.ITEM_BY_DSID(newFocusedNode.dsId), newFocusedNode);\n },\n [racingConditionDebounceTrackFocus],\n );\n\n const trackFocusLastChildItem = React.useCallback(\n (itemNode: DSMenuButtonT.PseudoFocusableMenuNodes) => {\n if (!isMenuNodeAllowedToHaveChildren(itemNode)) {\n console.log('focus last child item > itemNode', itemNode);\n throw new Error('Item node is not allowed to have children');\n }\n if (itemNode.children.length === 0) {\n console.log(itemNode);\n throw new Error('No children found in the item node');\n }\n const focusableChildrenNodes = getFocusableSiblingsList(itemNode.children[0]);\n if (focusableChildrenNodes.length === 0) {\n console.log('focus last child item > itemNode', itemNode);\n throw new Error('No focusable nodes found in the children of the item node');\n }\n return racingConditionDebounceTrackFocus(\n MENU_FOCUS_REGIONS.ITEM_BY_DSID(focusableChildrenNodes[focusableChildrenNodes.length - 1].dsId),\n focusableChildrenNodes[focusableChildrenNodes.length - 1],\n );\n },\n [racingConditionDebounceTrackFocus],\n );\n\n const trackFocusNextItem = React.useCallback(\n (itemNode: DSMenuButtonT.PseudoFocusableMenuNodes) => {\n const focusableSiblingsNodes = getFocusableSiblingsList(itemNode);\n // we find the current item node index in the focusableSiblingsNodes array\n const currIndex = focusableSiblingsNodes.findIndex((node) => node.dsId === itemNode.dsId);\n\n const nextIndex = currIndex + 1 < focusableSiblingsNodes.length ? currIndex + 1 : 0;\n const newFocusedNode = focusableSiblingsNodes[nextIndex];\n return racingConditionDebounceTrackFocus(MENU_FOCUS_REGIONS.ITEM_BY_DSID(newFocusedNode.dsId), newFocusedNode);\n },\n [racingConditionDebounceTrackFocus],\n );\n\n const trackFocusPreviousItem = React.useCallback(\n (itemNode: DSMenuButtonT.PseudoFocusableMenuNodes) => {\n const focusableSiblingsNodes = getFocusableSiblingsList(itemNode);\n // we find the current item node index in the focusableSiblingsNodes array\n const currIndex = focusableSiblingsNodes.findIndex((node) => node.dsId === itemNode.dsId);\n\n const previousIndex = currIndex - 1 >= 0 ? currIndex - 1 : focusableSiblingsNodes.length - 1;\n const newFocusNode = focusableSiblingsNodes[previousIndex];\n return racingConditionDebounceTrackFocus(MENU_FOCUS_REGIONS.ITEM_BY_DSID(newFocusNode.dsId), newFocusNode);\n },\n [racingConditionDebounceTrackFocus],\n );\n\n const trackFocusParentItem = React.useCallback(\n (itemNode: DSMenuButtonT.PseudoFocusableMenuNodes) => {\n const { parent } = itemNode;\n // this typecast is required because we are reading the parent property from the itemNode\n // while this function can receive any PseudoFocusableMenuNodes,\n // the parent property may be a non-pseudo focusable node (specifically when the parent property is group node)\n const parentNode = parent;\n if (!parentNode) {\n console.log('focus parent item', { itemNode, parentNode });\n throw new Error(`No parent node found for the item node`);\n }\n\n let nodeToFocus: DSMenuButtonT.PseudoFocusableMenuNodes | null = parentNode;\n // if parent is SingleSelectGroupNode then we can't focus it\n if (isGroup(parentNode)) {\n const groupParent = parentNode.parent;\n if (!isFocusableNode(groupParent) && !isRootNode(groupParent)) {\n console.log('focus parent item', { itemNode, parentNode, groupParent });\n throw new Error('No focusable parent node found for the item node');\n }\n nodeToFocus = groupParent;\n }\n\n const focusableNode = isRootNode(nodeToFocus) ? null : nodeToFocus;\n const newFocusNode = focusableNode;\n if (!newFocusNode) trackFocusTrigger();\n else trackFocusNode(newFocusNode);\n\n return focusableNode;\n },\n [trackFocusNode, trackFocusTrigger],\n );\n\n return React.useMemo(\n () => ({\n preventBlurReset,\n focusRegion,\n focusedElementItemNode,\n shouldFocusImmediately,\n trackFocusTrigger,\n trackFocusNode,\n trackFocusRegionReset,\n trackFocusFirstChildItem,\n trackFocusLastChildItem,\n trackFocusNextItem,\n trackFocusPreviousItem,\n trackFocusParentItem,\n }),\n [\n focusRegion,\n trackFocusFirstChildItem,\n trackFocusLastChildItem,\n trackFocusNextItem,\n trackFocusNode,\n trackFocusParentItem,\n trackFocusPreviousItem,\n trackFocusRegionReset,\n trackFocusTrigger,\n ],\n );\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "/* eslint-disable no-console */\n/* eslint-disable max-lines */\n/* eslint-disable max-statements */\n/* eslint-disable complexity */\n/* eslint-disable prettier/prettier */\nimport { throttle } from 'lodash-es';\nimport React from 'react';\nimport type { DSMenuButtonT } from '../../../react-desc-prop-types.js';\nimport {\n isFocusableNode,\n isGroup,\n isMenuNodeAllowedToHaveChildren,\n isRootNode,\n} from '../../../utils/nodesTypeguardsAndGetters.js';\nimport { TREE_STRUCTURE_ERRORS } from '../constants/Errors.js';\nimport { MENU_FOCUS_REGIONS } from '../constants/index.js';\nimport { getFocusableSiblingsList } from '../utils/nodeGettersByCriterias.js';\n\ntype MenuFocusRegionsValues = (typeof MENU_FOCUS_REGIONS)[keyof typeof MENU_FOCUS_REGIONS];\n// if MenuFocusRegionsValues may be a string or a function that returns a string, we want only the resolved strings\ntype ValidRegionsValues<T extends MenuFocusRegionsValues = MenuFocusRegionsValues> = T extends string\n ? T\n : // eslint-disable-next-line @typescript-eslint/no-explicit-any\n T extends (...args: any) => any\n ? ReturnType<T>\n : never;\n\nexport const useFocusTracker = () => {\n const shouldFocusImmediately = React.useRef(false);\n /*\n * Separates \"recording that focus is on the trigger\" from \"requesting focus return\n * to the trigger\": the trigger's own focus event tracks the region passively, while\n * menu-driven intents (close, keyboard nav) actively refocus. Without the split, the\n * deferred refocus can land after focus already legitimately moved on (e.g. the user\n * Tabbed away) and steal it back.\n */\n const shouldRefocusTrigger = React.useRef(true);\n const [focusRegion, setFocusRegion] = React.useState<ValidRegionsValues>('');\n // we want to keep the focus region trackers as stable as possible to avoid unnecessary re-renders\n // there is no need to change the trackers reference when the focus region changes,\n // since changing the focus region is always triggered by a final user interaction (so after reacts reconciliation)\n const focusedRegionPerformanceHelper = React.useRef('') as React.MutableRefObject<ValidRegionsValues>;\n const preventBlurReset = React.useRef(false);\n\n const focusedElementItemNode = React.useRef(\n null,\n ) as React.MutableRefObject<DSMenuButtonT.PseudoFocusableMenuNodes | null>;\n\n // typescript with debounce doesn't work well, so we need to disable the exhaustive deps rule here\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const racingConditionDebounceTrackFocus = React.useCallback(\n throttle(\n (\n newFocusRegion: ValidRegionsValues,\n focusNode: DSMenuButtonT.PseudoFocusableMenuNodes | null,\n flags?: { shouldFocusImmediately?: boolean; refocusTrigger?: boolean },\n ) => {\n setFocusRegion(newFocusRegion);\n focusedRegionPerformanceHelper.current = newFocusRegion;\n focusedElementItemNode.current = focusNode;\n\n shouldFocusImmediately.current = flags?.shouldFocusImmediately ?? false;\n shouldRefocusTrigger.current = flags?.refocusTrigger ?? true;\n\n return focusNode;\n },\n 50,\n { leading: true, trailing: true },\n ),\n [],\n );\n\n const trackFocusTrigger = React.useCallback(\n (options: { shouldFocusImmediately?: boolean; refocusTrigger?: boolean } = {}) =>\n racingConditionDebounceTrackFocus(MENU_FOCUS_REGIONS.TRIGGER, null, options),\n [racingConditionDebounceTrackFocus],\n );\n\n const trackFocusNode = React.useCallback(\n (nodeToFocus: DSMenuButtonT.PseudoFocusableMenuNodes) => {\n const newFocusRegion = MENU_FOCUS_REGIONS.ITEM_BY_DSID(nodeToFocus.dsId);\n racingConditionDebounceTrackFocus(newFocusRegion, nodeToFocus);\n },\n [racingConditionDebounceTrackFocus],\n );\n\n const trackFocusRegionReset = React.useCallback(\n () => racingConditionDebounceTrackFocus(MENU_FOCUS_REGIONS.RESET, null),\n [racingConditionDebounceTrackFocus],\n );\n\n const trackFocusFirstChildItem = React.useCallback(\n (itemNode: DSMenuButtonT.PseudoFocusableMenuNodes) => {\n if (!isMenuNodeAllowedToHaveChildren(itemNode)) {\n console.log('focus first child item > itemNode', itemNode);\n throw TREE_STRUCTURE_ERRORS.NODE_CANNOT_HAVE_CHILDREN;\n }\n if (itemNode.children.length === 0) {\n console.log(itemNode);\n throw new Error('No children found in the item node');\n }\n const focusableChildrenNodes = getFocusableSiblingsList(itemNode.children[0]);\n if (focusableChildrenNodes.length === 0) {\n console.log('focus first child item > itemNode', itemNode);\n throw new Error('No focusable nodes found in the children of the item node');\n }\n\n const newFocusedNode = focusableChildrenNodes[0];\n return racingConditionDebounceTrackFocus(MENU_FOCUS_REGIONS.ITEM_BY_DSID(newFocusedNode.dsId), newFocusedNode);\n },\n [racingConditionDebounceTrackFocus],\n );\n\n const trackFocusLastChildItem = React.useCallback(\n (itemNode: DSMenuButtonT.PseudoFocusableMenuNodes) => {\n if (!isMenuNodeAllowedToHaveChildren(itemNode)) {\n console.log('focus last child item > itemNode', itemNode);\n throw new Error('Item node is not allowed to have children');\n }\n if (itemNode.children.length === 0) {\n console.log(itemNode);\n throw new Error('No children found in the item node');\n }\n const focusableChildrenNodes = getFocusableSiblingsList(itemNode.children[0]);\n if (focusableChildrenNodes.length === 0) {\n console.log('focus last child item > itemNode', itemNode);\n throw new Error('No focusable nodes found in the children of the item node');\n }\n return racingConditionDebounceTrackFocus(\n MENU_FOCUS_REGIONS.ITEM_BY_DSID(focusableChildrenNodes[focusableChildrenNodes.length - 1].dsId),\n focusableChildrenNodes[focusableChildrenNodes.length - 1],\n );\n },\n [racingConditionDebounceTrackFocus],\n );\n\n const trackFocusNextItem = React.useCallback(\n (itemNode: DSMenuButtonT.PseudoFocusableMenuNodes) => {\n const focusableSiblingsNodes = getFocusableSiblingsList(itemNode);\n // we find the current item node index in the focusableSiblingsNodes array\n const currIndex = focusableSiblingsNodes.findIndex((node) => node.dsId === itemNode.dsId);\n\n const nextIndex = currIndex + 1 < focusableSiblingsNodes.length ? currIndex + 1 : 0;\n const newFocusedNode = focusableSiblingsNodes[nextIndex];\n return racingConditionDebounceTrackFocus(MENU_FOCUS_REGIONS.ITEM_BY_DSID(newFocusedNode.dsId), newFocusedNode);\n },\n [racingConditionDebounceTrackFocus],\n );\n\n const trackFocusPreviousItem = React.useCallback(\n (itemNode: DSMenuButtonT.PseudoFocusableMenuNodes) => {\n const focusableSiblingsNodes = getFocusableSiblingsList(itemNode);\n // we find the current item node index in the focusableSiblingsNodes array\n const currIndex = focusableSiblingsNodes.findIndex((node) => node.dsId === itemNode.dsId);\n\n const previousIndex = currIndex - 1 >= 0 ? currIndex - 1 : focusableSiblingsNodes.length - 1;\n const newFocusNode = focusableSiblingsNodes[previousIndex];\n return racingConditionDebounceTrackFocus(MENU_FOCUS_REGIONS.ITEM_BY_DSID(newFocusNode.dsId), newFocusNode);\n },\n [racingConditionDebounceTrackFocus],\n );\n\n const trackFocusParentItem = React.useCallback(\n (itemNode: DSMenuButtonT.PseudoFocusableMenuNodes) => {\n const { parent } = itemNode;\n // this typecast is required because we are reading the parent property from the itemNode\n // while this function can receive any PseudoFocusableMenuNodes,\n // the parent property may be a non-pseudo focusable node (specifically when the parent property is group node)\n const parentNode = parent;\n if (!parentNode) {\n console.log('focus parent item', { itemNode, parentNode });\n throw new Error(`No parent node found for the item node`);\n }\n\n let nodeToFocus: DSMenuButtonT.PseudoFocusableMenuNodes | null = parentNode;\n // if parent is SingleSelectGroupNode then we can't focus it\n if (isGroup(parentNode)) {\n const groupParent = parentNode.parent;\n if (!isFocusableNode(groupParent) && !isRootNode(groupParent)) {\n console.log('focus parent item', { itemNode, parentNode, groupParent });\n throw new Error('No focusable parent node found for the item node');\n }\n nodeToFocus = groupParent;\n }\n\n const focusableNode = isRootNode(nodeToFocus) ? null : nodeToFocus;\n const newFocusNode = focusableNode;\n if (!newFocusNode) trackFocusTrigger();\n else trackFocusNode(newFocusNode);\n\n return focusableNode;\n },\n [trackFocusNode, trackFocusTrigger],\n );\n\n return React.useMemo(\n () => ({\n preventBlurReset,\n focusRegion,\n focusedElementItemNode,\n shouldFocusImmediately,\n shouldRefocusTrigger,\n trackFocusTrigger,\n trackFocusNode,\n trackFocusRegionReset,\n trackFocusFirstChildItem,\n trackFocusLastChildItem,\n trackFocusNextItem,\n trackFocusPreviousItem,\n trackFocusParentItem,\n }),\n [\n focusRegion,\n trackFocusFirstChildItem,\n trackFocusLastChildItem,\n trackFocusNextItem,\n trackFocusNode,\n trackFocusParentItem,\n trackFocusPreviousItem,\n trackFocusRegionReset,\n trackFocusTrigger,\n ],\n );\n};\n"],
5
+ "mappings": "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",
6
6
  "names": ["React"]
7
7
  }
@@ -21,19 +21,20 @@ const useGlobalEvents = ({
21
21
  );
22
22
  React2.useEffect(() => {
23
23
  const closeMenuAndSubmenus = (e) => {
24
+ if (!isMenuOpen) return;
24
25
  handleChangeOpenedSubItems([], { itemNode: null, event: e });
25
- onOpinionatedClose({ skipFocus: true });
26
+ onOpinionatedClose({ skipFocus: true, reason: "window-blur", event: e });
26
27
  };
27
28
  const detectIfClickIsOutsideMenuAndCloseIfSo = (e) => {
28
29
  if (!isMenuOpen) return;
29
- if (buttonDOMNodeRef.current === e.target) return;
30
+ if (buttonDOMNodeRef.current?.contains(e.target)) return;
30
31
  const isClickInsideMainMenu = mainMenuRef.current?.contains(e.target);
31
32
  if (isClickInsideMainMenu) return;
32
33
  const submenuNodesToCheck = Object.values(submenusRefs.current).filter(Boolean);
33
34
  const isClickInsideAnySubmenu = submenuNodesToCheck.some((node) => node.contains(e.target));
34
35
  if (isClickInsideAnySubmenu) return;
35
36
  handleChangeOpenedSubItems([], { itemNode: null, event: e });
36
- onOpinionatedClose({ skipFocus: true });
37
+ onOpinionatedClose({ skipFocus: true, reason: "outside-click", event: e });
37
38
  };
38
39
  window.addEventListener("blur", closeMenuAndSubmenus);
39
40
  document.addEventListener("mousedown", detectIfClickIsOutsideMenuAndCloseIfSo);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../scripts/build/transpile/react-shim.js", "../../../../../src/parts/DSMenuBehaviouralContextProvider/config/useGlobalEvents.ts"],
4
- "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "import React from 'react';\nimport type { DSMenuButtonT } from '../../../react-desc-prop-types.js';\nimport type { useMenuOpenStatus } from './useMenuOpenStatus.js';\nimport type { DSMenuBehaviouralContextProviderT } from '../react-desc-prop-types.js';\n\ntype UseGlobalEventsConfig = {\n menuOpenStatus: ReturnType<typeof useMenuOpenStatus>;\n handleChangeOpenedSubItems: Required<DSMenuButtonT.MenuBehaviouralLayerOptionalProps>['onDisplayedSubmenuChange'];\n propsWithDefault: DSMenuBehaviouralContextProviderT.InternalProps;\n};\n\nexport const useGlobalEvents = ({\n menuOpenStatus,\n handleChangeOpenedSubItems,\n propsWithDefault,\n}: UseGlobalEventsConfig) => {\n const { onOpinionatedClose, isMenuOpen } = menuOpenStatus;\n const { buttonDOMNodeRef } = propsWithDefault;\n const mainMenuRef = React.useRef<HTMLDivElement | null>(null);\n // we are using \"portal\" but then we check the native evenet to check if \"on click outside\" the menu\n // for such reason, we need to track the current HTML NODES of the submenus (topmost parent)\n // this will allow us to check if the click event is outside the menu or any of it's submenus\n const submenusRefs = React.useRef<Record<DSMenuButtonT.WithSubmenuMenuNodes['dsId'], HTMLElement>>({});\n const handleSubmenusRefChange = React.useCallback(\n (htmlNode: HTMLElement | null, dsId: DSMenuButtonT.WithSubmenuMenuNodes['dsId']) => {\n if (htmlNode) {\n submenusRefs.current[dsId] = htmlNode;\n } else {\n delete submenusRefs.current[dsId];\n }\n },\n [],\n );\n React.useEffect(() => {\n // when the window loses focus, close all the menus and submenus\n const closeMenuAndSubmenus = (e: FocusEvent) => {\n handleChangeOpenedSubItems([], { itemNode: null, event: e });\n onOpinionatedClose({ skipFocus: true });\n };\n // when the user clicks \"outside\" the menu and submenus, close all the menus and submenus\n const detectIfClickIsOutsideMenuAndCloseIfSo = (e: MouseEvent | TouchEvent) => {\n // we are running this on literally ALL the clicks that happen on the window\n // we don't want to be consuming resources that are not strictly necessary\n // if the main menu is not open, we don't need to check if the click is outside the menu ofc\n if (!isMenuOpen) return;\n // we discard the fastest to check \"trigger\" click first\n if (buttonDOMNodeRef.current === e.target) return;\n // fastest check is the mainmenu, cuz we don't loop through all the submenus\n const isClickInsideMainMenu = mainMenuRef.current?.contains(e.target as Node);\n if (isClickInsideMainMenu) return;\n const submenuNodesToCheck = Object.values(submenusRefs.current).filter(Boolean);\n // we check if the event.target is a child of any of our known HTML nodes (via native .contains)\n const isClickInsideAnySubmenu = submenuNodesToCheck.some((node) => node.contains(e.target as Node));\n if (isClickInsideAnySubmenu) return;\n handleChangeOpenedSubItems([], { itemNode: null, event: e });\n onOpinionatedClose({ skipFocus: true });\n };\n\n window.addEventListener('blur', closeMenuAndSubmenus);\n document.addEventListener('mousedown', detectIfClickIsOutsideMenuAndCloseIfSo);\n document.addEventListener('touchstart', detectIfClickIsOutsideMenuAndCloseIfSo);\n return () => {\n window.removeEventListener('blur', closeMenuAndSubmenus);\n document.removeEventListener('mousedown', detectIfClickIsOutsideMenuAndCloseIfSo);\n document.removeEventListener('touchstart', detectIfClickIsOutsideMenuAndCloseIfSo);\n };\n }, [buttonDOMNodeRef, handleChangeOpenedSubItems, isMenuOpen, onOpinionatedClose]);\n\n return React.useMemo(\n () => ({\n mainMenuRef,\n submenusRefs,\n handleSubmenusRefChange,\n }),\n [handleSubmenusRefChange],\n );\n};\n"],
5
- "mappings": "AAAA,YAAY,WAAW;ACAvB,OAAOA,YAAW;AAWX,MAAM,kBAAkB,CAAC;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AACF,MAA6B;AAC3B,QAAM,EAAE,oBAAoB,WAAW,IAAI;AAC3C,QAAM,EAAE,iBAAiB,IAAI;AAC7B,QAAM,cAAcA,OAAM,OAA8B,IAAI;AAI5D,QAAM,eAAeA,OAAM,OAAwE,CAAC,CAAC;AACrG,QAAM,0BAA0BA,OAAM;AAAA,IACpC,CAAC,UAA8B,SAAqD;AAClF,UAAI,UAAU;AACZ,qBAAa,QAAQ,IAAI,IAAI;AAAA,MAC/B,OAAO;AACL,eAAO,aAAa,QAAQ,IAAI;AAAA,MAClC;AAAA,IACF;AAAA,IACA,CAAC;AAAA,EACH;AACA,EAAAA,OAAM,UAAU,MAAM;AAEpB,UAAM,uBAAuB,CAAC,MAAkB;AAC9C,iCAA2B,CAAC,GAAG,EAAE,UAAU,MAAM,OAAO,EAAE,CAAC;AAC3D,yBAAmB,EAAE,WAAW,KAAK,CAAC;AAAA,IACxC;AAEA,UAAM,yCAAyC,CAAC,MAA+B;AAI7E,UAAI,CAAC,WAAY;AAEjB,UAAI,iBAAiB,YAAY,EAAE,OAAQ;AAE3C,YAAM,wBAAwB,YAAY,SAAS,SAAS,EAAE,MAAc;AAC5E,UAAI,sBAAuB;AAC3B,YAAM,sBAAsB,OAAO,OAAO,aAAa,OAAO,EAAE,OAAO,OAAO;AAE9E,YAAM,0BAA0B,oBAAoB,KAAK,CAAC,SAAS,KAAK,SAAS,EAAE,MAAc,CAAC;AAClG,UAAI,wBAAyB;AAC7B,iCAA2B,CAAC,GAAG,EAAE,UAAU,MAAM,OAAO,EAAE,CAAC;AAC3D,yBAAmB,EAAE,WAAW,KAAK,CAAC;AAAA,IACxC;AAEA,WAAO,iBAAiB,QAAQ,oBAAoB;AACpD,aAAS,iBAAiB,aAAa,sCAAsC;AAC7E,aAAS,iBAAiB,cAAc,sCAAsC;AAC9E,WAAO,MAAM;AACX,aAAO,oBAAoB,QAAQ,oBAAoB;AACvD,eAAS,oBAAoB,aAAa,sCAAsC;AAChF,eAAS,oBAAoB,cAAc,sCAAsC;AAAA,IACnF;AAAA,EACF,GAAG,CAAC,kBAAkB,4BAA4B,YAAY,kBAAkB,CAAC;AAEjF,SAAOA,OAAM;AAAA,IACX,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,CAAC,uBAAuB;AAAA,EAC1B;AACF;",
4
+ "sourcesContent": ["import * as React from 'react';\nexport { React };\n", "import React from 'react';\nimport type { DSMenuButtonT } from '../../../react-desc-prop-types.js';\nimport type { useMenuOpenStatus } from './useMenuOpenStatus.js';\nimport type { DSMenuBehaviouralContextProviderT } from '../react-desc-prop-types.js';\n\ntype UseGlobalEventsConfig = {\n menuOpenStatus: ReturnType<typeof useMenuOpenStatus>;\n handleChangeOpenedSubItems: Required<DSMenuButtonT.MenuBehaviouralLayerOptionalProps>['onDisplayedSubmenuChange'];\n propsWithDefault: DSMenuBehaviouralContextProviderT.InternalProps;\n};\n\nexport const useGlobalEvents = ({\n menuOpenStatus,\n handleChangeOpenedSubItems,\n propsWithDefault,\n}: UseGlobalEventsConfig) => {\n const { onOpinionatedClose, isMenuOpen } = menuOpenStatus;\n const { buttonDOMNodeRef } = propsWithDefault;\n const mainMenuRef = React.useRef<HTMLDivElement | null>(null);\n // we are using \"portal\" but then we check the native evenet to check if \"on click outside\" the menu\n // for such reason, we need to track the current HTML NODES of the submenus (topmost parent)\n // this will allow us to check if the click event is outside the menu or any of it's submenus\n const submenusRefs = React.useRef<Record<DSMenuButtonT.WithSubmenuMenuNodes['dsId'], HTMLElement>>({});\n const handleSubmenusRefChange = React.useCallback(\n (htmlNode: HTMLElement | null, dsId: DSMenuButtonT.WithSubmenuMenuNodes['dsId']) => {\n if (htmlNode) {\n submenusRefs.current[dsId] = htmlNode;\n } else {\n delete submenusRefs.current[dsId];\n }\n },\n [],\n );\n React.useEffect(() => {\n // when the window loses focus, close all the menus and submenus\n const closeMenuAndSubmenus = (e: FocusEvent) => {\n // same guard as the click-outside handler: every instance listens to window blur,\n // a closed menu has nothing to close (and must not report `onDisplayedSubmenuChange`/`onClose`)\n if (!isMenuOpen) return;\n handleChangeOpenedSubItems([], { itemNode: null, event: e });\n onOpinionatedClose({ skipFocus: true, reason: 'window-blur', event: e });\n };\n // when the user clicks \"outside\" the menu and submenus, close all the menus and submenus\n const detectIfClickIsOutsideMenuAndCloseIfSo = (e: MouseEvent | TouchEvent) => {\n // we are running this on literally ALL the clicks that happen on the window\n // we don't want to be consuming resources that are not strictly necessary\n // if the main menu is not open, we don't need to check if the click is outside the menu ofc\n if (!isMenuOpen) return;\n // we discard the fastest to check \"trigger\" click first \u2014 the trigger CONTAINS its content (icons, text...),\n // a pointer going down on any of it is a trigger interaction (the click that follows toggles the menu),\n // not an outside one: closing here would make the click re-open the menu we just closed\n if (buttonDOMNodeRef.current?.contains(e.target as Node)) return;\n // fastest check is the mainmenu, cuz we don't loop through all the submenus\n const isClickInsideMainMenu = mainMenuRef.current?.contains(e.target as Node);\n if (isClickInsideMainMenu) return;\n const submenuNodesToCheck = Object.values(submenusRefs.current).filter(Boolean);\n // we check if the event.target is a child of any of our known HTML nodes (via native .contains)\n const isClickInsideAnySubmenu = submenuNodesToCheck.some((node) => node.contains(e.target as Node));\n if (isClickInsideAnySubmenu) return;\n handleChangeOpenedSubItems([], { itemNode: null, event: e });\n onOpinionatedClose({ skipFocus: true, reason: 'outside-click', event: e });\n };\n\n window.addEventListener('blur', closeMenuAndSubmenus);\n document.addEventListener('mousedown', detectIfClickIsOutsideMenuAndCloseIfSo);\n document.addEventListener('touchstart', detectIfClickIsOutsideMenuAndCloseIfSo);\n return () => {\n window.removeEventListener('blur', closeMenuAndSubmenus);\n document.removeEventListener('mousedown', detectIfClickIsOutsideMenuAndCloseIfSo);\n document.removeEventListener('touchstart', detectIfClickIsOutsideMenuAndCloseIfSo);\n };\n }, [buttonDOMNodeRef, handleChangeOpenedSubItems, isMenuOpen, onOpinionatedClose]);\n\n return React.useMemo(\n () => ({\n mainMenuRef,\n submenusRefs,\n handleSubmenusRefChange,\n }),\n [handleSubmenusRefChange],\n );\n};\n"],
5
+ "mappings": "AAAA,YAAY,WAAW;ACAvB,OAAOA,YAAW;AAWX,MAAM,kBAAkB,CAAC;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AACF,MAA6B;AAC3B,QAAM,EAAE,oBAAoB,WAAW,IAAI;AAC3C,QAAM,EAAE,iBAAiB,IAAI;AAC7B,QAAM,cAAcA,OAAM,OAA8B,IAAI;AAI5D,QAAM,eAAeA,OAAM,OAAwE,CAAC,CAAC;AACrG,QAAM,0BAA0BA,OAAM;AAAA,IACpC,CAAC,UAA8B,SAAqD;AAClF,UAAI,UAAU;AACZ,qBAAa,QAAQ,IAAI,IAAI;AAAA,MAC/B,OAAO;AACL,eAAO,aAAa,QAAQ,IAAI;AAAA,MAClC;AAAA,IACF;AAAA,IACA,CAAC;AAAA,EACH;AACA,EAAAA,OAAM,UAAU,MAAM;AAEpB,UAAM,uBAAuB,CAAC,MAAkB;AAG9C,UAAI,CAAC,WAAY;AACjB,iCAA2B,CAAC,GAAG,EAAE,UAAU,MAAM,OAAO,EAAE,CAAC;AAC3D,yBAAmB,EAAE,WAAW,MAAM,QAAQ,eAAe,OAAO,EAAE,CAAC;AAAA,IACzE;AAEA,UAAM,yCAAyC,CAAC,MAA+B;AAI7E,UAAI,CAAC,WAAY;AAIjB,UAAI,iBAAiB,SAAS,SAAS,EAAE,MAAc,EAAG;AAE1D,YAAM,wBAAwB,YAAY,SAAS,SAAS,EAAE,MAAc;AAC5E,UAAI,sBAAuB;AAC3B,YAAM,sBAAsB,OAAO,OAAO,aAAa,OAAO,EAAE,OAAO,OAAO;AAE9E,YAAM,0BAA0B,oBAAoB,KAAK,CAAC,SAAS,KAAK,SAAS,EAAE,MAAc,CAAC;AAClG,UAAI,wBAAyB;AAC7B,iCAA2B,CAAC,GAAG,EAAE,UAAU,MAAM,OAAO,EAAE,CAAC;AAC3D,yBAAmB,EAAE,WAAW,MAAM,QAAQ,iBAAiB,OAAO,EAAE,CAAC;AAAA,IAC3E;AAEA,WAAO,iBAAiB,QAAQ,oBAAoB;AACpD,aAAS,iBAAiB,aAAa,sCAAsC;AAC7E,aAAS,iBAAiB,cAAc,sCAAsC;AAC9E,WAAO,MAAM;AACX,aAAO,oBAAoB,QAAQ,oBAAoB;AACvD,eAAS,oBAAoB,aAAa,sCAAsC;AAChF,eAAS,oBAAoB,cAAc,sCAAsC;AAAA,IACnF;AAAA,EACF,GAAG,CAAC,kBAAkB,4BAA4B,YAAY,kBAAkB,CAAC;AAEjF,SAAOA,OAAM;AAAA,IACX,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,CAAC,uBAAuB;AAAA,EAC1B;AACF;",
6
6
  "names": ["React"]
7
7
  }
@@ -12,13 +12,17 @@ import { FORBIDDEN_BEHAVIOURS, UNEXPECTED_INTERNAL_ERRORS } from "../constants/E
12
12
  import { getNewSelectionMultipleSelect } from "../utils/multipleSelectionHelpers.js";
13
13
  import { getSubMenusPath } from "../utils/nodeGettersByCriterias.js";
14
14
  import { getNewSelectionSingleSelect } from "../utils/singleSelectionHelpers.js";
15
+ const invokeItemOnClickOnKeyboardActivation = (pseudoFocusedItemNode, event) => {
16
+ if (event?.type === "keydown" && pseudoFocusedItemNode.plainItem.onClick)
17
+ pseudoFocusedItemNode.plainItem.onClick(event);
18
+ };
15
19
  const useMenuItemEventsHandlers = ({
16
20
  propsWithDefault,
17
21
  focusTrackers,
18
22
  menuOpenStatus,
19
23
  handleChangeOpenedSubItems
20
24
  }) => {
21
- const { onItemSelected, onActivateItem, selectedNodes } = propsWithDefault;
25
+ const { onItemSelected, onActivateItem, selectedNodes, buttonDOMNodeRef } = propsWithDefault;
22
26
  const { onOpinionatedClose } = menuOpenStatus;
23
27
  const {
24
28
  trackFocusFirstChildItem,
@@ -28,7 +32,8 @@ const useMenuItemEventsHandlers = ({
28
32
  trackFocusParentItem,
29
33
  trackFocusNode,
30
34
  focusedElementItemNode,
31
- trackFocusTrigger
35
+ trackFocusTrigger,
36
+ trackFocusRegionReset
32
37
  } = focusTrackers;
33
38
  const openSubmenu = React2.useCallback(
34
39
  ({
@@ -90,10 +95,11 @@ const useMenuItemEventsHandlers = ({
90
95
  } else {
91
96
  onActivateItem(pseudoFocusedItemNode, { itemNode: pseudoFocusedItemNode, event });
92
97
  }
98
+ invokeItemOnClickOnKeyboardActivation(pseudoFocusedItemNode, event);
93
99
  const metainfo = { itemNode: pseudoFocusedItemNode, event };
94
100
  const newOpenedItems = [];
95
101
  handleChangeOpenedSubItems(newOpenedItems, metainfo);
96
- onOpinionatedClose();
102
+ onOpinionatedClose({ reason: "item-activation", event });
97
103
  }
98
104
  },
99
105
  [
@@ -116,18 +122,21 @@ const useMenuItemEventsHandlers = ({
116
122
  if (isMultipleSelectNode(pseudoFocusedItemNode)) {
117
123
  const newSelection = getNewSelectionMultipleSelect(selectedNodes, pseudoFocusedItemNode);
118
124
  onItemSelected(newSelection, { itemNode: pseudoFocusedItemNode, event });
125
+ invokeItemOnClickOnKeyboardActivation(pseudoFocusedItemNode, event);
119
126
  return;
120
127
  }
121
128
  if (isSingleSelectNode(pseudoFocusedItemNode)) {
122
129
  const newSelection = getNewSelectionSingleSelect(selectedNodes, pseudoFocusedItemNode);
123
130
  onItemSelected(newSelection, { itemNode: pseudoFocusedItemNode, event });
131
+ invokeItemOnClickOnKeyboardActivation(pseudoFocusedItemNode, event);
124
132
  return;
125
133
  }
126
134
  onActivateItem?.(pseudoFocusedItemNode, { itemNode: pseudoFocusedItemNode, event });
135
+ invokeItemOnClickOnKeyboardActivation(pseudoFocusedItemNode, event);
127
136
  const metainfo = { itemNode: pseudoFocusedItemNode, event };
128
137
  const newOpenedItems = [];
129
138
  handleChangeOpenedSubItems(newOpenedItems, metainfo);
130
- onOpinionatedClose();
139
+ onOpinionatedClose({ reason: "item-activation", event });
131
140
  },
132
141
  [
133
142
  onActivateItem,
@@ -159,16 +168,21 @@ const useMenuItemEventsHandlers = ({
159
168
  },
160
169
  [closeCurrentSubmenu, trackFocusParentItem]
161
170
  );
171
+ const handleMenuTabKeyDownCapture = React2.useCallback(
172
+ (event) => {
173
+ if (event.key !== "Tab") return;
174
+ buttonDOMNodeRef.current?.focus();
175
+ trackFocusRegionReset();
176
+ onOpinionatedClose({ skipFocus: true, reason: "tab", event });
177
+ },
178
+ [buttonDOMNodeRef, onOpinionatedClose, trackFocusRegionReset]
179
+ );
162
180
  const handleFocusableMenuItemKeyDown = React2.useCallback(
163
181
  (event) => {
182
+ if (event.key === "Tab") return;
164
183
  const pseudoFocusedItemNode = focusedElementItemNode.current;
165
184
  if (pseudoFocusedItemNode === null) throw UNEXPECTED_INTERNAL_ERRORS.FOCUSED_ITEM_NODE_NOT_SET;
166
185
  if (pseudoFocusedItemNode.plainItem.onKeyDown) pseudoFocusedItemNode.plainItem.onKeyDown(event);
167
- if (event.key === "Tab") {
168
- trackFocusTrigger({ shouldFocusImmediately: true });
169
- onOpinionatedClose();
170
- return;
171
- }
172
186
  if (event.key === "ArrowDown") {
173
187
  event?.preventDefault();
174
188
  trackFocusNextItem(pseudoFocusedItemNode);
@@ -201,6 +215,7 @@ const useMenuItemEventsHandlers = ({
201
215
  return;
202
216
  }
203
217
  if (event.key === "Escape") {
218
+ event.stopPropagation();
204
219
  if (event.altKey && isWithSubmenuNode(pseudoFocusedItemNode)) {
205
220
  closeSubmenusAfterNode({ itemNode: pseudoFocusedItemNode, event });
206
221
  return;
@@ -208,7 +223,7 @@ const useMenuItemEventsHandlers = ({
208
223
  const { currentSubmenuWasMainMenu } = closeCurrentSubmenu({ itemNode: pseudoFocusedItemNode, event });
209
224
  if (currentSubmenuWasMainMenu) {
210
225
  trackFocusTrigger({ shouldFocusImmediately: true });
211
- onOpinionatedClose({ skipFocus: true });
226
+ onOpinionatedClose({ skipFocus: true, reason: "escape", event });
212
227
  } else {
213
228
  trackFocusParentItem(pseudoFocusedItemNode);
214
229
  }
@@ -312,12 +327,14 @@ const useMenuItemEventsHandlers = ({
312
327
  );
313
328
  return React2.useMemo(
314
329
  () => ({
330
+ handleMenuTabKeyDownCapture,
315
331
  handleFocusableMenuItemKeyDown,
316
332
  handleFocusableMenuItemClick,
317
333
  handleFocusableMenuItemOnMouseEnter,
318
334
  handleFocusableMenuItemNativeFocusEvent
319
335
  }),
320
336
  [
337
+ handleMenuTabKeyDownCapture,
321
338
  handleFocusableMenuItemClick,
322
339
  handleFocusableMenuItemKeyDown,
323
340
  handleFocusableMenuItemNativeFocusEvent,