Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/passcode updates #940

Merged
merged 2 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading