@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.
Files changed (41) hide show
  1. package/dist/components/LockScreen/LockScreen.d.ts +5 -4
  2. package/dist/components/LockScreen/LockScreen.d.ts.map +1 -1
  3. package/dist/components/LockScreen/LockScreen.types.d.ts +8 -0
  4. package/dist/components/LockScreen/LockScreen.types.d.ts.map +1 -1
  5. package/dist/components/Logo/Logo.d.ts.map +1 -1
  6. package/dist/components/Logo/logoAssets.d.ts +7 -0
  7. package/dist/components/Logo/logoAssets.d.ts.map +1 -0
  8. package/dist/components/PinCodeInput/PinCodeInput.d.ts +11 -0
  9. package/dist/components/PinCodeInput/PinCodeInput.d.ts.map +1 -0
  10. package/dist/components/PinCodeInput/PinCodeInput.types.d.ts +26 -0
  11. package/dist/components/PinCodeInput/PinCodeInput.types.d.ts.map +1 -0
  12. package/dist/components/PinCodeInput/index.d.ts +3 -0
  13. package/dist/components/PinCodeInput/index.d.ts.map +1 -0
  14. package/dist/hooks/index.d.ts +1 -0
  15. package/dist/hooks/index.d.ts.map +1 -1
  16. package/dist/hooks/useMediaQuery.d.ts +7 -0
  17. package/dist/hooks/useMediaQuery.d.ts.map +1 -0
  18. package/dist/index.css +1 -1
  19. package/dist/index.d.ts +55 -6
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.esm.css +1 -1
  22. package/dist/index.esm.js +255 -63
  23. package/dist/index.esm.js.map +1 -1
  24. package/dist/index.js +256 -62
  25. package/dist/index.js.map +1 -1
  26. package/package.json +2 -1
  27. package/src/assets/logo/logo.png +0 -0
  28. package/src/assets/logo/logoFullWhite.png +0 -0
  29. package/src/assets/logo/logoIcon.png +0 -0
  30. package/src/assets/logo/logoWhite.png +0 -0
  31. package/src/components/LockScreen/LockScreen.tsx +215 -89
  32. package/src/components/LockScreen/LockScreen.types.ts +8 -0
  33. package/src/components/Logo/Logo.tsx +17 -25
  34. package/src/components/Logo/logoAssets.ts +16 -0
  35. package/src/components/PinCodeInput/PinCodeInput.tsx +184 -0
  36. package/src/components/PinCodeInput/PinCodeInput.types.ts +25 -0
  37. package/src/components/PinCodeInput/index.ts +2 -0
  38. package/src/hooks/index.ts +1 -0
  39. package/src/hooks/useMediaQuery.ts +31 -0
  40. package/src/index.ts +4 -0
  41. 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
+ }
@@ -0,0 +1,2 @@
1
+ export { PinCodeInput } from "./PinCodeInput";
2
+ export type { PinCodeInputProps } from "./PinCodeInput";
@@ -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 {
@@ -0,0 +1,5 @@
1
+ /** MED-1629: image imports resolve to a URL string (an inlined data URI in the Rollup build). */
2
+ declare module "*.png" {
3
+ const src: string;
4
+ export default src;
5
+ }