@reshaped/utilities 3.9.1-canary.2

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.
Files changed (58) hide show
  1. package/LICENSE.md +21 -0
  2. package/dist/dom/getShadowRoot.d.ts +2 -0
  3. package/dist/dom/getShadowRoot.js +5 -0
  4. package/dist/flyout/Flyout.d.ts +13 -0
  5. package/dist/flyout/Flyout.js +101 -0
  6. package/dist/flyout/constants.d.ts +9 -0
  7. package/dist/flyout/constants.js +9 -0
  8. package/dist/flyout/index.d.ts +1 -0
  9. package/dist/flyout/index.js +1 -0
  10. package/dist/flyout/tests/Flyout.test.d.ts +1 -0
  11. package/dist/flyout/tests/Flyout.test.js +129 -0
  12. package/dist/flyout/types.d.ts +24 -0
  13. package/dist/flyout/types.js +1 -0
  14. package/dist/flyout/utilities/applyPosition.d.ts +7 -0
  15. package/dist/flyout/utilities/applyPosition.js +103 -0
  16. package/dist/flyout/utilities/calculatePosition.d.ts +33 -0
  17. package/dist/flyout/utilities/calculatePosition.js +159 -0
  18. package/dist/flyout/utilities/centerBySize.d.ts +5 -0
  19. package/dist/flyout/utilities/centerBySize.js +7 -0
  20. package/dist/flyout/utilities/findClosestFixedContainer.d.ts +5 -0
  21. package/dist/flyout/utilities/findClosestFixedContainer.js +18 -0
  22. package/dist/flyout/utilities/findClosestScrollableContainer.d.ts +5 -0
  23. package/dist/flyout/utilities/findClosestScrollableContainer.js +12 -0
  24. package/dist/flyout/utilities/getPositionFallbacks.d.ts +8 -0
  25. package/dist/flyout/utilities/getPositionFallbacks.js +43 -0
  26. package/dist/flyout/utilities/getRTLPosition.d.ts +3 -0
  27. package/dist/flyout/utilities/getRTLPosition.js +8 -0
  28. package/dist/flyout/utilities/getRectFromCoordinates.d.ts +6 -0
  29. package/dist/flyout/utilities/getRectFromCoordinates.js +18 -0
  30. package/dist/flyout/utilities/isFullyVisible.d.ts +13 -0
  31. package/dist/flyout/utilities/isFullyVisible.js +28 -0
  32. package/dist/flyout/utilities/tests/applyPosition.test.d.ts +1 -0
  33. package/dist/flyout/utilities/tests/applyPosition.test.js +143 -0
  34. package/dist/flyout/utilities/tests/calculatePosition.test.d.ts +1 -0
  35. package/dist/flyout/utilities/tests/calculatePosition.test.js +536 -0
  36. package/dist/flyout/utilities/tests/centerBySize.test.d.ts +1 -0
  37. package/dist/flyout/utilities/tests/centerBySize.test.js +10 -0
  38. package/dist/flyout/utilities/tests/findClosestFixedContainer.test.d.ts +1 -0
  39. package/dist/flyout/utilities/tests/findClosestFixedContainer.test.js +46 -0
  40. package/dist/flyout/utilities/tests/findClosestScrollableContainer.test.d.ts +1 -0
  41. package/dist/flyout/utilities/tests/findClosestScrollableContainer.test.js +66 -0
  42. package/dist/flyout/utilities/tests/getPositionFallbacks.test.d.ts +1 -0
  43. package/dist/flyout/utilities/tests/getPositionFallbacks.test.js +114 -0
  44. package/dist/flyout/utilities/tests/getRTLPosition.test.d.ts +1 -0
  45. package/dist/flyout/utilities/tests/getRTLPosition.test.js +19 -0
  46. package/dist/flyout/utilities/tests/isFullyVisible.test.d.ts +1 -0
  47. package/dist/flyout/utilities/tests/isFullyVisible.test.js +129 -0
  48. package/dist/helpers/rafThrottle.d.ts +2 -0
  49. package/dist/helpers/rafThrottle.js +15 -0
  50. package/dist/helpers/tests/rafThrottle.test.d.ts +1 -0
  51. package/dist/helpers/tests/rafThrottle.test.js +49 -0
  52. package/dist/i18n/isRTL.d.ts +2 -0
  53. package/dist/i18n/isRTL.js +10 -0
  54. package/dist/i18n/tests/isRTL.test.d.ts +1 -0
  55. package/dist/i18n/tests/isRTL.test.js +51 -0
  56. package/dist/index.d.ts +1 -0
  57. package/dist/index.js +1 -0
  58. package/package.json +42 -0
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Reshaped
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ declare const getShadowRoot: (el: HTMLElement | null) => ShadowRoot | null;
2
+ export default getShadowRoot;
@@ -0,0 +1,5 @@
1
+ const getShadowRoot = (el) => {
2
+ const rootNode = el?.getRootNode();
3
+ return rootNode instanceof ShadowRoot ? rootNode : null;
4
+ };
5
+ export default getShadowRoot;
@@ -0,0 +1,13 @@
1
+ import applyPosition from "./utilities/applyPosition";
2
+ import type { Options } from "./types";
3
+ declare class Flyout {
4
+ #private;
5
+ constructor(options: Options);
6
+ /**
7
+ * Public methods
8
+ */
9
+ update: (options?: Partial<Options>) => ReturnType<typeof applyPosition>;
10
+ open: () => ReturnType<typeof this.update>;
11
+ close: () => void;
12
+ }
13
+ export default Flyout;
@@ -0,0 +1,101 @@
1
+ import rafThrottle from "../helpers/rafThrottle.js";
2
+ import applyPosition from "./utilities/applyPosition.js";
3
+ import findClosestScrollableContainer from "./utilities/findClosestScrollableContainer.js";
4
+ class Flyout {
5
+ #active = false;
6
+ #lastUsedPosition = null;
7
+ #options;
8
+ #handlerCleanupMap = {};
9
+ constructor(options) {
10
+ this.#options = options;
11
+ }
12
+ #update = (options) => {
13
+ const result = applyPosition({
14
+ ...this.#options,
15
+ fallbackPositions: options?.fallback === false ? [] : this.#options.fallbackPositions,
16
+ lastUsedPosition: this.#lastUsedPosition,
17
+ });
18
+ this.#lastUsedPosition = result.position;
19
+ return result;
20
+ };
21
+ #addParentScrollHandler = () => {
22
+ const { trigger, onClose } = this.#options;
23
+ if (!trigger)
24
+ return;
25
+ const container = findClosestScrollableContainer({ el: trigger });
26
+ if (!container)
27
+ return;
28
+ const handleScroll = rafThrottle(() => {
29
+ if (!this.#active)
30
+ return;
31
+ const triggerBounds = trigger.getBoundingClientRect();
32
+ const containerBounds = container.getBoundingClientRect();
33
+ if (triggerBounds.top < containerBounds.top ||
34
+ triggerBounds.left < containerBounds.left ||
35
+ triggerBounds.right > containerBounds.right ||
36
+ triggerBounds.bottom > containerBounds.bottom) {
37
+ onClose();
38
+ }
39
+ else {
40
+ this.#update({ fallback: false });
41
+ }
42
+ });
43
+ container.addEventListener("scroll", handleScroll, { passive: true });
44
+ this.#handlerCleanupMap.scroll = () => container.removeEventListener("scroll", handleScroll);
45
+ };
46
+ #addRTLHandler = () => {
47
+ const observer = new MutationObserver(() => {
48
+ if (!this.#active)
49
+ return;
50
+ this.#update();
51
+ });
52
+ observer.observe(document.documentElement, {
53
+ attributes: true,
54
+ attributeFilter: ["dir"],
55
+ });
56
+ this.#handlerCleanupMap.rtl = () => observer.disconnect();
57
+ };
58
+ #addResizeHandler = () => {
59
+ const handleResize = () => {
60
+ if (!this.#active)
61
+ return;
62
+ this.#update();
63
+ };
64
+ const observer = new ResizeObserver(handleResize);
65
+ window.addEventListener("resize", handleResize);
66
+ if (this.#options.trigger)
67
+ observer.observe(this.#options.trigger);
68
+ if (this.#options.content)
69
+ observer.observe(this.#options.content);
70
+ this.#handlerCleanupMap.resize = () => {
71
+ observer.disconnect();
72
+ window.removeEventListener("resize", handleResize);
73
+ };
74
+ };
75
+ #removeHandlers = () => {
76
+ Object.values(this.#handlerCleanupMap).forEach((cleanup) => cleanup());
77
+ this.#handlerCleanupMap = {};
78
+ };
79
+ /**
80
+ * Public methods
81
+ */
82
+ update = (options) => {
83
+ if (options)
84
+ this.#options = { ...this.#options, ...options };
85
+ return this.#update();
86
+ };
87
+ open = () => {
88
+ const result = this.#update();
89
+ this.#addParentScrollHandler();
90
+ this.#addRTLHandler();
91
+ this.#addResizeHandler();
92
+ this.#active = true;
93
+ return result;
94
+ };
95
+ close = () => {
96
+ this.#lastUsedPosition = null;
97
+ this.#active = false;
98
+ this.#removeHandlers();
99
+ };
100
+ }
101
+ export default Flyout;
@@ -0,0 +1,9 @@
1
+ export declare const CONTAINER_OFFSET = 8;
2
+ export declare const RESET_STYLES: {
3
+ readonly left: "0";
4
+ readonly top: "0";
5
+ readonly position: "absolute";
6
+ readonly visibility: "hidden";
7
+ readonly animation: "none";
8
+ readonly transition: "none";
9
+ };
@@ -0,0 +1,9 @@
1
+ export const CONTAINER_OFFSET = 8;
2
+ export const RESET_STYLES = {
3
+ left: "0",
4
+ top: "0",
5
+ position: "absolute",
6
+ visibility: "hidden",
7
+ animation: "none",
8
+ transition: "none",
9
+ };
@@ -0,0 +1 @@
1
+ export { default } from "./Flyout";
@@ -0,0 +1 @@
1
+ export { default } from "./Flyout.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,129 @@
1
+ import { expect, test, describe, beforeEach, afterEach, vi } from "vitest";
2
+ import Flyout from "../Flyout.js";
3
+ describe("flyout/Flyout", () => {
4
+ let content;
5
+ let trigger;
6
+ let onClose;
7
+ beforeEach(() => {
8
+ content = document.createElement("div");
9
+ content.style.position = "absolute";
10
+ content.style.width = "100px";
11
+ content.style.height = "50px";
12
+ document.body.appendChild(content);
13
+ trigger = document.createElement("button");
14
+ trigger.style.position = "absolute";
15
+ trigger.style.left = "100px";
16
+ trigger.style.top = "200px";
17
+ trigger.style.width = "50px";
18
+ trigger.style.height = "30px";
19
+ document.body.appendChild(trigger);
20
+ onClose = vi.fn();
21
+ });
22
+ afterEach(() => {
23
+ if (content.parentNode)
24
+ content.parentNode.removeChild(content);
25
+ if (trigger.parentNode)
26
+ trigger.parentNode.removeChild(trigger);
27
+ });
28
+ test("updates position", () => {
29
+ const flyout = new Flyout({
30
+ content,
31
+ trigger,
32
+ triggerCoordinates: null,
33
+ position: "top",
34
+ onClose,
35
+ });
36
+ const result = flyout.open();
37
+ expect(result.position).toBe("top");
38
+ trigger.style.top = "0px";
39
+ const resultUpdate = flyout.update();
40
+ expect(resultUpdate.position).toBe("bottom");
41
+ });
42
+ test("closes flyout when trigger scrolls out of container bounds", () => {
43
+ const container = document.createElement("div");
44
+ container.style.position = "relative";
45
+ container.style.width = "200px";
46
+ container.style.height = "200px";
47
+ container.style.overflow = "auto";
48
+ document.body.appendChild(container);
49
+ trigger.style.position = "relative";
50
+ container.appendChild(trigger);
51
+ const flyout = new Flyout({
52
+ content,
53
+ trigger,
54
+ triggerCoordinates: null,
55
+ position: "top",
56
+ onClose,
57
+ });
58
+ flyout.open();
59
+ // Scroll trigger out of bounds
60
+ trigger.style.position = "absolute";
61
+ trigger.style.top = "-100px";
62
+ container.scrollTop = 0;
63
+ // Trigger scroll event
64
+ container.dispatchEvent(new Event("scroll", { bubbles: true }));
65
+ // Wait for rafThrottle
66
+ return new Promise((resolve) => {
67
+ requestAnimationFrame(() => {
68
+ requestAnimationFrame(() => {
69
+ expect(onClose).toHaveBeenCalled();
70
+ document.body.removeChild(container);
71
+ resolve(undefined);
72
+ });
73
+ });
74
+ });
75
+ });
76
+ test("updates position when trigger scrolls but stays in bounds", () => {
77
+ const container = document.createElement("div");
78
+ container.style.position = "relative";
79
+ container.style.width = "500px";
80
+ container.style.height = "500px";
81
+ container.style.overflow = "auto";
82
+ document.body.appendChild(container);
83
+ trigger.style.position = "relative";
84
+ trigger.style.top = "100px";
85
+ container.appendChild(trigger);
86
+ const flyout = new Flyout({
87
+ content,
88
+ trigger,
89
+ triggerCoordinates: null,
90
+ position: "top",
91
+ onClose,
92
+ });
93
+ flyout.open();
94
+ // Scroll within bounds
95
+ container.scrollTop = 50;
96
+ container.dispatchEvent(new Event("scroll", { bubbles: true }));
97
+ // Should update, not close
98
+ return new Promise((resolve) => {
99
+ requestAnimationFrame(() => {
100
+ requestAnimationFrame(() => {
101
+ expect(onClose).not.toHaveBeenCalled();
102
+ document.body.removeChild(container);
103
+ resolve(undefined);
104
+ });
105
+ });
106
+ });
107
+ });
108
+ test("updates position on RTL direction change", async () => {
109
+ const flyout = new Flyout({
110
+ content,
111
+ trigger,
112
+ triggerCoordinates: null,
113
+ position: "start",
114
+ onClose,
115
+ });
116
+ const initialResult = flyout.open();
117
+ expect(initialResult.position).toBe("start");
118
+ // Start is positioned from the right side
119
+ expect(content.style.right).toBe("0px");
120
+ expect(content.style.left).toBe("");
121
+ document.documentElement.setAttribute("dir", "rtl");
122
+ await vi.waitFor(() => {
123
+ // End is position from the left side
124
+ expect(content.style.left).toBe("0px");
125
+ expect(content.style.right).toBe("");
126
+ });
127
+ document.documentElement.setAttribute("dir", "ltr");
128
+ });
129
+ });
@@ -0,0 +1,24 @@
1
+ type XSide = "start" | "end";
2
+ type YSide = "top" | "bottom";
3
+ export type Side = XSide | YSide;
4
+ export type Position = `${YSide}` | `${YSide}-${XSide}` | `${XSide}` | `${XSide}-${YSide}`;
5
+ export type Coordinates = {
6
+ x: number;
7
+ y: number;
8
+ };
9
+ export type Width = "trigger" | string;
10
+ export type Options = {
11
+ content: HTMLElement;
12
+ trigger?: HTMLElement | null;
13
+ container?: HTMLElement | null;
14
+ triggerCoordinates: Coordinates | null;
15
+ position: Position;
16
+ fallbackPositions?: Position[];
17
+ width?: Width;
18
+ fallbackAdjustLayout?: boolean;
19
+ fallbackMinHeight?: string;
20
+ contentGap?: number;
21
+ contentShift?: number;
22
+ onClose: () => void;
23
+ };
24
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { Position, Options } from "../types";
2
+ declare const applyPosition: (args: Options & {
3
+ lastUsedPosition: Position | null;
4
+ }) => {
5
+ position: Position;
6
+ };
7
+ export default applyPosition;
@@ -0,0 +1,103 @@
1
+ import getShadowRoot from "../../dom/getShadowRoot.js";
2
+ import isRTL from "../../i18n/isRTL.js";
3
+ import { CONTAINER_OFFSET, RESET_STYLES } from "../constants.js";
4
+ import calculatePosition from "./calculatePosition.js";
5
+ import findClosestFixedContainer from "./findClosestFixedContainer.js";
6
+ import getPositionFallbacks from "./getPositionFallbacks.js";
7
+ import getRectFromCoordinates from "./getRectFromCoordinates.js";
8
+ import isFullyVisible from "./isFullyVisible.js";
9
+ const applyPosition = (args) => {
10
+ const { trigger, content, triggerCoordinates, container: passedContainer, contentShift = 0, contentGap = 0, position, fallbackPositions, fallbackAdjustLayout, fallbackMinHeight, width, lastUsedPosition, } = args;
11
+ const rtl = isRTL();
12
+ const contentClone = content.cloneNode(true);
13
+ const triggerBounds = triggerCoordinates || trigger?.getBoundingClientRect();
14
+ contentClone.style.cssText = "";
15
+ if (!triggerBounds)
16
+ throw new Error("Trigger bounds are required");
17
+ const resolvedTriggerBounds = getRectFromCoordinates(triggerBounds);
18
+ Object.keys(RESET_STYLES).forEach((_key) => {
19
+ const key = _key;
20
+ const value = RESET_STYLES[key];
21
+ if (value)
22
+ contentClone.style[key] = value.toString();
23
+ });
24
+ // Insert inside shadow root if possible to make sure styles are applied correctly
25
+ const root = (trigger && getShadowRoot(trigger)) ?? document.body;
26
+ 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
+ const testPosition = (position, options) => {
34
+ if (options?.width === "100%") {
35
+ contentClone.style.width = `calc(100% - ${CONTAINER_OFFSET * 2}px)`;
36
+ }
37
+ else if (options?.width === "trigger") {
38
+ contentClone.style.width = `${resolvedTriggerBounds.width}px`;
39
+ }
40
+ else if (width) {
41
+ contentClone.style.width = width;
42
+ }
43
+ else {
44
+ contentClone.style.width = "";
45
+ }
46
+ return calculatePosition({
47
+ triggerBounds: resolvedTriggerBounds,
48
+ flyoutBounds: contentClone.getBoundingClientRect(),
49
+ containerBounds: renderContainerBounds,
50
+ position,
51
+ contentGap,
52
+ contentShift,
53
+ rtl,
54
+ width,
55
+ passedContainer: passedContainer ||
56
+ (closestFixedContainer !== document.body ? closestFixedContainer : undefined),
57
+ fallbackAdjustLayout,
58
+ fallbackMinHeight,
59
+ });
60
+ };
61
+ 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
+ };
68
+ return isFullyVisible({
69
+ flyoutBounds: calculated.boundaries,
70
+ visualContainerBounds,
71
+ renderContainerBounds,
72
+ });
73
+ };
74
+ let calculated = null;
75
+ const testOrder = getPositionFallbacks(position, fallbackPositions);
76
+ testOrder.some((currentPosition) => {
77
+ const tested = testPosition(currentPosition);
78
+ const visible = testVisibility(tested);
79
+ if (visible)
80
+ calculated = tested;
81
+ return visible;
82
+ });
83
+ // Try full width positions in case it doesn't fit on any side
84
+ if (!calculated) {
85
+ const smallScreenFallbackPositions = ["top", "bottom"].filter((position) => testOrder.includes(position));
86
+ smallScreenFallbackPositions.some((position) => {
87
+ const tested = testPosition(position, { width: "100%" });
88
+ const visible = testVisibility(tested);
89
+ if (visible)
90
+ calculated = tested;
91
+ return visible;
92
+ });
93
+ }
94
+ // None of the positions fit, use the last used position or the default position
95
+ if (!calculated)
96
+ calculated = testPosition(lastUsedPosition ?? position);
97
+ root.removeChild(contentClone);
98
+ Object.entries(calculated.styles).forEach(([key, value]) => {
99
+ content.style.setProperty(key, value);
100
+ });
101
+ return { position: calculated.position };
102
+ };
103
+ export default applyPosition;
@@ -0,0 +1,33 @@
1
+ import type { Width, Position } from "../types";
2
+ type Args = {
3
+ triggerBounds: DOMRect;
4
+ flyoutBounds: DOMRect;
5
+ containerBounds: DOMRect;
6
+ passedContainer?: HTMLElement | null;
7
+ position: Position;
8
+ rtl: boolean;
9
+ width?: Width;
10
+ contentGap: number;
11
+ contentShift: number;
12
+ fallbackAdjustLayout?: boolean;
13
+ fallbackMinHeight?: string;
14
+ };
15
+ declare const calculatePosition: (args: Args) => {
16
+ position: Position;
17
+ 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
+ top: number;
29
+ height: number;
30
+ width: number;
31
+ };
32
+ };
33
+ export default calculatePosition;
@@ -0,0 +1,159 @@
1
+ import { CONTAINER_OFFSET } from "../constants.js";
2
+ import centerBySize from "./centerBySize.js";
3
+ import getRTLPosition from "./getRTLPosition.js";
4
+ const calculatePosition = (args) => {
5
+ const { triggerBounds, flyoutBounds, containerBounds, position: passedPosition, rtl, width: passedWidth, contentGap = 0, contentShift = 0, passedContainer, fallbackAdjustLayout, fallbackMinHeight, } = args;
6
+ const position = rtl ? getRTLPosition(passedPosition) : passedPosition;
7
+ const isHorizontalPosition = !!position.match(/^(start|end)/);
8
+ let left = 0;
9
+ let top = 0;
10
+ let bottom = null;
11
+ 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);
34
+ switch (position) {
35
+ case "start":
36
+ case "start-top":
37
+ case "start-bottom":
38
+ left = relativeLeft - flyoutWidth - contentGap;
39
+ right = relativeRight + triggerWidth + contentGap;
40
+ break;
41
+ case "end":
42
+ case "end-top":
43
+ case "end-bottom":
44
+ left = relativeLeft + triggerWidth + contentGap;
45
+ break;
46
+ case "bottom":
47
+ case "top":
48
+ left = relativeLeft + centerBySize(triggerWidth, flyoutWidth) + contentShift;
49
+ break;
50
+ case "top-start":
51
+ case "bottom-start":
52
+ left = relativeLeft + contentShift;
53
+ break;
54
+ case "top-end":
55
+ case "bottom-end":
56
+ left = relativeLeft + triggerWidth - flyoutWidth + contentShift;
57
+ right = relativeRight - contentShift;
58
+ break;
59
+ default:
60
+ break;
61
+ }
62
+ switch (position) {
63
+ case "top":
64
+ case "top-start":
65
+ case "top-end":
66
+ top = relativeTop - flyoutHeight - contentGap;
67
+ bottom = relativeBottom + triggerHeight + contentGap;
68
+ break;
69
+ case "bottom":
70
+ case "bottom-start":
71
+ case "bottom-end":
72
+ top = relativeTop + triggerHeight + contentGap;
73
+ break;
74
+ case "start":
75
+ case "end":
76
+ top = relativeTop + centerBySize(triggerHeight, flyoutHeight) + contentShift;
77
+ break;
78
+ case "start-top":
79
+ case "end-top":
80
+ top = relativeTop + contentShift;
81
+ break;
82
+ case "start-bottom":
83
+ case "end-bottom":
84
+ top = relativeTop + triggerHeight - flyoutHeight + contentShift;
85
+ bottom = relativeBottom - contentShift;
86
+ break;
87
+ default:
88
+ break;
89
+ }
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
+ return {
141
+ position,
142
+ 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
+ top,
154
+ height: height ?? Math.ceil(flyoutHeight),
155
+ width: width ?? Math.ceil(flyoutWidth),
156
+ },
157
+ };
158
+ };
159
+ export default calculatePosition;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Get a position value which centers 2 elements vertically or horizontally
3
+ */
4
+ declare const centerBySize: (originSize: number, targetSize: number) => number;
5
+ export default centerBySize;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Get a position value which centers 2 elements vertically or horizontally
3
+ */
4
+ const centerBySize = (originSize, targetSize) => {
5
+ return Math.floor(originSize / 2 - targetSize / 2);
6
+ };
7
+ export default centerBySize;
@@ -0,0 +1,5 @@
1
+ type Args = {
2
+ el: HTMLElement | null;
3
+ };
4
+ declare const _default: (args: Args) => HTMLElement;
5
+ export default _default;