@ews-admin/global-design-system 1.30.0 → 1.32.0
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/dist/components/LockScreen/LockScreen.d.ts +5 -4
- package/dist/components/LockScreen/LockScreen.d.ts.map +1 -1
- package/dist/components/LockScreen/LockScreen.types.d.ts +8 -0
- package/dist/components/LockScreen/LockScreen.types.d.ts.map +1 -1
- package/dist/components/Logo/Logo.d.ts.map +1 -1
- package/dist/components/Logo/logoAssets.d.ts +7 -0
- package/dist/components/Logo/logoAssets.d.ts.map +1 -0
- package/dist/components/PinCodeInput/PinCodeInput.d.ts +11 -0
- package/dist/components/PinCodeInput/PinCodeInput.d.ts.map +1 -0
- package/dist/components/PinCodeInput/PinCodeInput.types.d.ts +26 -0
- package/dist/components/PinCodeInput/PinCodeInput.types.d.ts.map +1 -0
- package/dist/components/PinCodeInput/index.d.ts +3 -0
- package/dist/components/PinCodeInput/index.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useMediaQuery.d.ts +7 -0
- package/dist/hooks/useMediaQuery.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +55 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +255 -63
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +256 -62
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/assets/logo/logo.png +0 -0
- package/src/assets/logo/logoFullWhite.png +0 -0
- package/src/assets/logo/logoIcon.png +0 -0
- package/src/assets/logo/logoWhite.png +0 -0
- package/src/components/LockScreen/LockScreen.tsx +215 -89
- package/src/components/LockScreen/LockScreen.types.ts +8 -0
- package/src/components/Logo/Logo.tsx +17 -25
- package/src/components/Logo/logoAssets.ts +16 -0
- package/src/components/PinCodeInput/PinCodeInput.tsx +184 -0
- package/src/components/PinCodeInput/PinCodeInput.types.ts +25 -0
- package/src/components/PinCodeInput/index.ts +2 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useMediaQuery.ts +31 -0
- package/src/index.ts +4 -0
- package/src/types/assets.d.ts +5 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { cn } from "../../utils";
|
|
3
|
+
import type { PinCodeInputProps } from "./PinCodeInput.types";
|
|
4
|
+
|
|
5
|
+
const MASK = "•";
|
|
6
|
+
const onlyDigits = (text: string) => text.replace(/\D/g, "");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Row of one-digit boxes for a PIN or a one-time code (desktop keyboards):
|
|
10
|
+
* typing moves to the next box, Backspace goes back, arrows move, pasting a
|
|
11
|
+
* code fills the boxes and `onComplete` fires once every box is filled.
|
|
12
|
+
*/
|
|
13
|
+
const PinCodeInput = ({
|
|
14
|
+
length = 4,
|
|
15
|
+
onComplete,
|
|
16
|
+
onChange,
|
|
17
|
+
masked = true,
|
|
18
|
+
autoComplete = "off",
|
|
19
|
+
autoFocus = true,
|
|
20
|
+
disabled = false,
|
|
21
|
+
invalid = false,
|
|
22
|
+
resetKey,
|
|
23
|
+
digitLabel,
|
|
24
|
+
groupLabel,
|
|
25
|
+
className,
|
|
26
|
+
}: PinCodeInputProps) => {
|
|
27
|
+
const empty = useCallback(() => Array.from({ length }, () => ""), [length]);
|
|
28
|
+
const [digits, setDigits] = useState<string[]>(empty);
|
|
29
|
+
const digitsRef = useRef<string[]>(digits);
|
|
30
|
+
const inputs = useRef<Array<HTMLInputElement | null>>([]);
|
|
31
|
+
|
|
32
|
+
const focusBox = (index: number) => {
|
|
33
|
+
const input = inputs.current[Math.max(0, Math.min(index, length - 1))];
|
|
34
|
+
input?.focus();
|
|
35
|
+
input?.select();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const update = (next: string[]) => {
|
|
39
|
+
digitsRef.current = next;
|
|
40
|
+
setDigits(next);
|
|
41
|
+
const value = next.join("");
|
|
42
|
+
onChange?.(value);
|
|
43
|
+
if (value.length === length && next.every(Boolean)) onComplete(value);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Writes `chars` from box `start` on and focuses the box after them. */
|
|
47
|
+
const fill = (start: number, chars: string) => {
|
|
48
|
+
const next = [...digitsRef.current];
|
|
49
|
+
const written = chars.slice(0, length - start).split("");
|
|
50
|
+
written.forEach((char, offset) => {
|
|
51
|
+
next[start + offset] = char;
|
|
52
|
+
});
|
|
53
|
+
focusBox(start + written.length);
|
|
54
|
+
update(next);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Clear the boxes when the caller asks for it.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const cleared = empty();
|
|
60
|
+
digitsRef.current = cleared;
|
|
61
|
+
setDigits(cleared);
|
|
62
|
+
}, [resetKey, empty]);
|
|
63
|
+
|
|
64
|
+
// Focus the first empty box on mount, after a reset and once re-enabled.
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!autoFocus || disabled) return;
|
|
67
|
+
const firstEmpty = digitsRef.current.findIndex((digit) => !digit);
|
|
68
|
+
focusBox(firstEmpty === -1 ? length - 1 : firstEmpty);
|
|
69
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
70
|
+
}, [autoFocus, disabled, resetKey, length]);
|
|
71
|
+
|
|
72
|
+
const handleChange = (index: number, event: React.ChangeEvent<HTMLInputElement>) => {
|
|
73
|
+
if (disabled) return;
|
|
74
|
+
const { value, selectionStart } = event.target;
|
|
75
|
+
const typed = onlyDigits(value);
|
|
76
|
+
if (!typed) {
|
|
77
|
+
const next = [...digitsRef.current];
|
|
78
|
+
next[index] = "";
|
|
79
|
+
update(next);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// A digit typed next to the one already in the box: keep the new one.
|
|
83
|
+
if (digitsRef.current[index] && value.length === 2) {
|
|
84
|
+
const inserted = value.charAt((selectionStart ?? value.length) - 1);
|
|
85
|
+
if (/\d/.test(inserted)) {
|
|
86
|
+
fill(index, inserted);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// One digit, or a whole code (browser autofill of the one-time code).
|
|
91
|
+
fill(typed.length >= length ? 0 : index, typed);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleKeyDown = (index: number, event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
95
|
+
if (disabled) return;
|
|
96
|
+
switch (event.key) {
|
|
97
|
+
case "Backspace": {
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
const next = [...digitsRef.current];
|
|
100
|
+
const target = next[index] ? index : Math.max(0, index - 1);
|
|
101
|
+
next[target] = "";
|
|
102
|
+
focusBox(target);
|
|
103
|
+
update(next);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
case "Delete": {
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
const next = [...digitsRef.current];
|
|
109
|
+
next[index] = "";
|
|
110
|
+
update(next);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
case "ArrowLeft":
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
focusBox(index - 1);
|
|
116
|
+
return;
|
|
117
|
+
case "ArrowRight":
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
focusBox(index + 1);
|
|
120
|
+
return;
|
|
121
|
+
case "Home":
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
focusBox(0);
|
|
124
|
+
return;
|
|
125
|
+
case "End":
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
focusBox(length - 1);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handlePaste = (index: number, event: React.ClipboardEvent<HTMLInputElement>) => {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
if (disabled) return;
|
|
135
|
+
const pasted = onlyDigits(event.clipboardData.getData("text"));
|
|
136
|
+
if (!pasted) return;
|
|
137
|
+
fill(pasted.length >= length ? 0 : index, pasted);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
// Codes stay left-to-right in RTL locales.
|
|
142
|
+
<div
|
|
143
|
+
dir="ltr"
|
|
144
|
+
role="group"
|
|
145
|
+
aria-label={groupLabel}
|
|
146
|
+
data-testid="pin-code-input"
|
|
147
|
+
className={cn("flex justify-center gap-3", invalid && "animate-pin-shake", className)}
|
|
148
|
+
>
|
|
149
|
+
{digits.map((digit, index) => (
|
|
150
|
+
<input
|
|
151
|
+
key={index}
|
|
152
|
+
ref={(element) => {
|
|
153
|
+
inputs.current[index] = element;
|
|
154
|
+
}}
|
|
155
|
+
type="text"
|
|
156
|
+
inputMode="numeric"
|
|
157
|
+
pattern="[0-9]*"
|
|
158
|
+
autoComplete={index === 0 ? autoComplete : "off"}
|
|
159
|
+
autoCorrect="off"
|
|
160
|
+
spellCheck={false}
|
|
161
|
+
value={digit && masked ? MASK : digit}
|
|
162
|
+
disabled={disabled}
|
|
163
|
+
aria-label={digitLabel(index + 1, length)}
|
|
164
|
+
aria-invalid={invalid || undefined}
|
|
165
|
+
data-filled={!!digit}
|
|
166
|
+
onChange={(event) => handleChange(index, event)}
|
|
167
|
+
onKeyDown={(event) => handleKeyDown(index, event)}
|
|
168
|
+
onPaste={(event) => handlePaste(index, event)}
|
|
169
|
+
onFocus={(event) => event.target.select()}
|
|
170
|
+
className={cn(
|
|
171
|
+
"h-14 w-12 rounded-lg border bg-white text-center text-2xl font-medium text-ews-gray-900 outline-none",
|
|
172
|
+
"transition-colors focus:border-ews-primary focus:ring-2 focus:ring-ews-primary",
|
|
173
|
+
"disabled:cursor-not-allowed disabled:opacity-60",
|
|
174
|
+
invalid ? "border-ews-error" : digit ? "border-ews-primary" : "border-ews-gray-300",
|
|
175
|
+
disabled && digit && "animate-pulse",
|
|
176
|
+
)}
|
|
177
|
+
/>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export type { PinCodeInputProps } from "./PinCodeInput.types";
|
|
184
|
+
export { PinCodeInput };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface PinCodeInputProps {
|
|
2
|
+
/** Number of boxes (digits). Default 4. */
|
|
3
|
+
length?: number;
|
|
4
|
+
/** Called once every box is filled, with the full code. */
|
|
5
|
+
onComplete: (code: string) => void;
|
|
6
|
+
/** Called on every change of the typed value. */
|
|
7
|
+
onChange?: (value: string) => void;
|
|
8
|
+
/** Shows bullets instead of the typed digits (PIN). Default true. */
|
|
9
|
+
masked?: boolean;
|
|
10
|
+
/** `autocomplete` of the boxes, e.g. "one-time-code" for an SMS code. Default "off". */
|
|
11
|
+
autoComplete?: string;
|
|
12
|
+
/** Focuses the first empty box on mount, after a reset and when re-enabled. Default true. */
|
|
13
|
+
autoFocus?: boolean;
|
|
14
|
+
/** Ignores input (e.g. while the code is being checked). */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
/** Shows the boxes in the error color and shakes them. */
|
|
17
|
+
invalid?: boolean;
|
|
18
|
+
/** Changing this value clears the boxes (e.g. after a wrong code). */
|
|
19
|
+
resetKey?: string | number;
|
|
20
|
+
/** Accessible label of each box, e.g. (1, 4) => "Digit 1 of 4". */
|
|
21
|
+
digitLabel: (position: number, total: number) => string;
|
|
22
|
+
/** Accessible label of the group of boxes. */
|
|
23
|
+
groupLabel?: string;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { useDebounce, useDebouncedCallback } from "./useDebounce";
|
|
2
|
+
export { useMediaQuery } from "./useMediaQuery";
|
|
2
3
|
export { useIdleLock } from "./useIdleLock";
|
|
3
4
|
export type { UseIdleLockOptions, UseIdleLockResult } from "./useIdleLock";
|
|
4
5
|
export { useSelectField } from "./useSelectField";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
const matches = (query: string, fallback: boolean) =>
|
|
4
|
+
typeof window !== "undefined" && typeof window.matchMedia === "function"
|
|
5
|
+
? window.matchMedia(query).matches
|
|
6
|
+
: fallback;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Whether a CSS media query matches, updated when it changes. Returns
|
|
10
|
+
* `defaultValue` where `matchMedia` does not exist (server rendering, old
|
|
11
|
+
* test environments).
|
|
12
|
+
*/
|
|
13
|
+
export const useMediaQuery = (query: string, defaultValue = false): boolean => {
|
|
14
|
+
const [value, setValue] = useState(() => matches(query, defaultValue));
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
18
|
+
const list = window.matchMedia(query);
|
|
19
|
+
const onChange = () => setValue(list.matches);
|
|
20
|
+
onChange();
|
|
21
|
+
// Safari < 14 only has addListener.
|
|
22
|
+
if (list.addEventListener) {
|
|
23
|
+
list.addEventListener("change", onChange);
|
|
24
|
+
return () => list.removeEventListener("change", onChange);
|
|
25
|
+
}
|
|
26
|
+
list.addListener(onChange);
|
|
27
|
+
return () => list.removeListener(onChange);
|
|
28
|
+
}, [query]);
|
|
29
|
+
|
|
30
|
+
return value;
|
|
31
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -58,6 +58,9 @@ export type { SecureLoaderProps } from "./components/SecureLoader";
|
|
|
58
58
|
export { PinPad } from "./components/PinPad";
|
|
59
59
|
export type { PinPadProps } from "./components/PinPad";
|
|
60
60
|
|
|
61
|
+
export { PinCodeInput } from "./components/PinCodeInput";
|
|
62
|
+
export type { PinCodeInputProps } from "./components/PinCodeInput";
|
|
63
|
+
|
|
61
64
|
export { LockScreen } from "./components/LockScreen";
|
|
62
65
|
export type {
|
|
63
66
|
LockScreenMode,
|
|
@@ -109,6 +112,7 @@ export {
|
|
|
109
112
|
useDebounce,
|
|
110
113
|
useDebouncedCallback,
|
|
111
114
|
useIdleLock,
|
|
115
|
+
useMediaQuery,
|
|
112
116
|
useSelectField,
|
|
113
117
|
} from "./hooks";
|
|
114
118
|
export type {
|