@elliemae/ds-menu-button 3.80.0-next.51 → 3.80.0-next.53
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.
- package/dist/cjs/config/useSplitInherithedProps.js +13 -2
- package/dist/cjs/config/useSplitInherithedProps.js.map +2 -2
- package/dist/cjs/parts/DSFlyoutMenu/DSFlyoutMenu.js +25 -2
- package/dist/cjs/parts/DSFlyoutMenu/DSFlyoutMenu.js.map +2 -2
- package/dist/cjs/parts/DSMenuBehaviouralContextProvider/config/useFocusTracker.js +4 -1
- package/dist/cjs/parts/DSMenuBehaviouralContextProvider/config/useFocusTracker.js.map +2 -2
- package/dist/cjs/parts/DSMenuBehaviouralContextProvider/config/useGlobalEvents.js +4 -3
- package/dist/cjs/parts/DSMenuBehaviouralContextProvider/config/useGlobalEvents.js.map +2 -2
- package/dist/cjs/parts/DSMenuBehaviouralContextProvider/config/useMenuItemEventsHandlers.js +27 -10
- package/dist/cjs/parts/DSMenuBehaviouralContextProvider/config/useMenuItemEventsHandlers.js.map +2 -2
- package/dist/cjs/parts/DSMenuBehaviouralContextProvider/config/useMenuOpenStatus.js +14 -8
- package/dist/cjs/parts/DSMenuBehaviouralContextProvider/config/useMenuOpenStatus.js.map +2 -2
- package/dist/cjs/parts/DSOpinionatedButton/config/useTriggerEventsHandlers.js +19 -9
- package/dist/cjs/parts/DSOpinionatedButton/config/useTriggerEventsHandlers.js.map +2 -2
- package/dist/cjs/react-desc-prop-types.js +9 -2
- package/dist/cjs/react-desc-prop-types.js.map +2 -2
- package/dist/esm/config/useSplitInherithedProps.js +13 -2
- package/dist/esm/config/useSplitInherithedProps.js.map +2 -2
- package/dist/esm/parts/DSFlyoutMenu/DSFlyoutMenu.js +25 -2
- package/dist/esm/parts/DSFlyoutMenu/DSFlyoutMenu.js.map +2 -2
- package/dist/esm/parts/DSMenuBehaviouralContextProvider/config/useFocusTracker.js +4 -1
- package/dist/esm/parts/DSMenuBehaviouralContextProvider/config/useFocusTracker.js.map +2 -2
- package/dist/esm/parts/DSMenuBehaviouralContextProvider/config/useGlobalEvents.js +4 -3
- package/dist/esm/parts/DSMenuBehaviouralContextProvider/config/useGlobalEvents.js.map +2 -2
- package/dist/esm/parts/DSMenuBehaviouralContextProvider/config/useMenuItemEventsHandlers.js +27 -10
- package/dist/esm/parts/DSMenuBehaviouralContextProvider/config/useMenuItemEventsHandlers.js.map +2 -2
- package/dist/esm/parts/DSMenuBehaviouralContextProvider/config/useMenuOpenStatus.js +14 -8
- package/dist/esm/parts/DSMenuBehaviouralContextProvider/config/useMenuOpenStatus.js.map +2 -2
- package/dist/esm/parts/DSOpinionatedButton/config/useTriggerEventsHandlers.js +19 -9
- package/dist/esm/parts/DSOpinionatedButton/config/useTriggerEventsHandlers.js.map +2 -2
- package/dist/esm/react-desc-prop-types.js +9 -2
- package/dist/esm/react-desc-prop-types.js.map +2 -2
- package/dist/types/config/useSplitInherithedProps.d.ts +3 -2
- package/dist/types/parts/DSMenuBehaviouralContextProvider/config/useFocusTracker.d.ts +3 -1
- package/dist/types/parts/DSMenuBehaviouralContextProvider/config/useMenuItemEventsHandlers.d.ts +1 -0
- package/dist/types/parts/DSMenuBehaviouralContextProvider/config/useMenuOpenStatus.d.ts +12 -4
- package/dist/types/react-desc-prop-types.d.ts +13 -2
- 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
|
-
|
|
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
|
|
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;
|
|
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
|
-
[
|
|
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
|
|
5
|
-
"mappings": "AAAA,YAAY,WAAW;
|
|
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 = {
|
|
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
|
|
5
|
-
"mappings": "AAAA,YAAY,WAAW;ACKvB,SAAS,gBAAgB;AACzB,OAAOA,YAAW;AAElB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,6BAA6B;AACtC,SAAS,0BAA0B;AACnC,SAAS,gCAAgC;AAWlC,MAAM,kBAAkB,MAAM;AACnC,QAAM,yBAAyBA,OAAM,OAAO,KAAK;
|
|
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
|
|
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
|
|
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;
|
|
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,
|