Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
5b0abfb
feat(core): useAnimatedLayout, one module that glides the layout shif…
cixzhang Oct 26, 2026
b476220
feat(core): useAnimatedLayout({containerRef}) returns a handle that C…
cixzhang Oct 26, 2026
52c08dc
fix(core): the layout glide corrects only a real ancestor scale, not …
cixzhang Oct 26, 2026
7cfe5e0
chore(storybook): the Animated layout story's tags row is narrow, so …
cixzhang Oct 26, 2026
e2afc61
Merge 8ba53b45a4 (main, AST-105 current) into the animated layout draft
cixzhang Oct 26, 2026
dc023f3
wip(core): animated layout follows AST-105 (springs.settle, Reduce Mo…
cixzhang Oct 26, 2026
b60f171
feat(core): useAnimatedLayout follows AST-105
cixzhang Oct 26, 2026
ac58ce3
chore(storybook): the story's internal-reporting demos say they are s…
cixzhang Oct 26, 2026
427f850
chore(docsite): the ShadCN route lock lists the useAnimatedLayout hoo…
cixzhang Oct 26, 2026
1bccf04
fix(core): the animated layout reads springs.settle from the internal…
cixzhang Oct 26, 2026
b894315
test(core): the animated-layout browser proofs settle between changes…
cixzhang Oct 26, 2026
ea01d0c
fix(core): animated layout measures every grouped change, drops refus…
cixzhang Oct 26, 2026
186526e
fix(core): a retargeted glide keeps its motion and adds the new move
cixzhang Oct 26, 2026
5bcb797
docs(architecture): INV12 plays the glide through the shared rectangl…
cixzhang Oct 26, 2026
7bad709
docs(architecture): rewrap INV12
cixzhang Oct 26, 2026
3a5f9ae
refactor(core): useAnimatedLayout lands alone; Collapsible and TreeLi…
cixzhang Oct 26, 2026
7e13143
feat(core): Collapsible and TreeList adopt useAnimatedLayout
cixzhang Oct 26, 2026
b3826b7
feat(core): the changed element's exit, held out of flow on the same …
cixzhang Oct 26, 2026
f8c2db7
Merge the module's exit (feat/animated-layout) into the adoption
cixzhang Oct 26, 2026
044b865
feat(core): a Collapsible's or TreeList row's close plays the content…
cixzhang Oct 26, 2026
52863fc
test(core): the exiting group's rows leave the tree's keyboard order
cixzhang Oct 26, 2026
1c44863
chore(changeset): Collapsible and TreeList adopt animatedLayout
cixzhang Oct 26, 2026
a09ce74
fix(core): the internal module alone; a displaced changed element mov…
cixzhang Oct 26, 2026
980ff13
Merge feat/animated-layout into the adoption; the public hook and the…
cixzhang Oct 26, 2026
2f7cedb
fix(core): the exit frees exactly its space and is inert; elements th…
cixzhang Oct 26, 2026
f97811a
Merge feat/animated-layout (an exact, inert exit; app-moved followers…
cixzhang Oct 26, 2026
52841d7
fix(core): the exit measures the content after it, holds only a measu…
cixzhang Oct 26, 2026
86a458b
Merge feat/animated-layout (the exit measures the content after it an…
cixzhang Oct 26, 2026
bc32d66
fix(core): nested changes compose in any order; the exit finds the re…
cixzhang Oct 26, 2026
ac812d7
Merge feat/animated-layout (nested changes in any order; the exit's n…
cixzhang Oct 26, 2026
e4b19b0
fix(core): the exit's correction follows a scaled ancestor, and its m…
cixzhang Oct 26, 2026
731af79
Merge feat/animated-layout (scale-corrected exit; measured out of flo…
cixzhang Oct 26, 2026
c08d59b
fix(core): Collapsible and TreeList report every change, whatever cau…
cixzhang Oct 26, 2026
7c96972
test(core): the adopters' browser spec serves Storybook through a11y-…
cixzhang Oct 26, 2026
7ca7a04
fix(core): a TreeList row collapsing with an expanded child moves wha…
cixzhang Oct 26, 2026
5695f69
docs(core): Collapsible FR10 and TreeList FR8 cite AST-108 FR4a for t…
cixzhang Oct 26, 2026
d2a56cd
feat(core): the animated-layout walk moves the siblings before a chan…
cixzhang Oct 27, 2026
e43e328
perf(core): a held exit reads where the content after it goes once, w…
cixzhang Oct 27, 2026
24eaa87
perf(core): the glide reads every follower before it starts any, so a…
cixzhang Oct 27, 2026
8338da9
Merge main (#8482 landed as 7a530cc) into the adopters
cixzhang Oct 27, 2026
3ea5336
fix(core): the animated-layout walk glides a moved ancestor, reaches …
cixzhang Oct 27, 2026
3a9db0d
Merge the adopters (#8568 8338da94: main with #8482 landed)
cixzhang Oct 27, 2026
aaa7e68
Merge #8634 3a9db0dc (a moved ancestor glides; a frame's changes add …
cixzhang Oct 27, 2026
685ad78
fix(core): a pinned table column draws its own row dividers
cixzhang Oct 27, 2026
cc491f7
fix(core): the pinned divider's plumbing stays private
cixzhang Oct 27, 2026
660be13
Merge #8654 (pinned dividers) as a dependency
cixzhang Oct 27, 2026
be5b92f
feat(core): Table rows glide when a row's panel opens or closes, and …
cixzhang Oct 27, 2026
cbd36ef
perf(core): the Table row glide reads the layout once, with the glide…
cixzhang Oct 27, 2026
a0e510b
fix(core): an open detail panel carries its row's divider, themed or …
cixzhang Oct 27, 2026
d7cc624
test(Table): the panel divider tests read the carried width's token d…
cixzhang Oct 27, 2026
f23d42b
fix(core): the pinned divider's read runs on mount and resize only; i…
cixzhang Oct 27, 2026
0fb8037
fix(core): the gliding row's divider is drawn by one of its cells, no…
cixzhang Oct 27, 2026
67a023d
Merge #8654 (f23d42b3)
cixzhang Oct 27, 2026
8256e56
Stack base for #8656: #8639 (aaa7e686) with #8654 (f23d42b3)
cixzhang Oct 27, 2026
88917e6
fix(core): pinned dividers and edge shadows follow each commit, not o…
cixzhang Oct 27, 2026
5b90dbc
Merge #8654 (88917e61)
cixzhang Oct 27, 2026
2f2b8ce
Stack base for #8656: #8654 (88917e61) merged in
cixzhang Oct 27, 2026
82c240b
fix(core): the pinned divider draws the cell's own border in CSS and …
cixzhang Oct 27, 2026
6de98fb
Merge #8654 (82c240b5); fix(core): the panel divider and nested table…
cixzhang Oct 27, 2026
5b5f74d
Merge stack/table-row-glide-base
cixzhang Oct 27, 2026
bd36240
Stack base for #8656: #8654 (82c240b5) merged in
cixzhang Oct 27, 2026
ed2c0bd
fix(core): the pinned divider reads across the header edge and CSS ro…
cixzhang Oct 27, 2026
124dee3
Merge #8654 (ed2c0bd9)
cixzhang Oct 27, 2026
57ab35f
fix(core): the glide outranks theme and app cell colours, keeps the l…
cixzhang Oct 27, 2026
82ec5f6
Merge stack/table-row-glide-base
cixzhang Oct 27, 2026
3142902
Stack base for #8656: #8654 (ed2c0bd9) merged in
cixzhang Oct 27, 2026
5cf3af1
docs(Table): the grid note in useTableRowExpansion's doc is valid Jav…
cixzhang Oct 27, 2026
8fbfe64
fix(core): a table with column lines (dividers "columns" too) changes…
cixzhang Oct 27, 2026
aeb209a
fix(core): pinned dividers follow a colour-scheme switch; a panel car…
cixzhang Oct 27, 2026
60a01e8
fix(core): the closing hold adds to the wrapper's own margin; a panel…
cixzhang Oct 27, 2026
5c58a25
Merge #8654 (aeb209a8)
cixzhang Oct 27, 2026
fa509b9
Stack base for #8656: #8654 (aeb209a8) merged in
cixzhang Oct 27, 2026
44e8919
Merge stack/table-row-glide-base
cixzhang Oct 27, 2026
c8c46bc
fix(core): pinned and panel dividers follow a Theme mode switch too; …
cixzhang Oct 27, 2026
60dc4b8
Merge #8654 (c8c46bc6); fix(core): the glide finds only this table's …
cixzhang Oct 27, 2026
dd12cb0
Merge stack/table-row-glide-base
cixzhang Oct 27, 2026
c34f48f
Stack base for #8656: #8654 (c8c46bc6) merged in
cixzhang Oct 27, 2026
35944bc
fix(core): pinned dividers follow a Theme switched between the hooks'…
cixzhang Oct 27, 2026
5cdda33
fix(core): a chevron drops the glide's spring when its table turns to…
cixzhang Oct 27, 2026
c5066d5
Merge #8654 (35944bca)
cixzhang Oct 27, 2026
c17bdbb
Merge stack/table-row-glide-base
cixzhang Oct 27, 2026
bc32c1b
Stack base for #8656: #8654 (35944bca) merged in
cixzhang Oct 27, 2026
3d2d64c
fix(core): the scroll container attaches once with column resize comp…
cixzhang Oct 27, 2026
40f08df
test(Table): the long table's no-handle glide check waits long enough…
cixzhang Oct 27, 2026
ce4f3a0
Stack base for #8656: #8654 (3d2d64c3) merged in
cixzhang Oct 27, 2026
1fd99a1
Merge stack/table-row-glide-base (#8654 3d2d64c3)
cixzhang Oct 27, 2026
007f414
docs(spec): Collapsible FR10 and TreeList FR8 cite AST-105 for the sh…
cixzhang Oct 27, 2026
36c6fd8
Merge main: TreeList's animatedLayout row is FR9 beside main's FR8; a…
cixzhang Oct 27, 2026
7e54a7d
Merge #8568 (36c6fd87, as it landed)
cixzhang Oct 27, 2026
e45ef26
Merge main (f8e0c928376: #8568 landed as f8e0c92)
cixzhang Oct 27, 2026
6ab36a8
Merge #8634 (e45ef263: main through #8568 merged)
cixzhang Oct 27, 2026
bc5c7ac
Stack base for #8656: #8639 (6ab36a87, main through #8654 and #8568) …
cixzhang Oct 27, 2026
324bd5f
Merge stack/table-row-glide-base (#8639 6ab36a87, with #8654 as landed)
cixzhang Oct 27, 2026
202f3da
Merge main (8f42b3d2601: #8639 and #8634 landed)
cixzhang Oct 28, 2026
6b1bb99
fix(core): an app's border on a gliding row itself moves with the row…
cixzhang Oct 28, 2026
3ecdc48
fix(core): a glide hides every edge the divider can come from; a row'…
cixzhang Oct 28, 2026
eeacd26
fix(core): a gliding row's line sits where its divider rests at any w…
cixzhang Oct 28, 2026
b7aaa63
Merge main (0f3d2f49d20)
cixzhang Oct 28, 2026
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
7 changes: 7 additions & 0 deletions .changeset/table-row-expansion-glide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[feat] Table: `useTableRowExpansion` takes an `animatedLayout` handle from `useAnimatedLayout`. With it, opening or closing a row's panel glides the rows after it to their new places instead of jumping, and each row's divider moves with its row. Under Reduce Motion the rows fade in place on a touch screen and change at once with a mouse; in a table with column lines (`dividers="columns"` or `"grid"`) they change at once.

@cixzhang
127 changes: 126 additions & 1 deletion apps/storybook/stories/TableRowExpansion.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

import {useState} from 'react';
import React, {useMemo, useRef, useState} from 'react';
import type {Meta, StoryObj} from '@storybook/react';
import {
Table,
useTableRowExpansion,
useTableStickyColumns,
pixel,
proportional,
} from '@astryxdesign/core/Table';
import {useAnimatedLayout} from '@astryxdesign/core/hooks';
import type {TableColumn} from '@astryxdesign/core/Table';
import {Theme} from '@astryxdesign/core/theme';
import {neutralTheme} from '@astryxdesign/theme-neutral';
Expand Down Expand Up @@ -205,3 +207,126 @@ export const NotAllRowsExpandable: Story = {
);
},
};

const manyOrders: Order[] = Array.from({length: 2000}, (_, index) => ({
...orders[index % orders.length],
id: `ord-${String(index + 1).padStart(4, '0')}`,
}));
const sevenOrders = manyOrders.slice(0, 7);

/**
* With an `animatedLayout` handle, opening or closing a row's panel glides the
* rows after it instead of jumping them, the panel plays in on the same
* motion, and each row's divider moves with its row. The Customer column is
* pinned and the table scrolls horizontally, so the pinned cells' own
* dividers move too. Under Reduce Motion, and with column lines (the
* `dividers` control at "columns" or "grid"), the rows change at once. The
* `themed` control sets a blue theme border and an app's red border on the
* tbody's third row; `rowBorder` sets an app's 3 px green border on the
* fourth order's row itself (`tr`), which glides with the row too; `edgeBorder`
* sets one on the tbody's bottom (`tbody`) or as the top of the fifth order's
* cells (`top`), each of which wins the collapse and glides with its row;
* `wideBorder` sets a theme's 2 px green divider on every row.
*/
export const GlidingRows: StoryObj<{
themed?: boolean;
rowBorder?: boolean;
wideBorder?: boolean;
edgeBorder?: 'none' | 'tbody' | 'top';
dividers?: 'rows' | 'columns' | 'grid';
}> = {
args: {
themed: false,
rowBorder: false,
wideBorder: false,
edgeBorder: 'none',
dividers: 'rows',
},
render: ({themed, rowBorder, wideBorder, edgeBorder, dividers}) => {
const containerRef = useRef<HTMLDivElement>(null);
const animatedLayout = useAnimatedLayout({containerRef});
const [expandedKeys, setExpandedKeys] = useState<Set<string>>(
() => new Set(),
);
const expansion = useTableRowExpansion<Order>({
expandedKeys,
onToggle: key => setExpandedKeys(prev => toggleKey(prev, key)),
getRowKey: item => item.id,
renderExpanded: item => <OrderItems order={item} />,
animatedLayout,
});
const sticky = useTableStickyColumns<Order>({startKeys: ['customer']});
const wideColumns = useMemo(
() => columns.map(column => ({...column, width: pixel(220)})),
[],
);
const style: React.CSSProperties & Record<string, string | number> = {
maxWidth: 640,
...(themed === true ? {'--color-border': 'rgb(37, 99, 235)'} : {}),
...(wideBorder === true
? {'--color-border': 'rgb(22, 163, 74)', '--border-width': '2px'}
: {}),
};
return (
<div ref={containerRef} data-testid="gliding-rows" style={style}>
{themed && (
<style>{`[data-testid="gliding-rows"] tbody tr:nth-child(3) td { border-bottom-color: rgb(220, 38, 38); }`}</style>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This override reaches td only, so neither this story nor TableRowGlide.a11y.browser.spec.ts exercises FR14's verification case of "an app override of a row's border; both mid-glide and on a pinned cell", which is where the glide currently draws the divider twice. A tr border in the themed state, with an assertion of the moving row's pinned and unpinned line and nothing at its static place, would cover it.

)}
{rowBorder && (
<style>{`[data-testid="gliding-rows"] tr[data-expansion-key="${sevenOrders[3].id}"] { border-bottom: 3px solid rgb(22, 163, 74); }`}</style>
)}
{edgeBorder === 'tbody' && (
<style>{`[data-testid="gliding-rows"] tbody { border-bottom: 3px solid rgb(22, 163, 74); }`}</style>
)}
{edgeBorder === 'top' && (
<style>{`[data-testid="gliding-rows"] tr[data-expansion-key="${sevenOrders[4].id}"] td { border-top: 3px solid rgb(22, 163, 74); }`}</style>
)}
<Table
data={sevenOrders}
columns={wideColumns}
dividers={dividers}
idKey="id"
hasHover
plugins={{expansion, sticky}}
/>
<p>Content after the table glides too.</p>
</div>
);
},
};

/**
* A long table (2,000 rows, not virtualized) with an `animatedLayout`
* handle: a glide moves only the rows near the visible part, so opening a
* panel costs the same as in a short table.
*/
export const GlidingRowsLongTable: StoryObj<{glide?: boolean}> = {
args: {glide: true},
render: ({glide}) => {
const containerRef = useRef<HTMLDivElement>(null);
const animatedLayout = useAnimatedLayout({containerRef});
const [expandedKeys, setExpandedKeys] = useState<Set<string>>(
() => new Set(),
);
const expansion = useTableRowExpansion<Order>({
expandedKeys,
onToggle: key => setExpandedKeys(prev => toggleKey(prev, key)),
getRowKey: item => item.id,
renderExpanded: item => <OrderItems order={item} />,
animatedLayout: glide === false ? undefined : animatedLayout,
});
return (
<div
ref={containerRef}
data-testid="gliding-rows-long"
style={{maxWidth: 720, height: 520, overflow: 'auto'}}>
<Table
data={manyOrders}
columns={columns}
idKey="id"
plugins={{expansion}}
/>
</div>
);
},
};
3 changes: 3 additions & 0 deletions packages/core/src/Table/TableCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
mergeXStyle,
} from './useTableCellStyles';
import {mergeProps} from '../utils';
import {glidingDividerStyles} from './glidingDivider';
import {themeProps} from '../utils/themeProps';

/** Props for TableCell — thin `<td>` wrapper */
Expand Down Expand Up @@ -234,6 +235,8 @@ export function TableCell({
dividerColumnStyles.cell,
),
...(hasContextMenu ? [contextMenuCellStyles.cell] : []),
// While a glide moves the row, its divider moves with it.
glidingDividerStyles.cell,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This covers the bottom border only. In a dividers="grid" table the inline-end borders from dividerColumnStyles are painted at the cells' static place too, so mid-glide the column lines stay at the rows' final places: missing beside the moving rows and drawn through the content after the table.

]
: [];

Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/Table/TableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import * as stylex from '@stylexjs/stylex';
import {colorVars, durationVars, easeVars} from '../theme/tokens.stylex';
import type {StyleXStyles} from '../theme/types';
import {tableRowMarker} from './table.stylex';
import {glidingDividerStyles} from './glidingDivider';
import {TableContext} from './TableContext';
import {mergeProps} from '../utils';
import {themeProps} from '../utils/themeProps';
Expand Down Expand Up @@ -182,6 +183,8 @@ export function TableRow({
if (ctx.dividers === 'rows' || ctx.dividers === 'grid') {
// Note: last-body-row border removal is handled by TableCell
// to avoid affecting the header row in <thead>.
// While a glide moves the row, its divider moves with it.
rowStyles.push(glidingDividerStyles.row);
}

if (xstyle) {
Expand Down
Loading
Loading