@tangle-network/ui 11.12.0 → 11.13.0
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/CHANGELOG.md +17 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/auth/auth.d.ts +53 -0
- package/dist/{auth-DOETfnd9.js → auth/auth.js} +7 -37
- package/dist/auth/login-layout.d.ts +20 -0
- package/dist/auth/login-layout.js +36 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +2 -1
- package/dist/chat/agent-timeline.d.ts +83 -0
- package/dist/chat/agent-timeline.js +236 -0
- package/dist/chat/chat-container.d.ts +51 -0
- package/dist/chat/chat-container.js +309 -0
- package/dist/chat/chat-message.d.ts +22 -0
- package/dist/chat/chat-message.js +38 -0
- package/dist/chat/message-list.d.ts +32 -0
- package/dist/chat/message-list.js +37 -0
- package/dist/chat/thinking-indicator.d.ts +6 -0
- package/dist/chat/thinking-indicator.js +38 -0
- package/dist/chat/user-message.d.ts +21 -0
- package/dist/chat/user-message.js +42 -0
- package/dist/chat.d.ts +6 -1
- package/dist/chat.js +6 -1
- package/dist/editor/document-editor-pane.d.ts +37 -0
- package/dist/editor/document-editor-pane.js +199 -0
- package/dist/editor/editor-lazy.js +37 -0
- package/dist/editor/editor-loading.js +21 -0
- package/dist/editor/editor-peers.d.ts +6 -0
- package/dist/editor/editor-peers.js +155 -0
- package/dist/editor/editor-provider.d.ts +97 -0
- package/dist/editor/editor-provider.js +253 -0
- package/dist/editor/editor-toolbar.d.ts +11 -0
- package/dist/editor/editor-toolbar.js +106 -0
- package/dist/editor/markdown-conversion.js +23 -0
- package/dist/editor/markdown-document-editor.js +103 -0
- package/dist/editor/tiptap-editor.d.ts +45 -0
- package/dist/editor/tiptap-editor.js +271 -0
- package/dist/editor/use-editor.d.ts +67 -0
- package/dist/editor/use-editor.js +158 -0
- package/dist/editor.d.ts +6 -119
- package/dist/editor.js +5 -1
- package/dist/files/file-artifact-pane.d.ts +47 -0
- package/dist/files/file-artifact-pane.js +114 -0
- package/dist/files/file-format.d.ts +51 -0
- package/dist/{file-format-BgXbr-Ib.js → files/file-format.js} +1 -1
- package/dist/files/file-preview.d.ts +27 -0
- package/dist/files/file-preview.js +359 -0
- package/dist/files/file-tabs.d.ts +19 -0
- package/dist/files/file-tabs.js +48 -0
- package/dist/files/file-tree.d.ts +31 -0
- package/dist/files/file-tree.js +135 -0
- package/dist/files/rich-file-tree.d.ts +62 -0
- package/dist/files/rich-file-tree.js +122 -0
- package/dist/files.d.ts +6 -1
- package/dist/files.js +6 -2
- package/dist/hooks/use-auth.d.ts +40 -0
- package/dist/hooks/use-auth.js +91 -0
- package/dist/hooks/use-auto-scroll.d.ts +11 -0
- package/dist/hooks/use-auto-scroll.js +48 -0
- package/dist/hooks/use-dropdown-menu.d.ts +12 -0
- package/dist/hooks/use-dropdown-menu.js +31 -0
- package/dist/hooks/use-live-time.d.ts +7 -0
- package/dist/hooks/use-live-time.js +22 -0
- package/dist/hooks/use-realtime-session.d.ts +32 -0
- package/dist/hooks/use-realtime-session.js +235 -0
- package/dist/hooks/use-run-collapse-state.d.ts +13 -0
- package/dist/hooks/use-run-collapse-state.js +24 -0
- package/dist/hooks/use-run-groups.d.ts +11 -0
- package/dist/hooks/use-run-groups.js +85 -0
- package/dist/hooks/use-sdk-session.d.ts +59 -0
- package/dist/hooks/use-sdk-session.js +322 -0
- package/dist/hooks/use-sse-stream.d.ts +124 -0
- package/dist/hooks/use-sse-stream.js +229 -0
- package/dist/hooks/use-tool-call-stream.d.ts +23 -0
- package/dist/hooks/use-tool-call-stream.js +75 -0
- package/dist/hooks.d.ts +11 -49
- package/dist/hooks.js +10 -110
- package/dist/index.d.ts +99 -25
- package/dist/index.js +86 -36
- package/dist/lib/focus.d.ts +55 -0
- package/dist/{focus-BcE_SHqz.js → lib/focus.js} +1 -1
- package/dist/lib/utils.d.ts +4 -0
- package/dist/{utils-C_uf36nf.js → lib/utils.js} +1 -1
- package/dist/{code-block-B9miUPht.d.ts → markdown/code-block.d.ts} +5 -6
- package/dist/{code-block-B36KVUSN.js → markdown/code-block.js} +22 -47
- package/dist/{markdown-C0atSabr.d.ts → markdown/markdown.d.ts} +4 -5
- package/dist/{markdown-Db08KAYF.js → markdown/markdown.js} +3 -3
- package/dist/markdown.d.ts +2 -2
- package/dist/markdown.js +2 -2
- package/dist/{openui-artifact-renderer-fkBQaoLO.d.ts → openui/openui-artifact-renderer.d.ts} +20 -21
- package/dist/{openui-artifact-renderer-DG_i2lOR.js → openui/openui-artifact-renderer.js} +9 -7
- package/dist/{schema-BpW9rSSG.js → openui/schema-compiled.js} +2 -354
- package/dist/{schema-DAstk-J4.d.ts → openui/schema-data.d.ts} +3 -24
- package/dist/openui/schema-data.js +206 -0
- package/dist/openui-schema.d.ts +22 -3
- package/dist/openui-schema.js +138 -1
- package/dist/openui.d.ts +3 -2
- package/dist/openui.js +3 -2
- package/dist/{artifact-pane-BL_DFWNl.d.ts → primitives/artifact-pane.d.ts} +3 -4
- package/dist/{artifact-pane-DMXT9a8U.js → primitives/artifact-pane.js} +2 -2
- package/dist/primitives/avatar.d.ts +8 -0
- package/dist/primitives/avatar.js +25 -0
- package/dist/primitives/badge.d.ts +22 -0
- package/dist/primitives/badge.js +47 -0
- package/dist/{button-yoIv77e2.d.ts → primitives/button.d.ts} +2 -2
- package/dist/primitives/button.js +113 -0
- package/dist/primitives/card.d.ts +18 -0
- package/dist/primitives/card.js +52 -0
- package/dist/primitives/code-block.d.ts +6 -0
- package/dist/primitives/code-block.js +13 -0
- package/dist/primitives/dialog.d.ts +23 -0
- package/dist/primitives/dialog.js +59 -0
- package/dist/primitives/drop-zone.d.ts +23 -0
- package/dist/primitives/drop-zone.js +104 -0
- package/dist/primitives/dropdown-menu.d.ts +29 -0
- package/dist/{dropdown-menu-BpcLa5vF.js → primitives/dropdown-menu.js} +2 -23
- package/dist/primitives/empty-state.d.ts +11 -0
- package/dist/primitives/empty-state.js +32 -0
- package/dist/primitives/heading.d.ts +23 -0
- package/dist/primitives/heading.js +38 -0
- package/dist/primitives/input.d.ts +23 -0
- package/dist/primitives/input.js +110 -0
- package/dist/primitives/label.d.ts +6 -0
- package/dist/primitives/label.js +16 -0
- package/dist/primitives/metric-strip.d.ts +48 -0
- package/dist/primitives/metric-strip.js +52 -0
- package/dist/primitives/page-header.d.ts +25 -0
- package/dist/primitives/page-header.js +47 -0
- package/dist/primitives/page-shell.d.ts +14 -0
- package/dist/primitives/page-shell.js +17 -0
- package/dist/primitives/progress.d.ts +9 -0
- package/dist/primitives/progress.js +29 -0
- package/dist/primitives/segmented-control.d.ts +40 -0
- package/dist/primitives/segmented-control.js +57 -0
- package/dist/primitives/select.d.ts +15 -0
- package/dist/primitives/select.js +74 -0
- package/dist/primitives/sidebar-drop-zone.d.ts +21 -0
- package/dist/primitives/sidebar-drop-zone.js +88 -0
- package/dist/primitives/skeleton.d.ts +10 -0
- package/dist/primitives/skeleton.js +50 -0
- package/dist/primitives/stat-card.d.ts +16 -0
- package/dist/primitives/stat-card.js +60 -0
- package/dist/primitives/status-pill.d.ts +44 -0
- package/dist/primitives/status-pill.js +119 -0
- package/dist/primitives/switch.d.ts +6 -0
- package/dist/primitives/switch.js +16 -0
- package/dist/primitives/table.d.ts +22 -0
- package/dist/primitives/table.js +63 -0
- package/dist/primitives/tabs.d.ts +13 -0
- package/dist/{tabs-BVHR6NkO.js → primitives/tabs.js} +3 -3
- package/dist/primitives/terminal-display.d.ts +25 -0
- package/dist/primitives/terminal-display.js +109 -0
- package/dist/primitives/text-shimmer.d.ts +16 -0
- package/dist/{text-shimmer-BUUo6I-g.js → primitives/text-shimmer.js} +2 -2
- package/dist/primitives/theme-toggle.d.ts +11 -0
- package/dist/primitives/theme-toggle.js +154 -0
- package/dist/primitives/toast.d.ts +29 -0
- package/dist/primitives/toast.js +142 -0
- package/dist/primitives/toolbar.d.ts +55 -0
- package/dist/primitives/toolbar.js +37 -0
- package/dist/primitives/upload-progress.d.ts +23 -0
- package/dist/primitives/upload-progress.js +72 -0
- package/dist/primitives.d.ts +35 -5
- package/dist/primitives.js +35 -9
- package/dist/{index-COgqsFAQ.d.ts → redaction/redacted-document.d.ts} +6 -7
- package/dist/{redaction-CGGB2ENG.js → redaction/redacted-document.js} +2 -2
- package/dist/redaction.d.ts +1 -1
- package/dist/redaction.js +1 -1
- package/dist/run/expandable-content.js +41 -0
- package/dist/run/expanded-tool-detail.d.ts +12 -0
- package/dist/run/expanded-tool-detail.js +163 -0
- package/dist/run/inline-thinking-item.d.ts +11 -0
- package/dist/run/inline-thinking-item.js +60 -0
- package/dist/run/inline-tool-item.d.ts +25 -0
- package/dist/run/inline-tool-item.js +63 -0
- package/dist/run/run-group.d.ts +26 -0
- package/dist/{run-group-BWGeesgs.js → run/run-group.js} +9 -60
- package/dist/run/run-item-primitives.d.ts +5 -0
- package/dist/{run-ZsIKPA6J.js → run/run-item-primitives.js} +2 -2
- package/dist/run/run-row-shell.d.ts +53 -0
- package/dist/run/run-row-shell.js +114 -0
- package/dist/{tool-call-feed-BeB1_4pX.d.ts → run/tool-call-feed.d.ts} +7 -12
- package/dist/{tool-call-feed-BpmMLpPX.js → run/tool-call-feed.js} +4 -4
- package/dist/run/tool-call-step.d.ts +5 -0
- package/dist/run/tool-call-step.js +82 -0
- package/dist/run.d.ts +8 -2
- package/dist/run.js +7 -4
- package/dist/sdk-hooks.d.ts +8 -1
- package/dist/sdk-hooks.js +8 -2
- package/dist/stores/active-sessions-store.d.ts +83 -0
- package/dist/{active-sessions-store-Gt1R0rFG.js → stores/active-sessions-store.js} +1 -1
- package/dist/stores/chat-store.d.ts +14 -0
- package/dist/stores/chat-store.js +33 -0
- package/dist/stores.d.ts +3 -16
- package/dist/stores.js +2 -33
- package/dist/tool-previews/command-preview.d.ts +11 -0
- package/dist/tool-previews/command-preview.js +91 -0
- package/dist/tool-previews/diff-preview.d.ts +7 -0
- package/dist/tool-previews/diff-preview.js +76 -0
- package/dist/tool-previews/glob-results-preview.d.ts +7 -0
- package/dist/tool-previews/glob-results-preview.js +53 -0
- package/dist/tool-previews/grep-results-preview.d.ts +7 -0
- package/dist/tool-previews/grep-results-preview.js +99 -0
- package/dist/tool-previews/preview-primitives.js +58 -0
- package/dist/tool-previews/question-preview.d.ts +7 -0
- package/dist/tool-previews/question-preview.js +63 -0
- package/dist/tool-previews/web-search-preview.d.ts +7 -0
- package/dist/tool-previews/web-search-preview.js +78 -0
- package/dist/tool-previews/write-file-preview.d.ts +11 -0
- package/dist/tool-previews/write-file-preview.js +52 -0
- package/dist/tool-previews.d.ts +7 -1
- package/dist/tool-previews.js +7 -1
- package/dist/{branding-26d5ARIr.d.ts → types/branding.d.ts} +2 -3
- package/dist/{message-vhUI0c5G.d.ts → types/message.d.ts} +2 -3
- package/dist/{parts-5mhy9yJ3.d.ts → types/parts.d.ts} +8 -9
- package/dist/{run-OyWmIDqu.d.ts → types/run.d.ts} +9 -10
- package/dist/{tool-display-DgmL5rIU.d.ts → types/tool-display.d.ts} +5 -6
- package/dist/types.d.ts +5 -5
- package/dist/utils/copy-text.d.ts +7 -0
- package/dist/{utils-Bl_CZ0Qp.js → utils/copy-text.js} +1 -11
- package/dist/utils/format.d.ts +22 -0
- package/dist/{format-DfcLWqMa.js → utils/format.js} +1 -1
- package/dist/utils/time-ago.d.ts +3 -0
- package/dist/utils/time-ago.js +11 -0
- package/dist/utils/tool-display.d.ts +16 -0
- package/dist/{tool-display-BAuAVa43.js → utils/tool-display.js} +1 -1
- package/dist/utils.d.ts +6 -1
- package/dist/utils.js +6 -5
- package/package.json +2 -2
- package/src/files/rich-file-tree.stories.tsx +1 -1
- package/src/files/rich-file-tree.tsx +12 -9
- package/src/markdown/code-block.test.tsx +46 -2
- package/src/markdown/code-block.tsx +23 -60
- package/src/primitives/button.stories.tsx +37 -1
- package/src/primitives/button.test.tsx +230 -0
- package/src/primitives/button.tsx +62 -14
- package/src/primitives/card-anatomy.stories.tsx +51 -0
- package/src/primitives/card.test.tsx +71 -0
- package/src/primitives/card.tsx +25 -13
- package/src/primitives/heading.stories.tsx +35 -0
- package/src/primitives/heading.test.tsx +56 -0
- package/src/primitives/heading.tsx +76 -0
- package/src/primitives/index.ts +8 -0
- package/src/primitives/input.stories.tsx +28 -0
- package/src/primitives/input.test.tsx +161 -0
- package/src/primitives/input.tsx +34 -28
- package/src/primitives/page-header.stories.tsx +51 -0
- package/src/primitives/page-header.test.tsx +48 -0
- package/src/primitives/page-header.tsx +35 -35
- package/src/primitives/page-shell.stories.tsx +89 -0
- package/src/primitives/page-shell.test.tsx +24 -0
- package/src/primitives/page-shell.tsx +26 -0
- package/src/primitives/table-wrapper.stories.tsx +54 -0
- package/src/primitives/table.test.tsx +72 -0
- package/src/primitives/table.tsx +33 -12
- package/src/primitives/theme-toggle.test.tsx +167 -0
- package/src/primitives/theme-toggle.tsx +111 -39
- package/dist/active-sessions-store-nl70ZnMC.d.ts +0 -84
- package/dist/button-C87nEe2R.js +0 -78
- package/dist/chat-DDvF2oGQ.js +0 -672
- package/dist/diff-preview-BcK3nRXb.js +0 -533
- package/dist/document-editor-pane-BilJfiCo.d.ts +0 -136
- package/dist/document-editor-pane-POsq5Q0o.js +0 -1279
- package/dist/files-hywocVZz.js +0 -753
- package/dist/index-2UntyjBb.d.ts +0 -52
- package/dist/index-BgUHilZm.d.ts +0 -108
- package/dist/index-CJ8wRfiC.d.ts +0 -236
- package/dist/index-CPDET_s1.d.ts +0 -549
- package/dist/index-Cg6gYAqh.d.ts +0 -73
- package/dist/index-Cs7j8V7l.d.ts +0 -203
- package/dist/index-DjKxO357.d.ts +0 -128
- package/dist/primitives-Bx59fT-U.js +0 -1239
- package/dist/table-Bss73DhB.js +0 -146
- package/dist/tool-call-step-DNebqhmX.js +0 -434
- package/dist/use-auto-scroll-C5Pjeqon.js +0 -153
- package/dist/use-sse-stream-DjqRuDft.js +0 -884
- package/dist/use-sse-stream-Dm6R5YWz.d.ts +0 -282
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Button } from "./button";
|
|
3
|
+
import { PageHeader } from "./page-header";
|
|
4
|
+
import { StatusPill } from "./status-pill";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Primitives/PageHeader",
|
|
8
|
+
component: PageHeader,
|
|
9
|
+
parameters: { layout: "padded" },
|
|
10
|
+
args: {
|
|
11
|
+
title: "Activity",
|
|
12
|
+
description: "Inspect recent work and the scope of the results below.",
|
|
13
|
+
actions: <Button type="button">Export</Button>,
|
|
14
|
+
meta: <><span>12 results · Last 24 hours</span><StatusPill tone="success">Up to date</StatusPill></>,
|
|
15
|
+
},
|
|
16
|
+
} satisfies Meta<typeof PageHeader>;
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {};
|
|
21
|
+
export const Nested: Story = { args: { level: 2 } };
|
|
22
|
+
export const SandboxInputs: Story = {
|
|
23
|
+
args: { actions: undefined, action: <Button type="button">Create item</Button>, eyebrow: "Workspace", titleAs: "h2" },
|
|
24
|
+
};
|
|
25
|
+
export const NarrowLongContent: Story = {
|
|
26
|
+
render: () => <div data-narrow-header style={{ width: 320, maxWidth: "100%" }}>
|
|
27
|
+
<PageHeader
|
|
28
|
+
title="Production activity for workspace-with-a-very-long-unbroken-identifier-012345678901234567890123456789"
|
|
29
|
+
description="Inspect the selected activity without clipping the title or hiding action names."
|
|
30
|
+
eyebrow="Production workspace"
|
|
31
|
+
titleId="long-page-title"
|
|
32
|
+
actions={<>
|
|
33
|
+
<Button type="button" className="h-auto whitespace-normal">Export all selected activity for this workspace</Button>
|
|
34
|
+
<Button type="button" variant="outline" className="h-auto whitespace-normal">Create another activity item</Button>
|
|
35
|
+
</>}
|
|
36
|
+
meta={<><span>0 matching results</span><StatusPill tone="neutral">No filter matches</StatusPill></>}
|
|
37
|
+
/>
|
|
38
|
+
</div>,
|
|
39
|
+
// Executes when this story is opened in a real browser. Building Storybook
|
|
40
|
+
// alone does not run this layout assertion.
|
|
41
|
+
play: async ({ canvasElement }) => {
|
|
42
|
+
await canvasElement.ownerDocument.fonts.ready;
|
|
43
|
+
const root = canvasElement.querySelector<HTMLElement>("[data-narrow-header]")!;
|
|
44
|
+
if (root.scrollWidth > root.clientWidth + 1) throw new Error("PageHeader overflows its narrow container");
|
|
45
|
+
for (const button of root.querySelectorAll("button")) {
|
|
46
|
+
if (button.scrollWidth > button.clientWidth + 1 || button.scrollHeight > button.clientHeight + 1) {
|
|
47
|
+
throw new Error("A long action label is clipped");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
1
2
|
import { render, screen } from "@testing-library/react";
|
|
2
3
|
import { describe, expect, it } from "vitest";
|
|
3
4
|
import { PageHeader } from "./page-header";
|
|
@@ -54,4 +55,51 @@ describe("PageHeader", () => {
|
|
|
54
55
|
expect(container.querySelector("p")).toBeNull();
|
|
55
56
|
expect(screen.queryByRole("button")).toBeNull();
|
|
56
57
|
});
|
|
58
|
+
|
|
59
|
+
it("maps Sandbox inputs while keeping title, eyebrow and description separate", () => {
|
|
60
|
+
const { container } = render(<PageHeader title="Activity" titleAs="h3" eyebrow="Workspace" description="Recent events" action={<button type="button">Export</button>} />);
|
|
61
|
+
expect(screen.getByRole("heading", { level: 3 })).toHaveTextContent("Activity");
|
|
62
|
+
expect(screen.getAllByRole("heading")).toHaveLength(1);
|
|
63
|
+
expect(screen.getByText("Workspace").tagName).toBe("P");
|
|
64
|
+
expect(screen.getByRole("button", { name: "Export" })).toBeInTheDocument();
|
|
65
|
+
for (const attribute of ["action", "actions", "titleAs", "eyebrow", "level"]) {
|
|
66
|
+
expect(container.querySelector("header")).not.toHaveAttribute(attribute);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("gives actions precedence without duplicating controls, including explicit null", () => {
|
|
71
|
+
const fallback = <button type="button">Legacy</button>;
|
|
72
|
+
const { rerender } = render(<PageHeader title="Activity" action={fallback} actions={<button type="button">Canonical</button>} />);
|
|
73
|
+
expect(screen.getByRole("button", { name: "Canonical" })).toBeInTheDocument();
|
|
74
|
+
expect(screen.queryByRole("button", { name: "Legacy" })).toBeNull();
|
|
75
|
+
rerender(<PageHeader title="Activity" action={fallback} actions={null} />);
|
|
76
|
+
expect(screen.queryByRole("button")).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("lets titleAs override level's element but not its visual treatment", () => {
|
|
80
|
+
const { rerender } = render(<PageHeader title="Nested" level={2} />);
|
|
81
|
+
const classes = screen.getByRole("heading", { level: 2 }).className;
|
|
82
|
+
rerender(<PageHeader title="Nested" level={2} titleAs="h4" />);
|
|
83
|
+
expect(screen.getByRole("heading", { level: 4 }).className).toBe(classes);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("preserves the header ref, native attributes and zero-valued metadata", () => {
|
|
87
|
+
const ref = createRef<HTMLElement>();
|
|
88
|
+
render(<PageHeader ref={ref} title="Activity" titleId="activity-title" id="masthead" aria-labelledby="activity-title" className="custom" meta={0} />);
|
|
89
|
+
expect(ref.current?.tagName).toBe("HEADER");
|
|
90
|
+
expect(ref.current).toHaveAttribute("id", "masthead");
|
|
91
|
+
expect(ref.current).toHaveAttribute("aria-labelledby", "activity-title");
|
|
92
|
+
expect(ref.current).toHaveClass("custom");
|
|
93
|
+
expect(screen.getByText("0")).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("does not truncate long titles or put action labels inside the heading", () => {
|
|
97
|
+
const title = "A".repeat(180);
|
|
98
|
+
render(<PageHeader title={title} actions={<button type="button">Export the selected activity</button>} />);
|
|
99
|
+
const heading = screen.getByRole("heading", { name: title });
|
|
100
|
+
expect(heading).toHaveClass("[overflow-wrap:anywhere]");
|
|
101
|
+
expect(heading).not.toHaveClass("truncate");
|
|
102
|
+
expect(heading).not.toHaveTextContent("Export the selected activity");
|
|
103
|
+
expect(screen.getByRole("button").parentElement).toHaveClass("flex-wrap", "min-w-0", "max-w-full");
|
|
104
|
+
});
|
|
57
105
|
});
|
|
@@ -1,39 +1,32 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { cn } from "../lib/utils";
|
|
3
|
+
import { Heading } from "./heading";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* slot here so they cannot compete for the same one:
|
|
9
|
-
*
|
|
10
|
-
* - `title` — what the page IS. The `<h1>`, and the only `<h1>`.
|
|
11
|
-
* - `description` — one line on what it is for. Prose belongs here, never in
|
|
12
|
-
* the title slot: a paragraph rendered where a heading goes reads as body
|
|
13
|
-
* copy to a sighted reader and as nothing at all to a screen reader, which
|
|
14
|
-
* is how a page ends up with no accessible name.
|
|
15
|
-
* - `actions` — what the reader can DO from here, right-aligned and wrapping
|
|
16
|
-
* under the title on a narrow viewport rather than squeezing it.
|
|
17
|
-
*
|
|
18
|
-
* `meta` is the fourth register and the one most often missing: the small facts
|
|
19
|
-
* that qualify everything below (a count, a scope, a window). They sit on their
|
|
20
|
-
* own line under the title so a reader can tell the difference between "this
|
|
21
|
-
* page has nothing in it" and "this filter matches nothing".
|
|
22
|
-
*
|
|
23
|
-
* `titleId` is exposed so a page can point `aria-labelledby` at the heading
|
|
24
|
-
* from a region further down without minting a second copy of the string.
|
|
6
|
+
* A page masthead with separate title, prose, actions and qualifying metadata.
|
|
7
|
+
* Brand's existing props and header ref are preserved. Sandbox's existing
|
|
8
|
+
* action/eyebrow/titleAs inputs map into the same renderer, not a second header.
|
|
25
9
|
*/
|
|
26
10
|
export interface PageHeaderProps
|
|
27
11
|
extends Omit<React.HTMLAttributes<HTMLElement>, "title"> {
|
|
28
12
|
title: React.ReactNode;
|
|
29
13
|
description?: React.ReactNode;
|
|
14
|
+
/** Canonical action slot. If supplied (even null), takes precedence over action. */
|
|
30
15
|
actions?: React.ReactNode;
|
|
31
16
|
meta?: React.ReactNode;
|
|
32
17
|
titleId?: string;
|
|
33
|
-
/**
|
|
18
|
+
/** Existing nested-surface treatment: h2 and section typography. */
|
|
34
19
|
level?: 1 | 2;
|
|
20
|
+
/** Existing Sandbox action slot; used only when actions is undefined. */
|
|
21
|
+
action?: React.ReactNode;
|
|
22
|
+
eyebrow?: React.ReactNode;
|
|
23
|
+
/** Existing Sandbox semantic override; takes precedence over level's tag only. */
|
|
24
|
+
titleAs?: React.ElementType;
|
|
35
25
|
}
|
|
36
26
|
|
|
27
|
+
// A numeric zero is useful metadata, not an absent slot.
|
|
28
|
+
const hasContent = (node: React.ReactNode) => Boolean(node) || node === 0;
|
|
29
|
+
|
|
37
30
|
const PageHeader = React.forwardRef<HTMLElement, PageHeaderProps>(
|
|
38
31
|
(
|
|
39
32
|
{
|
|
@@ -41,39 +34,46 @@ const PageHeader = React.forwardRef<HTMLElement, PageHeaderProps>(
|
|
|
41
34
|
title,
|
|
42
35
|
description,
|
|
43
36
|
actions,
|
|
37
|
+
action,
|
|
38
|
+
eyebrow,
|
|
44
39
|
meta,
|
|
45
40
|
titleId,
|
|
41
|
+
titleAs,
|
|
46
42
|
level = 1,
|
|
47
43
|
...props
|
|
48
44
|
},
|
|
49
45
|
ref,
|
|
50
46
|
) => {
|
|
51
|
-
const
|
|
47
|
+
const resolvedActions = actions === undefined ? action : actions;
|
|
52
48
|
return (
|
|
53
|
-
<header ref={ref} className={cn("mb-6", className)} {...props}>
|
|
54
|
-
<div className="flex flex-wrap items-start justify-between gap-x-6 gap-y-3">
|
|
55
|
-
<div className="min-w-0">
|
|
49
|
+
<header ref={ref} className={cn("mb-6 min-w-0", className)} {...props}>
|
|
50
|
+
<div className="flex min-w-0 flex-wrap items-start justify-between gap-x-6 gap-y-3">
|
|
51
|
+
<div className="min-w-0 flex-[1_1_16rem]">
|
|
52
|
+
{hasContent(eyebrow) && (
|
|
53
|
+
<Heading variant="eyebrow" className="mb-1.5">{eyebrow}</Heading>
|
|
54
|
+
)}
|
|
56
55
|
<Heading
|
|
57
56
|
id={titleId}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)}
|
|
57
|
+
variant={level === 1 ? "page" : "section"}
|
|
58
|
+
as={titleAs ?? (level === 1 ? "h1" : "h2")}
|
|
59
|
+
className="text-balance"
|
|
62
60
|
>
|
|
63
61
|
{title}
|
|
64
62
|
</Heading>
|
|
65
|
-
{description && (
|
|
66
|
-
<p className="mt-1 max-w-prose text-muted-foreground text-sm">
|
|
63
|
+
{hasContent(description) && (
|
|
64
|
+
<p className="mt-1 max-w-prose text-muted-foreground text-sm [overflow-wrap:anywhere]">
|
|
67
65
|
{description}
|
|
68
66
|
</p>
|
|
69
67
|
)}
|
|
70
68
|
</div>
|
|
71
|
-
{
|
|
72
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
69
|
+
{hasContent(resolvedActions) && (
|
|
70
|
+
<div className="flex min-w-0 max-w-full flex-wrap items-center gap-2 [overflow-wrap:anywhere] [&>*]:min-w-0 [&>*]:max-w-full">
|
|
71
|
+
{resolvedActions}
|
|
72
|
+
</div>
|
|
73
73
|
)}
|
|
74
74
|
</div>
|
|
75
|
-
{meta && (
|
|
76
|
-
<div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-2 text-[var(--text-dim)] text-xs">
|
|
75
|
+
{hasContent(meta) && (
|
|
76
|
+
<div className="mt-3 flex min-w-0 flex-wrap items-center gap-x-4 gap-y-2 text-[var(--text-dim)] text-xs [overflow-wrap:anywhere]">
|
|
77
77
|
{meta}
|
|
78
78
|
</div>
|
|
79
79
|
)}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useId, useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Button } from "./button";
|
|
4
|
+
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "./card";
|
|
5
|
+
import { EmptyState } from "./empty-state";
|
|
6
|
+
import { Input } from "./input";
|
|
7
|
+
import { Metric, MetricStrip } from "./metric-strip";
|
|
8
|
+
import { PageHeader } from "./page-header";
|
|
9
|
+
import { PageShell } from "./page-shell";
|
|
10
|
+
import { StatusPill } from "./status-pill";
|
|
11
|
+
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "./table";
|
|
12
|
+
import { Toolbar } from "./toolbar";
|
|
13
|
+
|
|
14
|
+
type PageState = "empty" | "loading" | "error" | "populated";
|
|
15
|
+
const items = [{ name: "Documentation build", status: "Complete" }, { name: "Package validation", status: "Complete" }];
|
|
16
|
+
|
|
17
|
+
/** Local example data and real state transitions; no auth, routing or fetching in PageShell. */
|
|
18
|
+
function ActivityPage({ state, fluid = false }: { state: PageState; fluid?: boolean }) {
|
|
19
|
+
const titleId = useId();
|
|
20
|
+
const searchId = useId();
|
|
21
|
+
const [recovered, setRecovered] = useState(false);
|
|
22
|
+
const [query, setQuery] = useState("");
|
|
23
|
+
const current = recovered ? "populated" : state;
|
|
24
|
+
const available = current === "populated" ? items : [];
|
|
25
|
+
const matching = available.filter((item) => item.name.toLowerCase().includes(query.toLowerCase()));
|
|
26
|
+
const known = current === "empty" || current === "populated";
|
|
27
|
+
const recover = () => { setQuery(""); setRecovered(true); };
|
|
28
|
+
|
|
29
|
+
return <main>
|
|
30
|
+
<PageShell className={fluid ? "mx-0 max-w-none" : undefined}>
|
|
31
|
+
<PageHeader
|
|
32
|
+
title="Workspace activity"
|
|
33
|
+
eyebrow="Example workspace"
|
|
34
|
+
description="Search the example activity or add example items. State is owned by this story, not the layout."
|
|
35
|
+
actions={<Button type="button" disabled={current === "loading"} onClick={recover}>Add example items</Button>}
|
|
36
|
+
meta={known ? <span>{matching.length} matching of {available.length} available items</span> : <span>Activity is not available yet</span>}
|
|
37
|
+
/>
|
|
38
|
+
<MetricStrip>
|
|
39
|
+
<Metric label="Available" value={known ? available.length : "—"} />
|
|
40
|
+
<Metric label="Matching" value={known ? matching.length : "—"} />
|
|
41
|
+
<Metric label="Completed" value={known ? available.length : "—"} />
|
|
42
|
+
<Metric label="Failed" value={known ? 0 : "—"} />
|
|
43
|
+
</MetricStrip>
|
|
44
|
+
<section aria-labelledby={titleId}>
|
|
45
|
+
<Toolbar search={<Input id={searchId} label="Search activity" value={query} disabled={!known} onChange={(event) => setQuery(event.target.value)} placeholder="Search by name" />} />
|
|
46
|
+
<Card>
|
|
47
|
+
<CardHeader>
|
|
48
|
+
<CardTitle as="h2" id={titleId}>Activity items</CardTitle>
|
|
49
|
+
</CardHeader>
|
|
50
|
+
<CardContent aria-busy={current === "loading"}>
|
|
51
|
+
{current === "loading" ? <p role="status">Loading activity…</p> :
|
|
52
|
+
current === "error" ? <div className="space-y-3">
|
|
53
|
+
<p role="alert">Activity could not be loaded. The last request failed; retry to restore the example results.</p>
|
|
54
|
+
<Button type="button" onClick={recover}>Retry</Button>
|
|
55
|
+
</div> : matching.length === 0 ?
|
|
56
|
+
<EmptyState
|
|
57
|
+
title={query ? "No matching activity" : "No activity yet"}
|
|
58
|
+
description={query ? "Try a different search or clear the current filter." : "Add example items to see the populated table."}
|
|
59
|
+
action={<Button type="button" onClick={query ? () => setQuery("") : recover}>{query ? "Clear search" : "Add example items"}</Button>}
|
|
60
|
+
/> :
|
|
61
|
+
<Table wrapper={false}>
|
|
62
|
+
<TableCaption>Matching workspace activity</TableCaption>
|
|
63
|
+
<TableHeader><TableRow><TableHead scope="col">Item</TableHead><TableHead scope="col">State</TableHead></TableRow></TableHeader>
|
|
64
|
+
<TableBody>{matching.map((item) => <TableRow key={item.name}>
|
|
65
|
+
<TableHead scope="row" className="[overflow-wrap:anywhere]">{item.name}</TableHead>
|
|
66
|
+
<TableCell><StatusPill tone="success">{item.status}</StatusPill></TableCell>
|
|
67
|
+
</TableRow>)}</TableBody>
|
|
68
|
+
</Table>}
|
|
69
|
+
</CardContent>
|
|
70
|
+
<CardFooter className="text-sm text-muted-foreground">{known ? "Example data. Use search to exercise the filtered empty state." : "Missing data is not reported as zero activity."}</CardFooter>
|
|
71
|
+
</Card>
|
|
72
|
+
</section>
|
|
73
|
+
</PageShell>
|
|
74
|
+
</main>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const meta = {
|
|
78
|
+
title: "Primitives/PageShell",
|
|
79
|
+
component: PageShell,
|
|
80
|
+
parameters: { layout: "fullscreen" },
|
|
81
|
+
} satisfies Meta<typeof PageShell>;
|
|
82
|
+
export default meta;
|
|
83
|
+
type Story = StoryObj<typeof PageShell>;
|
|
84
|
+
|
|
85
|
+
export const Populated: Story = { render: () => <ActivityPage state="populated" /> };
|
|
86
|
+
export const Empty: Story = { render: () => <ActivityPage state="empty" /> };
|
|
87
|
+
export const Loading: Story = { render: () => <ActivityPage state="loading" /> };
|
|
88
|
+
export const Error: Story = { render: () => <ActivityPage state="error" /> };
|
|
89
|
+
export const FluidWidth: Story = { render: () => <ActivityPage state="populated" fluid /> };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { PageShell } from "./page-shell";
|
|
4
|
+
|
|
5
|
+
describe("PageShell", () => {
|
|
6
|
+
it("ports Sandbox's width, gutters and rhythm without imposing a landmark or grid", () => {
|
|
7
|
+
const { container } = render(<PageShell><p>Content</p></PageShell>);
|
|
8
|
+
const shell = container.firstElementChild;
|
|
9
|
+
expect(shell?.tagName).toBe("DIV");
|
|
10
|
+
expect(shell).toHaveClass("mx-auto", "max-w-6xl", "space-y-8", "px-6", "py-8", "lg:px-8");
|
|
11
|
+
expect(shell).not.toHaveAttribute("role");
|
|
12
|
+
expect(shell).not.toHaveClass("grid");
|
|
13
|
+
expect(screen.queryByRole("main")).toBeNull();
|
|
14
|
+
expect(screen.getByText("Content")).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("allows a caller-owned fluid layout without introducing shell behavior", () => {
|
|
18
|
+
const { container } = render(<main><PageShell className="mx-0 max-w-none px-0 lg:px-0"><p>Workbench</p></PageShell></main>);
|
|
19
|
+
const shell = container.querySelector("main > div");
|
|
20
|
+
expect(shell).toHaveClass("mx-0", "max-w-none", "px-0", "lg:px-0");
|
|
21
|
+
expect(shell).not.toHaveClass("mx-auto", "max-w-6xl", "px-6", "lg:px-8");
|
|
22
|
+
expect(screen.getAllByRole("main")).toHaveLength(1);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
|
|
4
|
+
export interface PageShellProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Ported from sandbox-ui/src/primitives/page-shell.tsx. An optional bounded
|
|
11
|
+
* width, gutter and vertical rhythm, not an application shell or a card grid.
|
|
12
|
+
* The caller owns landmarks, routing, auth, navigation and data state. Use
|
|
13
|
+
* className to override the bound/gutters, or omit it for a workbench layout.
|
|
14
|
+
*/
|
|
15
|
+
export function PageShell({ children, className }: PageShellProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className={cn(
|
|
19
|
+
"mx-auto min-w-0 w-full max-w-6xl space-y-8 px-6 py-8 lg:px-8",
|
|
20
|
+
className,
|
|
21
|
+
)}
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { focusRing } from "../lib/focus";
|
|
3
|
+
import { Heading } from "./heading";
|
|
4
|
+
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, type TableProps } from "./table";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Primitives/Table/Wrapper",
|
|
8
|
+
component: Table,
|
|
9
|
+
parameters: { layout: "padded" },
|
|
10
|
+
} satisfies Meta<typeof Table>;
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof meta>;
|
|
13
|
+
|
|
14
|
+
function WideTable(props: TableProps) {
|
|
15
|
+
return <Table className="min-w-[40rem]" {...props}>
|
|
16
|
+
<TableCaption>Build activity, including the full result identifier</TableCaption>
|
|
17
|
+
<TableHeader><TableRow><TableHead scope="col">Item</TableHead><TableHead scope="col">Result</TableHead><TableHead scope="col">Updated</TableHead></TableRow></TableHeader>
|
|
18
|
+
<TableBody><TableRow><TableHead scope="row">Documentation build</TableHead><TableCell>result_012345678901234567890123456789</TableCell><TableCell>Just now</TableCell></TableRow></TableBody>
|
|
19
|
+
</Table>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const checkScrollOwner: NonNullable<Story["play"]> = ({ canvasElement }) => {
|
|
23
|
+
const regions = canvasElement.querySelectorAll<HTMLElement>('[role="region"]');
|
|
24
|
+
if (regions.length !== 1) throw new Error("Expected exactly one named scroll region");
|
|
25
|
+
const region = regions[0]!;
|
|
26
|
+
if (region.querySelector(".overflow-auto")) throw new Error("Table introduced a nested scroll container");
|
|
27
|
+
if (region.scrollWidth <= region.clientWidth) throw new Error("Wide-table fixture does not overflow");
|
|
28
|
+
region.focus();
|
|
29
|
+
if (region.ownerDocument.activeElement !== region) throw new Error("Scroll owner is not focusable");
|
|
30
|
+
region.scrollLeft = 24;
|
|
31
|
+
if (region.scrollLeft <= 0) throw new Error("Named scroll region cannot scroll");
|
|
32
|
+
region.scrollLeft = 0;
|
|
33
|
+
if (!region.querySelector("table > caption") || !region.querySelector('table > thead th[scope="col"]')) {
|
|
34
|
+
throw new Error("Native table semantics were lost");
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const AccessibleOverflow: Story = {
|
|
39
|
+
render: () => <div className="space-y-3" style={{ width: 320, maxWidth: "100%" }}>
|
|
40
|
+
<Heading variant="section">All activity columns</Heading>
|
|
41
|
+
<p className="text-sm text-muted-foreground">Tab to the named region and use the arrow keys to reveal the remaining columns.</p>
|
|
42
|
+
<WideTable wrapperProps={{ role: "region", "aria-label": "Activity columns", tabIndex: 0, className: focusRing }} />
|
|
43
|
+
</div>,
|
|
44
|
+
play: checkScrollOwner,
|
|
45
|
+
};
|
|
46
|
+
export const CallerOwnedScroll: Story = {
|
|
47
|
+
render: () => <div className="space-y-3" style={{ width: 320, maxWidth: "100%" }}>
|
|
48
|
+
<Heading variant="section">Caller-owned scrolling</Heading>
|
|
49
|
+
<div role="region" aria-label="Activity columns" tabIndex={0} className={`overflow-auto ${focusRing}`}>
|
|
50
|
+
<WideTable wrapper={false} />
|
|
51
|
+
</div>
|
|
52
|
+
</div>,
|
|
53
|
+
play: checkScrollOwner,
|
|
54
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createRef } from "react";
|
|
2
|
+
import { render, screen, within } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, type TableProps } from "./table";
|
|
6
|
+
|
|
7
|
+
function UsageTable(props: TableProps) {
|
|
8
|
+
return <Table {...props}>
|
|
9
|
+
<TableCaption>Usage</TableCaption>
|
|
10
|
+
<TableHeader><TableRow><TableHead scope="col" id="resource">Resource</TableHead><TableHead scope="col" id="count">Count</TableHead></TableRow></TableHeader>
|
|
11
|
+
<TableBody><TableRow><TableHead scope="row" id="builds">Builds</TableHead><TableCell headers="builds count">3</TableCell></TableRow></TableBody>
|
|
12
|
+
<TableFooter><TableRow><TableCell colSpan={2}>One resource</TableCell></TableRow></TableFooter>
|
|
13
|
+
</Table>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("Table", () => {
|
|
17
|
+
it("preserves the default wrapper without imposing an unnamed region or tab stop", () => {
|
|
18
|
+
render(<UsageTable />);
|
|
19
|
+
const table = screen.getByRole("table", { name: "Usage" });
|
|
20
|
+
expect(table.parentElement?.tagName).toBe("DIV");
|
|
21
|
+
expect(table.parentElement).toHaveClass("relative", "w-full", "overflow-auto");
|
|
22
|
+
expect(table.parentElement).not.toHaveAttribute("role");
|
|
23
|
+
expect(table.parentElement).not.toHaveAttribute("tabindex");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("retains native caption, table sections, headers, scope and spans", () => {
|
|
27
|
+
render(<UsageTable />);
|
|
28
|
+
const table = screen.getByRole("table", { name: "Usage" });
|
|
29
|
+
expect(Array.from(table.children).map((node) => node.tagName)).toEqual(["CAPTION", "THEAD", "TBODY", "TFOOT"]);
|
|
30
|
+
expect(screen.getByRole("columnheader", { name: "Resource" })).toHaveAttribute("scope", "col");
|
|
31
|
+
expect(screen.getByRole("rowheader", { name: "Builds" })).toHaveAttribute("scope", "row");
|
|
32
|
+
expect(screen.getByRole("cell", { name: "3" })).toHaveAttribute("headers", "builds count");
|
|
33
|
+
expect(screen.getByRole("cell", { name: "One resource" })).toHaveAttribute("colspan", "2");
|
|
34
|
+
expect(screen.queryByRole("grid")).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("allows a caller-named keyboard-focusable wrapper separately from table attributes", async () => {
|
|
38
|
+
const user = userEvent.setup();
|
|
39
|
+
render(<><h2 id="usage-title">Resource usage</h2><UsageTable id="usage-table" className="min-w-[40rem]" wrapperProps={{ role: "region", "aria-labelledby": "usage-title", tabIndex: 0, className: "max-h-80" }} /></>);
|
|
40
|
+
const region = screen.getByRole("region", { name: "Resource usage" });
|
|
41
|
+
const table = within(region).getByRole("table", { name: "Usage" });
|
|
42
|
+
expect(region).toHaveClass("max-h-80", "overflow-auto");
|
|
43
|
+
expect(table).toHaveAttribute("id", "usage-table");
|
|
44
|
+
expect(table).not.toHaveAttribute("tabindex");
|
|
45
|
+
expect(table).not.toHaveClass("max-h-80");
|
|
46
|
+
await user.tab();
|
|
47
|
+
expect(region).toHaveFocus();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns a bare table for a caller-owned scroll region, without leaking wrapper props", () => {
|
|
51
|
+
render(<div role="region" aria-label="Outer" className="overflow-auto">
|
|
52
|
+
<UsageTable wrapper={false} wrapperProps={{ id: "unused-wrapper" }} />
|
|
53
|
+
</div>);
|
|
54
|
+
const outer = screen.getByRole("region", { name: "Outer" });
|
|
55
|
+
const table = screen.getByRole("table", { name: "Usage" });
|
|
56
|
+
expect(outer.firstElementChild).toBe(table);
|
|
57
|
+
expect(outer.querySelectorAll(".overflow-auto")).toHaveLength(0);
|
|
58
|
+
expect(table).not.toHaveAttribute("wrapper");
|
|
59
|
+
expect(table).not.toHaveAttribute("wrapperProps");
|
|
60
|
+
expect(table).not.toHaveAttribute("id", "unused-wrapper");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("keeps the public ref and native attributes/handlers on the table", async () => {
|
|
64
|
+
const ref = createRef<HTMLTableElement>();
|
|
65
|
+
const onClick = vi.fn();
|
|
66
|
+
render(<Table ref={ref} cellPadding={0} onClick={onClick} aria-label="Details"><TableBody><TableRow><TableCell>Open</TableCell></TableRow></TableBody></Table>);
|
|
67
|
+
expect(ref.current).toBe(screen.getByRole("table", { name: "Details" }));
|
|
68
|
+
expect(ref.current).toHaveAttribute("cellpadding", "0");
|
|
69
|
+
await userEvent.setup().click(screen.getByRole("cell", { name: "Open" }));
|
|
70
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
71
|
+
});
|
|
72
|
+
});
|
package/src/primitives/table.tsx
CHANGED
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { cn } from "../lib/utils";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
4
|
+
export interface TableProps extends React.TableHTMLAttributes<HTMLTableElement> {
|
|
5
|
+
/** Keep the existing scroll wrapper by default; false returns only the native table. */
|
|
6
|
+
wrapper?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Attributes for the wrapper, not the table (ignored when wrapper is false).
|
|
9
|
+
* For an overflowing table, supply tabIndex and an accessible region name.
|
|
10
|
+
* No extra tab stop or unnamed landmark is imposed on every small table.
|
|
11
|
+
*/
|
|
12
|
+
wrapperProps?: Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "dangerouslySetInnerHTML">;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const Table = React.forwardRef<HTMLTableElement, TableProps>(
|
|
16
|
+
({ className, wrapper = true, wrapperProps, ...props }, ref) => {
|
|
17
|
+
const table = (
|
|
18
|
+
<table
|
|
19
|
+
ref={ref}
|
|
20
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
if (!wrapper) return table;
|
|
25
|
+
|
|
26
|
+
const { className: wrapperClassName, ...attributes } = wrapperProps ?? {};
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
{...attributes}
|
|
30
|
+
className={cn("relative w-full overflow-auto", wrapperClassName)}
|
|
31
|
+
>
|
|
32
|
+
{table}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
);
|
|
16
37
|
Table.displayName = "Table";
|
|
17
38
|
|
|
18
39
|
const TableHeader = React.forwardRef<
|