reshaped 3.1.5 → 3.1.7

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 (125) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/dist/bundle.css +1 -1
  3. package/dist/bundle.d.ts +2 -0
  4. package/dist/bundle.js +10 -11
  5. package/dist/cjs/themes/_generator/definitions/reshaped.js +1 -1
  6. package/dist/cjs/themes/reshaped/theme.css +1 -1
  7. package/dist/components/Actionable/Actionable.d.ts +1 -1
  8. package/dist/components/Actionable/Actionable.js +2 -2
  9. package/dist/components/Actionable/Actionable.module.css +1 -1
  10. package/dist/components/Actionable/Actionable.types.d.ts +1 -0
  11. package/dist/components/Autocomplete/Autocomplete.js +12 -6
  12. package/dist/components/Button/Button.js +1 -1
  13. package/dist/components/Button/Button.module.css +1 -1
  14. package/dist/components/Card/Card.d.ts +1 -1
  15. package/dist/components/Card/Card.module.css +1 -1
  16. package/dist/components/Card/tests/Card.stories.d.ts +1 -1
  17. package/dist/components/Checkbox/Checkbox.module.css +1 -1
  18. package/dist/components/DropdownMenu/DropdownMenu.types.d.ts +1 -1
  19. package/dist/components/FormControl/FormControl.context.d.ts +2 -1
  20. package/dist/components/Grid/Grid.d.ts +6 -0
  21. package/dist/components/Grid/Grid.js +46 -0
  22. package/dist/components/Grid/Grid.module.css +1 -0
  23. package/dist/components/Grid/Grid.types.d.ts +31 -0
  24. package/dist/components/Grid/Grid.types.js +1 -0
  25. package/dist/components/Grid/index.d.ts +2 -0
  26. package/dist/components/Grid/index.js +1 -0
  27. package/dist/components/Grid/tests/Grid.stories.d.ts +18 -0
  28. package/dist/components/Grid/tests/Grid.stories.js +170 -0
  29. package/dist/components/Icon/Icon.module.css +1 -1
  30. package/dist/components/Link/Link.d.ts +1 -1
  31. package/dist/components/Loader/Loader.module.css +1 -1
  32. package/dist/components/Loader/Loader.types.d.ts +1 -1
  33. package/dist/components/Loader/tests/Loader.stories.js +5 -3
  34. package/dist/components/Modal/Modal.js +1 -1
  35. package/dist/components/Modal/Modal.module.css +1 -1
  36. package/dist/components/Overlay/Overlay.js +1 -1
  37. package/dist/components/Overlay/tests/Overlay.stories.js +1 -1
  38. package/dist/components/Popover/Popover.js +2 -4
  39. package/dist/components/Popover/Popover.types.d.ts +1 -1
  40. package/dist/components/Radio/Radio.module.css +1 -1
  41. package/dist/components/Resizable/Resizable.module.css +1 -1
  42. package/dist/components/ScrollArea/ScrollArea.js +1 -1
  43. package/dist/components/Select/Select.js +1 -1
  44. package/dist/components/Slider/Slider.module.css +1 -1
  45. package/dist/components/Slider/SliderControlled.js +2 -1
  46. package/dist/components/Switch/Switch.module.css +1 -1
  47. package/dist/components/Tabs/Tabs.d.ts +1 -1
  48. package/dist/components/Tabs/Tabs.module.css +1 -1
  49. package/dist/components/Tabs/TabsItem.d.ts +1 -1
  50. package/dist/components/Tabs/TabsItem.js +2 -3
  51. package/dist/components/Tabs/TabsList.js +1 -1
  52. package/dist/components/Tabs/tests/Tabs.stories.d.ts +15 -13
  53. package/dist/components/Tabs/tests/Tabs.stories.js +71 -8
  54. package/dist/components/Toast/ToastContainer.js +1 -2
  55. package/dist/components/Toast/ToastRegion.js +1 -1
  56. package/dist/components/Tooltip/Tooltip.js +1 -1
  57. package/dist/components/View/View.js +7 -3
  58. package/dist/components/View/View.module.css +1 -1
  59. package/dist/components/View/View.types.d.ts +2 -2
  60. package/dist/components/_private/Expandable/Expandable.js +9 -5
  61. package/dist/components/_private/Flyout/Flyout.module.css +1 -1
  62. package/dist/components/_private/Flyout/Flyout.types.d.ts +12 -3
  63. package/dist/components/_private/Flyout/FlyoutContent.js +1 -1
  64. package/dist/components/_private/Flyout/FlyoutControlled.js +34 -20
  65. package/dist/components/_private/Flyout/tests/Flyout.stories.d.ts +1 -0
  66. package/dist/components/_private/Flyout/tests/Flyout.stories.js +28 -18
  67. package/dist/components/_private/Flyout/useFlyout.d.ts +2 -1
  68. package/dist/components/_private/Flyout/useFlyout.js +46 -57
  69. package/dist/components/_private/Flyout/utilities/calculatePosition.js +16 -11
  70. package/dist/components/_private/Flyout/utilities/cooldown.d.ts +1 -1
  71. package/dist/components/_private/Flyout/utilities/cooldown.js +17 -5
  72. package/dist/components/_private/Flyout/utilities/getPositionFallbacks.d.ts +3 -0
  73. package/dist/components/_private/Flyout/utilities/getPositionFallbacks.js +39 -0
  74. package/dist/config/tailwind.d.ts +1 -1
  75. package/dist/hooks/_private/useOnClickOutside.js +3 -2
  76. package/dist/hooks/_private/useSingletonHotkeys.js +15 -12
  77. package/dist/hooks/_private/useSingletonKeyboardMode.js +1 -1
  78. package/dist/hooks/tests/useHotkeys.stories.js +3 -0
  79. package/dist/hooks/useDrag.js +2 -1
  80. package/dist/hooks/useScrollLock.js +12 -39
  81. package/dist/index.d.ts +2 -0
  82. package/dist/index.js +1 -0
  83. package/dist/styles/align/align.module.css +1 -0
  84. package/dist/styles/align/index.d.ts +3 -0
  85. package/dist/styles/align/index.js +10 -0
  86. package/dist/styles/justify/index.d.ts +3 -0
  87. package/dist/styles/justify/index.js +10 -0
  88. package/dist/styles/justify/justify.module.css +1 -0
  89. package/dist/styles/types.d.ts +2 -0
  90. package/dist/tests/ShadowDOM.stories.d.ts +6 -0
  91. package/dist/tests/ShadowDOM.stories.js +110 -0
  92. package/dist/themes/_generator/definitions/reshaped.js +1 -1
  93. package/dist/themes/_generator/tests/themes.stories.js +1 -1
  94. package/dist/themes/reshaped/theme.css +1 -1
  95. package/dist/utilities/a11y/TrapFocus.d.ts +1 -1
  96. package/dist/utilities/a11y/TrapFocus.js +14 -5
  97. package/dist/utilities/a11y/focus.d.ts +1 -1
  98. package/dist/utilities/a11y/focus.js +10 -5
  99. package/dist/utilities/a11y/index.d.ts +4 -0
  100. package/dist/utilities/a11y/index.js +3 -0
  101. package/dist/utilities/dom/flyout.d.ts +1 -0
  102. package/dist/utilities/dom/flyout.js +19 -0
  103. package/dist/utilities/dom/index.d.ts +3 -0
  104. package/dist/utilities/dom/index.js +3 -0
  105. package/dist/utilities/dom/shadowDom.d.ts +1 -0
  106. package/dist/utilities/dom/shadowDom.js +4 -0
  107. package/dist/utilities/dom/userSelect.d.ts +2 -0
  108. package/dist/utilities/dom/userSelect.js +6 -0
  109. package/dist/utilities/platform.d.ts +1 -0
  110. package/dist/utilities/platform.js +15 -0
  111. package/dist/utilities/scroll/disable.d.ts +7 -0
  112. package/dist/utilities/scroll/disable.js +13 -0
  113. package/dist/utilities/scroll/helpers.d.ts +1 -0
  114. package/dist/utilities/scroll/helpers.js +17 -0
  115. package/dist/utilities/scroll/index.d.ts +2 -0
  116. package/dist/utilities/scroll/index.js +2 -0
  117. package/dist/utilities/scroll/lock.d.ts +2 -0
  118. package/dist/utilities/scroll/lock.js +24 -0
  119. package/dist/utilities/scroll/lockSafari.d.ts +2 -0
  120. package/dist/utilities/scroll/lockSafari.js +31 -0
  121. package/dist/utilities/scroll/lockStandard.d.ts +2 -0
  122. package/dist/utilities/scroll/lockStandard.js +19 -0
  123. package/package.json +31 -29
  124. package/dist/utilities/dom.d.ts +0 -5
  125. package/dist/utilities/dom.js +0 -27
@@ -6,6 +6,7 @@ import View from "../../../View/index.js";
6
6
  import Theme from "../../../Theme/index.js";
7
7
  import Button from "../../../Button/index.js";
8
8
  import Flyout from "../index.js";
9
+ import TextField from "../../../TextField/index.js";
9
10
  export default { title: "Utilities/Internal/Flyout" };
10
11
  const Demo = (props) => {
11
12
  const { position = "bottom-start", children, ...rest } = props;
@@ -17,7 +18,7 @@ const Demo = (props) => {
17
18
  <div style={{
18
19
  background: "var(--rs-color-background-elevation-overlay)",
19
20
  padding: "var(--rs-unit-x4)",
20
- height: 100,
21
+ height: 150,
21
22
  width: 160,
22
23
  borderRadius: "var(--rs-radius-medium)",
23
24
  border: "1px solid var(--rs-color-border-neutral-faded)",
@@ -120,6 +121,19 @@ export const disableFlags = () => (<Example>
120
121
  <Demo disableHideAnimation>Content</Demo>
121
122
  </Example.Item>
122
123
  </Example>);
124
+ export const initialFocus = () => {
125
+ const initialFocusRef = React.useRef(null);
126
+ return (<Example>
127
+ <Example.Item title="focuses input on open">
128
+ <Demo initialFocusRef={initialFocusRef}>
129
+ <View gap={4}>
130
+ <Button onClick={() => { }}>Click me</Button>
131
+ <TextField name="foo" inputAttributes={{ ref: initialFocusRef }}/>
132
+ </View>
133
+ </Demo>
134
+ </Example.Item>
135
+ </Example>);
136
+ };
123
137
  class CustomElement extends window.HTMLElement {
124
138
  constructor() {
125
139
  super();
@@ -199,36 +213,32 @@ export const testInsideFixed = () => (<Example>
199
213
  </Example.Item>
200
214
  </Example>);
201
215
  export const testDynamicBounds = () => {
202
- const [left, setLeft] = React.useState("50%");
216
+ const [left, setLeft] = React.useState(50);
217
+ const [top, setTop] = React.useState(50);
203
218
  const [size, setSize] = React.useState("medium");
204
219
  const flyoutRef = React.useRef();
205
220
  React.useEffect(() => {
206
221
  flyoutRef.current?.updatePosition();
207
- }, [left]);
222
+ }, [left, top]);
208
223
  return (<View gap={4}>
209
224
  <View direction="row" gap={2}>
225
+ <Button onClick={() => setLeft((prev) => prev - 10)}>Left</Button>
226
+ <Button onClick={() => setLeft((prev) => prev + 10)}>Right</Button>
227
+ <Button onClick={() => setTop((prev) => prev - 10)}>Up</Button>
228
+ <Button onClick={() => setTop((prev) => prev + 10)}>Down</Button>
210
229
  <Button onClick={() => {
211
- setLeft("20%");
212
- }}>
213
- Left
214
- </Button>
215
- <Button onClick={() => {
216
- setLeft("50%");
230
+ setLeft(50);
231
+ setTop(50);
217
232
  }}>
218
233
  Center
219
234
  </Button>
220
- <Button onClick={() => {
221
- setLeft("70%");
222
- }}>
223
- Right
224
- </Button>
225
235
  <Button onClick={() => setSize("large")}>Large button</Button>
226
236
  <Button onClick={() => setSize("medium")}>Small button</Button>
227
237
  </View>
228
238
  <View height={100}>
229
- <Flyout position="bottom" active instanceRef={flyoutRef}>
239
+ <Flyout position="bottom" instanceRef={flyoutRef} disableCloseOnOutsideClick fallbackPositions={["top", "bottom"]}>
230
240
  <Flyout.Trigger>
231
- {(attributes) => (<div style={{ position: "absolute", left, top: "50%" }}>
241
+ {(attributes) => (<div style={{ position: "absolute", left: `${left}%`, top: `${top}%` }}>
232
242
  <Button color="primary" attributes={attributes} size={size}>
233
243
  Open
234
244
  </Button>
@@ -239,12 +249,12 @@ export const testDynamicBounds = () => {
239
249
  background: "var(--rs-color-background-elevation-overlay)",
240
250
  padding: "var(--rs-unit-x4)",
241
251
  height: 100,
242
- width: 160,
252
+ minWidth: 160,
243
253
  borderRadius: "var(--rs-radius-medium)",
244
254
  border: "1px solid var(--rs-color-border-neutral-faded)",
245
255
  boxSizing: "border-box",
246
256
  }}>
247
- {"Content"}
257
+ Content
248
258
  </div>
249
259
  </Flyout.Content>
250
260
  </Flyout>
@@ -8,13 +8,14 @@ type PassedFlyoutOptions = {
8
8
  width?: T.Width;
9
9
  position?: T.Position;
10
10
  defaultActive?: boolean;
11
- forcePosition?: boolean;
11
+ fallbackPositions?: T.Position[];
12
12
  container?: HTMLElement | null;
13
13
  };
14
14
  type UseFlyout = (args: PassedFlyoutOptions & {
15
15
  triggerElRef: ElementRef;
16
16
  flyoutElRef: ElementRef;
17
17
  triggerBoundsRef: React.RefObject<DOMRect | undefined>;
18
+ contentGap?: number;
18
19
  }) => Pick<T.State, "styles" | "position" | "status"> & {
19
20
  updatePosition: (options?: {
20
21
  sync?: boolean;
@@ -1,25 +1,8 @@
1
1
  import React from "react";
2
2
  import useRTL from "../../../hooks/useRTL.js";
3
- import { getClosestFlyoutTarget } from "../../../utilities/dom.js";
3
+ import { getClosestFlyoutTarget, getShadowRoot } from "../../../utilities/dom/index.js";
4
4
  import calculatePosition from "./utilities/calculatePosition.js";
5
- const topPos = ["top-start", "top", "top-end"];
6
- const bottomPos = ["bottom-start", "bottom", "bottom-end"];
7
- const startPos = ["start", "start-bottom", "start-top"];
8
- const endPos = ["end", "end-bottom", "end-top"];
9
- const order = {
10
- top: [...topPos, ...bottomPos, ...endPos, ...startPos],
11
- bottom: [...bottomPos, ...topPos, ...endPos, ...startPos],
12
- start: [...startPos, ...endPos, ...topPos, ...bottomPos],
13
- end: [...endPos, ...startPos, ...topPos, ...bottomPos],
14
- };
15
- /**
16
- * Get an order of positions to try to fit popover on the screen based on its starting position
17
- */
18
- const getPositionOrder = (position) => {
19
- const types = ["top", "bottom", "start", "end"];
20
- const type = types.find((type) => position.startsWith(type)) || "bottom";
21
- return order[type];
22
- };
5
+ import getPositionFallbacks from "./utilities/getPositionFallbacks.js";
23
6
  /**
24
7
  * Check if element visually fits on the screen
25
8
  */
@@ -60,12 +43,13 @@ const resetStyles = {
60
43
  * Set position of the target element to fit on the screen
61
44
  */
62
45
  const flyout = (args) => {
63
- const { triggerEl, flyoutEl, triggerBounds: passedTriggerBounds, ...options } = args;
64
- const { position, forcePosition, width, container } = options;
46
+ const { triggerEl, flyoutEl, triggerBounds: passedTriggerBounds, contentGap = 0, ...options } = args;
47
+ const { position, fallbackPositions, width, container, lastUsedFallback, onFallback } = options;
65
48
  const targetClone = flyoutEl.cloneNode(true);
66
49
  const triggerBounds = passedTriggerBounds || triggerEl.getBoundingClientRect();
50
+ const contentGapModifier = parseInt(getComputedStyle(flyoutEl).getPropertyValue("--rs-unit-x1"));
67
51
  // Reset all styles applied on the previous hook execution
68
- targetClone.style = "";
52
+ targetClone.style.cssText = "";
69
53
  Object.keys(resetStyles).forEach((key) => {
70
54
  const value = resetStyles[key];
71
55
  targetClone.style[key] = value.toString();
@@ -78,8 +62,7 @@ const flyout = (args) => {
78
62
  targetClone.style.width = width;
79
63
  }
80
64
  }
81
- const rootNode = triggerEl?.getRootNode();
82
- const shadowRoot = rootNode instanceof ShadowRoot ? rootNode : null;
65
+ const shadowRoot = getShadowRoot(triggerEl);
83
66
  // Insert inside shadow root if possible to make sure styles are applied correctly
84
67
  (shadowRoot || document.body).appendChild(targetClone);
85
68
  const flyoutBounds = targetClone.getBoundingClientRect();
@@ -89,37 +72,30 @@ const flyout = (args) => {
89
72
  top: containerBounds.top + document.documentElement.scrollTop - containerParent.scrollTop,
90
73
  left: containerBounds.left + document.documentElement.scrollLeft - containerParent.scrollLeft,
91
74
  };
92
- let calculated = calculatePosition({ triggerBounds, flyoutBounds, scopeOffset, ...options });
93
- if (!fullyVisible(calculated) && !forcePosition) {
94
- const order = getPositionOrder(position);
95
- const mobileOrder = order.filter((position) => position === "top" || position === "bottom");
96
- const test = (testOrder, extraOptions = {}) => {
97
- const { fullWidth } = extraOptions;
98
- testOrder.some((currentPosition) => {
99
- const calculateOptions = {
100
- ...options,
101
- width: fullWidth ? "full" : options.width,
102
- position: currentPosition,
103
- };
104
- const tested = calculatePosition({
105
- triggerBounds,
106
- flyoutBounds,
107
- scopeOffset,
108
- ...calculateOptions,
109
- });
110
- if (fullyVisible(tested)) {
111
- calculated = tested;
112
- return true;
113
- }
114
- return false;
115
- });
116
- };
117
- test(order);
118
- if (!fullyVisible(calculated)) {
119
- test(mobileOrder, { fullWidth: true });
75
+ let calculated = null;
76
+ const testOrder = getPositionFallbacks(position, fallbackPositions);
77
+ testOrder.some((currentPosition, index) => {
78
+ const tested = calculatePosition({
79
+ ...options,
80
+ triggerBounds,
81
+ flyoutBounds,
82
+ scopeOffset,
83
+ position: currentPosition,
84
+ contentGap: contentGap * contentGapModifier,
85
+ });
86
+ const visible = fullyVisible(tested);
87
+ const validPosition = visible || fallbackPositions?.length === 0;
88
+ // Saving first try in case non of the options work
89
+ if (validPosition || lastUsedFallback === currentPosition) {
90
+ calculated = tested;
91
+ onFallback(currentPosition);
120
92
  }
93
+ return validPosition;
94
+ });
95
+ if (!calculated) {
96
+ throw new Error(`Reshaped: Can't calculate styles for the ${position} position`);
121
97
  }
122
- targetClone.parentNode.removeChild(targetClone);
98
+ targetClone.parentNode?.removeChild(targetClone);
123
99
  return calculated;
124
100
  };
125
101
  const flyoutReducer = (state, action) => {
@@ -157,8 +133,13 @@ const flyoutReducer = (state, action) => {
157
133
  }
158
134
  };
159
135
  const useFlyout = (args) => {
160
- const { triggerElRef, flyoutElRef, triggerBoundsRef, ...options } = args;
161
- const { position: defaultPosition = "bottom", forcePosition, width, container } = options;
136
+ const { triggerElRef, flyoutElRef, triggerBoundsRef, contentGap, ...options } = args;
137
+ const { position: defaultPosition = "bottom", fallbackPositions, width, container } = options;
138
+ const lastUsedFallbackRef = React.useRef(defaultPosition);
139
+ // Memo the array internally to avoid new arrays triggering useCallback
140
+ const cachedFallbackPositions = React.useMemo(() => fallbackPositions,
141
+ // eslint-disable-next-line react-hooks/exhaustive-deps
142
+ [fallbackPositions?.join(" ")]);
162
143
  const [isRTL] = useRTL();
163
144
  const [state, dispatch] = React.useReducer(flyoutReducer, {
164
145
  position: defaultPosition,
@@ -177,6 +158,9 @@ const useFlyout = (args) => {
177
158
  const remove = React.useCallback(() => {
178
159
  dispatch({ type: "remove" });
179
160
  }, []);
161
+ const handleFallback = React.useCallback((position) => {
162
+ lastUsedFallbackRef.current = position;
163
+ }, []);
180
164
  const updatePosition = React.useCallback((options) => {
181
165
  if (!triggerElRef.current || !flyoutElRef.current)
182
166
  return;
@@ -186,21 +170,26 @@ const useFlyout = (args) => {
186
170
  triggerBounds: triggerBoundsRef.current,
187
171
  width,
188
172
  position: defaultPosition,
189
- forcePosition,
173
+ fallbackPositions: cachedFallbackPositions,
174
+ lastUsedFallback: lastUsedFallbackRef.current,
175
+ onFallback: handleFallback,
190
176
  rtl: isRTL,
191
177
  container,
178
+ contentGap,
192
179
  });
193
180
  if (nextFlyoutData)
194
181
  dispatch({ type: "position", payload: { ...nextFlyoutData, sync: options?.sync } });
195
182
  }, [
196
183
  container,
197
184
  defaultPosition,
198
- forcePosition,
185
+ cachedFallbackPositions,
199
186
  isRTL,
200
187
  flyoutElRef,
201
188
  triggerElRef,
202
189
  triggerBoundsRef,
203
190
  width,
191
+ contentGap,
192
+ handleFallback,
204
193
  ]);
205
194
  React.useEffect(() => {
206
195
  if (state.status === "rendered")
@@ -16,24 +16,29 @@ const centerBySize = (originSize, targetSize) => {
16
16
  * Calculate styles for the current position
17
17
  */
18
18
  const calculatePosition = (args) => {
19
- const { triggerBounds, flyoutBounds, scopeOffset, position: passedPosition, rtl, width } = args;
19
+ const { triggerBounds, flyoutBounds, scopeOffset, position: passedPosition, rtl, width, contentGap = 0, } = args;
20
+ const isFullWidth = width === "full" || width === "100%";
20
21
  let left = 0;
21
22
  let top = 0;
22
23
  let position = passedPosition;
23
24
  if (rtl)
24
25
  position = getRTLPosition(position);
25
- if (width === "full" || width === "trigger") {
26
+ if (isFullWidth || width === "trigger") {
26
27
  position = position.includes("top") ? "top" : "bottom";
27
28
  }
29
+ const isHorizontalPosition = position.match(/^(start|end)/);
30
+ const isVerticalPosition = position.match(/^(top|bottom)/);
31
+ const flyoutWidth = flyoutBounds.width + (isHorizontalPosition ? contentGap : 0);
32
+ const flyoutHeight = flyoutBounds.height + (isVerticalPosition ? contentGap : 0);
28
33
  switch (position) {
29
34
  case "bottom":
30
35
  case "top":
31
- left = centerBySize(triggerBounds.width, flyoutBounds.width) + triggerBounds.left;
36
+ left = centerBySize(triggerBounds.width, flyoutWidth) + triggerBounds.left;
32
37
  break;
33
38
  case "start":
34
39
  case "start-top":
35
40
  case "start-bottom":
36
- left = triggerBounds.left - flyoutBounds.width;
41
+ left = triggerBounds.left - flyoutWidth;
37
42
  break;
38
43
  case "end":
39
44
  case "end-top":
@@ -46,7 +51,7 @@ const calculatePosition = (args) => {
46
51
  break;
47
52
  case "top-end":
48
53
  case "bottom-end":
49
- left = triggerBounds.right - flyoutBounds.width;
54
+ left = triggerBounds.right - flyoutWidth;
50
55
  break;
51
56
  default:
52
57
  break;
@@ -55,7 +60,7 @@ const calculatePosition = (args) => {
55
60
  case "top":
56
61
  case "top-start":
57
62
  case "top-end":
58
- top = triggerBounds.top - flyoutBounds.height;
63
+ top = triggerBounds.top - flyoutHeight;
59
64
  break;
60
65
  case "bottom":
61
66
  case "bottom-start":
@@ -64,7 +69,7 @@ const calculatePosition = (args) => {
64
69
  break;
65
70
  case "start":
66
71
  case "end":
67
- top = centerBySize(triggerBounds.height, flyoutBounds.height) + triggerBounds.top;
72
+ top = centerBySize(triggerBounds.height, flyoutHeight) + triggerBounds.top;
68
73
  break;
69
74
  case "start-top":
70
75
  case "end-top":
@@ -72,7 +77,7 @@ const calculatePosition = (args) => {
72
77
  break;
73
78
  case "start-bottom":
74
79
  case "end-bottom":
75
- top = triggerBounds.bottom - flyoutBounds.height;
80
+ top = triggerBounds.bottom - flyoutHeight;
76
81
  break;
77
82
  default:
78
83
  break;
@@ -82,9 +87,9 @@ const calculatePosition = (args) => {
82
87
  }
83
88
  top = Math.round(top + (window.scrollY || 0) - scopeOffset.top);
84
89
  left = Math.round(left + (window.scrollX || 0) - scopeOffset.left);
85
- let widthStyle = Math.ceil(flyoutBounds.width);
86
- const height = Math.ceil(flyoutBounds.height);
87
- if (width === "full") {
90
+ let widthStyle = Math.ceil(flyoutWidth);
91
+ const height = Math.ceil(flyoutHeight);
92
+ if (isFullWidth) {
88
93
  left = SCREEN_OFFSET;
89
94
  widthStyle = window.innerWidth - SCREEN_OFFSET * 2;
90
95
  }
@@ -1,5 +1,5 @@
1
1
  declare class Cooldown {
2
- status: "cold" | "warm" | "cooling";
2
+ status: "warming" | "warm" | "cooling" | "cold";
3
3
  timer?: ReturnType<typeof setTimeout>;
4
4
  warm: () => void;
5
5
  cool: () => void;
@@ -1,18 +1,30 @@
1
+ import * as timeouts from "../Flyout.constants.js";
1
2
  class Cooldown {
2
3
  status = "cold";
3
4
  timer;
4
5
  warm = () => {
5
6
  clearTimeout(this.timer);
6
- this.status = "warm";
7
+ if (this.status === "cooling") {
8
+ this.status = "warm";
9
+ return;
10
+ }
11
+ this.status = "warming";
12
+ this.timer = setTimeout(() => {
13
+ this.status = "warm";
14
+ this.timer = undefined;
15
+ }, timeouts.mouseEnterShort);
7
16
  };
8
17
  cool = () => {
18
+ clearTimeout(this.timer);
19
+ if (this.status === "warming") {
20
+ this.status = "cold";
21
+ return;
22
+ }
9
23
  this.status = "cooling";
10
- const currentTimer = setTimeout(() => {
24
+ this.timer = setTimeout(() => {
11
25
  this.status = "cold";
12
- if (currentTimer === this.timer)
13
- this.timer = undefined;
26
+ this.timer = undefined;
14
27
  }, 500);
15
- this.timer = currentTimer;
16
28
  };
17
29
  }
18
30
  export default new Cooldown();
@@ -0,0 +1,3 @@
1
+ import type * as T from "../Flyout.types";
2
+ declare const getPositionFallbacks: (position: T.Position, availableFallbacks?: T.Position[]) => T.Position[];
3
+ export default getPositionFallbacks;
@@ -0,0 +1,39 @@
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
+ // Get an order of positions to try to fit flyout on the screen based on its starting position
16
+ const getPositionFallbacks = (position, availableFallbacks) => {
17
+ const result = [position];
18
+ const chunks = position.split("-");
19
+ const [firstChunk] = chunks;
20
+ const passedPositionOrder = positions[firstChunk];
21
+ const startingFallbackIndex = passedPositionOrder.indexOf(position);
22
+ const fallbackIndexOrder = [startingFallbackIndex];
23
+ passedPositionOrder.forEach((_, index) => {
24
+ if (index === startingFallbackIndex)
25
+ return;
26
+ fallbackIndexOrder.push(index);
27
+ });
28
+ [firstChunk, ...fallbackOrder[firstChunk]].forEach((fallbackSide) => {
29
+ const fallbackOrder = positions[fallbackSide];
30
+ fallbackIndexOrder.forEach((index) => {
31
+ const position = fallbackOrder[index];
32
+ if (availableFallbacks?.indexOf(position) === -1)
33
+ return;
34
+ result.push(position);
35
+ });
36
+ });
37
+ return result;
38
+ };
39
+ export default getPositionFallbacks;
@@ -1,2 +1,2 @@
1
1
  import type { ThemeDefinition } from "../themes/_generator/tokens/types";
2
- export declare const getTheme: (theme?: ThemeDefinition) => Record<"borderRadius" | "backgroundColor" | "borderColor" | "spacing" | "boxShadow" | "textColor" | "colors" | "screens", Record<string, string>>;
2
+ export declare const getTheme: (theme?: ThemeDefinition) => Record<"borderRadius" | "backgroundColor" | "borderColor" | "boxShadow" | "textColor" | "colors" | "spacing" | "screens", Record<string, string>>;
@@ -7,10 +7,11 @@ const useOnClickOutside = (refs, handler) => {
7
7
  return;
8
8
  const handleClick = (event) => {
9
9
  let isInside = false;
10
+ const clickedEl = event.composedPath()[0];
10
11
  refs.forEach((elRef) => {
11
12
  if (!elRef.current ||
12
- elRef.current === event.target ||
13
- elRef.current.contains(event.target)) {
13
+ elRef.current === clickedEl ||
14
+ elRef.current.contains(clickedEl)) {
14
15
  isInside = true;
15
16
  }
16
17
  });
@@ -70,17 +70,20 @@ export class HotkeyStore {
70
70
  const hotkeyData = this.hotkeyMap[pressedId];
71
71
  /**
72
72
  * Support for `mod` that represents both Mac and Win keyboards
73
+ * We create the hotkeyId again to sort the mod key correctly
73
74
  */
74
- const hotkeyControlModData = pressedFormattedKeys.includes("control") &&
75
- this.hotkeyMap[pressedId.replace("control", "mod")];
76
- const hotkeyMetaModData = pressedFormattedKeys.includes("meta") && this.hotkeyMap[pressedId.replace("meta", "mod")];
75
+ const controlToModPressedId = getHotkeyId(pressedId.replace("control", "mod"));
76
+ const metaToModPressedId = getHotkeyId(pressedId.replace("meta", "mod"));
77
+ const hotkeyControlModData = pressedFormattedKeys.includes("control") && this.hotkeyMap[controlToModPressedId];
78
+ const hotkeyMetaModData = pressedFormattedKeys.includes("meta") && this.hotkeyMap[metaToModPressedId];
77
79
  [hotkeyData, hotkeyControlModData, hotkeyMetaModData].forEach((hotkeyData) => {
78
80
  if (!hotkeyData)
79
81
  return;
80
82
  if (hotkeyData?.size) {
81
83
  hotkeyData.forEach((data) => {
84
+ const eventTarget = e.composedPath()[0];
82
85
  if (data.ref?.current &&
83
- !(e.target === data.ref.current || data.ref.current.contains(e.target))) {
86
+ !(eventTarget === data.ref.current || data.ref.current.contains(eventTarget))) {
84
87
  return;
85
88
  }
86
89
  const resolvedEvent = pressedMap[pressedId];
@@ -146,14 +149,6 @@ export const SingletonHotkeysProvider = (props) => {
146
149
  return false;
147
150
  return true;
148
151
  };
149
- const addHotkeys = React.useCallback((hotkeys, ref, options = {}) => {
150
- setHooksCount((prev) => prev + 1);
151
- globalHotkeyStore.bindHotkeys(hotkeys, ref, options);
152
- return () => {
153
- setHooksCount((prev) => prev - 1);
154
- globalHotkeyStore.unbindHotkeys(hotkeys);
155
- };
156
- }, []);
157
152
  const handleWindowKeyDown = React.useCallback((e) => {
158
153
  // Browsers trigger keyboard event without passing e.key when you click on autocomplete
159
154
  if (!e.key)
@@ -166,6 +161,14 @@ export const SingletonHotkeysProvider = (props) => {
166
161
  return;
167
162
  removePressedKey(e);
168
163
  }, [removePressedKey]);
164
+ const addHotkeys = React.useCallback((hotkeys, ref, options = {}) => {
165
+ setHooksCount((prev) => prev + 1);
166
+ globalHotkeyStore.bindHotkeys(hotkeys, ref, options);
167
+ return () => {
168
+ setHooksCount((prev) => prev - 1);
169
+ globalHotkeyStore.unbindHotkeys(hotkeys);
170
+ };
171
+ }, []);
169
172
  React.useEffect(() => {
170
173
  window.addEventListener("keydown", handleWindowKeyDown);
171
174
  window.addEventListener("keyup", handleWindowKeyUp);
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { enableKeyboardMode, disableKeyboardMode } from "../../utilities/a11y/keyboardMode.js";
2
+ import { enableKeyboardMode, disableKeyboardMode } from "../../utilities/a11y/index.js";
3
3
  const useSingletonKeyboardMode = () => {
4
4
  React.useEffect(() => {
5
5
  const handleKeyDown = (e) => {
@@ -6,6 +6,9 @@ function Example() {
6
6
  "shift + b + n": () => console.log("pressed"),
7
7
  "c + v": () => console.log(111),
8
8
  "Meta + v": () => console.log(222),
9
+ "control + enter": () => console.log("control + enter"),
10
+ "meta + enter": () => console.log("meta + enter"),
11
+ "mod + enter": () => console.log("mod + enter"),
9
12
  "mod + ArrowRight": () => console.log("right"),
10
13
  "mod + ArrowUp": () => console.log("top"),
11
14
  "shift + ArrowRight": () => console.log("right"),
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import React from "react";
3
- import { disableUserSelect, enableUserSelect, disableScroll, enableScroll } from "../utilities/dom.js";
3
+ import { disableUserSelect, enableUserSelect } from "../utilities/dom/index.js";
4
+ import { disableScroll, enableScroll } from "../utilities/scroll/index.js";
4
5
  import useToggle from "./useToggle.js";
5
6
  import useHotkeys from "./useHotkeys.js";
6
7
  import * as keys from "../constants/keys.js";
@@ -1,45 +1,18 @@
1
1
  "use client";
2
2
  import React from "react";
3
- const getScrollbarWidth = (() => {
4
- let scrollbarWidth;
5
- return () => {
6
- if (scrollbarWidth)
7
- return scrollbarWidth;
8
- const scrollDiv = document.createElement("div");
9
- scrollDiv.style.position = "absolute";
10
- scrollDiv.style.top = "-9999px";
11
- scrollDiv.style.width = "50px";
12
- scrollDiv.style.height = "50px";
13
- scrollDiv.style.overflow = "scroll";
14
- document.body.appendChild(scrollDiv);
15
- scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth;
16
- document.body.removeChild(scrollDiv);
17
- return scrollbarWidth;
18
- };
19
- })();
3
+ import { lockScroll, unlockScroll } from "../utilities/scroll/index.js";
20
4
  const useScrollLock = () => {
21
5
  const [locked, setLocked] = React.useState(false);
22
- const overflowStyleRef = React.useRef();
23
- const isOverflowingRef = React.useRef(false);
24
- const lockScroll = React.useCallback(() => {
25
- const targetEl = document.body;
26
- const rect = targetEl.getBoundingClientRect();
27
- isOverflowingRef.current = rect.left + rect.right < window.innerWidth;
28
- overflowStyleRef.current = targetEl.style.overflow;
29
- targetEl.style.overflow = "hidden";
30
- if (isOverflowingRef.current) {
31
- const scrollBarWidth = getScrollbarWidth();
32
- targetEl.style.paddingRight = `${scrollBarWidth}px`;
33
- }
34
- setLocked(true);
35
- }, [setLocked, isOverflowingRef, overflowStyleRef]);
36
- const unlockScroll = React.useCallback(() => {
37
- const targetEl = document.body;
38
- targetEl.style.overflow = overflowStyleRef.current || "";
39
- if (isOverflowingRef.current)
40
- targetEl.style.paddingRight = "";
41
- setLocked(false);
42
- }, [setLocked, isOverflowingRef, overflowStyleRef]);
43
- return { scrollLocked: locked, lockScroll, unlockScroll };
6
+ const handleLockScroll = React.useCallback(() => {
7
+ lockScroll(() => setLocked(true));
8
+ }, []);
9
+ const handleUnlockScroll = React.useCallback(() => {
10
+ unlockScroll(() => setLocked(false));
11
+ }, []);
12
+ return React.useMemo(() => ({
13
+ scrollLocked: locked,
14
+ lockScroll: handleLockScroll,
15
+ unlockScroll: handleUnlockScroll,
16
+ }), [locked, handleLockScroll, handleUnlockScroll]);
44
17
  };
45
18
  export default useScrollLock;
package/dist/index.d.ts CHANGED
@@ -41,6 +41,8 @@ export { default as FileUpload } from "./components/FileUpload";
41
41
  export type { FileUploadProps } from "./components/FileUpload";
42
42
  export { default as FormControl } from "./components/FormControl";
43
43
  export type { FormControlProps } from "./components/FormControl";
44
+ export { default as Grid } from "./components/Grid";
45
+ export type { GridProps, GridItemProps } from "./components/Grid";
44
46
  export { default as Hidden } from "./components/Hidden";
45
47
  export type { HiddenProps } from "./components/Hidden";
46
48
  export { default as HiddenVisually } from "./components/HiddenVisually";
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ export { default as Divider } from "./components/Divider/index.js";
21
21
  export { default as DropdownMenu } from "./components/DropdownMenu/index.js";
22
22
  export { default as FileUpload } from "./components/FileUpload/index.js";
23
23
  export { default as FormControl } from "./components/FormControl/index.js";
24
+ export { default as Grid } from "./components/Grid/index.js";
24
25
  export { default as Hidden } from "./components/Hidden/index.js";
25
26
  export { default as HiddenVisually } from "./components/HiddenVisually/index.js";
26
27
  export { default as Hotkey } from "./components/Hotkey/index.js";
@@ -0,0 +1 @@
1
+ .--align-start{align-items:flex-start!important}.--align-end{align-items:flex-end!important}.--align-center{align-items:center!important}.--align-stretch{align-items:stretch!important}.--align-baseline{align-items:baseline!important}@media (--rs-viewport-m ){.--align-start--m{align-items:flex-start!important}.--align-end--m{align-items:flex-end!important}.--align-center--m{align-items:center!important}.--align-stretch--m{align-items:stretch!important}.--align-baseline--m{align-items:baseline!important}}@media (--rs-viewport-l ){.--align-start--l{align-items:flex-start!important}.--align-end--l{align-items:flex-end!important}.--align-center--l{align-items:center!important}.--align-stretch--l{align-items:stretch!important}.--align-baseline--l{align-items:baseline!important}}@media (--rs-viewport-xl ){.--align-start--xl{align-items:flex-start!important}.--align-end--xl{align-items:flex-end!important}.--align-center--xl{align-items:center!important}.--align-stretch--xl{align-items:stretch!important}.--align-baseline--xl{align-items:baseline!important}}
@@ -0,0 +1,3 @@
1
+ import * as T from "../types";
2
+ declare const getAlignStyles: T.StaticStyleUtility<T.Align>;
3
+ export default getAlignStyles;