@joewinke/jatui 0.8.4 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -66,6 +66,12 @@
|
|
|
66
66
|
* ArrowRight from the parent explicitly focuses into the submenu.
|
|
67
67
|
*/
|
|
68
68
|
autoFocus?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Called when an action item (not a submenu owner) is chosen, before
|
|
71
|
+
* this menu closes. A nested submenu uses it to close the WHOLE menu
|
|
72
|
+
* chain on a pick; Escape/ArrowLeft in a submenu still close only it.
|
|
73
|
+
*/
|
|
74
|
+
onItemSelected?: () => void;
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
let {
|
|
@@ -77,7 +83,8 @@
|
|
|
77
83
|
zIndex = 70,
|
|
78
84
|
class: className = '',
|
|
79
85
|
onClose,
|
|
80
|
-
autoFocus = true
|
|
86
|
+
autoFocus = true,
|
|
87
|
+
onItemSelected
|
|
81
88
|
}: Props = $props();
|
|
82
89
|
|
|
83
90
|
// Submenu state — index into `items` of the currently-open submenu (if any),
|
|
@@ -124,6 +131,7 @@
|
|
|
124
131
|
submitAction(item.submit);
|
|
125
132
|
}
|
|
126
133
|
item.onSelect?.();
|
|
134
|
+
onItemSelected?.();
|
|
127
135
|
close();
|
|
128
136
|
}
|
|
129
137
|
|
|
@@ -397,6 +405,10 @@
|
|
|
397
405
|
zIndex={zIndex + 1}
|
|
398
406
|
autoFocus={false}
|
|
399
407
|
onClose={closeSubmenu}
|
|
408
|
+
onItemSelected={() => {
|
|
409
|
+
onItemSelected?.();
|
|
410
|
+
close();
|
|
411
|
+
}}
|
|
400
412
|
/>
|
|
401
413
|
{/if}
|
|
402
414
|
</div>
|
|
@@ -24,6 +24,12 @@ interface Props {
|
|
|
24
24
|
* ArrowRight from the parent explicitly focuses into the submenu.
|
|
25
25
|
*/
|
|
26
26
|
autoFocus?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Called when an action item (not a submenu owner) is chosen, before
|
|
29
|
+
* this menu closes. A nested submenu uses it to close the WHOLE menu
|
|
30
|
+
* chain on a pick; Escape/ArrowLeft in a submenu still close only it.
|
|
31
|
+
*/
|
|
32
|
+
onItemSelected?: () => void;
|
|
27
33
|
}
|
|
28
34
|
declare const ContextMenu: import("svelte").Component<Props, {}, "open">;
|
|
29
35
|
type ContextMenu = ReturnType<typeof ContextMenu>;
|
|
@@ -197,6 +197,35 @@
|
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
function positionPanel() {
|
|
201
|
+
if (!triggerRef) return;
|
|
202
|
+
const rect = triggerRef.getBoundingClientRect();
|
|
203
|
+
const width = Math.max(rect.width, 192);
|
|
204
|
+
// Viewport-aware flip: if there isn't room below the trigger but there
|
|
205
|
+
// is above, anchor the panel to the trigger's top and grow upward. This
|
|
206
|
+
// keeps the panel on-screen for rows near the bottom of a scroll area
|
|
207
|
+
// (e.g. suggested-task rows in the completed-session overlay).
|
|
208
|
+
const estPanelHeight = 300;
|
|
209
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
210
|
+
const spaceAbove = rect.top;
|
|
211
|
+
const flipUp = spaceBelow < estPanelHeight && spaceAbove > spaceBelow;
|
|
212
|
+
// Clamp left so a right-edge trigger doesn't push the panel off-screen.
|
|
213
|
+
const left = Math.max(8, Math.min(rect.left, window.innerWidth - width - 8));
|
|
214
|
+
// .sd-panel's class rule always sets `top: 100%`, so each branch must
|
|
215
|
+
// explicitly reset the property it isn't using — otherwise the flip-up
|
|
216
|
+
// branch ends up with both top (100% = viewport height) and bottom set
|
|
217
|
+
// while height is auto, which over-constrains to a negative (clamped
|
|
218
|
+
// to 0) height and the whole panel silently renders invisible.
|
|
219
|
+
const vertical = flipUp
|
|
220
|
+
? `bottom: ${window.innerHeight - rect.top + 4}px; top: auto;`
|
|
221
|
+
: `top: ${rect.bottom + 4}px; bottom: auto;`;
|
|
222
|
+
// min-width override is required: .sd-panel's class rule sets
|
|
223
|
+
// `min-width: max(100%, 14rem)`, and for a position:fixed element
|
|
224
|
+
// that 100% resolves against the viewport, not the trigger — without
|
|
225
|
+
// this override the panel balloons to ~100vw wide.
|
|
226
|
+
panelStyle = `position: fixed; z-index: 9999; ${vertical} left: ${left}px; width: ${width}px; min-width: ${width}px; margin-top: 0;`;
|
|
227
|
+
}
|
|
228
|
+
|
|
200
229
|
$effect(() => {
|
|
201
230
|
if (embedded) {
|
|
202
231
|
tick().then(() => { searchInput?.focus(); scrollHighlightedIntoView(); });
|
|
@@ -204,31 +233,26 @@
|
|
|
204
233
|
}
|
|
205
234
|
if (open) {
|
|
206
235
|
if (appendToBody && triggerRef) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// min-width override is required: .sd-panel's class rule sets
|
|
228
|
-
// `min-width: max(100%, 14rem)`, and for a position:fixed element
|
|
229
|
-
// that 100% resolves against the viewport, not the trigger — without
|
|
230
|
-
// this override the panel balloons to ~100vw wide.
|
|
231
|
-
panelStyle = `position: fixed; z-index: 9999; ${vertical} left: ${left}px; width: ${width}px; min-width: ${width}px; margin-top: 0;`;
|
|
236
|
+
positionPanel();
|
|
237
|
+
// A position:fixed panel is anchored to the VIEWPORT, so without this it
|
|
238
|
+
// stays put while the page scrolls and appears to follow the reader down
|
|
239
|
+
// the page (Joe UAT 2026-09-25). Re-anchor on every scroll (capture: any
|
|
240
|
+
// scroll container) and resize; close once the trigger leaves the view.
|
|
241
|
+
const reanchor = (e?: Event) => {
|
|
242
|
+
if (e && e.target instanceof Node && containerRef?.contains(e.target)) return;
|
|
243
|
+
const r = triggerRef?.getBoundingClientRect();
|
|
244
|
+
if (!r || r.bottom < 0 || r.top > window.innerHeight) { open = false; searchQuery = ''; return; }
|
|
245
|
+
positionPanel();
|
|
246
|
+
};
|
|
247
|
+
window.addEventListener('scroll', reanchor, { capture: true, passive: true });
|
|
248
|
+
window.addEventListener('resize', reanchor, { passive: true });
|
|
249
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
250
|
+
tick().then(() => { searchInput?.focus(); scrollHighlightedIntoView(); });
|
|
251
|
+
return () => {
|
|
252
|
+
window.removeEventListener('scroll', reanchor, { capture: true });
|
|
253
|
+
window.removeEventListener('resize', reanchor);
|
|
254
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
255
|
+
};
|
|
232
256
|
}
|
|
233
257
|
document.addEventListener('mousedown', handleClickOutside);
|
|
234
258
|
tick().then(() => { searchInput?.focus(); scrollHighlightedIntoView(); });
|