@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
@@ -0,0 +1,18 @@
1
+ import getShadowRoot from "../../dom/getShadowRoot.js";
2
+ const findClosestPositionContainer = (args) => {
3
+ const { el, iteration = 0 } = args;
4
+ const style = el && window.getComputedStyle(el);
5
+ const position = style?.position;
6
+ const isFixed = position === "fixed" || position === "sticky";
7
+ if (iteration === 0) {
8
+ const shadowRoot = getShadowRoot(el);
9
+ if (shadowRoot?.firstElementChild)
10
+ return shadowRoot.firstElementChild;
11
+ }
12
+ if (el === document.body || !el)
13
+ return document.body;
14
+ if (isFixed)
15
+ return el;
16
+ return findClosestPositionContainer({ el: el.parentElement, iteration: iteration + 1 });
17
+ };
18
+ export default (args) => findClosestPositionContainer({ ...args, iteration: 0 });
@@ -0,0 +1,5 @@
1
+ type Args = {
2
+ el: HTMLElement;
3
+ };
4
+ declare const _default: (args: Args) => HTMLElement | null;
5
+ export default _default;
@@ -0,0 +1,12 @@
1
+ const findClosestScrollableContainer = (args) => {
2
+ const { el, iteration } = args;
3
+ const style = el && window.getComputedStyle(el);
4
+ const overflowY = style.overflowY;
5
+ const isScrollable = overflowY.includes("scroll") || overflowY.includes("auto");
6
+ if (!el.parentElement)
7
+ return null;
8
+ if (isScrollable && el.scrollHeight > el.clientHeight)
9
+ return el;
10
+ return findClosestScrollableContainer({ el: el.parentElement, iteration: iteration + 1 });
11
+ };
12
+ export default (args) => findClosestScrollableContainer({ ...args, iteration: 0 });
@@ -0,0 +1,8 @@
1
+ import type { Position } from "../types";
2
+ /**
3
+ * Get an order of positions to try to fit flyout on the screen based on its starting position
4
+ * @param position - position being checked
5
+ * @param availableFallbacks
6
+ */
7
+ declare const getPositionFallbacks: (position: Position, availableFallbacks?: Position[]) => Position[];
8
+ export default getPositionFallbacks;
@@ -0,0 +1,43 @@
1
+ // All available positions for each side
2
+ const positions = {
3
+ top: ["top-start", "top-end", "top"],
4
+ bottom: ["bottom-start", "bottom-end", "bottom"],
5
+ start: ["start-top", "start-bottom", "start"],
6
+ end: ["end-top", "end-bottom", "end"],
7
+ };
8
+ // Order of sides to try depending on the starting side
9
+ const fallbackOrder = {
10
+ top: ["bottom", "start", "end"],
11
+ bottom: ["top", "end", "start"],
12
+ start: ["end", "top", "bottom"],
13
+ end: ["start", "bottom", "top"],
14
+ };
15
+ /**
16
+ * Get an order of positions to try to fit flyout on the screen based on its starting position
17
+ * @param position - position being checked
18
+ * @param availableFallbacks
19
+ */
20
+ const getPositionFallbacks = (position, availableFallbacks) => {
21
+ const result = new Set([position]);
22
+ const chunks = position.split("-");
23
+ const [firstChunk] = chunks;
24
+ const passedPositionOrder = positions[firstChunk];
25
+ const startingFallbackIndex = passedPositionOrder.indexOf(position);
26
+ const fallbackIndexOrder = [startingFallbackIndex];
27
+ passedPositionOrder.forEach((_, index) => {
28
+ if (index === startingFallbackIndex)
29
+ return;
30
+ fallbackIndexOrder.push(index);
31
+ });
32
+ [firstChunk, ...fallbackOrder[firstChunk]].forEach((fallbackSide) => {
33
+ const fallbackOrder = positions[fallbackSide];
34
+ fallbackIndexOrder.forEach((index) => {
35
+ const position = fallbackOrder[index];
36
+ if (availableFallbacks?.indexOf(position) === -1)
37
+ return;
38
+ result.add(position);
39
+ });
40
+ });
41
+ return Array.from(result);
42
+ };
43
+ export default getPositionFallbacks;
@@ -0,0 +1,3 @@
1
+ import { Position } from "../types";
2
+ declare const getRTLPosition: (position: Position) => Position;
3
+ export default getRTLPosition;
@@ -0,0 +1,8 @@
1
+ const getRTLPosition = (position) => {
2
+ if (position.includes("start"))
3
+ return position.replace("start", "end");
4
+ if (position.includes("end"))
5
+ return position.replace("end", "start");
6
+ return position;
7
+ };
8
+ export default getRTLPosition;
@@ -0,0 +1,6 @@
1
+ import type { Coordinates } from "../types";
2
+ /**
3
+ * Include any missing properties to match DOMRect interface when coordinates are passed
4
+ */
5
+ declare const getRectFromCoordinates: (coordinates: DOMRect | Coordinates) => DOMRect;
6
+ export default getRectFromCoordinates;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Include any missing properties to match DOMRect interface when coordinates are passed
3
+ */
4
+ const getRectFromCoordinates = (coordinates) => {
5
+ if ("width" in coordinates && coordinates.width !== undefined)
6
+ return coordinates;
7
+ return {
8
+ ...coordinates,
9
+ width: 0,
10
+ height: 0,
11
+ left: coordinates.x,
12
+ right: coordinates.x,
13
+ top: coordinates.y,
14
+ bottom: coordinates.y,
15
+ toJSON: () => { },
16
+ };
17
+ };
18
+ export default getRectFromCoordinates;
@@ -0,0 +1,13 @@
1
+ type Bounds = Pick<DOMRect, "left" | "top" | "width" | "height">;
2
+ /**
3
+ * Check if element visually fits within its render container
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
7
+ */
8
+ declare const isFullyVisible: (args: {
9
+ flyoutBounds: Bounds;
10
+ visualContainerBounds: Bounds;
11
+ renderContainerBounds: Bounds;
12
+ }) => boolean;
13
+ export default isFullyVisible;
@@ -0,0 +1,28 @@
1
+ import { CONTAINER_OFFSET } from "../constants.js";
2
+ /**
3
+ * Check if element visually fits within its render container
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
7
+ */
8
+ 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)
15
+ return false;
16
+ if (flyoutTop < containerTop + CONTAINER_OFFSET)
17
+ return false;
18
+ if (flyoutLeft + flyoutBounds.width >
19
+ containerLeft + visualContainerBounds.width - CONTAINER_OFFSET) {
20
+ return false;
21
+ }
22
+ if (flyoutTop + flyoutBounds.height >
23
+ containerTop + visualContainerBounds.height - CONTAINER_OFFSET) {
24
+ return false;
25
+ }
26
+ return true;
27
+ };
28
+ export default isFullyVisible;
@@ -0,0 +1,143 @@
1
+ import { expect, test, describe, beforeEach, afterEach, vi } from "vitest";
2
+ import applyPosition from "../applyPosition.js";
3
+ describe("flyout/applyPosition", () => {
4
+ let content;
5
+ let trigger;
6
+ beforeEach(() => {
7
+ // Create mock elements
8
+ content = document.createElement("div");
9
+ content.style.width = "100px";
10
+ content.style.height = "50px";
11
+ document.body.appendChild(content);
12
+ trigger = document.createElement("button");
13
+ trigger.style.position = "absolute";
14
+ trigger.style.left = "100px";
15
+ trigger.style.top = "200px";
16
+ trigger.style.width = "50px";
17
+ trigger.style.height = "30px";
18
+ document.body.appendChild(trigger);
19
+ });
20
+ afterEach(() => {
21
+ // Clean up
22
+ if (content.parentNode)
23
+ content.parentNode.removeChild(content);
24
+ if (trigger.parentNode)
25
+ trigger.parentNode.removeChild(trigger);
26
+ });
27
+ test("applies position when flyout is fully visible", () => {
28
+ const result = applyPosition({
29
+ content,
30
+ trigger,
31
+ triggerCoordinates: null,
32
+ position: "top",
33
+ lastUsedPosition: null,
34
+ onClose: vi.fn(),
35
+ });
36
+ expect(result.position).toBe("top");
37
+ });
38
+ test("uses fallback position when first position is not visible", () => {
39
+ // Position trigger at top edge so "top" won't fit
40
+ trigger.style.top = "0px";
41
+ const result = applyPosition({
42
+ content,
43
+ trigger,
44
+ triggerCoordinates: null,
45
+ position: "top",
46
+ lastUsedPosition: null,
47
+ onClose: vi.fn(),
48
+ });
49
+ expect(result.position).toBe("bottom");
50
+ });
51
+ test("uses triggerCoordinates when provided", () => {
52
+ const triggerCoordinates = {
53
+ x: 150,
54
+ y: 250,
55
+ };
56
+ const result = applyPosition({
57
+ content,
58
+ triggerCoordinates,
59
+ position: "top",
60
+ lastUsedPosition: null,
61
+ onClose: vi.fn(),
62
+ });
63
+ expect(result.position).toBe("top");
64
+ expect(content.style.transform).toBe(`translate(150px, ${250 - window.innerHeight}px)`);
65
+ });
66
+ test("throws error when triggerBounds are missing", () => {
67
+ expect(() => {
68
+ applyPosition({
69
+ content,
70
+ triggerCoordinates: null,
71
+ position: "top",
72
+ lastUsedPosition: null,
73
+ onClose: vi.fn(),
74
+ });
75
+ }).toThrow("Trigger bounds are required");
76
+ });
77
+ test("applies custom width when provided", () => {
78
+ applyPosition({
79
+ content,
80
+ trigger,
81
+ triggerCoordinates: null,
82
+ position: "top",
83
+ width: "200px",
84
+ lastUsedPosition: null,
85
+ onClose: vi.fn(),
86
+ });
87
+ expect(content.style.width).toBe("200px");
88
+ });
89
+ test("applies width 'trigger' option", () => {
90
+ const result = applyPosition({
91
+ content,
92
+ trigger,
93
+ triggerCoordinates: null,
94
+ position: "top",
95
+ width: "trigger",
96
+ lastUsedPosition: null,
97
+ onClose: vi.fn(),
98
+ });
99
+ expect(result.position).toBeTruthy();
100
+ });
101
+ test("applies width '100%' option in fallback", () => {
102
+ // Make content very large
103
+ content.style.width = "2000px";
104
+ content.style.height = "2000px";
105
+ const result = applyPosition({
106
+ content,
107
+ trigger,
108
+ triggerCoordinates: null,
109
+ position: "top",
110
+ lastUsedPosition: null,
111
+ onClose: vi.fn(),
112
+ });
113
+ // Should try full-width positions
114
+ expect(result.position).toBeTruthy();
115
+ });
116
+ test("applies fallbackPositions when provided", () => {
117
+ // Position trigger at top edge
118
+ trigger.style.top = "0px";
119
+ const result = applyPosition({
120
+ content,
121
+ trigger,
122
+ triggerCoordinates: null,
123
+ position: "top",
124
+ fallbackPositions: ["start"],
125
+ lastUsedPosition: null,
126
+ onClose: vi.fn(),
127
+ });
128
+ expect(result.position).toBe("start");
129
+ });
130
+ test("removes content clone from DOM after calculation", () => {
131
+ const initialChildCount = document.body.children.length;
132
+ applyPosition({
133
+ content,
134
+ trigger,
135
+ triggerCoordinates: null,
136
+ position: "top",
137
+ lastUsedPosition: null,
138
+ onClose: vi.fn(),
139
+ });
140
+ // Clone should be removed
141
+ expect(document.body.children.length).toBe(initialChildCount);
142
+ });
143
+ });