@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.
- package/LICENSE.md +21 -0
- package/dist/dom/getShadowRoot.d.ts +2 -0
- package/dist/dom/getShadowRoot.js +5 -0
- package/dist/flyout/Flyout.d.ts +13 -0
- package/dist/flyout/Flyout.js +101 -0
- package/dist/flyout/constants.d.ts +9 -0
- package/dist/flyout/constants.js +9 -0
- package/dist/flyout/index.d.ts +1 -0
- package/dist/flyout/index.js +1 -0
- package/dist/flyout/tests/Flyout.test.d.ts +1 -0
- package/dist/flyout/tests/Flyout.test.js +129 -0
- package/dist/flyout/types.d.ts +24 -0
- package/dist/flyout/types.js +1 -0
- package/dist/flyout/utilities/applyPosition.d.ts +7 -0
- package/dist/flyout/utilities/applyPosition.js +103 -0
- package/dist/flyout/utilities/calculatePosition.d.ts +33 -0
- package/dist/flyout/utilities/calculatePosition.js +159 -0
- package/dist/flyout/utilities/centerBySize.d.ts +5 -0
- package/dist/flyout/utilities/centerBySize.js +7 -0
- package/dist/flyout/utilities/findClosestFixedContainer.d.ts +5 -0
- package/dist/flyout/utilities/findClosestFixedContainer.js +18 -0
- package/dist/flyout/utilities/findClosestScrollableContainer.d.ts +5 -0
- package/dist/flyout/utilities/findClosestScrollableContainer.js +12 -0
- package/dist/flyout/utilities/getPositionFallbacks.d.ts +8 -0
- package/dist/flyout/utilities/getPositionFallbacks.js +43 -0
- package/dist/flyout/utilities/getRTLPosition.d.ts +3 -0
- package/dist/flyout/utilities/getRTLPosition.js +8 -0
- package/dist/flyout/utilities/getRectFromCoordinates.d.ts +6 -0
- package/dist/flyout/utilities/getRectFromCoordinates.js +18 -0
- package/dist/flyout/utilities/isFullyVisible.d.ts +13 -0
- package/dist/flyout/utilities/isFullyVisible.js +28 -0
- package/dist/flyout/utilities/tests/applyPosition.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/applyPosition.test.js +143 -0
- package/dist/flyout/utilities/tests/calculatePosition.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/calculatePosition.test.js +536 -0
- package/dist/flyout/utilities/tests/centerBySize.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/centerBySize.test.js +10 -0
- package/dist/flyout/utilities/tests/findClosestFixedContainer.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/findClosestFixedContainer.test.js +46 -0
- package/dist/flyout/utilities/tests/findClosestScrollableContainer.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/findClosestScrollableContainer.test.js +66 -0
- package/dist/flyout/utilities/tests/getPositionFallbacks.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/getPositionFallbacks.test.js +114 -0
- package/dist/flyout/utilities/tests/getRTLPosition.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/getRTLPosition.test.js +19 -0
- package/dist/flyout/utilities/tests/isFullyVisible.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/isFullyVisible.test.js +129 -0
- package/dist/helpers/rafThrottle.d.ts +2 -0
- package/dist/helpers/rafThrottle.js +15 -0
- package/dist/helpers/tests/rafThrottle.test.d.ts +1 -0
- package/dist/helpers/tests/rafThrottle.test.js +49 -0
- package/dist/i18n/isRTL.d.ts +2 -0
- package/dist/i18n/isRTL.js +10 -0
- package/dist/i18n/tests/isRTL.test.d.ts +1 -0
- package/dist/i18n/tests/isRTL.test.js +51 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- 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,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 @@
|
|
|
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,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;
|