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.
- package/CHANGELOG.md +4 -0
- package/bundle.css +1 -1
- package/bundle.d.ts +2 -0
- package/bundle.js +10 -10
- package/components/Actionable/Actionable.js +7 -7
- package/components/Actionable/tests/Actionable.stories.d.ts +1 -0
- package/components/Actionable/tests/Actionable.stories.js +10 -0
- package/components/Autocomplete/Autocomplete.js +1 -1
- package/components/Calendar/useCalendarKeyboardNavigation.js +1 -1
- package/components/DropdownMenu/DropdownMenu.js +1 -1
- package/components/FormControl/FormControl.context.js +1 -10
- package/components/PinField/PinField.constants.d.ts +3 -0
- package/components/PinField/PinField.constants.js +3 -0
- package/components/PinField/PinField.d.ts +3 -0
- package/components/PinField/PinField.js +10 -0
- package/components/PinField/PinField.module.css +1 -0
- package/components/PinField/PinField.types.d.ts +23 -0
- package/components/PinField/PinField.types.js +1 -0
- package/components/PinField/PinFieldControlled.d.ts +3 -0
- package/components/PinField/PinFieldControlled.js +163 -0
- package/components/PinField/PinFieldUncontrolled.d.ts +3 -0
- package/components/PinField/PinFieldUncontrolled.js +25 -0
- package/components/PinField/index.d.ts +2 -0
- package/components/PinField/index.js +1 -0
- package/components/PinField/tests/PinField.stories.d.ts +14 -0
- package/components/PinField/tests/PinField.stories.js +63 -0
- package/hooks/_private/useSingletonHotkeys.d.ts +6 -2
- package/hooks/_private/useSingletonHotkeys.js +19 -8
- package/hooks/useHotkeys.d.ts +1 -0
- package/hooks/useHotkeys.js +10 -2
- package/index.d.ts +2 -0
- package/index.js +1 -0
- 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
|
43
|
-
|
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,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,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,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 @@
|
|
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
|
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
|
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
|
-
|
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
|
-
|
102
|
+
if (data.options.preventDefault) {
|
103
|
+
resolvedEvent === null || resolvedEvent === void 0 ? void 0 : resolvedEvent.preventDefault();
|
104
|
+
}
|
103
105
|
data.callback(resolvedEvent);
|
104
|
-
|
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);
|
package/hooks/useHotkeys.d.ts
CHANGED
@@ -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;
|
package/hooks/useHotkeys.js
CHANGED
@@ -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
|
-
}, [
|
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.
|
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",
|