@ship-ui/core 0.17.7 → 0.17.8

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.
@@ -513,6 +513,12 @@
513
513
  "parameters": "parent",
514
514
  "returnType": "any",
515
515
  "description": ""
516
+ },
517
+ {
518
+ "name": "return",
519
+ "parameters": "pos.left >= 0 &&\n pos.top >= 0 &&\n pos.left + m.width <= window.innerWidth &&\n pos.top + m.height <= window.innerHeight\n );\n }\n\n /** Clamp a position so the popover stays within the viewport */\n #clampToViewport(pos: { left: number; top: number }, m: DOMRect",
520
+ "returnType": "any",
521
+ "description": ""
516
522
  }
517
523
  ],
518
524
  "cssVariables": [
@@ -2709,61 +2709,106 @@ class ShipPopover {
2709
2709
  }
2710
2710
  return this.#document.documentElement;
2711
2711
  }
2712
- #alignLeftUnder(triggerRect, menuRect) {
2713
- const newLeft = triggerRect.left;
2714
- const newTop = triggerRect.bottom + BASE_SPACE;
2712
+ /**
2713
+ * Position generators that mirror the CSS position-try-fallbacks.
2714
+ * Each returns { left, top } for the popover-content in fixed coordinates.
2715
+ */
2716
+ // bottom span-right: below trigger, left edge aligned with trigger left
2717
+ #bottomSpanRight(t, _m) {
2718
+ return { left: t.left, top: t.bottom + BASE_SPACE };
2719
+ }
2720
+ // top span-right: above trigger, left edge aligned with trigger left
2721
+ #topSpanRight(t, m) {
2722
+ return { left: t.left, top: t.top - m.height - BASE_SPACE };
2723
+ }
2724
+ // bottom span-left: below trigger, right edge aligned with trigger right
2725
+ #bottomSpanLeft(t, m) {
2726
+ return { left: t.right - m.width, top: t.bottom + BASE_SPACE };
2727
+ }
2728
+ // top span-left: above trigger, right edge aligned with trigger right
2729
+ #topSpanLeft(t, m) {
2730
+ return { left: t.right - m.width, top: t.top - m.height - BASE_SPACE };
2731
+ }
2732
+ // right span-bottom: to the right of trigger, top edge aligned with trigger top
2733
+ #rightSpanBottom(t, _m) {
2734
+ return { left: t.right + BASE_SPACE, top: t.top };
2735
+ }
2736
+ // left span-bottom: to the left of trigger, top edge aligned with trigger top
2737
+ #leftSpanBottom(t, m) {
2738
+ return { left: t.left - m.width - BASE_SPACE, top: t.top };
2739
+ }
2740
+ // right center: to the right of trigger, vertically centered
2741
+ #rightCenter(t, m) {
2742
+ return { left: t.right + BASE_SPACE, top: t.top + t.height / 2 - m.height / 2 };
2743
+ }
2744
+ // left center: to the left of trigger, vertically centered
2745
+ #leftCenter(t, m) {
2746
+ return { left: t.left - m.width - BASE_SPACE, top: t.top + t.height / 2 - m.height / 2 };
2747
+ }
2748
+ // right span-top: to the right of trigger, bottom edge aligned with trigger bottom
2749
+ #rightSpanTop(t, m) {
2750
+ return { left: t.right + BASE_SPACE, top: t.bottom - m.height };
2751
+ }
2752
+ // left span-top: to the left of trigger, bottom edge aligned with trigger bottom
2753
+ #leftSpanTop(t, m) {
2754
+ return { left: t.left - m.width - BASE_SPACE, top: t.bottom - m.height };
2755
+ }
2756
+ /** Check if a position fits entirely within the viewport */
2757
+ #fitsInViewport(pos, m) {
2758
+ return (pos.left >= 0 &&
2759
+ pos.top >= 0 &&
2760
+ pos.left + m.width <= window.innerWidth &&
2761
+ pos.top + m.height <= window.innerHeight);
2762
+ }
2763
+ /** Clamp a position so the popover stays within the viewport */
2764
+ #clampToViewport(pos, m) {
2715
2765
  return {
2716
- left: newLeft,
2717
- top: newTop,
2718
- };
2719
- }
2720
- #alignTopRight(triggerRect, menuRect) {
2721
- const newLeft = triggerRect.right + BASE_SPACE;
2722
- const newTop = triggerRect.top;
2723
- return {
2724
- left: newLeft,
2725
- top: newTop,
2726
- };
2727
- }
2728
- #alignBottomRight(triggerRect, menuRect) {
2729
- const newLeft = triggerRect.right + BASE_SPACE;
2730
- const newTop = triggerRect.bottom;
2731
- return {
2732
- left: newLeft,
2733
- top: newTop,
2734
- };
2735
- }
2736
- #alignLeftOver(triggerRect, menuRect) {
2737
- const newLeft = triggerRect.left;
2738
- const newTop = triggerRect.bottom - triggerRect.height - menuRect.height - BASE_SPACE;
2739
- return {
2740
- left: newLeft,
2741
- top: newTop,
2766
+ left: Math.max(0, Math.min(pos.left, window.innerWidth - m.width)),
2767
+ top: Math.max(0, Math.min(pos.top, window.innerHeight - m.height)),
2742
2768
  };
2743
2769
  }
2744
2770
  #calculateMenuPosition() {
2745
2771
  const triggerRect = this.triggerRef()?.nativeElement.getBoundingClientRect();
2746
2772
  const menuRect = this.popoverContentRef()?.nativeElement.getBoundingClientRect();
2747
- const tryOrderMultiLayer = [this.#alignTopRight, this.#alignBottomRight];
2748
- const tryOrderDefault = [this.#alignLeftUnder, this.#alignLeftOver];
2773
+ if (!triggerRect || !menuRect)
2774
+ return;
2775
+ // Mirror the CSS position-try-fallbacks order
2776
+ const tryOrderDefault = [
2777
+ this.#bottomSpanRight,
2778
+ this.#topSpanRight,
2779
+ this.#bottomSpanLeft,
2780
+ this.#topSpanLeft,
2781
+ this.#rightSpanBottom,
2782
+ this.#leftSpanBottom,
2783
+ this.#rightCenter,
2784
+ this.#leftCenter,
2785
+ this.#rightSpanTop,
2786
+ this.#leftSpanTop,
2787
+ ];
2788
+ const tryOrderMultiLayer = [
2789
+ this.#rightSpanBottom,
2790
+ this.#rightSpanTop,
2791
+ this.#leftSpanBottom,
2792
+ this.#leftSpanTop,
2793
+ this.#rightCenter,
2794
+ this.#leftCenter,
2795
+ this.#bottomSpanRight,
2796
+ this.#topSpanRight,
2797
+ this.#bottomSpanLeft,
2798
+ this.#topSpanLeft,
2799
+ ];
2749
2800
  const tryOrder = this.asMultiLayer() ? tryOrderMultiLayer : tryOrderDefault;
2750
- for (let i = 0; i < tryOrder.length; i++) {
2751
- const position = tryOrder[i](triggerRect, menuRect);
2752
- const outOfBoundsRight = position.left + (menuRect?.width || 0) > window.innerWidth;
2753
- const outOfBoundsBottom = position.top + (menuRect?.height || 0) > window.innerHeight;
2754
- if (!outOfBoundsRight && !outOfBoundsBottom) {
2755
- this.menuStyle.set({
2756
- left: position.left + 'px',
2757
- top: position.top + 'px',
2758
- });
2801
+ // Try each position, use the first one that fits
2802
+ for (const positionFn of tryOrder) {
2803
+ const pos = positionFn.call(this, triggerRect, menuRect);
2804
+ if (this.#fitsInViewport(pos, menuRect)) {
2805
+ this.menuStyle.set({ left: pos.left + 'px', top: pos.top + 'px' });
2759
2806
  return;
2760
2807
  }
2761
2808
  }
2762
- const fallbackPosition = tryOrder[0](triggerRect, menuRect);
2763
- this.menuStyle.set({
2764
- left: fallbackPosition.left + 'px',
2765
- top: fallbackPosition.top + 'px',
2766
- });
2809
+ // If nothing fits perfectly, use the first position clamped to viewport
2810
+ const fallback = this.#clampToViewport(tryOrder[0].call(this, triggerRect, menuRect), menuRect);
2811
+ this.menuStyle.set({ left: fallback.left + 'px', top: fallback.top + 'px' });
2767
2812
  }
2768
2813
  ngOnDestroy() {
2769
2814
  this.openAbort?.abort();