Skip to content
Open
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
2 changes: 2 additions & 0 deletions .changeset/mosaic-styled-table-primitive.md
Comment thread
alexcarpenter marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
1 change: 0 additions & 1 deletion packages/mosaic/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ Most problems a feature hits already have a shared answer. Find the row for your
| Current time for display labels | `useNow` + `MosaicProvider` ([#10098](https://github.com/clerk/javascript/pull/10098), once available) |
| Whether enterprise SSO permits adding another identification | `allowsIdentificationCreation` (`src/utils/allows-identification-creation.ts`) |
| An inline message row that animates open and closed | `useHeldMessage`, `useMessageHeight`, `FeedbackBody` (`src/utils/feedback.tsx`) + `styles/feedback.styles.ts` |
| A table whose sorting, paging and search are done by the server | `useServerDataTable` (`src/hooks/`) |
| Data read from a Clerk resource that should be cached across mounts | A query hook in `src/queries/`, called from the model (`use-user-sessions-query.ts`) |
| An item changed in place in an infinite list, kept until the next reload | `replaceInPages` (`src/utils/infinite-pages.ts`), passed to the list's `setData` |
| Siblings that slide when an item is added or removed | `useLayoutAnimation` (`src/primitives/hooks/`) |
Expand Down
2 changes: 1 addition & 1 deletion packages/mosaic/bundlewatch.config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"files": [
{ "path": "./dist/index.js", "maxSize": "95KB" },
{ "path": "./dist/styles.css", "maxSize": "16KB" }
{ "path": "./dist/styles.css", "maxSize": "17KB" }
]
}
11 changes: 5 additions & 6 deletions packages/mosaic/src/components/table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ export type {
TableAlign,
TableBodyProps,
TableCellProps,
TableColumnGroupProps,
TableColumnProps,
TableEmptyProps,
TableHeaderCellProps,
TableExpandButtonProps,
TableHeaderProps,
TableLayout,
TableProps,
TableRootProps,
TableRowProps,
TableSearchProps,
TableSelectAllCellProps,
TableSelectCellProps,
TableSelectionCellProps,
TableSelectionColumnProps,
TableSelectionMode,
TableSort,
TableSticky,
TableToolbarProps,
Expand Down
100 changes: 87 additions & 13 deletions packages/mosaic/src/components/table/table.feature.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,20 @@ function renderNarrowTable(minWidth: string) {
layout='fixed'
minWidth={minWidth}
>
<Table.ColumnGroup>
<Table.Column />
<Table.Column width='9rem' />
</Table.ColumnGroup>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Created</Table.HeaderCell>
</Table.Row>
<Table.Column>Name</Table.Column>
<Table.Column width='9rem'>Created</Table.Column>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>Alpha</Table.Cell>
<Table.Cell>Today</Table.Cell>
</Table.Row>
<Table.Body
items={['alpha']}
itemToStringValue={item => item}
>
{() => (
<Table.Row>
<Table.Cell>Alpha</Table.Cell>
<Table.Cell>Today</Table.Cell>
</Table.Row>
)}
</Table.Body>
</Table.Root>
</div>
Expand All @@ -47,3 +46,78 @@ describe('Mosaic Table', () => {
expect(nameColumnWidth()).toBe(176);
});
});

describe('Mosaic Table expand button', () => {
it.each([
['ltr', 'collapsed', 'none'],
['ltr', 'expanded', 'matrix(0, 1, -1, 0, 0, 0)'],
['rtl', 'collapsed', 'matrix(-1, 0, 0, 1, 0, 0)'],
['rtl', 'expanded', 'matrix(0, 1, -1, 0, 0, 0)'],
] as const)('points the %s chevron for a %s row', (dir, state, transform) => {
render(
<MosaicProvider>
<div dir={dir}>
<Table.Root
aria-label='Tree'
defaultExpanded={state === 'expanded' ? ['parent'] : []}
>
<Table.Header>
<Table.Column>Name</Table.Column>
</Table.Header>
<Table.Body
items={[{ id: 'parent', children: [{ id: 'child', children: [] }] }]}
itemToStringValue={item => item.id}
itemToChildren={item => item.children}
>
{item => (
<Table.Row expandable={item.children.length > 0}>
<Table.Cell>
{item.children.length > 0 && <Table.ExpandButton aria-label={`Expand ${item.id}`} />}
{item.id}
</Table.Cell>
</Table.Row>
)}
</Table.Body>
</Table.Root>
</div>
</MosaicProvider>,
);
const icon = screen.getByRole('button', { name: 'Expand parent' }).querySelector('svg');
expect(icon).not.toBeNull();
expect(icon && getComputedStyle(icon).transform).toBe(transform);
});
});

describe('Mosaic Table sticky fade', () => {
it.each([
['ltr', 'start', 'to right'],
['ltr', 'end', 'to left'],
['rtl', 'start', 'to left'],
['rtl', 'end', 'to right'],
] as const)('fades %s %s-pinned cells away from the pinned edge', (dir, side, gradient) => {
render(
<MosaicProvider>
<div dir={dir}>
<Table.Root aria-label='Keys'>
<Table.Header>
<Table.Column>Name</Table.Column>
</Table.Header>
<Table.Body
items={['alpha']}
itemToStringValue={item => item}
>
{() => (
<Table.Row>
<Table.Cell sticky={side}>Alpha</Table.Cell>
</Table.Row>
)}
</Table.Body>
</Table.Root>
</div>
</MosaicProvider>,
);
expect(getComputedStyle(screen.getByRole('cell', { name: 'Alpha' }), '::before').backgroundImage).toContain(
`linear-gradient(${gradient}`,
);
});
});
3 changes: 3 additions & 0 deletions packages/mosaic/src/components/table/table.markers.stylex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as stylex from '@stylexjs/stylex';

export const tableStateMarker = stylex.defineMarker();
110 changes: 87 additions & 23 deletions packages/mosaic/src/components/table/table.styles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import * as stylex from '@stylexjs/stylex';

import { colorVars, fontWeightVars, radiusVars, scrollFadeVars, space, typeScaleVars } from '../../tokens.stylex';
import {
colorVars,
durationVars,
focusVars,
fontWeightVars,
radiusVars,
scrollFadeVars,
space,
typeScaleVars,
} from '../../tokens.stylex';
import { tableStateMarker } from './table.markers.stylex';

const surface = 'var(--_cl-table-surface)';
const tint = 'var(--_cl-table-row-tint)';
Expand Down Expand Up @@ -45,50 +55,60 @@ export const styles = stylex.create({
'--_cl-table-surface': colorVars['--cl-color-background-subtle'],
backgroundColor: colorVars['--cl-color-background-subtle'],
},
bodyRow: {
borderBlockStartColor: colorVars['--cl-color-border'],
borderBlockStartStyle: 'solid',
borderBlockStartWidth: '1px',
},
interactiveRow: {
row: {
'--_cl-table-row-tint': {
default: null,
'@media (hover: hover)': { ':hover': colorVars['--cl-color-background-subtle'] },
':where([data-selected])': colorVars['--cl-color-neutral-alpha-100'],
'@media (hover: hover)': {
default: null,
':where([data-actionable]:not([data-selected]):hover)': colorVars['--cl-color-background-subtle'],
},
},
backgroundColor: {
default: null,
'@media (hover: hover)': { ':hover': colorVars['--cl-color-background-subtle'] },
':where([data-selected])': colorVars['--cl-color-neutral-alpha-100'],
'@media (hover: hover)': {
default: null,
':where([data-actionable]:not([data-selected]):hover)': colorVars['--cl-color-background-subtle'],
},
},
cursor: 'pointer',
},
selectedRow: {
'--_cl-table-row-tint': colorVars['--cl-color-neutral-alpha-100'],
backgroundColor: colorVars['--cl-color-neutral-alpha-100'],
borderBlockStartColor: colorVars['--cl-color-border'],
borderBlockStartStyle: 'solid',
borderBlockStartWidth: '1px',
cursor: { default: null, ':where([data-actionable])': 'pointer' },
position: 'relative',
},
headerCell: {
column: {
paddingBlock: space['2.5'],
paddingInline: space['4'],
color: colorVars['--cl-color-foreground-secondary'],
fontSize: typeScaleVars['--cl-text-xs-size'],
fontWeight: fontWeightVars['--cl-font-medium'],
lineHeight: typeScaleVars['--cl-text-xs-leading'],
position: 'relative',
verticalAlign: 'middle',
whiteSpace: 'nowrap',
},
sortableHeaderCell: {
paddingBlock: space['1.5'],
paddingInline: space['2'],
sortableColumn: {
color: {
default: colorVars['--cl-color-foreground-secondary'],
'@media (hover: hover)': { default: null, ':hover': colorVars['--cl-color-foreground'] },
},
cursor: 'pointer',
userSelect: 'none',
},
sortButton: {
color: 'inherit',
fontWeight: 'inherit',
sortLabel: {
gap: space['1'],
alignItems: 'center',
display: 'inline-flex',
},
cell: {
paddingBlock: space['3'],
paddingInline: space['4'],
color: colorVars['--cl-color-foreground'],
fontSize: typeScaleVars['--cl-text-sm-size'],
lineHeight: typeScaleVars['--cl-text-sm-leading'],
position: 'relative',
verticalAlign: 'middle',
},
selectCell: {
Expand All @@ -103,7 +123,7 @@ export const styles = stylex.create({
backgroundColor: surface,
backgroundImage: `linear-gradient(${tint}, ${tint})`,
position: 'sticky',
zIndex: 1,
zIndex: { default: 1, ':focus-visible': 2 },
'::before': {
insetBlock: 0,
content: '""',
Expand All @@ -112,9 +132,53 @@ export const styles = stylex.create({
width: scrollFadeVars['--cl-scroll-fade-size'],
},
},
emptyCell: {
focusRing: {
outlineOffset: { default: null, ':focus-visible': `calc(-1 * ${focusVars['--cl-focus-outline-width']})` },
zIndex: { default: null, ':focus-visible': 2 },
},
empty: {
textAlign: 'center',
},
expandButton: {
borderRadius: radiusVars['--cl-radius-sm'],
alignItems: 'center',
color: colorVars['--cl-color-foreground-secondary'],
cursor: 'pointer',
display: 'inline-flex',
justifyContent: 'center',
marginInlineEnd: space['1'],
verticalAlign: 'middle',
},
expandIcon: {
transform: {
default: null,
':is([dir="rtl"] *):not([data-expanded] *)': 'scaleX(-1)',
[stylex.when.ancestor('[data-expanded]', tableStateMarker)]: 'rotate(90deg)',
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
transitionDuration: durationVars['--cl-duration-fast'],
transitionProperty: 'transform',
},
});

export const sortIcons = stylex.create({
none: {
display: {
default: 'block',
[stylex.when.ancestor('[data-sort-direction]', tableStateMarker)]: 'none',
},
},
ascending: {
display: {
default: 'none',
[stylex.when.ancestor('[data-sort-direction="ascending"]', tableStateMarker)]: 'block',
},
},
descending: {
display: {
default: 'none',
[stylex.when.ancestor('[data-sort-direction="descending"]', tableStateMarker)]: 'block',
},
},
});

export const layouts = stylex.create({
Expand Down
Loading
Loading