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

chore(clerk-js): Introduce Form.PasswordInput, InputGroup, Checkbox #2087

Merged
merged 5 commits into from
Nov 16, 2023
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
5 changes: 5 additions & 0 deletions .changeset/proud-hairs-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Refactor of internal input group, password field, and checkbox inputs in forms.
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ export const VerifiedDomainPage = withCardStateProvider(() => {

{allowsEdit && (
<Form.ControlRow elementId={deletePending.id}>
<Form.Control {...deletePending.props} />
<Form.Checkbox {...deletePending.props} />
</Form.ControlRow>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,11 @@ export const VerifyDomainPage = withCardStateProvider(() => {
>
<Form.Root onSubmit={onSubmitPrepare}>
<Form.ControlRow elementId={emailField.id}>
<Form.Control
<Form.InputGroup
{...emailField.props}
autoFocus
groupSuffix={`@${domain.name}`}
required
groupSuffix={emailDomainSuffix}
isRequired
/>
</Form.ControlRow>
<FormButtons isDisabled={!canSubmit} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const _ResetPassword = () => {
return console.error(clerkInvalidFAPIResponse(status, supportEmail));
}
} catch (e) {
handleError(e, [passwordField, confirmField], card.setError);
return handleError(e, [passwordField, confirmField], card.setError);
}
};

Expand Down Expand Up @@ -117,14 +117,14 @@ export const _ResetPassword = () => {
style={{ display: 'none' }}
/>
<Form.ControlRow elementId={passwordField.id}>
<Form.Control
<Form.PasswordInput
{...passwordField.props}
required
isRequired
autoFocus
/>
</Form.ControlRow>
<Form.ControlRow elementId={confirmField.id}>
<Form.Control
<Form.PasswordInput
{...confirmField.props}
onChange={e => {
if (e.target.value) {
Expand All @@ -136,7 +136,7 @@ export const _ResetPassword = () => {
</Form.ControlRow>
{!requiresNewPassword && (
<Form.ControlRow elementId={sessionsField.id}>
<Form.Control {...sessionsField.props} />
<Form.Checkbox {...sessionsField.props} />
</Form.ControlRow>
)}
<Form.SubmitButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export const SignInFactorOnePasswordCard = (props: SignInFactorOnePasswordProps)
style={{ display: 'none' }}
/>
<Form.ControlRow elementId={passwordControl.id}>
<Form.Control
<Form.PasswordInput
{...passwordControl.props}
autoFocus
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ const InstantPasswordRow = ({ field }: { field?: FormControlState<'password'> })
!field.value && !autofilled ? { opacity: 0, height: 0, pointerEvents: 'none', marginTop: '-1rem' } : undefined
}
>
<Form.Control
<Form.PasswordInput
{...field.props}
ref={ref}
tabIndex={!field.value ? -1 : undefined}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const SignUpForm = (props: SignUpFormProps) => {
)}
{shouldShow('password') && (
<Form.ControlRow elementId='password'>
<Form.Control
<Form.PasswordInput
{...{
...formState.password.props,
isRequired: fields.password!.required,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,39 +143,42 @@ export const PasswordPage = withCardStateProvider(() => {
/>
{user.passwordEnabled && (
<Form.ControlRow elementId={currentPasswordField.id}>
<Form.Control
<Form.PasswordInput
{...currentPasswordField.props}
minLength={6}
required
isRequired
autoFocus
isDisabled={passwordEditDisabled}
/>
</Form.ControlRow>
)}
<Form.ControlRow elementId={passwordField.id}>
<Form.Control
<Form.PasswordInput
{...passwordField.props}
minLength={6}
required
isRequired
autoFocus={!user.passwordEnabled}
isDisabled={passwordEditDisabled}
/>
</Form.ControlRow>
<Form.ControlRow elementId={confirmField.id}>
<Form.Control
<Form.PasswordInput
{...confirmField.props}
onChange={e => {
if (e.target.value) {
setConfirmPasswordFeedback(e.target.value);
}
return confirmField.props.onChange(e);
}}
isRequired
isDisabled={passwordEditDisabled}
/>
</Form.ControlRow>
<Form.ControlRow elementId={sessionsField.id}>
<Form.Control
<Form.Checkbox
{...sessionsField.props}
//TODO: localize this
description={'It is advised to logout of all other devices that may user an old password'}
isDisabled={passwordEditDisabled}
/>
</Form.ControlRow>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe('PasswordPage', () => {

expect(screen.getByLabelText(/new password/i)).toBeDisabled();
expect(screen.getByLabelText(/confirm password/i)).toBeDisabled();
expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).toBeDisabled();
expect(screen.getByRole('checkbox', { name: /sign out of all other devices/i })).toBeDisabled();

expect(
screen.getByText(
Expand Down Expand Up @@ -113,7 +113,7 @@ describe('PasswordPage', () => {

expect(screen.getByLabelText(/new password/i)).not.toBeDisabled();
expect(screen.getByLabelText(/confirm password/i)).not.toBeDisabled();
expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).not.toBeDisabled();
expect(screen.getByRole('checkbox', { name: /sign out of all other devices/i })).not.toBeDisabled();

expect(
screen.queryByText(
Expand Down Expand Up @@ -149,7 +149,7 @@ describe('PasswordPage', () => {
expect(screen.getByLabelText(/current password/i)).toBeDisabled();
expect(screen.getByLabelText(/new password/i)).toBeDisabled();
expect(screen.getByLabelText(/confirm password/i)).toBeDisabled();
expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).toBeDisabled();
expect(screen.getByRole('checkbox', { name: /sign out of all other devices/i })).toBeDisabled();

expect(
screen.getByText(
Expand Down Expand Up @@ -185,7 +185,7 @@ describe('PasswordPage', () => {
expect(screen.getByLabelText(/current password/i)).not.toBeDisabled();
expect(screen.getByLabelText(/new password/i)).not.toBeDisabled();
expect(screen.getByLabelText(/confirm password/i)).not.toBeDisabled();
expect(screen.getByRole('checkbox', { name: 'Sign out of all other devices' })).not.toBeDisabled();
expect(screen.getByRole('checkbox', { name: /sign out of all other devices/i })).not.toBeDisabled();

expect(
screen.queryByText(
Expand Down
94 changes: 91 additions & 3 deletions packages/clerk-js/src/ui.retheme/elements/FieldControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import { useFormControlFeedback } from '../utils';
import { useCardState } from './contexts';
import type { FormFeedbackProps } from './FormControl';
import { FormFeedback } from './FormControl';
import { RadioItem } from './RadioGroup';
import { InputGroup } from './InputGroup';
import { PasswordInput } from './PasswordInput';
import { RadioItem, RadioLabel } from './RadioGroup';

type FormControlProps = Omit<PropsOfComponent<typeof Input>, 'label' | 'placeholder' | 'disabled' | 'required'> &
ReturnType<typeof useFormControlUtil<FieldId>>['props'];
Expand All @@ -37,7 +39,6 @@ const Root = (props: PropsWithChildren<FormControlProps>) => {
setInfo,
setSuccess,
setWarning,
setHasPassedComplexity,
clearFeedback,
feedbackType,
feedback,
Expand All @@ -63,7 +64,6 @@ const Root = (props: PropsWithChildren<FormControlProps>) => {
setSuccess={setSuccess}
setWarning={setWarning}
setInfo={setInfo}
setHasPassedComplexity={setHasPassedComplexity}
clearFeedback={clearFeedback}
sx={sx}
>
Expand Down Expand Up @@ -199,6 +199,66 @@ const FieldFeedback = (props: Pick<FormFeedbackProps, 'elementDescriptors'>) =>
);
};

const PasswordInputElement = forwardRef<HTMLInputElement>((_, ref) => {
const { t } = useLocalizations();
const formField = useFormField();
const { placeholder, ...inputProps } = sanitizeInputProps(formField, [
'validatePassword',
'setError',
'setWarning',
'setSuccess',
'setInfo',
'setHasPassedComplexity',
]);

return (
// @ts-expect-error
<PasswordInput
ref={ref}
elementDescriptor={descriptors.formFieldInput}
elementId={descriptors.formFieldInput.setId(formField.fieldId)}
{...inputProps}
placeholder={t(placeholder)}
/>
);
});

const CheckboxIndicator = forwardRef<HTMLInputElement>((_, ref) => {
const formField = useFormField();
const { placeholder, ...inputProps } = sanitizeInputProps(formField);

return (
<Input
ref={ref}
{...inputProps}
elementDescriptor={descriptors.formFieldInput}
elementId={descriptors.formFieldInput.setId(formField.fieldId)}
focusRing={false}
sx={t => ({
width: 'fit-content',
marginTop: t.space.$0x5,
})}
type='checkbox'
/>
);
});

const CheckboxLabel = (props: { description?: string | LocalizationKey }) => {
const { label, id } = useFormField();

if (!label) {
return null;
}

return (
<RadioLabel
label={label}
id={id}
description={props.description}
/>
);
};

const InputElement = forwardRef<HTMLInputElement>((_, ref) => {
const { t } = useLocalizations();
const formField = useFormField();
Expand All @@ -214,12 +274,40 @@ const InputElement = forwardRef<HTMLInputElement>((_, ref) => {
);
});

const InputGroupElement = forwardRef<
HTMLInputElement,
{
groupPrefix?: string;
groupSuffix?: string;
}
>((props, ref) => {
const { t } = useLocalizations();
const formField = useFormField();
const { placeholder, ...inputProps } = sanitizeInputProps(formField);

return (
<InputGroup
ref={ref}
elementDescriptor={descriptors.formFieldInput}
elementId={descriptors.formFieldInput.setId(formField.fieldId)}
{...inputProps}
groupPrefix={props.groupPrefix}
groupSuffix={props.groupSuffix}
placeholder={t(placeholder)}
/>
);
});

export const Field = {
Root: Root,
Label: FieldLabel,
LabelRow: FieldLabelRow,
Input: InputElement,
PasswordInput: PasswordInputElement,
InputGroup: InputGroupElement,
RadioItem: RadioItem,
CheckboxIndicator: CheckboxIndicator,
CheckboxLabel: CheckboxLabel,
Action: FieldAction,
AsOptional: FieldOptionalLabel,
LabelIcon: FieldLabelIcon,
Expand Down
40 changes: 40 additions & 0 deletions packages/clerk-js/src/ui.retheme/elements/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,43 @@ const PlainInput = (props: CommonInputProps) => {
);
};

const PasswordInput = (props: CommonInputProps) => {
return (
<CommonInputWrapper {...props}>
<Field.PasswordInput />
</CommonInputWrapper>
);
};

const InputGroup = (
props: CommonInputProps & {
groupPrefix?: string;
groupSuffix?: string;
},
) => {
const { groupSuffix, groupPrefix, ...fieldProps } = props;
return (
<CommonInputWrapper {...fieldProps}>
<Field.InputGroup {...{ groupSuffix, groupPrefix }} />
</CommonInputWrapper>
);
};

const Checkbox = (
props: CommonFieldRootProps & {
description?: string | LocalizationKey;
},
) => {
return (
<Field.Root {...props}>
<Flex align='start'>
<Field.CheckboxIndicator />
<Field.CheckboxLabel description={props.description} />
</Flex>
</Field.Root>
);
};

const RadioGroup = (
props: Omit<PropsOfComponent<typeof Field.Root>, 'infoText' | 'type' | 'validatePassword' | 'label' | 'placeholder'>,
) => {
Expand Down Expand Up @@ -189,7 +226,10 @@ export const Form = {
*/
Control: FormControl,
PlainInput,
PasswordInput,
InputGroup,
RadioGroup,
Checkbox,
SubmitButton: FormSubmit,
ResetButton: FormReset,
};
Expand Down
Loading
Loading