@linzjs/step-ag-grid 1.4.4 → 1.4.6
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/index.js +1631 -3040
- package/dist/index.js.map +1 -1
- package/dist/src/components/GridLoadableCell.d.ts +2 -0
- package/dist/src/components/gridForm/GridFormDropDown.d.ts +1 -1
- package/dist/src/components/gridForm/GridFormMultiSelect.d.ts +1 -1
- package/dist/src/components/gridPopoverEdit/GridPopoverMenu.d.ts +1 -1
- package/dist/src/react-menu3/components/ControlledMenu.d.ts +2 -0
- package/dist/src/react-menu3/components/FocusableItem.d.ts +9 -0
- package/dist/src/react-menu3/components/Menu.d.ts +16 -0
- package/dist/src/react-menu3/components/MenuButton.d.ts +8 -0
- package/dist/src/react-menu3/components/MenuDivider.d.ts +3 -0
- package/dist/src/react-menu3/components/MenuGroup.d.ts +12 -0
- package/dist/src/react-menu3/components/MenuHeader.d.ts +4 -0
- package/dist/src/react-menu3/components/MenuItem.d.ts +59 -0
- package/dist/src/react-menu3/components/MenuList.d.ts +57 -0
- package/dist/src/react-menu3/components/MenuRadioGroup.d.ts +44 -0
- package/dist/src/react-menu3/components/SubMenu.d.ts +110 -0
- package/dist/src/react-menu3/hooks/index.d.ts +9 -0
- package/dist/src/react-menu3/hooks/useBEM.d.ts +10 -0
- package/dist/src/react-menu3/hooks/useCombinedRef.d.ts +2 -0
- package/dist/src/react-menu3/hooks/useIsomorphicLayoutEffect.d.ts +3 -0
- package/dist/src/react-menu3/hooks/useItemEffect.d.ts +2 -0
- package/dist/src/react-menu3/hooks/useItemState.d.ts +7 -0
- package/dist/src/react-menu3/hooks/useItems.d.ts +7 -0
- package/dist/src/react-menu3/hooks/useMenuChange.d.ts +2 -0
- package/dist/src/react-menu3/hooks/useMenuState.d.ts +14 -0
- package/dist/src/react-menu3/hooks/useMenuStateAndFocus.d.ts +2 -0
- package/dist/src/react-menu3/index.d.ts +402 -0
- package/dist/src/react-menu3/positionUtils/getPositionHelpers.d.ts +11 -0
- package/dist/src/react-menu3/positionUtils/index.d.ts +3 -0
- package/dist/src/react-menu3/positionUtils/placeArrowHorizontal.d.ts +8 -0
- package/dist/src/react-menu3/positionUtils/placeArrowVertical.d.ts +8 -0
- package/dist/src/react-menu3/positionUtils/placeLeftorRight.d.ts +19 -0
- package/dist/src/react-menu3/positionUtils/placeToporBottom.d.ts +19 -0
- package/dist/src/react-menu3/positionUtils/positionContextMenu.d.ts +13 -0
- package/dist/src/react-menu3/positionUtils/positionMenu.d.ts +26 -0
- package/dist/src/react-menu3/style-utils/index.d.ts +11 -0
- package/dist/src/react-menu3/utils/constants.d.ts +92 -0
- package/dist/src/react-menu3/utils/index.d.ts +4 -0
- package/dist/src/react-menu3/utils/propTypes.d.ts +56 -0
- package/dist/src/react-menu3/utils/utils.d.ts +36 -0
- package/dist/src/react-menu3/utils/withHovering.d.ts +7 -0
- package/dist/src/stories/components/ReactMenu.stories.d.ts +8 -0
- package/dist/step-ag-grid.esm.js +1543 -2951
- package/dist/step-ag-grid.esm.js.map +1 -1
- package/package.json +22 -13
- package/src/components/GridLoadableCell.scss +4 -0
- package/src/components/GridLoadableCell.tsx +5 -1
- package/src/components/GridPopoverHook.tsx +4 -50
- package/src/components/gridForm/GridFormDropDown.tsx +3 -3
- package/src/components/gridForm/GridFormMultiSelect.tsx +12 -6
- package/src/components/gridForm/GridFormPopoutMenu.tsx +1 -1
- package/src/components/gridPopoverEdit/GridPopoverMenu.scss +6 -2
- package/src/components/gridPopoverEdit/GridPopoverMenu.tsx +1 -1
- package/src/components/gridRender/GridRenderPopoutMenuCell.tsx +7 -9
- package/src/react-menu3/components/ControlledMenu.tsx +261 -0
- package/src/react-menu3/components/FocusableItem.tsx +75 -0
- package/src/react-menu3/components/Menu.tsx +108 -0
- package/src/react-menu3/components/MenuButton.tsx +34 -0
- package/src/react-menu3/components/MenuDivider.tsx +17 -0
- package/src/react-menu3/components/MenuGroup.tsx +47 -0
- package/src/react-menu3/components/MenuHeader.tsx +17 -0
- package/src/react-menu3/components/MenuItem.tsx +183 -0
- package/src/react-menu3/components/MenuList.tsx +502 -0
- package/src/react-menu3/components/MenuRadioGroup.tsx +72 -0
- package/src/react-menu3/components/SubMenu.tsx +349 -0
- package/src/react-menu3/hooks/index.ts +9 -0
- package/src/react-menu3/hooks/useBEM.ts +33 -0
- package/src/react-menu3/hooks/useCombinedRef.ts +24 -0
- package/src/react-menu3/hooks/useIsomorphicLayoutEffect.ts +15 -0
- package/src/react-menu3/hooks/useItemEffect.ts +23 -0
- package/src/react-menu3/hooks/useItemState.ts +67 -0
- package/src/react-menu3/hooks/useItems.ts +111 -0
- package/src/react-menu3/hooks/useMenuChange.ts +12 -0
- package/src/react-menu3/hooks/useMenuState.ts +42 -0
- package/src/react-menu3/hooks/useMenuStateAndFocus.ts +17 -0
- package/src/react-menu3/index.d.ts +683 -0
- package/src/react-menu3/index.ts +442 -0
- package/src/react-menu3/positionUtils/getPositionHelpers.ts +83 -0
- package/src/react-menu3/positionUtils/index.ts +3 -0
- package/src/react-menu3/positionUtils/placeArrowHorizontal.ts +15 -0
- package/src/react-menu3/positionUtils/placeArrowVertical.ts +15 -0
- package/src/react-menu3/positionUtils/placeLeftorRight.ts +92 -0
- package/src/react-menu3/positionUtils/placeToporBottom.ts +93 -0
- package/src/react-menu3/positionUtils/positionContextMenu.ts +61 -0
- package/src/react-menu3/positionUtils/positionMenu.ts +80 -0
- package/src/react-menu3/style-utils/index.ts +83 -0
- package/src/react-menu3/style-utils.d.ts +110 -0
- package/src/react-menu3/styles/_mixins.scss +11 -0
- package/src/react-menu3/styles/_var.scss +20 -0
- package/src/react-menu3/styles/core.scss +80 -0
- package/src/react-menu3/styles/index.scss +70 -0
- package/src/react-menu3/styles/theme-dark.scss +30 -0
- package/src/react-menu3/styles/transitions/slide.scss +47 -0
- package/src/react-menu3/utils/constants.ts +116 -0
- package/src/react-menu3/utils/index.ts +4 -0
- package/src/react-menu3/utils/propTypes.ts +63 -0
- package/src/react-menu3/utils/utils.ts +88 -0
- package/src/react-menu3/utils/withHovering.tsx +27 -0
- package/src/stories/components/GridPopoutEditDropDown.stories.tsx +1 -3
- package/src/stories/components/ReactMenu.stories.tsx +37 -0
- package/src/utils/util.ts +2 -1
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { useRef, useContext, useEffect, useMemo, useImperativeHandle } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import { useBEM, useCombinedRef, useMenuChange, useMenuStateAndFocus, useItemEffect } from "../hooks";
|
|
4
|
+
import { MenuList } from "./MenuList";
|
|
5
|
+
import {
|
|
6
|
+
mergeProps,
|
|
7
|
+
batchedUpdates,
|
|
8
|
+
commonProps,
|
|
9
|
+
safeCall,
|
|
10
|
+
menuClass,
|
|
11
|
+
subMenuClass,
|
|
12
|
+
menuItemClass,
|
|
13
|
+
isMenuOpen,
|
|
14
|
+
withHovering,
|
|
15
|
+
SettingsContext,
|
|
16
|
+
ItemSettingsContext,
|
|
17
|
+
MenuListContext,
|
|
18
|
+
MenuListItemContext,
|
|
19
|
+
Keys,
|
|
20
|
+
HoverActionTypes,
|
|
21
|
+
FocusPositions,
|
|
22
|
+
} from "../utils";
|
|
23
|
+
import {
|
|
24
|
+
BaseProps,
|
|
25
|
+
ClassNameProp,
|
|
26
|
+
Hoverable,
|
|
27
|
+
MenuAlign,
|
|
28
|
+
MenuArrowModifiers,
|
|
29
|
+
MenuDirection,
|
|
30
|
+
MenuModifiers,
|
|
31
|
+
MenuOverflow,
|
|
32
|
+
MenuPosition,
|
|
33
|
+
RenderProp,
|
|
34
|
+
UncontrolledMenuProps,
|
|
35
|
+
} from "../index";
|
|
36
|
+
import { withHoveringResultProps } from "../utils/withHovering";
|
|
37
|
+
|
|
38
|
+
//
|
|
39
|
+
// SubMenu
|
|
40
|
+
// ----------------------------------------------------------------------
|
|
41
|
+
export type SubMenuItemModifiers = Readonly<{
|
|
42
|
+
/**
|
|
43
|
+
* Indicates if the submenu is open.
|
|
44
|
+
*/
|
|
45
|
+
open: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Indicates if the submenu item is being hovered and has focus.
|
|
48
|
+
*/
|
|
49
|
+
hover: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Indicates if the submenu and item are disabled.
|
|
52
|
+
*/
|
|
53
|
+
disabled: boolean;
|
|
54
|
+
}>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Common props for `Menu`, `SubMenu` and `ControlledMenu`
|
|
58
|
+
*/
|
|
59
|
+
interface BaseMenuProps extends Omit<BaseProps, "style"> {
|
|
60
|
+
/**
|
|
61
|
+
* Can be a string or a function which receives a modifier object and returns a CSS `class` string.
|
|
62
|
+
*/
|
|
63
|
+
menuClassName?: ClassNameProp<MenuModifiers>;
|
|
64
|
+
/**
|
|
65
|
+
* This prop is forwarded to the `style` prop of menu DOM element.
|
|
66
|
+
*/
|
|
67
|
+
menuStyle?: React.CSSProperties;
|
|
68
|
+
/**
|
|
69
|
+
* Can be a string or a function which receives a modifier object and returns a CSS `class` string.
|
|
70
|
+
*/
|
|
71
|
+
arrowClassName?: ClassNameProp<MenuArrowModifiers>;
|
|
72
|
+
/**
|
|
73
|
+
* This prop is forwarded to the `style` prop of menu arrow DOM element.
|
|
74
|
+
*/
|
|
75
|
+
arrowStyle?: React.CSSProperties;
|
|
76
|
+
/**
|
|
77
|
+
* Set `true` to display an arrow pointing to its anchor element.
|
|
78
|
+
*/
|
|
79
|
+
arrow?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Set the horizontal distance (in pixels) between menu and its anchor element.
|
|
82
|
+
* The value can be negative.
|
|
83
|
+
* @default 0
|
|
84
|
+
*/
|
|
85
|
+
offsetX?: number;
|
|
86
|
+
/**
|
|
87
|
+
* Set the vertical distance (in pixels) between menu and its anchor element.
|
|
88
|
+
* The value can be negative.
|
|
89
|
+
* @default 0
|
|
90
|
+
*/
|
|
91
|
+
offsetY?: number;
|
|
92
|
+
/**
|
|
93
|
+
* Set alignment of menu with anchor element.
|
|
94
|
+
* @default 'start'
|
|
95
|
+
*/
|
|
96
|
+
align?: MenuAlign;
|
|
97
|
+
/**
|
|
98
|
+
* Set direction in which menu expands against anchor element.
|
|
99
|
+
* @default 'bottom'
|
|
100
|
+
*/
|
|
101
|
+
direction?: MenuDirection;
|
|
102
|
+
/**
|
|
103
|
+
* Set the position of menu related to its anchor element:
|
|
104
|
+
*
|
|
105
|
+
* - 'auto' menu position is adjusted to have it contained within the viewport,
|
|
106
|
+
* even if it will be detached from the anchor element.
|
|
107
|
+
* This option allows to display menu in the viewport as much as possible.
|
|
108
|
+
*
|
|
109
|
+
* - 'anchor' menu position is adjusted to have it contained within the viewport,
|
|
110
|
+
* but it will be kept attached to the edges of anchor element.
|
|
111
|
+
*
|
|
112
|
+
* - 'initial' menu always stays at its initial position.
|
|
113
|
+
* @default 'auto'
|
|
114
|
+
*/
|
|
115
|
+
position?: MenuPosition;
|
|
116
|
+
/**
|
|
117
|
+
* Make the menu list scrollable or hidden when there is not enough viewport space to
|
|
118
|
+
* display all menu items. The prop is similar to the CSS `overflow` property.
|
|
119
|
+
* @default 'visible'
|
|
120
|
+
*/
|
|
121
|
+
overflow?: MenuOverflow;
|
|
122
|
+
/**
|
|
123
|
+
* Set computed overflow amount down to a child `MenuGroup`.
|
|
124
|
+
* The `MenuGroup` should have `takeOverflow` prop set as `true` accordingly.
|
|
125
|
+
*/
|
|
126
|
+
setDownOverflow?: boolean;
|
|
127
|
+
children?: React.ReactNode;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface SubMenuProps extends BaseMenuProps, Hoverable, UncontrolledMenuProps, withHoveringResultProps {
|
|
131
|
+
/**
|
|
132
|
+
* Properties of this object are spread to the submenu item DOM element.
|
|
133
|
+
*/
|
|
134
|
+
itemProps?: BaseProps<SubMenuItemModifiers>;
|
|
135
|
+
/**
|
|
136
|
+
* The submenu `label` can be a `string` or JSX element, or a render function that returns one.
|
|
137
|
+
*/
|
|
138
|
+
label?: RenderProp<SubMenuItemModifiers>;
|
|
139
|
+
/**
|
|
140
|
+
* - `undefined` submenu opens when the label item is hovered or clicked. This is the default behaviour.
|
|
141
|
+
* - 'clickOnly' submenu opens when the label item is clicked.
|
|
142
|
+
* - 'none' submenu doesn't open with mouse or keyboard events;
|
|
143
|
+
* you can call the `openMenu` function on `instanceRef` to open submenu programmatically.
|
|
144
|
+
*/
|
|
145
|
+
openTrigger?: "none" | "clickOnly";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const SubMenuFr = ({
|
|
149
|
+
"aria-label": ariaLabel,
|
|
150
|
+
className,
|
|
151
|
+
disabled,
|
|
152
|
+
direction,
|
|
153
|
+
label,
|
|
154
|
+
openTrigger,
|
|
155
|
+
onMenuChange,
|
|
156
|
+
isHovering,
|
|
157
|
+
instanceRef,
|
|
158
|
+
menuItemRef,
|
|
159
|
+
itemProps = {},
|
|
160
|
+
...restProps
|
|
161
|
+
}: SubMenuProps) => {
|
|
162
|
+
const settings = useContext(SettingsContext);
|
|
163
|
+
const { rootMenuRef } = settings;
|
|
164
|
+
const { submenuOpenDelay, submenuCloseDelay } = useContext(ItemSettingsContext);
|
|
165
|
+
const { parentMenuRef, parentDir, overflow: parentOverflow } = useContext(MenuListContext);
|
|
166
|
+
const { isParentOpen, isSubmenuOpen, setOpenSubmenuCount, dispatch, updateItems } = useContext(MenuListItemContext);
|
|
167
|
+
const isPortal = parentOverflow !== "visible";
|
|
168
|
+
|
|
169
|
+
// FIXME Matt no idea what's going on here
|
|
170
|
+
const [stateProps, toggleMenu, _openMenu] = useMenuStateAndFocus(settings);
|
|
171
|
+
|
|
172
|
+
const { state } = stateProps;
|
|
173
|
+
const isDisabled = !!disabled;
|
|
174
|
+
const isOpen = isMenuOpen(state);
|
|
175
|
+
const containerRef = useRef(null);
|
|
176
|
+
const timeoutId = useRef<ReturnType<typeof setTimeout>>();
|
|
177
|
+
|
|
178
|
+
const stopTimer = () => {
|
|
179
|
+
if (timeoutId.current) {
|
|
180
|
+
clearTimeout(timeoutId.current);
|
|
181
|
+
timeoutId.current = undefined;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const openMenu = (...args: any[]) => {
|
|
186
|
+
stopTimer();
|
|
187
|
+
setHover();
|
|
188
|
+
!isDisabled && _openMenu(...args);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const setHover = () => !isHovering && !isDisabled && dispatch(HoverActionTypes.SET, menuItemRef?.current, 0);
|
|
192
|
+
|
|
193
|
+
const delayOpen = (delay: number) => {
|
|
194
|
+
setHover();
|
|
195
|
+
if (!openTrigger) timeoutId.current = setTimeout(() => batchedUpdates(openMenu), Math.max(delay, 0));
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const handlePointerMove = () => {
|
|
199
|
+
if (timeoutId.current || isOpen || isDisabled) return;
|
|
200
|
+
|
|
201
|
+
if (isSubmenuOpen) {
|
|
202
|
+
timeoutId.current = setTimeout(() => delayOpen(submenuOpenDelay - submenuCloseDelay), submenuCloseDelay);
|
|
203
|
+
} else {
|
|
204
|
+
delayOpen(submenuOpenDelay);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const handlePointerLeave = () => {
|
|
209
|
+
stopTimer();
|
|
210
|
+
if (!isOpen) dispatch(HoverActionTypes.UNSET, menuItemRef?.current, 0);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
214
|
+
let handled = false;
|
|
215
|
+
|
|
216
|
+
switch (e.key) {
|
|
217
|
+
// LEFT key is bubbled up from submenu items
|
|
218
|
+
case Keys.LEFT:
|
|
219
|
+
if (isOpen) {
|
|
220
|
+
menuItemRef?.current && menuItemRef.current.focus();
|
|
221
|
+
toggleMenu(false);
|
|
222
|
+
handled = true;
|
|
223
|
+
}
|
|
224
|
+
break;
|
|
225
|
+
|
|
226
|
+
// prevent browser from scrolling page to the right
|
|
227
|
+
case Keys.RIGHT:
|
|
228
|
+
if (!isOpen) handled = true;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (handled) {
|
|
233
|
+
e.preventDefault();
|
|
234
|
+
e.stopPropagation();
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const handleItemKeyDown = (e: React.KeyboardEvent) => {
|
|
239
|
+
if (!isHovering) return;
|
|
240
|
+
|
|
241
|
+
switch (e.key) {
|
|
242
|
+
case Keys.ENTER:
|
|
243
|
+
case Keys.SPACE:
|
|
244
|
+
case Keys.RIGHT:
|
|
245
|
+
openTrigger !== "none" && openMenu(FocusPositions.FIRST);
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
useItemEffect(isDisabled, menuItemRef, updateItems);
|
|
251
|
+
useMenuChange(onMenuChange, isOpen);
|
|
252
|
+
|
|
253
|
+
useEffect(() => () => clearTimeout(timeoutId.current), []);
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
// Don't set focus when parent menu is closed, otherwise focus will be lost
|
|
256
|
+
// and onBlur event will be fired with relatedTarget setting as null.
|
|
257
|
+
if (isHovering && isParentOpen) {
|
|
258
|
+
menuItemRef?.current && menuItemRef.current.focus();
|
|
259
|
+
} else {
|
|
260
|
+
toggleMenu(false);
|
|
261
|
+
}
|
|
262
|
+
}, [isHovering, isParentOpen, toggleMenu, menuItemRef]);
|
|
263
|
+
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
setOpenSubmenuCount((count: number) => (isOpen ? count + 1 : Math.max(count - 1, 0)));
|
|
266
|
+
}, [setOpenSubmenuCount, isOpen]);
|
|
267
|
+
|
|
268
|
+
useImperativeHandle(instanceRef, () => ({
|
|
269
|
+
openMenu: (...args) => {
|
|
270
|
+
isParentOpen && openMenu(...args);
|
|
271
|
+
},
|
|
272
|
+
closeMenu: () => {
|
|
273
|
+
if (isOpen) {
|
|
274
|
+
menuItemRef?.current && menuItemRef.current.focus();
|
|
275
|
+
toggleMenu(false);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
}));
|
|
279
|
+
|
|
280
|
+
const modifiers = useMemo(
|
|
281
|
+
() => ({
|
|
282
|
+
open: isOpen,
|
|
283
|
+
hover: isHovering,
|
|
284
|
+
disabled: isDisabled,
|
|
285
|
+
submenu: true,
|
|
286
|
+
}),
|
|
287
|
+
[isOpen, isHovering, isDisabled],
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const { ref: externalItemRef, className: itemClassName, ...restItemProps } = itemProps;
|
|
291
|
+
|
|
292
|
+
const mergedItemProps = mergeProps(
|
|
293
|
+
{
|
|
294
|
+
onPointerMove: handlePointerMove,
|
|
295
|
+
onPointerLeave: handlePointerLeave,
|
|
296
|
+
onKeyDown: handleItemKeyDown,
|
|
297
|
+
onClick: () => openTrigger !== "none" && openMenu(),
|
|
298
|
+
},
|
|
299
|
+
restItemProps,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const getMenuList = () => {
|
|
303
|
+
const menuList = (
|
|
304
|
+
<MenuList
|
|
305
|
+
{...restProps}
|
|
306
|
+
{...stateProps}
|
|
307
|
+
ariaLabel={ariaLabel || (typeof label === "string" ? label : "Submenu")}
|
|
308
|
+
anchorRef={menuItemRef}
|
|
309
|
+
containerRef={isPortal ? rootMenuRef : containerRef}
|
|
310
|
+
direction={direction || (parentDir === "right" || parentDir === "left" ? parentDir : "right")}
|
|
311
|
+
parentScrollingRef={isPortal && parentMenuRef}
|
|
312
|
+
isDisabled={isDisabled}
|
|
313
|
+
/>
|
|
314
|
+
);
|
|
315
|
+
const container = rootMenuRef?.current;
|
|
316
|
+
return isPortal && container ? createPortal(menuList, container) : menuList;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<li
|
|
321
|
+
className={useBEM({ block: menuClass, element: subMenuClass, className })}
|
|
322
|
+
style={{ position: "relative" }}
|
|
323
|
+
role="presentation"
|
|
324
|
+
ref={containerRef}
|
|
325
|
+
onKeyDown={handleKeyDown}
|
|
326
|
+
>
|
|
327
|
+
<div
|
|
328
|
+
role="menuitem"
|
|
329
|
+
aria-haspopup
|
|
330
|
+
aria-expanded={isOpen}
|
|
331
|
+
{...mergedItemProps}
|
|
332
|
+
{...commonProps(isDisabled, isHovering)}
|
|
333
|
+
ref={useCombinedRef(externalItemRef, menuItemRef)}
|
|
334
|
+
className={useBEM({
|
|
335
|
+
block: menuClass,
|
|
336
|
+
element: menuItemClass,
|
|
337
|
+
modifiers,
|
|
338
|
+
className: itemClassName,
|
|
339
|
+
})}
|
|
340
|
+
>
|
|
341
|
+
{useMemo(() => safeCall(label, modifiers), [label, modifiers])}
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
{state && getMenuList()}
|
|
345
|
+
</li>
|
|
346
|
+
);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export const SubMenu = withHovering("SubMenu", SubMenuFr) as any as typeof SubMenuFr;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { useBEM } from "./useBEM";
|
|
2
|
+
export { useCombinedRef } from "./useCombinedRef";
|
|
3
|
+
export { useLayoutEffect } from "./useIsomorphicLayoutEffect";
|
|
4
|
+
export { useItems } from "./useItems";
|
|
5
|
+
export { useItemEffect } from "./useItemEffect";
|
|
6
|
+
export { useItemState } from "./useItemState";
|
|
7
|
+
export { useMenuChange } from "./useMenuChange";
|
|
8
|
+
export { useMenuState } from "./useMenuState";
|
|
9
|
+
export { useMenuStateAndFocus } from "./useMenuStateAndFocus";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { ClassNameProp } from "../index";
|
|
3
|
+
|
|
4
|
+
type useBemModifiers = Record<string, boolean | string | undefined>;
|
|
5
|
+
|
|
6
|
+
interface useBemProps {
|
|
7
|
+
block: string;
|
|
8
|
+
element?: string;
|
|
9
|
+
modifiers?: useBemModifiers;
|
|
10
|
+
className?: ClassNameProp<any>; // FIXME Matt any?
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Generate className following BEM methodology: http://getbem.com/naming/
|
|
14
|
+
// Modifier value can be one of the following types: boolean, string, undefined
|
|
15
|
+
export const useBEM = ({ block, element, modifiers, className }: useBemProps) =>
|
|
16
|
+
useMemo(() => {
|
|
17
|
+
const blockElement = element ? `${block}__${element}` : block;
|
|
18
|
+
|
|
19
|
+
let classString = blockElement;
|
|
20
|
+
modifiers &&
|
|
21
|
+
Object.keys(modifiers).forEach((name) => {
|
|
22
|
+
const value = modifiers[name];
|
|
23
|
+
if (value) classString += ` ${blockElement}--${value === true ? name : `${name}-${value}`}`;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
let expandedClassName = typeof className === "function" ? className(modifiers) : className;
|
|
27
|
+
if (typeof expandedClassName === "string") {
|
|
28
|
+
expandedClassName = expandedClassName.trim();
|
|
29
|
+
if (expandedClassName) classString += ` ${expandedClassName}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return classString;
|
|
33
|
+
}, [block, element, modifiers, className]);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useMemo, LegacyRef, ForwardedRef } from "react";
|
|
2
|
+
|
|
3
|
+
// Adapted from material-ui
|
|
4
|
+
// https://github.com/mui-org/material-ui/blob/f996027d00e7e4bff3fc040786c1706f9c6c3f82/packages/material-ui-utils/src/useForkRef.ts
|
|
5
|
+
|
|
6
|
+
const setRef = <T>(ref: ForwardedRef<T> | undefined, instance: T | null) => {
|
|
7
|
+
if (typeof ref === "function") {
|
|
8
|
+
ref(instance);
|
|
9
|
+
} else if (ref) {
|
|
10
|
+
ref.current = instance;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const useCombinedRef = <Instance>(
|
|
15
|
+
refA: ForwardedRef<Instance> | undefined,
|
|
16
|
+
refB: ForwardedRef<Instance> | undefined,
|
|
17
|
+
): LegacyRef<Instance> => {
|
|
18
|
+
return useMemo(() => {
|
|
19
|
+
return (instance: any) => {
|
|
20
|
+
setRef(refA, instance);
|
|
21
|
+
setRef(refB, instance);
|
|
22
|
+
};
|
|
23
|
+
}, [refA, refB]);
|
|
24
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect } from "react";
|
|
2
|
+
|
|
3
|
+
// Get around a warning when using useLayoutEffect on the server.
|
|
4
|
+
// https://github.com/reduxjs/react-redux/blob/b48d087d76f666e1c6c5a9713bbec112a1631841/src/utils/useIsomorphicLayoutEffect.js#L12
|
|
5
|
+
// https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
|
|
6
|
+
// https://github.com/facebook/react/issues/14927#issuecomment-549457471
|
|
7
|
+
|
|
8
|
+
const useIsomorphicLayoutEffect =
|
|
9
|
+
typeof window !== "undefined" &&
|
|
10
|
+
typeof window.document !== "undefined" &&
|
|
11
|
+
typeof window.document.createElement !== "undefined"
|
|
12
|
+
? useLayoutEffect
|
|
13
|
+
: useEffect;
|
|
14
|
+
|
|
15
|
+
export { useIsomorphicLayoutEffect as useLayoutEffect };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MutableRefObject } from "react";
|
|
2
|
+
import { useLayoutEffect } from "./useIsomorphicLayoutEffect";
|
|
3
|
+
|
|
4
|
+
export const useItemEffect = (
|
|
5
|
+
isDisabled: boolean | undefined,
|
|
6
|
+
menuItemRef: MutableRefObject<any> | undefined,
|
|
7
|
+
updateItems: (item: any, isMounted?: boolean) => void,
|
|
8
|
+
) => {
|
|
9
|
+
useLayoutEffect(() => {
|
|
10
|
+
if (!menuItemRef) return;
|
|
11
|
+
if (process.env.NODE_ENV !== "production" && !updateItems) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`[React-Menu] This menu item or submenu should be rendered under a menu: ${menuItemRef.current.outerHTML}`,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
if (isDisabled) return;
|
|
17
|
+
const item = menuItemRef.current;
|
|
18
|
+
updateItems(item, true);
|
|
19
|
+
return () => {
|
|
20
|
+
updateItems(item);
|
|
21
|
+
};
|
|
22
|
+
}, [isDisabled, menuItemRef, updateItems]);
|
|
23
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useRef, useContext, useEffect, FocusEvent, MutableRefObject } from "react";
|
|
2
|
+
import { ItemSettingsContext, MenuListItemContext, HoverActionTypes } from "../utils";
|
|
3
|
+
import { useItemEffect } from "./useItemEffect";
|
|
4
|
+
|
|
5
|
+
// This hook includes some common stateful logic in MenuItem and FocusableItem
|
|
6
|
+
export const useItemState = (
|
|
7
|
+
menuItemRef: MutableRefObject<any> | undefined,
|
|
8
|
+
focusRef: MutableRefObject<any> | undefined,
|
|
9
|
+
isHovering?: boolean,
|
|
10
|
+
isDisabled?: boolean,
|
|
11
|
+
) => {
|
|
12
|
+
const { submenuCloseDelay } = useContext(ItemSettingsContext);
|
|
13
|
+
const { isParentOpen, isSubmenuOpen, dispatch, updateItems } = useContext(MenuListItemContext);
|
|
14
|
+
const timeoutId = useRef<ReturnType<typeof setTimeout>>();
|
|
15
|
+
|
|
16
|
+
const setHover = () => {
|
|
17
|
+
!isHovering && !isDisabled && dispatch(HoverActionTypes.SET, menuItemRef?.current, 0);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const unsetHover = () => {
|
|
21
|
+
!isDisabled && dispatch(HoverActionTypes.UNSET, menuItemRef?.current, 0);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const onBlur = (e: FocusEvent) => {
|
|
25
|
+
// Focus has moved out of the entire item
|
|
26
|
+
// It handles situation such as clicking on a sibling disabled menu item
|
|
27
|
+
if (isHovering && !e.currentTarget.contains(e.relatedTarget)) unsetHover();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const onPointerMove = () => {
|
|
31
|
+
if (isSubmenuOpen) {
|
|
32
|
+
if (!timeoutId.current)
|
|
33
|
+
timeoutId.current = setTimeout(() => {
|
|
34
|
+
timeoutId.current = undefined;
|
|
35
|
+
setHover();
|
|
36
|
+
}, submenuCloseDelay);
|
|
37
|
+
} else {
|
|
38
|
+
setHover();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const onPointerLeave = (_: PointerEvent, keepHover: boolean) => {
|
|
43
|
+
if (timeoutId.current) {
|
|
44
|
+
clearTimeout(timeoutId.current);
|
|
45
|
+
timeoutId.current = undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
!keepHover && unsetHover();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
useItemEffect(isDisabled, menuItemRef, updateItems);
|
|
52
|
+
useEffect(() => () => clearTimeout(timeoutId.current), []);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
// Don't set focus when parent menu is closed, otherwise focus will be lost
|
|
55
|
+
// and onBlur event will be fired with relatedTarget setting as null.
|
|
56
|
+
if (isHovering && isParentOpen) {
|
|
57
|
+
focusRef?.current && focusRef.current.focus();
|
|
58
|
+
}
|
|
59
|
+
}, [focusRef, isHovering, isParentOpen]);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
setHover,
|
|
63
|
+
onBlur,
|
|
64
|
+
onPointerMove,
|
|
65
|
+
onPointerLeave,
|
|
66
|
+
};
|
|
67
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, MutableRefObject } from "react";
|
|
2
|
+
import { HoverActionTypes, indexOfNode } from "../utils";
|
|
3
|
+
import { FocusPosition } from "../index";
|
|
4
|
+
|
|
5
|
+
export const useItems = (menuRef: MutableRefObject<any>, focusRef: MutableRefObject<any> | undefined) => {
|
|
6
|
+
const [hoverItem, setHoverItem] = useState<any>();
|
|
7
|
+
const stateRef = useRef({
|
|
8
|
+
items: [] as any[],
|
|
9
|
+
hoverIndex: -1,
|
|
10
|
+
sorted: false,
|
|
11
|
+
});
|
|
12
|
+
const mutableState = stateRef.current;
|
|
13
|
+
|
|
14
|
+
const updateItems = useCallback(
|
|
15
|
+
(item?: any, isMounted?: boolean) => {
|
|
16
|
+
const { items } = mutableState;
|
|
17
|
+
if (!item) {
|
|
18
|
+
mutableState.items = [];
|
|
19
|
+
} else if (isMounted) {
|
|
20
|
+
items.push(item);
|
|
21
|
+
} else {
|
|
22
|
+
const index = items.indexOf(item);
|
|
23
|
+
if (index > -1) {
|
|
24
|
+
items.splice(index, 1);
|
|
25
|
+
if (item.contains(document.activeElement)) {
|
|
26
|
+
focusRef?.current?.focus();
|
|
27
|
+
setHoverItem(undefined);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
mutableState.hoverIndex = -1;
|
|
32
|
+
mutableState.sorted = false;
|
|
33
|
+
},
|
|
34
|
+
[mutableState, focusRef],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const dispatch = useCallback(
|
|
38
|
+
(actionType: number, item: any, nextIndex: FocusPosition) => {
|
|
39
|
+
const { items, hoverIndex } = mutableState;
|
|
40
|
+
const sortItems = () => {
|
|
41
|
+
if (mutableState.sorted) return;
|
|
42
|
+
const orderedNodes = menuRef.current.querySelectorAll(".szh-menu__item");
|
|
43
|
+
items.sort((a, b) => indexOfNode(orderedNodes, a) - indexOfNode(orderedNodes, b));
|
|
44
|
+
mutableState.sorted = true;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
let index = -1,
|
|
48
|
+
newItem = undefined;
|
|
49
|
+
switch (actionType) {
|
|
50
|
+
case HoverActionTypes.RESET:
|
|
51
|
+
break;
|
|
52
|
+
|
|
53
|
+
case HoverActionTypes.SET:
|
|
54
|
+
newItem = item;
|
|
55
|
+
break;
|
|
56
|
+
|
|
57
|
+
case HoverActionTypes.UNSET:
|
|
58
|
+
newItem = (prevItem: any) => (prevItem === item ? undefined : prevItem);
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
case HoverActionTypes.FIRST:
|
|
62
|
+
sortItems();
|
|
63
|
+
index = 0;
|
|
64
|
+
newItem = items[index];
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
case HoverActionTypes.LAST:
|
|
68
|
+
sortItems();
|
|
69
|
+
index = items.length - 1;
|
|
70
|
+
newItem = items[index];
|
|
71
|
+
break;
|
|
72
|
+
|
|
73
|
+
case HoverActionTypes.SET_INDEX:
|
|
74
|
+
if (typeof nextIndex !== "number") break;
|
|
75
|
+
sortItems();
|
|
76
|
+
index = nextIndex;
|
|
77
|
+
newItem = items[index];
|
|
78
|
+
break;
|
|
79
|
+
|
|
80
|
+
case HoverActionTypes.INCREASE:
|
|
81
|
+
sortItems();
|
|
82
|
+
index = hoverIndex;
|
|
83
|
+
if (index < 0) index = items.indexOf(item);
|
|
84
|
+
index++;
|
|
85
|
+
if (index >= items.length) index = 0;
|
|
86
|
+
newItem = items[index];
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
case HoverActionTypes.DECREASE:
|
|
90
|
+
sortItems();
|
|
91
|
+
index = hoverIndex;
|
|
92
|
+
if (index < 0) index = items.indexOf(item);
|
|
93
|
+
index--;
|
|
94
|
+
if (index < 0) index = items.length - 1;
|
|
95
|
+
newItem = items[index];
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
default:
|
|
99
|
+
if (process.env.NODE_ENV !== "production")
|
|
100
|
+
throw new Error(`[React-Menu] Unknown hover action type: ${actionType}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!newItem) index = -1;
|
|
104
|
+
setHoverItem(newItem);
|
|
105
|
+
mutableState.hoverIndex = index;
|
|
106
|
+
},
|
|
107
|
+
[menuRef, mutableState],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return { hoverItem, dispatch, updateItems };
|
|
111
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useRef, useEffect } from "react";
|
|
2
|
+
import { safeCall } from "../utils";
|
|
3
|
+
import { EventHandler, MenuChangeEvent } from "../index";
|
|
4
|
+
|
|
5
|
+
export const useMenuChange = (onMenuChange?: EventHandler<MenuChangeEvent>, isOpen?: boolean) => {
|
|
6
|
+
const prevOpen = useRef(isOpen);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (onMenuChange && prevOpen.current !== isOpen) safeCall(onMenuChange, { open: !!isOpen });
|
|
10
|
+
prevOpen.current = isOpen;
|
|
11
|
+
}, [onMenuChange, isOpen]);
|
|
12
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useTransition } from "react-transition-state";
|
|
2
|
+
import { MenuStateMap, getTransition } from "../utils";
|
|
3
|
+
import { MenuState, MenuStateOptions } from "../index";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A custom Hook which helps manage the states of `ControlledMenu`.
|
|
7
|
+
*/
|
|
8
|
+
export const useMenuState = (
|
|
9
|
+
props?: MenuStateOptions,
|
|
10
|
+
): [
|
|
11
|
+
{
|
|
12
|
+
/**
|
|
13
|
+
* Menu state which should be forwarded to `ControlledMenu`.
|
|
14
|
+
*/
|
|
15
|
+
state?: MenuState;
|
|
16
|
+
/**
|
|
17
|
+
* Stop transition animation. This function value should be forwarded to `ControlledMenu`.
|
|
18
|
+
*/
|
|
19
|
+
endTransition: () => void;
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Open or close menu.
|
|
24
|
+
*
|
|
25
|
+
* - If no parameter is supplied, this function will toggle state between open and close phases.
|
|
26
|
+
* - You can set a boolean parameter to explicitly switch into one of the two phases.
|
|
27
|
+
*/
|
|
28
|
+
(open?: boolean) => void,
|
|
29
|
+
] => {
|
|
30
|
+
const { initialMounted, unmountOnClose, transition, transitionTimeout } = props ?? {
|
|
31
|
+
transition: false,
|
|
32
|
+
};
|
|
33
|
+
const [state, toggleMenu, endTransition] = useTransition({
|
|
34
|
+
mountOnEnter: !initialMounted,
|
|
35
|
+
unmountOnExit: unmountOnClose,
|
|
36
|
+
timeout: transitionTimeout ?? 500,
|
|
37
|
+
enter: getTransition(transition, "open"),
|
|
38
|
+
exit: getTransition(transition, "close"),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return [{ state: MenuStateMap[state], endTransition }, toggleMenu];
|
|
42
|
+
};
|