@vuu-ui/vuu-layout 0.0.27 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +201 -0
  2. package/cjs/index.js +7395 -0
  3. package/index.css +952 -0
  4. package/index.css.map +7 -0
  5. package/package.json +12 -13
  6. package/src/chest-of-drawers/Drawer.css +46 -40
  7. package/src/chest-of-drawers/Drawer.tsx +4 -4
  8. package/src/dialog/Dialog.tsx +2 -2
  9. package/src/drag-drop/BoxModel.ts +1 -1
  10. package/src/drag-drop/DropMenu.css +15 -14
  11. package/src/drag-drop/DropMenu.tsx +8 -15
  12. package/src/drag-drop/DropTarget.ts +6 -6
  13. package/src/drag-drop/DropTargetRenderer.css +3 -3
  14. package/src/drag-drop/DropTargetRenderer.tsx +8 -13
  15. package/src/flexbox/Flexbox.tsx +1 -1
  16. package/src/flexbox/FluidGrid.tsx +1 -1
  17. package/src/index.ts +2 -1
  18. package/src/layout-header/Header.css +3 -3
  19. package/src/layout-header/Header.tsx +2 -2
  20. package/src/layout-provider/useLayoutDragDrop.ts +3 -3
  21. package/src/layout-view/View.css +5 -2
  22. package/src/layout-view/View.tsx +1 -1
  23. package/src/layout-view/useViewResize.ts +1 -1
  24. package/src/menu/ContextMenu.css +22 -0
  25. package/src/menu/ContextMenu.jsx +121 -0
  26. package/src/menu/MenuList.css +150 -0
  27. package/src/menu/MenuList.jsx +179 -0
  28. package/src/menu/aim/aim.js +92 -0
  29. package/src/menu/aim/corners.js +114 -0
  30. package/src/menu/aim/point-in-polygon.js +25 -0
  31. package/src/menu/aim/utils.js +19 -0
  32. package/src/menu/context-menu-provider.jsx +135 -0
  33. package/src/menu/index.js +4 -0
  34. package/src/menu/key-code.js +61 -0
  35. package/src/menu/list-dom-utils.js +22 -0
  36. package/src/menu/use-cascade.js +292 -0
  37. package/src/menu/use-click-away.js +22 -0
  38. package/src/menu/use-items-with-ids.js +75 -0
  39. package/src/menu/use-keyboard-navigation.js +162 -0
  40. package/src/menu/utils.js +5 -0
  41. package/src/palette/Palette.css +2 -2
  42. package/src/palette/Palette.tsx +1 -1
  43. package/src/palette/{PaletteUitk.css → PaletteSalt.css} +0 -0
  44. package/src/palette/{PaletteUitk.tsx → PaletteSalt.tsx} +4 -4
  45. package/src/palette/index.ts +1 -1
  46. package/src/popup/index.js +2 -0
  47. package/src/popup/popup-provider.js +0 -0
  48. package/src/popup/popup-service.css +15 -0
  49. package/src/popup/popup-service.js +281 -0
  50. package/src/portal/Portal.jsx +50 -0
  51. package/src/portal/index.ts +3 -0
  52. package/src/portal/render-portal.jsx +68 -0
  53. package/src/portal/utils.js +16 -0
  54. package/src/responsive/breakpoints.ts +22 -8
  55. package/src/stack/Stack.css +3 -3
  56. package/src/stack/Stack.tsx +3 -2
  57. package/src/stack/StackLayout.tsx +1 -1
  58. package/src/tools/devtools-box/layout-configurator.jsx +1 -1
  59. package/src/tools/devtools-tree/layout-tree-viewer.jsx +1 -1
  60. package/src/utils/apply-handlers.js +15 -0
  61. package/README.md +0 -1
  62. package/src/action-buttons/action-buttons.css +0 -12
  63. package/src/action-buttons/action-buttons.tsx +0 -30
@@ -0,0 +1,114 @@
1
+ import { distance, bullseye } from './aim';
2
+
3
+ function inside(source, targetMin, targetMax) {
4
+ if (source >= targetMin && source <= targetMax) return 0;
5
+ else if (source > targetMin) return -1;
6
+ else return 1;
7
+ }
8
+
9
+ export function corners(source, target) {
10
+ source = { left: source.pageX, top: source.pageY };
11
+ target = target.getBoundingClientRect();
12
+
13
+ let ver, hor;
14
+
15
+ hor = inside(source.left, target.left, target.left + target.width);
16
+ ver = inside(source.top, target.top, source.top + target.height);
17
+
18
+ if (hor === -1 && ver === -1) return ['top-right', 'bottom-left'];
19
+ if (hor === -1 && ver === 0) return ['top-right', 'bottom-right'];
20
+ if (hor === -1 && ver === 1) return ['top-left', 'bottom-right'];
21
+
22
+ if (hor === 0 && ver === -1) return ['bottom-right', 'bottom-left'];
23
+ if (hor === 0 && ver === 0) return [];
24
+ if (hor === 0 && ver === 1) return ['top-left', 'top-right'];
25
+
26
+ if (hor === 1 && ver === -1) return ['bottom-right', 'top-left'];
27
+ if (hor === 1 && ver === 0) return ['bottom-left', 'top-left'];
28
+ if (hor === 1 && ver === 1) return ['bottom-left', 'top-right'];
29
+ }
30
+
31
+ export function boundaries(corners, source, target, adjustment = false) {
32
+ if (target instanceof HTMLElement || target instanceof SVGElement) {
33
+ target = target.getBoundingClientRect();
34
+ }
35
+
36
+ if (!source) return [];
37
+ else if (source instanceof Event) {
38
+ source = {
39
+ left: source.pageX,
40
+ top: source.pageY
41
+ };
42
+ } else if (source.x) {
43
+ source = {
44
+ left: source.x,
45
+ top: source.y
46
+ };
47
+ }
48
+
49
+ let tolerance = adjustment !== false ? Math.round(adjustment / 10) * 1.5 : 0;
50
+ const position = {
51
+ left: target.left - tolerance,
52
+ top: target.top - tolerance,
53
+ width: target.width + tolerance * 2,
54
+ height: target.height + tolerance * 2
55
+ };
56
+
57
+ var doc = document.documentElement;
58
+ var left = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
59
+ var top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
60
+
61
+ let first = true;
62
+ let positions = [];
63
+ corners.forEach((corner) => {
64
+ switch (corner) {
65
+ case 'top-right':
66
+ if (first) positions.push({ x: target.left + target.width + left, y: target.top + top });
67
+ positions.push({ x: position.left + position.width + left, y: position.top + top });
68
+ if (!first) positions.push({ x: target.left + target.width + left, y: target.top + top });
69
+ break;
70
+ case 'top-left':
71
+ if (first) positions.push({ x: target.left + left, y: target.top + top });
72
+ positions.push({ x: position.left + left, y: position.top + top });
73
+ if (!first) positions.push({ x: target.left + left, y: target.top + top });
74
+ break;
75
+ case 'bottom-right':
76
+ if (first)
77
+ positions.push({
78
+ x: target.left + target.width + left,
79
+ y: target.top + target.height + top
80
+ });
81
+ positions.push({
82
+ x: position.left + position.width + left,
83
+ y: position.top + position.height + top
84
+ });
85
+ if (!first)
86
+ positions.push({
87
+ x: target.left + target.width + left,
88
+ y: target.top + target.height + top
89
+ });
90
+ break;
91
+ case 'bottom-left':
92
+ if (first) positions.push({ x: target.left + left, y: target.top + target.height + top });
93
+ positions.push({ x: position.left + left, y: position.top + position.height + top });
94
+ if (!first) positions.push({ x: target.left + left, y: target.top + target.height + top });
95
+ break;
96
+ }
97
+ if (first) {
98
+ positions.push({ x: source.left, y: source.top });
99
+ }
100
+ first = false;
101
+ });
102
+
103
+ if (adjustment === false) {
104
+ const be = bullseye(corners, positions, { x: source.left, y: source.top });
105
+ if (be) {
106
+ const dist = Math.round(distance({ x: source.left, y: source.top }, be));
107
+ return boundaries(corners, source, target, dist);
108
+ }
109
+ }
110
+
111
+ return positions;
112
+ }
113
+
114
+ export default corners;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @license MIT
3
+ * @url https://github.com/substack/point-in-polygon
4
+ */
5
+
6
+ export default function (point, vs) {
7
+ // ray-casting algorithm based on
8
+ // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
9
+
10
+ var x = point[0],
11
+ y = point[1];
12
+
13
+ var inside = false;
14
+ for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
15
+ var xi = vs[i][0],
16
+ yi = vs[i][1];
17
+ var xj = vs[j][0],
18
+ yj = vs[j][1];
19
+
20
+ var intersect = yi > y != yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
21
+ if (intersect) inside = !inside;
22
+ }
23
+
24
+ return inside;
25
+ }
@@ -0,0 +1,19 @@
1
+ function scrollPosition() {
2
+ const scrollTop = document.documentElement.scrollTop
3
+ ? document.documentElement.scrollTop
4
+ : document.body.scrollTop;
5
+ const scrollLeft = document.documentElement.scrollLeft
6
+ ? document.documentElement.scrollLeft
7
+ : document.body.scrollLeft;
8
+
9
+ return { scrollTop, scrollLeft };
10
+ }
11
+
12
+ export function mousePosition(event) {
13
+ const sPos = scrollPosition();
14
+
15
+ const x = document.all ? event.clientX + sPos.scrollLeft : event.pageX;
16
+ const y = document.all ? event.clientY + sPos.scrollTop : event.pageY;
17
+
18
+ return { x, y };
19
+ }
@@ -0,0 +1,135 @@
1
+ import React, { useCallback, useContext, useMemo } from 'react';
2
+ import { PopupService } from '../popup';
3
+ import ContextMenu from './ContextMenu';
4
+ import { MenuItem, MenuItemGroup } from './MenuList';
5
+
6
+ const showContextMenu = (e, menuDescriptors, handleContextMenuAction) => {
7
+ const { clientX: left, clientY: top } = e;
8
+ const menuItems = (menuDescriptors) => {
9
+ const fromDescriptor = ({ children, label, icon, action, options }, i) =>
10
+ children ? (
11
+ <MenuItemGroup key={i} label={label}>
12
+ {children.map(fromDescriptor)}
13
+ </MenuItemGroup>
14
+ ) : (
15
+ <MenuItem key={i} action={action} data-icon={icon} options={options}>
16
+ {label}
17
+ </MenuItem>
18
+ );
19
+
20
+ return menuDescriptors.map(fromDescriptor);
21
+ };
22
+
23
+ const handleClose = (menuId, options) => {
24
+ if (menuId) {
25
+ handleContextMenuAction(menuId, options);
26
+ PopupService.hidePopup();
27
+ }
28
+ };
29
+
30
+ const component = (
31
+ <ContextMenu onClose={handleClose} position={{ x: left, y: top }}>
32
+ {menuItems(menuDescriptors)}
33
+ </ContextMenu>
34
+ );
35
+ PopupService.showPopup({ left: 0, top: 0, component });
36
+ };
37
+
38
+ export const ContextMenuContext = React.createContext(null);
39
+
40
+ const NO_INHERITED_CONTEXT = {
41
+ menuItemDescriptors: []
42
+ };
43
+
44
+ // The menuBuilder will always be supplied by the code that will display the local
45
+ // context menu. It will be passed all configured menu descriptors. It is free to
46
+ // augment, replace or ignore the existing menu descriptors.
47
+ export const useContextMenu = () => {
48
+ const { menuActionHandler, menuBuilders } = useContext(ContextMenuContext);
49
+
50
+ const buildMenuOptions = useCallback((menuBuilders, location, options) => {
51
+ let results = [];
52
+ for (const menuBuilder of menuBuilders) {
53
+ // Maybe we should leave the concatenation to the menuBuilder, then it can control menuItem order
54
+ results = results.concat(menuBuilder(location, options));
55
+ }
56
+ return results;
57
+ }, []);
58
+
59
+ const handleShowContextMenu = (e, location, options) => {
60
+ e.stopPropagation();
61
+ e.preventDefault();
62
+ const menuItemDescriptors = buildMenuOptions(menuBuilders, location, options);
63
+ if (menuItemDescriptors.length) {
64
+ showContextMenu(e, menuItemDescriptors, menuActionHandler);
65
+ }
66
+ };
67
+
68
+ return handleShowContextMenu;
69
+ };
70
+
71
+ const Provider = ({
72
+ children,
73
+ context: { menuBuilders: inheritedMenuBuilders, menuActionHandler: inheritedMenuActionHandler },
74
+ menuActionHandler,
75
+ menuBuilder
76
+ }) => {
77
+ const menuBuilders = useMemo(() => {
78
+ if (inheritedMenuBuilders && menuBuilder) {
79
+ return inheritedMenuBuilders.concat(menuBuilder);
80
+ } else if (menuBuilder) {
81
+ return [menuBuilder];
82
+ } else {
83
+ return inheritedMenuBuilders || [];
84
+ }
85
+ }, [inheritedMenuBuilders, menuBuilder]);
86
+
87
+ const handleMenuAction = useCallback(
88
+ (type, options) => {
89
+ if (menuActionHandler && menuActionHandler(type, options)) {
90
+ return true;
91
+ }
92
+
93
+ if (inheritedMenuActionHandler && inheritedMenuActionHandler(type, options)) {
94
+ return true;
95
+ }
96
+ },
97
+ [inheritedMenuActionHandler, menuActionHandler]
98
+ );
99
+
100
+ return (
101
+ <ContextMenuContext.Provider
102
+ value={{
103
+ menuActionHandler: handleMenuAction,
104
+ menuBuilders
105
+ }}
106
+ >
107
+ {children}
108
+ </ContextMenuContext.Provider>
109
+ );
110
+ };
111
+
112
+ // Need an option for local menu to override higher-level menu, rather than extend
113
+ export const ContextMenuProvider = ({
114
+ children,
115
+ menuActionHandler,
116
+ menuBuilder,
117
+ menuItemDescriptors,
118
+ label
119
+ }) => {
120
+ return (
121
+ <ContextMenuContext.Consumer>
122
+ {(parentContext) => (
123
+ <Provider
124
+ context={parentContext || NO_INHERITED_CONTEXT}
125
+ label={label}
126
+ menuActionHandler={menuActionHandler}
127
+ menuBuilder={menuBuilder}
128
+ menuItemDescriptors={menuItemDescriptors}
129
+ >
130
+ {children}
131
+ </Provider>
132
+ )}
133
+ </ContextMenuContext.Consumer>
134
+ );
135
+ };
@@ -0,0 +1,4 @@
1
+ export { default as ContextMenu } from './ContextMenu';
2
+ export { default as MenuList } from './MenuList';
3
+ export * from './MenuList';
4
+ export * from './context-menu-provider';
@@ -0,0 +1,61 @@
1
+ function union(set1, ...sets) {
2
+ const result = new Set(set1);
3
+ for (let set of sets) {
4
+ for (let element of set) {
5
+ result.add(element);
6
+ }
7
+ }
8
+ return result;
9
+ }
10
+
11
+ export const ArrowUp = 'ArrowUp';
12
+ export const ArrowDown = 'ArrowDown';
13
+ export const ArrowLeft = 'ArrowLeft';
14
+ export const Backspace = 'Backspace';
15
+ export const ArrowRight = 'ArrowRight';
16
+ export const Enter = 'Enter';
17
+ export const Escape = 'Escape';
18
+ export const Delete = 'Delete';
19
+
20
+ const actionKeys = new Set([Enter, Delete]);
21
+ const focusKeys = new Set(['Tab']);
22
+ // const navigationKeys = new Set(["Home", "End", "ArrowRight", "ArrowLeft","ArrowDown", "ArrowUp"]);
23
+ const arrowLeftRightKeys = new Set(['ArrowRight', 'ArrowLeft']);
24
+ const verticalNavigationKeys = new Set(['Home', 'End', 'ArrowDown', 'ArrowUp']);
25
+ const horizontalNavigationKeys = new Set(['Home', 'End', 'ArrowRight', 'ArrowLeft']);
26
+ const functionKeys = new Set([
27
+ 'F1',
28
+ 'F2',
29
+ 'F3',
30
+ 'F4',
31
+ 'F5',
32
+ 'F6',
33
+ 'F7',
34
+ 'F8',
35
+ 'F9',
36
+ 'F10',
37
+ 'F11',
38
+ 'F12'
39
+ ]);
40
+ const specialKeys = union(
41
+ actionKeys,
42
+ horizontalNavigationKeys,
43
+ verticalNavigationKeys,
44
+ arrowLeftRightKeys,
45
+ functionKeys,
46
+ focusKeys
47
+ );
48
+ export const isCharacterKey = (evt) => {
49
+ if (specialKeys.has(evt.key)) {
50
+ return false;
51
+ }
52
+ if (typeof evt.which === 'number' && evt.which > 0) {
53
+ return !evt.ctrlKey && !evt.metaKey && !evt.altKey && evt.which !== 8;
54
+ }
55
+ };
56
+
57
+ export const isNavigationKey = ({ key }, orientation = 'vertical') => {
58
+ const navigationKeys =
59
+ orientation === 'vertical' ? verticalNavigationKeys : horizontalNavigationKeys;
60
+ return navigationKeys.has(key);
61
+ };
@@ -0,0 +1,22 @@
1
+ export const listItemElement = (listEl, listItemIdx) =>
2
+ listEl.querySelector(`:scope > [data-idx="${listItemIdx}"]`);
3
+
4
+ export function listItemIndex(listItemEl) {
5
+ if (listItemEl) {
6
+ let idx = listItemEl.dataset.idx;
7
+ if (idx) {
8
+ return parseInt(idx, 10);
9
+ // eslint-disable-next-line no-cond-assign
10
+ } else if ((idx = listItemEl.ariaPosInSet)) {
11
+ return parseInt(idx, 10) - 1;
12
+ }
13
+ }
14
+ }
15
+
16
+ export const listItemId = (el) => el?.id;
17
+
18
+ export const closestListItem = (el) => el.closest('[data-idx],[aria-posinset]');
19
+
20
+ export const closestListItemId = (el) => listItemId(closestListItem(el));
21
+
22
+ export const closestListItemIndex = (el) => listItemIndex(closestListItem(el));
@@ -0,0 +1,292 @@
1
+ import { useCallback, useMemo, useRef, useState } from "react";
2
+
3
+ import { closestListItem, listItemIndex } from "./list-dom-utils";
4
+ // import {mousePosition} from './aim/utils';
5
+ // import {aiming} from './aim/aim';
6
+
7
+ const nudge = (menus, distance, pos) => {
8
+ return menus.map((m, i) =>
9
+ i === menus.length - 1
10
+ ? {
11
+ ...m,
12
+ [pos]: m[pos] - distance,
13
+ }
14
+ : m
15
+ );
16
+ };
17
+ const nudgeLeft = (menus, distance) => nudge(menus, distance, "left");
18
+ const nudgeUp = (menus, distance) => nudge(menus, distance, "top");
19
+
20
+ const flipSides = (id, menus) => {
21
+ const [parentMenu, menu] = menus.slice(-2);
22
+ const el = document.getElementById(`${id}-${menu.id}`);
23
+ const { width } = el.getBoundingClientRect();
24
+ return menus.map((m) =>
25
+ m === menu
26
+ ? {
27
+ ...m,
28
+ left: parentMenu.left - (width - 2),
29
+ }
30
+ : m
31
+ );
32
+ };
33
+
34
+ const closedNode = (el) =>
35
+ el.ariaHasPopup === "true" && el.ariaExpanded !== "true";
36
+ const getPosition = (el, openMenus) => {
37
+ const [{ left, top: menuTop }] = openMenus.slice(-1);
38
+ // const {top, right, bottom, left} = el.getBoundingClientRect();
39
+ // this will not work for MenuList within window, we need the
40
+ // const {offsetLeft: left, offsetTop: menuTop} = el.closest('.hwMenuList');
41
+ const { offsetWidth: width, offsetTop: top } = el;
42
+ return { left: left + width, top: top + menuTop };
43
+ };
44
+
45
+ export const getItemId = (id) => {
46
+ let pos = id.lastIndexOf("-");
47
+ return pos === -1 ? id : id.slice(pos + 1);
48
+ };
49
+
50
+ export const getMenuId = (id) => {
51
+ const itemId = getItemId(id);
52
+ const pos = itemId.lastIndexOf(".");
53
+ return pos > -1 ? itemId.slice(0, pos) : "root";
54
+ };
55
+
56
+ const getMenuDepth = (id) => {
57
+ let count = 0,
58
+ pos = id.indexOf(".", 0);
59
+ while (pos !== -1) {
60
+ count += 1;
61
+ pos = id.indexOf(".", pos + 1);
62
+ }
63
+ return count;
64
+ };
65
+ const identifyItem = (el) => [
66
+ getMenuId(el.id),
67
+ getItemId(el.id),
68
+ el.ariaHasPopup === "true",
69
+ el.ariaExpanded === "true",
70
+ getMenuDepth(el.id),
71
+ ];
72
+
73
+ export const useCascade = ({
74
+ id,
75
+ onActivate,
76
+ onMouseEnterItem,
77
+ position: { x: posX, y: posY },
78
+ }) => {
79
+ const [, forceRefresh] = useState({});
80
+ const openMenus = useRef([{ id: "root", left: posX, top: posY }]);
81
+
82
+ const setOpenMenus = useCallback((menus) => {
83
+ openMenus.current = menus;
84
+ forceRefresh({});
85
+ }, []);
86
+
87
+ const menuOpenPendingTimeout = useRef(null);
88
+ const menuClosePendingTimeout = useRef(null);
89
+ const menuState = useRef({ root: "no-popup" });
90
+ const prevLevel = useRef(0);
91
+
92
+ // const prevAim = useRef({mousePos: null, distance: true});
93
+
94
+ const openMenu = useCallback(
95
+ (menuId = "root", itemId = null, listItemEl = null) => {
96
+ if (menuId === "root" && itemId === null) {
97
+ setOpenMenus([{ id: "root", left: posX, top: posY }]);
98
+ } else {
99
+ menuState.current[menuId] = "popup-open";
100
+ const doc = listItemEl ? listItemEl.ownerDocument : document;
101
+ const el = doc.getElementById(`${id}-${menuId}-${itemId}`);
102
+ const { left, top } = getPosition(el, openMenus.current);
103
+ setOpenMenus(openMenus.current.concat({ id: itemId, left, top }));
104
+ }
105
+ },
106
+ [id, posX, posY, setOpenMenus]
107
+ );
108
+
109
+ const closeMenu = useCallback(
110
+ (menuId) => {
111
+ if (menuId === "root") {
112
+ setOpenMenus([]);
113
+ } else {
114
+ setOpenMenus(openMenus.current.slice(0, -1));
115
+ }
116
+ },
117
+ [setOpenMenus]
118
+ );
119
+
120
+ const closeMenus = useCallback(
121
+ (menuId, itemId) => {
122
+ const menus = openMenus.current.slice();
123
+ let { id: lastMenuId } = menus[menus.length - 1];
124
+ while (menus.length > 1 && !itemId.startsWith(lastMenuId)) {
125
+ const parentMenuId = getMenuId(lastMenuId);
126
+ menus.pop();
127
+ menuState.current[lastMenuId] = "no-popup";
128
+ menuState.current[parentMenuId] = "no-popup";
129
+ ({ id: lastMenuId } = menus[menus.length - 1]);
130
+ }
131
+ if (menus.length < openMenus.current.length) {
132
+ setOpenMenus(menus);
133
+ }
134
+ },
135
+ [setOpenMenus]
136
+ );
137
+
138
+ const scheduleOpen = useCallback(
139
+ (menuId, itemId, listItemEl) => {
140
+ if (menuOpenPendingTimeout.current) {
141
+ clearTimeout(menuOpenPendingTimeout.current);
142
+ }
143
+ menuOpenPendingTimeout.current = setTimeout(() => {
144
+ console.log(`scheduleOpen timed out opening ${itemId}`);
145
+ closeMenus(menuId, itemId);
146
+ menuState.current[menuId] = "popup-open";
147
+ menuState.current[itemId] = "no-popup";
148
+ openMenu(menuId, itemId, listItemEl);
149
+ }, 400);
150
+ },
151
+ [closeMenus, openMenu]
152
+ );
153
+
154
+ const scheduleClose = useCallback(
155
+ (openMenuId, menuId, itemId) => {
156
+ console.log(
157
+ `scheduleClose openMenuId ${openMenuId} menuId ${menuId} itemId ${itemId}`
158
+ );
159
+ menuState.current[openMenuId] = "pending-close";
160
+ menuClosePendingTimeout.current = setTimeout(() => {
161
+ closeMenus(menuId, itemId);
162
+ }, 400);
163
+ },
164
+ [closeMenus]
165
+ );
166
+
167
+ const handleRender = useCallback(() => {
168
+ const { current: menus } = openMenus;
169
+ const [menu] = menus.slice(-1);
170
+ const el = document.getElementById(`${id}-${menu.id}`);
171
+ if (el) {
172
+ const { right, bottom } = el.getBoundingClientRect();
173
+ const { clientHeight, clientWidth } = document.body;
174
+ if (right > clientWidth) {
175
+ const newMenus =
176
+ menus.length > 1
177
+ ? flipSides(id, menus)
178
+ : nudgeLeft(menus, right - clientWidth);
179
+ setOpenMenus(newMenus);
180
+ } else if (bottom > clientHeight) {
181
+ const newMenus = nudgeUp(menus, bottom - clientHeight);
182
+ setOpenMenus(newMenus);
183
+ }
184
+ }
185
+ }, [id, setOpenMenus]);
186
+
187
+ const listItemProps = useMemo(
188
+ () => ({
189
+ onMouseEnter: (evt) => {
190
+ const listItemEl = closestListItem(evt.target);
191
+ const [menuId, itemId, isGroup, isOpen, level] =
192
+ identifyItem(listItemEl);
193
+ const sameLevel = prevLevel.current === level;
194
+ const {
195
+ current: { [menuId]: state },
196
+ } = menuState;
197
+ prevLevel.current = level;
198
+
199
+ // console.log(
200
+ // `%conMouseEnter #${menuId}[${itemId}] @${level}
201
+ // isGroup ${isGroup} isOpen ${isOpen}
202
+ // openMenus [${openMenus.current.join(',')}]
203
+ // state='${JSON.stringify(menuState.current)}`,
204
+ // 'color: green; font-weight: bold;'
205
+ // );
206
+
207
+ if (state === "no-popup" && isGroup) {
208
+ // Shouldn;t we always set this ?
209
+ menuState.current[menuId] = "popup-pending";
210
+ scheduleOpen(menuId, itemId, listItemEl);
211
+ } else if (state === "popup-pending" && !isGroup) {
212
+ menuState.current[menuId] = "no-popup";
213
+ clearTimeout(menuOpenPendingTimeout.current);
214
+ menuOpenPendingTimeout.current = null;
215
+ } else if (state === "popup-pending" && isGroup) {
216
+ clearTimeout(menuOpenPendingTimeout.current);
217
+ scheduleOpen(menuId, itemId, listItemEl);
218
+ } else if (state === "popup-open") {
219
+ const [{ id: parentMenuId }, { id: openMenuId }] =
220
+ openMenus.current.slice(-2);
221
+ if (
222
+ parentMenuId === menuId &&
223
+ menuState.current[openMenuId] !== "pending-close" &&
224
+ sameLevel
225
+ ) {
226
+ scheduleClose(openMenuId, menuId, itemId);
227
+ if (isGroup && !isOpen) {
228
+ scheduleOpen(menuId, itemId, listItemEl);
229
+ }
230
+ } else if (
231
+ parentMenuId === menuId &&
232
+ isGroup &&
233
+ itemId !== openMenuId &&
234
+ menuState.current[openMenuId] === "pending-close"
235
+ ) {
236
+ // if there is already an item queued for opening cancel it
237
+ scheduleOpen(menuId, itemId, listItemEl);
238
+ } else if (isGroup) {
239
+ closeMenus(menuId, itemId);
240
+ scheduleOpen(menuId, itemId, listItemEl);
241
+ } else if (
242
+ !(menuState.current[openMenuId] === "pending-close" && sameLevel)
243
+ ) {
244
+ closeMenus(menuId, itemId);
245
+ }
246
+ }
247
+
248
+ if (state === "pending-close") {
249
+ if (menuOpenPendingTimeout.current) {
250
+ clearTimeout(menuOpenPendingTimeout.current);
251
+ menuOpenPendingTimeout.current = null;
252
+ }
253
+ clearTimeout(menuClosePendingTimeout.current);
254
+ menuClosePendingTimeout.current = null;
255
+ menuState.current[menuId] = "popup-open";
256
+ }
257
+
258
+ onMouseEnterItem(evt, itemId);
259
+ },
260
+
261
+ onClick: (evt) => {
262
+ const listItemEl = closestListItem(evt.target);
263
+ const idx = listItemIndex(listItemEl);
264
+ if (closedNode(listItemEl).ariaHasPopup === "true") {
265
+ if (listItemEl.ariaExpanded !== "true") {
266
+ openMenu(idx);
267
+ } else {
268
+ // do nothing
269
+ }
270
+ } else {
271
+ onActivate(getItemId(listItemEl.id));
272
+ }
273
+ },
274
+ }),
275
+ [
276
+ closeMenus,
277
+ onActivate,
278
+ onMouseEnterItem,
279
+ openMenu,
280
+ scheduleClose,
281
+ scheduleOpen,
282
+ ]
283
+ );
284
+
285
+ return {
286
+ closeMenu,
287
+ handleRender,
288
+ listItemProps,
289
+ openMenu,
290
+ openMenus: openMenus.current,
291
+ };
292
+ };
@@ -0,0 +1,22 @@
1
+ import { useEffect } from 'react';
2
+
3
+ export const useClickAway = ({ containerClassName, isOpen, onClose }) => {
4
+ useEffect(() => {
5
+ const clickHandler = isOpen
6
+ ? (evt) => {
7
+ const container = evt.target.closest(`.${containerClassName}`);
8
+ if (container === null) {
9
+ onClose('root');
10
+ }
11
+ }
12
+ : null;
13
+
14
+ document.body.addEventListener('click', clickHandler, true);
15
+
16
+ return () => {
17
+ if (clickHandler) {
18
+ document.body.removeEventListener('click', clickHandler, true);
19
+ }
20
+ };
21
+ }, [containerClassName, isOpen, onClose]);
22
+ };