Skip to content

Commit db3eefe

Browse files
authored
chore(clerk-js): Introduce Form.RadioGroup (#2034)
* chore(clerk-js): Introduce Form.RadioGroup * chore(clerk-js): Address PR comments
1 parent 213546e commit db3eefe

File tree

6 files changed

+545
-68
lines changed

6 files changed

+545
-68
lines changed

.changeset/slow-wombats-battle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Refactor of internal radio input in forms.

packages/clerk-js/src/ui/components/OrganizationProfile/VerifiedDomainPage.tsx

Lines changed: 49 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { OrganizationDomainResource, OrganizationEnrollmentMode } from '@clerk/types';
1+
import type {
2+
OrganizationDomainResource,
3+
OrganizationEnrollmentMode,
4+
OrganizationSettingsResource,
5+
} from '@clerk/types';
26

37
import { CalloutWithAction, useGate } from '../../common';
48
import { useCoreOrganization, useEnvironment } from '../../contexts';
@@ -51,6 +55,46 @@ const useCalloutLabel = (
5155
];
5256
};
5357

58+
const buildEnrollmentOptions = (settings: OrganizationSettingsResource) => {
59+
const _options = [];
60+
if (settings.domains.enrollmentModes.includes('manual_invitation')) {
61+
_options.push({
62+
value: 'manual_invitation',
63+
label: localizationKeys('organizationProfile.verifiedDomainPage.enrollmentTab.manualInvitationOption__label'),
64+
description: localizationKeys(
65+
'organizationProfile.verifiedDomainPage.enrollmentTab.manualInvitationOption__description',
66+
),
67+
});
68+
}
69+
70+
if (settings.domains.enrollmentModes.includes('automatic_invitation')) {
71+
_options.push({
72+
value: 'automatic_invitation',
73+
label: localizationKeys('organizationProfile.verifiedDomainPage.enrollmentTab.automaticInvitationOption__label'),
74+
description: localizationKeys(
75+
'organizationProfile.verifiedDomainPage.enrollmentTab.automaticInvitationOption__description',
76+
),
77+
});
78+
}
79+
80+
if (settings.domains.enrollmentModes.includes('automatic_suggestion')) {
81+
_options.push({
82+
value: 'automatic_suggestion',
83+
label: localizationKeys('organizationProfile.verifiedDomainPage.enrollmentTab.automaticSuggestionOption__label'),
84+
description: localizationKeys(
85+
'organizationProfile.verifiedDomainPage.enrollmentTab.automaticSuggestionOption__description',
86+
),
87+
});
88+
}
89+
90+
return _options;
91+
};
92+
93+
const useEnrollmentOptions = () => {
94+
const { organizationSettings } = useEnvironment();
95+
return buildEnrollmentOptions(organizationSettings);
96+
};
97+
5498
export const VerifiedDomainPage = withCardStateProvider(() => {
5599
const card = useCardState();
56100
const { organizationSettings } = useEnvironment();
@@ -71,49 +115,11 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
71115
const breadcrumbTitle = localizationKeys('organizationProfile.profilePage.domainSection.title');
72116
const allowsEdit = mode === 'edit';
73117

118+
const enrollmentOptions = useEnrollmentOptions();
74119
const enrollmentMode = useFormControl('enrollmentMode', '', {
75120
type: 'radio',
76-
radioOptions: [
77-
...(organizationSettings.domains.enrollmentModes.includes('manual_invitation')
78-
? [
79-
{
80-
value: 'manual_invitation',
81-
label: localizationKeys(
82-
'organizationProfile.verifiedDomainPage.enrollmentTab.manualInvitationOption__label',
83-
),
84-
description: localizationKeys(
85-
'organizationProfile.verifiedDomainPage.enrollmentTab.manualInvitationOption__description',
86-
),
87-
},
88-
]
89-
: []),
90-
...(organizationSettings.domains.enrollmentModes.includes('automatic_invitation')
91-
? [
92-
{
93-
value: 'automatic_invitation',
94-
label: localizationKeys(
95-
'organizationProfile.verifiedDomainPage.enrollmentTab.automaticInvitationOption__label',
96-
),
97-
description: localizationKeys(
98-
'organizationProfile.verifiedDomainPage.enrollmentTab.automaticInvitationOption__description',
99-
),
100-
},
101-
]
102-
: []),
103-
...(organizationSettings.domains.enrollmentModes.includes('automatic_suggestion')
104-
? [
105-
{
106-
value: 'automatic_suggestion',
107-
label: localizationKeys(
108-
'organizationProfile.verifiedDomainPage.enrollmentTab.automaticSuggestionOption__label',
109-
),
110-
description: localizationKeys(
111-
'organizationProfile.verifiedDomainPage.enrollmentTab.automaticSuggestionOption__description',
112-
),
113-
},
114-
]
115-
: []),
116-
],
121+
radioOptions: enrollmentOptions,
122+
isRequired: true,
117123
});
118124

119125
const deletePending = useFormControl('deleteExistingInvitationsSuggestions', '', {
@@ -252,7 +258,7 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
252258
gap={6}
253259
>
254260
<Form.ControlRow elementId={enrollmentMode.id}>
255-
<Form.Control {...enrollmentMode.props} />
261+
<Form.RadioGroup {...enrollmentMode.props} />
256262
</Form.ControlRow>
257263

258264
{allowsEdit && (

packages/clerk-js/src/ui/elements/FieldControl.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { useFormControlFeedback } from '../utils';
2222
import { useCardState } from './contexts';
2323
import type { FormFeedbackProps } from './FormControl';
2424
import { FormFeedback } from './FormControl';
25+
import { RadioItem } from './RadioGroup';
2526

2627
type FormControlProps = Omit<PropsOfComponent<typeof Input>, 'label' | 'placeholder' | 'disabled' | 'required'> &
2728
ReturnType<typeof useFormControlUtil<FieldId>>['props'];
@@ -218,6 +219,7 @@ export const Field = {
218219
Label: FieldLabel,
219220
LabelRow: FieldLabelRow,
220221
Input: InputElement,
222+
RadioItem: RadioItem,
221223
Action: FieldAction,
222224
AsOptional: FieldOptionalLabel,
223225
LabelIcon: FieldLabelIcon,

packages/clerk-js/src/ui/elements/Form.tsx

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { PropsWithChildren } from 'react';
44
import React, { useState } from 'react';
55

66
import type { LocalizationKey } from '../customizables';
7-
import { Button, descriptors, Flex, Form as FormPrim, localizationKeys } from '../customizables';
7+
import { Button, Col, descriptors, Flex, Form as FormPrim, localizationKeys } from '../customizables';
88
import { useLoadingStatus } from '../hooks';
99
import type { PropsOfComponent } from '../styledSystem';
1010
import { useCardState } from './contexts';
@@ -76,35 +76,31 @@ const FormRoot = (props: FormProps): JSX.Element => {
7676
const FormSubmit = (props: PropsOfComponent<typeof Button>) => {
7777
const { isLoading, isDisabled } = useFormState();
7878
return (
79-
<>
80-
<Button
81-
elementDescriptor={descriptors.formButtonPrimary}
82-
block
83-
textVariant='buttonExtraSmallBold'
84-
isLoading={isLoading}
85-
isDisabled={isDisabled}
86-
type='submit'
87-
{...props}
88-
localizationKey={props.localizationKey || localizationKeys('formButtonPrimary')}
89-
/>
90-
</>
79+
<Button
80+
elementDescriptor={descriptors.formButtonPrimary}
81+
block
82+
textVariant='buttonExtraSmallBold'
83+
isLoading={isLoading}
84+
isDisabled={isDisabled}
85+
type='submit'
86+
{...props}
87+
localizationKey={props.localizationKey || localizationKeys('formButtonPrimary')}
88+
/>
9189
);
9290
};
9391

9492
const FormReset = (props: PropsOfComponent<typeof Button>) => {
9593
const { isLoading, isDisabled } = useFormState();
9694
return (
97-
<>
98-
<Button
99-
elementDescriptor={descriptors.formButtonReset}
100-
block
101-
variant='ghost'
102-
textVariant='buttonExtraSmallBold'
103-
type='reset'
104-
isDisabled={isLoading || isDisabled}
105-
{...props}
106-
/>
107-
</>
95+
<Button
96+
elementDescriptor={descriptors.formButtonReset}
97+
block
98+
variant='ghost'
99+
textVariant='buttonExtraSmallBold'
100+
type='reset'
101+
isDisabled={isLoading || isDisabled}
102+
{...props}
103+
/>
108104
);
109105
};
110106

@@ -160,6 +156,31 @@ const PlainInput = (props: CommonInputProps) => {
160156
);
161157
};
162158

159+
const RadioGroup = (
160+
props: Omit<PropsOfComponent<typeof Field.Root>, 'infoText' | 'type' | 'validatePassword' | 'label' | 'placeholder'>,
161+
) => {
162+
const { radioOptions, ...fieldProps } = props;
163+
return (
164+
<Field.Root {...fieldProps}>
165+
<Col
166+
elementDescriptor={descriptors.formFieldRadioGroup}
167+
gap={2}
168+
>
169+
{radioOptions?.map(({ value, description, label }) => (
170+
<Field.RadioItem
171+
key={value}
172+
value={value}
173+
label={label}
174+
description={description}
175+
/>
176+
))}
177+
</Col>
178+
179+
<Field.Feedback />
180+
</Field.Root>
181+
);
182+
};
183+
163184
export const Form = {
164185
Root: FormRoot,
165186
ControlRow: FormControlRow,
@@ -168,6 +189,7 @@ export const Form = {
168189
*/
169190
Control: FormControl,
170191
PlainInput,
192+
RadioGroup,
171193
SubmitButton: FormSubmit,
172194
ResetButton: FormReset,
173195
};

packages/clerk-js/src/ui/elements/RadioGroup.tsx

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import { useId } from 'react';
1+
import { forwardRef, useId } from 'react';
22

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

8+
/**
9+
* @deprecated
10+
*/
711
export const RadioGroup = (
812
props: PropsOfComponent<typeof Input> & {
913
radioOptions?: {
@@ -30,6 +34,9 @@ export const RadioGroup = (
3034
);
3135
};
3236

37+
/**
38+
* @deprecated
39+
*/
3340
const RadioGroupItem = (props: {
3441
inputProps: PropsOfComponent<typeof Input>;
3542
value: string;
@@ -86,3 +93,87 @@ const RadioGroupItem = (props: {
8693
</Flex>
8794
);
8895
};
96+
97+
const RadioIndicator = forwardRef<HTMLInputElement, { value: string; id: string }>((props, ref) => {
98+
const formField = useFormField();
99+
const { value, placeholder, ...inputProps } = sanitizeInputProps(formField);
100+
101+
return (
102+
<Input
103+
ref={ref}
104+
{...inputProps}
105+
elementDescriptor={descriptors.formFieldRadioInput}
106+
id={props.id}
107+
focusRing={false}
108+
sx={t => ({
109+
width: 'fit-content',
110+
marginTop: t.space.$0x5,
111+
})}
112+
type='radio'
113+
value={props.value}
114+
checked={props.value === value}
115+
/>
116+
);
117+
});
118+
119+
export const RadioLabel = (props: {
120+
label: string | LocalizationKey;
121+
description?: string | LocalizationKey;
122+
id?: string;
123+
}) => {
124+
return (
125+
<FormLabel
126+
elementDescriptor={descriptors.formFieldRadioLabel}
127+
htmlFor={props.id}
128+
sx={t => ({
129+
padding: `${t.space.$none} ${t.space.$2}`,
130+
display: 'flex',
131+
flexDirection: 'column',
132+
})}
133+
>
134+
<Text
135+
elementDescriptor={descriptors.formFieldRadioLabelTitle}
136+
variant='regularMedium'
137+
localizationKey={props.label}
138+
/>
139+
140+
{props.description && (
141+
<Text
142+
elementDescriptor={descriptors.formFieldRadioLabelDescription}
143+
colorScheme='neutral'
144+
variant='smallRegular'
145+
localizationKey={props.description}
146+
/>
147+
)}
148+
</FormLabel>
149+
);
150+
};
151+
152+
export const RadioItem = forwardRef<
153+
HTMLInputElement,
154+
{
155+
value: string;
156+
label: string | LocalizationKey;
157+
description?: string | LocalizationKey;
158+
}
159+
>((props, ref) => {
160+
const randomId = useId();
161+
return (
162+
<Flex
163+
elementDescriptor={descriptors.formFieldRadioGroupItem}
164+
align='start'
165+
>
166+
<RadioIndicator
167+
id={randomId}
168+
ref={ref}
169+
value={props.value}
170+
/>
171+
172+
<RadioLabel
173+
id={randomId}
174+
label={props.label}
175+
description={props.description}
176+
/>
177+
</Flex>
178+
);
179+
});

0 commit comments

Comments
 (0)