diff --git a/.changeset/section-contained-groups.md b/.changeset/section-contained-groups.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/section-contained-groups.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/mosaic/src/components/panel/panel.styles.ts b/packages/mosaic/src/components/panel/panel.styles.ts index 65befe926a3..18b83f92f02 100644 --- a/packages/mosaic/src/components/panel/panel.styles.ts +++ b/packages/mosaic/src/components/panel/panel.styles.ts @@ -4,7 +4,7 @@ import { colorVars, radiusVars, space } from '../../tokens.stylex'; export const styles = stylex.create({ root: { - gap: space['4'], + gap: space['6'], display: 'flex', flexDirection: 'column', }, @@ -40,7 +40,7 @@ export const styles = stylex.create({ verticalAlign: 'middle', }, sections: { - gap: space['10'], + gap: space['8'], display: 'flex', flexDirection: 'column', }, diff --git a/packages/mosaic/src/components/panel/panel.test.tsx b/packages/mosaic/src/components/panel/panel.test.tsx index d3d6f600fe2..b1cb0260563 100644 --- a/packages/mosaic/src/components/panel/panel.test.tsx +++ b/packages/mosaic/src/components/panel/panel.test.tsx @@ -13,7 +13,11 @@ function Account() { Account - Email addresses + + + Email addresses + + diff --git a/packages/mosaic/src/components/profile/profile.test.tsx b/packages/mosaic/src/components/profile/profile.test.tsx index a2aa563fb59..93c559ca09c 100644 --- a/packages/mosaic/src/components/profile/profile.test.tsx +++ b/packages/mosaic/src/components/profile/profile.test.tsx @@ -37,7 +37,11 @@ function Surface(rootProps: Partial) { Account - Email addresses + + + Email addresses + + diff --git a/packages/mosaic/src/components/section/index.ts b/packages/mosaic/src/components/section/index.ts index 559650ed471..f1c1cf21ee3 100644 --- a/packages/mosaic/src/components/section/index.ts +++ b/packages/mosaic/src/components/section/index.ts @@ -2,9 +2,11 @@ export { Section } from './section'; export { sectionCompactStyles } from './section.styles'; export type { SectionActionsProps, + SectionBodyProps, SectionContentProps, SectionDescriptionProps, SectionGroupProps, + SectionHeaderProps, SectionItemProps, SectionItemsProps, SectionLabelProps, diff --git a/packages/mosaic/src/components/section/section.markers.stylex.ts b/packages/mosaic/src/components/section/section.markers.stylex.ts index b0755da6f9c..fac54253625 100644 --- a/packages/mosaic/src/components/section/section.markers.stylex.ts +++ b/packages/mosaic/src/components/section/section.markers.stylex.ts @@ -1,3 +1,5 @@ import * as stylex from '@stylexjs/stylex'; -export const sectionItemsMarker = stylex.defineMarker(); +export const sectionHeaderMarker = stylex.defineMarker(); +export const sectionHeaderDescriptionMarker = stylex.defineMarker(); +export const sectionNestedItemMarker = stylex.defineMarker(); diff --git a/packages/mosaic/src/components/section/section.styles.ts b/packages/mosaic/src/components/section/section.styles.ts index 20f7e45dc94..3cfef0569b5 100644 --- a/packages/mosaic/src/components/section/section.styles.ts +++ b/packages/mosaic/src/components/section/section.styles.ts @@ -1,80 +1,92 @@ import * as stylex from '@stylexjs/stylex'; import { colorVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; -import { sectionItemsMarker } from './section.markers.stylex'; +import { sectionHeaderDescriptionMarker, sectionHeaderMarker, sectionNestedItemMarker } from './section.markers.stylex'; const compact = '@container cl-section (width < 26rem)'; -/* eslint-disable @stylexjs/no-lookahead-selectors -- Mosaic's supported browsers include :has(); - the marker keeps this selector scoped to Section.Items. */ export const styles = stylex.create({ root: { display: 'flex', flexDirection: 'column', - rowGap: space['3'], + rowGap: space['8'], width: '100%', }, - title: { - color: colorVars['--cl-color-foreground'], - fontWeight: fontWeightVars['--cl-font-medium'], - }, group: { borderColor: colorVars['--cl-color-border'], borderRadius: radiusVars['--cl-radius-xl'], borderStyle: 'solid', borderWidth: '1px', - overflow: 'hidden', backgroundColor: colorVars['--cl-color-background'], containerName: 'cl-section', containerType: 'inline-size', + display: 'flex', + flexDirection: 'column', width: '100%', }, - row: { + header: { + marginInline: space['4'], + paddingBlock: space['3'], + alignContent: 'center', + alignItems: 'center', + columnGap: space['3'], + display: 'grid', + gridTemplateColumns: 'minmax(0, 1fr) auto', + minHeight: space['13'], + width: 'auto', + }, + title: { + gridColumn: '1', + color: colorVars['--cl-color-foreground'], + fontWeight: fontWeightVars['--cl-font-medium'], + }, + headerDescription: { + gridColumn: '1', + marginTop: space['0.5'], + }, + headerActions: { + gridColumn: '2', + gridRowEnd: { + default: 'auto', + [stylex.when.siblingBefore(':where(*)', sectionHeaderDescriptionMarker)]: 'span 2', + }, + gridRowStart: '1', + }, + body: { marginInline: space['4'], borderBlockStartColor: colorVars['--cl-color-border'], borderBlockStartStyle: 'solid', + borderBlockStartWidth: { + default: '0px', + [stylex.when.siblingBefore(':where(*)', sectionHeaderMarker)]: '1px', + }, + display: 'flex', + flexDirection: 'column', + width: 'auto', + }, + row: { + paddingBlock: space['4'], + borderBlockStartColor: colorVars['--cl-color-border'], + borderBlockStartStyle: 'solid', borderBlockStartWidth: { default: '1px', ':first-child': '0px', }, display: 'flex', flexDirection: 'column', - paddingBlockEnd: { - default: space['4'], - [stylex.when.descendant(':where(*)', sectionItemsMarker)]: 0, - }, - paddingBlockStart: { - default: space['4'], - [stylex.when.descendant(':where(*)', sectionItemsMarker)]: space['3'], - }, - rowGap: { - default: space['2'], - [stylex.when.descendant(':where(*)', sectionItemsMarker)]: 0, - }, - minHeight: { - default: `calc(${space['18.5']} + 1px)`, - [stylex.when.descendant(':where(*)', sectionItemsMarker)]: 0, - }, - width: 'auto', + justifyContent: 'center', + rowGap: space['2'], + minHeight: `calc(${space['18.5']} + 1px)`, + width: '100%', }, items: { + listStyle: 'none', display: 'flex', flexDirection: 'column', - marginBlockStart: space['3'], width: '100%', }, item: { - paddingBlock: { - default: null, - [stylex.when.ancestor(':where(*)', sectionItemsMarker)]: space['4'], - }, alignItems: 'center', - borderBlockStartColor: colorVars['--cl-color-border'], - borderBlockStartStyle: 'solid', - borderBlockStartWidth: { - default: '0px', - [stylex.when.ancestor(':where(*)', sectionItemsMarker)]: '1px', - }, columnGap: space['3'], display: 'flex', flexWrap: 'nowrap', @@ -85,6 +97,15 @@ export const styles = stylex.create({ flexWrap: { [compact]: 'wrap', default: 'nowrap' }, rowGap: { [compact]: space['3'], default: null }, }, + nestedItem: { + paddingBlock: space['4'], + borderBlockStartColor: colorVars['--cl-color-border'], + borderBlockStartStyle: 'solid', + borderBlockStartWidth: { + default: '0px', + [stylex.when.siblingBefore(':where(*)', sectionNestedItemMarker)]: '1px', + }, + }, mediaBase: { alignItems: 'center', alignSelf: 'center', @@ -146,7 +167,6 @@ export const styles = stylex.create({ width: '100%', }, }); -/* eslint-enable @stylexjs/no-lookahead-selectors */ export const sectionCompactStyles = stylex.create({ hidden: { diff --git a/packages/mosaic/src/components/section/section.test.tsx b/packages/mosaic/src/components/section/section.test.tsx index 16f84e8acb3..3eea27e3a23 100644 --- a/packages/mosaic/src/components/section/section.test.tsx +++ b/packages/mosaic/src/components/section/section.test.tsx @@ -1,5 +1,5 @@ import * as stylex from '@stylexjs/stylex'; -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor, within } from '@testing-library/react'; import React from 'react'; import { describe, expect, it } from 'vitest'; @@ -9,6 +9,7 @@ import { Section } from './section'; const overrides = stylex.create({ root: { containerType: 'inline-size' }, group: { borderWidth: 2 }, + body: { marginInline: 0 }, item: { minHeight: 80 }, label: { color: 'red' }, }); @@ -17,35 +18,52 @@ const atoms = (style: stylex.StyleXStyles) => (stylex.props(style).className ?? '').split(' ').filter(name => /^x[a-z0-9]+$/.test(name)); describe('Section', () => { - it('renders an accessible section and every compound part', () => { + it('renders a card named by its heading, with every compound part', () => { render( - - Account + - - - - Icon - - - Name - Shown throughout the application. - - Control - - + + Account + Who you are to the application. + Add + + + + + + Icon + + + Name + Shown throughout the application. + + Control + + + , ); - expect(screen.getByRole('region', { name: 'Account' })).toHaveClass('cl-section'); + const group = screen.getByRole('group', { name: 'Account' }); + expect(group).toBe(screen.getByTestId('group')); + expect(group).toHaveClass('cl-section-group'); + expect(group.tagName).toBe('DIV'); + expect(screen.getByTestId('root')).toHaveClass('cl-section'); + expect(screen.getByTestId('root').tagName).toBe('SECTION'); + expect(screen.queryByRole('region')).not.toBeInTheDocument(); expect(screen.getByRole('heading', { level: 2, name: 'Account' })).toHaveClass('cl-section-title'); - expect(screen.getByTestId('group')).toHaveClass('cl-section-group'); + expect(screen.getByTestId('header')).toHaveClass('cl-section-header'); + expect(screen.getByTestId('header')).toContainElement(screen.getByRole('heading', { name: 'Account' })); + expect(screen.getByTestId('header-description')).toHaveClass('cl-section-description'); + expect(screen.getByTestId('header-actions')).toHaveClass('cl-section-actions'); + expect(screen.getByTestId('body')).toHaveClass('cl-section-body'); expect(screen.getByTestId('row')).toHaveClass('cl-section-row'); expect(screen.getByTestId('item')).toHaveClass('cl-section-item'); + expect(screen.getByTestId('item').tagName).toBe('DIV'); expect(screen.getByTestId('media')).toHaveClass('cl-section-media'); expect(screen.getByTestId('media')).toHaveAttribute('data-size', 'lg'); expect(screen.getByTestId('content')).toHaveClass('cl-section-content'); @@ -54,11 +72,40 @@ describe('Section', () => { expect(screen.getByTestId('actions')).toHaveClass('cl-section-actions'); }); + it('names each card by its own title', () => { + render( + + + + Email + + + + + + Phone + + + + , + ); + + const email = screen.getByRole('group', { name: 'Email' }); + const phone = screen.getByRole('group', { name: 'Phone' }); + expect(email).not.toBe(phone); + expect(email).toHaveAttribute('aria-labelledby', screen.getByRole('heading', { name: 'Email' }).id); + expect(phone).toHaveAttribute('aria-labelledby', screen.getByRole('heading', { name: 'Phone' }).id); + }); + it('takes its title level from an enclosing HeadingLevelProvider', () => { render( - Account + + + Account + + , ); @@ -68,84 +115,100 @@ describe('Section', () => { it('lets the render prop override the heading level', () => { render( - - }>Account - , + + + }>Account + + , ); expect(screen.getByRole('heading', { level: 5, name: 'Account' })).toHaveClass('cl-section-title'); + expect(screen.getByRole('group', { name: 'Account' })).toBeInTheDocument(); }); - it('supports an explicit accessible name', () => { + it('supports explicit accessible names on the section and on a card', () => { render( - Account - + + + + + + Email + + , ); expect(screen.getByRole('region', { name: 'Account preferences' })).toBeInTheDocument(); - expect(screen.getByRole('heading', { level: 2, name: 'Account' })).toBeInTheDocument(); + expect(screen.getByRole('group', { name: 'Password' })).not.toHaveAttribute('aria-labelledby'); + expect(screen.getByRole('group', { name: 'Contact' })).toBeInTheDocument(); + expect(screen.queryByRole('group', { name: 'Email' })).not.toBeInTheDocument(); }); - it('composes multiple items in one row', () => { + it('renders a list of values as a real list under the header', () => { render( - - Profile - - + + + Email + Add + + + + + + ada@example.com + + More + - Email + grace@example.com - Edit - - - - ada@example.com - - More - - - - - , + + + , ); - expect(screen.getByText('Email')).toBeInTheDocument(); - expect(screen.getByText('ada@example.com')).toBeInTheDocument(); - expect(screen.getAllByText(/Edit|More/)).toHaveLength(2); - expect(screen.getByTestId('items')).toHaveClass('cl-section-items'); - expect(screen.getByTestId('row')).not.toHaveAttribute('data-variant'); - expect(screen.getByTestId('items')).toHaveAttribute('data-nested'); + const list = screen.getByRole('list'); + expect(list).toBe(screen.getByTestId('items')); + expect(list).toHaveClass('cl-section-items'); + expect(list.tagName).toBe('UL'); + expect(within(list).getAllByRole('listitem')).toHaveLength(2); + expect(screen.getByTestId('nested-item').tagName).toBe('LI'); expect(screen.getByTestId('nested-item')).toHaveAttribute('data-nested'); expect(screen.getByTestId('nested-content')).toHaveAttribute('data-nested'); + expect(screen.getByTestId('header')).not.toHaveAttribute('data-nested'); + expect(screen.getAllByText(/Add|More/)).toHaveLength(2); }); - it('retains public nesting hooks while deriving layout from the item collection structure', () => { + it('marks only items inside Section.Items as nested', () => { render( - - - - Email - - one@example.com - two@example.com - + + + + Name - - , + + one@example.com + two@example.com + + + , ); - expect(screen.getByTestId('row')).not.toHaveAttribute('data-variant'); + expect(screen.getByText('Name')).not.toHaveAttribute('data-nested'); + expect(screen.getByText('Name').tagName).toBe('DIV'); expect(screen.getByText('one@example.com')).toHaveAttribute('data-nested'); expect(screen.getByText('two@example.com')).toHaveAttribute('data-nested'); + expect(screen.queryByRole('listitem', { name: 'Name' })).not.toBeInTheDocument(); }); it('applies xstyle on every part and forwards refs and custom elements', () => { const rootRef = React.createRef(); const groupRef = React.createRef(); + const bodyRef = React.createRef(); const itemRef = React.createRef(); const contentRef = React.createRef(); const actionsRef = React.createRef(); @@ -156,22 +219,29 @@ describe('Section', () => { render={props =>
} xstyle={overrides.root} > - Account - - - - Name - - - - + + Account + + + + + + Name + + + + + , ); @@ -179,6 +249,7 @@ describe('Section', () => { expect(rootRef.current?.tagName).toBe('ARTICLE'); expect(rootRef.current).toHaveClass('cl-section', ...atoms(overrides.root)); expect(groupRef.current).toHaveClass('cl-section-group', ...atoms(overrides.group)); + expect(bodyRef.current).toHaveClass('cl-section-body', ...atoms(overrides.body)); expect(itemRef.current).toHaveClass('cl-section-item', ...atoms(overrides.item)); expect(contentRef.current).toHaveClass('cl-section-content'); expect(screen.getByText('Name')).toHaveClass('cl-section-label', ...atoms(overrides.label)); @@ -187,20 +258,18 @@ describe('Section', () => { it('merges a render-sourced className instead of clobbering its own', () => { render( - - }> - Name - - , + }> + Name + , ); - expect(screen.getByText('Name').parentElement).toHaveClass('cl-section-group', 'cl-section-row'); + expect(screen.getByText('Name').parentElement).toHaveClass('cl-section-body', 'cl-section-row'); }); it('renders a row-level error as a sibling of the item, with the alert glyph', () => { render( - - + + @@ -209,8 +278,8 @@ describe('Section', () => { File type not supported. - - , + + , ); const error = screen.getByTestId('error'); diff --git a/packages/mosaic/src/components/section/section.tsx b/packages/mosaic/src/components/section/section.tsx index 161b2d095d8..4baeeca3ee7 100644 --- a/packages/mosaic/src/components/section/section.tsx +++ b/packages/mosaic/src/components/section/section.tsx @@ -12,14 +12,16 @@ import { reset } from '../../utils/reset.styles'; import { sizes as typographySizes, styles as typographyStyles } from '../../utils/typography.styles'; import type { HeadingProps } from '../heading'; import { Heading, useHeadingLevel } from '../heading'; -import { sectionItemsMarker } from './section.markers.stylex'; +import { sectionHeaderDescriptionMarker, sectionHeaderMarker, sectionNestedItemMarker } from './section.markers.stylex'; import { styles } from './section.styles'; export type SectionRootProps = Omit, 'title'>; -export type SectionTitleProps = Omit; export type SectionGroupProps = MosaicComponentProps<'div'>; +export type SectionHeaderProps = MosaicComponentProps<'div'>; +export type SectionTitleProps = Omit; +export type SectionBodyProps = MosaicComponentProps<'div'>; export type SectionRowProps = MosaicComponentProps<'div'>; -export type SectionItemsProps = MosaicComponentProps<'div'>; +export type SectionItemsProps = MosaicComponentProps<'ul'>; export type SectionItemProps = MosaicComponentProps<'div'> & { wrap?: boolean }; export type SectionMediaSize = 'sm' | 'md' | 'lg' | 'xl'; export type SectionMediaProps = MosaicComponentProps<'div'> & { size?: SectionMediaSize }; @@ -36,103 +38,121 @@ const mediaSizes = { xl: styles.mediaXl, }; -const SectionTitleContext = React.createContext> | null>(null); +const SectionGroupContext = React.createContext> | null>(null); +const SectionHeaderContext = React.createContext(false); const SectionItemsContext = React.createContext(false); const SectionItemWrapContext = React.createContext(false); -const Root = React.forwardRef(function SectionRoot( +const Root = React.forwardRef(function SectionRoot({ render, xstyle, ...rest }, ref) { + return useRender({ + defaultTagName: 'section', + render, + ref, + props: mergeStyleProps(themeProps('section'), stylex.props(reset.base, styles.root, xstyle), rest), + }); +}); + +const Group = React.forwardRef(function SectionGroup( { render, xstyle, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...rest }, ref, ) { - const [titleIds, setTitleIds] = React.useState([]); + const [titleId, setTitleId] = React.useState(); const element = useRender({ - defaultTagName: 'section', + defaultTagName: 'div', render, ref, props: { - ...mergeStyleProps(themeProps('section'), stylex.props(reset.base, styles.root, xstyle), rest), + role: 'group', + ...mergeStyleProps(themeProps('section-group'), stylex.props(reset.base, styles.group, xstyle), rest), 'aria-label': ariaLabel, - 'aria-labelledby': ariaLabelledBy ?? (ariaLabel ? undefined : titleIds.join(' ') || undefined), + 'aria-labelledby': ariaLabelledBy ?? (ariaLabel ? undefined : titleId), + }, + }); + + return {element}; +}); + +const Header = React.forwardRef(function SectionHeader( + { render, xstyle, ...rest }, + ref, +) { + const element = useRender({ + defaultTagName: 'div', + render, + ref, + props: { + ...mergeStyleProps( + themeProps('section-header'), + stylex.props(reset.base, styles.header, sectionHeaderMarker, xstyle), + rest, + ), }, }); - return {element}; + return {element}; }); const Title = React.forwardRef(function SectionTitle( - { id: idProp, render, xstyle, ...rest }, + { id: idProp, xstyle, ...rest }, ref, ) { - const setTitleIds = React.useContext(SectionTitleContext); + const setTitleId = React.useContext(SectionGroupContext); const generatedId = React.useId(); const level = useHeadingLevel(); - const id = idProp ?? (setTitleIds ? `cl-section-${generatedId}-title` : undefined); + const id = idProp ?? (setTitleId ? `cl-section-${generatedId}-title` : undefined); useSafeLayoutEffect(() => { - if (!id || !setTitleIds) { + if (!id || !setTitleId) { return undefined; } - setTitleIds(ids => (ids.includes(id) ? ids : [...ids, id])); - return () => setTitleIds(ids => ids.filter(value => value !== id)); - }, [id, setTitleIds]); + setTitleId(id); + return () => setTitleId(current => (current === id ? undefined : current)); + }, [id, setTitleId]); return ( ); }); -const Group = React.forwardRef(function SectionGroup( - { render, xstyle, ...rest }, - ref, -) { +const Body = React.forwardRef(function SectionBody({ render, xstyle, ...rest }, ref) { return useRender({ defaultTagName: 'div', render, ref, - props: { - ...mergeStyleProps(themeProps('section-group'), stylex.props(reset.base, styles.group, xstyle), rest), - }, + props: mergeStyleProps(themeProps('section-body'), stylex.props(reset.base, styles.body, xstyle), rest), }); }); -const Items = React.forwardRef(function SectionItems( - { render, xstyle, ...rest }, - ref, -) { - const element = useRender({ +const Row = React.forwardRef(function SectionRow({ render, xstyle, ...rest }, ref) { + return useRender({ defaultTagName: 'div', render, ref, - props: { - ...mergeStyleProps( - themeProps('section-items', { nested: true }), - stylex.props(reset.base, styles.items, sectionItemsMarker, xstyle), - rest, - ), - }, + props: mergeStyleProps(themeProps('section-row'), stylex.props(reset.base, styles.row, xstyle), rest), }); - - return {element}; }); -const Row = React.forwardRef(function SectionRow({ render, xstyle, ...rest }, ref) { - return useRender({ - defaultTagName: 'div', +const Items = React.forwardRef(function SectionItems( + { render, xstyle, ...rest }, + ref, +) { + const element = useRender({ + defaultTagName: 'ul', render, ref, - props: { - ...mergeStyleProps(themeProps('section-row'), stylex.props(reset.base, styles.row, xstyle), rest), - }, + props: mergeStyleProps(themeProps('section-items'), stylex.props(reset.base, styles.items, xstyle), rest), }); + + return {element}; }); const Item = React.forwardRef(function SectionItem( @@ -142,16 +162,21 @@ const Item = React.forwardRef(function Section const nested = React.useContext(SectionItemsContext); const element = useRender({ - defaultTagName: 'div', + defaultTagName: nested ? 'li' : 'div', render, ref, - props: { - ...mergeStyleProps( - themeProps('section-item', { nested, wrap }), - stylex.props(reset.base, styles.item, wrap && styles.itemWrap, xstyle), - rest, + props: mergeStyleProps( + themeProps('section-item', { nested, wrap }), + stylex.props( + reset.base, + styles.item, + nested && styles.nestedItem, + nested && sectionNestedItemMarker, + wrap && styles.itemWrap, + xstyle, ), - }, + rest, + ), }); return {element}; @@ -165,13 +190,11 @@ const Media = React.forwardRef(function Secti defaultTagName: 'div', render, ref, - props: { - ...mergeStyleProps( - themeProps('section-media', { size }), - stylex.props(reset.base, styles.mediaBase, mediaSizes[size], xstyle), - rest, - ), - }, + props: mergeStyleProps( + themeProps('section-media', { size }), + stylex.props(reset.base, styles.mediaBase, mediaSizes[size], xstyle), + rest, + ), }); }); @@ -185,13 +208,11 @@ const Content = React.forwardRef(function S defaultTagName: 'div', render, ref, - props: { - ...mergeStyleProps( - themeProps('section-content', { nested }), - stylex.props(reset.base, styles.content, xstyle), - rest, - ), - }, + props: mergeStyleProps( + themeProps('section-content', { nested }), + stylex.props(reset.base, styles.content, xstyle), + rest, + ), }); }); @@ -203,9 +224,7 @@ const Label = React.forwardRef(function Secti defaultTagName: 'div', render, ref, - props: { - ...mergeStyleProps(themeProps('section-label'), stylex.props(reset.base, styles.label, xstyle), rest), - }, + props: mergeStyleProps(themeProps('section-label'), stylex.props(reset.base, styles.label, xstyle), rest), }); }); @@ -213,13 +232,23 @@ const Description = React.forwardRef(fu { render, xstyle, ...rest }, ref, ) { + const inHeader = React.useContext(SectionHeaderContext); + return useRender({ defaultTagName: 'div', render, ref, - props: { - ...mergeStyleProps(themeProps('section-description'), stylex.props(reset.base, styles.description, xstyle), rest), - }, + props: mergeStyleProps( + themeProps('section-description'), + stylex.props( + reset.base, + styles.description, + inHeader && styles.headerDescription, + inHeader && sectionHeaderDescriptionMarker, + xstyle, + ), + rest, + ), }); }); @@ -228,18 +257,17 @@ const Actions = React.forwardRef(function S ref, ) { const wrap = React.useContext(SectionItemWrapContext); + const inHeader = React.useContext(SectionHeaderContext); return useRender({ defaultTagName: 'div', render, ref, - props: { - ...mergeStyleProps( - themeProps('section-actions'), - stylex.props(reset.base, styles.actions, wrap && styles.actionsWrap, xstyle), - rest, - ), - }, + props: mergeStyleProps( + themeProps('section-actions'), + stylex.props(reset.base, styles.actions, wrap && styles.actionsWrap, inHeader && styles.headerActions, xstyle), + rest, + ), }); }); @@ -300,13 +328,16 @@ const SectionError = React.forwardRef(f }); /** - * A compound component that fixes section semantics, surface treatment, row grouping, - * and item layout while leaving each item's content composable. + * A compound component for a topic of settings. `Section.Root` stacks cards; each `Section.Group` + * is a card named by the `Section.Title` in its `Section.Header`, with a `Section.Body` holding + * either `Section.Row`s (one setting each) or a `Section.Items` list of values. */ export const Section = { Root, - Title, Group, + Header, + Title, + Body, Row, Items, Item, diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.view.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.view.tsx index 22d4fd7ec69..29b6aaf86a7 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.view.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.view.tsx @@ -26,30 +26,34 @@ export function OrganizationProfileDangerSectionView({ return ( - {m.sectionTitle} - {onLeave ? ( - - ) : null} - {onDelete ? ( - - ) : null} + + {m.sectionTitle} + + + {onLeave ? ( + + ) : null} + {onDelete ? ( + + ) : null} + ); diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-workspace-section/organization-profile-workspace-section.view.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-workspace-section/organization-profile-workspace-section.view.tsx index 11d06db54d8..8d4031cb3bf 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-workspace-section/organization-profile-workspace-section.view.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-workspace-section/organization-profile-workspace-section.view.tsx @@ -32,24 +32,28 @@ export function OrganizationProfileWorkspaceSectionView({ return ( - {m.sectionTitle} - - - + + {m.sectionTitle} + + + + + + ); diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-billing-panel.view.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-billing-panel.view.test.tsx index 3b51dd0350a..fa3df75c6cb 100644 --- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-billing-panel.view.test.tsx +++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-billing-panel.view.test.tsx @@ -55,7 +55,8 @@ describe('UserProfileBillingPanelView', () => { expect(screen.getByRole('heading', { level: 2, name: 'Billing' })).toBeInTheDocument(); expect(screen.getByRole('heading', { level: 3, name: 'Subscription' })).toBeInTheDocument(); - expect(screen.getByRole('region', { name: 'Payment methods' })).toBeInTheDocument(); + expect(screen.getByRole('group', { name: 'Payment methods' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 3, name: 'Payment methods' })).toBeInTheDocument(); expect(screen.getByText('Basic Plan')).toBeInTheDocument(); expect(screen.getByText('$12.00')).toBeInTheDocument(); expect(screen.getByText('Visa •••• 0644')).toBeInTheDocument(); diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.view.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.view.test.tsx index d265a034b6d..c5996575d9d 100644 --- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.view.test.tsx +++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.view.test.tsx @@ -77,7 +77,8 @@ describe('UserProfileEnterpriseAccountsSectionView', () => { ]} />, ); - expect(screen.getByRole('region', { name: 'Enterprise accounts' })).toBeInTheDocument(); + expect(screen.getByRole('group', { name: 'Enterprise accounts' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Enterprise accounts' })).toBeInTheDocument(); expect(screen.getByText('Acme Okta')).toBeInTheDocument(); expect(screen.getByText('test@acme.com')).toBeInTheDocument(); expect(screen.getByText('Requires action')).toBeInTheDocument(); diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-mfa-section.view.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-mfa-section.view.test.tsx index 9ead5c56cd0..d5042353c3a 100644 --- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-mfa-section.view.test.tsx +++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-mfa-section.view.test.tsx @@ -42,7 +42,6 @@ describe('MFA section', () => { {} : undefined} addControl={add === 'custom' ? : undefined} @@ -61,7 +60,7 @@ describe('MFA section', () => { await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Remove' })); const fallback = add === 'none' - ? screen.getByRole('region', { name: 'Authentication' }) + ? screen.getByRole('group', { name: '2-step verification' }) : screen.getByRole('button', { name: add === 'picker' ? 'Add verification method' : 'Add custom method' }); await waitFor(() => expect(fallback).toHaveFocus()); }, diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-passkeys-section.view.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-passkeys-section.view.test.tsx index fd65d4206b1..4de4c2d4f02 100644 --- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-passkeys-section.view.test.tsx +++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-passkeys-section.view.test.tsx @@ -29,10 +29,10 @@ function renderView(overrides: Partial = {} describe('passkeys section', () => { it('shows existing passkeys without actions when their callbacks are unavailable', () => { - renderView({ onAdd: undefined, onRename: undefined, onRemove: undefined, sectionTitle: 'Authentication' }); + renderView({ onAdd: undefined, onRename: undefined, onRemove: undefined }); - expect(screen.getByRole('heading', { name: 'Authentication' })).toBeVisible(); - expect(screen.getByText('Passkeys')).toBeVisible(); + expect(screen.getByRole('group', { name: 'Passkeys' })).toBeVisible(); + expect(screen.getByRole('heading', { name: 'Passkeys' })).toBeVisible(); expect(screen.getByText('MacBook')).toBeVisible(); expect(screen.getByText('iPhone')).toBeVisible(); expect(screen.queryByRole('button')).not.toBeInTheDocument(); @@ -120,10 +120,10 @@ describe('passkeys section', () => { }); it('keeps an empty section visible when Add is unavailable', () => { - renderView({ passkeys: [], onAdd: undefined, sectionTitle: 'Authentication' }); + renderView({ passkeys: [], onAdd: undefined }); - expect(screen.getByRole('heading', { name: 'Authentication' })).toBeVisible(); - expect(screen.getByText('Passkeys')).toBeVisible(); + expect(screen.getByRole('group', { name: 'Passkeys' })).toBeVisible(); + expect(screen.getByRole('heading', { name: 'Passkeys' })).toBeVisible(); expect(screen.getByText('No passkeys added')).toBeVisible(); expect(screen.queryByRole('button', { name: 'Add passkey' })).not.toBeInTheDocument(); }); @@ -179,7 +179,7 @@ describe('passkeys section', () => { await user.click(screen.getByRole('button', { name: 'Add passkey' })); expect(screen.getByText('MacBook')).toBeVisible(); } else { - expect(screen.getByRole('region', { name: 'Passkeys' })).toHaveFocus(); + expect(screen.getByRole('group', { name: 'Passkeys' })).toHaveFocus(); } }); diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-password-section.view.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-password-section.view.test.tsx index 9fa73ae723a..2a9b023d80a 100644 --- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-password-section.view.test.tsx +++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-password-section.view.test.tsx @@ -43,8 +43,8 @@ describe('UserProfilePasswordSectionView', () => { const user = userEvent.setup(); renderView({ hasPassword: false, onSubmitPassword }); - expect(screen.getByRole('heading', { level: 2, name: 'Authentication' })).toBeInTheDocument(); - expect(screen.getByText('Password')).toBeVisible(); + expect(screen.getByRole('group', { name: 'Password' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 2, name: 'Password' })).toHaveClass('cl-section-title'); expect(screen.queryByText('••••••••••••••••••')).not.toBeInTheDocument(); expect(screen.getByText('No password set')).toBeVisible(); await user.click(screen.getByRole('button', { name: 'Set password' })); @@ -109,7 +109,7 @@ describe('UserProfilePasswordSectionView', () => { , ); - expect(screen.getByRole('region', { name: 'Authentication' })).toBeVisible(); + expect(screen.getByRole('group', { name: 'Password' })).toBeVisible(); expect(screen.queryByRole('button')).not.toBeInTheDocument(); }); diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx index a12b39214b2..94f16ce7583 100644 --- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx +++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx @@ -137,8 +137,9 @@ describe('UserProfileProfilePanelView', () => { expect(screen.getByRole('heading', { level: 2, name: 'Account' })).toBeInTheDocument(); expect(screen.getByRole('region', { name: 'Account' })).toContainElement( - document.querySelector('.cl-section-group'), + screen.getByRole('group', { name: 'Profile' }), ); + expect(screen.getByRole('heading', { level: 3, name: 'Profile' })).toHaveClass('cl-section-title'); expect(screen.getByText('Name')).toHaveClass('cl-section-label'); expect(screen.getByText('Username')).toHaveClass('cl-section-label'); expect(screen.getByText('Preston Booth')).toHaveClass('cl-section-description'); @@ -151,8 +152,8 @@ describe('UserProfileProfilePanelView', () => { expect(screen.getByText('+1 (801) 888-8181')).toBeInTheDocument(); expect(screen.getByText('Profile picture')).toHaveClass('cl-section-label'); expect(screen.getByText('Recommend size 1:1, up to 10MB.')).toHaveClass('cl-section-description'); - expect(screen.getByText('Email')).toHaveClass('cl-section-label'); - expect(screen.getByText('Phone')).toHaveClass('cl-section-label'); + expect(screen.getByRole('heading', { level: 3, name: 'Email' })).toHaveClass('cl-section-title'); + expect(screen.getByRole('heading', { level: 3, name: 'Phone' })).toHaveClass('cl-section-title'); expect(screen.getByText('item1@clerk.dev').closest('.cl-section-description')).not.toBeNull(); expect(screen.getByRole('button', { name: 'Upload' })).toBeInTheDocument(); const profilePicture = screen.getByText('Profile picture').closest('.cl-section-item'); @@ -213,7 +214,7 @@ describe('UserProfileProfilePanelView', () => { expect(screen.queryByRole('alert')).toBeNull(); }); - it('breaks out both contact types when multiple accounts are allowed', () => { + it('nests both contact types as groups inside Account when multiple accounts are allowed', () => { renderView({ emails: [{ id: 'email_1', value: 'item1@clerk.dev', isDefault: true }], onAddEmail: vi.fn(), @@ -222,12 +223,15 @@ describe('UserProfileProfilePanelView', () => { }); const accountSection = screen.getByRole('region', { name: 'Account' }); - const emailSection = screen.getByRole('region', { name: 'Email' }); - const phoneSection = screen.getByRole('region', { name: 'Phone' }); + const emailSection = within(accountSection).getByRole('group', { name: 'Email' }); + const phoneSection = within(accountSection).getByRole('group', { name: 'Phone' }); - expect(accountSection).not.toContainElement(emailSection); - expect(accountSection).not.toContainElement(phoneSection); - expect(emailSection).toHaveTextContent('item1@clerk.dev'); + expect(screen.queryByRole('region', { name: 'Email' })).not.toBeInTheDocument(); + expect(screen.queryByRole('region', { name: 'Phone' })).not.toBeInTheDocument(); + expect(within(emailSection).getByRole('heading', { level: 3, name: 'Email' })).toBeInTheDocument(); + expect(emailSection.querySelector('.cl-section-header')).toHaveTextContent('Email'); + expect(within(emailSection).getByRole('list')).toContainElement(screen.getByText('item1@clerk.dev')); + expect(within(emailSection).getAllByRole('listitem')).toHaveLength(1); expect(phoneSection).toHaveTextContent('+1 (801) 888-8181'); expect(within(emailSection).getByRole('button', { name: 'Add email' })).toHaveTextContent('Add'); expect(within(phoneSection).getByRole('button', { name: 'Add phone number' })).toHaveTextContent('Add'); @@ -247,8 +251,8 @@ describe('UserProfileProfilePanelView', () => { expect(accountSection).toHaveTextContent('+1 (801) 888-8181'); expect(within(accountSection).getByRole('button', { name: 'Update email' })).toBeInTheDocument(); expect(within(accountSection).getByRole('button', { name: 'Update phone number' })).toBeInTheDocument(); - expect(screen.queryByRole('region', { name: 'Email' })).not.toBeInTheDocument(); - expect(screen.queryByRole('region', { name: 'Phone' })).not.toBeInTheDocument(); + expect(screen.queryByRole('group', { name: 'Email' })).not.toBeInTheDocument(); + expect(screen.queryByRole('group', { name: 'Phone' })).not.toBeInTheDocument(); }); it('forwards inline contact update and add actions', async () => { @@ -273,7 +277,7 @@ describe('UserProfileProfilePanelView', () => { it('renders an actionable empty state when no phone number exists', () => { renderView({ phones: [], onSendPhoneCode: () => Promise.resolve(), onVerifyPhoneCode: () => Promise.resolve() }); - const phoneSection = screen.getByRole('region', { name: 'Phone' }); + const phoneSection = screen.getByRole('group', { name: 'Phone' }); const emptyState = within(phoneSection).getByText('No phone numbers added'); expect(emptyState.closest('.cl-section-items')).not.toBeNull(); @@ -352,7 +356,7 @@ describe('UserProfileProfilePanelView', () => { expect(screen.getByRole('heading', { level: 3, name: 'Web3 wallets' })).toBeInTheDocument(); expect(screen.getByText('MetaMask')).toBeInTheDocument(); expect(screen.getByText('0x1234...5678')).toBeInTheDocument(); - expect(within(screen.getByRole('region', { name: 'Web3 wallets' })).getByText('Primary')).toBeInTheDocument(); + expect(within(screen.getByRole('group', { name: 'Web3 wallets' })).getByText('Primary')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Connect Coinbase Wallet' })).toBeVisible(); expect(screen.getByRole('button', { name: 'Manage Coinbase Wallet' })).toBeVisible(); diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-security-panel.view.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-security-panel.view.test.tsx index f1bab06b2aa..052f8b22877 100644 --- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-security-panel.view.test.tsx +++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-security-panel.view.test.tsx @@ -61,16 +61,20 @@ describe('UserProfileSecurityPanelView', () => { renderView({ onDeleteAccount: vi.fn(() => Promise.resolve()) }); expect(screen.getByRole('heading', { level: 2, name: 'Security' })).toBeInTheDocument(); - expect(screen.getByRole('heading', { level: 3, name: 'Authentication' })).toBeInTheDocument(); + const authentication = screen.getByRole('region', { name: 'Authentication' }); + expect(screen.queryByRole('heading', { name: 'Authentication' })).not.toBeInTheDocument(); expect(screen.getByRole('heading', { level: 3, name: 'Active devices' })).toBeInTheDocument(); expect(screen.getByRole('heading', { level: 3, name: 'Danger zone' })).toBeInTheDocument(); - expect(screen.getByText('Password')).toBeVisible(); - expect(screen.getByText('Passkeys')).toBeVisible(); - expect(screen.getByText('2-step verification')).toBeVisible(); - expect(screen.getByRole('region', { name: 'Passkeys' })).toBeInTheDocument(); - expect(screen.getByRole('region', { name: '2-step verification' })).toBeInTheDocument(); + expect(within(authentication).getByRole('heading', { level: 3, name: 'Password' })).toBeInTheDocument(); + expect(within(authentication).getByRole('group', { name: 'Password' })).toBeInTheDocument(); + expect(within(authentication).getByRole('heading', { level: 3, name: 'Passkeys' })).toBeInTheDocument(); + expect(within(authentication).getByRole('heading', { level: 3, name: '2-step verification' })).toBeInTheDocument(); + expect(within(authentication).getByRole('group', { name: 'Passkeys' })).toBeInTheDocument(); + expect(within(authentication).getByRole('group', { name: '2-step verification' })).toBeInTheDocument(); + const activeDevices = screen.getByRole('group', { name: 'Active devices' }); + expect(within(activeDevices).getAllByRole('listitem')).toHaveLength(3); + expect(screen.queryByRole('button', { name: 'Sign out of all devices' })).not.toBeInTheDocument(); expect(screen.getByText('This device')).toBeInTheDocument(); - expect(screen.getByText('2 other devices')).toBeInTheDocument(); expect( screen.getByText('Permanently delete this account and all its data. This cannot be undone.'), ).toBeInTheDocument(); @@ -115,7 +119,12 @@ describe('UserProfileSecurityPanelView', () => { }); await user.click(screen.getByRole('button', { name: 'Add passkey' })); - await user.click(screen.getByRole('button', { name: 'Sign out of all devices' })); + const signOutAll = screen.getByRole('button', { name: 'Sign out of all devices' }); + expect(signOutAll).toHaveAttribute('data-variant', 'outline'); + expect(screen.getByRole('group', { name: 'Active devices' }).querySelector('.cl-section-header')).toContainElement( + signOutAll, + ); + await user.click(signOutAll); await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Sign out' })); await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()); @@ -131,8 +140,8 @@ 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('region', { name: 'Other devices' }); - await user.click(within(otherDevices).getByRole('button', { name: 'Manage Safari on iOS' })); + const activeDevices = screen.getByRole('group', { name: 'Active devices' }); + await user.click(within(activeDevices).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()); @@ -179,7 +188,7 @@ describe('UserProfileSecurityPanelView', () => { expect(screen.queryByRole('menuitem', { name: 'Sign out' })).not.toBeInTheDocument(); }); - it('keeps the authentication heading on MFA when existing passkeys are hidden', () => { + it('keeps the authentication section on MFA when existing passkeys are hidden', () => { renderView({ hasPassword: false, passkeysVisible: false, @@ -191,19 +200,18 @@ describe('UserProfileSecurityPanelView', () => { expect(screen.queryByText('Passkeys')).not.toBeInTheDocument(); expect(screen.queryByText('Passkey')).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Add passkey' })).not.toBeInTheDocument(); - expect(screen.getByRole('heading', { name: 'Authentication' })).toBeVisible(); - expect(screen.getByText('2-step verification')).toBeVisible(); + expect(screen.getByRole('region', { name: 'Authentication' })).toBeVisible(); + expect(screen.getByRole('heading', { level: 3, name: '2-step verification' })).toBeVisible(); }); - it('keeps one authentication heading when passkeys are empty and Add is unavailable', () => { + it('keeps the passkeys card in the authentication section when passkeys are empty and Add is unavailable', () => { renderView({ hasPassword: false, passkeys: [], onAddPasskey: undefined }); const section = screen.getByRole('region', { name: 'Authentication' }); - expect(within(section).getByText('Passkeys')).toBeVisible(); + expect(within(section).getByRole('heading', { level: 3, name: 'Passkeys' })).toBeVisible(); expect(within(section).getByText('No passkeys added')).toBeVisible(); - expect(screen.getByRole('heading', { name: 'Authentication' })).toBeVisible(); expect(screen.queryByRole('button', { name: 'Add passkey' })).not.toBeInTheDocument(); - expect(screen.getByRole('region', { name: '2-step verification' })).toBeVisible(); + expect(screen.getByRole('group', { name: '2-step verification' })).toBeVisible(); }); it('keeps the empty section and final passkey confirmation mounted without Add', async () => { @@ -226,7 +234,7 @@ describe('UserProfileSecurityPanelView', () => { /> , ); - expect(screen.getByText('Authentication')).toBeInTheDocument(); + expect(screen.getByRole('region', { name: 'Authentication' })).toBeInTheDocument(); expect(screen.getByRole('alertdialog', { name: 'Remove passkey' })).toBeVisible(); await act(async () => { @@ -234,8 +242,8 @@ describe('UserProfileSecurityPanelView', () => { await removal.promise; }); await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()); - expect(screen.getByRole('heading', { name: 'Authentication' })).toBeVisible(); - expect(screen.getByText('Passkeys')).toBeVisible(); + expect(screen.getByRole('region', { name: 'Authentication' })).toBeVisible(); + expect(screen.getByRole('heading', { name: 'Passkeys' })).toBeVisible(); expect(screen.getByText('No passkeys added')).toBeVisible(); expect(screen.queryByRole('button', { name: 'Add passkey' })).not.toBeInTheDocument(); }); diff --git a/packages/mosaic/src/features/user-profile/user-profile-account-section/user-profile-account-section.styles.ts b/packages/mosaic/src/features/user-profile/user-profile-account-section/user-profile-account-section.styles.ts deleted file mode 100644 index 73d555e346a..00000000000 --- a/packages/mosaic/src/features/user-profile/user-profile-account-section/user-profile-account-section.styles.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as stylex from '@stylexjs/stylex'; - -import { space } from '../../../tokens.stylex'; - -export const styles = stylex.create({ - sections: { - gap: space['8'], - display: 'flex', - flexDirection: 'column', - width: '100%', - }, -}); diff --git a/packages/mosaic/src/features/user-profile/user-profile-account-section/user-profile-account-section.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-account-section/user-profile-account-section.view.tsx index 890216a2425..70683936c20 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-account-section/user-profile-account-section.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-account-section/user-profile-account-section.view.tsx @@ -1,9 +1,6 @@ -import * as stylex from '@stylexjs/stylex'; - import { Section } from '../../../components/section'; import { useMessages } from '../../../localization'; import type { FileRejection } from '../../../primitives/file-upload'; -import { styles } from './user-profile-account-section.styles'; import type { UserProfileEmail, UserProfileNameAttribute, @@ -115,10 +112,12 @@ export function UserProfileAccountSectionView({ ); return ( -
- - {m.sectionTitle} - + + + + {m.sectionTitle} + + {!allowMultipleAccounts ? emailRow : null} {!allowMultipleAccounts ? phoneRow : null} - - - {allowMultipleAccounts ? ( - - {emailRow} - - ) : null} - {allowMultipleAccounts ? ( - - {phoneRow} - - ) : null} -
+ + + {allowMultipleAccounts ? emailRow : null} + {allowMultipleAccounts ? phoneRow : null} + ); } diff --git a/packages/mosaic/src/features/user-profile/user-profile-account-section/user-profile-contact-list-row.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-account-section/user-profile-contact-list-row.view.tsx index d21c5abd97b..61fbe70ee0e 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-account-section/user-profile-contact-list-row.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-account-section/user-profile-contact-list-row.view.tsx @@ -20,6 +20,7 @@ export interface UserProfileContactListRowViewProps { onVerify?: (id: string) => void; onSetPrimary?: (id: string) => void; onRemove?: (id: string) => void; + children?: ReactNode; } export function UserProfileContactListRowView({ @@ -33,21 +34,18 @@ export function UserProfileContactListRowView({ addAction, rowRef, triggerRef, + children, }: UserProfileContactListRowViewProps) { const m = useMessages('userProfileAccountSection'); const emptyDescription = m[kind].empty; return ( - - - - {label} - + + {label} {addAction ? ( {addAction} ) : onAdd ? ( @@ -68,57 +66,60 @@ export function UserProfileContactListRowView({ ) : null} - - - {items.length === 0 ? ( - - - {emptyDescription} - - - ) : ( - items.map(item => { - const actions: ActionMenuAction[] = []; + + + + {items.length === 0 ? ( + + + {emptyDescription} + + + ) : ( + items.map(item => { + const actions: ActionMenuAction[] = []; - if (item.isVerified === false && onVerify) { - actions.push({ - label: item.isDefault ? m.completeVerification : m[kind].verify, - onClick: () => onVerify(item.id), - }); - } else if (!item.isDefault && item.isVerified === true && onSetPrimary) { - actions.push({ label: m.setPrimary, onClick: () => onSetPrimary(item.id) }); - } + if (item.isVerified === false && onVerify) { + actions.push({ + label: item.isDefault ? m.completeVerification : m[kind].verify, + onClick: () => onVerify(item.id), + }); + } else if (!item.isDefault && item.isVerified === true && onSetPrimary) { + actions.push({ label: m.setPrimary, onClick: () => onSetPrimary(item.id) }); + } - if (onRemove && item.canRemove !== false) { - actions.push({ - label: m[kind].remove, - color: 'negative', - onClick: () => onRemove(item.id), - }); - } + if (onRemove && item.canRemove !== false) { + actions.push({ + label: m[kind].remove, + color: 'negative', + onClick: () => onRemove(item.id), + }); + } - return ( - - - - {item.value} - {item.isDefault ? {m.primary} : null} - - - {actions.length > 0 ? ( - - - - ) : null} - - ); - }) - )} - - + return ( + + + + {item.value} + {item.isDefault ? {m.primary} : null} + + + {actions.length > 0 ? ( + + + + ) : null} + + ); + }) + )} + + {children} + + ); } diff --git a/packages/mosaic/src/features/user-profile/user-profile-account-section/user-profile-email-row.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-account-section/user-profile-email-row.view.tsx index 471492e5a03..11a4674b6b8 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-account-section/user-profile-email-row.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-account-section/user-profile-email-row.view.tsx @@ -121,15 +121,16 @@ export function UserProfileEmailRowView({ onRemove={onRemoveEmail ? removeEmail : undefined} onSetPrimary={onSetPrimaryEmail && !isSettingPrimary ? id => void setPrimaryEmail(id) : undefined} onVerify={onVerifyEmail} - /> - {primaryError ? ( - - {primaryError} - - ) : null} + > + {primaryError ? ( + + {primaryError} + + ) : null} + {onRemoveEmail ? ( void setPrimaryPhone(id) : undefined} onVerify={onVerifyPhone} - /> - {primaryError ? ( - - {primaryError} - - ) : null} + > + {primaryError ? ( + + {primaryError} + + ) : null} + {onRemovePhone ? ( { const signedOut = signedOutAll.current; signedOutAll.current = false; @@ -85,70 +85,54 @@ export function UserProfileActiveDevicesSectionView({ return (
- {m.title} - {currentDevices.length > 0 ? ( - currentDevices.map(device => ( - + + {m.title} + {onSignOutAllOtherDevices && otherDevices.length > 0 ? ( + + + + ) : null} + + + + {currentDevices.length > 0 ? ( + currentDevices.map(device => ( + deviceDetails.open(device)} + /> + )) + ) : ( + + + {m.emptyCurrent} + + + )} + {otherDevices.map(device => ( deviceDetails.open(device)} /> - - )) - ) : ( - - - - {m.emptyCurrent} - - - - )} + ))} + + - {otherDevices.length > 0 ? ( - - - - - - - {fill(otherDevices.length === 1 ? m.otherDevice : m.otherDevices, { - count: String(otherDevices.length), - })} - - - {onSignOutAllOtherDevices ? ( - - - - ) : null} - - - {otherDevices.map(device => ( - deviceDetails.open(device)} - /> - ))} - - - - - ) : null} {onSignOutAllOtherDevices ? ( - History -
- {/* TODO: Replace this inline implementation with the Mosaic Table component. */} -
- - - - - - - - - - {items.length > 0 ? ( - items.map(item => ( - + + + History + + + {/* TODO: Replace this inline implementation with the Mosaic Table component. */} +
+
- Invoice - - Amount - - Status - -
+ + + - - - - - )) - ) : ( - - - )} - -
- -
{item.dateLabel}
-
{item.invoiceLabel}
-
{item.amountLabel} - {item.statusLabel} - - {onView ? ( - - ) : null} -
+ - No invoices yet - -
-
- {pagination ? ( - // TODO: Replace this inline implementation with the Mosaic Pagination component. -
-
- - - -
- + Status + + + + + + {items.length > 0 ? ( + items.map(item => ( + + +
{item.dateLabel}
+
{item.invoiceLabel}
+ + {item.amountLabel} + + {item.statusLabel} + + + {onView ? ( + + ) : null} + + + )) + ) : ( + + + No invoices yet + + + )} + +
- ) : null} -
+ {pagination ? ( + // TODO: Replace this inline implementation with the Mosaic Pagination component. +
+
+ + + +
+ +
+ ) : null} + + ); } diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section.view.tsx index 23214faddc8..273fb46c62d 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section.view.tsx @@ -39,7 +39,7 @@ export function UserProfileConnectedAccountsSectionView({ onRemove, }: UserProfileConnectedAccountsSectionViewProps) { const m = useMessages('userProfileConnectedAccounts'); - const section = useRef(null); + const section = useRef(null); const removalFocus = useListRemovalFocus({ ids: accounts.map(account => account.id), onRemove, @@ -55,30 +55,34 @@ export function UserProfileConnectedAccountsSectionView({ return ( <> {hasRows ? ( - - {m.title} - - {accounts.map(account => ( - removeAccount.open(account) : undefined} - /> - ))} - {onConnect - ? availableProviders.map(provider => ( - - )) - : null} + + + + {m.title} + + + {accounts.map(account => ( + removeAccount.open(account) : undefined} + /> + ))} + {onConnect + ? availableProviders.map(provider => ( + + )) + : null} + ) : null} diff --git a/packages/mosaic/src/features/user-profile/user-profile-delete-section/user-profile-delete-section.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-delete-section/user-profile-delete-section.view.tsx index e79c0ca791c..0922220d9a8 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-delete-section/user-profile-delete-section.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-delete-section/user-profile-delete-section.view.tsx @@ -20,40 +20,44 @@ export function UserProfileDeleteSectionView({ onDelete }: UserProfileDeleteSect return ( - {m.sectionTitle} - - - - {m.sectionLabel} - {m.sectionDescription} - - - - {m.actionLabel} - - } - title={m.dialogTitle} - description={m.dialogDescription} - fieldLabel={fill(m.fieldLabel, { phrase: m.fieldPlaceholder })} - confirmationValue={m.fieldPlaceholder} - actionLabel={m.actionLabel} - cancelLabel={m.cancelLabel} - onDelete={onConfirm} - isDeleting={isDeleting} - errorMessage={errorMessage} - /> - - - + + {m.sectionTitle} + + + + + + {m.sectionLabel} + {m.sectionDescription} + + + + {m.actionLabel} + + } + title={m.dialogTitle} + description={m.dialogDescription} + fieldLabel={fill(m.fieldLabel, { phrase: m.fieldPlaceholder })} + confirmationValue={m.fieldPlaceholder} + actionLabel={m.actionLabel} + cancelLabel={m.cancelLabel} + onDelete={onConfirm} + isDeleting={isDeleting} + errorMessage={errorMessage} + /> + + + + ); diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.view.tsx index c6321e37602..35ab56040f0 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.view.tsx @@ -25,25 +25,29 @@ export function UserProfileEnterpriseAccountsSectionView({ } return ( - {m.title} - {accounts.map(account => ( - - ))} - {onConnect - ? connections.map(connection => ( - - )) - : null} + + {m.title} + + + {accounts.map(account => ( + + ))} + {onConnect + ? connections.map(connection => ( + + )) + : null} + ); diff --git a/packages/mosaic/src/features/user-profile/user-profile-mfa-section.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-mfa-section.view.tsx index 1a5270fe6ce..e1cf0982664 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-mfa-section.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-mfa-section.view.tsx @@ -26,7 +26,6 @@ export interface UserProfileMfaSectionViewProps { addableMethods?: readonly UserProfileMfaAddableMethod[]; addButtonRef?: Ref; addControl?: ReactNode; - sectionTitle?: string; onAdd?: (type: UserProfileMfaAddableMethod) => void; onRegenerateBackupCodes?: () => void; onRemove?: (id: string) => void | Promise; @@ -38,14 +37,13 @@ export function UserProfileMfaSectionView({ addableMethods, addButtonRef, addControl, - sectionTitle, onAdd, onRegenerateBackupCodes, onRemove, onSetDefault, }: UserProfileMfaSectionViewProps) { const m = useMessages('userProfileMfa'); - const section = useRef(null); + const section = useRef(null); const removalFocus = useListRemovalFocus({ ids: methods.map(method => method.id), onRemove, @@ -107,7 +105,6 @@ export function UserProfileMfaSectionView({ emptyLabel={m.empty} hasItems={methods.length > 0} label={m.label} - sectionTitle={sectionTitle} > {methods.map(method => ( void; addError?: string; onRename?: (id: string, name: string) => void | Promise; @@ -29,14 +28,13 @@ export interface UserProfilePasskeysSectionViewProps { export function UserProfilePasskeysSectionView({ passkeys, - sectionTitle, onAdd, addError, onRename, onRemove, }: UserProfilePasskeysSectionViewProps) { const addButton = useRef(null); - const section = useRef(null); + const section = useRef(null); const removalFocus = useListRemovalFocus({ ids: passkeys.map(passkey => passkey.id), onRemove, @@ -46,68 +44,60 @@ export function UserProfilePasskeysSectionView({ return ( <> - - {sectionTitle ? {sectionTitle} : null} - - - - - {m.label} - - {onAdd ? ( - - - - ) : null} - - - {addError} - + + {m.label} + {onAdd ? ( + + + + ) : null} + + {addError} + + + + {passkeys.length > 0 ? ( - - {passkeys.map(passkey => ( - removePasskey.open(passkey) : undefined} - /> - ))} - + passkeys.map(passkey => ( + removePasskey.open(passkey) : undefined} + /> + )) ) : ( - - - - {m.empty} - - - + + + {m.empty} + + )} - - - + + + {onRemove ? ( ) { +}: UserProfilePasswordSectionViewProps) { const m = useMessages('userProfilePasswordSection'); return ( - + - {m.label} {hasPassword ? m.masked : m.noPasswordSet} {managedBy ? ( @@ -42,7 +41,7 @@ export function UserProfilePasswordRowView({ ) : null} - + ); } diff --git a/packages/mosaic/src/features/user-profile/user-profile-password-section/user-profile-password-section.messages.ts b/packages/mosaic/src/features/user-profile/user-profile-password-section/user-profile-password-section.messages.ts index bd01cb2de1f..ebf73640adf 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-password-section/user-profile-password-section.messages.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-password-section/user-profile-password-section.messages.ts @@ -1,5 +1,4 @@ export const userProfilePasswordSectionMessages = { - sectionTitle: 'Authentication', label: 'Password', masked: '••••••••••••••••••', noPasswordSet: 'No password set', diff --git a/packages/mosaic/src/features/user-profile/user-profile-password-section/user-profile-password-section.types.ts b/packages/mosaic/src/features/user-profile/user-profile-password-section/user-profile-password-section.types.ts index 386c278c2d8..44fc4da20c6 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-password-section/user-profile-password-section.types.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-password-section/user-profile-password-section.types.ts @@ -18,7 +18,6 @@ export interface UserProfilePasswordManagedBy { } export interface UserProfilePasswordSectionViewProps { - sectionTitle?: string; hasPassword?: boolean; requiresCurrentPassword?: boolean; /** Replaces the edit action with the enterprise provider’s name. */ diff --git a/packages/mosaic/src/features/user-profile/user-profile-password-section/user-profile-password-section.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-password-section/user-profile-password-section.view.tsx index 3e1261091f2..f941a18e8f4 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-password-section/user-profile-password-section.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-password-section/user-profile-password-section.view.tsx @@ -12,29 +12,29 @@ export type { } from './user-profile-password-section.types'; export function UserProfilePasswordSectionView({ - sectionTitle, hasPassword = false, requiresCurrentPassword = false, managedBy, onSubmitPassword, }: UserProfilePasswordSectionViewProps) { const m = useMessages('userProfilePasswordSection'); - const title = sectionTitle ?? m.sectionTitle; if (!hasPassword && !managedBy && !onSubmitPassword) { return null; } return ( - - {title ? {title} : null} - + + + {m.label} + + - - + + ); } diff --git a/packages/mosaic/src/features/user-profile/user-profile-payment-methods-section.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-payment-methods-section.view.tsx index 1a58aa7fd1f..0b2dd39a425 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-payment-methods-section.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-payment-methods-section.view.tsx @@ -28,32 +28,30 @@ export function UserProfilePaymentMethodsSectionView({ onRemove, }: UserProfilePaymentMethodsSectionViewProps) { return ( - + - - - - Payment methods - - {onAdd ? ( - - - - ) : null} - + /> + Add + + + ) : null} + + {paymentMethods.length > 0 ? ( paymentMethods.map(paymentMethod => ( @@ -72,7 +70,7 @@ export function UserProfilePaymentMethodsSectionView({ )} - + ); diff --git a/packages/mosaic/src/features/user-profile/user-profile-security-list.tsx b/packages/mosaic/src/features/user-profile/user-profile-security-list.tsx index e8a2ee18d70..b0f831ee975 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-security-list.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-security-list.tsx @@ -5,7 +5,6 @@ import { Icon } from '../../components/icon'; import { Section } from '../../components/section'; export function UserProfileSecurityList({ - sectionTitle, sectionRef, label, addLabel, @@ -15,8 +14,7 @@ export function UserProfileSecurityList({ addControl, children, }: { - sectionTitle?: string; - sectionRef?: Ref; + sectionRef?: Ref; label: string; addLabel: string; emptyLabel: string; @@ -26,52 +24,46 @@ export function UserProfileSecurityList({ children: ReactNode; }) { return ( - - {sectionTitle ? {sectionTitle} : null} - - - - - {label} - - {addControl ? ( - {addControl} - ) : onAdd ? ( - - - - ) : null} - - - {hasItems ? ( - children - ) : ( - - - {emptyLabel} - - - )} - - - - + + {label} + {addControl ? ( + {addControl} + ) : onAdd ? ( + + + + ) : null} + + + + {hasItems ? ( + children + ) : ( + + + {emptyLabel} + + + )} + + + ); } diff --git a/packages/mosaic/src/features/user-profile/user-profile-security-panel.styles.ts b/packages/mosaic/src/features/user-profile/user-profile-security-panel.styles.ts index 67454eca28a..ab7f24daae3 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-security-panel.styles.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-security-panel.styles.ts @@ -27,7 +27,4 @@ export const styles = stylex.create({ display: 'flex', flexDirection: 'column', }, - emptySectionCards: { - display: 'none', - }, }); diff --git a/packages/mosaic/src/features/user-profile/user-profile-security-panel.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-security-panel.view.tsx index 0062d803e29..95dafc3cdfa 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-security-panel.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-security-panel.view.tsx @@ -1,7 +1,7 @@ -import * as stylex from '@stylexjs/stylex'; import type { ReactElement, ReactNode } from 'react'; import { Panel } from '../../components/panel'; +import { Section } from '../../components/section'; import { themeProps } from '../../props'; import type { UserProfileActiveDevicesSectionViewProps, @@ -19,7 +19,6 @@ import type { UserProfilePasswordSectionViewProps, } from './user-profile-password-section/user-profile-password-section.view'; import { UserProfilePasswordSectionView } from './user-profile-password-section/user-profile-password-section.view'; -import { styles } from './user-profile-security-panel.styles'; export type { UserProfileDevice, @@ -86,38 +85,38 @@ export function UserProfileSecurityPanelView({ }> Security -
- {showPassword ? ( - - ) : null} - {showPasskeys ? ( - - ) : null} - {mfaMethods !== undefined ? ( - - ) : null} -
+ {hasAuthentication ? ( + + {showPassword ? ( + + ) : null} + {showPasskeys ? ( + + ) : null} + {mfaMethods !== undefined ? ( + + ) : null} + + ) : null} {devices ? ( - Subscription - - - - {subscription.planName} - {subscription.priceLabel} - - {onChangePlan ? ( + + Subscription + + + + + + {subscription.planName} + {subscription.priceLabel} + + {onChangePlan ? ( + + + + ) : null} + + + + + + Total due + {subscription.renewsAtLabel} + - + {subscription.totalDueLabel} - ) : null} - - - - - - Total due - {subscription.renewsAtLabel} - - - {subscription.totalDueLabel} - - - + + +
); diff --git a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section.view.tsx index f4cd5ad851b..427f30c8e65 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section.view.tsx @@ -43,7 +43,7 @@ export function UserProfileWeb3WalletsSectionView({ onRemove, }: UserProfileWeb3WalletsSectionViewProps) { const m = useMessages('userProfileWeb3Wallets'); - const section = useRef(null); + const section = useRef(null); const removalFocus = useListRemovalFocus({ ids: wallets.map(wallet => wallet.id), onRemove, @@ -59,30 +59,34 @@ export function UserProfileWeb3WalletsSectionView({ return ( <> {hasRows ? ( - - {m.title} - - {wallets.map(wallet => ( - removeWallet.open(wallet) : undefined} - /> - ))} - {onConnect - ? availableProviders.map(provider => ( - - )) - : null} + + + + {m.title} + + + {wallets.map(wallet => ( + removeWallet.open(wallet) : undefined} + /> + ))} + {onConnect + ? availableProviders.map(provider => ( + + )) + : null} + ) : null} diff --git a/packages/swingset/src/lib/exampleSnippet.ts b/packages/swingset/src/lib/exampleSnippet.ts index 446e57c27af..8a9fd7f8440 100644 --- a/packages/swingset/src/lib/exampleSnippet.ts +++ b/packages/swingset/src/lib/exampleSnippet.ts @@ -41,9 +41,20 @@ function unwrapReturn(fnSource: string): string { return expr; } - // `return ;` — a single expression statement; take up to its terminating semicolon. - const semicolon = expr.indexOf(';'); - return semicolon === -1 ? expr : expr.slice(0, semicolon); + // `return ;` — the return is the function's last statement, so its terminating semicolon + // is the last one; an earlier one can sit inside the JSX (an inline ` + + + + Profile + + + + + + Name + Preston Booth + + + + + + + + + + Username + Prestonb.xyz + + + + + + + + + {[ + { label: 'Email', values: ['item1@clerk.dev', 'item2@clerk.dev'] }, + { label: 'Phone', values: ['+1 801-888-8181'] }, + ].map(({ label, values }) => ( + + + {label} + + + + + + + {values.map((value, index) => ( + + + + {value} + {index === 0 ? Primary : null} + + + + + + + ))} + + + + ))} + + + ); +} diff --git a/packages/swingset/src/stories/user-profile-passkeys-section.mdx b/packages/swingset/src/stories/user-profile-passkeys-section.mdx index b36689c2f9c..40f74188b38 100644 --- a/packages/swingset/src/stories/user-profile-passkeys-section.mdx +++ b/packages/swingset/src/stories/user-profile-passkeys-section.mdx @@ -14,14 +14,13 @@ import * as Stories from './user-profile-passkeys-section.stories'; ## Props -| Prop | Type | Default | Description | -| -------------- | ----------------------------------------------------- | ------------ | ---------------------------------------------------------------- | -| `passkeys` | `UserProfilePasskey[]` | — (required) | Names and formatted creation/last-used labels. | -| `sectionTitle` | `string` | — | Optional surrounding section heading. | -| `onAdd` | `() => void` | — | Shows Add when supplied. | -| `addError` | `string` | — | Inline creation error. | -| `onRename` | `(id: string, name: string) => void \| Promise` | — | Resolve to close; reject with an Error to display the failure. | -| `onRemove` | `(id: string) => void \| Promise` | — | Shared confirmation awaits removal and displays rejected errors. | +| Prop | Type | Default | Description | +| ---------- | ----------------------------------------------------- | ------------ | ---------------------------------------------------------------- | +| `passkeys` | `UserProfilePasskey[]` | — (required) | Names and formatted creation/last-used labels. | +| `onAdd` | `() => void` | — | Shows Add when supplied. | +| `addError` | `string` | — | Inline creation error. | +| `onRename` | `(id: string, name: string) => void \| Promise` | — | Resolve to close; reject with an Error to display the failure. | +| `onRemove` | `(id: string) => void \| Promise` | — | Shared confirmation awaits removal and displays rejected errors. | ## Usage diff --git a/packages/swingset/src/stories/user-profile-passkeys-section.stories.tsx b/packages/swingset/src/stories/user-profile-passkeys-section.stories.tsx index f57448d1b14..9ea16aa95c0 100644 --- a/packages/swingset/src/stories/user-profile-passkeys-section.stories.tsx +++ b/packages/swingset/src/stories/user-profile-passkeys-section.stories.tsx @@ -18,12 +18,7 @@ export const meta: StoryMeta = { function PasskeysExample({ empty = false, failOnce = false }: { empty?: boolean; failOnce?: boolean }): ReactElement { const fixture = usePasskeysFixture({ empty, failOnce }); - return ( - - ); + return ; } export function Default(): ReactElement { @@ -39,7 +34,6 @@ export function CreationUnavailable(): ReactElement { return ( );