Skip to content

Commit 290d85c

Browse files
alexcarpenterLauraBeatris
authored andcommitted
refactor: Switch improvements (#7253)
1 parent db1beb2 commit 290d85c

File tree

3 files changed

+116
-96
lines changed

3 files changed

+116
-96
lines changed

packages/clerk-js/src/ui/components/devPrompts/EnableOrganizationsPrompt/index.tsx

Lines changed: 115 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { useClerk } from '@clerk/shared/react';
22
import type { __internal_EnableOrganizationsPromptProps } from '@clerk/shared/types';
33
// eslint-disable-next-line no-restricted-imports
4-
import { css } from '@emotion/react';
5-
import { forwardRef, useMemo, useRef, useState } from 'react';
4+
import { css, type Theme } from '@emotion/react';
5+
import { forwardRef, useId, useMemo, useRef, useState } from 'react';
66

77
import { Modal } from '@/ui/elements/Modal';
88
import { common, InternalThemeProvider } from '@/ui/styledSystem';
99

1010
import { DevTools } from '../../../../core/resources/DevTools';
1111
import type { Environment } from '../../../../core/resources/Environment';
12-
import { Flex } from '../../../customizables';
12+
import { Flex, Span } from '../../../customizables';
1313
import { Portal } from '../../../elements/Portal';
1414
import { basePromptElementStyles, handleDashboardUrlParsing, PromptContainer, PromptSuccessIcon } from '../shared';
1515

@@ -88,7 +88,8 @@ const EnableOrganizationsPromptInternal = ({
8888
sx={() => ({
8989
display: 'flex',
9090
flexDirection: 'column',
91-
maxWidth: '30rem',
91+
width: '30rem',
92+
maxWidth: 'calc(100vw - 2rem)',
9293
})}
9394
>
9495
<Flex
@@ -302,10 +303,11 @@ const EnableOrganizationsPromptInternal = ({
302303

303304
{!isEnabled && clerk?.user && (
304305
<Flex sx={t => ({ marginTop: t.sizes.$3 })}>
305-
<AllowPersonalAccountSwitch
306+
<Switch
307+
label='Allow personal account'
308+
description='This is an uncommon setting, meant for applications that sell to both organizations and individual users. Most B2B applications require users to be part of an organization, and should keep this setting disabled.'
306309
checked={allowPersonalAccount}
307-
onChange={() => setAllowPersonalAccount(!allowPersonalAccount)}
308-
isDisabled={false}
310+
onChange={() => setAllowPersonalAccount(prev => !prev)}
309311
/>
310312
</Flex>
311313
)}
@@ -314,8 +316,10 @@ const EnableOrganizationsPromptInternal = ({
314316
<span
315317
css={css`
316318
height: 1px;
319+
display: block;
320+
width: calc(100% - 2px);
321+
margin-inline: auto;
317322
background-color: #151515;
318-
width: 100%;
319323
box-shadow: 0px 1px 0px 0px #424242;
320324
`}
321325
/>
@@ -461,118 +465,134 @@ const PromptButton = forwardRef<HTMLButtonElement, PromptButtonProps>(({ variant
461465
);
462466
});
463467

464-
type AllowPersonalAccountSwitchProps = {
465-
checked: boolean;
466-
isDisabled: boolean;
467-
onChange: (checked: boolean) => void;
468+
type SwitchProps = React.ComponentProps<'input'> & {
469+
label: string;
470+
description?: string;
468471
};
469472

470-
const AllowPersonalAccountSwitch = forwardRef<HTMLDivElement, AllowPersonalAccountSwitchProps>(
471-
({ checked, onChange, isDisabled = false }, ref) => {
473+
const TRACK_PADDING = '2px';
474+
const TRACK_INNER_WIDTH = (t: Theme) => t.sizes.$6;
475+
const TRACK_HEIGHT = (t: Theme) => t.sizes.$4;
476+
const THUMB_WIDTH = (t: Theme) => t.sizes.$3;
477+
478+
const Switch = forwardRef<HTMLInputElement, SwitchProps>(
479+
({ label, description, checked: controlledChecked, defaultChecked, onChange, ...props }, ref) => {
480+
const descriptionId = useId();
481+
482+
const isControlled = controlledChecked !== undefined;
483+
const [internalChecked, setInternalChecked] = useState(!!defaultChecked);
484+
const checked = isControlled ? controlledChecked : internalChecked;
485+
472486
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
473-
if (isDisabled) {
474-
return;
487+
if (!isControlled) {
488+
setInternalChecked(e.target.checked);
475489
}
476-
477-
onChange?.(e.target.checked);
490+
onChange?.(e);
478491
};
479492

480493
return (
481494
<Flex
482-
ref={ref}
483-
direction='row'
484-
align='center'
485-
as='label'
486-
gap={2}
487-
sx={t => ({
488-
isolation: 'isolate',
489-
width: 'fit-content',
490-
'&:has(input:focus-visible) > input + span': {
491-
...common.focusRingStyles(t),
492-
},
493-
})}
495+
direction='col'
496+
gap={1}
494497
>
495-
{/* The order of the elements is important here for the focus ring to work. The input is visually hidden, so the focus ring is applied to the span. */}
496-
<input
497-
type='checkbox'
498-
role='switch'
499-
disabled={isDisabled}
500-
checked={checked}
501-
onChange={handleChange}
502-
style={{
503-
...common.visuallyHidden(),
504-
}}
505-
/>
506498
<Flex
507-
as='span'
508-
data-checked={checked}
509-
sx={t => ({
510-
minWidth: t.sizes.$7,
511-
alignSelf: 'flex-start',
512-
height: t.sizes.$4,
513-
alignItems: 'center',
514-
position: 'relative',
515-
borderColor: '#DBDBE0',
516-
backgroundColor: checked ? '#DBDBE0' : t.colors.$primary500,
517-
borderRadius: 999,
518-
transition: 'background-color 0.2s',
519-
opacity: isDisabled ? 0.6 : 1,
520-
cursor: isDisabled ? 'not-allowed' : 'pointer',
521-
outline: 'none',
522-
boxSizing: 'border-box',
523-
boxShadow:
524-
'0px 0px 6px 0px rgba(255, 255, 255, 0.04) inset, 0px 0px 0px 1px rgba(255, 255, 255, 0.04) inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.04) inset, 0px 0px 0px 1px rgba(0, 0, 0, 0.1)',
525-
})}
499+
as='label'
500+
gap={2}
501+
align='center'
502+
sx={{
503+
isolation: 'isolate',
504+
userSelect: 'none',
505+
'&:has(input:focus-visible) > input + span': {
506+
outline: '2px solid white',
507+
outlineOffset: '2px',
508+
},
509+
'&:has(input:disabled) > input + span': {
510+
opacity: 0.6,
511+
cursor: 'not-allowed',
512+
pointerEvents: 'none',
513+
},
514+
}}
526515
>
527-
<Flex
528-
sx={t => ({
529-
position: 'absolute',
530-
left: t.sizes.$0x5,
531-
width: t.sizes.$3,
532-
height: t.sizes.$3,
533-
borderRadius: '50%',
534-
backgroundColor: 'white',
535-
boxShadow: t.shadows.$switchControl,
536-
transform: `translateX(${checked ? t.sizes.$3 : 0})`,
537-
transition: 'transform 0.2s',
538-
zIndex: 1,
539-
})}
516+
<input
517+
type='checkbox'
518+
{...props}
519+
ref={ref}
520+
role='switch'
521+
{...(isControlled ? { checked } : { defaultChecked })}
522+
onChange={handleChange}
523+
css={{ ...common.visuallyHidden() }}
524+
aria-describedby={description ? descriptionId : undefined}
540525
/>
541-
</Flex>
542-
543-
<Flex
544-
direction='col'
545-
gap={1}
546-
>
526+
<Span
527+
sx={t => {
528+
const trackWidth = `calc(${TRACK_INNER_WIDTH(t)} + ${TRACK_PADDING} + ${TRACK_PADDING})`;
529+
const trackHeight = `calc(${TRACK_HEIGHT(t)} + ${TRACK_PADDING})`;
530+
return {
531+
display: 'flex',
532+
alignItems: 'center',
533+
paddingInline: TRACK_PADDING,
534+
width: trackWidth,
535+
height: trackHeight,
536+
border: `1px solid rgba(118, 118, 132, 0.25)`,
537+
backgroundColor: checked ? 'rgba(255, 255, 255, 0.75)' : `rgba(255, 255, 255, 0.1)`,
538+
borderRadius: 999,
539+
transition: 'background-color 0.2s ease-in-out',
540+
'&:hover': {
541+
borderColor: `rgba(118, 118, 132, 0.5)`,
542+
},
543+
};
544+
}}
545+
>
546+
<Span
547+
sx={t => {
548+
const size = THUMB_WIDTH(t);
549+
const maxTranslateX = `calc(${TRACK_INNER_WIDTH(t)} - ${size} - ${TRACK_PADDING})`;
550+
return {
551+
width: size,
552+
height: size,
553+
borderRadius: 9999,
554+
backgroundColor: 'white',
555+
transform: `translateX(${checked ? maxTranslateX : '0'})`,
556+
transition: 'transform 0.2s ease-in-out',
557+
'@media (prefers-reduced-motion: reduce)': {
558+
transition: 'none',
559+
},
560+
};
561+
}}
562+
/>
563+
</Span>
547564
<span
548565
css={[
549566
basePromptElementStyles,
550567
css`
551568
font-size: 0.875rem;
552-
font-weight: 400;
553-
line-height: 1.23;
569+
font-weight: 500;
570+
line-height: 1.25;
554571
color: white;
555572
`,
556573
]}
557574
>
558-
Allow personal account
575+
{label}
559576
</span>
560-
561-
<span
562-
css={[
577+
</Flex>
578+
{description ? (
579+
<Span
580+
id={descriptionId}
581+
sx={t => [
563582
basePromptElementStyles,
564-
css`
565-
color: #b4b4b4;
566-
font-size: 0.8125rem;
567-
font-weight: 400;
568-
line-height: 1.23;
569-
`,
583+
{
584+
display: 'block',
585+
paddingInlineStart: `calc(${TRACK_INNER_WIDTH(t)} + ${TRACK_PADDING} + ${TRACK_PADDING} + ${t.sizes.$2})`,
586+
fontSize: '0.75rem',
587+
lineHeight: '1.3333333333',
588+
color: '#c3c3c6',
589+
textWrap: 'pretty',
590+
},
570591
]}
571592
>
572-
This is an uncommon setting, meant for applications that sell to both organizations and individual users.
573-
Most B2B applications require users to be part of an organization, and should keep this setting disabled.
574-
</span>
575-
</Flex>
593+
{description}
594+
</Span>
595+
) : null}
576596
</Flex>
577597
);
578598
},

packages/clerk-js/src/ui/components/devPrompts/KeylessPrompt/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ const KeylessPromptInternal = (_props: KeylessPromptProps) => {
118118
minWidth: '13.4rem',
119119
paddingLeft: `${t.space.$3}`,
120120
borderRadius: '1.25rem',
121+
transition: 'all 195ms cubic-bezier(0.2, 0.61, 0.1, 1)',
121122

122123
'&[data-expanded="false"]:hover': {
123124
background: 'linear-gradient(180deg, rgba(255, 255, 255, 0.20) 0%, rgba(255, 255, 255, 0) 100%), #1f1f1f',

packages/clerk-js/src/ui/components/devPrompts/shared.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export function PromptContainer({ children, sx, ...props }: React.ComponentProps
1717
background: 'linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0) 100%), #1f1f1f',
1818
boxShadow:
1919
'0px 0px 0px 0.5px #2F3037 inset, 0px 1px 0px 0px rgba(255, 255, 255, 0.08) inset, 0px 0px 0.8px 0.8px rgba(255, 255, 255, 0.20) inset, 0px 0px 0px 0px rgba(255, 255, 255, 0.72), 0px 16px 36px -6px rgba(0, 0, 0, 0.36), 0px 6px 16px -2px rgba(0, 0, 0, 0.20);',
20-
transition: 'all 195ms cubic-bezier(0.2, 0.61, 0.1, 1)',
2120
},
2221
sx,
2322
]}

0 commit comments

Comments
 (0)