@marianmeres/stuic 2.54.0 → 2.55.0

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.
@@ -51,6 +51,8 @@
51
51
  @supports (anchor-name: --anchor) {
52
52
  .stuic-popover {
53
53
  /* position-area is set via inline style based on position param */
54
+ /* prefer fallback position with most available width (helps on narrow mobile screens) */
55
+ position-try-order: most-width;
54
56
  /* fallbacks ensure popover stays within viewport */
55
57
  position-try-fallbacks:
56
58
  --pop-top-span-right, --pop-top-span-left, flip-block, --pop-bottom-span-right,
@@ -1,6 +1,7 @@
1
1
  import { mount, unmount } from "svelte";
2
2
  import { twMerge } from "../../utils/tw-merge.js";
3
3
  import { addAnchorName, removeAnchorName } from "../../utils/anchor-name.js";
4
+ import { BodyScroll } from "../../utils/body-scroll-locker.js";
4
5
  import PopoverContent from "./PopoverContent.svelte";
5
6
  //
6
7
  import "./index.css";
@@ -215,6 +216,7 @@ export function popover(anchorEl, fn) {
215
216
  let isVisible = false;
216
217
  let do_debug = false;
217
218
  let prevOpen = undefined;
219
+ let switchingToFallback = false; // flag to prevent recursion during fallback switch
218
220
  // Unique identifiers
219
221
  const rnd = Math.random().toString(36).slice(2);
220
222
  const id = `popover-${rnd}`;
@@ -382,6 +384,8 @@ export function popover(anchorEl, fn) {
382
384
  popoverEl.classList.add(...twMerge("stuic-popover-fallback", _classPopover, currentOptions.class).split(/\s/));
383
385
  wrapperEl.appendChild(popoverEl);
384
386
  document.body.appendChild(wrapperEl);
387
+ // Lock body scroll in fallback (modal) mode
388
+ BodyScroll.lock();
385
389
  // Click on wrapper (outside popover) closes
386
390
  if (currentOptions.closeOnClickOutside !== false) {
387
391
  wrapperEl.addEventListener("click", (e) => {
@@ -419,6 +423,35 @@ export function popover(anchorEl, fn) {
419
423
  requestAnimationFrame(() => {
420
424
  popoverEl?.classList.add("pop-visible");
421
425
  backdropEl?.classList.add("pop-visible");
426
+ // Check for viewport overflow and switch to fallback if needed
427
+ if (useAnchorPositioning && popoverEl && !switchingToFallback) {
428
+ // Use another RAF to let browser finalize positioning
429
+ requestAnimationFrame(() => {
430
+ if (!popoverEl)
431
+ return;
432
+ const rect = popoverEl.getBoundingClientRect();
433
+ const viewportWidth = window.innerWidth;
434
+ if (rect.left < 0 || rect.right > viewportWidth) {
435
+ debug("overflow detected, switching to fallback mode");
436
+ switchingToFallback = true;
437
+ // Quick cleanup (skip transition)
438
+ if (mountedComponent) {
439
+ unmount(mountedComponent);
440
+ mountedComponent = null;
441
+ }
442
+ popoverEl.remove();
443
+ popoverEl = null;
444
+ isVisible = false;
445
+ // Re-show in fallback mode
446
+ const originalForceFallback = currentOptions.forceFallback;
447
+ currentOptions.forceFallback = true;
448
+ show();
449
+ currentOptions.forceFallback = originalForceFallback;
450
+ switchingToFallback = false;
451
+ return;
452
+ }
453
+ });
454
+ }
422
455
  currentOptions.onShow?.();
423
456
  });
424
457
  // Add event listeners
@@ -454,6 +487,10 @@ export function popover(anchorEl, fn) {
454
487
  // Remove event listeners
455
488
  document.removeEventListener("keydown", onEscape);
456
489
  document.removeEventListener("click", onClickOutside);
490
+ // Unlock body scroll if we were in fallback mode (wrapperEl only exists in fallback)
491
+ if (wrapperEl) {
492
+ BodyScroll.unlock();
493
+ }
457
494
  // Transition out
458
495
  popoverEl?.classList.remove("pop-visible");
459
496
  backdropEl?.classList.remove("pop-visible");
@@ -577,6 +614,10 @@ export function popover(anchorEl, fn) {
577
614
  unmount(mountedComponent);
578
615
  mountedComponent = null;
579
616
  }
617
+ // Unlock body scroll if we were in fallback mode
618
+ if (wrapperEl) {
619
+ BodyScroll.unlock();
620
+ }
580
621
  popoverEl?.remove();
581
622
  backdropEl?.remove();
582
623
  wrapperEl?.remove();
@@ -231,7 +231,7 @@
231
231
  flex items-center gap-2
232
232
  px-3 py-1.5
233
233
  min-h-[44px]
234
- text-left text-sm
234
+ text-left
235
235
  rounded-md
236
236
  cursor-pointer
237
237
  touch-action-manipulation
@@ -145,7 +145,7 @@ export interface Props extends Omit<HTMLButtonAttributes, "children"> {
145
145
  export declare const DROPDOWN_MENU_BASE_CLASSES = "stuic-dropdown-menu relative inline-block";
146
146
  export declare const DROPDOWN_MENU_TRIGGER_CLASSES = "\n\t\tinline-flex items-center justify-center gap-2\n\t\tpx-3 py-2\n\t\trounded-md border\n\t\tbg-white dark:bg-neutral-800\n\t\ttext-neutral-900 dark:text-neutral-100\n\t\tborder-neutral-200 dark:border-neutral-700\n\t\thover:brightness-95 dark:hover:brightness-110\n\t\tfocus-visible:outline-2 focus-visible:outline-offset-2\n\t\tcursor-pointer\n\t";
147
147
  export declare const DROPDOWN_MENU_DROPDOWN_CLASSES = "\n\t\tstuic-dropdown-menu-dropdown\n\t\tbg-white dark:bg-neutral-800\n\t\ttext-neutral-900 dark:text-neutral-100\n\t\tborder border-neutral-200 dark:border-neutral-700\n\t\trounded-md shadow-sm\n\t\tp-1\n\t\toverflow-y-auto\n\t\tz-50\n\t\tmin-w-48\n\t";
148
- export declare const DROPDOWN_MENU_ITEM_CLASSES = "\n\t\tw-full\n\t\tflex items-center gap-2\n\t\tpx-3 py-1.5\n\t\tmin-h-[44px]\n\t\ttext-left text-sm\n\t\trounded-md\n\t\tcursor-pointer\n\t\ttouch-action-manipulation\n\t\thover:bg-neutral-100 dark:hover:bg-neutral-700\n\t\tfocus:outline-none\n\t\tfocus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-600\n\t";
148
+ export declare const DROPDOWN_MENU_ITEM_CLASSES = "\n\t\tw-full\n\t\tflex items-center gap-2\n\t\tpx-3 py-1.5\n\t\tmin-h-[44px]\n\t\ttext-left \n\t\trounded-md\n\t\tcursor-pointer\n\t\ttouch-action-manipulation\n\t\thover:bg-neutral-100 dark:hover:bg-neutral-700\n\t\tfocus:outline-none\n\t\tfocus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-600\n\t";
149
149
  export declare const DROPDOWN_MENU_DIVIDER_CLASSES = "\n\t\th-px my-1\n\t\tbg-neutral-200 dark:bg-neutral-700\n\t";
150
150
  export declare const DROPDOWN_MENU_HEADER_CLASSES = "\n\t\tpx-2 py-1.5\n\t\ttext-xs font-semibold uppercase tracking-wide\n\t\ttext-neutral-500 dark:text-neutral-400\n\t\tselect-none\n\t";
151
151
  export declare const DROPDOWN_MENU_BACKDROP_CLASSES = "\n\t\tstuic-dropdown-menu-backdrop\n\t\tfixed inset-0 bg-black/25\n\t\tz-40\n\t";
@@ -20,8 +20,7 @@
20
20
  * ```
21
21
  */
22
22
  export declare class BodyScroll {
23
+ #private;
23
24
  static lock(): void;
24
25
  static unlock(): void;
25
- private static _get_body_style;
26
- private static _restore_body_styles;
27
26
  }
@@ -31,7 +31,7 @@ export class BodyScroll {
31
31
  if (data.originalScrollY === undefined) {
32
32
  const scrollY = window.scrollY || window.pageYOffset;
33
33
  // Save body styles as serialized json
34
- data.originalScrollStyleBackup = BodyScroll._get_body_style();
34
+ data.originalScrollStyleBackup = BodyScroll.#get_body_style();
35
35
  // Save the original scroll position in dataset
36
36
  data.originalScrollY = `${scrollY}`;
37
37
  data.scrollLockCount = "1";
@@ -64,7 +64,7 @@ export class BodyScroll {
64
64
  else {
65
65
  // This is the last component, restore everything
66
66
  const originalScrollY = parseInt(data.originalScrollY, 10);
67
- BodyScroll._restore_body_styles(data.originalScrollStyleBackup);
67
+ BodyScroll.#restore_body_styles(data.originalScrollStyleBackup);
68
68
  // Remove our data attributes
69
69
  delete data.originalScrollY;
70
70
  delete data.originalScrollStyleBackup;
@@ -74,7 +74,7 @@ export class BodyScroll {
74
74
  }
75
75
  }
76
76
  }
77
- static _get_body_style() {
77
+ static #get_body_style() {
78
78
  // we want only explicitly defined, not computed
79
79
  const style = document.body.style;
80
80
  return JSON.stringify({
@@ -86,7 +86,7 @@ export class BodyScroll {
86
86
  right: style.left || null,
87
87
  });
88
88
  }
89
- static _restore_body_styles(originalJsonString) {
89
+ static #restore_body_styles(originalJsonString) {
90
90
  let original = {
91
91
  position: null,
92
92
  top: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "2.54.0",
3
+ "version": "2.55.0",
4
4
  "files": [
5
5
  "dist",
6
6
  "!dist/**/*.test.*",