@reshaped/headless 3.10.0-canary.10

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 (60) hide show
  1. package/LICENSE.md +21 -0
  2. package/dist/components/Actionable/Actionable.d.ts +4 -0
  3. package/dist/components/Actionable/Actionable.js +72 -0
  4. package/dist/components/Actionable/Actionable.types.d.ts +35 -0
  5. package/dist/components/Actionable/Actionable.types.js +1 -0
  6. package/dist/components/Actionable/index.d.ts +2 -0
  7. package/dist/components/Actionable/index.js +1 -0
  8. package/dist/components/Reshaped/Reshaped.d.ts +4 -0
  9. package/dist/components/Reshaped/Reshaped.js +11 -0
  10. package/dist/components/Reshaped/Reshaped.types.d.ts +5 -0
  11. package/dist/components/Reshaped/Reshaped.types.js +1 -0
  12. package/dist/components/Reshaped/index.d.ts +2 -0
  13. package/dist/components/Reshaped/index.js +1 -0
  14. package/dist/hooks/_internal/useSingletonHotkeys.d.ts +31 -0
  15. package/dist/hooks/_internal/useSingletonHotkeys.js +191 -0
  16. package/dist/hooks/_internal/useSingletonKeyboardMode.d.ts +13 -0
  17. package/dist/hooks/_internal/useSingletonKeyboardMode.js +59 -0
  18. package/dist/hooks/_internal/useSingletonRTL.d.ts +6 -0
  19. package/dist/hooks/_internal/useSingletonRTL.js +40 -0
  20. package/dist/hooks/tests/useHandlerRef.stories.d.ts +14 -0
  21. package/dist/hooks/tests/useHandlerRef.stories.js +40 -0
  22. package/dist/hooks/tests/useHotkeys.stories.d.ts +43 -0
  23. package/dist/hooks/tests/useHotkeys.stories.js +165 -0
  24. package/dist/hooks/tests/useKeyboardArrowNavigation.stories.d.ts +15 -0
  25. package/dist/hooks/tests/useKeyboardArrowNavigation.stories.js +107 -0
  26. package/dist/hooks/tests/useKeyboardMode.stories.d.ts +11 -0
  27. package/dist/hooks/tests/useKeyboardMode.stories.js +36 -0
  28. package/dist/hooks/tests/useOnClickOutside.stories.d.ts +23 -0
  29. package/dist/hooks/tests/useOnClickOutside.stories.js +98 -0
  30. package/dist/hooks/tests/useRTL.stories.d.ts +11 -0
  31. package/dist/hooks/tests/useRTL.stories.js +24 -0
  32. package/dist/hooks/tests/useScrollLock.stories.d.ts +14 -0
  33. package/dist/hooks/tests/useScrollLock.stories.js +75 -0
  34. package/dist/hooks/tests/useToggle.stories.d.ts +13 -0
  35. package/dist/hooks/tests/useToggle.stories.js +50 -0
  36. package/dist/hooks/useHandlerRef.d.ts +8 -0
  37. package/dist/hooks/useHandlerRef.js +16 -0
  38. package/dist/hooks/useHotkeys.d.ts +11 -0
  39. package/dist/hooks/useHotkeys.js +27 -0
  40. package/dist/hooks/useIsomorphicLayoutEffect.d.ts +3 -0
  41. package/dist/hooks/useIsomorphicLayoutEffect.js +4 -0
  42. package/dist/hooks/useKeyboardArrowNavigation.d.ts +9 -0
  43. package/dist/hooks/useKeyboardArrowNavigation.js +62 -0
  44. package/dist/hooks/useKeyboardMode.d.ts +2 -0
  45. package/dist/hooks/useKeyboardMode.js +2 -0
  46. package/dist/hooks/useOnClickOutside.d.ts +5 -0
  47. package/dist/hooks/useOnClickOutside.js +63 -0
  48. package/dist/hooks/useRTL.d.ts +2 -0
  49. package/dist/hooks/useRTL.js +2 -0
  50. package/dist/hooks/useScrollLock.d.ts +10 -0
  51. package/dist/hooks/useScrollLock.js +25 -0
  52. package/dist/hooks/useToggle.d.ts +7 -0
  53. package/dist/hooks/useToggle.js +19 -0
  54. package/dist/index.d.ts +14 -0
  55. package/dist/index.js +15 -0
  56. package/dist/internal.d.ts +8 -0
  57. package/dist/internal.js +8 -0
  58. package/dist/types/global.d.ts +7 -0
  59. package/dist/types/global.js +1 -0
  60. package/package.json +49 -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,4 @@
1
+ import React from "react";
2
+ import type * as T from "./Actionable.types";
3
+ declare const Actionable: React.ForwardRefExoticComponent<T.Props & React.RefAttributes<T.Ref>>;
4
+ export default Actionable;
@@ -0,0 +1,72 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { classNames, keys } from "@reshaped/utilities";
4
+ import { forwardRef } from "react";
5
+ const Actionable = forwardRef((props, ref) => {
6
+ const { children, render, href, onClick, type, disabled, as, stopPropagation, className, attributes, } = props;
7
+ const rootAttributes = { ...attributes };
8
+ const hasClickHandler = onClick || attributes?.onClick;
9
+ const hasFocusHandler = attributes?.onFocus || attributes?.onBlur;
10
+ const isLink = Boolean(href || attributes?.href);
11
+ // Including attributes ref for the cases when event listeners are added through it
12
+ // To make sure it doesn't render a span
13
+ const isButton = Boolean(hasClickHandler || hasFocusHandler || type || attributes?.ref);
14
+ const renderedAsButton = !isLink && isButton && (!as || as === "button");
15
+ // Using any here to let TS save on type resolving, otherwise TS throws an error due to the type complexity
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ let TagName;
18
+ if (isLink) {
19
+ TagName = "a";
20
+ rootAttributes.href = disabled ? undefined : href || attributes?.href;
21
+ }
22
+ else if (renderedAsButton) {
23
+ TagName = "button";
24
+ rootAttributes.type = type || attributes?.type || "button";
25
+ rootAttributes.disabled = disabled || attributes?.disabled;
26
+ }
27
+ else if (isButton) {
28
+ const isFocusable = as === "label";
29
+ const simulateButton = !isFocusable || hasClickHandler || hasFocusHandler;
30
+ TagName = as || "span";
31
+ rootAttributes.role = simulateButton ? "button" : undefined;
32
+ rootAttributes.tabIndex = simulateButton ? 0 : undefined;
33
+ }
34
+ else {
35
+ TagName = as || "span";
36
+ }
37
+ const handlePress = (event) => {
38
+ if (disabled)
39
+ return;
40
+ if (stopPropagation)
41
+ event.stopPropagation();
42
+ onClick?.(event);
43
+ attributes?.onClick?.(event);
44
+ };
45
+ const handleKeyDown = (event) => {
46
+ const isSpace = event.key === keys.SPACE;
47
+ const isEnter = event.key === keys.ENTER;
48
+ if (!isSpace && !isEnter)
49
+ return;
50
+ if (rootAttributes.role !== "button")
51
+ return;
52
+ if (stopPropagation)
53
+ event.stopPropagation();
54
+ event.preventDefault();
55
+ handlePress(event);
56
+ };
57
+ const tagAttributes = {
58
+ ref: ref,
59
+ // rootAttributes can receive ref from Flyout
60
+ ...rootAttributes,
61
+ className: classNames(className),
62
+ onClick: handlePress,
63
+ onKeyDown: handleKeyDown,
64
+ "aria-disabled": disabled ? true : undefined,
65
+ children: children,
66
+ };
67
+ if (render)
68
+ return render(tagAttributes);
69
+ return _jsx(TagName, { ...tagAttributes });
70
+ });
71
+ Actionable.displayName = "Actionable";
72
+ export default Actionable;
@@ -0,0 +1,35 @@
1
+ import type { Attributes as AttributesType } from "../../types/global";
2
+ import type { ClassName } from "@reshaped/utilities";
3
+ import type React from "react";
4
+ export type AttributesRef = React.RefObject<HTMLButtonElement | null>;
5
+ type Attributes = AttributesType<"button"> & Omit<React.JSX.IntrinsicElements["a"], keyof AttributesType<"button">> & {
6
+ ref?: AttributesRef;
7
+ };
8
+ export type RenderAttributes = AttributesType<"a"> & {
9
+ ref: React.RefObject<HTMLAnchorElement | null>;
10
+ children: React.ReactNode;
11
+ };
12
+ export type Props = {
13
+ /** Node for inserting the content */
14
+ children?: React.ReactNode;
15
+ /** Render a custom root element, useful for integrating with routers */
16
+ render?: (attributes: RenderAttributes) => React.ReactNode;
17
+ /** Callback when clicked, renders it as a button tag if href is not provided */
18
+ onClick?: (e: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) => void;
19
+ /** URL, renders it as an anchor tag */
20
+ href?: string;
21
+ /** Type attribute, renders it as a button tag */
22
+ type?: React.ButtonHTMLAttributes<HTMLButtonElement>["type"];
23
+ /** Disable from user interaction */
24
+ disabled?: boolean;
25
+ /** Prevent the event from bubbling up to the parent */
26
+ stopPropagation?: boolean;
27
+ /** Render as a different element */
28
+ as?: keyof React.JSX.IntrinsicElements;
29
+ /** Additional classname for the root element */
30
+ className?: ClassName;
31
+ /** Additional attributes for the root element */
32
+ attributes?: Attributes;
33
+ };
34
+ export type Ref = HTMLButtonElement | HTMLAnchorElement;
35
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export { default } from "./Actionable";
2
+ export type { Props as ActionableProps, Ref as ActionableRef } from "./Actionable.types";
@@ -0,0 +1 @@
1
+ export { default } from "./Actionable.js";
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ import type * as T from "./Reshaped.types";
3
+ declare const Reshaped: React.FC<T.Props>;
4
+ export default Reshaped;
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { SingletonHotkeysProvider } from "../../hooks/_internal/useSingletonHotkeys.js";
4
+ import { SingletonKeyboardModeProvider } from "../../hooks/_internal/useSingletonKeyboardMode.js";
5
+ import { SingletonRTLProvider } from "../../hooks/_internal/useSingletonRTL.js";
6
+ const Reshaped = (props) => {
7
+ const { children } = props;
8
+ return (_jsx(SingletonRTLProvider, { children: _jsx(SingletonKeyboardModeProvider, { children: _jsx(SingletonHotkeysProvider, { children: children }) }) }));
9
+ };
10
+ Reshaped.displayName = "Headless.ReshapedProvider";
11
+ export default Reshaped;
@@ -0,0 +1,5 @@
1
+ import type React from "react";
2
+ export type Props = {
3
+ /** Node for inserting children */
4
+ children?: React.ReactNode;
5
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export { default } from "./Reshaped";
2
+ export type { Props as ReshapedProps } from "./Reshaped.types";
@@ -0,0 +1 @@
1
+ export { default } from "./Reshaped.js";
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ /**
3
+ * Types
4
+ */
5
+ type Callback = (e?: KeyboardEvent) => void;
6
+ type PressedMap = Map<string, KeyboardEvent>;
7
+ export type Hotkeys = Record<string, Callback | null>;
8
+ type HotkeyOptions = {
9
+ preventDefault?: boolean;
10
+ };
11
+ type Context = {
12
+ isPressed: (key: string) => boolean;
13
+ addHotkeys: (hotkeys: Hotkeys, ref: React.RefObject<HTMLElement | null>, options?: HotkeyOptions) => (() => void) | undefined;
14
+ };
15
+ type HotkeyData = {
16
+ callback: Callback;
17
+ ref: React.RefObject<HTMLElement | null>;
18
+ options: HotkeyOptions;
19
+ };
20
+ export declare class HotkeyStore {
21
+ hotkeyMap: Record<string, Set<HotkeyData>>;
22
+ getSize: () => number;
23
+ bindHotkeys: (hotkeys: Hotkeys, ref: React.RefObject<HTMLElement | null>, options: HotkeyOptions) => void;
24
+ unbindHotkeys: (hotkeys: Hotkeys) => void;
25
+ handleKeyDown: (pressedMap: PressedMap, e: KeyboardEvent) => void;
26
+ }
27
+ export declare const SingletonHotkeysProvider: React.FC<{
28
+ children: React.ReactNode;
29
+ }>;
30
+ export declare const useSingletonHotkeys: () => Context;
31
+ export {};
@@ -0,0 +1,191 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import React from "react";
4
+ /**
5
+ * Utilities
6
+ */
7
+ const COMBINATION_DELIMETER = "+";
8
+ const pressedMap = new Map();
9
+ let modifiedKeys = [];
10
+ const formatHotkey = (hotkey) => {
11
+ if (hotkey === " ")
12
+ return hotkey;
13
+ return hotkey.replace(/\s/g, "").toLowerCase();
14
+ };
15
+ // Normalize passed key combinations to turn them into a consistent ids
16
+ const getHotkeyId = (hotkey) => {
17
+ return formatHotkey(hotkey).split(COMBINATION_DELIMETER).sort().join(COMBINATION_DELIMETER);
18
+ };
19
+ const getEventKey = (e) => {
20
+ if (!e.key)
21
+ return;
22
+ // Having alt pressed modifies e.key value, so relying on e.code for it
23
+ if (e.altKey && /^[Key|Digit|Numpad]/.test(e.code)) {
24
+ return e.code.toLowerCase().replace(/key|digit|numpad/, "");
25
+ }
26
+ return e.key.toLowerCase();
27
+ };
28
+ // Removing the unknown gets highlighted an invalid syntax
29
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
30
+ const walkHotkeys = (hotkeys, cb) => {
31
+ Object.keys(hotkeys).forEach((key) => {
32
+ key.split(",").forEach((hotkey) => {
33
+ const data = hotkeys[key];
34
+ if (!data)
35
+ return;
36
+ cb(getHotkeyId(hotkey), data);
37
+ });
38
+ });
39
+ };
40
+ export class HotkeyStore {
41
+ hotkeyMap = {};
42
+ getSize = () => Object.keys(this.hotkeyMap).length;
43
+ bindHotkeys = (hotkeys, ref, options) => {
44
+ walkHotkeys(hotkeys, (id, hotkeyData) => {
45
+ if (!hotkeyData)
46
+ return;
47
+ if (!this.hotkeyMap[id]) {
48
+ this.hotkeyMap[id] = new Set();
49
+ }
50
+ this.hotkeyMap[id].add({ callback: hotkeyData, ref, options });
51
+ });
52
+ };
53
+ unbindHotkeys = (hotkeys) => {
54
+ walkHotkeys(hotkeys, (id, hotkeyCallback) => {
55
+ if (!hotkeyCallback)
56
+ return;
57
+ this.hotkeyMap[id]?.forEach((data) => {
58
+ if (data.callback === hotkeyCallback) {
59
+ this.hotkeyMap[id].delete(data);
60
+ }
61
+ });
62
+ if (!this.hotkeyMap[id]?.size) {
63
+ delete this.hotkeyMap[id];
64
+ }
65
+ });
66
+ };
67
+ handleKeyDown = (pressedMap, e) => {
68
+ if (!pressedMap.size)
69
+ return;
70
+ const pressedKeys = [...pressedMap.keys()];
71
+ const pressedId = getHotkeyId(pressedKeys.join(COMBINATION_DELIMETER));
72
+ const pressedFormattedKeys = pressedId.split(COMBINATION_DELIMETER);
73
+ const hotkeyData = this.hotkeyMap[pressedId];
74
+ /**
75
+ * Support for `mod` that represents both Mac and Win keyboards
76
+ * We create the hotkeyId again to sort the mod key correctly
77
+ */
78
+ const controlToModPressedId = getHotkeyId(pressedId.replace("control", "mod"));
79
+ const metaToModPressedId = getHotkeyId(pressedId.replace("meta", "mod"));
80
+ const hotkeyControlModData = pressedFormattedKeys.includes("control") && this.hotkeyMap[controlToModPressedId];
81
+ const hotkeyMetaModData = pressedFormattedKeys.includes("meta") && this.hotkeyMap[metaToModPressedId];
82
+ [hotkeyData, hotkeyControlModData, hotkeyMetaModData].forEach((hotkeyData) => {
83
+ if (!hotkeyData)
84
+ return;
85
+ if (hotkeyData?.size) {
86
+ hotkeyData.forEach((data) => {
87
+ const eventTarget = e.composedPath()[0];
88
+ if (data.ref.current &&
89
+ !(eventTarget === data.ref.current || data.ref.current.contains(eventTarget))) {
90
+ return;
91
+ }
92
+ const resolvedEvent = pressedMap.get(pressedId);
93
+ if (data.options.preventDefault) {
94
+ resolvedEvent?.preventDefault();
95
+ e.preventDefault();
96
+ }
97
+ data.callback(e);
98
+ });
99
+ }
100
+ });
101
+ };
102
+ }
103
+ const globalHotkeyStore = new HotkeyStore();
104
+ /**
105
+ * Components / Hooks
106
+ */
107
+ const HotkeyContext = React.createContext({});
108
+ export const SingletonHotkeysProvider = (props) => {
109
+ const { children } = props;
110
+ // eslint-disable-next-line
111
+ const [_, setTriggerCount] = React.useState(0);
112
+ // Only handle key presses when there is at least one hook listening for hotkeys
113
+ const [hooksCount, setHooksCount] = React.useState(0);
114
+ const addPressedKey = React.useCallback((e) => {
115
+ if (e.repeat || hooksCount === 0)
116
+ return;
117
+ const eventKey = getEventKey(e);
118
+ if (!eventKey)
119
+ return;
120
+ pressedMap.set(eventKey, e);
121
+ setTriggerCount(pressedMap.size);
122
+ // Key up won't trigger for other keys while Meta is pressed so we need to cache them
123
+ // and remove on Meta keyup
124
+ if (e.metaKey)
125
+ modifiedKeys.push(...pressedMap.keys());
126
+ if (pressedMap.has("Meta"))
127
+ modifiedKeys.push(eventKey);
128
+ }, [hooksCount]);
129
+ const removePressedKey = React.useCallback((e) => {
130
+ if (hooksCount === 0)
131
+ return;
132
+ const eventKey = getEventKey(e);
133
+ if (!eventKey)
134
+ return;
135
+ pressedMap.delete(eventKey);
136
+ if (eventKey === "meta" || eventKey === "control") {
137
+ pressedMap.delete("mod");
138
+ }
139
+ if (eventKey === "meta") {
140
+ modifiedKeys.forEach((key) => {
141
+ if (!pressedMap.has(key))
142
+ return;
143
+ pressedMap.delete(key);
144
+ });
145
+ modifiedKeys = [];
146
+ }
147
+ setTriggerCount(pressedMap.size);
148
+ }, [hooksCount]);
149
+ const isPressed = (hotkey) => {
150
+ const keys = formatHotkey(hotkey).split(COMBINATION_DELIMETER);
151
+ if (keys.some((key) => !pressedMap.has(key)))
152
+ return false;
153
+ return true;
154
+ };
155
+ const handleWindowKeyDown = React.useCallback((e) => {
156
+ // Browsers trigger keyboard event without passing e.key when you click on autocomplete
157
+ if (!e.key)
158
+ return;
159
+ addPressedKey(e);
160
+ globalHotkeyStore.handleKeyDown(pressedMap, e);
161
+ }, [addPressedKey]);
162
+ const handleWindowKeyUp = React.useCallback((e) => {
163
+ if (!e.key)
164
+ return;
165
+ removePressedKey(e);
166
+ }, [removePressedKey]);
167
+ const handleWindowBlur = React.useCallback(() => {
168
+ pressedMap.clear();
169
+ modifiedKeys = [];
170
+ }, []);
171
+ const addHotkeys = React.useCallback((hotkeys, ref, options = {}) => {
172
+ setHooksCount((prev) => prev + 1);
173
+ globalHotkeyStore.bindHotkeys(hotkeys, ref, options);
174
+ return () => {
175
+ setHooksCount((prev) => prev - 1);
176
+ globalHotkeyStore.unbindHotkeys(hotkeys);
177
+ };
178
+ }, []);
179
+ React.useEffect(() => {
180
+ window.addEventListener("keydown", handleWindowKeyDown);
181
+ window.addEventListener("keyup", handleWindowKeyUp);
182
+ window.addEventListener("blur", handleWindowBlur);
183
+ return () => {
184
+ window.removeEventListener("keydown", handleWindowKeyDown);
185
+ window.removeEventListener("keyup", handleWindowKeyUp);
186
+ window.removeEventListener("blur", handleWindowBlur);
187
+ };
188
+ }, [handleWindowKeyDown, handleWindowKeyUp, handleWindowBlur]);
189
+ return (_jsx(HotkeyContext.Provider, { value: { addHotkeys, isPressed }, children: children }));
190
+ };
191
+ export const useSingletonHotkeys = () => React.useContext(HotkeyContext);
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ type ContextProps = {
3
+ disabledRef: React.RefObject<boolean> | null;
4
+ disable: () => void;
5
+ enable: () => void;
6
+ activate: () => void;
7
+ deactivate: () => void;
8
+ };
9
+ export declare const SingletonKeyboardModeProvider: React.FC<{
10
+ children: React.ReactNode;
11
+ }>;
12
+ export declare const useSingletonKeyboardMode: () => ContextProps;
13
+ export {};
@@ -0,0 +1,59 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { activateKeyboardMode, deactivateKeyboardMode } from "@reshaped/utilities/internal";
4
+ import React from "react";
5
+ const ESC = "Escape";
6
+ const SingletonKeyboardModeContext = React.createContext({
7
+ disabledRef: null,
8
+ disable: () => { },
9
+ enable: () => { },
10
+ activate: () => { },
11
+ deactivate: () => { },
12
+ });
13
+ export const SingletonKeyboardModeProvider = (props) => {
14
+ const disabledRef = React.useRef(false);
15
+ const disable = React.useCallback(() => {
16
+ disabledRef.current = true;
17
+ }, []);
18
+ const enable = React.useCallback(() => {
19
+ disabledRef.current = false;
20
+ }, []);
21
+ const activate = React.useCallback(() => {
22
+ if (disabledRef.current)
23
+ return;
24
+ activateKeyboardMode();
25
+ }, []);
26
+ const deactivate = React.useCallback(() => {
27
+ if (disabledRef.current)
28
+ return;
29
+ deactivateKeyboardMode();
30
+ }, []);
31
+ const handleKeyDown = React.useCallback((e) => {
32
+ if (e.metaKey || e.altKey || e.ctrlKey)
33
+ return;
34
+ // Prevent focus ring from appearing when using mouse but closing with esc
35
+ if (e.key === ESC)
36
+ return;
37
+ activate();
38
+ }, [activate]);
39
+ const handleClick = React.useCallback(() => {
40
+ deactivate();
41
+ }, [deactivate]);
42
+ React.useEffect(() => {
43
+ window.addEventListener("keydown", handleKeyDown);
44
+ window.addEventListener("mousedown", handleClick);
45
+ return () => {
46
+ window.removeEventListener("keydown", handleKeyDown);
47
+ window.removeEventListener("mousedown", handleClick);
48
+ };
49
+ }, [handleClick, handleKeyDown]);
50
+ const value = React.useMemo(() => ({
51
+ disabledRef,
52
+ disable,
53
+ enable,
54
+ activate,
55
+ deactivate,
56
+ }), [disable, enable, activate, deactivate]);
57
+ return (_jsx(SingletonKeyboardModeContext.Provider, { value: value, children: props.children }));
58
+ };
59
+ export const useSingletonKeyboardMode = () => React.useContext(SingletonKeyboardModeContext);
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ export declare const useSingletonRTL: (defaultRTL?: boolean) => [boolean, React.Dispatch<React.SetStateAction<boolean>>];
3
+ export declare const SingletonRTLProvider: React.FC<{
4
+ children: React.ReactNode;
5
+ defaultRTL?: boolean;
6
+ }>;
@@ -0,0 +1,40 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { isRTL } from "@reshaped/utilities";
4
+ import React from "react";
5
+ import useIsomorphicLayoutEffect from "../useIsomorphicLayoutEffect.js";
6
+ const SingletonRTLContext = React.createContext({
7
+ rtl: [false, () => { }],
8
+ });
9
+ export const useSingletonRTL = (defaultRTL) => {
10
+ const state = React.useState(defaultRTL || false);
11
+ const [rtl, setRTL] = state;
12
+ /**
13
+ * Handle changing dir attribute directly
14
+ */
15
+ useIsomorphicLayoutEffect(() => {
16
+ const observer = new MutationObserver((mutations) => {
17
+ mutations.forEach((mutation) => {
18
+ if (mutation.attributeName !== "dir")
19
+ return;
20
+ const nextRTL = isRTL();
21
+ if (rtl !== nextRTL)
22
+ setRTL(nextRTL);
23
+ });
24
+ });
25
+ observer.observe(document.documentElement, { attributes: true });
26
+ return () => observer.disconnect();
27
+ }, [rtl]);
28
+ /**
29
+ * Handle setRTL usage
30
+ */
31
+ useIsomorphicLayoutEffect(() => {
32
+ document.documentElement.setAttribute("dir", rtl ? "rtl" : "ltr");
33
+ }, [rtl]);
34
+ return state;
35
+ };
36
+ export const SingletonRTLProvider = (props) => {
37
+ const { children, defaultRTL } = props;
38
+ const rtlState = useSingletonRTL(defaultRTL);
39
+ return (_jsx(SingletonRTLContext.Provider, { value: { rtl: rtlState }, children: children }));
40
+ };
@@ -0,0 +1,14 @@
1
+ import { StoryObj } from "@storybook/react-vite";
2
+ import { Mock } from "storybook/test";
3
+ declare const _default: {
4
+ title: string;
5
+ parameters: {
6
+ chromatic: {
7
+ disableSnapshot: boolean;
8
+ };
9
+ };
10
+ };
11
+ export default _default;
12
+ export declare const base: StoryObj<{
13
+ handleEffect: Mock;
14
+ }>;
@@ -0,0 +1,40 @@
1
+ import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { expect, fn, userEvent } from "storybook/test";
4
+ import useHandlerRef from "../useHandlerRef.js";
5
+ export default {
6
+ title: "Headless/Hooks/useHandlerRef",
7
+ parameters: {
8
+ chromatic: { disableSnapshot: true },
9
+ },
10
+ };
11
+ const Component = (props) => {
12
+ const { onEffect, count } = props;
13
+ const onEffectRef = useHandlerRef(onEffect);
14
+ React.useEffect(() => {
15
+ onEffectRef.current();
16
+ }, [onEffectRef]);
17
+ return _jsxs(_Fragment, { children: ["Counter: ", count] });
18
+ };
19
+ export const base = {
20
+ name: "base",
21
+ args: {
22
+ handleEffect: fn(),
23
+ },
24
+ render: (args) => {
25
+ const [count, setCount] = React.useState(0);
26
+ // Creating a new handler on each render
27
+ const handleEffect = () => {
28
+ args.handleEffect(0);
29
+ };
30
+ return (_jsxs("div", { style: { display: "flex", gap: "4px" }, children: [_jsx("button", { onClick: () => setCount((prev) => prev + 1), children: "Increase count" }), _jsx(Component, { onEffect: handleEffect, count: count })] }));
31
+ },
32
+ play: async ({ canvas, args }) => {
33
+ const button = canvas.getAllByRole("button")[0];
34
+ // mount, triggered twice in dev mode
35
+ expect(args.handleEffect).toHaveBeenCalledTimes(2);
36
+ await userEvent.click(button);
37
+ // Rerendering the component doesn't re-trigger the effect
38
+ expect(args.handleEffect).toHaveBeenCalledTimes(2);
39
+ },
40
+ };
@@ -0,0 +1,43 @@
1
+ import { StoryObj } from "@storybook/react-vite";
2
+ import { fn } from "storybook/test";
3
+ declare const _default: {
4
+ title: string;
5
+ parameters: {
6
+ chromatic: {
7
+ disableSnapshot: boolean;
8
+ };
9
+ };
10
+ };
11
+ export default _default;
12
+ export declare const base: {
13
+ name: string;
14
+ render: () => import("react/jsx-runtime").JSX.Element;
15
+ };
16
+ export declare const singleKey: StoryObj<{
17
+ handleHotkey: ReturnType<typeof fn>;
18
+ }>;
19
+ export declare const modKey: StoryObj<{
20
+ handleHotkey: ReturnType<typeof fn>;
21
+ }>;
22
+ export declare const modKeyHold: StoryObj<{
23
+ handleHotkey: ReturnType<typeof fn>;
24
+ }>;
25
+ export declare const keyList: StoryObj<{
26
+ handleHotkey: ReturnType<typeof fn>;
27
+ }>;
28
+ export declare const keyCombination: StoryObj<{
29
+ handleHotkey: ReturnType<typeof fn>;
30
+ }>;
31
+ export declare const keyCombinationFormat: StoryObj<{
32
+ handleHotkey: ReturnType<typeof fn>;
33
+ }>;
34
+ export declare const keyCombinationOrder: StoryObj<{
35
+ handleHotkey: ReturnType<typeof fn>;
36
+ }>;
37
+ export declare const keyCombinationMoreThanRequired: StoryObj<{
38
+ handleHotkey: ReturnType<typeof fn>;
39
+ }>;
40
+ export declare const optionModified: StoryObj<{
41
+ handleHotkey: ReturnType<typeof fn>;
42
+ handleHotkeyModified: ReturnType<typeof fn>;
43
+ }>;