@reshaped/utilities 3.9.1-canary.2 → 3.9.1-canary.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.
@@ -9,10 +9,9 @@ class Flyout {
9
9
  constructor(options) {
10
10
  this.#options = options;
11
11
  }
12
- #update = (options) => {
12
+ #update = () => {
13
13
  const result = applyPosition({
14
14
  ...this.#options,
15
- fallbackPositions: options?.fallback === false ? [] : this.#options.fallbackPositions,
16
15
  lastUsedPosition: this.#lastUsedPosition,
17
16
  });
18
17
  this.#lastUsedPosition = result.position;
@@ -23,11 +22,14 @@ class Flyout {
23
22
  if (!trigger)
24
23
  return;
25
24
  const container = findClosestScrollableContainer({ el: trigger });
26
- if (!container)
27
- return;
25
+ const root = container || window;
28
26
  const handleScroll = rafThrottle(() => {
29
27
  if (!this.#active)
30
28
  return;
29
+ if (!container) {
30
+ this.#update();
31
+ return;
32
+ }
31
33
  const triggerBounds = trigger.getBoundingClientRect();
32
34
  const containerBounds = container.getBoundingClientRect();
33
35
  if (triggerBounds.top < containerBounds.top ||
@@ -37,11 +39,11 @@ class Flyout {
37
39
  onClose();
38
40
  }
39
41
  else {
40
- this.#update({ fallback: false });
42
+ this.#update();
41
43
  }
42
44
  });
43
- container.addEventListener("scroll", handleScroll, { passive: true });
44
- this.#handlerCleanupMap.scroll = () => container.removeEventListener("scroll", handleScroll);
45
+ root.addEventListener("scroll", handleScroll, { passive: true });
46
+ this.#handlerCleanupMap.scroll = () => root.removeEventListener("scroll", handleScroll);
45
47
  };
46
48
  #addRTLHandler = () => {
47
49
  const observer = new MutationObserver(() => {
@@ -1,4 +1,4 @@
1
- export declare const CONTAINER_OFFSET = 8;
1
+ export declare const VIEWPORT_OFFSET = 8;
2
2
  export declare const RESET_STYLES: {
3
3
  readonly left: "0";
4
4
  readonly top: "0";
@@ -1,4 +1,4 @@
1
- export const CONTAINER_OFFSET = 8;
1
+ export const VIEWPORT_OFFSET = 8;
2
2
  export const RESET_STYLES = {
3
3
  left: "0",
4
4
  top: "0",
@@ -1,16 +1,22 @@
1
1
  import getShadowRoot from "../../dom/getShadowRoot.js";
2
2
  import isRTL from "../../i18n/isRTL.js";
3
- import { CONTAINER_OFFSET, RESET_STYLES } from "../constants.js";
3
+ import { VIEWPORT_OFFSET, RESET_STYLES } from "../constants.js";
4
+ import calculateLayoutAdjustment from "./calculateLayoutAdjustment.js";
4
5
  import calculatePosition from "./calculatePosition.js";
5
- import findClosestFixedContainer from "./findClosestFixedContainer.js";
6
6
  import getPositionFallbacks from "./getPositionFallbacks.js";
7
7
  import getRectFromCoordinates from "./getRectFromCoordinates.js";
8
8
  import isFullyVisible from "./isFullyVisible.js";
9
9
  const applyPosition = (args) => {
10
- const { trigger, content, triggerCoordinates, container: passedContainer, contentShift = 0, contentGap = 0, position, fallbackPositions, fallbackAdjustLayout, fallbackMinHeight, width, lastUsedPosition, } = args;
10
+ const { trigger, content, triggerCoordinates, container, contentShift = 0, contentGap = 0, position, fallbackPositions, fallbackAdjustLayout, fallbackMinHeight, width, lastUsedPosition, } = args;
11
11
  const rtl = isRTL();
12
12
  const contentClone = content.cloneNode(true);
13
13
  const triggerBounds = triggerCoordinates || trigger?.getBoundingClientRect();
14
+ const containerBounds = container?.getBoundingClientRect() ?? {
15
+ width: window.innerWidth,
16
+ height: window.innerHeight,
17
+ left: 0,
18
+ top: 0,
19
+ };
14
20
  contentClone.style.cssText = "";
15
21
  if (!triggerBounds)
16
22
  throw new Error("Trigger bounds are required");
@@ -24,15 +30,9 @@ const applyPosition = (args) => {
24
30
  // Insert inside shadow root if possible to make sure styles are applied correctly
25
31
  const root = (trigger && getShadowRoot(trigger)) ?? document.body;
26
32
  root.appendChild(contentClone);
27
- const closestFixedContainer = !passedContainer && trigger ? findClosestFixedContainer({ el: trigger }) : undefined;
28
- const container = passedContainer ||
29
- // Render inside fixed position container automatically to keep their position synced on scroll
30
- closestFixedContainer ||
31
- document.body;
32
- const renderContainerBounds = container.getBoundingClientRect();
33
33
  const testPosition = (position, options) => {
34
34
  if (options?.width === "100%") {
35
- contentClone.style.width = `calc(100% - ${CONTAINER_OFFSET * 2}px)`;
35
+ contentClone.style.width = `calc(100% - ${VIEWPORT_OFFSET * 2}px)`;
36
36
  }
37
37
  else if (options?.width === "trigger") {
38
38
  contentClone.style.width = `${resolvedTriggerBounds.width}px`;
@@ -43,32 +43,37 @@ const applyPosition = (args) => {
43
43
  else {
44
44
  contentClone.style.width = "";
45
45
  }
46
- return calculatePosition({
46
+ const flyoutBounds = contentClone.getBoundingClientRect();
47
+ const result = calculatePosition({
47
48
  triggerBounds: resolvedTriggerBounds,
48
- flyoutBounds: contentClone.getBoundingClientRect(),
49
- containerBounds: renderContainerBounds,
49
+ flyoutBounds,
50
50
  position,
51
51
  contentGap,
52
52
  contentShift,
53
53
  rtl,
54
- width,
55
- passedContainer: passedContainer ||
56
- (closestFixedContainer !== document.body ? closestFixedContainer : undefined),
54
+ });
55
+ const adjustedResult = calculateLayoutAdjustment({
56
+ ...result,
57
57
  fallbackAdjustLayout,
58
58
  fallbackMinHeight,
59
+ width,
60
+ flyoutBounds,
61
+ containerBounds,
62
+ triggerBounds: resolvedTriggerBounds,
59
63
  });
64
+ return adjustedResult;
60
65
  };
61
66
  const testVisibility = (calculated) => {
62
- const visualContainerBounds = passedContainer?.getBoundingClientRect() ?? {
63
- width: window.innerWidth,
64
- height: window.innerHeight,
65
- left: window.scrollX,
66
- top: window.scrollY,
67
+ const flyoutBounds = {
68
+ // Flyout is rendered in body with position absolute, so bounds need to include the page scroll
69
+ left: calculated.styles.left,
70
+ top: calculated.styles.top,
71
+ height: calculated.styles.height ?? Math.ceil(contentClone.clientHeight),
72
+ width: calculated.styles.width ?? Math.ceil(contentClone.clientWidth),
67
73
  };
68
74
  return isFullyVisible({
69
- flyoutBounds: calculated.boundaries,
70
- visualContainerBounds,
71
- renderContainerBounds,
75
+ flyoutBounds,
76
+ containerBounds,
72
77
  });
73
78
  };
74
79
  let calculated = null;
@@ -95,7 +100,22 @@ const applyPosition = (args) => {
95
100
  if (!calculated)
96
101
  calculated = testPosition(lastUsedPosition ?? position);
97
102
  root.removeChild(contentClone);
98
- Object.entries(calculated.styles).forEach(([key, value]) => {
103
+ const { styles } = calculated;
104
+ const translateX = (styles.right !== null ? -styles.right : styles.left) + window.scrollX;
105
+ const translateY = (styles.bottom !== null ? -styles.bottom : styles.top) + window.scrollY;
106
+ const resolvedStyles = {
107
+ left: styles.right === null ? "0px" : undefined,
108
+ right: styles.right === null ? undefined : "0px",
109
+ top: styles.bottom === null ? "0px" : undefined,
110
+ bottom: styles.bottom === null ? undefined : "0px",
111
+ height: styles.height !== null ? `${styles.height}px` : undefined,
112
+ width: styles.width !== null ? `${styles.width}px` : (width ?? undefined),
113
+ transform: `translate(${translateX}px, ${translateY}px)`,
114
+ };
115
+ content.style.cssText = "";
116
+ Object.entries(resolvedStyles).forEach(([key, value]) => {
117
+ if (!value)
118
+ return;
99
119
  content.style.setProperty(key, value);
100
120
  });
101
121
  return { position: calculated.position };
@@ -0,0 +1,19 @@
1
+ import type { CalculatePositionResult } from "./calculatePosition";
2
+ import type { Options } from "../types";
3
+ type CalculateLayoutAdjustmentArgs = CalculatePositionResult & Pick<Options, "fallbackAdjustLayout" | "fallbackMinHeight" | "width"> & {
4
+ flyoutBounds: DOMRect;
5
+ triggerBounds: DOMRect;
6
+ containerBounds: Pick<DOMRect, "left" | "top" | "width" | "height">;
7
+ };
8
+ declare const calculateLayoutAdjustment: (args: CalculateLayoutAdjustmentArgs) => {
9
+ position: import("../types").Position;
10
+ styles: {
11
+ left: number;
12
+ right: number | null;
13
+ top: number;
14
+ bottom: number | null;
15
+ height: number | null;
16
+ width: number | null;
17
+ };
18
+ };
19
+ export default calculateLayoutAdjustment;
@@ -0,0 +1,73 @@
1
+ import { VIEWPORT_OFFSET } from "../constants.js";
2
+ const calculateLayoutAdjustment = (args) => {
3
+ const { position, styles, fallbackAdjustLayout, fallbackMinHeight, width: passedWidth, flyoutBounds, containerBounds, triggerBounds, } = args;
4
+ const { width: flyoutWidth, height: flyoutHeight } = flyoutBounds;
5
+ const { width: containerWidth, height: containerHeight, top: containerTop, left: containerLeft, } = containerBounds;
6
+ const isHorizontalPosition = !!position.match(/^(start|end)/);
7
+ let top = styles.top;
8
+ let left = styles.left;
9
+ let bottom = styles.bottom;
10
+ let right = styles.right;
11
+ let height = null;
12
+ let width = null;
13
+ if (fallbackAdjustLayout) {
14
+ const getOverflow = () => {
15
+ return {
16
+ top: containerTop + VIEWPORT_OFFSET - top,
17
+ bottom: top + flyoutHeight + VIEWPORT_OFFSET - containerTop - containerHeight,
18
+ left: containerLeft + VIEWPORT_OFFSET - left,
19
+ right: left + flyoutWidth + VIEWPORT_OFFSET - containerLeft - containerWidth,
20
+ };
21
+ };
22
+ const overflow = getOverflow();
23
+ if (isHorizontalPosition) {
24
+ if (overflow.top > 0) {
25
+ top = containerTop + VIEWPORT_OFFSET;
26
+ if (bottom !== null)
27
+ bottom = bottom - overflow.top;
28
+ }
29
+ else if (overflow.bottom > 0) {
30
+ top = top - overflow.bottom;
31
+ }
32
+ }
33
+ else {
34
+ if (overflow.left > 0) {
35
+ left = VIEWPORT_OFFSET + containerLeft;
36
+ if (right !== null)
37
+ right = right - overflow.left;
38
+ }
39
+ else if (overflow.right > 0) {
40
+ left = left - overflow.right;
41
+ }
42
+ }
43
+ const updatedOverflow = getOverflow();
44
+ if (updatedOverflow.top > 0) {
45
+ height = Math.max(parseInt(fallbackMinHeight ?? "0"), flyoutHeight - updatedOverflow.top);
46
+ top = top + (flyoutHeight - height);
47
+ }
48
+ else if (updatedOverflow.bottom > 0) {
49
+ height = Math.max(parseInt(fallbackMinHeight ?? "0"), flyoutHeight - updatedOverflow.bottom);
50
+ if (bottom !== null)
51
+ bottom = bottom + (flyoutHeight - height);
52
+ }
53
+ }
54
+ if (passedWidth === "100%") {
55
+ left = VIEWPORT_OFFSET;
56
+ width = window.innerWidth - VIEWPORT_OFFSET * 2;
57
+ }
58
+ else if (passedWidth === "trigger") {
59
+ width = triggerBounds.width;
60
+ }
61
+ return {
62
+ position,
63
+ styles: {
64
+ left,
65
+ right,
66
+ top,
67
+ bottom,
68
+ height,
69
+ width,
70
+ },
71
+ };
72
+ };
73
+ export default calculateLayoutAdjustment;
@@ -1,33 +1,20 @@
1
- import type { Width, Position } from "../types";
2
- type Args = {
1
+ import type { Position } from "../types";
2
+ type CalculatePositionArgs = {
3
3
  triggerBounds: DOMRect;
4
4
  flyoutBounds: DOMRect;
5
- containerBounds: DOMRect;
6
- passedContainer?: HTMLElement | null;
7
5
  position: Position;
8
6
  rtl: boolean;
9
- width?: Width;
10
7
  contentGap: number;
11
8
  contentShift: number;
12
- fallbackAdjustLayout?: boolean;
13
- fallbackMinHeight?: string;
14
9
  };
15
- declare const calculatePosition: (args: Args) => {
10
+ export type CalculatePositionResult = {
16
11
  position: Position;
17
12
  styles: {
18
- left: string | null;
19
- right: string | null;
20
- top: string | null;
21
- bottom: string | null;
22
- transform: string;
23
- height: string | null;
24
- width: string | null;
25
- };
26
- boundaries: {
27
- left: number;
28
13
  top: number;
29
- height: number;
30
- width: number;
14
+ left: number;
15
+ bottom: number | null;
16
+ right: number | null;
31
17
  };
32
18
  };
19
+ declare const calculatePosition: (args: CalculatePositionArgs) => CalculatePositionResult;
33
20
  export default calculatePosition;
@@ -1,36 +1,19 @@
1
- import { CONTAINER_OFFSET } from "../constants.js";
2
1
  import centerBySize from "./centerBySize.js";
3
2
  import getRTLPosition from "./getRTLPosition.js";
4
3
  const calculatePosition = (args) => {
5
- const { triggerBounds, flyoutBounds, containerBounds, position: passedPosition, rtl, width: passedWidth, contentGap = 0, contentShift = 0, passedContainer, fallbackAdjustLayout, fallbackMinHeight, } = args;
4
+ const { triggerBounds, flyoutBounds, position: passedPosition, rtl, contentGap = 0, contentShift = 0, } = args;
6
5
  const position = rtl ? getRTLPosition(passedPosition) : passedPosition;
7
- const isHorizontalPosition = !!position.match(/^(start|end)/);
8
6
  let left = 0;
9
7
  let top = 0;
10
8
  let bottom = null;
11
9
  let right = null;
12
- let height = null;
13
- let width = null;
14
- const flyoutWidth = flyoutBounds.width;
15
- const flyoutHeight = flyoutBounds.height;
16
- const triggerWidth = triggerBounds.width;
17
- const triggerHeight = triggerBounds.height;
18
- // Detect passed container scroll to sync the flyout position with it
19
- const containerX = passedContainer?.scrollLeft;
20
- const containerY = passedContainer?.scrollTop;
21
- const scrollX = containerX ?? window.scrollX;
22
- const scrollY = containerY ?? window.scrollY;
23
- const renderContainerHeight = passedContainer?.clientHeight ?? window.innerHeight;
24
- const renderContainerWidth = passedContainer?.clientWidth ?? window.innerWidth;
25
- // When rendering in the body, bottom bounds will be larrger than the viewport so we calculate it manually
26
- const containerBoundsBottom = passedContainer
27
- ? containerBounds.bottom
28
- : window.innerHeight - scrollY;
29
- // When inside a container, adjut position based on the container scroll since flyout is rendered outside the scroll area
30
- const relativeLeft = triggerBounds.left - containerBounds.left + (containerX || 0);
31
- const relativeRight = containerBounds.right - triggerBounds.right - (containerX || 0);
32
- const relativeTop = triggerBounds.top - containerBounds.top - (containerY || 0);
33
- const relativeBottom = containerBoundsBottom - triggerBounds.bottom - (containerY || 0);
10
+ const { width: flyoutWidth, height: flyoutHeight } = flyoutBounds;
11
+ const { width: triggerWidth, height: triggerHeight, left: triggerLeft, top: triggerTop, right: triggerRight, bottom: triggerBottom, } = triggerBounds;
12
+ // Convert rect values to css position values
13
+ const relativeLeft = triggerLeft;
14
+ const relativeRight = window.innerWidth - triggerRight;
15
+ const relativeTop = triggerTop;
16
+ const relativeBottom = window.innerHeight - triggerBottom;
34
17
  switch (position) {
35
18
  case "start":
36
19
  case "start-top":
@@ -87,72 +70,13 @@ const calculatePosition = (args) => {
87
70
  default:
88
71
  break;
89
72
  }
90
- if (fallbackAdjustLayout) {
91
- const getOverflow = () => {
92
- return {
93
- top: -top + scrollY + CONTAINER_OFFSET,
94
- bottom: top + flyoutHeight + CONTAINER_OFFSET - scrollY - renderContainerHeight,
95
- left: -left + scrollX + CONTAINER_OFFSET,
96
- right: left + flyoutWidth + CONTAINER_OFFSET - scrollX - renderContainerWidth,
97
- };
98
- };
99
- const overflow = getOverflow();
100
- if (isHorizontalPosition) {
101
- if (overflow.top > 0) {
102
- top = CONTAINER_OFFSET + scrollY;
103
- if (bottom !== null)
104
- bottom = bottom - overflow.top;
105
- }
106
- else if (overflow.bottom > 0) {
107
- top = top - overflow.bottom;
108
- }
109
- }
110
- else {
111
- if (overflow.left > 0) {
112
- left = CONTAINER_OFFSET + scrollX;
113
- if (right !== null)
114
- right = right - overflow.left;
115
- }
116
- else if (overflow.right > 0) {
117
- left = left - overflow.right;
118
- }
119
- }
120
- const updatedOverflow = getOverflow();
121
- if (updatedOverflow.top > 0) {
122
- height = Math.max(parseInt(fallbackMinHeight ?? "0"), flyoutHeight - updatedOverflow.top);
123
- top = top + (flyoutHeight - height);
124
- }
125
- else if (updatedOverflow.bottom > 0) {
126
- height = Math.max(parseInt(fallbackMinHeight ?? "0"), flyoutHeight - updatedOverflow.bottom);
127
- if (bottom !== null)
128
- bottom = bottom + (flyoutHeight - height);
129
- }
130
- }
131
- if (passedWidth === "100%") {
132
- left = CONTAINER_OFFSET;
133
- width = window.innerWidth - CONTAINER_OFFSET * 2;
134
- }
135
- else if (passedWidth === "trigger") {
136
- width = triggerBounds.width;
137
- }
138
- const translateX = right !== null ? -right : left;
139
- const translateY = bottom !== null ? -bottom : top;
140
73
  return {
141
74
  position,
142
75
  styles: {
143
- left: right === null ? "0px" : null,
144
- right: right === null ? null : "0px",
145
- top: bottom === null ? "0px" : null,
146
- bottom: bottom === null ? null : "0px",
147
- transform: `translate(${translateX}px, ${translateY}px)`,
148
- height: height !== null ? `${height}px` : null,
149
- width: width !== null ? `${width}px` : (passedWidth ?? null),
150
- },
151
- boundaries: {
152
- left,
153
76
  top,
154
- height: height ?? Math.ceil(flyoutHeight),
155
- width: width ?? Math.ceil(flyoutWidth),
77
+ left,
78
+ bottom,
79
+ right,
156
80
  },
157
81
  };
158
82
  };
@@ -2,12 +2,10 @@ type Bounds = Pick<DOMRect, "left" | "top" | "width" | "height">;
2
2
  /**
3
3
  * Check if element visually fits within its render container
4
4
  * @param flyoutBounds - Bounds of the flyout content
5
- * @param visualContainerBounds - Bounds of the container where the flyout content should visually fit
6
- * @param renderContainerBounds - Bounds of the container where flyout content is rendered
5
+ * @param containerBounds - Bounds of the container where the flyout content should visually fit
7
6
  */
8
7
  declare const isFullyVisible: (args: {
9
8
  flyoutBounds: Bounds;
10
- visualContainerBounds: Bounds;
11
- renderContainerBounds: Bounds;
9
+ containerBounds: Bounds;
12
10
  }) => boolean;
13
11
  export default isFullyVisible;
@@ -1,26 +1,23 @@
1
- import { CONTAINER_OFFSET } from "../constants.js";
1
+ import { VIEWPORT_OFFSET } from "../constants.js";
2
2
  /**
3
3
  * Check if element visually fits within its render container
4
4
  * @param flyoutBounds - Bounds of the flyout content
5
- * @param visualContainerBounds - Bounds of the container where the flyout content should visually fit
6
- * @param renderContainerBounds - Bounds of the container where flyout content is rendered
5
+ * @param containerBounds - Bounds of the container where the flyout content should visually fit
7
6
  */
8
7
  const isFullyVisible = (args) => {
9
- const { flyoutBounds, visualContainerBounds, renderContainerBounds } = args;
10
- const flyoutLeft = renderContainerBounds.left + flyoutBounds.left;
11
- const flyoutTop = renderContainerBounds.top + flyoutBounds.top;
12
- const containerLeft = visualContainerBounds.left;
13
- const containerTop = visualContainerBounds.top;
14
- if (flyoutLeft < containerLeft + CONTAINER_OFFSET)
8
+ const { flyoutBounds, containerBounds } = args;
9
+ const flyoutLeft = flyoutBounds.left;
10
+ const flyoutTop = flyoutBounds.top;
11
+ const containerLeft = containerBounds.left;
12
+ const containerTop = containerBounds.top;
13
+ if (flyoutLeft < containerLeft + VIEWPORT_OFFSET)
15
14
  return false;
16
- if (flyoutTop < containerTop + CONTAINER_OFFSET)
15
+ if (flyoutTop < containerTop + VIEWPORT_OFFSET)
17
16
  return false;
18
- if (flyoutLeft + flyoutBounds.width >
19
- containerLeft + visualContainerBounds.width - CONTAINER_OFFSET) {
17
+ if (flyoutLeft + flyoutBounds.width > containerLeft + containerBounds.width - VIEWPORT_OFFSET) {
20
18
  return false;
21
19
  }
22
- if (flyoutTop + flyoutBounds.height >
23
- containerTop + visualContainerBounds.height - CONTAINER_OFFSET) {
20
+ if (flyoutTop + flyoutBounds.height > containerTop + containerBounds.height - VIEWPORT_OFFSET) {
24
21
  return false;
25
22
  }
26
23
  return true;