Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ describe('UserProfileSecurityPanelView', () => {
expect(screen.getByRole('group', { name: 'Passkeys' })).toBeInTheDocument();
expect(screen.getByRole('group', { name: '2-step verification' })).toBeInTheDocument();
expect(screen.getByText('This device')).toBeInTheDocument();
expect(screen.getByText('2 other devices')).toBeInTheDocument();
expect(screen.getByText('Clerk App on macOS')).toBeInTheDocument();
expect(
screen.getByText('Permanently delete this account and all its data. This cannot be undone.'),
).toBeInTheDocument();
Expand Down Expand Up @@ -131,8 +131,7 @@ describe('UserProfileSecurityPanelView', () => {
await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Remove', exact: true }));
await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument());

const otherDevices = screen.getByRole('group', { name: 'Other devices' });
await user.click(within(otherDevices).getByRole('button', { name: 'Manage Safari on iOS' }));
await user.click(screen.getByRole('button', { name: 'Manage Safari on iOS' }));
await user.click(screen.getByRole('menuitem', { name: 'Sign out' }));
await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Sign out' }));
await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ export function UserProfileActiveDevicesSectionView({
}
};

// Confirming takes the whole card with it, trigger included — hence the dialog mounted outside
// it, and the current device as the place focus lands. Cancelling keeps the trigger, so focus
// goes back to it.
// Confirming takes the trigger with the other devices — hence the dialog mounted outside it, and
// the current device as the place focus lands. Cancelling keeps the trigger, so focus goes back
// to it.
const focusAfterSignOutAll = () => {
const signedOut = signedOutAll.current;
signedOutAll.current = false;
Expand All @@ -86,7 +86,20 @@ export function UserProfileActiveDevicesSectionView({
<div {...stylex.props(styles.sectionCards)}>
<Section.Root>
<Section.Group>
<Section.Title>{m.title}</Section.Title>
<div {...stylex.props(styles.titleRow)}>
<Section.Title>{m.title}</Section.Title>
{onSignOutAllOtherDevices && otherDevices.length > 0 ? (
<Button
ref={signOutAllTrigger}
color='neutral'
size='sm'
variant='ghost'
onClick={() => setIsSignOutAllOpen(true)}
>
{m.signOutAll}
</Button>
) : null}
</div>
<Section.Surface>
{currentDevices.length > 0 ? (
currentDevices.map(device => (
Expand All @@ -107,50 +120,18 @@ export function UserProfileActiveDevicesSectionView({
</Section.Item>
</Section.Row>
)}
{otherDevices.map(device => (
<Section.Row key={device.id}>
<DeviceItem
device={device}
triggerRef={removalFocus.registerTrigger(device.id)}
onSignOut={openSignOut}
onViewDetails={device => deviceDetails.open(device)}
/>
</Section.Row>
))}
</Section.Surface>
</Section.Group>
{otherDevices.length > 0 ? (
<Section.Group
variant='contained'
aria-label={m.otherDevicesTitle}
>
<Section.Surface>
<Section.Header>
<Section.Content>
<Section.Label>
{fill(otherDevices.length === 1 ? m.otherDevice : m.otherDevices, {
count: String(otherDevices.length),
})}
</Section.Label>
</Section.Content>
{onSignOutAllOtherDevices ? (
<Section.Actions>
<Button
ref={signOutAllTrigger}
color='neutral'
size='sm'
variant='outline'
onClick={() => setIsSignOutAllOpen(true)}
>
{m.signOutAll}
</Button>
</Section.Actions>
) : null}
</Section.Header>
<Section.Items>
{otherDevices.map(device => (
<DeviceItem
key={device.id}
device={device}
triggerRef={removalFocus.registerTrigger(device.id)}
onSignOut={openSignOut}
onViewDetails={device => deviceDetails.open(device)}
/>
))}
</Section.Items>
</Section.Surface>
</Section.Group>
) : null}
</Section.Root>
{onSignOutAllOtherDevices ? (
<Confirmation
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
export const userProfileActiveDevicesMessages = {
title: 'Active devices',
otherDevicesTitle: 'Other devices',
emptyCurrent: 'No current device available',
otherDevice: '{count} other device',
otherDevices: '{count} other devices',
signOutAll: 'Sign out of all devices',
signOutAllError: 'Something went wrong signing these devices out. Please try again.',
signOutAllDialog: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,33 @@ export function UserProfilePasswordRowView({
}: Omit<UserProfilePasswordSectionViewProps, 'sectionTitle'>) {
const m = useMessages('userProfilePasswordSection');
return (
<Section.Row>
<Section.Item>
<>
<Section.Header>
<Section.Content>
<Section.Label>{m.label}</Section.Label>
<Section.Description>{hasPassword ? m.masked : m.noPasswordSet}</Section.Description>
</Section.Content>
{managedBy ? (
<Section.Actions>
<ManagedByLabel {...managedBy} />
</Section.Actions>
) : onSubmitPassword ? (
<Section.Actions>
<EditPassword
hasPassword={hasPassword}
requiresCurrentPassword={requiresCurrentPassword}
onSubmit={onSubmitPassword}
/>
</Section.Actions>
) : null}
</Section.Item>
</Section.Row>
</Section.Header>
<Section.Items>
<Section.Item>
<Section.Content>
<Section.Description>{hasPassword ? m.masked : m.noPasswordSet}</Section.Description>
</Section.Content>
{managedBy ? (
<Section.Actions>
<ManagedByLabel {...managedBy} />
</Section.Actions>
) : onSubmitPassword ? (
<Section.Actions>
<EditPassword
hasPassword={hasPassword}
requiresCurrentPassword={requiresCurrentPassword}
onSubmit={onSubmitPassword}
/>
</Section.Actions>
) : null}
</Section.Item>
</Section.Items>
</>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export function UserProfilePasswordSectionView({
}

const group = (
<Section.Group aria-label={asGroup && !title ? m.label : undefined}>
<Section.Group
variant={asGroup ? 'contained' : 'default'}
aria-label={asGroup ? m.label : undefined}
>
{title ? <Section.Title>{title}</Section.Title> : null}
<Section.Surface>
<UserProfilePasswordRowView
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export const styles = stylex.create({
borderWidth: '1px',
backgroundColor: colorVars['--cl-color-background-subtle'],
},
titleRow: {
alignItems: 'center',
columnGap: space['3'],
display: 'flex',
justifyContent: 'space-between',
},
sectionCards: {
gap: space['3'],
display: 'flex',
Expand Down
10 changes: 10 additions & 0 deletions packages/swingset/src/stories/user-profile.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,13 @@ and animates the popup; the profile paints it, names it, and carries the dismiss
name='Overlay'
storyModule={Stories}
/>

## Flat contained groups

Every contained group restyled flat from external CSS, using the same selectors as the Section
`FlatContainedGroups` example.

<Story
name='FlatContainedGroups'
storyModule={Stories}
/>
87 changes: 87 additions & 0 deletions packages/swingset/src/stories/user-profile.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,90 @@ export function Overlay() {
</Dialog.Root>
);
}

export function FlatContainedGroups() {
const { activePage, setActivePage, pages } = useUserProfileFixture();
return (
<>
<style>{`
@scope {
:scope {
--section-stack-gap: 1.5rem;
}
.cl-user-profile-security-panel .cl-section:first-child > .cl-section-group:first-child > .cl-section-title {
position: absolute !important;
width: 1px !important;
height: 1px !important;
margin: -1px !important;
padding: 0 !important;
overflow: hidden !important;
clip-path: inset(50%) !important;
white-space: nowrap !important;
border: 0 !important;
}
.cl-section,
:has(> .cl-section, > div > .cl-section):not([class*='-panel']) {
gap: var(--section-stack-gap) !important;
}
.cl-section-group:not([data-variant='contained']),
.cl-section-group[data-variant='contained'] > .cl-section-surface {
row-gap: 0 !important;
padding: 0 2px 2px !important;
border: 1px solid var(--cl-color-border-subtle) !important;
border-radius: calc(var(--cl-radius-xl) + 3px) !important;
background: var(--cl-color-background-subtle) !important;
}
.cl-section-group > .cl-section-title,
.cl-section-group > :has(> .cl-section-title),
.cl-section-header {
margin: 0 !important;
padding: 0.625rem calc(1rem + 3px) !important;
border: none !important;
}
.cl-section-group:has(.cl-section-actions > .cl-button[data-color='negative']) {
border-color: transparent !important;
background: var(--cl-color-negative-subtle) !important;
}
.cl-section-group:has(.cl-section-actions > .cl-button[data-color='negative']) > .cl-section-title {
color: var(--cl-color-negative) !important;
}
.cl-section-header .cl-section-label {
font-family: var(--cl-font-family-sans) !important;
font-size: var(--cl-text-base-size) !important;
font-weight: var(--cl-font-semibold) !important;
line-height: var(--cl-text-base-leading) !important;
}
.cl-section-header .cl-button {
padding: 0 !important;
border: none !important;
background: none !important;
}
.cl-section-items {
border: 1px solid var(--cl-color-border) !important;
border-radius: var(--cl-radius-xl) !important;
background: var(--cl-color-background) !important;
}
.cl-section-group:not([data-variant='contained']) > .cl-section-surface > .cl-section-row {
margin-inline: 0 !important;
padding-inline: calc(var(--cl-spacing) * 4) !important;
}
.cl-section-items {
padding-inline: 0 !important;
}
.cl-section-items > .cl-section-item {
padding-inline: calc(var(--cl-spacing) * 4) !important;
}
.cl-section-items .cl-section-item {
min-height: var(--cl-section-row-min-height) !important;
}
}
`}</style>
<UserProfileView
activePage={activePage}
pages={pages}
onPageChange={setActivePage}
elevation='flush'
/>
</>
);
}
Loading