Skip to content

Commit 9681f0d

Browse files
committed
Refactor to use RadioInput primitive
1 parent 9a3d301 commit 9681f0d

File tree

1 file changed

+99
-234
lines changed
  • packages/ui/src/components/devPrompts/EnableOrganizationsPrompt

1 file changed

+99
-234
lines changed

packages/ui/src/components/devPrompts/EnableOrganizationsPrompt/index.tsx

Lines changed: 99 additions & 234 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,38 @@
1-
import { createContextAndHook, useClerk } from '@clerk/shared/react';
1+
import { useClerk } from '@clerk/shared/react';
22
import type { __internal_EnableOrganizationsPromptProps, EnableEnvironmentSettingParams } from '@clerk/shared/types';
33
// eslint-disable-next-line no-restricted-imports
44
import type { SerializedStyles } from '@emotion/react';
55
// eslint-disable-next-line no-restricted-imports
66
import { css } from '@emotion/react';
7-
import React, { forwardRef, useId, useLayoutEffect, useRef, useState } from 'react';
7+
import { type ComponentProps, forwardRef, useId, useLayoutEffect, useRef, useState } from 'react';
88

99
import { useEnvironment } from '@/ui/contexts';
1010
import { Modal } from '@/ui/elements/Modal';
1111
import { InternalThemeProvider } from '@/ui/styledSystem';
1212

13-
import { Flex } from '../../../customizables';
13+
import { Col, Flex } from '../../../customizables';
1414
import { Portal } from '../../../elements/Portal';
15+
import { RadioInput } from '../../../primitives';
1516
import { basePromptElementStyles, ClerkLogoIcon, PromptContainer, PromptSuccessIcon } from '../shared';
1617

1718
const organizationsDashboardUrl = 'https://dashboard.clerk.com/~/organizations-settings';
1819

20+
const RADIO_INPUT_SIZE = '1rem';
21+
const RADIO_GAP = '0.5rem';
22+
23+
const radioOptions = [
24+
{
25+
value: 'require',
26+
label: 'Require organization membership',
27+
description: 'Users need to belong to at least one organization.',
28+
},
29+
{
30+
value: 'allow',
31+
label: 'Allow personal accounts',
32+
description: 'Users can work outside of an organization with a personal account',
33+
},
34+
];
35+
1936
const EnableOrganizationsPromptInternal = ({
2037
caller,
2138
onSuccess,
@@ -24,11 +41,11 @@ const EnableOrganizationsPromptInternal = ({
2441
const clerk = useClerk();
2542
const [isLoading, setIsLoading] = useState(false);
2643
const [isEnabled, setIsEnabled] = useState(false);
27-
const [allowPersonalAccount, setAllowPersonalAccount] = useState(false);
44+
const [membershipOption, setMembershipOption] = useState<'require' | 'allow'>('require');
2845

46+
const radioGroupName = useId();
2947
const initialFocusRef = useRef<HTMLHeadingElement>(null);
3048
const environment = useEnvironment();
31-
const radioGroupLabelId = useId();
3249

3350
const isComponent = !caller.startsWith('use');
3451

@@ -44,7 +61,7 @@ const EnableOrganizationsPromptInternal = ({
4461
};
4562

4663
if (hasPersonalAccountsEnabled) {
47-
params.organization_allow_personal_accounts = allowPersonalAccount;
64+
params.organization_allow_personal_accounts = membershipOption === 'allow';
4865
}
4966

5067
void environment
@@ -141,7 +158,6 @@ const EnableOrganizationsPromptInternal = ({
141158
) : (
142159
<>
143160
<p
144-
id={radioGroupLabelId}
145161
css={[
146162
basePromptElementStyles,
147163
css`
@@ -180,32 +196,82 @@ const EnableOrganizationsPromptInternal = ({
180196
</Flex>
181197

182198
{hasPersonalAccountsEnabled && (
183-
<Flex
199+
<Col
200+
role='radiogroup'
201+
gap={3}
184202
sx={t => ({ marginTop: t.sizes.$2 })}
185-
direction='col'
186203
>
187-
<RadioGroup
188-
value={allowPersonalAccount ? 'allow' : 'require'}
189-
onChange={value => setAllowPersonalAccount(value === 'allow')}
190-
labelledBy={radioGroupLabelId}
191-
>
192-
<RadioGroupItem
193-
value='require'
194-
label={
195-
<Flex gap={2}>
196-
<span>Require organization membership</span>
197-
<PromptBadge>Standard</PromptBadge>
198-
</Flex>
199-
}
200-
description='Users need to belong to at least one organization.'
201-
/>
202-
<RadioGroupItem
203-
value='allow'
204-
label='Allow personal accounts'
205-
description='Users can work outside of an organization with a personal account'
206-
/>
207-
</RadioGroup>
208-
</Flex>
204+
{radioOptions.map(option => {
205+
const optionId = `${radioGroupName}-${option.value}`;
206+
const descriptionId = `${optionId}-description`;
207+
return (
208+
<Flex
209+
key={option.value}
210+
direction='col'
211+
gap={1}
212+
>
213+
<label
214+
htmlFor={optionId}
215+
css={css`
216+
${basePromptElementStyles};
217+
display: flex;
218+
align-items: flex-start;
219+
gap: ${RADIO_GAP};
220+
cursor: pointer;
221+
user-select: none;
222+
`}
223+
>
224+
<RadioInput
225+
id={optionId}
226+
name={radioGroupName}
227+
value={option.value}
228+
checked={membershipOption === option.value}
229+
onChange={() => setMembershipOption(option.value as 'require' | 'allow')}
230+
aria-describedby={option.description ? descriptionId : undefined}
231+
css={css`
232+
width: ${RADIO_INPUT_SIZE};
233+
height: ${RADIO_INPUT_SIZE};
234+
margin-top: 0.125rem;
235+
flex-shrink: 0;
236+
`}
237+
/>
238+
239+
<span
240+
css={[
241+
basePromptElementStyles,
242+
css`
243+
font-size: 0.875rem;
244+
font-weight: 500;
245+
line-height: 1.25;
246+
color: white;
247+
`,
248+
]}
249+
>
250+
{option.label}
251+
</span>
252+
</label>
253+
254+
{option.description && (
255+
<span
256+
id={descriptionId}
257+
css={[
258+
basePromptElementStyles,
259+
css`
260+
padding-inline-start: calc(${RADIO_INPUT_SIZE} + ${RADIO_GAP});
261+
font-size: 0.75rem;
262+
line-height: 1.33;
263+
color: #c3c3c6;
264+
text-wrap: pretty;
265+
`,
266+
]}
267+
>
268+
{option.description}
269+
</span>
270+
)}
271+
</Flex>
272+
);
273+
})}
274+
</Col>
209275
)}
210276
</Flex>
211277

@@ -353,7 +419,7 @@ const buttonVariantStyles = {
353419

354420
type PromptButtonVariant = keyof typeof buttonVariantStyles;
355421

356-
type PromptButtonProps = Pick<React.ComponentProps<'button'>, 'onClick' | 'children' | 'disabled'> & {
422+
type PromptButtonProps = Pick<ComponentProps<'button'>, 'onClick' | 'children' | 'disabled'> & {
357423
variant?: PromptButtonVariant;
358424
};
359425

@@ -368,208 +434,7 @@ const PromptButton = forwardRef<HTMLButtonElement, PromptButtonProps>(({ variant
368434
);
369435
});
370436

371-
type PromptBadgeProps = {
372-
children: React.ReactNode;
373-
};
374-
375-
const PromptBadge = ({ children }: PromptBadgeProps): JSX.Element => {
376-
return (
377-
<span
378-
css={css`
379-
${basePromptElementStyles};
380-
display: inline-flex;
381-
align-items: center;
382-
padding: 0.125rem 0.375rem;
383-
border-radius: 0.25rem;
384-
font-size: 0.6875rem;
385-
font-weight: 500;
386-
line-height: 1.23;
387-
background-color: #ebebeb;
388-
color: #2b2b34;
389-
`}
390-
>
391-
{children}
392-
</span>
393-
);
394-
};
395-
396-
type RadioGroupContextValue = {
397-
name: string;
398-
value: string;
399-
onChange: (value: string) => void;
400-
};
401-
402-
const [RadioGroupContext, useRadioGroup] = createContextAndHook<RadioGroupContextValue>('RadioGroupContext');
403-
404-
type RadioGroupProps = {
405-
value: string;
406-
onChange: (value: string) => void;
407-
children: React.ReactNode;
408-
labelledBy?: string;
409-
};
410-
411-
const RadioGroup = ({ value, onChange, children, labelledBy }: RadioGroupProps): JSX.Element => {
412-
const name = useId();
413-
const contextValue = React.useMemo(() => ({ value: { name, value, onChange } }), [name, value, onChange]);
414-
415-
return (
416-
<RadioGroupContext.Provider value={contextValue}>
417-
<Flex
418-
role='radiogroup'
419-
direction='col'
420-
gap={3}
421-
aria-orientation='vertical'
422-
aria-labelledby={labelledBy}
423-
>
424-
{children}
425-
</Flex>
426-
</RadioGroupContext.Provider>
427-
);
428-
};
429-
430-
type RadioGroupItemProps = {
431-
value: string;
432-
label: React.ReactNode;
433-
description?: React.ReactNode;
434-
};
435-
436-
const RADIO_INDICATOR_SIZE = '1rem';
437-
const RADIO_GAP = '0.5rem';
438-
439-
const RadioGroupItem = ({ value, label, description }: RadioGroupItemProps): JSX.Element => {
440-
const { name, value: selectedValue, onChange } = useRadioGroup();
441-
const descriptionId = useId();
442-
const checked = value === selectedValue;
443-
444-
return (
445-
<Flex
446-
direction='col'
447-
gap={1}
448-
>
449-
<label
450-
css={css`
451-
${basePromptElementStyles};
452-
display: flex;
453-
align-items: flex-start;
454-
gap: ${RADIO_GAP};
455-
cursor: pointer;
456-
user-select: none;
457-
458-
&:has(input:focus-visible) > span:first-of-type {
459-
outline: 2px solid white;
460-
outline-offset: 2px;
461-
}
462-
463-
&:hover:has(input:not(:checked)) > span:first-of-type {
464-
background-color: rgba(255, 255, 255, 0.08);
465-
}
466-
467-
&:hover:has(input:checked) > span:first-of-type {
468-
background-color: rgba(108, 71, 255, 0.8);
469-
background-color: color-mix(in srgb, #6c47ff 80%, transparent);
470-
}
471-
`}
472-
>
473-
<input
474-
type='radio'
475-
name={name}
476-
value={value}
477-
checked={checked}
478-
onChange={() => onChange(value)}
479-
aria-describedby={description ? descriptionId : undefined}
480-
css={css`
481-
${basePromptElementStyles};
482-
position: absolute;
483-
width: 1px;
484-
height: 1px;
485-
padding: 0;
486-
margin: -1px;
487-
overflow: hidden;
488-
clip: rect(0, 0, 0, 0);
489-
white-space: nowrap;
490-
border-width: 0;
491-
`}
492-
/>
493-
494-
<span
495-
aria-hidden='true'
496-
css={css`
497-
${basePromptElementStyles};
498-
position: relative;
499-
display: inline-flex;
500-
align-items: center;
501-
justify-content: center;
502-
width: ${RADIO_INDICATOR_SIZE};
503-
height: ${RADIO_INDICATOR_SIZE};
504-
margin-top: 0.125rem;
505-
flex-shrink: 0;
506-
border-radius: 50%;
507-
border: 1px solid rgba(255, 255, 255, 0.3);
508-
background-color: transparent;
509-
transition: 120ms ease-in-out;
510-
transition-property: border-color, background-color, box-shadow;
511-
512-
${checked &&
513-
css`
514-
border-width: 2px;
515-
border-color: #6c47ff;
516-
background-color: #6c47ff;
517-
background-color: color-mix(in srgb, #6c47ff 100%, transparent);
518-
box-shadow: 0 0 0 2px rgba(108, 71, 255, 0.2);
519-
`}
520-
521-
&::after {
522-
content: '';
523-
position: absolute;
524-
width: 0.375rem;
525-
height: 0.375rem;
526-
border-radius: 50%;
527-
background-color: white;
528-
opacity: ${checked ? 1 : 0};
529-
transform: scale(${checked ? 1 : 0});
530-
transition: 120ms ease-in-out;
531-
transition-property: opacity, transform;
532-
}
533-
`}
534-
/>
535-
536-
<span
537-
css={[
538-
basePromptElementStyles,
539-
css`
540-
font-size: 0.875rem;
541-
font-weight: 500;
542-
line-height: 1.25;
543-
color: white;
544-
`,
545-
]}
546-
>
547-
{label}
548-
</span>
549-
</label>
550-
551-
{description && (
552-
<span
553-
id={descriptionId}
554-
css={[
555-
basePromptElementStyles,
556-
css`
557-
padding-inline-start: calc(${RADIO_INDICATOR_SIZE} + ${RADIO_GAP});
558-
font-size: 0.75rem;
559-
line-height: 1.33;
560-
color: #c3c3c6;
561-
text-wrap: pretty;
562-
`,
563-
]}
564-
>
565-
{description}
566-
</span>
567-
)}
568-
</Flex>
569-
);
570-
};
571-
572-
const Link = forwardRef<HTMLAnchorElement, React.ComponentProps<'a'> & { css?: SerializedStyles }>(
437+
const Link = forwardRef<HTMLAnchorElement, ComponentProps<'a'> & { css?: SerializedStyles }>(
573438
({ children, css: cssProp, ...props }, ref) => {
574439
return (
575440
<a

0 commit comments

Comments
 (0)