@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.
- package/LICENSE +201 -0
- package/cjs/index.js +7395 -0
- package/index.css +952 -0
- package/index.css.map +7 -0
- package/package.json +12 -13
- package/src/chest-of-drawers/Drawer.css +46 -40
- package/src/chest-of-drawers/Drawer.tsx +4 -4
- package/src/dialog/Dialog.tsx +2 -2
- package/src/drag-drop/BoxModel.ts +1 -1
- package/src/drag-drop/DropMenu.css +15 -14
- package/src/drag-drop/DropMenu.tsx +8 -15
- package/src/drag-drop/DropTarget.ts +6 -6
- package/src/drag-drop/DropTargetRenderer.css +3 -3
- package/src/drag-drop/DropTargetRenderer.tsx +8 -13
- package/src/flexbox/Flexbox.tsx +1 -1
- package/src/flexbox/FluidGrid.tsx +1 -1
- package/src/index.ts +2 -1
- package/src/layout-header/Header.css +3 -3
- package/src/layout-header/Header.tsx +2 -2
- package/src/layout-provider/useLayoutDragDrop.ts +3 -3
- package/src/layout-view/View.css +5 -2
- package/src/layout-view/View.tsx +1 -1
- package/src/layout-view/useViewResize.ts +1 -1
- package/src/menu/ContextMenu.css +22 -0
- package/src/menu/ContextMenu.jsx +121 -0
- package/src/menu/MenuList.css +150 -0
- package/src/menu/MenuList.jsx +179 -0
- package/src/menu/aim/aim.js +92 -0
- package/src/menu/aim/corners.js +114 -0
- package/src/menu/aim/point-in-polygon.js +25 -0
- package/src/menu/aim/utils.js +19 -0
- package/src/menu/context-menu-provider.jsx +135 -0
- package/src/menu/index.js +4 -0
- package/src/menu/key-code.js +61 -0
- package/src/menu/list-dom-utils.js +22 -0
- package/src/menu/use-cascade.js +292 -0
- package/src/menu/use-click-away.js +22 -0
- package/src/menu/use-items-with-ids.js +75 -0
- package/src/menu/use-keyboard-navigation.js +162 -0
- package/src/menu/utils.js +5 -0
- package/src/palette/Palette.css +2 -2
- package/src/palette/Palette.tsx +1 -1
- package/src/palette/{PaletteUitk.css → PaletteSalt.css} +0 -0
- package/src/palette/{PaletteUitk.tsx → PaletteSalt.tsx} +4 -4
- package/src/palette/index.ts +1 -1
- package/src/popup/index.js +2 -0
- package/src/popup/popup-provider.js +0 -0
- package/src/popup/popup-service.css +15 -0
- package/src/popup/popup-service.js +281 -0
- package/src/portal/Portal.jsx +50 -0
- package/src/portal/index.ts +3 -0
- package/src/portal/render-portal.jsx +68 -0
- package/src/portal/utils.js +16 -0
- package/src/responsive/breakpoints.ts +22 -8
- package/src/stack/Stack.css +3 -3
- package/src/stack/Stack.tsx +3 -2
- package/src/stack/StackLayout.tsx +1 -1
- package/src/tools/devtools-box/layout-configurator.jsx +1 -1
- package/src/tools/devtools-tree/layout-tree-viewer.jsx +1 -1
- package/src/utils/apply-handlers.js +15 -0
- package/README.md +0 -1
- package/src/action-buttons/action-buttons.css +0 -12
- 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,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
|
+
};
|