@reshaped/utilities 3.9.1-canary.2 → 3.10.0-canary.4

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 (100) hide show
  1. package/dist/a11y/Chain.d.ts +20 -0
  2. package/dist/a11y/Chain.js +60 -0
  3. package/dist/a11y/TrapFocus.d.ts +28 -0
  4. package/dist/a11y/TrapFocus.js +162 -0
  5. package/dist/a11y/TrapScreenReader.d.ts +15 -0
  6. package/dist/a11y/TrapScreenReader.js +42 -0
  7. package/dist/a11y/focus.d.ts +38 -0
  8. package/dist/a11y/focus.js +101 -0
  9. package/dist/a11y/index.d.ts +4 -0
  10. package/dist/a11y/index.js +3 -0
  11. package/dist/a11y/keyboardMode.d.ts +4 -0
  12. package/dist/a11y/keyboardMode.js +10 -0
  13. package/dist/a11y/tests/Chain.test.js +88 -0
  14. package/dist/a11y/tests/TrapFocus.test.d.ts +1 -0
  15. package/dist/a11y/tests/TrapFocus.test.js +313 -0
  16. package/dist/a11y/tests/TrapScreenReader.test.d.ts +1 -0
  17. package/dist/a11y/tests/TrapScreenReader.test.js +126 -0
  18. package/dist/a11y/tests/focus.test.d.ts +1 -0
  19. package/dist/a11y/tests/focus.test.js +278 -0
  20. package/dist/a11y/tests/keyboardMode.test.d.ts +1 -0
  21. package/dist/a11y/tests/keyboardMode.test.js +27 -0
  22. package/dist/a11y/types.d.ts +24 -0
  23. package/dist/a11y/types.js +1 -0
  24. package/dist/constants/keys.d.ts +11 -0
  25. package/dist/constants/keys.js +11 -0
  26. package/dist/css/StyleCache.d.ts +7 -0
  27. package/dist/css/StyleCache.js +19 -0
  28. package/dist/css/classNames.d.ts +7 -0
  29. package/dist/css/classNames.js +19 -0
  30. package/dist/css/index.d.ts +2 -0
  31. package/dist/css/index.js +4 -0
  32. package/dist/css/tests/StyleCache.test.d.ts +1 -0
  33. package/dist/css/tests/StyleCache.test.js +45 -0
  34. package/dist/css/tests/classNames.test.d.ts +1 -0
  35. package/dist/css/tests/classNames.test.js +63 -0
  36. package/dist/dom/findClosestScrollableContainer.d.ts +5 -0
  37. package/dist/dom/findClosestScrollableContainer.js +12 -0
  38. package/dist/dom/findParent.d.ts +2 -0
  39. package/dist/dom/findParent.js +10 -0
  40. package/dist/dom/index.d.ts +3 -0
  41. package/dist/dom/index.js +4 -0
  42. package/dist/dom/tests/findClosestScrollableContainer.test.d.ts +1 -0
  43. package/dist/dom/tests/findClosestScrollableContainer.test.js +61 -0
  44. package/dist/dom/tests/findParent.test.d.ts +1 -0
  45. package/dist/dom/tests/findParent.test.js +45 -0
  46. package/dist/flyout/Flyout.js +11 -9
  47. package/dist/flyout/constants.d.ts +1 -1
  48. package/dist/flyout/constants.js +1 -1
  49. package/dist/flyout/index.d.ts +1 -1
  50. package/dist/flyout/index.js +1 -1
  51. package/dist/flyout/tests/Flyout.test.js +1 -1
  52. package/dist/flyout/types.d.ts +1 -1
  53. package/dist/flyout/utilities/applyPosition.js +46 -26
  54. package/dist/flyout/utilities/calculateLayoutAdjustment.d.ts +19 -0
  55. package/dist/flyout/utilities/calculateLayoutAdjustment.js +73 -0
  56. package/dist/flyout/utilities/calculatePosition.d.ts +7 -20
  57. package/dist/flyout/utilities/calculatePosition.js +11 -87
  58. package/dist/flyout/utilities/isFullyVisible.d.ts +2 -4
  59. package/dist/flyout/utilities/isFullyVisible.js +11 -14
  60. package/dist/flyout/utilities/tests/applyPosition.test.js +1 -1
  61. package/dist/flyout/utilities/tests/calculateLayoutAdjustment.test.d.ts +1 -0
  62. package/dist/flyout/utilities/tests/calculateLayoutAdjustment.test.js +384 -0
  63. package/dist/flyout/utilities/tests/calculatePosition.test.js +244 -293
  64. package/dist/flyout/utilities/tests/centerBySize.test.js +1 -1
  65. package/dist/flyout/utilities/tests/getPositionFallbacks.test.js +1 -1
  66. package/dist/flyout/utilities/tests/getRTLPosition.test.js +1 -1
  67. package/dist/flyout/utilities/tests/isFullyVisible.test.js +28 -52
  68. package/dist/helpers/classNames.d.ts +7 -0
  69. package/dist/helpers/classNames.js +19 -0
  70. package/dist/helpers/index.d.ts +1 -0
  71. package/dist/helpers/index.js +2 -0
  72. package/dist/helpers/tests/classNames.test.d.ts +1 -0
  73. package/dist/helpers/tests/classNames.test.js +63 -0
  74. package/dist/i18n/index.d.ts +1 -0
  75. package/dist/i18n/index.js +2 -0
  76. package/dist/index.d.ts +5 -1
  77. package/dist/index.js +5 -1
  78. package/dist/internal.d.ts +11 -0
  79. package/dist/internal.js +10 -0
  80. package/dist/platform/index.d.ts +1 -0
  81. package/dist/platform/index.js +16 -0
  82. package/dist/scroll/disable.d.ts +7 -0
  83. package/dist/scroll/disable.js +15 -0
  84. package/dist/scroll/helpers.d.ts +1 -0
  85. package/dist/scroll/helpers.js +17 -0
  86. package/dist/scroll/index.d.ts +2 -0
  87. package/dist/scroll/index.js +4 -0
  88. package/dist/scroll/lock.d.ts +7 -0
  89. package/dist/scroll/lock.js +26 -0
  90. package/dist/scroll/lockSafari.d.ts +2 -0
  91. package/dist/scroll/lockSafari.js +20 -0
  92. package/dist/scroll/lockStandard.d.ts +4 -0
  93. package/dist/scroll/lockStandard.js +15 -0
  94. package/dist/scroll/tests/lock.test.d.ts +1 -0
  95. package/dist/scroll/tests/lock.test.js +81 -0
  96. package/package.json +6 -1
  97. package/dist/flyout/utilities/findClosestFixedContainer.d.ts +0 -5
  98. package/dist/flyout/utilities/findClosestFixedContainer.js +0 -18
  99. package/dist/flyout/utilities/tests/findClosestFixedContainer.test.js +0 -46
  100. /package/dist/{flyout/utilities/tests/findClosestFixedContainer.test.d.ts → a11y/tests/Chain.test.d.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { expect, test, describe } from "vitest";
2
- import centerBySize from "../centerBySize.js";
2
+ import centerBySize from "flyout/utilities/centerBySize";
3
3
  describe("flyout/centerBySize", () => {
4
4
  test("centers even value", () => {
5
5
  expect(centerBySize(100, 50)).toEqual(25);
@@ -1,5 +1,5 @@
1
1
  import { expect, test, describe } from "vitest";
2
- import getPositionFallbacks from "../getPositionFallbacks.js";
2
+ import getPositionFallbacks from "flyout/utilities/getPositionFallbacks";
3
3
  describe("flyout/getPositionFallbacks", () => {
4
4
  test("returns original position first for top-start", () => {
5
5
  const result = getPositionFallbacks("top-start");
@@ -1,5 +1,5 @@
1
1
  import { expect, test, describe } from "vitest";
2
- import getRTLPosition from "../getRTLPosition.js";
2
+ import getRTLPosition from "flyout/utilities/getRTLPosition";
3
3
  describe("flyout/getRTLPosition", () => {
4
4
  test("keeps top position", () => {
5
5
  expect(getRTLPosition("top")).toEqual("top");
@@ -1,35 +1,31 @@
1
+ import isFullyVisible from "flyout/utilities/isFullyVisible";
1
2
  import { expect, test, describe } from "vitest";
2
- import isFullyVisible from "../isFullyVisible.js";
3
3
  describe("flyout/isFullyVisible", () => {
4
4
  test("returns true when flyout is fully visible within visual container", () => {
5
5
  const result = isFullyVisible({
6
6
  flyoutBounds: { left: 8, top: 8, width: 50, height: 50 },
7
- visualContainerBounds: { left: 0, top: 0, width: 100, height: 100 },
8
- renderContainerBounds: { left: 0, top: 0, width: 100, height: 100 },
7
+ containerBounds: { left: 0, top: 0, width: 100, height: 100 },
9
8
  });
10
9
  expect(result).toBe(true);
11
10
  });
12
11
  test("returns true when flyout is fully visible and is exactly on the edges of the visual container", () => {
13
12
  const result = isFullyVisible({
14
13
  flyoutBounds: { left: 8, top: 8, width: 84, height: 84 },
15
- visualContainerBounds: { left: 0, top: 0, width: 100, height: 100 },
16
- renderContainerBounds: { left: 0, top: 0, width: 100, height: 100 },
14
+ containerBounds: { left: 0, top: 0, width: 100, height: 100 },
17
15
  });
18
16
  expect(result).toBe(true);
19
17
  });
20
18
  test("returns false when flyout extends beyond left edge", () => {
21
19
  const result = isFullyVisible({
22
20
  flyoutBounds: { left: 7, top: 8, width: 50, height: 50 },
23
- visualContainerBounds: { left: 0, top: 0, width: 100, height: 100 },
24
- renderContainerBounds: { left: 0, top: 0, width: 100, height: 100 },
21
+ containerBounds: { left: 0, top: 0, width: 100, height: 100 },
25
22
  });
26
23
  expect(result).toBe(false);
27
24
  });
28
25
  test("returns false when flyout extends beyond top edge", () => {
29
26
  const result = isFullyVisible({
30
27
  flyoutBounds: { left: 8, top: 7, width: 50, height: 50 },
31
- visualContainerBounds: { left: 0, top: 0, width: 100, height: 100 },
32
- renderContainerBounds: { left: 0, top: 0, width: 100, height: 100 },
28
+ containerBounds: { left: 0, top: 0, width: 100, height: 100 },
33
29
  });
34
30
  expect(result).toBe(false);
35
31
  });
@@ -41,8 +37,7 @@ describe("flyout/isFullyVisible", () => {
41
37
  width: 50,
42
38
  height: 50,
43
39
  },
44
- visualContainerBounds: { left: 0, top: 0, width: 100, height: 100 },
45
- renderContainerBounds: { left: 0, top: 0, width: 100, height: 100 },
40
+ containerBounds: { left: 0, top: 0, width: 100, height: 100 },
46
41
  });
47
42
  expect(result).toBe(false);
48
43
  });
@@ -54,75 +49,56 @@ describe("flyout/isFullyVisible", () => {
54
49
  width: 50,
55
50
  height: 50,
56
51
  },
57
- visualContainerBounds: { left: 0, top: 0, width: 100, height: 100 },
58
- renderContainerBounds: { left: 0, top: 0, width: 100, height: 100 },
52
+ containerBounds: { left: 0, top: 0, width: 100, height: 100 },
59
53
  });
60
54
  expect(result).toBe(false);
61
55
  });
62
- /**
63
- * Render container
64
- */
65
- test("returns true when flyout is fully visible with offset render container", () => {
56
+ test("returns true when flyout is fully visible within container with non-zero position", () => {
66
57
  const result = isFullyVisible({
67
- flyoutBounds: { left: 58, top: 58, width: 100, height: 100 },
68
- renderContainerBounds: { left: 50, top: 50, width: 200, height: 200 },
69
- visualContainerBounds: { left: 100, top: 100, width: 200, height: 200 },
58
+ flyoutBounds: { left: 108, top: 208, width: 50, height: 50 },
59
+ containerBounds: { left: 100, top: 200, width: 100, height: 100 },
70
60
  });
71
61
  expect(result).toBe(true);
72
62
  });
73
- test("returns false when flyout extends beyond left edge with offset render container", () => {
63
+ test("returns false when flyout extends beyond left edge of container with non-zero position", () => {
74
64
  const result = isFullyVisible({
75
- flyoutBounds: { left: 0, top: 58, width: 50, height: 50 },
76
- renderContainerBounds: { left: 50, top: 50, width: 200, height: 200 },
77
- visualContainerBounds: { left: 100, top: 100, width: 200, height: 200 },
65
+ flyoutBounds: { left: 107, top: 208, width: 50, height: 50 },
66
+ containerBounds: { left: 100, top: 200, width: 100, height: 100 },
78
67
  });
79
68
  expect(result).toBe(false);
80
69
  });
81
- test("returns false when flyout extends beyond top edge with offset render container", () => {
70
+ test("returns false when flyout extends beyond top edge of container with non-zero position", () => {
82
71
  const result = isFullyVisible({
83
- flyoutBounds: { left: 58, top: 0, width: 50, height: 50 },
84
- renderContainerBounds: { left: 50, top: 50, width: 200, height: 200 },
85
- visualContainerBounds: { left: 100, top: 100, width: 200, height: 200 },
72
+ flyoutBounds: { left: 108, top: 207, width: 50, height: 50 },
73
+ containerBounds: { left: 100, top: 200, width: 100, height: 100 },
86
74
  });
87
75
  expect(result).toBe(false);
88
76
  });
89
- test("returns false when flyout extends beyond right edge with offset render container", () => {
77
+ test("returns false when flyout extends beyond multiple edges", () => {
90
78
  const result = isFullyVisible({
91
- flyoutBounds: { left: 200 - 7, top: 58, width: 50, height: 50 },
92
- renderContainerBounds: { left: 50, top: 50, width: 200, height: 200 },
93
- visualContainerBounds: { left: 100, top: 100, width: 200, height: 200 },
79
+ flyoutBounds: { left: 5, top: 5, width: 100, height: 100 },
80
+ containerBounds: { left: 0, top: 0, width: 100, height: 100 },
94
81
  });
95
82
  expect(result).toBe(false);
96
83
  });
97
- test("returns false when flyout extends beyond bottom edge with offset render container", () => {
84
+ test("returns false when flyout is completely outside container", () => {
98
85
  const result = isFullyVisible({
99
- flyoutBounds: { left: 58, top: 200 - 7, width: 50, height: 50 },
100
- renderContainerBounds: { left: 50, top: 50, width: 200, height: 200 },
101
- visualContainerBounds: { left: 100, top: 100, width: 200, height: 200 },
86
+ flyoutBounds: { left: 200, top: 200, width: 50, height: 50 },
87
+ containerBounds: { left: 0, top: 0, width: 100, height: 100 },
102
88
  });
103
89
  expect(result).toBe(false);
104
90
  });
105
- test("returns false when flyout is larger than visual container", () => {
91
+ test("returns true when flyout has zero dimensions but is within bounds", () => {
106
92
  const result = isFullyVisible({
107
- flyoutBounds: { left: 0, top: 0, width: 300, height: 300 },
108
- visualContainerBounds: { left: 0, top: 0, width: 200, height: 200 },
109
- renderContainerBounds: { left: 0, top: 0, width: 200, height: 200 },
110
- });
111
- expect(result).toBe(false);
112
- });
113
- test("returns true when flyout is fully visible with negative render container position", () => {
114
- const result = isFullyVisible({
115
- flyoutBounds: { left: 118, top: 118, width: 100, height: 50 },
116
- visualContainerBounds: { left: 100, top: 100, width: 200, height: 200 },
117
- renderContainerBounds: { left: -10, top: -10, width: 200, height: 200 },
93
+ flyoutBounds: { left: 50, top: 50, width: 0, height: 0 },
94
+ containerBounds: { left: 0, top: 0, width: 100, height: 100 },
118
95
  });
119
96
  expect(result).toBe(true);
120
97
  });
121
- test("returns false when flyout extends beyond left with negative render container position", () => {
98
+ test("returns false when container is smaller than required offsets", () => {
122
99
  const result = isFullyVisible({
123
- flyoutBounds: { left: 0, top: 110, width: 100, height: 50 },
124
- visualContainerBounds: { left: 100, top: 100, width: 200, height: 200 },
125
- renderContainerBounds: { left: -10, top: -10, width: 200, height: 200 },
100
+ flyoutBounds: { left: 8, top: 8, width: 1, height: 1 },
101
+ containerBounds: { left: 0, top: 0, width: 10, height: 10 },
126
102
  });
127
103
  expect(result).toBe(false);
128
104
  });
@@ -0,0 +1,7 @@
1
+ type ClassNameValue = string | null | undefined | false;
2
+ export type ClassName = ClassNameValue | ClassNameValue[] | ClassName[];
3
+ /**
4
+ * Resolve an array of values into a classname string
5
+ */
6
+ declare const classNames: (...args: ClassName[]) => string;
7
+ export default classNames;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Resolve an array of values into a classname string
3
+ */
4
+ const classNames = (...args) => {
5
+ return args
6
+ .reduce((acc, cur) => {
7
+ if (Array.isArray(cur)) {
8
+ const className = classNames(...cur);
9
+ if (!className)
10
+ return acc;
11
+ return `${acc} ${className}`;
12
+ }
13
+ if (cur)
14
+ return `${acc} ${cur}`;
15
+ return acc;
16
+ }, "")
17
+ .trim();
18
+ };
19
+ export default classNames;
@@ -0,0 +1 @@
1
+ export { default as rafThrottle } from "./rafThrottle";
@@ -0,0 +1,2 @@
1
+ // Internal
2
+ export { default as rafThrottle } from "./rafThrottle.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
1
+ import { expect, test, describe } from "vitest";
2
+ import classNames from "../classNames.js";
3
+ describe("helpers/classNames", () => {
4
+ test("returns empty string when no arguments are provided", () => {
5
+ expect(classNames()).toBe("");
6
+ });
7
+ test("returns a single class name", () => {
8
+ expect(classNames("foo")).toBe("foo");
9
+ });
10
+ test("concatenates multiple class names", () => {
11
+ expect(classNames("foo", "bar", "baz")).toBe("foo bar baz");
12
+ });
13
+ test("filters out null values", () => {
14
+ expect(classNames("foo", null, "bar")).toBe("foo bar");
15
+ expect(classNames(null, "foo", null)).toBe("foo");
16
+ expect(classNames(null, null)).toBe("");
17
+ });
18
+ test("filters out undefined values", () => {
19
+ expect(classNames("foo", undefined, "bar")).toBe("foo bar");
20
+ expect(classNames(undefined, "foo", undefined)).toBe("foo");
21
+ expect(classNames(undefined, undefined)).toBe("");
22
+ });
23
+ test("filters out false values", () => {
24
+ expect(classNames("foo", false, "bar")).toBe("foo bar");
25
+ expect(classNames(false, "foo", false)).toBe("foo");
26
+ expect(classNames(false, false)).toBe("");
27
+ });
28
+ test("handles arrays of class names", () => {
29
+ expect(classNames(["foo", "bar"])).toBe("foo bar");
30
+ expect(classNames(["foo", "bar"], "baz")).toBe("foo bar baz");
31
+ expect(classNames("foo", ["bar", "baz"])).toBe("foo bar baz");
32
+ });
33
+ test("handles nested arrays", () => {
34
+ expect(classNames(["foo", ["bar", "baz"]])).toBe("foo bar baz");
35
+ expect(classNames([["foo", "bar"], "baz"])).toBe("foo bar baz");
36
+ expect(classNames([["foo"], ["bar"], ["baz"]])).toBe("foo bar baz");
37
+ });
38
+ test("filters out falsy values in arrays", () => {
39
+ expect(classNames(["foo", null, "bar"])).toBe("foo bar");
40
+ expect(classNames(["foo", undefined, "bar"])).toBe("foo bar");
41
+ expect(classNames(["foo", false, "bar"])).toBe("foo bar");
42
+ expect(classNames([null, undefined, false])).toBe("");
43
+ });
44
+ test("handles deeply nested arrays with falsy values", () => {
45
+ expect(classNames([["foo", null], [undefined, "bar"], false])).toBe("foo bar");
46
+ expect(classNames(["foo", [null, undefined, ["bar", false, "baz"]]])).toBe("foo bar baz");
47
+ });
48
+ test("handles empty arrays", () => {
49
+ expect(classNames([])).toBe("");
50
+ expect(classNames("foo", [], "bar")).toBe("foo bar");
51
+ expect(classNames([[], []])).toBe("");
52
+ });
53
+ test("handles empty strings", () => {
54
+ expect(classNames("", "foo")).toBe("foo");
55
+ expect(classNames("foo", "", "bar")).toBe("foo bar");
56
+ expect(classNames("", "", "")).toBe("");
57
+ });
58
+ test("handles conditional class names", () => {
59
+ const isActive = true;
60
+ const isDisabled = false;
61
+ expect(classNames("button", isActive && "active", isDisabled && "disabled")).toBe("button active");
62
+ });
63
+ });
@@ -0,0 +1 @@
1
+ export { default as isRTL } from "./isRTL";
@@ -0,0 +1,2 @@
1
+ // External
2
+ export { default as isRTL } from "./isRTL.js";
package/dist/index.d.ts CHANGED
@@ -1 +1,5 @@
1
- export { default as Flyout } from "./flyout";
1
+ export { Flyout } from "./flyout";
2
+ export { TrapFocus } from "./a11y";
3
+ export { lockScroll } from "./scroll";
4
+ export { isRTL } from "./i18n";
5
+ export { classNames, type ClassName } from "./css";
package/dist/index.js CHANGED
@@ -1 +1,5 @@
1
- export { default as Flyout } from "./flyout/index.js";
1
+ export { Flyout } from "./flyout/index.js";
2
+ export { TrapFocus } from "./a11y/index.js";
3
+ export { lockScroll } from "./scroll/index.js";
4
+ export { isRTL } from "./i18n/index.js";
5
+ export { classNames } from "./css/index.js";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Internal utilities re-used in other Reshaped components but not meant to be used as a public API
3
+ * Their API is subject to change without a major version bump.
4
+ *
5
+ * If you want to use one of these utilities, open an issue or a PR about moving it to the public API file
6
+ */
7
+ export { focusableSelector, getActiveElement, getFocusableElements, focusNextElement, focusPreviousElement, focusFirstElement, focusLastElement, activateKeyboardMode, deactivateKeyboardMode, checkKeyboardMode, } from "./a11y";
8
+ export type { TrapMode, FocusableElement } from "./a11y";
9
+ export { disableScroll, enableScroll } from "./scroll";
10
+ export { rafThrottle } from "./helpers";
11
+ export { getShadowRoot, findParent } from "./dom";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Internal utilities re-used in other Reshaped components but not meant to be used as a public API
3
+ * Their API is subject to change without a major version bump.
4
+ *
5
+ * If you want to use one of these utilities, open an issue or a PR about moving it to the public API file
6
+ */
7
+ export { focusableSelector, getActiveElement, getFocusableElements, focusNextElement, focusPreviousElement, focusFirstElement, focusLastElement, activateKeyboardMode, deactivateKeyboardMode, checkKeyboardMode, } from "./a11y/index.js";
8
+ export { disableScroll, enableScroll } from "./scroll/index.js";
9
+ export { rafThrottle } from "./helpers/index.js";
10
+ export { getShadowRoot, findParent } from "./dom/index.js";
@@ -0,0 +1 @@
1
+ export declare const isIOS: () => boolean;
@@ -0,0 +1,16 @@
1
+ const testPlatform = (re) => {
2
+ // Using experimental and deprecated features here since that's the only way to detect platform consistently
3
+ const platform =
4
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
5
+ // @ts-ignore
6
+ window.navigator.userAgentData?.platform || window.navigator.platform;
7
+ return typeof window !== "undefined" ? re.test(platform) : false;
8
+ };
9
+ const isIPhone = () => testPlatform(/^iPhone/i);
10
+ const isMac = () => testPlatform(/^Mac/i);
11
+ const isIPad = () => {
12
+ return (testPlatform(/^iPad/i) ||
13
+ // iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
14
+ (isMac() && navigator.maxTouchPoints > 1));
15
+ };
16
+ export const isIOS = () => isIPhone() || isIPad();
@@ -0,0 +1,7 @@
1
+ export declare const preventDefault: (e: Event) => void;
2
+ /**
3
+ * Prevent scrolling events for the cases when dragging elements
4
+ * without locking the page with overflow
5
+ */
6
+ export declare const disableScroll: () => void;
7
+ export declare const enableScroll: () => void;
@@ -0,0 +1,15 @@
1
+ export const preventDefault = (e) => e.preventDefault();
2
+ /**
3
+ * Prevent scrolling events for the cases when dragging elements
4
+ * without locking the page with overflow
5
+ */
6
+ export const disableScroll = () => {
7
+ window.addEventListener("wheel", preventDefault, { passive: false });
8
+ window.addEventListener("touchmove", preventDefault, { passive: false });
9
+ document.body.style.userSelect = "none";
10
+ };
11
+ export const enableScroll = () => {
12
+ window.removeEventListener("wheel", preventDefault);
13
+ window.removeEventListener("touchmove", preventDefault);
14
+ document.body.style.userSelect = "";
15
+ };
@@ -0,0 +1 @@
1
+ export declare const getScrollbarWidth: () => number;
@@ -0,0 +1,17 @@
1
+ export const getScrollbarWidth = (() => {
2
+ let scrollbarWidth;
3
+ return () => {
4
+ if (scrollbarWidth)
5
+ return scrollbarWidth;
6
+ const scrollDiv = document.createElement("div");
7
+ scrollDiv.style.position = "absolute";
8
+ scrollDiv.style.top = "-9999px";
9
+ scrollDiv.style.width = "50px";
10
+ scrollDiv.style.height = "50px";
11
+ scrollDiv.style.overflow = "scroll";
12
+ document.body.appendChild(scrollDiv);
13
+ scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth;
14
+ document.body.removeChild(scrollDiv);
15
+ return scrollbarWidth;
16
+ };
17
+ })();
@@ -0,0 +1,2 @@
1
+ export { lockScroll } from "./lock";
2
+ export { disableScroll, enableScroll } from "./disable";
@@ -0,0 +1,4 @@
1
+ // External
2
+ export { lockScroll } from "./lock.js";
3
+ // Internal
4
+ export { disableScroll, enableScroll } from "./disable.js";
@@ -0,0 +1,7 @@
1
+ export declare const lockScroll: (args: {
2
+ containerEl?: HTMLElement | null;
3
+ originEl?: HTMLElement | null;
4
+ callback?: () => void;
5
+ }) => ((args?: {
6
+ callback?: () => void;
7
+ }) => void) | undefined;
@@ -0,0 +1,26 @@
1
+ import { findClosestScrollableContainer } from "../dom/index.js";
2
+ import { isIOS } from "../platform/index.js";
3
+ import lockSafariScroll from "./lockSafari.js";
4
+ import lockStandardScroll from "./lockStandard.js";
5
+ export const lockScroll = (args) => {
6
+ const isIOSLock = isIOS();
7
+ let reset = () => { };
8
+ const container = args.containerEl ??
9
+ (args.originEl && findClosestScrollableContainer({ el: args.originEl })) ??
10
+ document.body;
11
+ const lockedBodyScroll = container === document.body;
12
+ // Already locked so no need to lock again and trigger the callback
13
+ if (container.style.overflow === "hidden")
14
+ return;
15
+ if (isIOSLock && lockedBodyScroll) {
16
+ reset = lockSafariScroll();
17
+ }
18
+ else {
19
+ reset = lockStandardScroll({ container });
20
+ }
21
+ args.callback?.();
22
+ return (args) => {
23
+ reset();
24
+ args?.callback?.();
25
+ };
26
+ };
@@ -0,0 +1,2 @@
1
+ declare const lockSafariScroll: () => () => void;
2
+ export default lockSafariScroll;
@@ -0,0 +1,20 @@
1
+ import { StyleCache } from "../css/index.js";
2
+ const styleCache = new StyleCache();
3
+ const lockSafariScroll = () => {
4
+ const viewport = window.visualViewport;
5
+ const offsetLeft = viewport?.offsetLeft || 0;
6
+ const offsetTop = viewport?.offsetTop || 0;
7
+ const { scrollX, scrollY } = window;
8
+ styleCache.set(document.body, {
9
+ position: "fixed",
10
+ top: `${-(scrollY - Math.floor(offsetTop))}px`,
11
+ left: `${-(scrollX - Math.floor(offsetLeft))}px`,
12
+ right: "0",
13
+ overflow: "hidden",
14
+ });
15
+ return () => {
16
+ styleCache.reset();
17
+ window.scrollTo({ top: scrollY, left: scrollX, behavior: "instant" });
18
+ };
19
+ };
20
+ export default lockSafariScroll;
@@ -0,0 +1,4 @@
1
+ declare const lockStandardScroll: (args: {
2
+ container: HTMLElement;
3
+ }) => () => void;
4
+ export default lockStandardScroll;
@@ -0,0 +1,15 @@
1
+ import { StyleCache } from "../css/index.js";
2
+ import { getScrollbarWidth } from "./helpers.js";
3
+ const styleCache = new StyleCache();
4
+ const lockStandardScroll = (args) => {
5
+ const { container } = args;
6
+ const rect = container.getBoundingClientRect();
7
+ const isOverflowing = rect.left + rect.right < window.innerWidth;
8
+ styleCache.set(container, { overflow: "hidden" });
9
+ if (isOverflowing) {
10
+ const scrollBarWidth = getScrollbarWidth();
11
+ styleCache.set(container, { paddingRight: `${scrollBarWidth}px` });
12
+ }
13
+ return () => styleCache.reset();
14
+ };
15
+ export default lockStandardScroll;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,81 @@
1
+ import { expect, test, describe, beforeEach, vi, afterEach } from "vitest";
2
+ import { lockScroll } from "../lock.js";
3
+ // Mock the platform module to avoid iOS path
4
+ vi.mock("@/platform", () => ({
5
+ isIOS: () => false,
6
+ }));
7
+ describe("scroll/lockScroll", () => {
8
+ beforeEach(() => {
9
+ // Reset the body styles before each test
10
+ document.body.style.overflow = "";
11
+ document.body.style.paddingRight = "";
12
+ document.body.innerHTML = "";
13
+ });
14
+ afterEach(() => {
15
+ // Clean up
16
+ document.body.style.overflow = "";
17
+ document.body.style.paddingRight = "";
18
+ document.body.innerHTML = "";
19
+ });
20
+ test("locks scroll on body", () => {
21
+ const unlock = lockScroll({});
22
+ expect(document.body.style.overflow).toBe("hidden");
23
+ unlock?.();
24
+ expect(document.body.style.overflow).toBe("");
25
+ });
26
+ test("locks scroll on custom container element", () => {
27
+ const container = document.createElement("div");
28
+ container.style.overflow = "auto";
29
+ container.style.height = "200px";
30
+ document.body.appendChild(container);
31
+ const unlock = lockScroll({ containerEl: container });
32
+ expect(container.style.overflow).toBe("hidden");
33
+ expect(document.body.style.overflow).not.toBe("hidden");
34
+ unlock?.();
35
+ expect(container.style.overflow).toBe("auto");
36
+ });
37
+ test("finds scrollable container from origin element", () => {
38
+ const scrollableContainer = document.createElement("div");
39
+ scrollableContainer.style.overflow = "auto";
40
+ scrollableContainer.style.height = "200px";
41
+ document.body.appendChild(scrollableContainer);
42
+ // Add content to make it scrollable
43
+ const content = document.createElement("div");
44
+ content.style.height = "500px";
45
+ scrollableContainer.appendChild(content);
46
+ const originEl = document.createElement("div");
47
+ scrollableContainer.appendChild(originEl);
48
+ const unlock = lockScroll({ originEl });
49
+ expect(scrollableContainer.style.overflow).toBe("hidden");
50
+ unlock?.();
51
+ expect(scrollableContainer.style.overflow).toBe("auto");
52
+ });
53
+ test("unlocks after multiple locks", () => {
54
+ const unlock1 = lockScroll({});
55
+ const unlock2 = lockScroll({});
56
+ expect(document.body.style.overflow).toBe("hidden");
57
+ unlock1?.();
58
+ expect(document.body.style.overflow).toBe("");
59
+ unlock2?.();
60
+ expect(document.body.style.overflow).toBe("");
61
+ });
62
+ test("calls lock callback immediately", () => {
63
+ const lockCb = vi.fn();
64
+ lockScroll({ callback: lockCb });
65
+ expect(lockCb).toHaveBeenCalledTimes(1);
66
+ });
67
+ test("calls unlock callback when unlocking", () => {
68
+ const unlockCb = vi.fn();
69
+ const unlock = lockScroll({});
70
+ unlock?.({ callback: unlockCb });
71
+ expect(unlockCb).toHaveBeenCalledTimes(1);
72
+ });
73
+ test("handles origin element when no scrollable container is found (falls back to body)", () => {
74
+ const originEl = document.createElement("div");
75
+ document.body.appendChild(originEl);
76
+ const unlock = lockScroll({ originEl });
77
+ expect(document.body.style.overflow).toBe("hidden");
78
+ unlock?.();
79
+ expect(document.body.style.overflow).toBe("");
80
+ });
81
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reshaped/utilities",
3
3
  "description": "Vanilla JS utilities for implementing common UI patterns",
4
- "version": "3.9.1-canary.2",
4
+ "version": "3.10.0-canary.4",
5
5
  "license": "MIT",
6
6
  "homepage": "https://reshaped.so",
7
7
  "repository": {
@@ -27,6 +27,11 @@
27
27
  "import": "./dist/index.js",
28
28
  "default": "./dist/index.js"
29
29
  },
30
+ "./internal": {
31
+ "types": "./dist/internal.d.ts",
32
+ "import": "./dist/internal.js",
33
+ "default": "./dist/internal.js"
34
+ },
30
35
  "./package.json": "./package.json"
31
36
  },
32
37
  "peerDependencies": {
@@ -1,5 +0,0 @@
1
- type Args = {
2
- el: HTMLElement | null;
3
- };
4
- declare const _default: (args: Args) => HTMLElement;
5
- export default _default;
@@ -1,18 +0,0 @@
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 });