@charcuterie/ui 4.26.0 → 4.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/TableViewport/TableViewport.d.ts.map +1 -1
- package/dist/TableViewport/TableViewport.js +62 -10
- package/dist/TableViewport/TableViewport.js.map +1 -1
- package/package.json +1 -1
- package/src/TableViewport/TableViewport.mdx +2 -0
- package/src/TableViewport/TableViewport.test.tsx +89 -1
- package/src/TableViewport/TableViewport.tsx +82 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TableViewport.d.ts","sourceRoot":"","sources":["../../src/TableViewport/TableViewport.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"TableViewport.d.ts","sourceRoot":"","sources":["../../src/TableViewport/TableViewport.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAWrD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,2EAA2E;IAC3E,QAAQ,EAAE,SAAS,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,2FAA2F;AAC3F,eAAO,MAAM,aAAa,GAAI,8CAK3B,kBAAkB,KAAG,SAqIvB,CAAA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useState } from "react";
|
|
2
|
+
import { useCallback, useLayoutEffect, useRef, useState, } from "react";
|
|
3
3
|
import { Button } from "../Button/Button.js";
|
|
4
4
|
import { Dialog } from "../Dialog/Dialog.js";
|
|
5
5
|
import { toClassName } from "../toClassName.js";
|
|
@@ -9,10 +9,69 @@ export const TableViewport = ({ children, className, columnCount, label, }) => {
|
|
|
9
9
|
const [inlineHeight, setInlineHeight] = useState(0);
|
|
10
10
|
const scrollRef = useRef(null);
|
|
11
11
|
const scrollPosition = useRef(0);
|
|
12
|
+
const expansion = useRef(null);
|
|
13
|
+
const isClosing = useRef(false);
|
|
14
|
+
useLayoutEffect(() => {
|
|
15
|
+
// Restore after React has actually returned the table to this pane.
|
|
16
|
+
// A frame scheduled from the animation's promise can run before that commit
|
|
17
|
+
// on a busy renderer, when the empty pane clamps scrollLeft back to zero.
|
|
18
|
+
if (!isExpanded && scrollRef.current)
|
|
19
|
+
scrollRef.current.scrollLeft = scrollPosition.current;
|
|
20
|
+
}, [isExpanded]);
|
|
21
|
+
const animateExpansion = useCallback((node) => {
|
|
22
|
+
const dialog = node?.closest('[role="dialog"]');
|
|
23
|
+
if (!dialog ||
|
|
24
|
+
matchMedia("(prefers-reduced-motion: reduce)")
|
|
25
|
+
.matches)
|
|
26
|
+
return;
|
|
27
|
+
const style = getComputedStyle(dialog);
|
|
28
|
+
const duration = Number.parseFloat(style.getPropertyValue("--duration-slow"));
|
|
29
|
+
// Zero-duration profiles (including ePaper) remove motion entirely.
|
|
30
|
+
if (!(duration > 0))
|
|
31
|
+
return;
|
|
32
|
+
const animation = dialog.animate([
|
|
33
|
+
{
|
|
34
|
+
opacity: 0,
|
|
35
|
+
transform: "translateY(0.75rem) scale(0.98)",
|
|
36
|
+
},
|
|
37
|
+
{ opacity: 1, transform: "none" },
|
|
38
|
+
], {
|
|
39
|
+
duration,
|
|
40
|
+
easing: style
|
|
41
|
+
.getPropertyValue("--easing-standard")
|
|
42
|
+
.trim() || "ease-out",
|
|
43
|
+
fill: "both",
|
|
44
|
+
});
|
|
45
|
+
expansion.current = animation;
|
|
46
|
+
// Unmounting a document can cancel an unfinished enter or exit.
|
|
47
|
+
void animation.finished.catch(() => { });
|
|
48
|
+
return () => {
|
|
49
|
+
animation.cancel();
|
|
50
|
+
expansion.current = null;
|
|
51
|
+
};
|
|
52
|
+
}, []);
|
|
53
|
+
const close = () => {
|
|
54
|
+
if (isClosing.current)
|
|
55
|
+
return;
|
|
56
|
+
isClosing.current = true;
|
|
57
|
+
const finish = () => {
|
|
58
|
+
setIsExpanded(false);
|
|
59
|
+
isClosing.current = false;
|
|
60
|
+
};
|
|
61
|
+
const animation = expansion.current;
|
|
62
|
+
if (!animation ||
|
|
63
|
+
matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
64
|
+
finish();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Reversing also handles Escape during the opening animation without a jump.
|
|
68
|
+
animation.reverse();
|
|
69
|
+
void animation.finished.then(finish, () => { });
|
|
70
|
+
};
|
|
12
71
|
const contentStyle = {
|
|
13
72
|
"--table-min-inline-size": `${Math.max(1, columnCount) * 14}rem`,
|
|
14
73
|
};
|
|
15
|
-
const content = (_jsx("div", { className: "charcuterie-table-content", style: contentStyle, children: children }));
|
|
74
|
+
const content = (_jsx("div", { className: "charcuterie-table-content", ref: isExpanded ? animateExpansion : undefined, style: contentStyle, children: children }));
|
|
16
75
|
return (_jsxs("div", { className: toClassName("min-w-0", className), children: [_jsx("div", { className: "flex items-center justify-end border border-border-subtle bg-surface-raised p-2", children: _jsx(Button, { appearance: "ghost", intent: "neutral", size: "sm", onClick: () => {
|
|
17
76
|
const pane = scrollRef.current;
|
|
18
77
|
setInlineHeight(pane?.getBoundingClientRect().height ?? 0);
|
|
@@ -20,13 +79,6 @@ export const TableViewport = ({ children, className, columnCount, label, }) => {
|
|
|
20
79
|
setIsExpanded(true);
|
|
21
80
|
}, children: "Expand table" }) }), _jsx("section", { "aria-label": label, className: "charcuterie-scrollbar charcuterie-table-scroll border border-border-subtle border-t-0", ref: scrollRef,
|
|
22
81
|
// biome-ignore lint/a11y/noNoninteractiveTabindex: horizontal scroll must be reachable by keyboard
|
|
23
|
-
tabIndex: 0, style: isExpanded ? { height: inlineHeight } : undefined, children: isExpanded ? null : content }), _jsx(Dialog, { heading: label, isVisible: isExpanded, size: "full", onClose:
|
|
24
|
-
setIsExpanded(false);
|
|
25
|
-
requestAnimationFrame(() => {
|
|
26
|
-
if (scrollRef.current)
|
|
27
|
-
scrollRef.current.scrollLeft =
|
|
28
|
-
scrollPosition.current;
|
|
29
|
-
});
|
|
30
|
-
}, children: isExpanded ? content : null })] }));
|
|
82
|
+
tabIndex: 0, style: isExpanded ? { height: inlineHeight } : undefined, children: isExpanded ? null : content }), _jsx(Dialog, { heading: label, isVisible: isExpanded, size: "full", onClose: close, children: isExpanded ? content : null })] }));
|
|
31
83
|
};
|
|
32
84
|
//# sourceMappingURL=TableViewport.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TableViewport.js","sourceRoot":"","sources":["../../src/TableViewport/TableViewport.tsx"],"names":[],"mappings":";AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"TableViewport.js","sourceRoot":"","sources":["../../src/TableViewport/TableViewport.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -10,6 +10,8 @@ its rows and uses the page's vertical scroll. Expand table opens a full-window `
|
|
|
10
10
|
The dialog body is the only scroll surface; headers and the first column stay visible.
|
|
11
11
|
Escape and Close return focus to the trigger. Only one table is mounted, so links and IDs
|
|
12
12
|
are never duplicated. The inline height and horizontal position survive expansion.
|
|
13
|
+
Expansion fades and zooms in; dismissal reverses the animation before returning to the
|
|
14
|
+
document. Reduced-motion and zero-duration profiles skip the animation.
|
|
13
15
|
|
|
14
16
|
Apps provide the semantic table, its column count, and its accessible label. Sanitizing
|
|
15
17
|
Markdown HTML and delegating document links remain the app's responsibilities. Use
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { composeStories } from "@storybook/react"
|
|
2
2
|
import { expect, userEvent, waitFor } from "storybook/test"
|
|
3
|
-
import { test } from "vitest"
|
|
3
|
+
import { test, vi } from "vitest"
|
|
4
4
|
import { expectNoAxeViolations } from "../expectNoAxeViolations.testHelpers.ts"
|
|
5
5
|
import { mountStory } from "../mountStory.testHelpers.ts"
|
|
6
6
|
import { expectAgentDrivable } from "../testing/index.ts"
|
|
@@ -26,6 +26,8 @@ test("long inline comparisons grow with the page and expand without duplicate ta
|
|
|
26
26
|
role: "dialog",
|
|
27
27
|
name: "Printer comparison",
|
|
28
28
|
})
|
|
29
|
+
for (const animation of dialog.getAnimations())
|
|
30
|
+
animation.finish()
|
|
29
31
|
await expect(
|
|
30
32
|
document.querySelectorAll("table").length,
|
|
31
33
|
).toBe(1)
|
|
@@ -56,3 +58,89 @@ test("long inline comparisons grow with the page and expand without duplicate ta
|
|
|
56
58
|
canvas.getByRole("table"),
|
|
57
59
|
).toBeInTheDocument()
|
|
58
60
|
})
|
|
61
|
+
|
|
62
|
+
test("keeps the table and focus trap until the reverse animation completes", async () => {
|
|
63
|
+
const { body, canvas } = await mountStory(Interactive)
|
|
64
|
+
const trigger = canvas.getByRole("button", {
|
|
65
|
+
name: "Expand table",
|
|
66
|
+
})
|
|
67
|
+
const pane = canvas.getByRole("region", {
|
|
68
|
+
name: "Printer comparison",
|
|
69
|
+
})
|
|
70
|
+
pane.scrollLeft = 120
|
|
71
|
+
const scrollLeft = pane.scrollLeft
|
|
72
|
+
const height = pane.getBoundingClientRect().height
|
|
73
|
+
await userEvent.click(trigger)
|
|
74
|
+
const dialog = body.getByRole("dialog", {
|
|
75
|
+
name: "Printer comparison",
|
|
76
|
+
})
|
|
77
|
+
const [animation] = dialog.getAnimations()
|
|
78
|
+
await expect(animation).toBeDefined()
|
|
79
|
+
animation.pause()
|
|
80
|
+
animation.currentTime =
|
|
81
|
+
Number(animation.effect?.getTiming().duration) / 2
|
|
82
|
+
await expect(
|
|
83
|
+
Number(getComputedStyle(dialog).opacity),
|
|
84
|
+
).toBeGreaterThan(0)
|
|
85
|
+
await expect(
|
|
86
|
+
Number(getComputedStyle(dialog).opacity),
|
|
87
|
+
).toBeLessThan(1)
|
|
88
|
+
await expect(pane.getBoundingClientRect().height).toBe(
|
|
89
|
+
height,
|
|
90
|
+
)
|
|
91
|
+
await userEvent.keyboard("{Escape}")
|
|
92
|
+
await animation.ready
|
|
93
|
+
animation.pause()
|
|
94
|
+
await expect(animation.playbackRate).toBe(-1)
|
|
95
|
+
await expect(body.getByRole("dialog")).toBe(dialog)
|
|
96
|
+
await expect(
|
|
97
|
+
document.querySelectorAll("table"),
|
|
98
|
+
).toHaveLength(1)
|
|
99
|
+
// A second dismiss during exit must not reverse back into an opening.
|
|
100
|
+
await userEvent.click(
|
|
101
|
+
body.getByRole("button", { name: "Close" }),
|
|
102
|
+
)
|
|
103
|
+
await expect(animation.playbackRate).toBe(-1)
|
|
104
|
+
animation.finish()
|
|
105
|
+
await waitFor(() =>
|
|
106
|
+
expect(body.queryByRole("dialog")).toBeNull(),
|
|
107
|
+
)
|
|
108
|
+
await waitFor(() =>
|
|
109
|
+
expect(document.activeElement).toBe(trigger),
|
|
110
|
+
)
|
|
111
|
+
await waitFor(() =>
|
|
112
|
+
expect(pane.scrollLeft).toBe(scrollLeft),
|
|
113
|
+
)
|
|
114
|
+
await expect(
|
|
115
|
+
canvas.getByRole("table"),
|
|
116
|
+
).toBeInTheDocument()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test("reduced motion opens and closes immediately without a table animation", async () => {
|
|
120
|
+
const { body, canvas } = await mountStory(Interactive)
|
|
121
|
+
const matchMedia = window.matchMedia.bind(window)
|
|
122
|
+
const media = vi
|
|
123
|
+
.spyOn(window, "matchMedia")
|
|
124
|
+
.mockImplementation((query) =>
|
|
125
|
+
query === "(prefers-reduced-motion: reduce)"
|
|
126
|
+
? { ...matchMedia(query), matches: true }
|
|
127
|
+
: matchMedia(query),
|
|
128
|
+
)
|
|
129
|
+
try {
|
|
130
|
+
await userEvent.click(
|
|
131
|
+
canvas.getByRole("button", { name: "Expand table" }),
|
|
132
|
+
)
|
|
133
|
+
const dialog = body.getByRole("dialog", {
|
|
134
|
+
name: "Printer comparison",
|
|
135
|
+
})
|
|
136
|
+
await expect(dialog.getAnimations()).toHaveLength(0)
|
|
137
|
+
await userEvent.click(
|
|
138
|
+
body.getByRole("button", { name: "Close" }),
|
|
139
|
+
)
|
|
140
|
+
await waitFor(() =>
|
|
141
|
+
expect(body.queryByRole("dialog")).toBeNull(),
|
|
142
|
+
)
|
|
143
|
+
} finally {
|
|
144
|
+
media.mockRestore()
|
|
145
|
+
}
|
|
146
|
+
})
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { CSSProperties, ReactNode } from "react"
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
useCallback,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from "react"
|
|
3
8
|
import { Button } from "../Button/Button.tsx"
|
|
4
9
|
import { Dialog } from "../Dialog/Dialog.tsx"
|
|
5
10
|
import { toClassName } from "../toClassName.ts"
|
|
@@ -23,12 +28,87 @@ export const TableViewport = ({
|
|
|
23
28
|
const [inlineHeight, setInlineHeight] = useState(0)
|
|
24
29
|
const scrollRef = useRef<HTMLElement>(null)
|
|
25
30
|
const scrollPosition = useRef(0)
|
|
31
|
+
const expansion = useRef<Animation | null>(null)
|
|
32
|
+
const isClosing = useRef(false)
|
|
33
|
+
useLayoutEffect(() => {
|
|
34
|
+
// Restore after React has actually returned the table to this pane.
|
|
35
|
+
// A frame scheduled from the animation's promise can run before that commit
|
|
36
|
+
// on a busy renderer, when the empty pane clamps scrollLeft back to zero.
|
|
37
|
+
if (!isExpanded && scrollRef.current)
|
|
38
|
+
scrollRef.current.scrollLeft = scrollPosition.current
|
|
39
|
+
}, [isExpanded])
|
|
40
|
+
const animateExpansion = useCallback(
|
|
41
|
+
(node: HTMLDivElement | null) => {
|
|
42
|
+
const dialog = node?.closest<HTMLElement>(
|
|
43
|
+
'[role="dialog"]',
|
|
44
|
+
)
|
|
45
|
+
if (
|
|
46
|
+
!dialog ||
|
|
47
|
+
matchMedia("(prefers-reduced-motion: reduce)")
|
|
48
|
+
.matches
|
|
49
|
+
)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
const style = getComputedStyle(dialog)
|
|
53
|
+
const duration = Number.parseFloat(
|
|
54
|
+
style.getPropertyValue("--duration-slow"),
|
|
55
|
+
)
|
|
56
|
+
// Zero-duration profiles (including ePaper) remove motion entirely.
|
|
57
|
+
if (!(duration > 0)) return
|
|
58
|
+
|
|
59
|
+
const animation = dialog.animate(
|
|
60
|
+
[
|
|
61
|
+
{
|
|
62
|
+
opacity: 0,
|
|
63
|
+
transform: "translateY(0.75rem) scale(0.98)",
|
|
64
|
+
},
|
|
65
|
+
{ opacity: 1, transform: "none" },
|
|
66
|
+
],
|
|
67
|
+
{
|
|
68
|
+
duration,
|
|
69
|
+
easing:
|
|
70
|
+
style
|
|
71
|
+
.getPropertyValue("--easing-standard")
|
|
72
|
+
.trim() || "ease-out",
|
|
73
|
+
fill: "both",
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
expansion.current = animation
|
|
77
|
+
// Unmounting a document can cancel an unfinished enter or exit.
|
|
78
|
+
void animation.finished.catch(() => {})
|
|
79
|
+
return () => {
|
|
80
|
+
animation.cancel()
|
|
81
|
+
expansion.current = null
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
[],
|
|
85
|
+
)
|
|
86
|
+
const close = () => {
|
|
87
|
+
if (isClosing.current) return
|
|
88
|
+
isClosing.current = true
|
|
89
|
+
const finish = () => {
|
|
90
|
+
setIsExpanded(false)
|
|
91
|
+
isClosing.current = false
|
|
92
|
+
}
|
|
93
|
+
const animation = expansion.current
|
|
94
|
+
if (
|
|
95
|
+
!animation ||
|
|
96
|
+
matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
97
|
+
) {
|
|
98
|
+
finish()
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
// Reversing also handles Escape during the opening animation without a jump.
|
|
102
|
+
animation.reverse()
|
|
103
|
+
void animation.finished.then(finish, () => {})
|
|
104
|
+
}
|
|
26
105
|
const contentStyle = {
|
|
27
106
|
"--table-min-inline-size": `${Math.max(1, columnCount) * 14}rem`,
|
|
28
107
|
} as CSSProperties
|
|
29
108
|
const content = (
|
|
30
109
|
<div
|
|
31
110
|
className="charcuterie-table-content"
|
|
111
|
+
ref={isExpanded ? animateExpansion : undefined}
|
|
32
112
|
style={contentStyle}
|
|
33
113
|
>
|
|
34
114
|
{children}
|
|
@@ -70,14 +150,7 @@ export const TableViewport = ({
|
|
|
70
150
|
heading={label}
|
|
71
151
|
isVisible={isExpanded}
|
|
72
152
|
size="full"
|
|
73
|
-
onClose={
|
|
74
|
-
setIsExpanded(false)
|
|
75
|
-
requestAnimationFrame(() => {
|
|
76
|
-
if (scrollRef.current)
|
|
77
|
-
scrollRef.current.scrollLeft =
|
|
78
|
-
scrollPosition.current
|
|
79
|
-
})
|
|
80
|
-
}}
|
|
153
|
+
onClose={close}
|
|
81
154
|
>
|
|
82
155
|
{isExpanded ? content : null}
|
|
83
156
|
</Dialog>
|