Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/edge-reveal-reversal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] With an `animatedLayout` handle, a Collapsible or a TreeList row closed while it opens, or opened again while it closes, continues from what it shows: the content's edge stays on the content after it on every frame, instead of jumping back to fully shown or hidden. Under Reduce Motion with a touch pointer, where the content cross-fades instead, a reversal still starts its fade over.

@vjeux
34 changes: 33 additions & 1 deletion apps/storybook/stories/AnimatedLayout.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {Meta, StoryObj} from '@storybook/react';
import {ScrollableArea} from '@astryxdesign/core/ScrollableArea';
import {TreeList} from '@astryxdesign/core/TreeList';
import type {TreeListItemData} from '@astryxdesign/core/TreeList';
import {Collapsible} from '@astryxdesign/core/Collapsible';
import {Collapsible, CollapsibleGroup} from '@astryxdesign/core/Collapsible';
import {useAnimatedLayout} from '@astryxdesign/core/hooks';

const meta: Meta = {
Expand Down Expand Up @@ -143,6 +143,38 @@ function Demo() {
New
</span>
</div>
<Section title="A CollapsibleGroup, one open at a time, of unequal heights">
<div style={card}>
<CollapsibleGroup type="single">
<Collapsible
trigger="Return policy"
value="returns"
animatedLayout={animatedLayout}>
<p style={{margin: '4px 0'}}>
Return any item within 30 days.
</p>
<p style={{margin: '4px 0'}}>Items must be unused.</p>
<p style={{margin: '4px 0'}}>Refunds take 5 business days.</p>
<p style={{margin: '4px 0'}}>Exchanges ship free.</p>
<p style={{margin: '4px 0'}}>
Gift returns get store credit.
</p>
</Collapsible>
<Collapsible
trigger="Gift options"
value="gifts"
animatedLayout={animatedLayout}>
<p style={{margin: '4px 0'}}>Add a note at checkout.</p>
</Collapsible>
<Collapsible
trigger="Delivery windows"
value="delivery"
animatedLayout={animatedLayout}>
<p style={{margin: '4px 0'}}>Choose a two-hour window.</p>
</Collapsible>
</CollapsibleGroup>
</div>
</Section>
<div style={{display: 'contents'}}>
<div style={plain}>Inside a display: contents wrapper (1)</div>
<div style={plain}>Inside a display: contents wrapper (2)</div>
Expand Down
25 changes: 25 additions & 0 deletions packages/core/src/SideNav/SideNavRailMotion.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

import {readFileSync} from 'node:fs';
import {join} from 'node:path';
import {act, fireEvent, render, screen} from '@testing-library/react';
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';

Expand Down Expand Up @@ -610,6 +612,29 @@ describe('SideNav rail motion (component:SideNav FR6, component:AppShell FR17)',
expect((copy as HTMLElement).style.width).toBe(`${EXPANDED}px`);
});

it("plays the animated-layout module's two-state edge reveal on its inline edge, keeping only its labels' and icons' fades (spec:AST-105 FR9)", () => {
const source = readFileSync(join(__dirname, 'railMotion.ts'), 'utf8')
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/\/\/.*$/gm, '');
// The module holds the old rail, clips its inline-end edge and rides the
// content's glide (architecture:react-component-runtime INV12).
expect(source).toMatch(/\bholdLayoutEdge\(/);
expect(source).toMatch(/\bplayLayoutEdge\(/);
expect(source).toMatch(/edge: 'inline-end'/);
// No clip, copy or ride of the rail's own.
for (const own of [
'clipPath',
'inset(',
'holdLayoutSwap',
'playLayoutSwap',
'playLayoutFrames',
'zIndex',
'getBoundingClientRect',
]) {
expect(source, own).not.toContain(own);
}
});

it('changes at once without Web Animations', async () => {
delete (HTMLElement.prototype as unknown as {animate?: unknown}).animate;
render(<Shell />);
Expand Down
Loading
Loading