mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
parent
254fac52a7
commit
20f7ad9863
8 changed files with 94 additions and 94 deletions
|
@ -13,7 +13,6 @@ type Value = { countryCallingCode?: CountryCallingCode; nationalNumber?: string
|
|||
|
||||
export type Props = {
|
||||
name: string;
|
||||
autoComplete?: AutoCompleteType;
|
||||
// eslint-disable-next-line react/boolean-prop-naming
|
||||
autoFocus?: boolean;
|
||||
className?: string;
|
||||
|
@ -27,7 +26,6 @@ export type Props = {
|
|||
|
||||
const PhoneInput = ({
|
||||
name,
|
||||
autoComplete,
|
||||
autoFocus,
|
||||
className,
|
||||
placeholder,
|
||||
|
@ -50,6 +48,7 @@ const PhoneInput = ({
|
|||
<span>{`+${countryCallingCode}`}</span>
|
||||
<DownArrowIcon />
|
||||
<select
|
||||
autoComplete="tel-country-code"
|
||||
onChange={({ target: { value } }) => {
|
||||
onChange({ countryCallingCode: value });
|
||||
|
||||
|
@ -77,13 +76,13 @@ const PhoneInput = ({
|
|||
{countrySelector}
|
||||
<input
|
||||
ref={inputReference}
|
||||
type="tel"
|
||||
inputMode="tel"
|
||||
autoComplete="tel-national"
|
||||
autoFocus={autoFocus}
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
value={nationalNumber}
|
||||
type="tel"
|
||||
inputMode="numeric"
|
||||
autoComplete={autoComplete}
|
||||
onFocus={() => {
|
||||
setOnFocus(true);
|
||||
}}
|
||||
|
|
|
@ -83,7 +83,7 @@ describe('Passcode Component', () => {
|
|||
|
||||
if (inputElements[2]) {
|
||||
fireEvent.input(inputElements[2], { target: { value: '37' } });
|
||||
expect(onChange).toBeCalledWith(['1', '2', '7', '4', '5', '6']);
|
||||
expect(onChange).toBeCalledWith(['1', '2', '3', '7', '5', '6']);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ export type Props = {
|
|||
onChange: (value: string[]) => void;
|
||||
};
|
||||
|
||||
const isNumeric = (char: string) => /^\d*$/.test(char);
|
||||
const isNumeric = (char: string) => /^\d+$/.test(char);
|
||||
|
||||
const normalize = (value: string[], length: number): string[] => {
|
||||
if (value.length > length) {
|
||||
|
@ -38,15 +38,6 @@ const normalize = (value: string[], length: number): string[] => {
|
|||
return value;
|
||||
};
|
||||
|
||||
const trim = (oldValue: string | undefined, newValue: string) => {
|
||||
// Pop oldValue from the latest input to get the updated Digit
|
||||
if (newValue.length > 1 && oldValue) {
|
||||
return newValue.replace(oldValue, '');
|
||||
}
|
||||
|
||||
return newValue;
|
||||
};
|
||||
|
||||
const Passcode = ({ name, className, value, length = defaultLength, error, onChange }: Props) => {
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
const inputReferences = useRef<Array<HTMLInputElement | null>>(
|
||||
|
@ -56,6 +47,32 @@ const Passcode = ({ name, className, value, length = defaultLength, error, onCha
|
|||
|
||||
const codes = useMemo(() => normalize(value, length), [length, value]);
|
||||
|
||||
const updateValue = useCallback(
|
||||
(data: string, targetId: number) => {
|
||||
// Filter non-numeric input
|
||||
if (!isNumeric(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chars = data.split('');
|
||||
const trimmedChars = chars.slice(0, Math.min(chars.length, codes.length - targetId));
|
||||
|
||||
const value = [
|
||||
...codes.slice(0, targetId),
|
||||
...trimmedChars,
|
||||
...codes.slice(targetId + trimmedChars.length, codes.length),
|
||||
];
|
||||
|
||||
onChange(value);
|
||||
|
||||
// Move to the next target
|
||||
const nextTarget =
|
||||
inputReferences.current[Math.min(targetId + trimmedChars.length, codes.length - 1)];
|
||||
nextTarget?.focus();
|
||||
},
|
||||
[codes, onChange]
|
||||
);
|
||||
|
||||
const onInputHandler: FormEventHandler<HTMLInputElement> = useCallback(
|
||||
(event) => {
|
||||
const { target } = event;
|
||||
|
@ -67,72 +84,16 @@ const Passcode = ({ name, className, value, length = defaultLength, error, onCha
|
|||
const { value, dataset } = target;
|
||||
|
||||
// Unrecognized target input field
|
||||
if (dataset.id === undefined) {
|
||||
if (!dataset.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
// Filter non-numeric input
|
||||
if (!isNumeric(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetId = Number(dataset.id);
|
||||
|
||||
// Update the root input value
|
||||
onChange(Object.assign([], codes, { [targetId]: trim(codes[targetId], value) }));
|
||||
|
||||
// Move to the next target
|
||||
if (value) {
|
||||
const nextTarget = inputReferences.current[targetId + 1];
|
||||
nextTarget?.focus();
|
||||
}
|
||||
updateValue(value, Number(dataset.id));
|
||||
},
|
||||
[codes, onChange]
|
||||
[updateValue]
|
||||
);
|
||||
|
||||
const onKeyDownHandler: KeyboardEventHandler<HTMLInputElement> = useCallback((event) => {
|
||||
const { key, target } = event;
|
||||
|
||||
if (!(target instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { value, dataset } = target;
|
||||
|
||||
if (!dataset.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetId = Number(dataset.id);
|
||||
|
||||
const nextTarget = inputReferences.current[targetId + 1];
|
||||
const previousTarget = inputReferences.current[targetId - 1];
|
||||
|
||||
switch (key) {
|
||||
case 'Backspace':
|
||||
if (!value) {
|
||||
previousTarget?.focus();
|
||||
}
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
event.preventDefault();
|
||||
previousTarget?.focus();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
event.preventDefault();
|
||||
nextTarget?.focus();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onPasteHandler: ClipboardEventHandler<HTMLInputElement> = useCallback(
|
||||
(event) => {
|
||||
if (!(event.target instanceof HTMLInputElement)) {
|
||||
|
@ -144,29 +105,64 @@ const Passcode = ({ name, className, value, length = defaultLength, error, onCha
|
|||
clipboardData,
|
||||
} = event;
|
||||
|
||||
const data = clipboardData.getData('text');
|
||||
|
||||
// Unrecognized target input field
|
||||
if (!dataset.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
updateValue(data, Number(dataset.id));
|
||||
},
|
||||
[updateValue]
|
||||
);
|
||||
|
||||
const data = clipboardData.getData('text');
|
||||
const onKeyDownHandler: KeyboardEventHandler<HTMLInputElement> = useCallback(
|
||||
(event) => {
|
||||
const { key, target } = event;
|
||||
|
||||
if (!data || !isNumeric(data)) {
|
||||
if (!(target instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { value, dataset } = target;
|
||||
|
||||
if (!dataset.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chars = data.split('');
|
||||
const targetId = Number(dataset.id);
|
||||
const trimmedChars = chars.slice(0, Math.min(chars.length, codes.length - targetId));
|
||||
|
||||
const value = [
|
||||
...codes.slice(0, targetId),
|
||||
...trimmedChars,
|
||||
...codes.slice(targetId + trimmedChars.length, codes.length),
|
||||
];
|
||||
const nextTarget = inputReferences.current[targetId + 1];
|
||||
const previousTarget = inputReferences.current[targetId - 1];
|
||||
|
||||
onChange(value);
|
||||
switch (key) {
|
||||
case 'Backspace':
|
||||
event.preventDefault();
|
||||
|
||||
if (!value) {
|
||||
previousTarget?.focus();
|
||||
break;
|
||||
}
|
||||
|
||||
onChange(Object.assign([], codes, { [targetId]: '' }));
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
event.preventDefault();
|
||||
previousTarget?.focus();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
event.preventDefault();
|
||||
nextTarget?.focus();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[codes, onChange]
|
||||
);
|
||||
|
@ -191,9 +187,9 @@ const Passcode = ({ name, className, value, length = defaultLength, error, onCha
|
|||
name={`${name}_${index}`}
|
||||
data-id={index}
|
||||
value={codes[index]}
|
||||
type="text" // Number type allows 'e' as input but returns empty value
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
maxLength={2} // Allow overwrite input
|
||||
pattern="[0-9]*"
|
||||
autoComplete="off"
|
||||
onPaste={onPasteHandler}
|
||||
onInput={onInputHandler}
|
||||
|
|
|
@ -94,6 +94,7 @@ const CreateAccount = ({ className, autoFocus }: Props) => {
|
|||
className={styles.inputField}
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
placeholder={t('input.password')}
|
||||
{...fieldRegister('password', passwordValidation)}
|
||||
onClear={() => {
|
||||
|
@ -104,6 +105,7 @@ const CreateAccount = ({ className, autoFocus }: Props) => {
|
|||
className={styles.inputField}
|
||||
name="confirm_password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
placeholder={t('input.confirm_password')}
|
||||
{...fieldRegister('confirmPassword', (confirmPassword) =>
|
||||
confirmPasswordValidation(fieldValue.password, confirmPassword)
|
||||
|
|
|
@ -99,11 +99,13 @@ const EmailPasswordless = ({ type, autoFocus, className }: Props) => {
|
|||
<>
|
||||
<form className={classNames(styles.form, className)}>
|
||||
<Input
|
||||
autoFocus={autoFocus}
|
||||
className={styles.inputField}
|
||||
type="email"
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
inputMode="email"
|
||||
placeholder={t('input.email')}
|
||||
autoFocus={autoFocus}
|
||||
className={styles.inputField}
|
||||
{...register('email', emailValidation)}
|
||||
onClear={() => {
|
||||
setFieldValue((state) => ({ ...state, email: '' }));
|
||||
|
|
|
@ -115,9 +115,8 @@ const PhonePasswordless = ({ type, autoFocus, className }: Props) => {
|
|||
<form className={classNames(styles.form, className)}>
|
||||
<PhoneInput
|
||||
name="phone"
|
||||
className={styles.inputField}
|
||||
autoComplete="mobile"
|
||||
placeholder={t('input.phone_number')}
|
||||
className={styles.inputField}
|
||||
countryCallingCode={phoneNumber.countryCallingCode}
|
||||
nationalNumber={phoneNumber.nationalNumber}
|
||||
autoFocus={autoFocus}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { useEffect, useState } from 'react';
|
|||
import * as styles from '@/App.module.scss';
|
||||
import { Context } from '@/hooks/use-page-context';
|
||||
import initI18n from '@/i18n/init';
|
||||
import { SignInExperienceSettingsResponse, Platform } from '@/types';
|
||||
import { SignInExperienceSettingsResponse, SignInExperienceSettings, Platform } from '@/types';
|
||||
import { parseQueryParameters } from '@/utils';
|
||||
import { getPrimarySignInMethod, getSecondarySignInMethods } from '@/utils/sign-in-experience';
|
||||
import { filterPreviewSocialConnectors } from '@/utils/social-connectors';
|
||||
|
@ -66,7 +66,7 @@ const usePreview = (context: Context): [boolean, PreviewConfig?] => {
|
|||
platform,
|
||||
} = previewConfig;
|
||||
|
||||
const experienceSettings = {
|
||||
const experienceSettings: SignInExperienceSettings = {
|
||||
...rest,
|
||||
branding: {
|
||||
...branding,
|
||||
|
|
4
packages/ui/src/include.d/dom.d.ts
vendored
4
packages/ui/src/include.d/dom.d.ts
vendored
|
@ -69,7 +69,9 @@ type AutoCompleteType =
|
|||
| 'sex'
|
||||
| 'url'
|
||||
| 'photo'
|
||||
| 'mobile';
|
||||
| 'tel'
|
||||
| 'tel-country-code'
|
||||
| 'tel-national';
|
||||
|
||||
// TO-DO: remove me
|
||||
interface Body {
|
||||
|
|
Loading…
Reference in a new issue