Skip to content

Commit 92cdc6a

Browse files
feat(mosaic): rebuild styled Table on the Table primitive (#10421)
1 parent f3d75f1 commit 92cdc6a

40 files changed

Lines changed: 1290 additions & 2916 deletions
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

‎packages/mosaic/AGENTS.md‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ Most problems a feature hits already have a shared answer. Find the row for your
5151
| Current time for display labels | `useNow` (`src/hooks/`) |
5252
| Whether enterprise SSO permits adding another identification | `allowsIdentificationCreation` (`src/utils/allows-identification-creation.ts`) |
5353
| An inline message row that animates open and closed | `useHeldMessage`, `useMessageHeight`, `FeedbackBody` (`src/utils/feedback.tsx`) + `src/styles/feedback.styles.ts` |
54-
| A table whose sorting, paging and search are done by the server | `useServerDataTable` (`src/hooks/`) |
5554
| Data read from a Clerk resource that should be cached across mounts | A query hook in `src/queries/`, called from the model (`src/queries/use-user-sessions-query.ts`) |
5655
| 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` |
5756
| Siblings that slide when an item is added or removed | `useLayoutAnimation` (`src/primitives/hooks/`) |
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"files": [
33
{ "path": "./dist/index.js", "maxSize": "95KB" },
4-
{ "path": "./dist/styles.css", "maxSize": "16KB" }
4+
{ "path": "./dist/styles.css", "maxSize": "17KB" }
55
]
66
}

‎packages/mosaic/src/components/table/index.ts‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,16 @@ export type {
33
TableAlign,
44
TableBodyProps,
55
TableCellProps,
6-
TableColumnGroupProps,
76
TableColumnProps,
8-
TableEmptyProps,
9-
TableHeaderCellProps,
7+
TableExpandButtonProps,
108
TableHeaderProps,
119
TableLayout,
12-
TableProps,
10+
TableRootProps,
1311
TableRowProps,
1412
TableSearchProps,
15-
TableSelectAllCellProps,
16-
TableSelectCellProps,
13+
TableSelectionCellProps,
14+
TableSelectionColumnProps,
15+
TableSelectionMode,
1716
TableSort,
1817
TableSticky,
1918
TableToolbarProps,

‎packages/mosaic/src/components/table/table.feature.test.tsx‎

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,20 @@ function renderNarrowTable(minWidth: string) {
1313
layout='fixed'
1414
minWidth={minWidth}
1515
>
16-
<Table.ColumnGroup>
17-
<Table.Column />
18-
<Table.Column width='9rem' />
19-
</Table.ColumnGroup>
2016
<Table.Header>
21-
<Table.Row>
22-
<Table.HeaderCell>Name</Table.HeaderCell>
23-
<Table.HeaderCell>Created</Table.HeaderCell>
24-
</Table.Row>
17+
<Table.Column>Name</Table.Column>
18+
<Table.Column width='9rem'>Created</Table.Column>
2519
</Table.Header>
26-
<Table.Body>
27-
<Table.Row>
28-
<Table.Cell>Alpha</Table.Cell>
29-
<Table.Cell>Today</Table.Cell>
30-
</Table.Row>
20+
<Table.Body
21+
items={['alpha']}
22+
itemToStringValue={item => item}
23+
>
24+
{() => (
25+
<Table.Row>
26+
<Table.Cell>Alpha</Table.Cell>
27+
<Table.Cell>Today</Table.Cell>
28+
</Table.Row>
29+
)}
3130
</Table.Body>
3231
</Table.Root>
3332
</div>
@@ -47,3 +46,78 @@ describe('Mosaic Table', () => {
4746
expect(nameColumnWidth()).toBe(176);
4847
});
4948
});
49+
50+
describe('Mosaic Table expand button', () => {
51+
it.each([
52+
['ltr', 'collapsed', 'none'],
53+
['ltr', 'expanded', 'matrix(0, 1, -1, 0, 0, 0)'],
54+
['rtl', 'collapsed', 'matrix(-1, 0, 0, 1, 0, 0)'],
55+
['rtl', 'expanded', 'matrix(0, 1, -1, 0, 0, 0)'],
56+
] as const)('points the %s chevron for a %s row', (dir, state, transform) => {
57+
render(
58+
<MosaicProvider>
59+
<div dir={dir}>
60+
<Table.Root
61+
aria-label='Tree'
62+
defaultExpanded={state === 'expanded' ? ['parent'] : []}
63+
>
64+
<Table.Header>
65+
<Table.Column>Name</Table.Column>
66+
</Table.Header>
67+
<Table.Body
68+
items={[{ id: 'parent', children: [{ id: 'child', children: [] }] }]}
69+
itemToStringValue={item => item.id}
70+
itemToChildren={item => item.children}
71+
>
72+
{item => (
73+
<Table.Row expandable={item.children.length > 0}>
74+
<Table.Cell>
75+
{item.children.length > 0 && <Table.ExpandButton aria-label={`Expand ${item.id}`} />}
76+
{item.id}
77+
</Table.Cell>
78+
</Table.Row>
79+
)}
80+
</Table.Body>
81+
</Table.Root>
82+
</div>
83+
</MosaicProvider>,
84+
);
85+
const icon = screen.getByRole('button', { name: 'Expand parent' }).querySelector('svg');
86+
expect(icon).not.toBeNull();
87+
expect(icon && getComputedStyle(icon).transform).toBe(transform);
88+
});
89+
});
90+
91+
describe('Mosaic Table sticky fade', () => {
92+
it.each([
93+
['ltr', 'start', 'to right'],
94+
['ltr', 'end', 'to left'],
95+
['rtl', 'start', 'to left'],
96+
['rtl', 'end', 'to right'],
97+
] as const)('fades %s %s-pinned cells away from the pinned edge', (dir, side, gradient) => {
98+
render(
99+
<MosaicProvider>
100+
<div dir={dir}>
101+
<Table.Root aria-label='Keys'>
102+
<Table.Header>
103+
<Table.Column>Name</Table.Column>
104+
</Table.Header>
105+
<Table.Body
106+
items={['alpha']}
107+
itemToStringValue={item => item}
108+
>
109+
{() => (
110+
<Table.Row>
111+
<Table.Cell sticky={side}>Alpha</Table.Cell>
112+
</Table.Row>
113+
)}
114+
</Table.Body>
115+
</Table.Root>
116+
</div>
117+
</MosaicProvider>,
118+
);
119+
expect(getComputedStyle(screen.getByRole('cell', { name: 'Alpha' }), '::before').backgroundImage).toContain(
120+
`linear-gradient(${gradient}`,
121+
);
122+
});
123+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import * as stylex from '@stylexjs/stylex';
2+
3+
export const tableStateMarker = stylex.defineMarker();

‎packages/mosaic/src/components/table/table.styles.ts‎

Lines changed: 87 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import * as stylex from '@stylexjs/stylex';
22

3-
import { colorVars, fontWeightVars, radiusVars, scrollFadeVars, space, typeScaleVars } from '../../tokens.stylex';
3+
import {
4+
colorVars,
5+
durationVars,
6+
focusVars,
7+
fontWeightVars,
8+
radiusVars,
9+
scrollFadeVars,
10+
space,
11+
typeScaleVars,
12+
} from '../../tokens.stylex';
13+
import { tableStateMarker } from './table.markers.stylex';
414

515
const surface = 'var(--_cl-table-surface)';
616
const tint = 'var(--_cl-table-row-tint)';
@@ -45,50 +55,60 @@ export const styles = stylex.create({
4555
'--_cl-table-surface': colorVars['--cl-color-background-subtle'],
4656
backgroundColor: colorVars['--cl-color-background-subtle'],
4757
},
48-
bodyRow: {
49-
borderBlockStartColor: colorVars['--cl-color-border'],
50-
borderBlockStartStyle: 'solid',
51-
borderBlockStartWidth: '1px',
52-
},
53-
interactiveRow: {
58+
row: {
5459
'--_cl-table-row-tint': {
5560
default: null,
56-
'@media (hover: hover)': { ':hover': colorVars['--cl-color-background-subtle'] },
61+
':where([data-selected])': colorVars['--cl-color-neutral-alpha-100'],
62+
'@media (hover: hover)': {
63+
default: null,
64+
':where([data-actionable]:not([data-selected]):hover)': colorVars['--cl-color-background-subtle'],
65+
},
5766
},
5867
backgroundColor: {
5968
default: null,
60-
'@media (hover: hover)': { ':hover': colorVars['--cl-color-background-subtle'] },
69+
':where([data-selected])': colorVars['--cl-color-neutral-alpha-100'],
70+
'@media (hover: hover)': {
71+
default: null,
72+
':where([data-actionable]:not([data-selected]):hover)': colorVars['--cl-color-background-subtle'],
73+
},
6174
},
62-
cursor: 'pointer',
63-
},
64-
selectedRow: {
65-
'--_cl-table-row-tint': colorVars['--cl-color-neutral-alpha-100'],
66-
backgroundColor: colorVars['--cl-color-neutral-alpha-100'],
75+
borderBlockStartColor: colorVars['--cl-color-border'],
76+
borderBlockStartStyle: 'solid',
77+
borderBlockStartWidth: '1px',
78+
cursor: { default: null, ':where([data-actionable])': 'pointer' },
79+
position: 'relative',
6780
},
68-
headerCell: {
81+
column: {
6982
paddingBlock: space['2.5'],
7083
paddingInline: space['4'],
7184
color: colorVars['--cl-color-foreground-secondary'],
7285
fontSize: typeScaleVars['--cl-text-xs-size'],
7386
fontWeight: fontWeightVars['--cl-font-medium'],
7487
lineHeight: typeScaleVars['--cl-text-xs-leading'],
88+
position: 'relative',
7589
verticalAlign: 'middle',
7690
whiteSpace: 'nowrap',
7791
},
78-
sortableHeaderCell: {
79-
paddingBlock: space['1.5'],
80-
paddingInline: space['2'],
92+
sortableColumn: {
93+
color: {
94+
default: colorVars['--cl-color-foreground-secondary'],
95+
'@media (hover: hover)': { default: null, ':hover': colorVars['--cl-color-foreground'] },
96+
},
97+
cursor: 'pointer',
98+
userSelect: 'none',
8199
},
82-
sortButton: {
83-
color: 'inherit',
84-
fontWeight: 'inherit',
100+
sortLabel: {
101+
gap: space['1'],
102+
alignItems: 'center',
103+
display: 'inline-flex',
85104
},
86105
cell: {
87106
paddingBlock: space['3'],
88107
paddingInline: space['4'],
89108
color: colorVars['--cl-color-foreground'],
90109
fontSize: typeScaleVars['--cl-text-sm-size'],
91110
lineHeight: typeScaleVars['--cl-text-sm-leading'],
111+
position: 'relative',
92112
verticalAlign: 'middle',
93113
},
94114
selectCell: {
@@ -103,7 +123,7 @@ export const styles = stylex.create({
103123
backgroundColor: surface,
104124
backgroundImage: `linear-gradient(${tint}, ${tint})`,
105125
position: 'sticky',
106-
zIndex: 1,
126+
zIndex: { default: 1, ':focus-visible': 2 },
107127
'::before': {
108128
insetBlock: 0,
109129
content: '""',
@@ -112,9 +132,53 @@ export const styles = stylex.create({
112132
width: scrollFadeVars['--cl-scroll-fade-size'],
113133
},
114134
},
115-
emptyCell: {
135+
focusRing: {
136+
outlineOffset: { default: null, ':focus-visible': `calc(-1 * ${focusVars['--cl-focus-outline-width']})` },
137+
zIndex: { default: null, ':focus-visible': 2 },
138+
},
139+
empty: {
116140
textAlign: 'center',
117141
},
142+
expandButton: {
143+
borderRadius: radiusVars['--cl-radius-sm'],
144+
alignItems: 'center',
145+
color: colorVars['--cl-color-foreground-secondary'],
146+
cursor: 'pointer',
147+
display: 'inline-flex',
148+
justifyContent: 'center',
149+
marginInlineEnd: space['1'],
150+
verticalAlign: 'middle',
151+
},
152+
expandIcon: {
153+
transform: {
154+
default: null,
155+
':is([dir="rtl"] *):not([data-expanded] *)': 'scaleX(-1)',
156+
[stylex.when.ancestor('[data-expanded]', tableStateMarker)]: 'rotate(90deg)',
157+
},
158+
transitionDuration: durationVars['--cl-duration-fast'],
159+
transitionProperty: 'transform',
160+
},
161+
});
162+
163+
export const sortIcons = stylex.create({
164+
none: {
165+
display: {
166+
default: 'block',
167+
[stylex.when.ancestor('[data-sort-direction]', tableStateMarker)]: 'none',
168+
},
169+
},
170+
ascending: {
171+
display: {
172+
default: 'none',
173+
[stylex.when.ancestor('[data-sort-direction="ascending"]', tableStateMarker)]: 'block',
174+
},
175+
},
176+
descending: {
177+
display: {
178+
default: 'none',
179+
[stylex.when.ancestor('[data-sort-direction="descending"]', tableStateMarker)]: 'block',
180+
},
181+
},
118182
});
119183

120184
export const layouts = stylex.create({

0 commit comments

Comments
 (0)