reshaped 2.10.10 → 2.10.11

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 (33) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/bundle.css +1 -1
  3. package/bundle.d.ts +2 -0
  4. package/bundle.js +10 -10
  5. package/components/Actionable/Actionable.js +7 -7
  6. package/components/Actionable/tests/Actionable.stories.d.ts +1 -0
  7. package/components/Actionable/tests/Actionable.stories.js +10 -0
  8. package/components/Autocomplete/Autocomplete.js +1 -1
  9. package/components/Calendar/useCalendarKeyboardNavigation.js +1 -1
  10. package/components/DropdownMenu/DropdownMenu.js +1 -1
  11. package/components/FormControl/FormControl.context.js +1 -10
  12. package/components/PinField/PinField.constants.d.ts +3 -0
  13. package/components/PinField/PinField.constants.js +3 -0
  14. package/components/PinField/PinField.d.ts +3 -0
  15. package/components/PinField/PinField.js +10 -0
  16. package/components/PinField/PinField.module.css +1 -0
  17. package/components/PinField/PinField.types.d.ts +23 -0
  18. package/components/PinField/PinField.types.js +1 -0
  19. package/components/PinField/PinFieldControlled.d.ts +3 -0
  20. package/components/PinField/PinFieldControlled.js +163 -0
  21. package/components/PinField/PinFieldUncontrolled.d.ts +3 -0
  22. package/components/PinField/PinFieldUncontrolled.js +25 -0
  23. package/components/PinField/index.d.ts +2 -0
  24. package/components/PinField/index.js +1 -0
  25. package/components/PinField/tests/PinField.stories.d.ts +14 -0
  26. package/components/PinField/tests/PinField.stories.js +63 -0
  27. package/hooks/_private/useSingletonHotkeys.d.ts +6 -2
  28. package/hooks/_private/useSingletonHotkeys.js +19 -8
  29. package/hooks/useHotkeys.d.ts +1 -0
  30. package/hooks/useHotkeys.js +10 -2
  31. package/index.d.ts +2 -0
  32. package/index.js +1 -0
  33. package/package.json +1 -1
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { forwardRef } from "react";
4
4
  import { classNames } from "../../utilities/helpers.js";
5
+ import * as keys from "../../constants/keys.js";
5
6
  import s from "./Actionable.module.css";
6
7
  const Actionable = forwardRef((props, ref) => {
7
8
  const { children, href, onClick, type, disabled, insetFocus, borderRadius, as, fullWidth, className, attributes, } = props;
@@ -13,8 +14,9 @@ const Actionable = forwardRef((props, ref) => {
13
14
  const isButton = Boolean(hasClickHandler || hasFocusHandler || type);
14
15
  let TagName;
15
16
  if (isLink) {
16
- rootAttributes.href = disabled ? undefined : href || (attributes === null || attributes === void 0 ? void 0 : attributes.href);
17
17
  TagName = "a";
18
+ rootAttributes.href = disabled ? undefined : href || (attributes === null || attributes === void 0 ? void 0 : attributes.href);
19
+ rootAttributes.role = hasClickHandler ? "button" : attributes === null || attributes === void 0 ? void 0 : attributes.role;
18
20
  }
19
21
  else if (isButton && (!as || as === "button")) {
20
22
  TagName = "button";
@@ -39,14 +41,12 @@ const Actionable = forwardRef((props, ref) => {
39
41
  (_a = attributes === null || attributes === void 0 ? void 0 : attributes.onClick) === null || _a === void 0 ? void 0 : _a.call(attributes, event);
40
42
  };
41
43
  const handleKeyDown = (event) => {
42
- const simulatingButton = rootAttributes.role === "button";
43
- // These cases are handled correctly by the browsers
44
- if (simulatingButton || isLink)
45
- return;
46
- const isSpace = event.key === " ";
47
- const isEnter = event.key === "Enter";
44
+ const isSpace = event.key === keys.SPACE;
45
+ const isEnter = event.key === keys.ENTER;
48
46
  if (!isSpace && !isEnter)
49
47
  return;
48
+ if (!hasClickHandler)
49
+ return;
50
50
  event.preventDefault();
51
51
  handlePress(event);
52
52
  };
@@ -12,3 +12,4 @@ export declare const role: () => import("react").JSX.Element;
12
12
  export declare const disabled: () => import("react").JSX.Element;
13
13
  export declare const fullWidth: () => import("react").JSX.Element;
14
14
  export declare const focusRing: () => import("react").JSX.Element;
15
+ export declare const edgeCases: () => import("react").JSX.Element;
@@ -61,3 +61,13 @@ export const focusRing = () => (<Example>
61
61
  </Actionable>
62
62
  </Example.Item>
63
63
  </Example>);
64
+ export const edgeCases = () => (<Example>
65
+ <Example.Item title="insetFocus">
66
+ <form onSubmit={(e) => {
67
+ e.preventDefault();
68
+ alert("Submitted");
69
+ }}>
70
+ <Actionable type="submit">Submit</Actionable>
71
+ </form>
72
+ </Example.Item>
73
+ </Example>);
@@ -28,7 +28,7 @@ const Autocomplete = (props) => {
28
28
  const handleClose = () => setActive(false);
29
29
  useHotkeys({
30
30
  [`${keys.UP},${keys.DOWN}`]: () => handleOpen(),
31
- }, [handleOpen], { ref: inputRef });
31
+ }, [handleOpen], { ref: inputRef, preventDefault: true });
32
32
  const handleChange = (args) => {
33
33
  onChange === null || onChange === void 0 ? void 0 : onChange(args);
34
34
  setLocked(false);
@@ -61,6 +61,6 @@ const useCalendarKeyboardNavigation = (props) => {
61
61
  [keys.RIGHT]: () => focusDate({ delta: 1, onMonthChange: changeToNextMonth }),
62
62
  [keys.UP]: () => focusDate({ delta: -verticalDelta, onMonthChange: changeToPreviousMonth }),
63
63
  [keys.DOWN]: () => focusDate({ delta: verticalDelta, onMonthChange: changeToNextMonth }),
64
- }, [changeToNextMonth, changeToPreviousMonth, focusDate, verticalDelta], { ref: rootRef });
64
+ }, [changeToNextMonth, changeToPreviousMonth, focusDate, verticalDelta], { ref: rootRef, preventDefault: true });
65
65
  };
66
66
  export default useCalendarKeyboardNavigation;
@@ -72,7 +72,7 @@ const DropdownMenuSubTriggerItem = (props) => {
72
72
  var _a;
73
73
  (_a = subMenuInstance === null || subMenuInstance === void 0 ? void 0 : subMenuInstance.current) === null || _a === void 0 ? void 0 : _a.open();
74
74
  },
75
- }, [], { ref: attributes === null || attributes === void 0 ? void 0 : attributes.ref });
75
+ }, [], { ref: attributes === null || attributes === void 0 ? void 0 : attributes.ref, preventDefault: true });
76
76
  return (_jsx(DropdownMenuItem, Object.assign({}, menuItemProps, { attributes: Object.assign(Object.assign({}, attributes), { ref }), endSlot: _jsx(Icon, { autoWidth: true, svg: IconChevronRight, className: s.arrow }), children: children })));
77
77
  };
78
78
  const DropdownMenuSubTrigger = (props) => {
@@ -1,15 +1,6 @@
1
1
  "use client";
2
2
  import React from "react";
3
- const FormControlContext = React.createContext({
4
- attributes: {
5
- id: "",
6
- "aria-describedby": "",
7
- },
8
- required: undefined,
9
- hasError: false,
10
- errorRef: () => { },
11
- helperRef: () => { },
12
- });
3
+ const FormControlContext = React.createContext({ attributes: {} });
13
4
  export const Provider = FormControlContext.Provider;
14
5
  export const useFormControlPrivate = () => React.useContext(FormControlContext);
15
6
  export const useFormControl = () => {
@@ -0,0 +1,3 @@
1
+ export declare const regExpNumericChar = "\\d";
2
+ export declare const regExpAlphabeticChar = "[a-zA-Z]";
3
+ export declare const regExpAlphaNumericChar = "(\\d|[a-zA-Z])";
@@ -0,0 +1,3 @@
1
+ export const regExpNumericChar = "\\d";
2
+ export const regExpAlphabeticChar = "[a-zA-Z]";
3
+ export const regExpAlphaNumericChar = `(${regExpNumericChar}|${regExpAlphabeticChar})`;
@@ -0,0 +1,3 @@
1
+ import type * as T from "./PinField.types";
2
+ declare const PinField: (props: T.Props) => import("react/jsx-runtime").JSX.Element;
3
+ export default PinField;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import PinFieldControlled from "./PinFieldControlled.js";
3
+ import PinFieldUncontrolled from "./PinFieldUncontrolled.js";
4
+ const PinField = (props) => {
5
+ const { value } = props;
6
+ if (value !== undefined)
7
+ return _jsx(PinFieldControlled, Object.assign({}, props));
8
+ return _jsx(PinFieldUncontrolled, Object.assign({}, props));
9
+ };
10
+ export default PinField;
@@ -0,0 +1 @@
1
+ .root{display:inline-flex;margin:-1px 0;overflow-y:clip;padding:1px 0}.input,.root{vertical-align:top}.input{background:transparent;border:transparent;caret-color:transparent;color:transparent;font-size:16px;inset:0;outline:none;padding-left:100%;position:absolute}.item{cursor:text}.item--focused{border-color:var(--rs-color-border-primary);box-shadow:0 0 0 1px var(--rs-color-border-primary)}.item--focused:empty:before{animation:rs-pin-field-caret 1s ease-out infinite;background:var(--rs-color-foreground-neutral);border-radius:999px;content:"";height:var(--rs-font-size-body-2);left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:1px}@media (hover:hover){.root:hover .input{pointer-events:none}}@keyframes rs-pin-field-caret{0%,49.9%,to{opacity:1}50%,99.9%{opacity:0}}
@@ -0,0 +1,23 @@
1
+ import type * as G from "../../types/global";
2
+ export type Size = "medium" | "large" | "xlarge";
3
+ type BaseProps = {
4
+ name: string;
5
+ valueLength?: number;
6
+ charPattern?: "alphabetic" | "numeric" | "alphanumeric";
7
+ size?: G.Responsive<Size>;
8
+ variant?: "outline" | "faded";
9
+ onChange?: G.ChangeHandler<string>;
10
+ inputAttributes?: G.Attributes<"input", BaseProps>;
11
+ className?: G.ClassName;
12
+ attributes?: G.Attributes<"div", BaseProps>;
13
+ };
14
+ export type ControlledProps = BaseProps & {
15
+ value: string;
16
+ defaultValue?: never;
17
+ };
18
+ export type UncontrolledProps = BaseProps & {
19
+ value?: never;
20
+ defaultValue?: string;
21
+ };
22
+ export type Props = ControlledProps | UncontrolledProps;
23
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import type * as T from "./PinField.types";
2
+ declare const PinFieldControlled: (props: T.ControlledProps) => import("react/jsx-runtime").JSX.Element;
3
+ export default PinFieldControlled;
@@ -0,0 +1,163 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import { responsivePropDependency } from "../../utilities/helpers.js";
5
+ import View from "../View/index.js";
6
+ import Text from "../Text/index.js";
7
+ import useHotkeys from "../../hooks/useHotkeys.js";
8
+ import { useFormControl } from "../FormControl/index.js";
9
+ import { onNextFrame } from "../../utilities/animation.js";
10
+ import * as keys from "../../constants/keys.js";
11
+ import { regExpNumericChar, regExpAlphabeticChar, regExpAlphaNumericChar, } from "./PinField.constants.js";
12
+ import s from "./PinField.module.css";
13
+ const sizeMap = {
14
+ medium: 9,
15
+ large: 12,
16
+ xlarge: 14,
17
+ };
18
+ const patternMap = {
19
+ numeric: regExpNumericChar,
20
+ alphabetic: regExpAlphabeticChar,
21
+ alphanumeric: regExpAlphaNumericChar,
22
+ };
23
+ const PinFieldControlled = (props) => {
24
+ const { valueLength = 4, value, onChange, name, charPattern = "numeric", size = "medium", variant = "outline", className, attributes, inputAttributes, } = props;
25
+ const pattern = patternMap[charPattern];
26
+ const responsiveInputSize = responsivePropDependency(size, (value) => sizeMap[value]);
27
+ const responsiveTextVariant = responsivePropDependency(size, (value) => value === "medium" ? "body-3" : "body-2");
28
+ const responsiveRadius = responsivePropDependency(size, (value) => value === "xlarge" ? "medium" : "small");
29
+ const [focusedIndex, setFocusedIndex] = React.useState(null);
30
+ const formControl = useFormControl();
31
+ /**
32
+ * Most of the logic works based on this mode parameter
33
+ * `edit` mode means user has selected an already filled item and is changing its value,
34
+ * so caret position is related to the value rendered before it
35
+ * `type` means user is typing a new value in an empty item,
36
+ * so caret is positioned before the value that's going to be added
37
+ */
38
+ const modeRef = React.useRef("type");
39
+ const inputRef = React.useRef(null);
40
+ const nodes = [];
41
+ /**
42
+ * Update displayed item focus based on the current caret position and current mode
43
+ */
44
+ const syncSelection = React.useCallback((index) => {
45
+ var _a;
46
+ const el = inputRef.current;
47
+ if (!el || el.selectionStart === null)
48
+ return;
49
+ const mode = modeRef.current;
50
+ const selectionStart = (_a = index !== null && index !== void 0 ? index : el.selectionStart) !== null && _a !== void 0 ? _a : 0;
51
+ const nextSelectionStart = Math.min(mode === "type" ? el.value.length : el.value.length - 1, Math.max(0, selectionStart));
52
+ if (modeRef.current === "type") {
53
+ el.selectionStart = nextSelectionStart;
54
+ el.selectionEnd = nextSelectionStart;
55
+ }
56
+ else {
57
+ el.selectionStart = nextSelectionStart;
58
+ el.selectionEnd = nextSelectionStart + 1;
59
+ }
60
+ setFocusedIndex(el.selectionStart);
61
+ }, []);
62
+ /**
63
+ * Using onNextFrame here to wait for the native behavior first
64
+ */
65
+ useHotkeys({
66
+ [`${keys.LEFT},${keys.UP}`]: () => {
67
+ onNextFrame(() => {
68
+ const el = inputRef.current;
69
+ if (!el || el.selectionStart === null)
70
+ return;
71
+ const mode = modeRef.current;
72
+ const nextMode = !value.length ? "type" : "edit";
73
+ modeRef.current = nextMode;
74
+ syncSelection(mode === "type" && nextMode === "edit" ? el.selectionStart : el.selectionStart - 1);
75
+ });
76
+ },
77
+ [`${keys.RIGHT},${keys.DOWN}`]: () => {
78
+ onNextFrame(() => {
79
+ const el = inputRef.current;
80
+ if (!el || el.selectionStart === null)
81
+ return;
82
+ const nextMode = el.selectionStart === value.length && el.selectionStart !== valueLength
83
+ ? "type"
84
+ : "edit";
85
+ modeRef.current = nextMode;
86
+ syncSelection(el.selectionStart);
87
+ });
88
+ },
89
+ }, [value, syncSelection, valueLength], {
90
+ ref: inputRef,
91
+ });
92
+ const handleFocus = () => {
93
+ /**
94
+ * Tabing into the input might select the whole value
95
+ * so we're resetting that behavior
96
+ */
97
+ syncSelection(value.length);
98
+ };
99
+ const handleBlur = () => {
100
+ setFocusedIndex(null);
101
+ };
102
+ const handlePaste = (e) => {
103
+ if (focusedIndex === null || !inputRef.current)
104
+ return;
105
+ /**
106
+ * Input might not have enough space for pasting the value so we free up space based on the pasted value length
107
+ */
108
+ const data = e.clipboardData.getData("text");
109
+ const updatedValue = value.slice(0, focusedIndex) + value.slice(focusedIndex + data.length);
110
+ /**
111
+ * Manually update the value and selection to preserve all not affected values and keep caret in the correct position
112
+ */
113
+ inputRef.current.value = updatedValue;
114
+ inputRef.current.selectionEnd = focusedIndex;
115
+ inputRef.current.selectionStart = inputRef.current.selectionStart;
116
+ };
117
+ const handleInput = (event) => {
118
+ const el = event.target;
119
+ const nextValue = el.value;
120
+ const matcher = new RegExp(`^${pattern}+$`);
121
+ if (nextValue && !nextValue.match(matcher))
122
+ return;
123
+ if (el.selectionStart === null)
124
+ return;
125
+ const nextMode =
126
+ // Finished editing the last character
127
+ nextValue.length === valueLength ||
128
+ // Staying inside the sequence
129
+ nextValue.length > el.selectionStart
130
+ ? "edit"
131
+ : "type";
132
+ modeRef.current = nextMode;
133
+ onChange === null || onChange === void 0 ? void 0 : onChange({ event, name, value: nextValue });
134
+ onNextFrame(() => {
135
+ syncSelection();
136
+ });
137
+ };
138
+ /**
139
+ * Manually handle correct caret position when any of the items are clicked
140
+ */
141
+ const handleItemClick = (event, index) => {
142
+ if (!inputRef.current)
143
+ return;
144
+ event.preventDefault();
145
+ inputRef.current.focus();
146
+ modeRef.current = index >= value.length ? "type" : "edit";
147
+ syncSelection(index);
148
+ };
149
+ for (let i = 0; i < valueLength; i++) {
150
+ nodes.push(_jsx(View, { height: responsiveInputSize, width: responsiveInputSize, borderRadius: responsiveRadius, borderColor: variant === "faded" ? "transparent" : "neutral", backgroundColor: variant === "faded" ? "neutral-faded" : "elevation-base", align: "center", justify: "center", className: [s.item, focusedIndex === i && s["item--focused"]], attributes: {
151
+ onMouseDown: (e) => {
152
+ handleItemClick(e, i);
153
+ },
154
+ onTouchStart: (e) => {
155
+ handleItemClick(e, i);
156
+ },
157
+ }, children: value[i] && _jsx(Text, { variant: responsiveTextVariant, children: value[i] }) }, i));
158
+ }
159
+ return (_jsxs(View, { gap: 2, direction: "row", className: [s.root, className], attributes: attributes, children: [nodes, _jsx("input", Object.assign({}, inputAttributes, formControl.attributes, { type: "text",
160
+ // className={s.input}
161
+ onFocus: handleFocus, onBlur: handleBlur, onPaste: handlePaste, onInput: handleInput, value: value, name: name, maxLength: valueLength, ref: inputRef, autoComplete: (inputAttributes === null || inputAttributes === void 0 ? void 0 : inputAttributes.autoComplete) || "one-time-code", inputMode: charPattern === "numeric" ? "numeric" : undefined, pattern: `${pattern}{${valueLength}}` }))] }));
162
+ };
163
+ export default PinFieldControlled;
@@ -0,0 +1,3 @@
1
+ import type * as T from "./PinField.types";
2
+ declare const PinFieldUncontrolled: (props: T.UncontrolledProps) => import("react/jsx-runtime").JSX.Element;
3
+ export default PinFieldUncontrolled;
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ import { jsx as _jsx } from "react/jsx-runtime";
14
+ import React from "react";
15
+ import PinFieldControlled from "./PinFieldControlled.js";
16
+ const PinFieldUncontrolled = (props) => {
17
+ const { defaultValue, onChange } = props, controlledProps = __rest(props, ["defaultValue", "onChange"]);
18
+ const [value, setValue] = React.useState(defaultValue || "");
19
+ const handleChange = (args) => {
20
+ setValue(args.value);
21
+ onChange === null || onChange === void 0 ? void 0 : onChange(args);
22
+ };
23
+ return _jsx(PinFieldControlled, Object.assign({}, controlledProps, { value: value, onChange: handleChange }));
24
+ };
25
+ export default PinFieldUncontrolled;
@@ -0,0 +1,2 @@
1
+ export { default } from "./PinField";
2
+ export type { Props as PinFieldProps } from "./PinField.types";
@@ -0,0 +1 @@
1
+ export { default } from "./PinField.js";
@@ -0,0 +1,14 @@
1
+ declare const _default: {
2
+ title: string;
3
+ component: (props: import("./..").PinFieldProps) => import("react").JSX.Element;
4
+ parameters: {
5
+ iframe: {
6
+ url: string;
7
+ };
8
+ };
9
+ };
10
+ export default _default;
11
+ export declare const base: () => import("react").JSX.Element;
12
+ export declare const variant: () => import("react").JSX.Element;
13
+ export declare const size: () => import("react").JSX.Element;
14
+ export declare const formControl: () => import("react").JSX.Element;
@@ -0,0 +1,63 @@
1
+ import { Example } from "../../../utilities/storybook/index.js";
2
+ import PinField from "../index.js";
3
+ import FormControl from "../../FormControl/index.js";
4
+ export default {
5
+ title: "Components/PinField",
6
+ component: PinField,
7
+ parameters: {
8
+ iframe: {
9
+ url: "https://reshaped.so/docs/components/pin-field",
10
+ },
11
+ },
12
+ };
13
+ export const base = () => (<Example>
14
+ <Example.Item title="no value">
15
+ <PinField name="pin"/>
16
+ </Example.Item>
17
+
18
+ <Example.Item title="defaultValue: 12">
19
+ <PinField name="pin2" defaultValue="12"/>
20
+ </Example.Item>
21
+
22
+ <Example.Item title="value: 12">
23
+ <PinField name="pin3" value="12"/>
24
+ </Example.Item>
25
+
26
+ <Example.Item title="defaultValue: 12, valueLength: 6">
27
+ <PinField name="pin4" defaultValue="12" valueLength={6}/>
28
+ </Example.Item>
29
+
30
+ <Example.Item title="defaultValue: ab, charPattern: alphabetic">
31
+ <PinField name="pin5" defaultValue="ab" charPattern="alphabetic"/>
32
+ </Example.Item>
33
+
34
+ <Example.Item title="defaultValue: ab, charPattern: alphanumeric">
35
+ <PinField name="pin6" defaultValue="ab" charPattern="alphanumeric"/>
36
+ </Example.Item>
37
+ </Example>);
38
+ export const variant = () => (<Example>
39
+ <Example.Item title="variant: faded">
40
+ <PinField name="pin" variant="faded"/>
41
+ </Example.Item>
42
+ </Example>);
43
+ export const size = () => (<Example>
44
+ <Example.Item title="size: large">
45
+ <PinField name="pin" size="large"/>
46
+ </Example.Item>
47
+
48
+ <Example.Item title="size: xlarge">
49
+ <PinField name="pin" size="xlarge"/>
50
+ </Example.Item>
51
+
52
+ <Example.Item title="size: responsive, s: medium, m+: xlarge">
53
+ <PinField name="pin" size={{ s: "medium", m: "xlarge" }}/>
54
+ </Example.Item>
55
+ </Example>);
56
+ export const formControl = () => (<Example>
57
+ <Example.Item title="with form control">
58
+ <FormControl>
59
+ <FormControl.Label>Label</FormControl.Label>
60
+ <PinField name="pin"/>
61
+ </FormControl>
62
+ </Example.Item>
63
+ </Example>);
@@ -5,20 +5,24 @@ import React from "react";
5
5
  type Callback = (e: KeyboardEvent) => void;
6
6
  type PressedKeys = Record<string, KeyboardEvent>;
7
7
  type Hotkeys = Record<string, Callback | null>;
8
+ type HotkeyOptions = {
9
+ preventDefault?: boolean;
10
+ };
8
11
  type Context = {
9
12
  isPressed: (key: string) => boolean;
10
- addHotkeys: (hotkeys: Hotkeys, ref: React.RefObject<HTMLElement | null>) => (() => void) | undefined;
13
+ addHotkeys: (hotkeys: Hotkeys, ref: React.RefObject<HTMLElement | null>, options?: HotkeyOptions) => (() => void) | undefined;
11
14
  };
12
15
  export declare class HotkeyStore {
13
16
  hotkeyMap: Record<string, {
14
17
  data: Set<{
15
18
  callback: Callback;
16
19
  ref: React.RefObject<HTMLElement | null>;
20
+ options: HotkeyOptions;
17
21
  }>;
18
22
  used: boolean;
19
23
  }>;
20
24
  getSize: () => number;
21
- bindHotkeys: (hotkeys: Hotkeys, ref: React.RefObject<HTMLElement | null>) => void;
25
+ bindHotkeys: (hotkeys: Hotkeys, ref: React.RefObject<HTMLElement | null>, options: HotkeyOptions) => void;
22
26
  unbindHotkeys: (hotkeys: Hotkeys) => void;
23
27
  handleKeyDown: (pressedMap: PressedKeys, e: KeyboardEvent) => void;
24
28
  handleKeyUp: (e: KeyboardEvent) => void;
@@ -5,7 +5,7 @@ import React from "react";
5
5
  */
6
6
  const COMBINATION_DELIMETER = "+";
7
7
  const pressedMap = {};
8
- const modifiedKeys = [];
8
+ let modifiedKeys = [];
9
9
  const formatHotkey = (hotkey) => {
10
10
  if (hotkey === " ")
11
11
  return hotkey;
@@ -61,14 +61,14 @@ export class HotkeyStore {
61
61
  constructor() {
62
62
  this.hotkeyMap = {};
63
63
  this.getSize = () => Object.keys(this.hotkeyMap).length;
64
- this.bindHotkeys = (hotkeys, ref) => {
64
+ this.bindHotkeys = (hotkeys, ref, options) => {
65
65
  walkHotkeys(hotkeys, (id, hotkeyData) => {
66
66
  if (!hotkeyData)
67
67
  return;
68
68
  if (!this.hotkeyMap[id]) {
69
69
  this.hotkeyMap[id] = { data: new Set(), used: false };
70
70
  }
71
- this.hotkeyMap[id].data.add({ callback: hotkeyData, ref });
71
+ this.hotkeyMap[id].data.add({ callback: hotkeyData, ref, options });
72
72
  });
73
73
  };
74
74
  this.unbindHotkeys = (hotkeys) => {
@@ -99,9 +99,16 @@ export class HotkeyStore {
99
99
  return;
100
100
  }
101
101
  const resolvedEvent = pressedMap[pressedId];
102
- resolvedEvent === null || resolvedEvent === void 0 ? void 0 : resolvedEvent.preventDefault();
102
+ if (data.options.preventDefault) {
103
+ resolvedEvent === null || resolvedEvent === void 0 ? void 0 : resolvedEvent.preventDefault();
104
+ }
103
105
  data.callback(resolvedEvent);
104
- this.hotkeyMap[pressedId].used = true;
106
+ /**
107
+ * While meta is pressed - other keys keyup won't trigger and
108
+ * we want to allow calling the same shortcut multiple times while meta was pressed
109
+ */
110
+ if (!(resolvedEvent === null || resolvedEvent === void 0 ? void 0 : resolvedEvent.metaKey))
111
+ this.hotkeyMap[pressedId].used = true;
105
112
  });
106
113
  }
107
114
  });
@@ -140,7 +147,7 @@ export const SingletonHotkeysProvider = (props) => {
140
147
  setTriggerCount(Object.keys(pressedMap).length);
141
148
  // Key up won't trigger for other keys while Meta is pressed so we need to cache them
142
149
  // and remove on Meta keyup
143
- if (eventKey === "meta") {
150
+ if (eventKey === "meta" || e.metaKey) {
144
151
  modifiedKeys.push(...Object.keys(pressedMap));
145
152
  }
146
153
  if (pressedMap.Meta) {
@@ -159,8 +166,12 @@ export const SingletonHotkeysProvider = (props) => {
159
166
  }
160
167
  if (eventKey === "meta") {
161
168
  modifiedKeys.forEach((key) => {
169
+ if (!pressedMap[key])
170
+ return;
171
+ globalHotkeyStore.handleKeyUp(pressedMap[key]);
162
172
  delete pressedMap[key];
163
173
  });
174
+ modifiedKeys = [];
164
175
  }
165
176
  setTriggerCount(Object.keys(pressedMap).length);
166
177
  }, [hooksCount]);
@@ -170,9 +181,9 @@ export const SingletonHotkeysProvider = (props) => {
170
181
  return false;
171
182
  return true;
172
183
  };
173
- const addHotkeys = React.useCallback((hotkeys, ref) => {
184
+ const addHotkeys = React.useCallback((hotkeys, ref, options = {}) => {
174
185
  setHooksCount((prev) => prev + 1);
175
- globalHotkeyStore.bindHotkeys(hotkeys, ref);
186
+ globalHotkeyStore.bindHotkeys(hotkeys, ref, options);
176
187
  return () => {
177
188
  setHooksCount((prev) => prev - 1);
178
189
  globalHotkeyStore.unbindHotkeys(hotkeys);
@@ -2,6 +2,7 @@ import React from "react";
2
2
  declare const useHotkeys: <Element_1 extends HTMLElement>(hotkeys: Record<string, ((e: KeyboardEvent) => void) | null>, deps?: unknown[], options?: {
3
3
  ref?: React.RefObject<Element_1> | undefined;
4
4
  disabled?: boolean | undefined;
5
+ preventDefault?: boolean | undefined;
5
6
  } | undefined) => {
6
7
  ref: React.RefObject<Element_1>;
7
8
  checkHotkeyState: (key: string) => boolean;
@@ -8,10 +8,18 @@ const useHotkeys = (hotkeys, deps = [], options) => {
8
8
  React.useEffect(() => {
9
9
  if (options === null || options === void 0 ? void 0 : options.disabled)
10
10
  return;
11
- const remove = addHotkeys(hotkeys, elementRef);
11
+ const remove = addHotkeys(hotkeys, elementRef, { preventDefault: options === null || options === void 0 ? void 0 : options.preventDefault });
12
12
  return () => remove === null || remove === void 0 ? void 0 : remove();
13
13
  // eslint-disable-next-line react-hooks/exhaustive-deps
14
- }, [addHotkeys, Object.keys(hotkeys).join(","), options === null || options === void 0 ? void 0 : options.disabled, ...deps]);
14
+ }, [
15
+ addHotkeys,
16
+ // eslint-disable-next-line react-hooks/exhaustive-deps
17
+ Object.keys(hotkeys).join(","),
18
+ options === null || options === void 0 ? void 0 : options.disabled,
19
+ options === null || options === void 0 ? void 0 : options.preventDefault,
20
+ // eslint-disable-next-line react-hooks/exhaustive-deps
21
+ ...deps,
22
+ ]);
15
23
  return { ref: elementRef, checkHotkeyState: isPressed };
16
24
  };
17
25
  export default useHotkeys;
package/index.d.ts CHANGED
@@ -63,6 +63,8 @@ export { default as Overlay } from "./components/Overlay";
63
63
  export type { OverlayProps } from "./components/Overlay";
64
64
  export { default as Pagination } from "./components/Pagination";
65
65
  export type { PaginationProps } from "./components/Pagination";
66
+ export { default as PinField } from "./components/PinField";
67
+ export type { PinFieldProps } from "./components/PinField";
66
68
  export { default as Popover } from "./components/Popover";
67
69
  export type { PopoverProps } from "./components/Popover";
68
70
  export { default as Progress } from "./components/Progress";
package/index.js CHANGED
@@ -32,6 +32,7 @@ export { default as MenuItem } from "./components/MenuItem/index.js";
32
32
  export { default as Modal } from "./components/Modal/index.js";
33
33
  export { default as Overlay } from "./components/Overlay/index.js";
34
34
  export { default as Pagination } from "./components/Pagination/index.js";
35
+ export { default as PinField } from "./components/PinField/index.js";
35
36
  export { default as Popover } from "./components/Popover/index.js";
36
37
  export { default as Progress } from "./components/Progress/index.js";
37
38
  export { default as Radio } from "./components/Radio/index.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reshaped",
3
3
  "description": "Professionally crafted design system in React & Figma for building products of any scale and complexity",
4
- "version": "2.10.10",
4
+ "version": "2.10.11",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "email": "hello@reshaped.so",
7
7
  "homepage": "https://reshaped.so",