Skip to content

Commit

Permalink
Merge pull request #940 from WestpacGEL/feature/passcode-updates
Browse files Browse the repository at this point in the history
Feature/passcode updates
  • Loading branch information
HZ991 authored Dec 3, 2024
2 parents eaa6cff + 649ee5e commit 19dd242
Show file tree
Hide file tree
Showing 5 changed files with 362 additions and 117 deletions.
3 changes: 1 addition & 2 deletions apps/site/src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { type ArticleRowsProps } from './components/home-page/home-page.types';

export default async function Homepage() {
const [urls, home] = await Promise.all([reader().singletons.url.read(), reader().singletons.homePage.readOrThrow()]);

const articleRows = await Promise.all(
home.articleRows.map(row => {
return new Promise<ArticleRowsProps>((resolve, reject) => {
Expand All @@ -30,7 +29,7 @@ export default async function Homepage() {
);

return (
<main className="pb-8 font-gel-sans text-gel-text">
<main className="font-gel-sans text-gel-text pb-8">
<Hero />
<ActionBar />
<HomePageContent articleRows={articleRows} />
Expand Down
220 changes: 140 additions & 80 deletions packages/ui/src/components/pass-code/pass-code.component.tsx
Original file line number Diff line number Diff line change
@@ -1,97 +1,157 @@
/* eslint-disable sonarjs/cognitive-complexity */
'use client';

import React, { ChangeEvent, ClipboardEvent, KeyboardEvent, useCallback, useRef, useState } from 'react';
import React, {
ChangeEvent,
ClipboardEvent,
FocusEvent,
KeyboardEvent,
forwardRef,
useCallback,
useImperativeHandle,
useRef,
useState,
} from 'react';

import { Input } from '../index.js';

import { styles as passCodeStyles } from './pass-code.styles.js';
import { type PassCodeProps } from './pass-code.types.js';
import { PassCodeProps, PassCodeRef } from './pass-code.types.js';

export function PassCode({ length, onComplete, className, ...props }: PassCodeProps) {
const [passcode, setPasscode] = useState<(string | undefined)[]>(Array.from({ length }).map(() => undefined));
const inputRefs = useRef<Array<HTMLInputElement | null>>([]);
export const PassCode = forwardRef<PassCodeRef, PassCodeProps>(
({ length, value, onChange, onComplete, className, type = 'alphanumeric', onBlur, ...props }, ref) => {
const [internalPasscode, setInternalPasscode] = useState<string[]>(Array.from({ length }).map(() => ''));
const passcode = value ? value : internalPasscode;
const inputRefs = useRef<Array<HTMLInputElement | null>>([]);

const styles = passCodeStyles({});
const styles = passCodeStyles({});

const handleChange = useCallback(
(index: number, event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value.slice(-1);
useImperativeHandle(ref, () => ({
focus: () => {
inputRefs.current[0]?.focus();
},
clear: () => {
setInternalPasscode(Array.from({ length }).map(() => ''));
},
}));

// Update the passcode state
const newPasscode = [...passcode.slice(0, index), value, ...passcode.slice(index + 1)];
setPasscode(newPasscode);
const handleChange = useCallback(
(index: number, event: ChangeEvent<HTMLInputElement>) => {
const inputValue = event.target.value.slice(-1);
if (
(type === 'numbers' && /^\d$/.test(inputValue)) ||
(type === 'letters' && /^[a-zA-Z]$/.test(inputValue)) ||
(type === 'alphanumeric' && /^[a-zA-Z0-9]$/.test(inputValue))
) {
const newPasscode = [...passcode.slice(0, index), inputValue, ...passcode.slice(index + 1)];
if (onChange) {
onChange(newPasscode);
} else {
setInternalPasscode(newPasscode);
}

// Move to the next input if available
if (index < length - 1 && value !== '') {
inputRefs.current[index + 1]?.focus();
}

// Call onComplete when passcode is complete
if (newPasscode.filter(passcode => !passcode).length === 0) {
onComplete(newPasscode.join(''));
}
},
[passcode, length, onComplete],
);

const handlePaste = useCallback(
(index: number, event: ClipboardEvent<HTMLInputElement>) => {
event.preventDefault();
const pastedData = event.clipboardData.getData('text');
const validData = pastedData.slice(0, length - index).split('');
const previousSlice = passcode.slice(0, index);
const afterSlice = passcode.slice(index);
const newPasscode = [...previousSlice, ...[...validData, ...afterSlice.slice(validData.length)]].slice(0, length);
setPasscode(newPasscode);
if (newPasscode.filter(passcode => !passcode).length === 0) {
onComplete(newPasscode.join(''));
}
},
[length, onComplete, passcode],
);
// Move to the next input if available
if (index < length - 1 && inputValue !== '') {
inputRefs.current[index + 1]?.focus();
}
if (newPasscode.filter(passcode => !passcode).length === 0 && onComplete) {
onComplete(newPasscode.join(''));
}
}
},
[passcode, length, onChange, onComplete, type],
);

const handleKeyDown = useCallback(
(index: number, event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Backspace' && index > 0) {
const handlePaste = useCallback(
(index: number, event: ClipboardEvent<HTMLInputElement>) => {
event.preventDefault();
const newPasscode = [...passcode.slice(0, index), undefined, ...passcode.slice(index + 1)];
setPasscode(newPasscode);
const previousInput = inputRefs.current[index - 1];
const currentInput = inputRefs.current[index];
if (previousInput) {
previousInput.focus();
const pastedData = event.clipboardData.getData('text');
const validData = pastedData
.slice(0, length - index)
.split('')
.filter(char => {
if (type === 'numbers') return /^\d$/.test(char);
if (type === 'letters') return /^[a-zA-Z]$/.test(char);
return /^[a-zA-Z0-9]$/.test(char);
});
const previousSlice = passcode.slice(0, index);
const afterSlice = passcode.slice(index);
const newPasscode = [...previousSlice, ...[...validData, ...afterSlice.slice(validData.length)]].slice(
0,
length,
);
if (onChange) {
onChange(newPasscode);
} else {
setInternalPasscode(newPasscode);
}
if (newPasscode.filter(passcode => !passcode).length === 0 && onComplete) {
onComplete(newPasscode.join(''));
}
if (currentInput) {
currentInput.value = '';
},
[passcode, length, onChange, onComplete, type],
);

const handleKeyDown = useCallback(
(index: number, event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Backspace') {
event.preventDefault();
const newPasscode = [...passcode.slice(0, index), '', ...passcode.slice(index + 1)];
if (onChange) {
onChange(newPasscode);
} else {
setInternalPasscode(newPasscode);
}
const previousInput = inputRefs.current[index - 1];
const currentInput = inputRefs.current[index];
if (previousInput) {
previousInput.focus();
}
if (currentInput) {
currentInput.value = '';
}
}
},
[passcode, onChange],
);

const handleFocus = useCallback(
(index: number) => {
inputRefs.current[index]?.select();
},
[inputRefs],
);

const handleBlur = useCallback(
(index: number, event: FocusEvent<HTMLInputElement>) => {
if (onBlur) {
onBlur(index, event);
}
}
},
[passcode],
);
},
[onBlur],
);

const handleFocus = useCallback(
(index: number) => {
inputRefs.current[index]?.select();
},
[inputRefs],
);
return (
<div {...props} className={styles.base({ className })}>
{Array.from({ length }).map((_, index) => (
<Input
size="large"
key={index}
value={passcode[index] || ''}
onChange={e => handleChange(index, e)}
onPaste={e => handlePaste(index, e)}
onKeyDown={e => handleKeyDown(index, e)}
onFocus={() => handleFocus(index)}
onBlur={e => handleBlur(index, e)}
ref={input => (inputRefs.current[index] = input)}
className={styles.input({})}
aria-label={`Passcode digit ${index + 1}`}
inputMode={type === 'numbers' ? 'numeric' : 'text'}
/>
))}
</div>
);
},
);

return (
<div {...props} className={styles.base({ className })}>
{passcode.map((digit, index) => (
<Input
size="large"
key={index}
value={digit}
onChange={e => handleChange(index, e)}
onPaste={e => handlePaste(index, e)}
onKeyDown={e => handleKeyDown(index, e)}
onFocus={() => handleFocus(index)}
ref={input => (inputRefs.current[index] = input)}
className={styles.input({})}
aria-label={`Passcode digit ${index + 1}`}
/>
))}
</div>
);
}
PassCode.displayName = 'PassCode';
Loading

0 comments on commit 19dd242

Please sign in to comment.