@harborclient/sdk 1.0.67 → 1.0.69
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/components/FooterIcon/index.d.ts +2 -1
- package/dist/components/FooterIcon/index.d.ts.map +1 -1
- package/dist/components/FooterIcon/index.js +3 -2
- package/dist/components/RowActionsMenu/Submenu.d.ts +49 -0
- package/dist/components/RowActionsMenu/Submenu.d.ts.map +1 -0
- package/dist/components/RowActionsMenu/Submenu.js +156 -0
- package/dist/components/RowActionsMenu/index.d.ts +58 -5
- package/dist/components/RowActionsMenu/index.d.ts.map +1 -1
- package/dist/components/RowActionsMenu/index.js +293 -55
- package/dist/components/TabBar/ClosingTabShell.d.ts +20 -0
- package/dist/components/TabBar/ClosingTabShell.d.ts.map +1 -0
- package/dist/components/TabBar/ClosingTabShell.js +126 -0
- package/dist/components/TabBar/TabBarShell.d.ts +58 -0
- package/dist/components/TabBar/TabBarShell.d.ts.map +1 -0
- package/dist/components/TabBar/TabBarShell.js +37 -0
- package/dist/components/TabBar/TabContextMenu.d.ts +23 -0
- package/dist/components/TabBar/TabContextMenu.d.ts.map +1 -0
- package/dist/components/TabBar/TabContextMenu.js +119 -0
- package/dist/components/TabBar/TabNewButton.d.ts +26 -0
- package/dist/components/TabBar/TabNewButton.d.ts.map +1 -0
- package/dist/components/TabBar/TabNewButton.js +14 -0
- package/dist/components/TabBar/index.d.ts +92 -0
- package/dist/components/TabBar/index.d.ts.map +1 -0
- package/dist/components/TabBar/index.js +224 -0
- package/dist/components/TabBar/tabCloseMenuHelpers.d.ts +41 -0
- package/dist/components/TabBar/tabCloseMenuHelpers.d.ts.map +1 -0
- package/dist/components/TabBar/tabCloseMenuHelpers.js +48 -0
- package/dist/components/TabBar/types.d.ts +60 -0
- package/dist/components/TabBar/types.d.ts.map +1 -0
- package/dist/components/TabBar/types.js +1 -0
- package/dist/components/TabBar/useExitingTabItems.d.ts +73 -0
- package/dist/components/TabBar/useExitingTabItems.d.ts.map +1 -0
- package/dist/components/TabBar/useExitingTabItems.js +103 -0
- package/dist/components/TabBar/useSortableTabItem.d.ts +31 -0
- package/dist/components/TabBar/useSortableTabItem.d.ts.map +1 -0
- package/dist/components/TabBar/useSortableTabItem.js +29 -0
- package/dist/components/Toolbar/index.d.ts.map +1 -1
- package/dist/components/Toolbar/index.js +21 -5
- package/dist/components/classes.d.ts +9 -0
- package/dist/components/classes.d.ts.map +1 -1
- package/dist/components/classes.js +15 -2
- package/dist/components/footerBarUtils.d.ts +11 -0
- package/dist/components/footerBarUtils.d.ts.map +1 -0
- package/dist/components/footerBarUtils.js +10 -0
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +3 -1
- package/dist/components/menuPosition.d.ts +48 -0
- package/dist/components/menuPosition.d.ts.map +1 -0
- package/dist/components/menuPosition.js +49 -0
- package/dist/components/rowActionsMenuHelpers.d.ts +17 -0
- package/dist/components/rowActionsMenuHelpers.d.ts.map +1 -1
- package/dist/components/rowActionsMenuHelpers.js +47 -0
- package/dist/styles.css +12 -0
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -1
|
@@ -1,34 +1,50 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@harborclient/sdk/jsx-runtime";
|
|
2
|
-
import { faBars, faCheck } from '@fortawesome/free-solid-svg-icons';
|
|
3
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from '@harborclient/sdk/react';
|
|
2
|
+
import { faBars, faCaretRight, faCheck } from '@fortawesome/free-solid-svg-icons';
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from '@harborclient/sdk/react';
|
|
4
4
|
import { Button } from '../Button/index.js';
|
|
5
5
|
import { FaIcon } from '../FaIcon/index.js';
|
|
6
|
-
import {
|
|
6
|
+
import { MENU_MIN_WIDTH_PX, clampMenuPosition, getTriggerAnchoredMenuPosition } from '../menuPosition.js';
|
|
7
|
+
import { portalToBody } from '../portalToBody.js';
|
|
8
|
+
import { findAdjacentEnabledIndex, findEdgeEnabledIndex, isMenuItemEnabled, menuItemClass } from '../rowActionsMenuHelpers.js';
|
|
9
|
+
import { cn, resolveMenuTypeahead } from '../utils.js';
|
|
10
|
+
import { Submenu } from './Submenu.js';
|
|
7
11
|
const TYPEAHEAD_TIMEOUT_MS = 500;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const base = 'flex w-full cursor-pointer items-center gap-2 border-none bg-transparent px-3.5 py-1.5 text-left text-[16px] app-no-drag';
|
|
13
|
-
return variant === 'danger'
|
|
14
|
-
? `${base} text-text hover:bg-danger/15 hover:text-danger`
|
|
15
|
-
: `${base} text-text hover:bg-selection`;
|
|
16
|
-
}
|
|
12
|
+
/** Delay before a hovered row's submenu opens, so passing over rows doesn't flash flyouts. */
|
|
13
|
+
const SUBMENU_OPEN_DELAY_MS = 120;
|
|
14
|
+
/** Delay before a submenu closes after the pointer leaves its row and panel. */
|
|
15
|
+
const SUBMENU_CLOSE_DELAY_MS = 200;
|
|
17
16
|
/**
|
|
18
17
|
* Hamburger-triggered dropdown for row-level actions (rename, delete, etc.).
|
|
18
|
+
*
|
|
19
|
+
* The menu panel is portaled to `document.body` with fixed positioning so it is
|
|
20
|
+
* not clipped by overflow-hidden sidebar or scroll containers. Items with a
|
|
21
|
+
* `submenu` open a nested flyout panel beside them on hover, click, or
|
|
22
|
+
* `ArrowRight`/`Enter`.
|
|
19
23
|
*/
|
|
20
|
-
export function RowActionsMenu({ groups, menuId, openMenuId, onOpenChange, className, ...props }) {
|
|
24
|
+
export function RowActionsMenu({ groups, menuId, openMenuId, onOpenChange, triggerVariant, triggerLabel, triggerIcon = faBars, triggerAriaLabel, triggerTitle, triggerClassName, className, ...props }) {
|
|
21
25
|
const isOpen = openMenuId === menuId;
|
|
22
26
|
const menuElementId = `${menuId}-menu`;
|
|
27
|
+
const submenuElementId = `${menuId}-submenu`;
|
|
23
28
|
const rootRef = useRef(null);
|
|
24
29
|
const triggerRef = useRef(null);
|
|
30
|
+
const panelRef = useRef(null);
|
|
25
31
|
const itemRefs = useRef([]);
|
|
26
32
|
const typeaheadBuffer = useRef('');
|
|
27
33
|
const typeaheadTimer = useRef(null);
|
|
28
34
|
const wasOpenRef = useRef(isOpen);
|
|
35
|
+
const submenuOpenTimer = useRef(null);
|
|
36
|
+
const submenuCloseTimer = useRef(null);
|
|
29
37
|
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
38
|
+
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
|
|
39
|
+
const [openSubmenuIndex, setOpenSubmenuIndex] = useState(null);
|
|
30
40
|
const flatItems = useMemo(() => groups.flat(), [groups]);
|
|
31
41
|
const itemLabels = useMemo(() => flatItems.map((item) => item.label), [flatItems]);
|
|
42
|
+
const hasEnabledItems = useMemo(() => flatItems.some((item) => isMenuItemEnabled(item)), [flatItems]);
|
|
43
|
+
const isLabeledTrigger = triggerLabel != null;
|
|
44
|
+
const resolvedTriggerVariant = triggerVariant ?? (isLabeledTrigger ? 'secondary' : 'icon');
|
|
45
|
+
const resolvedTriggerTitle = triggerTitle ?? (isLabeledTrigger ? undefined : 'Actions');
|
|
46
|
+
const resolvedTriggerAriaLabel = triggerAriaLabel ??
|
|
47
|
+
(typeof triggerLabel === 'string' ? triggerLabel : isLabeledTrigger ? 'Menu' : 'Row actions');
|
|
32
48
|
/**
|
|
33
49
|
* Clears the accumulated typeahead buffer.
|
|
34
50
|
*/
|
|
@@ -39,16 +55,102 @@ export function RowActionsMenu({ groups, menuId, openMenuId, onOpenChange, class
|
|
|
39
55
|
typeaheadTimer.current = null;
|
|
40
56
|
}
|
|
41
57
|
}, []);
|
|
58
|
+
/**
|
|
59
|
+
* Cancels any pending submenu open/close timers.
|
|
60
|
+
*/
|
|
61
|
+
const clearSubmenuTimers = useCallback(() => {
|
|
62
|
+
if (submenuOpenTimer.current != null) {
|
|
63
|
+
window.clearTimeout(submenuOpenTimer.current);
|
|
64
|
+
submenuOpenTimer.current = null;
|
|
65
|
+
}
|
|
66
|
+
if (submenuCloseTimer.current != null) {
|
|
67
|
+
window.clearTimeout(submenuCloseTimer.current);
|
|
68
|
+
submenuCloseTimer.current = null;
|
|
69
|
+
}
|
|
70
|
+
}, []);
|
|
42
71
|
/**
|
|
43
72
|
* Closes the menu and returns focus to the trigger button.
|
|
44
73
|
*/
|
|
45
74
|
const closeMenu = useCallback(() => {
|
|
46
75
|
clearTypeahead();
|
|
76
|
+
clearSubmenuTimers();
|
|
47
77
|
onOpenChange(null);
|
|
48
78
|
requestAnimationFrame(() => {
|
|
49
79
|
triggerRef.current?.focus();
|
|
50
80
|
});
|
|
51
|
-
}, [clearTypeahead, onOpenChange]);
|
|
81
|
+
}, [clearSubmenuTimers, clearTypeahead, onOpenChange]);
|
|
82
|
+
/**
|
|
83
|
+
* Opens the submenu belonging to the item at `index` immediately, canceling
|
|
84
|
+
* any pending open/close timers.
|
|
85
|
+
*/
|
|
86
|
+
const openSubmenuAt = useCallback((index) => {
|
|
87
|
+
clearSubmenuTimers();
|
|
88
|
+
setOpenSubmenuIndex(index);
|
|
89
|
+
}, [clearSubmenuTimers]);
|
|
90
|
+
/**
|
|
91
|
+
* Closes the currently open submenu and returns focus to its parent row.
|
|
92
|
+
*/
|
|
93
|
+
const closeSubmenuAndRefocus = useCallback(() => {
|
|
94
|
+
clearSubmenuTimers();
|
|
95
|
+
setOpenSubmenuIndex((current) => {
|
|
96
|
+
if (current != null) {
|
|
97
|
+
requestAnimationFrame(() => {
|
|
98
|
+
itemRefs.current[current]?.focus();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
});
|
|
103
|
+
}, [clearSubmenuTimers]);
|
|
104
|
+
/**
|
|
105
|
+
* Schedules opening the submenu at `index` after a short hover-intent
|
|
106
|
+
* delay, so moving the pointer across sibling rows doesn't flash flyouts.
|
|
107
|
+
*/
|
|
108
|
+
const scheduleOpenSubmenu = useCallback((index) => {
|
|
109
|
+
clearSubmenuTimers();
|
|
110
|
+
submenuOpenTimer.current = window.setTimeout(() => {
|
|
111
|
+
submenuOpenTimer.current = null;
|
|
112
|
+
setOpenSubmenuIndex(index);
|
|
113
|
+
}, SUBMENU_OPEN_DELAY_MS);
|
|
114
|
+
}, [clearSubmenuTimers]);
|
|
115
|
+
/**
|
|
116
|
+
* Schedules closing the open submenu after a short delay, giving the
|
|
117
|
+
* pointer time to travel from the row into the flyout panel.
|
|
118
|
+
*/
|
|
119
|
+
const scheduleCloseSubmenu = useCallback(() => {
|
|
120
|
+
if (submenuOpenTimer.current != null) {
|
|
121
|
+
window.clearTimeout(submenuOpenTimer.current);
|
|
122
|
+
submenuOpenTimer.current = null;
|
|
123
|
+
}
|
|
124
|
+
submenuCloseTimer.current = window.setTimeout(() => {
|
|
125
|
+
submenuCloseTimer.current = null;
|
|
126
|
+
setOpenSubmenuIndex(null);
|
|
127
|
+
}, SUBMENU_CLOSE_DELAY_MS);
|
|
128
|
+
}, []);
|
|
129
|
+
/**
|
|
130
|
+
* Handles pointer hover over a row: opens that row's submenu (immediately
|
|
131
|
+
* if a different submenu is already open, otherwise after a short delay),
|
|
132
|
+
* or schedules closing the open submenu when hovering a row without one.
|
|
133
|
+
*/
|
|
134
|
+
const handleItemMouseEnter = useCallback((item, itemIndex) => {
|
|
135
|
+
if (item.submenu) {
|
|
136
|
+
if (openSubmenuIndex === itemIndex) {
|
|
137
|
+
clearSubmenuTimers();
|
|
138
|
+
}
|
|
139
|
+
else if (openSubmenuIndex != null) {
|
|
140
|
+
openSubmenuAt(itemIndex);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
scheduleOpenSubmenu(itemIndex);
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (openSubmenuIndex != null) {
|
|
148
|
+
scheduleCloseSubmenu();
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
clearSubmenuTimers();
|
|
152
|
+
}
|
|
153
|
+
}, [clearSubmenuTimers, openSubmenuAt, openSubmenuIndex, scheduleCloseSubmenu, scheduleOpenSubmenu]);
|
|
52
154
|
/**
|
|
53
155
|
* Focuses a menu item by index and updates roving tabindex state.
|
|
54
156
|
*/
|
|
@@ -62,11 +164,35 @@ export function RowActionsMenu({ groups, menuId, openMenuId, onOpenChange, class
|
|
|
62
164
|
* Opens the menu and focuses the first or last item.
|
|
63
165
|
*/
|
|
64
166
|
const openMenu = useCallback((focusLast = false) => {
|
|
65
|
-
if (
|
|
167
|
+
if (!hasEnabledItems)
|
|
66
168
|
return;
|
|
67
|
-
|
|
169
|
+
const edgeIndex = findEdgeEnabledIndex(flatItems, focusLast);
|
|
170
|
+
if (edgeIndex == null)
|
|
171
|
+
return;
|
|
172
|
+
setFocusedIndex(edgeIndex);
|
|
68
173
|
onOpenChange(menuId);
|
|
69
|
-
}, [flatItems
|
|
174
|
+
}, [flatItems, hasEnabledItems, menuId, onOpenChange]);
|
|
175
|
+
/**
|
|
176
|
+
* Updates fixed menu coordinates from the trigger and measured panel size.
|
|
177
|
+
*/
|
|
178
|
+
const updateMenuPosition = useCallback(() => {
|
|
179
|
+
const trigger = triggerRef.current;
|
|
180
|
+
if (!trigger) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const triggerRect = trigger.getBoundingClientRect();
|
|
184
|
+
const panelRect = panelRef.current?.getBoundingClientRect();
|
|
185
|
+
const menuSize = {
|
|
186
|
+
width: panelRect?.width ?? MENU_MIN_WIDTH_PX,
|
|
187
|
+
height: panelRect?.height ?? 0
|
|
188
|
+
};
|
|
189
|
+
const requested = getTriggerAnchoredMenuPosition(triggerRect, menuSize);
|
|
190
|
+
if (menuSize.height > 0) {
|
|
191
|
+
setMenuPosition(clampMenuPosition(requested, menuSize));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
setMenuPosition(requested);
|
|
195
|
+
}, []);
|
|
70
196
|
/**
|
|
71
197
|
* Moves focus into the menu after it opens and item refs are mounted.
|
|
72
198
|
*/
|
|
@@ -79,30 +205,66 @@ export function RowActionsMenu({ groups, menuId, openMenuId, onOpenChange, class
|
|
|
79
205
|
wasOpenRef.current = isOpen;
|
|
80
206
|
}, [focusedIndex, isOpen]);
|
|
81
207
|
/**
|
|
82
|
-
* Resets item refs when the menu closes.
|
|
208
|
+
* Resets item refs and submenu state when the menu closes.
|
|
83
209
|
*/
|
|
84
210
|
useEffect(() => {
|
|
85
211
|
if (!isOpen) {
|
|
86
212
|
itemRefs.current = [];
|
|
87
213
|
setFocusedIndex(0);
|
|
214
|
+
setOpenSubmenuIndex(null);
|
|
88
215
|
clearTypeahead();
|
|
216
|
+
clearSubmenuTimers();
|
|
89
217
|
}
|
|
90
|
-
}, [clearTypeahead, isOpen]);
|
|
218
|
+
}, [clearSubmenuTimers, clearTypeahead, isOpen]);
|
|
91
219
|
/**
|
|
92
|
-
*
|
|
220
|
+
* Re-clamps the portaled menu after mount once panel dimensions are known.
|
|
221
|
+
*/
|
|
222
|
+
useLayoutEffect(() => {
|
|
223
|
+
if (!isOpen) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
updateMenuPosition();
|
|
227
|
+
}, [groups, isOpen, updateMenuPosition]);
|
|
228
|
+
/**
|
|
229
|
+
* Tracks trigger movement while the menu is open so fixed coordinates stay aligned.
|
|
230
|
+
*/
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
if (!isOpen) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
updateMenuPosition();
|
|
236
|
+
window.addEventListener('scroll', updateMenuPosition, true);
|
|
237
|
+
window.addEventListener('resize', updateMenuPosition);
|
|
238
|
+
return () => {
|
|
239
|
+
window.removeEventListener('scroll', updateMenuPosition, true);
|
|
240
|
+
window.removeEventListener('resize', updateMenuPosition);
|
|
241
|
+
};
|
|
242
|
+
}, [isOpen, updateMenuPosition]);
|
|
243
|
+
/**
|
|
244
|
+
* Closes the menu on outside click, and closes the open submenu (or, if
|
|
245
|
+
* none is open, the whole menu) on Escape, while the menu is open.
|
|
93
246
|
*/
|
|
94
247
|
useEffect(() => {
|
|
95
248
|
if (!isOpen)
|
|
96
249
|
return;
|
|
97
250
|
const handleMouseDown = (e) => {
|
|
98
|
-
|
|
99
|
-
|
|
251
|
+
const target = e.target;
|
|
252
|
+
if (rootRef.current?.contains(target) ||
|
|
253
|
+
panelRef.current?.contains(target) ||
|
|
254
|
+
document.getElementById(submenuElementId)?.contains(target)) {
|
|
255
|
+
return;
|
|
100
256
|
}
|
|
257
|
+
closeMenu();
|
|
101
258
|
};
|
|
102
259
|
const handleKeyDown = (e) => {
|
|
103
260
|
if (e.key === 'Escape') {
|
|
104
261
|
e.preventDefault();
|
|
105
|
-
|
|
262
|
+
if (openSubmenuIndex != null) {
|
|
263
|
+
closeSubmenuAndRefocus();
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
closeMenu();
|
|
267
|
+
}
|
|
106
268
|
}
|
|
107
269
|
};
|
|
108
270
|
document.addEventListener('mousedown', handleMouseDown);
|
|
@@ -111,7 +273,7 @@ export function RowActionsMenu({ groups, menuId, openMenuId, onOpenChange, class
|
|
|
111
273
|
document.removeEventListener('mousedown', handleMouseDown);
|
|
112
274
|
document.removeEventListener('keydown', handleKeyDown);
|
|
113
275
|
};
|
|
114
|
-
}, [closeMenu, isOpen]);
|
|
276
|
+
}, [closeMenu, closeSubmenuAndRefocus, isOpen, openSubmenuIndex, submenuElementId]);
|
|
115
277
|
/**
|
|
116
278
|
* Handles keyboard interaction on the menu trigger when closed.
|
|
117
279
|
*/
|
|
@@ -129,24 +291,59 @@ export function RowActionsMenu({ groups, menuId, openMenuId, onOpenChange, class
|
|
|
129
291
|
}
|
|
130
292
|
};
|
|
131
293
|
/**
|
|
132
|
-
* Handles keyboard navigation within the open menu.
|
|
294
|
+
* Handles keyboard navigation within the open menu. Arrow/Home/End/typeahead
|
|
295
|
+
* are ignored while a submenu has focus, since `Submenu` stops propagation
|
|
296
|
+
* for the keys it handles itself.
|
|
133
297
|
*/
|
|
134
298
|
const handleMenuKeyDown = (event) => {
|
|
135
|
-
if (
|
|
299
|
+
if (!hasEnabledItems)
|
|
136
300
|
return;
|
|
137
301
|
if (event.key === 'Tab') {
|
|
138
302
|
closeMenu();
|
|
139
303
|
return;
|
|
140
304
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
305
|
+
if (event.key === 'ArrowRight') {
|
|
306
|
+
const candidate = flatItems[focusedIndex];
|
|
307
|
+
if (candidate?.submenu) {
|
|
308
|
+
event.preventDefault();
|
|
309
|
+
openSubmenuAt(focusedIndex);
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
314
|
+
const direction = event.key === 'ArrowDown' ? 1 : -1;
|
|
315
|
+
const nextIndex = findAdjacentEnabledIndex(flatItems, focusedIndex, direction);
|
|
316
|
+
if (nextIndex != null) {
|
|
317
|
+
event.preventDefault();
|
|
318
|
+
clearTypeahead();
|
|
319
|
+
focusItem(nextIndex);
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (event.key === 'Home') {
|
|
324
|
+
const firstIndex = findEdgeEnabledIndex(flatItems, false);
|
|
325
|
+
if (firstIndex != null) {
|
|
326
|
+
event.preventDefault();
|
|
327
|
+
clearTypeahead();
|
|
328
|
+
focusItem(firstIndex);
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (event.key === 'End') {
|
|
333
|
+
const lastIndex = findEdgeEnabledIndex(flatItems, true);
|
|
334
|
+
if (lastIndex != null) {
|
|
335
|
+
event.preventDefault();
|
|
336
|
+
clearTypeahead();
|
|
337
|
+
focusItem(lastIndex);
|
|
338
|
+
}
|
|
146
339
|
return;
|
|
147
340
|
}
|
|
148
341
|
const typeahead = resolveMenuTypeahead(itemLabels, focusedIndex, event.key, typeaheadBuffer.current);
|
|
149
342
|
if (typeahead) {
|
|
343
|
+
const candidate = flatItems[typeahead.index];
|
|
344
|
+
if (!candidate || !isMenuItemEnabled(candidate)) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
150
347
|
event.preventDefault();
|
|
151
348
|
typeaheadBuffer.current = typeahead.buffer;
|
|
152
349
|
if (typeaheadTimer.current != null) {
|
|
@@ -159,28 +356,69 @@ export function RowActionsMenu({ groups, menuId, openMenuId, onOpenChange, class
|
|
|
159
356
|
focusItem(typeahead.index);
|
|
160
357
|
}
|
|
161
358
|
};
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
359
|
+
const openSubmenuItem = openSubmenuIndex != null ? flatItems[openSubmenuIndex] : undefined;
|
|
360
|
+
const openSubmenuAnchorRect = openSubmenuIndex != null ? itemRefs.current[openSubmenuIndex]?.getBoundingClientRect() : null;
|
|
361
|
+
const menuPanel = isOpen ? (_jsx("div", { ref: panelRef, id: menuElementId, role: "menu", className: "hc-row-actions-menu-panel app-no-drag fixed z-50 min-w-[200px] rounded-md border border-separator bg-surface py-1 shadow-md", style: { left: menuPosition.x, top: menuPosition.y }, onKeyDown: handleMenuKeyDown, onMouseLeave: () => {
|
|
362
|
+
if (openSubmenuIndex != null) {
|
|
363
|
+
scheduleCloseSubmenu();
|
|
364
|
+
}
|
|
365
|
+
}, children: groups.map((group, groupIndex) => {
|
|
366
|
+
let flatIndex = groups.slice(0, groupIndex).reduce((count, g) => count + g.length, 0);
|
|
367
|
+
return (_jsx("div", { className: groupIndex > 0
|
|
368
|
+
? 'hc-row-actions-menu-group border-t border-separator'
|
|
369
|
+
: 'hc-row-actions-menu-group', children: group.map((item) => {
|
|
370
|
+
const itemIndex = flatIndex++;
|
|
371
|
+
const isCheckboxItem = item.checked !== undefined;
|
|
372
|
+
const isDisabled = item.disabled === true;
|
|
373
|
+
const hasSubmenu = item.submenu !== undefined;
|
|
374
|
+
return (_jsxs("button", { ref: (el) => {
|
|
375
|
+
itemRefs.current[itemIndex] = isDisabled ? null : el;
|
|
376
|
+
}, type: "button", role: isCheckboxItem ? 'menuitemcheckbox' : 'menuitem', "aria-checked": isCheckboxItem ? item.checked : undefined, "aria-disabled": isDisabled || undefined, "aria-haspopup": hasSubmenu ? 'menu' : undefined, "aria-expanded": hasSubmenu ? openSubmenuIndex === itemIndex : undefined, "aria-controls": hasSubmenu && openSubmenuIndex === itemIndex ? submenuElementId : undefined, disabled: isDisabled, tabIndex: isDisabled ? -1 : itemIndex === focusedIndex ? 0 : -1, className: cn('hc-row-actions-menu-item', menuItemClass(item.variant, isDisabled)), onMouseEnter: () => {
|
|
377
|
+
if (!isDisabled) {
|
|
378
|
+
handleItemMouseEnter(item, itemIndex);
|
|
379
|
+
}
|
|
380
|
+
}, onClick: (e) => {
|
|
381
|
+
e.stopPropagation();
|
|
382
|
+
if (isDisabled) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (item.submenu) {
|
|
386
|
+
if (openSubmenuIndex === itemIndex) {
|
|
387
|
+
closeSubmenuAndRefocus();
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
openSubmenuAt(itemIndex);
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
closeMenu();
|
|
395
|
+
item.onSelect();
|
|
396
|
+
}, children: [isCheckboxItem ? (_jsx("span", { className: "hc-row-actions-menu-item-check inline-flex w-4 shrink-0 justify-center", "aria-hidden": true, children: item.checked ? _jsx(FaIcon, { icon: faCheck, className: "h-3 w-3" }) : null })) : null, _jsx("span", { className: "hc-row-actions-menu-item-label min-w-0", children: item.label }), hasSubmenu ? (_jsx(FaIcon, { icon: faCaretRight, className: "ml-auto h-3 w-3 shrink-0", "aria-hidden": true })) : null] }, item.label));
|
|
397
|
+
}) }, groupIndex));
|
|
398
|
+
}) })) : null;
|
|
399
|
+
const submenuPanel = openSubmenuItem?.submenu && openSubmenuAnchorRect ? (_jsx(Submenu, { groups: openSubmenuItem.submenu, anchorRect: openSubmenuAnchorRect, menuElementId: submenuElementId, onSelectItem: (item) => {
|
|
400
|
+
closeMenu();
|
|
401
|
+
item.onSelect?.();
|
|
402
|
+
}, onRequestClose: closeSubmenuAndRefocus, onCloseAll: closeMenu, onMouseEnter: clearSubmenuTimers, onMouseLeave: scheduleCloseSubmenu })) : null;
|
|
403
|
+
const labeledTriggerVariant = resolvedTriggerVariant === 'icon' || resolvedTriggerVariant === 'iconDanger'
|
|
404
|
+
? 'secondary'
|
|
405
|
+
: resolvedTriggerVariant;
|
|
406
|
+
const triggerButton = isLabeledTrigger ? (_jsxs(Button, { innerRef: triggerRef, type: "button", variant: labeledTriggerVariant, className: cn('hc-row-actions-menu-trigger', triggerClassName), title: resolvedTriggerTitle, "aria-haspopup": "menu", "aria-expanded": isOpen, "aria-controls": isOpen ? menuElementId : undefined, onClick: (e) => {
|
|
407
|
+
e.stopPropagation();
|
|
408
|
+
if (isOpen) {
|
|
409
|
+
closeMenu();
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
openMenu(false);
|
|
413
|
+
}
|
|
414
|
+
}, onKeyDown: handleTriggerKeyDown, children: [_jsx(FaIcon, { icon: triggerIcon, className: "h-3.5 w-3.5", "aria-hidden": true }), triggerLabel] })) : (_jsx(Button, { innerRef: triggerRef, type: "button", variant: "icon", className: cn('hc-row-actions-menu-trigger', triggerClassName), title: resolvedTriggerTitle, "aria-label": resolvedTriggerAriaLabel, "aria-haspopup": "menu", "aria-expanded": isOpen, "aria-controls": isOpen ? menuElementId : undefined, onClick: (e) => {
|
|
415
|
+
e.stopPropagation();
|
|
416
|
+
if (isOpen) {
|
|
417
|
+
closeMenu();
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
openMenu(false);
|
|
421
|
+
}
|
|
422
|
+
}, onKeyDown: handleTriggerKeyDown, children: _jsx(FaIcon, { icon: triggerIcon, className: "h-3.5 w-3.5" }) }));
|
|
423
|
+
return (_jsxs("div", { ref: rootRef, ...props, className: cn('hc-row-actions-menu shrink-0', className), children: [triggerButton, menuPanel ? portalToBody(menuPanel) : null, submenuPanel] }));
|
|
186
424
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { JSX, ReactNode } from 'react';
|
|
2
|
+
interface Props {
|
|
3
|
+
/**
|
|
4
|
+
* Tab row content to measure and collapse horizontally.
|
|
5
|
+
*/
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
/**
|
|
8
|
+
* Called after the close width transition finishes.
|
|
9
|
+
*/
|
|
10
|
+
onComplete: () => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Horizontally collapses a tab row with a quick width and opacity "zap out".
|
|
14
|
+
*
|
|
15
|
+
* Keeps children mounted until the width transition reaches zero so siblings
|
|
16
|
+
* do not reflow until the animation completes.
|
|
17
|
+
*/
|
|
18
|
+
export declare function ClosingTabShell({ children, onComplete }: Props): JSX.Element | null;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=ClosingTabShell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ClosingTabShell.d.ts","sourceRoot":"","sources":["../../../src/components/TabBar/ClosingTabShell.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,SAAS,EAAsD,MAAM,OAAO,CAAC;AAEhG,UAAU,KAAK;IACb;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;OAEG;IACH,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB;AA+HD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,KAAK,GAAG,GAAG,CAAC,OAAO,GAAG,IAAI,CA0CnF"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@harborclient/sdk/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from '@harborclient/sdk/react';
|
|
3
|
+
/**
|
|
4
|
+
* Returns whether the user prefers reduced motion.
|
|
5
|
+
*
|
|
6
|
+
* @returns True when the OS requests minimized animation.
|
|
7
|
+
*/
|
|
8
|
+
function prefersReducedMotion() {
|
|
9
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Manages width collapse state for a single closing tab shell.
|
|
13
|
+
*
|
|
14
|
+
* @param onComplete - Invoked after the close animation finishes.
|
|
15
|
+
* @returns State and handlers for the animated shell.
|
|
16
|
+
*/
|
|
17
|
+
function useClosingTabShell(onComplete) {
|
|
18
|
+
const innerRef = useRef(null);
|
|
19
|
+
const completedRef = useRef(false);
|
|
20
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
21
|
+
const [contentWidth, setContentWidth] = useState(0);
|
|
22
|
+
const [transitionEnabled, setTransitionEnabled] = useState(false);
|
|
23
|
+
const [reducedMotion, setReducedMotion] = useState(prefersReducedMotion);
|
|
24
|
+
/**
|
|
25
|
+
* Subscribes to OS reduced-motion preference changes.
|
|
26
|
+
*/
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
29
|
+
/**
|
|
30
|
+
* Updates reduced-motion state when the OS preference changes.
|
|
31
|
+
*/
|
|
32
|
+
const handleChange = () => {
|
|
33
|
+
setReducedMotion(mediaQuery.matches);
|
|
34
|
+
};
|
|
35
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
36
|
+
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
37
|
+
}, []);
|
|
38
|
+
/**
|
|
39
|
+
* Measures the tab row so the outer shell can animate from its natural width to zero.
|
|
40
|
+
*/
|
|
41
|
+
useLayoutEffect(() => {
|
|
42
|
+
if (!innerRef.current) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const element = innerRef.current;
|
|
46
|
+
/**
|
|
47
|
+
* Writes the latest measured width from layout or resize observation.
|
|
48
|
+
*
|
|
49
|
+
* @param width - Measured inner width in pixels.
|
|
50
|
+
*/
|
|
51
|
+
const applyWidth = (width) => {
|
|
52
|
+
if (width > 0) {
|
|
53
|
+
setContentWidth(width);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
57
|
+
applyWidth(entry.contentRect.width);
|
|
58
|
+
});
|
|
59
|
+
observer.observe(element);
|
|
60
|
+
applyWidth(element.getBoundingClientRect().width);
|
|
61
|
+
return () => observer.disconnect();
|
|
62
|
+
}, []);
|
|
63
|
+
/**
|
|
64
|
+
* Starts the collapse animation after the first paint, or completes immediately
|
|
65
|
+
* when reduced motion is enabled.
|
|
66
|
+
*/
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (reducedMotion) {
|
|
69
|
+
onComplete();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (contentWidth <= 0) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
requestAnimationFrame(() => {
|
|
76
|
+
setTransitionEnabled(true);
|
|
77
|
+
requestAnimationFrame(() => {
|
|
78
|
+
setCollapsed(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}, [contentWidth, onComplete, reducedMotion]);
|
|
82
|
+
/**
|
|
83
|
+
* Finishes the exit animation once the width transition completes.
|
|
84
|
+
*/
|
|
85
|
+
const handleTransitionEnd = useCallback((event) => {
|
|
86
|
+
if (event.propertyName !== 'width' || completedRef.current) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
completedRef.current = true;
|
|
90
|
+
onComplete();
|
|
91
|
+
}, [onComplete]);
|
|
92
|
+
return {
|
|
93
|
+
collapsed,
|
|
94
|
+
contentWidth,
|
|
95
|
+
innerRef,
|
|
96
|
+
reducedMotion,
|
|
97
|
+
transitionEnabled,
|
|
98
|
+
handleTransitionEnd
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Horizontally collapses a tab row with a quick width and opacity "zap out".
|
|
103
|
+
*
|
|
104
|
+
* Keeps children mounted until the width transition reaches zero so siblings
|
|
105
|
+
* do not reflow until the animation completes.
|
|
106
|
+
*/
|
|
107
|
+
export function ClosingTabShell({ children, onComplete }) {
|
|
108
|
+
const { collapsed, contentWidth, innerRef, reducedMotion, transitionEnabled, handleTransitionEnd } = useClosingTabShell(onComplete);
|
|
109
|
+
if (reducedMotion) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const outerClassName = [
|
|
113
|
+
'flex shrink-0 overflow-hidden',
|
|
114
|
+
transitionEnabled
|
|
115
|
+
? 'transition-[width] duration-150 ease-in motion-reduce:transition-none'
|
|
116
|
+
: 'transition-none'
|
|
117
|
+
].join(' ');
|
|
118
|
+
const innerClassName = [
|
|
119
|
+
'flex shrink-0',
|
|
120
|
+
transitionEnabled
|
|
121
|
+
? 'transition-[opacity,transform] duration-150 ease-in origin-right motion-reduce:transition-none'
|
|
122
|
+
: 'transition-none',
|
|
123
|
+
collapsed ? 'scale-x-95 opacity-0' : 'scale-x-100 opacity-100'
|
|
124
|
+
].join(' ');
|
|
125
|
+
return (_jsx("div", { className: outerClassName, style: { width: collapsed ? 0 : contentWidth }, onTransitionEnd: handleTransitionEnd, "aria-hidden": true, inert: true, children: _jsx("div", { ref: innerRef, className: innerClassName, children: children }) }));
|
|
126
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { JSX, MouseEvent } from 'react';
|
|
2
|
+
import type { TabBarItem, TabBarSortableCursor } from './types.js';
|
|
3
|
+
interface Props<TId extends string | number> {
|
|
4
|
+
/**
|
|
5
|
+
* Tab row data to render.
|
|
6
|
+
*/
|
|
7
|
+
item: TabBarItem<TId>;
|
|
8
|
+
/**
|
|
9
|
+
* Tab order index for the tab label; all tabs stay in sequential Tab order.
|
|
10
|
+
*/
|
|
11
|
+
tabIndex: number;
|
|
12
|
+
/**
|
|
13
|
+
* Stable dnd-kit sortable id for this tab row.
|
|
14
|
+
*/
|
|
15
|
+
sortableId: string;
|
|
16
|
+
/**
|
|
17
|
+
* When true, drag reordering is disabled for this tab.
|
|
18
|
+
*/
|
|
19
|
+
sortableDisabled?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* When true, renders a non-interactive snapshot for the close animation.
|
|
22
|
+
*/
|
|
23
|
+
exiting?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Prefix for tab element ids (for example `request-tab-`).
|
|
26
|
+
*/
|
|
27
|
+
tabIdPrefix: string;
|
|
28
|
+
/**
|
|
29
|
+
* Prefix for linked tab panel ids (for example `request-tabpanel-`).
|
|
30
|
+
*/
|
|
31
|
+
panelIdPrefix: string;
|
|
32
|
+
/**
|
|
33
|
+
* Maximum width class for the tab shell.
|
|
34
|
+
*/
|
|
35
|
+
maxTabWidthClass?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Cursor style when sortable drag is enabled.
|
|
38
|
+
*/
|
|
39
|
+
sortableCursor?: TabBarSortableCursor;
|
|
40
|
+
/**
|
|
41
|
+
* Called when the user selects this tab.
|
|
42
|
+
*/
|
|
43
|
+
onSelect: (id: TId) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Called when the user closes this tab.
|
|
46
|
+
*/
|
|
47
|
+
onClose: (id: TId) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Called when the user opens the tab context menu.
|
|
50
|
+
*/
|
|
51
|
+
onContextMenu?: (id: TId, event: MouseEvent<HTMLDivElement>) => void;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Single document-style tab row with close button and optional drag reordering.
|
|
55
|
+
*/
|
|
56
|
+
export declare function TabBarShell<TId extends string | number>({ item, tabIndex, sortableId, sortableDisabled, exiting, tabIdPrefix, panelIdPrefix, maxTabWidthClass, sortableCursor, onSelect, onClose, onContextMenu }: Props<TId>): JSX.Element;
|
|
57
|
+
export {};
|
|
58
|
+
//# sourceMappingURL=TabBarShell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TabBarShell.d.ts","sourceRoot":"","sources":["../../../src/components/TabBar/TabBarShell.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAiB,UAAU,EAAE,MAAM,OAAO,CAAC;AAI5D,OAAO,KAAK,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGnE,UAAU,KAAK,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM;IACzC;;OAEG;IACH,IAAI,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC;IAEtB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,cAAc,CAAC,EAAE,oBAAoB,CAAC;IAEtC;;OAEG;IACH,QAAQ,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,IAAI,CAAC;IAE5B;;OAEG;IACH,OAAO,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,IAAI,CAAC;IAE3B;;OAEG;IACH,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,UAAU,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;CACtE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,SAAS,MAAM,GAAG,MAAM,EAAE,EACvD,IAAI,EACJ,QAAQ,EACR,UAAU,EACV,gBAAwB,EACxB,OAAe,EACf,WAAW,EACX,aAAa,EACb,gBAAkC,EAClC,cAA0B,EAC1B,QAAQ,EACR,OAAO,EACP,aAAa,EACd,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,OAAO,CA4E1B"}
|