Skip to content

Commit

Permalink
chore(clerk-js): Introduce Form.RadioGroup (#2034)
Browse files Browse the repository at this point in the history
* chore(clerk-js): Introduce Form.RadioGroup

* chore(clerk-js): Address PR comments
  • Loading branch information
panteliselef authored Nov 7, 2023
1 parent 213546e commit db3eefe
Show file tree
Hide file tree
Showing 6 changed files with 545 additions and 68 deletions.
5 changes: 5 additions & 0 deletions .changeset/slow-wombats-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Refactor of internal radio input in forms.
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { OrganizationDomainResource, OrganizationEnrollmentMode } from '@clerk/types';
import type {
OrganizationDomainResource,
OrganizationEnrollmentMode,
OrganizationSettingsResource,
} from '@clerk/types';

import { CalloutWithAction, useGate } from '../../common';
import { useCoreOrganization, useEnvironment } from '../../contexts';
Expand Down Expand Up @@ -51,6 +55,46 @@ const useCalloutLabel = (
];
};

const buildEnrollmentOptions = (settings: OrganizationSettingsResource) => {
const _options = [];
if (settings.domains.enrollmentModes.includes('manual_invitation')) {
_options.push({
value: 'manual_invitation',
label: localizationKeys('organizationProfile.verifiedDomainPage.enrollmentTab.manualInvitationOption__label'),
description: localizationKeys(
'organizationProfile.verifiedDomainPage.enrollmentTab.manualInvitationOption__description',
),
});
}

if (settings.domains.enrollmentModes.includes('automatic_invitation')) {
_options.push({
value: 'automatic_invitation',
label: localizationKeys('organizationProfile.verifiedDomainPage.enrollmentTab.automaticInvitationOption__label'),
description: localizationKeys(
'organizationProfile.verifiedDomainPage.enrollmentTab.automaticInvitationOption__description',
),
});
}

if (settings.domains.enrollmentModes.includes('automatic_suggestion')) {
_options.push({
value: 'automatic_suggestion',
label: localizationKeys('organizationProfile.verifiedDomainPage.enrollmentTab.automaticSuggestionOption__label'),
description: localizationKeys(
'organizationProfile.verifiedDomainPage.enrollmentTab.automaticSuggestionOption__description',
),
});
}

return _options;
};

const useEnrollmentOptions = () => {
const { organizationSettings } = useEnvironment();
return buildEnrollmentOptions(organizationSettings);
};

export const VerifiedDomainPage = withCardStateProvider(() => {
const card = useCardState();
const { organizationSettings } = useEnvironment();
Expand All @@ -71,49 +115,11 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
const breadcrumbTitle = localizationKeys('organizationProfile.profilePage.domainSection.title');
const allowsEdit = mode === 'edit';

const enrollmentOptions = useEnrollmentOptions();
const enrollmentMode = useFormControl('enrollmentMode', '', {
type: 'radio',
radioOptions: [
...(organizationSettings.domains.enrollmentModes.includes('manual_invitation')
? [
{
value: 'manual_invitation',
label: localizationKeys(
'organizationProfile.verifiedDomainPage.enrollmentTab.manualInvitationOption__label',
),
description: localizationKeys(
'organizationProfile.verifiedDomainPage.enrollmentTab.manualInvitationOption__description',
),
},
]
: []),
...(organizationSettings.domains.enrollmentModes.includes('automatic_invitation')
? [
{
value: 'automatic_invitation',
label: localizationKeys(
'organizationProfile.verifiedDomainPage.enrollmentTab.automaticInvitationOption__label',
),
description: localizationKeys(
'organizationProfile.verifiedDomainPage.enrollmentTab.automaticInvitationOption__description',
),
},
]
: []),
...(organizationSettings.domains.enrollmentModes.includes('automatic_suggestion')
? [
{
value: 'automatic_suggestion',
label: localizationKeys(
'organizationProfile.verifiedDomainPage.enrollmentTab.automaticSuggestionOption__label',
),
description: localizationKeys(
'organizationProfile.verifiedDomainPage.enrollmentTab.automaticSuggestionOption__description',
),
},
]
: []),
],
radioOptions: enrollmentOptions,
isRequired: true,
});

const deletePending = useFormControl('deleteExistingInvitationsSuggestions', '', {
Expand Down Expand Up @@ -252,7 +258,7 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
gap={6}
>
<Form.ControlRow elementId={enrollmentMode.id}>
<Form.Control {...enrollmentMode.props} />
<Form.RadioGroup {...enrollmentMode.props} />
</Form.ControlRow>

{allowsEdit && (
Expand Down
2 changes: 2 additions & 0 deletions packages/clerk-js/src/ui/elements/FieldControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { useFormControlFeedback } from '../utils';
import { useCardState } from './contexts';
import type { FormFeedbackProps } from './FormControl';
import { FormFeedback } from './FormControl';
import { RadioItem } from './RadioGroup';

type FormControlProps = Omit<PropsOfComponent<typeof Input>, 'label' | 'placeholder' | 'disabled' | 'required'> &
ReturnType<typeof useFormControlUtil<FieldId>>['props'];
Expand Down Expand Up @@ -218,6 +219,7 @@ export const Field = {
Label: FieldLabel,
LabelRow: FieldLabelRow,
Input: InputElement,
RadioItem: RadioItem,
Action: FieldAction,
AsOptional: FieldOptionalLabel,
LabelIcon: FieldLabelIcon,
Expand Down
70 changes: 46 additions & 24 deletions packages/clerk-js/src/ui/elements/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { PropsWithChildren } from 'react';
import React, { useState } from 'react';

import type { LocalizationKey } from '../customizables';
import { Button, descriptors, Flex, Form as FormPrim, localizationKeys } from '../customizables';
import { Button, Col, descriptors, Flex, Form as FormPrim, localizationKeys } from '../customizables';
import { useLoadingStatus } from '../hooks';
import type { PropsOfComponent } from '../styledSystem';
import { useCardState } from './contexts';
Expand Down Expand Up @@ -76,35 +76,31 @@ const FormRoot = (props: FormProps): JSX.Element => {
const FormSubmit = (props: PropsOfComponent<typeof Button>) => {
const { isLoading, isDisabled } = useFormState();
return (
<>
<Button
elementDescriptor={descriptors.formButtonPrimary}
block
textVariant='buttonExtraSmallBold'
isLoading={isLoading}
isDisabled={isDisabled}
type='submit'
{...props}
localizationKey={props.localizationKey || localizationKeys('formButtonPrimary')}
/>
</>
<Button
elementDescriptor={descriptors.formButtonPrimary}
block
textVariant='buttonExtraSmallBold'
isLoading={isLoading}
isDisabled={isDisabled}
type='submit'
{...props}
localizationKey={props.localizationKey || localizationKeys('formButtonPrimary')}
/>
);
};

const FormReset = (props: PropsOfComponent<typeof Button>) => {
const { isLoading, isDisabled } = useFormState();
return (
<>
<Button
elementDescriptor={descriptors.formButtonReset}
block
variant='ghost'
textVariant='buttonExtraSmallBold'
type='reset'
isDisabled={isLoading || isDisabled}
{...props}
/>
</>
<Button
elementDescriptor={descriptors.formButtonReset}
block
variant='ghost'
textVariant='buttonExtraSmallBold'
type='reset'
isDisabled={isLoading || isDisabled}
{...props}
/>
);
};

Expand Down Expand Up @@ -160,6 +156,31 @@ const PlainInput = (props: CommonInputProps) => {
);
};

const RadioGroup = (
props: Omit<PropsOfComponent<typeof Field.Root>, 'infoText' | 'type' | 'validatePassword' | 'label' | 'placeholder'>,
) => {
const { radioOptions, ...fieldProps } = props;
return (
<Field.Root {...fieldProps}>
<Col
elementDescriptor={descriptors.formFieldRadioGroup}
gap={2}
>
{radioOptions?.map(({ value, description, label }) => (
<Field.RadioItem
key={value}
value={value}
label={label}
description={description}
/>
))}
</Col>

<Field.Feedback />
</Field.Root>
);
};

export const Form = {
Root: FormRoot,
ControlRow: FormControlRow,
Expand All @@ -168,6 +189,7 @@ export const Form = {
*/
Control: FormControl,
PlainInput,
RadioGroup,
SubmitButton: FormSubmit,
ResetButton: FormReset,
};
Expand Down
93 changes: 92 additions & 1 deletion packages/clerk-js/src/ui/elements/RadioGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { useId } from 'react';
import { forwardRef, useId } from 'react';

import type { LocalizationKey } from '../customizables';
import { Col, descriptors, Flex, FormLabel, Input, Text } from '../customizables';
import { sanitizeInputProps, useFormField } from '../primitives/hooks';
import type { PropsOfComponent } from '../styledSystem';

/**
* @deprecated
*/
export const RadioGroup = (
props: PropsOfComponent<typeof Input> & {
radioOptions?: {
Expand All @@ -30,6 +34,9 @@ export const RadioGroup = (
);
};

/**
* @deprecated
*/
const RadioGroupItem = (props: {
inputProps: PropsOfComponent<typeof Input>;
value: string;
Expand Down Expand Up @@ -86,3 +93,87 @@ const RadioGroupItem = (props: {
</Flex>
);
};

const RadioIndicator = forwardRef<HTMLInputElement, { value: string; id: string }>((props, ref) => {
const formField = useFormField();
const { value, placeholder, ...inputProps } = sanitizeInputProps(formField);

return (
<Input
ref={ref}
{...inputProps}
elementDescriptor={descriptors.formFieldRadioInput}
id={props.id}
focusRing={false}
sx={t => ({
width: 'fit-content',
marginTop: t.space.$0x5,
})}
type='radio'
value={props.value}
checked={props.value === value}
/>
);
});

export const RadioLabel = (props: {
label: string | LocalizationKey;
description?: string | LocalizationKey;
id?: string;
}) => {
return (
<FormLabel
elementDescriptor={descriptors.formFieldRadioLabel}
htmlFor={props.id}
sx={t => ({
padding: `${t.space.$none} ${t.space.$2}`,
display: 'flex',
flexDirection: 'column',
})}
>
<Text
elementDescriptor={descriptors.formFieldRadioLabelTitle}
variant='regularMedium'
localizationKey={props.label}
/>

{props.description && (
<Text
elementDescriptor={descriptors.formFieldRadioLabelDescription}
colorScheme='neutral'
variant='smallRegular'
localizationKey={props.description}
/>
)}
</FormLabel>
);
};

export const RadioItem = forwardRef<
HTMLInputElement,
{
value: string;
label: string | LocalizationKey;
description?: string | LocalizationKey;
}
>((props, ref) => {
const randomId = useId();
return (
<Flex
elementDescriptor={descriptors.formFieldRadioGroupItem}
align='start'
>
<RadioIndicator
id={randomId}
ref={ref}
value={props.value}
/>

<RadioLabel
id={randomId}
label={props.label}
description={props.description}
/>
</Flex>
);
});
Loading

0 comments on commit db3eefe

Please sign in to comment.