@westpac/ui 0.29.0 → 0.30.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.
@@ -1,2 +1,40 @@
1
- import { type PassCodeProps } from './pass-code.types.js';
2
- export declare function PassCode({ length, onComplete, className, ...props }: PassCodeProps): import("react/jsx-runtime").JSX.Element;
1
+ import React from 'react';
2
+ import { PassCodeRef } from './pass-code.types.js';
3
+ export declare const PassCode: React.ForwardRefExoticComponent<{
4
+ length: number;
5
+ onBlur?: (index: number, event: React.FocusEvent<HTMLInputElement>) => void;
6
+ onChange?: (passcode: string[]) => void;
7
+ onComplete?: (passcode: string) => void;
8
+ type?: "numbers" | "letters" | "alphanumeric";
9
+ value?: string[];
10
+ } & import("tailwind-variants").VariantProps<import("tailwind-variants").TVReturnType<{
11
+ [key: string]: {
12
+ [key: string]: import("tailwind-variants").ClassValue | {
13
+ base?: import("tailwind-variants").ClassValue;
14
+ input?: import("tailwind-variants").ClassValue;
15
+ };
16
+ };
17
+ } | {
18
+ [x: string]: {
19
+ [x: string]: import("tailwind-variants").ClassValue | {
20
+ base?: import("tailwind-variants").ClassValue;
21
+ input?: import("tailwind-variants").ClassValue;
22
+ };
23
+ };
24
+ } | {}, {
25
+ base: string;
26
+ input: string;
27
+ }, undefined, TVConfig<V, EV>, {
28
+ [key: string]: {
29
+ [key: string]: import("tailwind-variants").ClassValue | {
30
+ base?: import("tailwind-variants").ClassValue;
31
+ input?: import("tailwind-variants").ClassValue;
32
+ };
33
+ };
34
+ } | {}, {
35
+ base: string;
36
+ input: string;
37
+ }, import("tailwind-variants").TVReturnType<unknown, {
38
+ base: string;
39
+ input: string;
40
+ }, undefined, TVConfig<V, EV>, unknown, unknown, undefined>>> & Omit<React.HTMLAttributes<Element>, "onChange"> & React.RefAttributes<PassCodeRef>>;
@@ -13,39 +13,65 @@ function _extends() {
13
13
  };
14
14
  return _extends.apply(this, arguments);
15
15
  }
16
- import React, { useCallback, useRef, useState } from 'react';
16
+ import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react';
17
17
  import { Input } from '../index.js';
18
18
  import { styles as passCodeStyles } from './pass-code.styles.js';
19
- export function PassCode({ length , onComplete , className , ...props }) {
20
- const [passcode, setPasscode] = useState(Array.from({
19
+ export const PassCode = forwardRef(({ length , value , onChange , onComplete , className , type ='alphanumeric' , onBlur , ...props }, ref)=>{
20
+ const [internalPasscode, setInternalPasscode] = useState(Array.from({
21
21
  length
22
- }).map(()=>undefined));
22
+ }).map(()=>''));
23
+ const passcode = value ? value : internalPasscode;
23
24
  const inputRefs = useRef([]);
24
25
  const styles = passCodeStyles({});
26
+ useImperativeHandle(ref, ()=>{
27
+ return {
28
+ focus: ()=>{
29
+ var _inputRefs_current_;
30
+ (_inputRefs_current_ = inputRefs.current[0]) === null || _inputRefs_current_ === void 0 ? void 0 : _inputRefs_current_.focus();
31
+ },
32
+ clear: ()=>{
33
+ setInternalPasscode(Array.from({
34
+ length
35
+ }).map(()=>''));
36
+ }
37
+ };
38
+ });
25
39
  const handleChange = useCallback((index, event)=>{
26
- const value = event.target.value.slice(-1);
27
- const newPasscode = [
28
- ...passcode.slice(0, index),
29
- value,
30
- ...passcode.slice(index + 1)
31
- ];
32
- setPasscode(newPasscode);
33
- if (index < length - 1 && value !== '') {
34
- var _inputRefs_current_;
35
- (_inputRefs_current_ = inputRefs.current[index + 1]) === null || _inputRefs_current_ === void 0 ? void 0 : _inputRefs_current_.focus();
36
- }
37
- if (newPasscode.filter((passcode)=>!passcode).length === 0) {
38
- onComplete(newPasscode.join(''));
40
+ const inputValue = event.target.value.slice(-1);
41
+ if (type === 'numbers' && /^\d$/.test(inputValue) || type === 'letters' && /^[a-zA-Z]$/.test(inputValue) || type === 'alphanumeric' && /^[a-zA-Z0-9]$/.test(inputValue)) {
42
+ const newPasscode = [
43
+ ...passcode.slice(0, index),
44
+ inputValue,
45
+ ...passcode.slice(index + 1)
46
+ ];
47
+ if (onChange) {
48
+ onChange(newPasscode);
49
+ } else {
50
+ setInternalPasscode(newPasscode);
51
+ }
52
+ if (index < length - 1 && inputValue !== '') {
53
+ var _inputRefs_current_;
54
+ (_inputRefs_current_ = inputRefs.current[index + 1]) === null || _inputRefs_current_ === void 0 ? void 0 : _inputRefs_current_.focus();
55
+ }
56
+ if (newPasscode.filter((passcode)=>!passcode).length === 0 && onComplete) {
57
+ onComplete(newPasscode.join(''));
58
+ }
39
59
  }
40
60
  }, [
41
61
  passcode,
42
62
  length,
43
- onComplete
63
+ onChange,
64
+ onComplete,
65
+ type
44
66
  ]);
45
67
  const handlePaste = useCallback((index, event)=>{
46
68
  event.preventDefault();
47
69
  const pastedData = event.clipboardData.getData('text');
48
- const validData = pastedData.slice(0, length - index).split('');
70
+ const validData = pastedData.slice(0, length - index).split('').filter((char)=>{
71
+ if (type === 'numbers') return /^\d$/.test(char);
72
+ if (type === 'letters') return /^[a-zA-Z]$/.test(char);
73
+ return /^[a-zA-Z0-9]$/.test(char);
74
+ });
49
75
  const previousSlice = passcode.slice(0, index);
50
76
  const afterSlice = passcode.slice(index);
51
77
  const newPasscode = [
@@ -55,24 +81,34 @@ export function PassCode({ length , onComplete , className , ...props }) {
55
81
  ...afterSlice.slice(validData.length)
56
82
  ]
57
83
  ].slice(0, length);
58
- setPasscode(newPasscode);
59
- if (newPasscode.filter((passcode)=>!passcode).length === 0) {
84
+ if (onChange) {
85
+ onChange(newPasscode);
86
+ } else {
87
+ setInternalPasscode(newPasscode);
88
+ }
89
+ if (newPasscode.filter((passcode)=>!passcode).length === 0 && onComplete) {
60
90
  onComplete(newPasscode.join(''));
61
91
  }
62
92
  }, [
93
+ passcode,
63
94
  length,
95
+ onChange,
64
96
  onComplete,
65
- passcode
97
+ type
66
98
  ]);
67
99
  const handleKeyDown = useCallback((index, event)=>{
68
- if (event.key === 'Backspace' && index > 0) {
100
+ if (event.key === 'Backspace') {
69
101
  event.preventDefault();
70
102
  const newPasscode = [
71
103
  ...passcode.slice(0, index),
72
- undefined,
104
+ '',
73
105
  ...passcode.slice(index + 1)
74
106
  ];
75
- setPasscode(newPasscode);
107
+ if (onChange) {
108
+ onChange(newPasscode);
109
+ } else {
110
+ setInternalPasscode(newPasscode);
111
+ }
76
112
  const previousInput = inputRefs.current[index - 1];
77
113
  const currentInput = inputRefs.current[index];
78
114
  if (previousInput) {
@@ -83,7 +119,8 @@ export function PassCode({ length , onComplete , className , ...props }) {
83
119
  }
84
120
  }
85
121
  }, [
86
- passcode
122
+ passcode,
123
+ onChange
87
124
  ]);
88
125
  const handleFocus = useCallback((index)=>{
89
126
  var _inputRefs_current_index;
@@ -91,20 +128,32 @@ export function PassCode({ length , onComplete , className , ...props }) {
91
128
  }, [
92
129
  inputRefs
93
130
  ]);
131
+ const handleBlur = useCallback((index, event)=>{
132
+ if (onBlur) {
133
+ onBlur(index, event);
134
+ }
135
+ }, [
136
+ onBlur
137
+ ]);
94
138
  return React.createElement("div", _extends({}, props, {
95
139
  className: styles.base({
96
140
  className
97
141
  })
98
- }), passcode.map((digit, index)=>React.createElement(Input, {
142
+ }), Array.from({
143
+ length
144
+ }).map((_, index)=>React.createElement(Input, {
99
145
  size: "large",
100
146
  key: index,
101
- value: digit,
147
+ value: passcode[index] || '',
102
148
  onChange: (e)=>handleChange(index, e),
103
149
  onPaste: (e)=>handlePaste(index, e),
104
150
  onKeyDown: (e)=>handleKeyDown(index, e),
105
151
  onFocus: ()=>handleFocus(index),
152
+ onBlur: (e)=>handleBlur(index, e),
106
153
  ref: (input)=>inputRefs.current[index] = input,
107
154
  className: styles.input({}),
108
- "aria-label": `Passcode digit ${index + 1}`
155
+ "aria-label": `Passcode digit ${index + 1}`,
156
+ inputMode: type === 'numbers' ? 'numeric' : 'text'
109
157
  })));
110
- }
158
+ });
159
+ PassCode.displayName = 'PassCode';
@@ -1,7 +1,39 @@
1
- import { HTMLAttributes } from 'react';
1
+ import { FocusEvent, HTMLAttributes } from 'react';
2
2
  import { type VariantProps } from 'tailwind-variants';
3
3
  import { styles } from './pass-code.styles.js';
4
4
  export type PassCodeProps = {
5
+ /**
6
+ * Number of passcode inputs
7
+ */
5
8
  length: number;
6
- onComplete: (passcode: string) => void;
7
- } & VariantProps<typeof styles> & HTMLAttributes<Element>;
9
+ /**
10
+ * Callback when the input is blurred
11
+ */
12
+ onBlur?: (index: number, event: FocusEvent<HTMLInputElement>) => void;
13
+ /**
14
+ * Callback when the input value changes
15
+ */
16
+ onChange?: (passcode: string[]) => void;
17
+ /**
18
+ * Callback when the passcode is completely typed
19
+ */
20
+ onComplete?: (passcode: string) => void;
21
+ /**
22
+ * Type of passcode input
23
+ */
24
+ type?: 'numbers' | 'letters' | 'alphanumeric';
25
+ /**
26
+ * Value of the passcode input
27
+ */
28
+ value?: string[];
29
+ } & VariantProps<typeof styles> & Omit<HTMLAttributes<Element>, 'onChange'>;
30
+ export type PassCodeRef = {
31
+ /**
32
+ * Clear the passcode input, for non-controlled component only
33
+ */
34
+ clear: () => void;
35
+ /**
36
+ * Focus on the first input
37
+ */
38
+ focus: () => void;
39
+ };
@@ -3964,6 +3964,9 @@ body {
3964
3964
  .w-\[33\%\] {
3965
3965
  width: 33%;
3966
3966
  }
3967
+ .w-\[350px\] {
3968
+ width: 350px;
3969
+ }
3967
3970
  .w-\[36\.2ex\] {
3968
3971
  width: 36.2ex;
3969
3972
  }
@@ -3964,6 +3964,9 @@ body {
3964
3964
  .w-\[33\%\] {
3965
3965
  width: 33%;
3966
3966
  }
3967
+ .w-\[350px\] {
3968
+ width: 350px;
3969
+ }
3967
3970
  .w-\[36\.2ex\] {
3968
3971
  width: 36.2ex;
3969
3972
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@westpac/ui",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "license": "MIT",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -257,9 +257,9 @@
257
257
  "typescript": "^5.5.4",
258
258
  "vite": "^5.2.12",
259
259
  "vitest": "^0.30.1",
260
- "@westpac/ts-config": "~0.0.0",
260
+ "@westpac/eslint-config": "~0.4.0",
261
261
  "@westpac/test-config": "~0.0.0",
262
- "@westpac/eslint-config": "~0.4.0"
262
+ "@westpac/ts-config": "~0.0.0"
263
263
  },
264
264
  "dependencies": {
265
265
  "@duetds/date-picker": "~1.4.0",
@@ -1,97 +1,157 @@
1
+ /* eslint-disable sonarjs/cognitive-complexity */
1
2
  'use client';
2
3
 
3
- import React, { ChangeEvent, ClipboardEvent, KeyboardEvent, useCallback, useRef, useState } from 'react';
4
+ import React, {
5
+ ChangeEvent,
6
+ ClipboardEvent,
7
+ FocusEvent,
8
+ KeyboardEvent,
9
+ forwardRef,
10
+ useCallback,
11
+ useImperativeHandle,
12
+ useRef,
13
+ useState,
14
+ } from 'react';
4
15
 
5
16
  import { Input } from '../index.js';
6
17
 
7
18
  import { styles as passCodeStyles } from './pass-code.styles.js';
8
- import { type PassCodeProps } from './pass-code.types.js';
19
+ import { PassCodeProps, PassCodeRef } from './pass-code.types.js';
9
20
 
10
- export function PassCode({ length, onComplete, className, ...props }: PassCodeProps) {
11
- const [passcode, setPasscode] = useState<(string | undefined)[]>(Array.from({ length }).map(() => undefined));
12
- const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
21
+ export const PassCode = forwardRef<PassCodeRef, PassCodeProps>(
22
+ ({ length, value, onChange, onComplete, className, type = 'alphanumeric', onBlur, ...props }, ref) => {
23
+ const [internalPasscode, setInternalPasscode] = useState<string[]>(Array.from({ length }).map(() => ''));
24
+ const passcode = value ? value : internalPasscode;
25
+ const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
13
26
 
14
- const styles = passCodeStyles({});
27
+ const styles = passCodeStyles({});
15
28
 
16
- const handleChange = useCallback(
17
- (index: number, event: ChangeEvent<HTMLInputElement>) => {
18
- const value = event.target.value.slice(-1);
29
+ useImperativeHandle(ref, () => ({
30
+ focus: () => {
31
+ inputRefs.current[0]?.focus();
32
+ },
33
+ clear: () => {
34
+ setInternalPasscode(Array.from({ length }).map(() => ''));
35
+ },
36
+ }));
19
37
 
20
- // Update the passcode state
21
- const newPasscode = [...passcode.slice(0, index), value, ...passcode.slice(index + 1)];
22
- setPasscode(newPasscode);
38
+ const handleChange = useCallback(
39
+ (index: number, event: ChangeEvent<HTMLInputElement>) => {
40
+ const inputValue = event.target.value.slice(-1);
41
+ if (
42
+ (type === 'numbers' && /^\d$/.test(inputValue)) ||
43
+ (type === 'letters' && /^[a-zA-Z]$/.test(inputValue)) ||
44
+ (type === 'alphanumeric' && /^[a-zA-Z0-9]$/.test(inputValue))
45
+ ) {
46
+ const newPasscode = [...passcode.slice(0, index), inputValue, ...passcode.slice(index + 1)];
47
+ if (onChange) {
48
+ onChange(newPasscode);
49
+ } else {
50
+ setInternalPasscode(newPasscode);
51
+ }
23
52
 
24
- // Move to the next input if available
25
- if (index < length - 1 && value !== '') {
26
- inputRefs.current[index + 1]?.focus();
27
- }
28
-
29
- // Call onComplete when passcode is complete
30
- if (newPasscode.filter(passcode => !passcode).length === 0) {
31
- onComplete(newPasscode.join(''));
32
- }
33
- },
34
- [passcode, length, onComplete],
35
- );
36
-
37
- const handlePaste = useCallback(
38
- (index: number, event: ClipboardEvent<HTMLInputElement>) => {
39
- event.preventDefault();
40
- const pastedData = event.clipboardData.getData('text');
41
- const validData = pastedData.slice(0, length - index).split('');
42
- const previousSlice = passcode.slice(0, index);
43
- const afterSlice = passcode.slice(index);
44
- const newPasscode = [...previousSlice, ...[...validData, ...afterSlice.slice(validData.length)]].slice(0, length);
45
- setPasscode(newPasscode);
46
- if (newPasscode.filter(passcode => !passcode).length === 0) {
47
- onComplete(newPasscode.join(''));
48
- }
49
- },
50
- [length, onComplete, passcode],
51
- );
53
+ // Move to the next input if available
54
+ if (index < length - 1 && inputValue !== '') {
55
+ inputRefs.current[index + 1]?.focus();
56
+ }
57
+ if (newPasscode.filter(passcode => !passcode).length === 0 && onComplete) {
58
+ onComplete(newPasscode.join(''));
59
+ }
60
+ }
61
+ },
62
+ [passcode, length, onChange, onComplete, type],
63
+ );
52
64
 
53
- const handleKeyDown = useCallback(
54
- (index: number, event: KeyboardEvent<HTMLInputElement>) => {
55
- if (event.key === 'Backspace' && index > 0) {
65
+ const handlePaste = useCallback(
66
+ (index: number, event: ClipboardEvent<HTMLInputElement>) => {
56
67
  event.preventDefault();
57
- const newPasscode = [...passcode.slice(0, index), undefined, ...passcode.slice(index + 1)];
58
- setPasscode(newPasscode);
59
- const previousInput = inputRefs.current[index - 1];
60
- const currentInput = inputRefs.current[index];
61
- if (previousInput) {
62
- previousInput.focus();
68
+ const pastedData = event.clipboardData.getData('text');
69
+ const validData = pastedData
70
+ .slice(0, length - index)
71
+ .split('')
72
+ .filter(char => {
73
+ if (type === 'numbers') return /^\d$/.test(char);
74
+ if (type === 'letters') return /^[a-zA-Z]$/.test(char);
75
+ return /^[a-zA-Z0-9]$/.test(char);
76
+ });
77
+ const previousSlice = passcode.slice(0, index);
78
+ const afterSlice = passcode.slice(index);
79
+ const newPasscode = [...previousSlice, ...[...validData, ...afterSlice.slice(validData.length)]].slice(
80
+ 0,
81
+ length,
82
+ );
83
+ if (onChange) {
84
+ onChange(newPasscode);
85
+ } else {
86
+ setInternalPasscode(newPasscode);
87
+ }
88
+ if (newPasscode.filter(passcode => !passcode).length === 0 && onComplete) {
89
+ onComplete(newPasscode.join(''));
63
90
  }
64
- if (currentInput) {
65
- currentInput.value = '';
91
+ },
92
+ [passcode, length, onChange, onComplete, type],
93
+ );
94
+
95
+ const handleKeyDown = useCallback(
96
+ (index: number, event: KeyboardEvent<HTMLInputElement>) => {
97
+ if (event.key === 'Backspace') {
98
+ event.preventDefault();
99
+ const newPasscode = [...passcode.slice(0, index), '', ...passcode.slice(index + 1)];
100
+ if (onChange) {
101
+ onChange(newPasscode);
102
+ } else {
103
+ setInternalPasscode(newPasscode);
104
+ }
105
+ const previousInput = inputRefs.current[index - 1];
106
+ const currentInput = inputRefs.current[index];
107
+ if (previousInput) {
108
+ previousInput.focus();
109
+ }
110
+ if (currentInput) {
111
+ currentInput.value = '';
112
+ }
113
+ }
114
+ },
115
+ [passcode, onChange],
116
+ );
117
+
118
+ const handleFocus = useCallback(
119
+ (index: number) => {
120
+ inputRefs.current[index]?.select();
121
+ },
122
+ [inputRefs],
123
+ );
124
+
125
+ const handleBlur = useCallback(
126
+ (index: number, event: FocusEvent<HTMLInputElement>) => {
127
+ if (onBlur) {
128
+ onBlur(index, event);
66
129
  }
67
- }
68
- },
69
- [passcode],
70
- );
130
+ },
131
+ [onBlur],
132
+ );
71
133
 
72
- const handleFocus = useCallback(
73
- (index: number) => {
74
- inputRefs.current[index]?.select();
75
- },
76
- [inputRefs],
77
- );
134
+ return (
135
+ <div {...props} className={styles.base({ className })}>
136
+ {Array.from({ length }).map((_, index) => (
137
+ <Input
138
+ size="large"
139
+ key={index}
140
+ value={passcode[index] || ''}
141
+ onChange={e => handleChange(index, e)}
142
+ onPaste={e => handlePaste(index, e)}
143
+ onKeyDown={e => handleKeyDown(index, e)}
144
+ onFocus={() => handleFocus(index)}
145
+ onBlur={e => handleBlur(index, e)}
146
+ ref={input => (inputRefs.current[index] = input)}
147
+ className={styles.input({})}
148
+ aria-label={`Passcode digit ${index + 1}`}
149
+ inputMode={type === 'numbers' ? 'numeric' : 'text'}
150
+ />
151
+ ))}
152
+ </div>
153
+ );
154
+ },
155
+ );
78
156
 
79
- return (
80
- <div {...props} className={styles.base({ className })}>
81
- {passcode.map((digit, index) => (
82
- <Input
83
- size="large"
84
- key={index}
85
- value={digit}
86
- onChange={e => handleChange(index, e)}
87
- onPaste={e => handlePaste(index, e)}
88
- onKeyDown={e => handleKeyDown(index, e)}
89
- onFocus={() => handleFocus(index)}
90
- ref={input => (inputRefs.current[index] = input)}
91
- className={styles.input({})}
92
- aria-label={`Passcode digit ${index + 1}`}
93
- />
94
- ))}
95
- </div>
96
- );
97
- }
157
+ PassCode.displayName = 'PassCode';
@@ -1,10 +1,46 @@
1
- import { HTMLAttributes } from 'react';
1
+ import { FocusEvent, HTMLAttributes } from 'react';
2
2
  import { type VariantProps } from 'tailwind-variants';
3
3
 
4
4
  import { styles } from './pass-code.styles.js';
5
5
 
6
6
  export type PassCodeProps = {
7
+ /**
8
+ * Number of passcode inputs
9
+ */
7
10
  length: number;
8
- onComplete: (passcode: string) => void;
11
+ /**
12
+ * Callback when the input is blurred
13
+ */
14
+ onBlur?: (index: number, event: FocusEvent<HTMLInputElement>) => void;
15
+ /**
16
+ * Callback when the input value changes
17
+ */
18
+ onChange?: (passcode: string[]) => void;
19
+ /**
20
+ * Callback when the passcode is completely typed
21
+ */
22
+ onComplete?: (passcode: string) => void;
23
+ /**
24
+ * Type of passcode input
25
+ */
26
+ type?: 'numbers' | 'letters' | 'alphanumeric';
27
+ /**
28
+ * Value of the passcode input
29
+ */
30
+ value?: string[];
9
31
  } & VariantProps<typeof styles> &
10
- HTMLAttributes<Element>;
32
+ Omit<HTMLAttributes<Element>, 'onChange'>;
33
+
34
+ /*
35
+ * Passcode input ref used to access the passcode input functions via useImperativeHandle hook
36
+ */
37
+ export type PassCodeRef = {
38
+ /**
39
+ * Clear the passcode input, for non-controlled component only
40
+ */
41
+ clear: () => void;
42
+ /**
43
+ * Focus on the first input
44
+ */
45
+ focus: () => void;
46
+ };