@joewinke/jatui 0.36.2 → 0.36.3
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/actions/floating.js
CHANGED
|
@@ -132,10 +132,20 @@ export function floating(node, options) {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
startAutoUpdate();
|
|
135
|
-
// Animate in after first position has been applied (next microtask)
|
|
135
|
+
// Animate in after first position has been applied (next microtask).
|
|
136
|
+
// The end value is `none`, NOT `translateY(0)`: a non-none inline transform
|
|
137
|
+
// makes this element the CONTAINING BLOCK for any descendant
|
|
138
|
+
// `position: fixed` popover — a nested ContextMenu submenu is rendered
|
|
139
|
+
// inside this panel — silently re-anchoring that popover's viewport
|
|
140
|
+
// coordinates to this element (MOS jatui-fjch2: a submenu rendered on top
|
|
141
|
+
// of its parent menu). `none` still interpolates from translateY(2px)
|
|
142
|
+
// through the identity matrix, so the entrance animation is visually
|
|
143
|
+
// identical, and once it completes the element no longer creates a
|
|
144
|
+
// containing block. (opacity < 1 only creates a stacking context, not a
|
|
145
|
+
// containing block, so it is safe to keep.)
|
|
136
146
|
requestAnimationFrame(() => {
|
|
137
147
|
node.style.opacity = '1';
|
|
138
|
-
node.style.transform = '
|
|
148
|
+
node.style.transform = 'none';
|
|
139
149
|
});
|
|
140
150
|
// Defer outside-pointer listeners to avoid catching the opening event itself
|
|
141
151
|
const listenerTimer = setTimeout(() => {
|
|
@@ -68,9 +68,19 @@
|
|
|
68
68
|
* menus; the component sets this to `false` when it recursively renders
|
|
69
69
|
* itself as a nested submenu so that arrow-navigating over a parent item
|
|
70
70
|
* with a submenu doesn't yank keyboard focus out of the parent row.
|
|
71
|
-
*
|
|
71
|
+
* The initial programmatic autofocus does NOT open a submenu that
|
|
72
|
+
* happens to live on the first item — a submenu opens on hover, click,
|
|
73
|
+
* Enter/Space or ArrowRight only. ArrowRight from the parent explicitly
|
|
74
|
+
* focuses into the submenu.
|
|
72
75
|
*/
|
|
73
76
|
autoFocus?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Floating-ui placement for this menu panel. Top-level menus anchor at
|
|
79
|
+
* the cursor and open 'bottom-start'; a nested submenu passes
|
|
80
|
+
* 'left-start' when it must flip to the parent's LEFT edge because
|
|
81
|
+
* there is no room to the right of it.
|
|
82
|
+
*/
|
|
83
|
+
placement?: 'bottom-start' | 'left-start';
|
|
74
84
|
/**
|
|
75
85
|
* Called when an action item (not a submenu owner) is chosen, before
|
|
76
86
|
* this menu closes. A nested submenu uses it to close the WHOLE menu
|
|
@@ -89,6 +99,7 @@
|
|
|
89
99
|
class: className = '',
|
|
90
100
|
onClose,
|
|
91
101
|
autoFocus = true,
|
|
102
|
+
placement: placementProp = 'bottom-start',
|
|
92
103
|
onItemSelected
|
|
93
104
|
}: Props = $props();
|
|
94
105
|
|
|
@@ -97,6 +108,15 @@
|
|
|
97
108
|
let submenuIndex = $state<number | null>(null);
|
|
98
109
|
let submenuX = $state(0);
|
|
99
110
|
let submenuY = $state(0);
|
|
111
|
+
// 'bottom-start' when the submenu opens to the right of the parent item,
|
|
112
|
+
// 'left-start' when it must flip to the parent's LEFT edge (viewport edge).
|
|
113
|
+
let submenuPlacement = $state<'bottom-start' | 'left-start'>('bottom-start');
|
|
114
|
+
|
|
115
|
+
// Suppresses the 'onfocus' open-submenu side effect DURING the menu's
|
|
116
|
+
// own initial programmatic autofocus, so a first item that happens to
|
|
117
|
+
// own a submenu does not instantly open it before any user interaction
|
|
118
|
+
// (the submenu still opens on hover, click, Enter/Space or ArrowRight).
|
|
119
|
+
let suppressFocusOpen = false;
|
|
100
120
|
|
|
101
121
|
/**
|
|
102
122
|
* Unique id for THIS menu panel. The roving-focus queries below target
|
|
@@ -127,8 +147,20 @@
|
|
|
127
147
|
|
|
128
148
|
function openSubmenuAt(el: HTMLElement, index: number) {
|
|
129
149
|
const rect = el.getBoundingClientRect();
|
|
130
|
-
|
|
150
|
+
// The submenu panel is `min-w-[200px]`. When there is no room to the
|
|
151
|
+
// right of the parent item (viewport edge), anchor the submenu at the
|
|
152
|
+
// parent's LEFT edge with placement 'left-start' so the two menus
|
|
153
|
+
// never overlap.
|
|
154
|
+
const SUBMENU_WIDTH = 200;
|
|
155
|
+
let nextPlacement: 'bottom-start' | 'left-start' = 'bottom-start';
|
|
156
|
+
let anchorX = rect.right;
|
|
157
|
+
if (rect.right + SUBMENU_WIDTH > window.innerWidth) {
|
|
158
|
+
anchorX = rect.left;
|
|
159
|
+
nextPlacement = 'left-start';
|
|
160
|
+
}
|
|
161
|
+
submenuX = anchorX;
|
|
131
162
|
submenuY = rect.top;
|
|
163
|
+
submenuPlacement = nextPlacement;
|
|
132
164
|
submenuIndex = index;
|
|
133
165
|
}
|
|
134
166
|
|
|
@@ -335,7 +367,11 @@
|
|
|
335
367
|
$effect(() => {
|
|
336
368
|
if (open && menuEl && autoFocus) {
|
|
337
369
|
requestAnimationFrame(() => {
|
|
370
|
+
// .focus() dispatches the focus event synchronously, so the
|
|
371
|
+
// guard brackets this call exactly.
|
|
372
|
+
suppressFocusOpen = true;
|
|
338
373
|
menuItemButtons()[0]?.focus();
|
|
374
|
+
suppressFocusOpen = false;
|
|
339
375
|
});
|
|
340
376
|
}
|
|
341
377
|
});
|
|
@@ -354,7 +390,7 @@
|
|
|
354
390
|
data-context-menu={menuId}
|
|
355
391
|
class="min-w-[200px] bg-base-200 border border-base-300 rounded-md py-1 {className}"
|
|
356
392
|
style="z-index: {zIndex};"
|
|
357
|
-
use:floating={{ anchor, placement:
|
|
393
|
+
use:floating={{ anchor, placement: placementProp, flip: true, shift: true, offset: 2, onDismiss: close }}
|
|
358
394
|
onkeydown={handleKeydown}
|
|
359
395
|
>
|
|
360
396
|
{#if header}
|
|
@@ -388,6 +424,10 @@
|
|
|
388
424
|
onclick={(e) => handleItemClick(item, i, e)}
|
|
389
425
|
onpointerenter={(e) => handleItemPointerEnter(item, i, e)}
|
|
390
426
|
onfocus={(e) => {
|
|
427
|
+
// A programmatic autofocus (the initial focus when the menu
|
|
428
|
+
// opens) must NOT open the submenu — only user gestures
|
|
429
|
+
// (hover, click, Enter/Space, ArrowRight) do.
|
|
430
|
+
if (suppressFocusOpen) return;
|
|
391
431
|
if (item.submenu?.length && !item.disabled) {
|
|
392
432
|
openSubmenuAt(e.currentTarget as HTMLElement, i);
|
|
393
433
|
} else {
|
|
@@ -427,6 +467,7 @@
|
|
|
427
467
|
open={true}
|
|
428
468
|
x={submenuX}
|
|
429
469
|
y={submenuY}
|
|
470
|
+
placement={submenuPlacement}
|
|
430
471
|
items={items[submenuIndex].submenu ?? []}
|
|
431
472
|
zIndex={zIndex + 1}
|
|
432
473
|
autoFocus={false}
|
|
@@ -21,9 +21,19 @@ interface Props {
|
|
|
21
21
|
* menus; the component sets this to `false` when it recursively renders
|
|
22
22
|
* itself as a nested submenu so that arrow-navigating over a parent item
|
|
23
23
|
* with a submenu doesn't yank keyboard focus out of the parent row.
|
|
24
|
-
*
|
|
24
|
+
* The initial programmatic autofocus does NOT open a submenu that
|
|
25
|
+
* happens to live on the first item — a submenu opens on hover, click,
|
|
26
|
+
* Enter/Space or ArrowRight only. ArrowRight from the parent explicitly
|
|
27
|
+
* focuses into the submenu.
|
|
25
28
|
*/
|
|
26
29
|
autoFocus?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Floating-ui placement for this menu panel. Top-level menus anchor at
|
|
32
|
+
* the cursor and open 'bottom-start'; a nested submenu passes
|
|
33
|
+
* 'left-start' when it must flip to the parent's LEFT edge because
|
|
34
|
+
* there is no room to the right of it.
|
|
35
|
+
*/
|
|
36
|
+
placement?: 'bottom-start' | 'left-start';
|
|
27
37
|
/**
|
|
28
38
|
* Called when an action item (not a submenu owner) is chosen, before
|
|
29
39
|
* this menu closes. A nested submenu uses it to close the WHOLE menu
|