diff --git a/.changeset/warm-news-wonder.md b/.changeset/warm-news-wonder.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/warm-news-wonder.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/wet-bats-tickle.md b/.changeset/wet-bats-tickle.md new file mode 100644 index 0000000000..50b3cbc194 --- /dev/null +++ b/.changeset/wet-bats-tickle.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Inputs will now trim usernames and email addresses since whitespace as a prefix or suffix is invalid in these fields. diff --git a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx index 67bc05d34b..7142db7e1f 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx @@ -82,6 +82,7 @@ export function _SignInStart(): JSX.Element { const textIdentifierField = useFormControl('identifier', initialValues[identifierAttribute] || '', { ...currentIdentifier, isRequired: true, + transformer: value => value.trim(), }); const phoneIdentifierField = useFormControl('identifier', initialValues['phone_number'] || '', { diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx index 65b12c2ebb..a090f6d02a 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx @@ -68,6 +68,7 @@ function _SignUpStart(): JSX.Element { type: 'text', label: localizationKeys('formFieldLabel__username'), placeholder: localizationKeys('formFieldInputPlaceholder__username'), + transformer: value => value.trim(), }), phoneNumber: useFormControl('phoneNumber', signUp.phoneNumber || initialValues.phoneNumber || '', { type: 'tel', diff --git a/packages/clerk-js/src/ui/primitives/Input.tsx b/packages/clerk-js/src/ui/primitives/Input.tsx index 212866e49b..717c4b120b 100644 --- a/packages/clerk-js/src/ui/primitives/Input.tsx +++ b/packages/clerk-js/src/ui/primitives/Input.tsx @@ -76,12 +76,7 @@ export const Input = React.forwardRef((props, ref) * type="email" will not allow characters like this one "รถ", instead remove type email and provide a pattern that accepts any character before the "@" symbol */ - const typeProps = - type === 'email' - ? { - pattern: '^.*@[a-zA-Z0-9\\-]+\\.[a-zA-Z0-9\\-\\.]+$', - } - : { type }; + const typeProps = type === 'email' ? { pattern: '^.*@[a-zA-Z0-9\\-]+\\.[a-zA-Z0-9\\-\\.]+$' } : { type }; const passwordManagerProps = ignorePasswordManager ? { diff --git a/packages/clerk-js/src/ui/utils/useFormControl.ts b/packages/clerk-js/src/ui/utils/useFormControl.ts index 7144a7301f..dddb506063 100644 --- a/packages/clerk-js/src/ui/utils/useFormControl.ts +++ b/packages/clerk-js/src/ui/utils/useFormControl.ts @@ -8,10 +8,13 @@ import { useLocalizations } from '../localization'; type SelectOption = { value: string; label?: string }; +type Transformer = (value: string) => string; + type Options = { isRequired?: boolean; placeholder?: string | LocalizationKey; options?: SelectOption[]; + transformer?: Transformer; defaultChecked?: boolean; infoText?: LocalizationKey | string; } & ( @@ -75,12 +78,21 @@ export type FormControlState = FieldStateProps & { export type FeedbackType = 'success' | 'error' | 'warning' | 'info'; +const emailTransformer = (v: string) => v.trim(); +const applyTransformers = (v: string, transformers: Transformer[]) => { + let value = v; + for (let i = 0; i < transformers.length; i++) { + value = transformers[i](value); + } + return value; +}; + export const useFormControl = ( id: Id, initialState: string, opts?: Options, ): FormControlState => { - opts = opts || { + const options = opts || { type: 'text', label: '', isRequired: false, @@ -88,11 +100,18 @@ export const useFormControl = ( options: [], defaultChecked: false, }; + const transformers: Transformer[] = []; + if (options.transformer) { + transformers.push(options.transformer); + } + if (options.type === 'email') { + transformers.push(emailTransformer); + } const { translateError, t } = useLocalizations(); - const [value, setValueInternal] = useState(initialState); + const [value, setValueInternal] = useState(applyTransformers(initialState, transformers)); const [isFocused, setFocused] = useState(false); - const [checked, setCheckedInternal] = useState(opts?.defaultChecked || false); + const [checked, setCheckedInternal] = useState(options?.defaultChecked || false); const [hasPassedComplexity, setHasPassedComplexity] = useState(false); const [feedback, setFeedback] = useState<{ message: string; type: FeedbackType }>({ message: '', @@ -100,10 +119,10 @@ export const useFormControl = ( }); const onChange: FormControlState['onChange'] = event => { - if (opts?.type === 'checkbox') { + if (options?.type === 'checkbox') { return setCheckedInternal(event.target.checked); } - return setValueInternal(event.target.value || ''); + return setValueInternal(applyTransformers(event.target.value || '', transformers)); }; const setValue: FormControlState['setValue'] = val => setValueInternal(val || ''); @@ -135,7 +154,7 @@ export const useFormControl = ( setFocused(false); }; - const { defaultChecked, validatePassword: validatePasswordProp, buildErrorMessage, ...restOpts } = opts; + const { defaultChecked, validatePassword: validatePasswordProp, buildErrorMessage, ...restOpts } = options; const props = { id, @@ -148,13 +167,13 @@ export const useFormControl = ( onBlur, onFocus, setWarning, - feedback: feedback.message || t(opts.infoText), + feedback: feedback.message || t(options.infoText), feedbackType: feedback.type, setInfo, clearFeedback, hasPassedComplexity, setHasPassedComplexity, - validatePassword: opts.type === 'password' ? opts.validatePassword : undefined, + validatePassword: options.type === 'password' ? options.validatePassword : undefined, isFocused, ...restOpts, };