|
1 | | -import { useClerk } from '@clerk/shared/react'; |
| 1 | +import { createContextAndHook, useClerk } from '@clerk/shared/react'; |
2 | 2 | import type { __internal_EnableOrganizationsPromptProps, EnableEnvironmentSettingParams } from '@clerk/shared/types'; |
3 | 3 | // eslint-disable-next-line no-restricted-imports |
4 | 4 | import type { SerializedStyles } from '@emotion/react'; |
5 | 5 | // eslint-disable-next-line no-restricted-imports |
6 | | -import { css, type Theme } from '@emotion/react'; |
7 | | -import { forwardRef, useId, useLayoutEffect, useRef, useState } from 'react'; |
| 6 | +import { css } from '@emotion/react'; |
| 7 | +import React, { forwardRef, useId, useLayoutEffect, useRef, useState } from 'react'; |
8 | 8 |
|
9 | 9 | import { useEnvironment } from '@/ui/contexts'; |
10 | 10 | import { Modal } from '@/ui/elements/Modal'; |
11 | | -import { common, InternalThemeProvider } from '@/ui/styledSystem'; |
| 11 | +import { InternalThemeProvider } from '@/ui/styledSystem'; |
12 | 12 |
|
13 | | -import { Box, Flex, Span } from '../../../customizables'; |
| 13 | +import { Box, Flex } from '../../../customizables'; |
14 | 14 | import { Portal } from '../../../elements/Portal'; |
15 | 15 | import { basePromptElementStyles, ClerkLogoIcon, PromptContainer, PromptSuccessIcon } from '../shared'; |
16 | 16 |
|
@@ -197,12 +197,21 @@ const EnableOrganizationsPromptInternal = ({ |
197 | 197 | })} |
198 | 198 | > |
199 | 199 | <Flex sx={t => ({ marginTop: t.sizes.$2 })}> |
200 | | - <Switch |
201 | | - label='Allow personal account' |
202 | | - description='Allow users to work outside of an organization by providing a personal account. We do not recommend for B2B SaaS apps.' |
203 | | - checked={allowPersonalAccount} |
204 | | - onChange={() => setAllowPersonalAccount(prev => !prev)} |
205 | | - /> |
| 200 | + <RadioGroup |
| 201 | + value={allowPersonalAccount ? 'allow' : 'require'} |
| 202 | + onChange={value => setAllowPersonalAccount(value === 'allow')} |
| 203 | + > |
| 204 | + <RadioGroupItem |
| 205 | + value='require' |
| 206 | + label='Require organization membership' |
| 207 | + description='Users will be required to create or join an organization to access the application. Common for most B2B SaaS applications.' |
| 208 | + /> |
| 209 | + <RadioGroupItem |
| 210 | + value='allow' |
| 211 | + label='Allow personal accounts' |
| 212 | + description='Users will be able to work outside of an organization by providing a personal account.' |
| 213 | + /> |
| 214 | + </RadioGroup> |
206 | 215 | </Flex> |
207 | 216 | </Flex> |
208 | 217 | </Box> |
@@ -368,136 +377,132 @@ const PromptButton = forwardRef<HTMLButtonElement, PromptButtonProps>(({ variant |
368 | 377 | ); |
369 | 378 | }); |
370 | 379 |
|
371 | | -type SwitchProps = React.ComponentProps<'input'> & { |
| 380 | +type RadioGroupContextValue = { |
| 381 | + name: string; |
| 382 | + value: string; |
| 383 | + onChange: (value: string) => void; |
| 384 | +}; |
| 385 | + |
| 386 | +const [RadioGroupContext, useRadioGroup] = createContextAndHook<RadioGroupContextValue>('RadioGroupContext'); |
| 387 | + |
| 388 | +type RadioGroupProps = { |
| 389 | + value: string; |
| 390 | + onChange: (value: string) => void; |
| 391 | + children: React.ReactNode; |
| 392 | +}; |
| 393 | + |
| 394 | +const RadioGroup = ({ value, onChange, children }: RadioGroupProps) => { |
| 395 | + const name = useId(); |
| 396 | + const contextValue = React.useMemo(() => ({ value: { name, value, onChange } }), [name, value, onChange]); |
| 397 | + |
| 398 | + return ( |
| 399 | + <RadioGroupContext.Provider value={contextValue}> |
| 400 | + <Flex |
| 401 | + direction='col' |
| 402 | + gap={3} |
| 403 | + > |
| 404 | + {children} |
| 405 | + </Flex> |
| 406 | + </RadioGroupContext.Provider> |
| 407 | + ); |
| 408 | +}; |
| 409 | + |
| 410 | +type RadioGroupItemProps = { |
| 411 | + value: string; |
372 | 412 | label: string; |
373 | 413 | description?: string; |
374 | 414 | }; |
375 | 415 |
|
376 | | -const TRACK_PADDING = '2px'; |
377 | | -const TRACK_INNER_WIDTH = (t: Theme) => t.sizes.$6; |
378 | | -const TRACK_HEIGHT = (t: Theme) => t.sizes.$4; |
379 | | -const THUMB_WIDTH = (t: Theme) => t.sizes.$3; |
380 | | - |
381 | | -const Switch = forwardRef<HTMLInputElement, SwitchProps>( |
382 | | - ({ label, description, checked: controlledChecked, defaultChecked, onChange, ...props }, ref) => { |
383 | | - const descriptionId = useId(); |
| 416 | +const RadioGroupItem = ({ value, label, description }: RadioGroupItemProps) => { |
| 417 | + const { name, value: selectedValue, onChange } = useRadioGroup(); |
| 418 | + const descriptionId = useId(); |
| 419 | + const checked = value === selectedValue; |
384 | 420 |
|
385 | | - const isControlled = controlledChecked !== undefined; |
386 | | - const [internalChecked, setInternalChecked] = useState(!!defaultChecked); |
387 | | - const checked = isControlled ? controlledChecked : internalChecked; |
| 421 | + return ( |
| 422 | + <Flex |
| 423 | + as='label' |
| 424 | + gap={2} |
| 425 | + align='start' |
| 426 | + sx={{ |
| 427 | + cursor: 'pointer', |
| 428 | + userSelect: 'none', |
| 429 | + }} |
| 430 | + > |
| 431 | + <input |
| 432 | + type='radio' |
| 433 | + name={name} |
| 434 | + value={value} |
| 435 | + checked={checked} |
| 436 | + onChange={() => onChange(value)} |
| 437 | + aria-describedby={description ? descriptionId : undefined} |
| 438 | + css={css` |
| 439 | + ${basePromptElementStyles}; |
| 440 | + appearance: none; |
| 441 | + width: 1rem; |
| 442 | + height: 1rem; |
| 443 | + margin: 0; |
| 444 | + margin-top: 0.125rem; |
| 445 | + border: 1px solid rgba(255, 255, 255, 0.3); |
| 446 | + border-radius: 50%; |
| 447 | + background-color: transparent; |
| 448 | + cursor: pointer; |
| 449 | + flex-shrink: 0; |
| 450 | + transition: 120ms ease-in-out; |
| 451 | + transition-property: border-color, background-color, box-shadow; |
| 452 | +
|
| 453 | + &:checked { |
| 454 | + border-color: #fff; |
| 455 | + background-color: #fff; |
| 456 | + box-shadow: inset 0 0 0 3px #1f1f1f; |
| 457 | + } |
388 | 458 |
|
389 | | - const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
390 | | - if (!isControlled) { |
391 | | - setInternalChecked(e.target.checked); |
392 | | - } |
393 | | - onChange?.(e); |
394 | | - }; |
| 459 | + &:focus-visible { |
| 460 | + outline: 2px solid white; |
| 461 | + outline-offset: 2px; |
| 462 | + } |
395 | 463 |
|
396 | | - return ( |
| 464 | + &:hover:not(:checked) { |
| 465 | + border-color: rgba(255, 255, 255, 0.5); |
| 466 | + } |
| 467 | + `} |
| 468 | + /> |
397 | 469 | <Flex |
398 | 470 | direction='col' |
399 | 471 | gap={1} |
400 | 472 | > |
401 | | - <Flex |
402 | | - as='label' |
403 | | - gap={2} |
404 | | - align='center' |
405 | | - sx={{ |
406 | | - isolation: 'isolate', |
407 | | - userSelect: 'none', |
408 | | - '&:has(input:focus-visible) > input + span': { |
409 | | - outline: '2px solid white', |
410 | | - outlineOffset: '2px', |
411 | | - }, |
412 | | - '&:has(input:disabled) > input + span': { |
413 | | - opacity: 0.6, |
414 | | - cursor: 'not-allowed', |
415 | | - pointerEvents: 'none', |
416 | | - }, |
417 | | - }} |
| 473 | + <span |
| 474 | + css={[ |
| 475 | + basePromptElementStyles, |
| 476 | + css` |
| 477 | + font-size: 0.875rem; |
| 478 | + font-weight: 500; |
| 479 | + line-height: 1.25; |
| 480 | + color: white; |
| 481 | + `, |
| 482 | + ]} |
418 | 483 | > |
419 | | - <input |
420 | | - type='checkbox' |
421 | | - {...props} |
422 | | - ref={ref} |
423 | | - role='switch' |
424 | | - {...(isControlled ? { checked } : { defaultChecked })} |
425 | | - onChange={handleChange} |
426 | | - css={{ ...common.visuallyHidden() }} |
427 | | - aria-describedby={description ? descriptionId : undefined} |
428 | | - /> |
429 | | - <Span |
430 | | - sx={t => { |
431 | | - const trackWidth = `calc(${TRACK_INNER_WIDTH(t)} + ${TRACK_PADDING} + ${TRACK_PADDING})`; |
432 | | - const trackHeight = `calc(${TRACK_HEIGHT(t)} + ${TRACK_PADDING})`; |
433 | | - return { |
434 | | - display: 'flex', |
435 | | - alignItems: 'center', |
436 | | - paddingInline: TRACK_PADDING, |
437 | | - width: trackWidth, |
438 | | - height: trackHeight, |
439 | | - border: '1px solid rgba(255, 255, 255, 0.2)', |
440 | | - backgroundColor: checked ? '#6C47FF' : 'rgba(0, 0, 0, 0.2)', |
441 | | - borderRadius: 999, |
442 | | - transition: 'background-color 0.2s ease-in-out', |
443 | | - }; |
444 | | - }} |
445 | | - > |
446 | | - <Span |
447 | | - sx={t => { |
448 | | - const size = THUMB_WIDTH(t); |
449 | | - const maxTranslateX = `calc(${TRACK_INNER_WIDTH(t)} - ${size} - ${TRACK_PADDING})`; |
450 | | - return { |
451 | | - width: size, |
452 | | - height: size, |
453 | | - borderRadius: 9999, |
454 | | - backgroundColor: 'white', |
455 | | - boxShadow: '0px 0px 0px 1px rgba(0, 0, 0, 0.1)', |
456 | | - transform: `translateX(${checked ? maxTranslateX : '0'})`, |
457 | | - transition: 'transform 0.2s ease-in-out', |
458 | | - '@media (prefers-reduced-motion: reduce)': { |
459 | | - transition: 'none', |
460 | | - }, |
461 | | - }; |
462 | | - }} |
463 | | - /> |
464 | | - </Span> |
| 484 | + {label} |
| 485 | + </span> |
| 486 | + {description && ( |
465 | 487 | <span |
| 488 | + id={descriptionId} |
466 | 489 | css={[ |
467 | 490 | basePromptElementStyles, |
468 | 491 | css` |
469 | | - font-size: 0.875rem; |
470 | | - font-weight: 500; |
471 | | - line-height: 1.25; |
472 | | - color: white; |
| 492 | + font-size: 0.75rem; |
| 493 | + line-height: 1.3333333333; |
| 494 | + color: #c3c3c6; |
| 495 | + text-wrap: pretty; |
473 | 496 | `, |
474 | 497 | ]} |
475 | | - > |
476 | | - {label} |
477 | | - </span> |
478 | | - </Flex> |
479 | | - {description ? ( |
480 | | - <Span |
481 | | - id={descriptionId} |
482 | | - sx={t => [ |
483 | | - basePromptElementStyles, |
484 | | - { |
485 | | - display: 'block', |
486 | | - paddingInlineStart: `calc(${TRACK_INNER_WIDTH(t)} + ${TRACK_PADDING} + ${TRACK_PADDING} + ${t.sizes.$2})`, |
487 | | - fontSize: '0.75rem', |
488 | | - lineHeight: '1.3333333333', |
489 | | - color: '#c3c3c6', |
490 | | - textWrap: 'pretty', |
491 | | - }, |
492 | | - ]} |
493 | 498 | > |
494 | 499 | {description} |
495 | | - </Span> |
496 | | - ) : null} |
| 500 | + </span> |
| 501 | + )} |
497 | 502 | </Flex> |
498 | | - ); |
499 | | - }, |
500 | | -); |
| 503 | + </Flex> |
| 504 | + ); |
| 505 | +}; |
501 | 506 |
|
502 | 507 | const Link = forwardRef<HTMLAnchorElement, React.ComponentProps<'a'> & { css?: SerializedStyles }>( |
503 | 508 | ({ children, css: cssProp, ...props }, ref) => { |
|
0 commit comments