@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.
@@ -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 = 'translateY(0)';
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
- * ArrowRight from the parent explicitly focuses into the submenu.
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
- submenuX = rect.right;
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: 'bottom-start', flip: true, shift: true, offset: 2, onDismiss: close }}
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
- * ArrowRight from the parent explicitly focuses into the submenu.
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.36.2",
3
+ "version": "0.36.3",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",