@r2digisolutions/ui 0.24.3 → 0.24.5

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.
@@ -1,6 +1,36 @@
1
1
  <script lang="ts">
2
2
  import type { TContextMenuEntry } from '../core/types.js';
3
3
 
4
+ // --- Action portal ---
5
+ function portal(node: HTMLElement, target: HTMLElement | null = null) {
6
+ const tgt = target ?? document.body;
7
+ const placeholder = document.createComment('portal-placeholder');
8
+ if (!node.parentNode) return { destroy: () => {} };
9
+ node.parentNode.insertBefore(placeholder, node);
10
+ tgt.appendChild(node);
11
+ return {
12
+ destroy() {
13
+ if (node.isConnected) node.remove();
14
+ if (placeholder.isConnected && placeholder.parentNode) {
15
+ placeholder.parentNode.insertBefore(node, placeholder);
16
+ placeholder.remove();
17
+ }
18
+ }
19
+ };
20
+ }
21
+
22
+ // --- Util para clamping a viewport ---
23
+ function clampToViewport(x: number, y: number, menuW: number, menuH: number, padding = 8) {
24
+ const vw = document.documentElement.clientWidth;
25
+ const vh = document.documentElement.clientHeight;
26
+ let nx = x;
27
+ let ny = y;
28
+ if (nx + menuW + padding > vw) nx = Math.max(padding, vw - menuW - padding);
29
+ if (ny + menuH + padding > vh) ny = Math.max(padding, vh - menuH - padding);
30
+ return { x: nx, y: ny };
31
+ }
32
+
33
+ // --- Props ---
4
34
  type Props = {
5
35
  items?: TContextMenuEntry[];
6
36
  x?: number;
@@ -9,6 +39,7 @@
9
39
  title?: string;
10
40
  searchable?: boolean;
11
41
  context?: any;
42
+ portalTarget?: HTMLElement | null;
12
43
  };
13
44
  let {
14
45
  items = [],
@@ -17,9 +48,11 @@
17
48
  open = $bindable(false),
18
49
  title = '',
19
50
  searchable = true,
20
- context = null
51
+ context = null,
52
+ portalTarget = null
21
53
  }: Props = $props();
22
54
 
55
+ // --- Estado interno ---
23
56
  let stack = $state<{ label: string; items: TContextMenuEntry[] }[]>([]);
24
57
  let q = $state('');
25
58
  const current = $derived(stack.length ? stack[stack.length - 1] : { label: title, items });
@@ -27,7 +60,6 @@
27
60
  let menuEl: HTMLDivElement | null = $state(null);
28
61
 
29
62
  function close() {
30
- if (menuEl) menuEl.hidePopover();
31
63
  open = false;
32
64
  stack = [];
33
65
  q = '';
@@ -96,54 +128,89 @@
96
128
  }
97
129
  }
98
130
 
99
- // Manage popover open/close
131
+ // Teclado (Escape / Backspace)
100
132
  $effect(() => {
101
- if (!menuEl) return;
102
- if (open) {
103
- menuEl.style.setProperty('--popover-x', `${x}px`);
104
- menuEl.style.setProperty('--popover-y', `${y}px`);
105
- menuEl.showPopover();
106
- } else {
107
- menuEl.hidePopover();
108
- }
133
+ if (!open) return;
134
+ const handler = (e: KeyboardEvent) => onKey(e);
135
+ document.addEventListener('keydown', handler);
136
+ return () => document.removeEventListener('keydown', handler);
137
+ });
138
+
139
+ // Cierres robustos: click-fuera, scroll/resize, y bloqueo de menú nativo
140
+ $effect(() => {
141
+ if (!open || !menuEl) return;
142
+
143
+ const onPointerDown = (ev: PointerEvent) => {
144
+ if (!menuEl || menuEl.contains(ev.target as Node)) return;
145
+ close();
146
+ };
147
+
148
+ const onNativeCtx = (ev: MouseEvent) => {
149
+ if (!menuEl || menuEl.contains(ev.target as Node)) return;
150
+ ev.preventDefault();
151
+ };
152
+
153
+ let timeout: NodeJS.Timeout;
154
+ const onScrollOrResize = () => {
155
+ clearTimeout(timeout);
156
+ timeout = setTimeout(() => close(), 100);
157
+ };
158
+
159
+ document.addEventListener('pointerdown', onPointerDown);
160
+ document.addEventListener('contextmenu', onNativeCtx);
161
+ window.addEventListener('scroll', onScrollOrResize, true);
162
+ window.addEventListener('resize', onScrollOrResize, true);
163
+
164
+ return () => {
165
+ clearTimeout(timeout);
166
+ document.removeEventListener('pointerdown', onPointerDown);
167
+ document.removeEventListener('contextmenu', onNativeCtx);
168
+ window.removeEventListener('scroll', onScrollOrResize, true);
169
+ window.removeEventListener('resize', onScrollOrResize, true);
170
+ };
109
171
  });
110
172
 
111
- // Clamp position to viewport
173
+ // Clamp de posición tras abrir o cambiar coords
112
174
  $effect(() => {
113
175
  if (!open || !menuEl) return;
114
176
  requestAnimationFrame(() => {
115
177
  if (!menuEl) return;
116
178
  const rect = menuEl.getBoundingClientRect();
117
- const vw = document.documentElement.clientWidth;
118
- const vh = document.documentElement.clientHeight;
119
- let nx = x;
120
- let ny = y;
121
- const padding = 8;
122
- if (nx + rect.width + padding > vw) nx = Math.max(padding, vw - rect.width - padding);
123
- if (ny + rect.height + padding > vh) ny = Math.max(padding, vh - rect.height - padding);
179
+ const { x: nx, y: ny } = clampToViewport(x, y, rect.width, rect.height, 8);
124
180
  if (nx !== x || ny !== y) {
125
181
  x = nx;
126
182
  y = ny;
127
- menuEl.style.setProperty('--popover-x', `${nx}px`);
128
- menuEl.style.setProperty('--popover-y', `${ny}px`);
129
183
  }
130
184
  });
131
185
  });
132
186
  </script>
133
187
 
134
188
  {#if open}
189
+ <!-- BACKDROP -->
190
+ <div
191
+ use:portal={portalTarget}
192
+ role="dialog"
193
+ class="fixed inset-0 z-[2147483646]"
194
+ onclick={() => close()}
195
+ oncontextmenu={(e) => e.preventDefault()}
196
+ aria-modal="true"
197
+ tabindex="0"
198
+ style="pointer-events:auto"
199
+ />
200
+
201
+ <!-- MENÚ -->
135
202
  <div
136
203
  bind:this={menuEl}
137
- popover="manual"
138
- class="w-72 rounded-2xl bg-white p-2 shadow-xl ring-1 ring-black/5 dark:bg-gray-900"
139
- style="position: fixed; left: var(--popover-x); top: var(--popover-y);"
204
+ use:portal={portalTarget}
205
+ class="fixed z-[2147483647] w-72 rounded-2xl bg-white p-2 shadow-xl ring-1 ring-black/5 dark:bg-gray-900"
206
+ style={`left:${x}px; top:${y}px`}
140
207
  oncontextmenu={(e) => e.preventDefault()}
141
- onkeydown={onKey}
142
208
  >
143
209
  <div class="flex items-center gap-1 px-1 py-1">
144
210
  {#if stack.length > 0}
145
211
  <button
146
212
  class="rounded-lg p-1 hover:bg-gray-100 dark:hover:bg-gray-800"
213
+ role="dialog"
147
214
  aria-label="Atrás"
148
215
  onclick={back}
149
216
  >
@@ -155,10 +222,8 @@
155
222
  stroke="currentColor"
156
223
  stroke-width="2"
157
224
  stroke-linecap="round"
158
- stroke-linejoin="round"
225
+ stroke-linejoin="round"><polyline points="15 18 9 12 15 6" /></svg
159
226
  >
160
- <polyline points="15 18 9 12 15 6" />
161
- </svg>
162
227
  </button>
163
228
  {/if}
164
229
  <div class="min-w-0 flex-1 truncate px-1 text-xs font-medium opacity-70">
@@ -184,15 +249,16 @@
184
249
  {:else}
185
250
  <button
186
251
  class="flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm hover:bg-gray-100 disabled:opacity-50 dark:hover:bg-gray-800"
252
+ role="dialog"
187
253
  disabled={it.disabled}
188
254
  onclick={() => clickItem(it)}
189
255
  >
190
256
  <span class="truncate">{it.label}</span>
191
257
  <span class="flex items-center gap-2">
192
258
  {#if it.shortcut}
193
- <kbd class="rounded bg-gray-100 px-1 text-[10px] dark:bg-gray-800">
194
- {it.shortcut}
195
- </kbd>
259
+ <kbd class="rounded bg-gray-100 px-1 text-[10px] dark:bg-gray-800"
260
+ >{it.shortcut}</kbd
261
+ >
196
262
  {/if}
197
263
  {#if hasChildren(it)}
198
264
  <svg
@@ -204,10 +270,8 @@
204
270
  stroke="currentColor"
205
271
  stroke-width="2"
206
272
  stroke-linecap="round"
207
- stroke-linejoin="round"
273
+ stroke-linejoin="round"><polyline points="9 18 15 12 9 6" /></svg
208
274
  >
209
- <polyline points="9 18 15 12 9 6" />
210
- </svg>
211
275
  {/if}
212
276
  </span>
213
277
  </button>
@@ -216,12 +280,3 @@
216
280
  </div>
217
281
  </div>
218
282
  {/if}
219
-
220
- <style>
221
- [popover] {
222
- margin: 0;
223
- border: none;
224
- padding: 0;
225
- z-index: 2147483647;
226
- }
227
- </style>
@@ -7,6 +7,7 @@ type Props = {
7
7
  title?: string;
8
8
  searchable?: boolean;
9
9
  context?: any;
10
+ portalTarget?: HTMLElement | null;
10
11
  };
11
12
  declare const ContextMenu: import("svelte").Component<Props, {}, "open">;
12
13
  type ContextMenu = ReturnType<typeof ContextMenu>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@r2digisolutions/ui",
3
- "version": "0.24.3",
3
+ "version": "0.24.5",
4
4
  "private": false,
5
5
  "packageManager": "bun@1.2.23",
6
6
  "publishConfig": {