@multiplatform.one/components 7.7.0 → 7.7.2
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/cjs/Accordion/index.native.js +1 -1
- package/dist/cjs/Accordion/index.native.js.map +1 -1
- package/dist/cjs/Alert/index.cjs +13 -29
- package/dist/cjs/Alert/index.native.js +17 -37
- package/dist/cjs/Alert/index.native.js.map +1 -1
- package/dist/cjs/AnimateHeight/index.native.js.map +1 -1
- package/dist/cjs/Avatar/getInitials.native.js.map +1 -1
- package/dist/cjs/Avatar/index.native.js.map +1 -1
- package/dist/cjs/Badge/index.native.js.map +1 -1
- package/dist/cjs/Button.native.js.map +1 -1
- package/dist/cjs/Card.native.js.map +1 -1
- package/dist/cjs/CardFooter.native.js.map +1 -1
- package/dist/cjs/CardHeader.native.js.map +1 -1
- package/dist/cjs/Carousel/index.native.js +1 -1
- package/dist/cjs/Carousel/index.native.js.map +1 -1
- package/dist/cjs/Chip/index.native.js.map +1 -1
- package/dist/cjs/ContextMenu/index.native.js.map +1 -1
- package/dist/cjs/CopyField/index.native.js.map +1 -1
- package/dist/cjs/DotIndicator/index.native.js.map +1 -1
- package/dist/cjs/DropdownMenu/index.native.js.map +1 -1
- package/dist/cjs/ErrorBoundary/index.native.js.map +1 -1
- package/dist/cjs/Heading.native.js.map +1 -1
- package/dist/cjs/Meter/index.native.js +1 -1
- package/dist/cjs/Meter/index.native.js.map +1 -1
- package/dist/cjs/Meter/meter.native.js.map +1 -1
- package/dist/cjs/Pagination.native.js +1 -1
- package/dist/cjs/Pagination.native.js.map +1 -1
- package/dist/cjs/ProgressSteps/index.native.js.map +1 -1
- package/dist/cjs/Quote/index.native.js.map +1 -1
- package/dist/cjs/Separator/index.native.js.map +1 -1
- package/dist/cjs/SheetModal/detents.native.js.map +1 -1
- package/dist/cjs/Skeleton/index.native.js.map +1 -1
- package/dist/cjs/SkipLink/index.native.js.map +1 -1
- package/dist/cjs/SortableList/SortableHandle.native.js.map +1 -1
- package/dist/cjs/SortableList/index.native.js.map +1 -1
- package/dist/cjs/SortableList/useSortableRows.native.js.map +1 -1
- package/dist/cjs/Tabs/index.native.js.map +1 -1
- package/dist/cjs/Text.native.js.map +1 -1
- package/dist/cjs/Toast/index.native.js.map +1 -1
- package/dist/cjs/Toast/store.native.js.map +1 -1
- package/dist/cjs/Tooltip/nativePress.native.js.map +1 -1
- package/dist/cjs/Tooltip/tooltipShared.native.js.map +1 -1
- package/dist/cjs/TreeSelect/index.native.js +1 -1
- package/dist/cjs/TreeSelect/index.native.js.map +1 -1
- package/dist/cjs/TreeSelect/treeSelectModel.native.js.map +1 -1
- package/dist/cjs/Video/PlayerChrome.native.js.map +1 -1
- package/dist/cjs/Video/VideoFrame.native.js.map +1 -1
- package/dist/cjs/Video/types.native.js.map +1 -1
- package/dist/cjs/Wheel/index.native.js.map +1 -1
- package/dist/cjs/Wizard/index.native.js +2 -2
- package/dist/cjs/Wizard/index.native.js.map +1 -1
- package/dist/cjs/actions/ActionBar.cjs +3 -1
- package/dist/cjs/actions/ActionBar.native.js +11 -2
- package/dist/cjs/actions/ActionBar.native.js.map +1 -1
- package/dist/cjs/actions/DestructiveAction.native.js.map +1 -1
- package/dist/cjs/actions/devWarn.native.js.map +1 -1
- package/dist/cjs/actions/index.native.js.map +1 -1
- package/dist/cjs/actions/severity.native.js.map +1 -1
- package/dist/cjs/charts/Axis.native.js.map +1 -1
- package/dist/cjs/charts/BarChart.native.js.map +1 -1
- package/dist/cjs/charts/ChartSurface.native.js.map +1 -1
- package/dist/cjs/charts/LineChart.native.js.map +1 -1
- package/dist/cjs/charts/PieChart.native.js.map +1 -1
- package/dist/cjs/charts/Sparkline.native.js.map +1 -1
- package/dist/cjs/charts/accessibility.native.js.map +1 -1
- package/dist/cjs/charts/composeChart.native.js.map +1 -1
- package/dist/cjs/charts/index.native.js.map +1 -1
- package/dist/cjs/charts/math.native.js.map +1 -1
- package/dist/cjs/charts/series.native.js.map +1 -1
- package/dist/cjs/charts/svg.native.js.map +1 -1
- package/dist/cjs/charts/types.native.js.map +1 -1
- package/dist/cjs/charts/useChartTheme.native.js.map +1 -1
- package/dist/cjs/code/Code/index.native.js.map +1 -1
- package/dist/cjs/code/CodeBlock/index.native.js +6 -6
- package/dist/cjs/code/CodeBlock/index.native.js.map +1 -1
- package/dist/cjs/code/HighlightedCode/index.native.js.map +1 -1
- package/dist/cjs/code/Highlighter/index.native.js.map +1 -1
- package/dist/cjs/code/Pre/index.native.js.map +1 -1
- package/dist/cjs/code/index.native.js +5 -5
- package/dist/cjs/code/index.native.js.map +1 -1
- package/dist/cjs/componentColors.native.js.map +1 -1
- package/dist/cjs/feedback/ConfirmDialog.native.js.map +1 -1
- package/dist/cjs/feedback/NotifyHost.native.js +2 -2
- package/dist/cjs/feedback/NotifyHost.native.js.map +1 -1
- package/dist/cjs/feedback/copy.native.js.map +1 -1
- package/dist/cjs/feedback/index.native.js.map +1 -1
- package/dist/cjs/feedback/notify.native.js.map +1 -1
- package/dist/cjs/feedback/route.native.js.map +1 -1
- package/dist/cjs/feedback/store.native.js.map +1 -1
- package/dist/cjs/hooks/index.native.js +1 -1
- package/dist/cjs/hooks/index.native.js.map +1 -1
- package/dist/cjs/hooks/useClipboard.native.js.map +1 -1
- package/dist/cjs/hooks/useDocumentTitle.native.js.map +1 -1
- package/dist/cjs/icons/CheckRegular.native.js.map +1 -1
- package/dist/cjs/icons/MinusRegular.native.js.map +1 -1
- package/dist/cjs/icons/index.native.js.map +1 -1
- package/dist/cjs/icons/lucide.native.js.map +1 -1
- package/dist/cjs/icons/themed.native.js.map +1 -1
- package/dist/cjs/images/Image/ImageFrame.native.js +1 -1
- package/dist/cjs/images/Image/ImageFrame.native.js.map +1 -1
- package/dist/cjs/images/Image/index.native.js +1 -1
- package/dist/cjs/images/Image/shared.native.js +1 -1
- package/dist/cjs/images/Image/shared.native.js.map +1 -1
- package/dist/cjs/images/Image/types.native.js.map +1 -1
- package/dist/cjs/images/index.native.js.map +1 -1
- package/dist/cjs/index.native.js +29 -29
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/knobImmuneExports.native.js.map +1 -1
- package/dist/cjs/layouts/AsyncBoundary.native.js +2 -2
- package/dist/cjs/layouts/AsyncBoundary.native.js.map +1 -1
- package/dist/cjs/layouts/Debug/index.native.js.map +1 -1
- package/dist/cjs/layouts/EmptyState.native.js.map +1 -1
- package/dist/cjs/layouts/KeyboardAvoidingWrapper/behavior.native.js.map +1 -1
- package/dist/cjs/layouts/PageTemplates.native.js.map +1 -1
- package/dist/cjs/layouts/PaneScaffold.native.js.map +1 -1
- package/dist/cjs/layouts/SafeAreaWrapper/insets.native.js.map +1 -1
- package/dist/cjs/layouts/ViewSwitcher.native.js.map +1 -1
- package/dist/cjs/layouts/attachments.native.js +1 -1
- package/dist/cjs/layouts/attachments.native.js.map +1 -1
- package/dist/cjs/layouts/breadcrumbs.native.js.map +1 -1
- package/dist/cjs/layouts/index.native.js +3 -3
- package/dist/cjs/layouts/index.native.js.map +1 -1
- package/dist/cjs/layouts/page.native.js.map +1 -1
- package/dist/cjs/layouts/pageHeadingScope.native.js.map +1 -1
- package/dist/cjs/layouts/sidebar.native.js.map +1 -1
- package/dist/cjs/layouts/tags.native.js +1 -1
- package/dist/cjs/layouts/tags.native.js.map +1 -1
- package/dist/cjs/layouts/timeline.native.js +2 -2
- package/dist/cjs/layouts/timeline.native.js.map +1 -1
- package/dist/cjs/layouts/toolbar.native.js.map +1 -1
- package/dist/cjs/shared/actionScan.native.js.map +1 -1
- package/dist/cjs/shared/i18n.native.js.map +1 -1
- package/dist/cjs/shared/t.native.js.map +1 -1
- package/dist/cjs/shared/textRidesText.native.js.map +1 -1
- package/dist/cjs/shared/useKeyboardInset.native.js.map +1 -1
- package/dist/cjs/shared/useReturnFocusOnClose.native.js.map +1 -1
- package/dist/cjs/surfaces.native.js +1 -1
- package/dist/cjs/surfaces.native.js.map +1 -1
- package/dist/cjs/tamagui.native.js.map +1 -1
- package/dist/cjs/testSetup.native.js.map +1 -1
- package/dist/cjs/utils/setTinted.native.js.map +1 -1
- package/dist/cjs/utils/unwrapText.native.js.map +1 -1
- package/dist/cjs/views/Calendar.native.js.map +1 -1
- package/dist/cjs/views/Dashboard.native.js +1 -1
- package/dist/cjs/views/Dashboard.native.js.map +1 -1
- package/dist/cjs/views/Gantt.native.js +1 -1
- package/dist/cjs/views/Gantt.native.js.map +1 -1
- package/dist/cjs/views/ImageGrid.native.js +1 -1
- package/dist/cjs/views/ImageGrid.native.js.map +1 -1
- package/dist/cjs/views/List.native.js.map +1 -1
- package/dist/cjs/views/ReportBuilder.native.js +1 -1
- package/dist/cjs/views/ReportBuilder.native.js.map +1 -1
- package/dist/cjs/views/TreeView.native.js.map +1 -1
- package/dist/cjs/views/calendarMath.native.js.map +1 -1
- package/dist/cjs/views/ganttMath.native.js.map +1 -1
- package/dist/cjs/views/index.native.js +1 -1
- package/dist/cjs/views/index.native.js.map +1 -1
- package/dist/cjs/views/kanbanDnd.native.js.map +1 -1
- package/dist/cjs/views/treeViewDnd.native.js.map +1 -1
- package/dist/cjs/views/treeViewLayout.native.js.map +1 -1
- package/dist/cjs/views/treeViewMotion.native.js.map +1 -1
- package/dist/esm/Alert/index.mjs +14 -31
- package/dist/esm/Alert/index.mjs.map +1 -1
- package/dist/esm/Alert/index.native.js +36 -57
- package/dist/esm/Alert/index.native.js.map +1 -1
- package/dist/esm/actions/ActionBar.mjs +6 -4
- package/dist/esm/actions/ActionBar.mjs.map +1 -1
- package/dist/esm/actions/ActionBar.native.js +13 -4
- package/dist/esm/actions/ActionBar.native.js.map +1 -1
- package/dist/jsx/Alert/index.mjs +14 -31
- package/dist/jsx/Alert/index.mjs.map +1 -1
- package/dist/jsx/Alert/index.native.js +17 -37
- package/dist/jsx/Alert/index.native.js.map +1 -1
- package/dist/jsx/actions/ActionBar.mjs +6 -4
- package/dist/jsx/actions/ActionBar.mjs.map +1 -1
- package/dist/jsx/actions/ActionBar.native.js +11 -2
- package/dist/jsx/actions/ActionBar.native.js.map +1 -1
- package/dist/jsx/feedback/NotifyHost.native.js +1 -1
- package/dist/jsx/index.native.js +1 -1
- package/dist/jsx/layouts/AsyncBoundary.native.js +1 -1
- package/dist/jsx/layouts/AsyncBoundary.native.js.map +1 -1
- package/package.json +9 -9
- package/src/Alert/index.tsx +9 -4
- package/src/AnimateHeight/AnimateHeight.stories.spec.tsx +37 -0
- package/src/AnimateHeight/AnimateHeight.stories.tsx +95 -0
- package/src/Avatar/Avatar.spec.tsx +4 -1
- package/src/Button.spec.tsx +26 -15
- package/src/SheetModal/SheetModal.stories.spec.tsx +54 -0
- package/src/SheetModal/SheetModal.stories.tsx +109 -0
- package/src/actions/ActionBar.stories.spec.tsx +45 -0
- package/src/actions/ActionBar.stories.tsx +161 -0
- package/src/actions/ActionBar.tsx +8 -1
- package/src/charts/Axis.stories.spec.tsx +86 -0
- package/src/charts/Axis.stories.tsx +186 -0
- package/src/charts/ChartSurface.stories.spec.tsx +43 -0
- package/src/charts/ChartSurface.stories.tsx +148 -0
- package/types/Alert/index.d.ts.map +1 -1
- package/types/actions/ActionBar.d.ts.map +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The story file itself is the artifact under test here: MPO-193 landed
|
|
3
|
+
* AnimateHeight's first Kitchen Sink title, and a story that never mounts is
|
|
4
|
+
* the failure mode the previous wave shipped. These render every exported
|
|
5
|
+
* story through the real providers so a broken import or a bad arg fails the
|
|
6
|
+
* package tests instead of the gallery.
|
|
7
|
+
*/
|
|
8
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
9
|
+
import { cleanup, screen } from "@testing-library/react";
|
|
10
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
11
|
+
import meta, { AlwaysOpen, Default, FollowsContent } from "./AnimateHeight.stories";
|
|
12
|
+
|
|
13
|
+
afterEach(cleanup);
|
|
14
|
+
|
|
15
|
+
describe("AnimateHeight stories", () => {
|
|
16
|
+
it("declares the Components/AnimateHeight title against the component", () => {
|
|
17
|
+
expect(meta.title).toBe("Components/AnimateHeight");
|
|
18
|
+
expect(meta.component).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("Main mounts collapsed with the expand affordance", () => {
|
|
22
|
+
renderWithProviders(<>{Default.render?.({} as never, {} as never)}</>);
|
|
23
|
+
expect(screen.getByText("Expand")).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("Follower mode mounts with content already visible", () => {
|
|
27
|
+
renderWithProviders(<>{FollowsContent.render?.({} as never, {} as never)}</>);
|
|
28
|
+
expect(screen.getByText("Height is measured, not guessed.")).toBeTruthy();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("Open renders its children when open is true", () => {
|
|
32
|
+
renderWithProviders(
|
|
33
|
+
<>{AlwaysOpen.render?.(AlwaysOpen.args as never, { args: AlwaysOpen.args } as never)}</>,
|
|
34
|
+
);
|
|
35
|
+
expect(screen.getByText("Toggle `open` in the controls panel.")).toBeTruthy();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Button } from "@multiplatform.one/forms";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Paragraph, SizableText, YStack } from "tamagui";
|
|
5
|
+
import { Card } from "../surfaces";
|
|
6
|
+
import { AnimateHeight } from "./index";
|
|
7
|
+
|
|
8
|
+
const lines = [
|
|
9
|
+
"Height is measured, not guessed.",
|
|
10
|
+
"CSS cannot transition to height: auto, so the box drives a numeric height.",
|
|
11
|
+
"Web measures with a ResizeObserver; native measures with onLayout.",
|
|
12
|
+
"Padding belongs inside the children, never on the container.",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function Collapsible() {
|
|
16
|
+
const [open, setOpen] = useState(false);
|
|
17
|
+
return (
|
|
18
|
+
<YStack gap="$3" maxWidth={420}>
|
|
19
|
+
<Button onPress={() => setOpen((value) => !value)}>{open ? "Collapse" : "Expand"}</Button>
|
|
20
|
+
<AnimateHeight open={open}>
|
|
21
|
+
<Card>
|
|
22
|
+
<YStack gap="$2">
|
|
23
|
+
{lines.map((line) => (
|
|
24
|
+
<Paragraph key={line}>{line}</Paragraph>
|
|
25
|
+
))}
|
|
26
|
+
</YStack>
|
|
27
|
+
</Card>
|
|
28
|
+
</AnimateHeight>
|
|
29
|
+
</YStack>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function Follower() {
|
|
34
|
+
const [count, setCount] = useState(1);
|
|
35
|
+
return (
|
|
36
|
+
<YStack gap="$3" maxWidth={420}>
|
|
37
|
+
<Button onPress={() => setCount((value) => (value >= lines.length ? 1 : value + 1))}>
|
|
38
|
+
{`Add a line (${count}/${lines.length})`}
|
|
39
|
+
</Button>
|
|
40
|
+
<AnimateHeight>
|
|
41
|
+
<Card>
|
|
42
|
+
<YStack gap="$2">
|
|
43
|
+
{lines.slice(0, count).map((line) => (
|
|
44
|
+
<Paragraph key={line}>{line}</Paragraph>
|
|
45
|
+
))}
|
|
46
|
+
</YStack>
|
|
47
|
+
</Card>
|
|
48
|
+
</AnimateHeight>
|
|
49
|
+
</YStack>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const meta: Meta<typeof AnimateHeight> = {
|
|
54
|
+
title: "Components/AnimateHeight",
|
|
55
|
+
component: AnimateHeight,
|
|
56
|
+
parameters: {
|
|
57
|
+
status: { type: "beta" },
|
|
58
|
+
docs: {
|
|
59
|
+
description: {
|
|
60
|
+
component:
|
|
61
|
+
'Auto-height container. CSS never interpolates `height: auto`, so this measures the content (ResizeObserver on web, onLayout on native) and tweens a numeric height through the knob `transition`. Two modes: omit `open` for follower (height tracks content while mounted), pass `open` for collapsible (children unmount once the collapse settles). Under reduced motion or `animation="none"` the knob transition is undefined and heights apply instantly, so state changes still land.',
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
argTypes: {
|
|
66
|
+
open: { control: "boolean" },
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
export default meta;
|
|
70
|
+
|
|
71
|
+
type Story = StoryObj<typeof AnimateHeight>;
|
|
72
|
+
|
|
73
|
+
export const Default: Story = {
|
|
74
|
+
name: "Main",
|
|
75
|
+
render: () => <Collapsible />,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const FollowsContent: Story = {
|
|
79
|
+
name: "Follower mode",
|
|
80
|
+
render: () => <Follower />,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const AlwaysOpen: Story = {
|
|
84
|
+
name: "Open",
|
|
85
|
+
args: { open: true },
|
|
86
|
+
render: (args) => (
|
|
87
|
+
<YStack maxWidth={420}>
|
|
88
|
+
<AnimateHeight {...args}>
|
|
89
|
+
<Card>
|
|
90
|
+
<SizableText>Toggle `open` in the controls panel.</SizableText>
|
|
91
|
+
</Card>
|
|
92
|
+
</AnimateHeight>
|
|
93
|
+
</YStack>
|
|
94
|
+
),
|
|
95
|
+
};
|
|
@@ -67,7 +67,10 @@ describe("Avatar", () => {
|
|
|
67
67
|
const compact = renderWithProviders(<Avatar name="Ada Lovelace" compact />);
|
|
68
68
|
expect(pxOf(page.container)).toBe(36);
|
|
69
69
|
expect(pxOf(nested.container)).toBe(32);
|
|
70
|
-
|
|
70
|
+
// LC-76: density is not size. `compact` steps SPACE only (see
|
|
71
|
+
// resolveKnobs.applyDensity / compactSpaceMap) — it must not shrink a
|
|
72
|
+
// painted control. Only `nested` and an explicit size token move the face.
|
|
73
|
+
expect(pxOf(compact.container)).toBe(36);
|
|
71
74
|
expect(
|
|
72
75
|
nested.container.querySelector("[data-avatar-nested]")?.getAttribute("data-avatar-nested"),
|
|
73
76
|
).toBe("true");
|
package/src/Button.spec.tsx
CHANGED
|
@@ -119,21 +119,32 @@ describe("components Button — elevation and size knobs reach the frame", () =>
|
|
|
119
119
|
expect(screen.getByRole("button").className).toMatch(/_bxsh-/);
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
-
it("size knob drives the frame height
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
122
|
+
it("size knob drives the frame height", () => {
|
|
123
|
+
// The generated size recipes resolve a control height to a concrete px
|
|
124
|
+
// atom, not a `$size` token class — asserting `_h-t-size-N` tested the
|
|
125
|
+
// pre-recipe emission shape, not the knob. Read the height back instead:
|
|
126
|
+
// small/medium/large must be three distinct, ascending heights.
|
|
127
|
+
const heightOf = () => {
|
|
128
|
+
const atom = screen
|
|
129
|
+
.getByRole("button")
|
|
130
|
+
.className.split(" ")
|
|
131
|
+
.find((c) => /^_h-\d+px$/.test(c));
|
|
132
|
+
expect(atom).toBeDefined();
|
|
133
|
+
return Number(atom!.replace(/^_h-|px$/g, ""));
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const heights: number[] = [];
|
|
137
|
+
for (const size of ["small", "medium", "large"] as const) {
|
|
138
|
+
renderWithProviders(
|
|
139
|
+
<Preset overrides={{ size }}>
|
|
140
|
+
<Button>Save</Button>
|
|
141
|
+
</Preset>,
|
|
142
|
+
);
|
|
143
|
+
heights.push(heightOf());
|
|
144
|
+
cleanup();
|
|
145
|
+
}
|
|
146
|
+
expect(heights[0]).toBeLessThan(heights[1]);
|
|
147
|
+
expect(heights[1]).toBeLessThan(heights[2]);
|
|
137
148
|
});
|
|
138
149
|
});
|
|
139
150
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SheetModal was the harness inside SheetFrame.stories and had no title of its
|
|
3
|
+
* own, so the gallery listed the frame and not the sheet (MPO-193). These
|
|
4
|
+
* mount every exported story so an unrenderable one fails here, not in the
|
|
5
|
+
* Kitchen Sink.
|
|
6
|
+
*
|
|
7
|
+
* Presence is asserted by accessible NAME, not by counting dialogs: tamagui's
|
|
8
|
+
* Sheet keeps its portal in the tree either side of `open`, and happy-dom runs
|
|
9
|
+
* no animation, so `role="dialog"` matches more than one node whatever the
|
|
10
|
+
* open state is.
|
|
11
|
+
*/
|
|
12
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
13
|
+
import { cleanup, screen } from "@testing-library/react";
|
|
14
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
15
|
+
import meta, { Default, OpenOnMount, Scrollable, TallDetent } from "./SheetModal.stories";
|
|
16
|
+
|
|
17
|
+
afterEach(cleanup);
|
|
18
|
+
|
|
19
|
+
describe("SheetModal stories", () => {
|
|
20
|
+
it("declares the Components/SheetModal title against the component", () => {
|
|
21
|
+
expect(meta.title).toBe("Components/SheetModal");
|
|
22
|
+
expect(meta.component).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("every exported story mounts its trigger", () => {
|
|
26
|
+
for (const story of [Default, Scrollable, TallDetent, OpenOnMount]) {
|
|
27
|
+
renderWithProviders(<>{story.render?.({} as never, {} as never)}</>);
|
|
28
|
+
expect(screen.getAllByText("Open sheet").length).toBeGreaterThan(0);
|
|
29
|
+
cleanup();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("the sheet frame is a labelled modal dialog", () => {
|
|
34
|
+
renderWithProviders(<>{OpenOnMount.render?.({} as never, {} as never)}</>);
|
|
35
|
+
// tamagui renders the sheet through a portal AND inline, so the frame
|
|
36
|
+
// appears more than once with the same accessible name; every copy has to
|
|
37
|
+
// carry the modal semantics.
|
|
38
|
+
const frames = screen.getAllByRole("dialog", { name: "Move to project" });
|
|
39
|
+
expect(frames.length).toBeGreaterThan(0);
|
|
40
|
+
for (const frame of frames) expect(frame.getAttribute("aria-modal")).toBe("true");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("the action row inside the sheet carries cancel and primary", () => {
|
|
44
|
+
renderWithProviders(<>{OpenOnMount.render?.({} as never, {} as never)}</>);
|
|
45
|
+
expect(screen.getAllByText("Cancel").length).toBeGreaterThan(0);
|
|
46
|
+
expect(screen.getAllByText("Move").length).toBeGreaterThan(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("the scrollable story fills the body with rows", () => {
|
|
50
|
+
renderWithProviders(<>{Scrollable.render?.({} as never, {} as never)}</>);
|
|
51
|
+
expect(screen.getAllByText("Project 1").length).toBeGreaterThan(0);
|
|
52
|
+
expect(screen.getAllByText("Project 30").length).toBeGreaterThan(0);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Button } from "@multiplatform.one/forms";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Paragraph, SizableText, YStack } from "tamagui";
|
|
5
|
+
import { ActionBar } from "../actions/ActionBar";
|
|
6
|
+
import { SheetModal } from "./index";
|
|
7
|
+
|
|
8
|
+
function Harness({
|
|
9
|
+
scrollable = false,
|
|
10
|
+
snapPoint,
|
|
11
|
+
startOpen = false,
|
|
12
|
+
}: {
|
|
13
|
+
scrollable?: boolean;
|
|
14
|
+
snapPoint?: number;
|
|
15
|
+
startOpen?: boolean;
|
|
16
|
+
}) {
|
|
17
|
+
const [open, setOpen] = useState(startOpen);
|
|
18
|
+
return (
|
|
19
|
+
<YStack minHeight={480} gap="$3">
|
|
20
|
+
<Button onPress={() => setOpen(true)}>Open sheet</Button>
|
|
21
|
+
<Paragraph size="$2">
|
|
22
|
+
Dismiss by tapping the scrim, dragging to the bottom, or pressing Escape.
|
|
23
|
+
</Paragraph>
|
|
24
|
+
<SheetModal
|
|
25
|
+
open={open}
|
|
26
|
+
onOpenChange={setOpen}
|
|
27
|
+
scrollable={scrollable}
|
|
28
|
+
{...(snapPoint === undefined ? {} : { snapPoint })}
|
|
29
|
+
header={<SizableText fontWeight="600">Move to project</SizableText>}
|
|
30
|
+
>
|
|
31
|
+
{scrollable ? (
|
|
32
|
+
<YStack gap="$2">
|
|
33
|
+
{Array.from({ length: 30 }).map((_, index) => (
|
|
34
|
+
<Paragraph key={index}>Project {index + 1}</Paragraph>
|
|
35
|
+
))}
|
|
36
|
+
</YStack>
|
|
37
|
+
) : (
|
|
38
|
+
<YStack gap="$3">
|
|
39
|
+
<Paragraph>
|
|
40
|
+
The sheet is a dialog region, so the action row inside it caps at two actions.
|
|
41
|
+
</Paragraph>
|
|
42
|
+
<ActionBar
|
|
43
|
+
cancel={
|
|
44
|
+
<Button chromeless onPress={() => setOpen(false)}>
|
|
45
|
+
Cancel
|
|
46
|
+
</Button>
|
|
47
|
+
}
|
|
48
|
+
primary={
|
|
49
|
+
<Button accent onPress={() => setOpen(false)}>
|
|
50
|
+
Move
|
|
51
|
+
</Button>
|
|
52
|
+
}
|
|
53
|
+
/>
|
|
54
|
+
</YStack>
|
|
55
|
+
)}
|
|
56
|
+
</SheetModal>
|
|
57
|
+
</YStack>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const meta: Meta<typeof SheetModal> = {
|
|
62
|
+
title: "Components/SheetModal",
|
|
63
|
+
component: SheetModal,
|
|
64
|
+
parameters: {
|
|
65
|
+
status: { type: "beta" },
|
|
66
|
+
layout: "fullscreen",
|
|
67
|
+
docs: {
|
|
68
|
+
description: {
|
|
69
|
+
component:
|
|
70
|
+
"Modal bottom sheet. Anatomy follows the Apple HIG sheet (grabber, percent detent, swipe to dismiss) with Radix Dialog semantics: `role=dialog`, `aria-modal`, focus moved in, trapped, and returned on close, and Escape dismissal wired here because tamagui's Sheet has no key handling of its own. The frame opens a dialog region, so an ActionBar inside it inherits the two-action cap. Web renders tamagui's Sheet; the native twin is an RN Modal, because a raw Sheet cannot re-present from a trigger tap on device.",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
argTypes: {
|
|
75
|
+
snapPoint: { control: { type: "range", min: 30, max: 95, step: 5 } },
|
|
76
|
+
scrollable: { control: "boolean" },
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
export default meta;
|
|
80
|
+
|
|
81
|
+
type Story = StoryObj<typeof SheetModal>;
|
|
82
|
+
|
|
83
|
+
export const Default: Story = {
|
|
84
|
+
name: "Main",
|
|
85
|
+
render: () => <Harness />,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const Scrollable: Story = {
|
|
89
|
+
name: "Scrollable body",
|
|
90
|
+
render: () => <Harness scrollable />,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const TallDetent: Story = {
|
|
94
|
+
name: "Tall detent",
|
|
95
|
+
parameters: {
|
|
96
|
+
docs: {
|
|
97
|
+
description: {
|
|
98
|
+
story:
|
|
99
|
+
"`snapPoint` is a percent of travel. 92 is the tallest height real callers pass (the command palette); every one of those was a no-op on device until the detent resolver landed.",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
render: () => <Harness snapPoint={92} />,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const OpenOnMount: Story = {
|
|
107
|
+
name: "Open",
|
|
108
|
+
render: () => <Harness startOpen />,
|
|
109
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActionBar was rendered inside DestructiveAction.stories but had no title of
|
|
3
|
+
* its own, so the gallery never listed it (MPO-193). These mount every
|
|
4
|
+
* exported story so an unrenderable one fails here, not in the Kitchen Sink.
|
|
5
|
+
*/
|
|
6
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
7
|
+
import { cleanup, screen } from "@testing-library/react";
|
|
8
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
9
|
+
import meta, { Alignments, CancelNeverDisabled, Default, WithSecondary } from "./ActionBar.stories";
|
|
10
|
+
|
|
11
|
+
afterEach(cleanup);
|
|
12
|
+
|
|
13
|
+
describe("ActionBar stories", () => {
|
|
14
|
+
it("declares the Components/ActionBar title against the component", () => {
|
|
15
|
+
expect(meta.title).toBe("Components/ActionBar");
|
|
16
|
+
expect(meta.component).toBeDefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("Main renders the cancel and primary slots", () => {
|
|
20
|
+
renderWithProviders(
|
|
21
|
+
<>{Default.render?.(Default.args as never, { args: Default.args } as never)}</>,
|
|
22
|
+
);
|
|
23
|
+
expect(screen.getByText("Cancel")).toBeTruthy();
|
|
24
|
+
expect(screen.getByText("Save changes")).toBeTruthy();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("Cancel + secondary + primary renders every slot", () => {
|
|
28
|
+
renderWithProviders(<>{WithSecondary.render?.({} as never, {} as never)}</>);
|
|
29
|
+
expect(screen.getByText("Cancel")).toBeTruthy();
|
|
30
|
+
expect(screen.getByText("Save draft")).toBeTruthy();
|
|
31
|
+
expect(screen.getByText("Preview")).toBeTruthy();
|
|
32
|
+
expect(screen.getByText("Publish")).toBeTruthy();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("Alignments renders one bar per align value", () => {
|
|
36
|
+
renderWithProviders(<>{Alignments.render?.({} as never, {} as never)}</>);
|
|
37
|
+
expect(screen.getAllByText("Confirm")).toHaveLength(3);
|
|
38
|
+
expect(screen.getByText('align="between"')).toBeTruthy();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("the cancel-disabled story still paints a reachable cancel", () => {
|
|
42
|
+
renderWithProviders(<>{CancelNeverDisabled.render?.({} as never, {} as never)}</>);
|
|
43
|
+
expect(screen.getByText("Cancel")).toBeTruthy();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Button } from "@multiplatform.one/forms";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Paragraph, YStack } from "tamagui";
|
|
5
|
+
import { Card } from "../surfaces";
|
|
6
|
+
import { ActionBar, type ActionBarProps } from "./ActionBar";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Presses land in the story itself rather than the Actions panel: the panel's
|
|
10
|
+
* `action()` pulls the Storybook preview runtime into the module graph, which
|
|
11
|
+
* the package's own vitest run cannot load, and the colocated spec mounts
|
|
12
|
+
* these stories for real.
|
|
13
|
+
*/
|
|
14
|
+
function useLastPressed() {
|
|
15
|
+
const [last, setLast] = useState<string | null>(null);
|
|
16
|
+
return {
|
|
17
|
+
last,
|
|
18
|
+
press: (label: string) => () => setLast(label),
|
|
19
|
+
readout: (
|
|
20
|
+
<Paragraph size="$2" color="$color11">
|
|
21
|
+
{last === null ? "No action pressed yet." : `Last pressed: ${last}`}
|
|
22
|
+
</Paragraph>
|
|
23
|
+
),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const meta: Meta<typeof ActionBar> = {
|
|
28
|
+
title: "Components/ActionBar",
|
|
29
|
+
component: ActionBar,
|
|
30
|
+
parameters: {
|
|
31
|
+
status: { type: "beta" },
|
|
32
|
+
docs: {
|
|
33
|
+
description: {
|
|
34
|
+
component:
|
|
35
|
+
"The action row. Slots (`primary`, `secondary`, `cancel`) keep the primary singular by construction and order the row cancel → secondary → primary, so the trailing default lands under the thumb. `align` picks the justification: `end` for dialogs and toolbars, `start` for page-level forms. DG-ACT-05 strips `disabled` off the cancel slot and DEV-warns; DG-ACT-01 DEV-warns on a second primary, including one composed into free children; DG-OVL-04 DEV-warns past two actions inside a dialog region unless `allowManyActions` ejects.",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
argTypes: {
|
|
40
|
+
align: { control: "radio", options: ["start", "end", "between"] },
|
|
41
|
+
allowManyActions: { control: "boolean" },
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
export default meta;
|
|
45
|
+
|
|
46
|
+
type Story = StoryObj<typeof ActionBar>;
|
|
47
|
+
|
|
48
|
+
function Basic(args: Partial<ActionBarProps>) {
|
|
49
|
+
const { press, readout } = useLastPressed();
|
|
50
|
+
return (
|
|
51
|
+
<Card maxWidth={480}>
|
|
52
|
+
<YStack gap="$3">
|
|
53
|
+
{readout}
|
|
54
|
+
<ActionBar
|
|
55
|
+
{...args}
|
|
56
|
+
cancel={
|
|
57
|
+
<Button chromeless onPress={press("Cancel")}>
|
|
58
|
+
Cancel
|
|
59
|
+
</Button>
|
|
60
|
+
}
|
|
61
|
+
primary={
|
|
62
|
+
<Button accent onPress={press("Save changes")}>
|
|
63
|
+
Save changes
|
|
64
|
+
</Button>
|
|
65
|
+
}
|
|
66
|
+
/>
|
|
67
|
+
</YStack>
|
|
68
|
+
</Card>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function FullRow() {
|
|
73
|
+
const { press, readout } = useLastPressed();
|
|
74
|
+
return (
|
|
75
|
+
<Card maxWidth={520}>
|
|
76
|
+
<YStack gap="$3">
|
|
77
|
+
{readout}
|
|
78
|
+
<ActionBar
|
|
79
|
+
cancel={
|
|
80
|
+
<Button chromeless onPress={press("Cancel")}>
|
|
81
|
+
Cancel
|
|
82
|
+
</Button>
|
|
83
|
+
}
|
|
84
|
+
secondary={[
|
|
85
|
+
<Button key="draft" onPress={press("Save draft")}>
|
|
86
|
+
Save draft
|
|
87
|
+
</Button>,
|
|
88
|
+
<Button key="preview" onPress={press("Preview")}>
|
|
89
|
+
Preview
|
|
90
|
+
</Button>,
|
|
91
|
+
]}
|
|
92
|
+
primary={
|
|
93
|
+
<Button accent onPress={press("Publish")}>
|
|
94
|
+
Publish
|
|
95
|
+
</Button>
|
|
96
|
+
}
|
|
97
|
+
/>
|
|
98
|
+
</YStack>
|
|
99
|
+
</Card>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const Default: Story = {
|
|
104
|
+
name: "Main",
|
|
105
|
+
args: { align: "end" },
|
|
106
|
+
render: (args) => <Basic {...args} />,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const WithSecondary: Story = {
|
|
110
|
+
name: "Cancel + secondary + primary",
|
|
111
|
+
render: () => <FullRow />,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const Alignments: Story = {
|
|
115
|
+
render: () => (
|
|
116
|
+
<YStack gap="$4" maxWidth={520}>
|
|
117
|
+
{(["start", "end", "between"] as const).map((align) => (
|
|
118
|
+
<Card key={align}>
|
|
119
|
+
<YStack gap="$2">
|
|
120
|
+
<Paragraph size="$2">{`align="${align}"`}</Paragraph>
|
|
121
|
+
<ActionBar
|
|
122
|
+
align={align}
|
|
123
|
+
cancel={<Button chromeless>Cancel</Button>}
|
|
124
|
+
primary={<Button accent>Confirm</Button>}
|
|
125
|
+
/>
|
|
126
|
+
</YStack>
|
|
127
|
+
</Card>
|
|
128
|
+
))}
|
|
129
|
+
</YStack>
|
|
130
|
+
),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const CancelNeverDisabled: Story = {
|
|
134
|
+
name: "Cancel is never disabled",
|
|
135
|
+
parameters: {
|
|
136
|
+
docs: {
|
|
137
|
+
description: {
|
|
138
|
+
story:
|
|
139
|
+
"DG-ACT-05: the cancel slot is handed `disabled`, the prop is stripped, and DEV logs `[actions] cancel-disabled`. A dismissal a user cannot reach is a trap, so the escape hatch stays live even while the primary is blocked. Open the console to see the warn.",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
render: () => {
|
|
144
|
+
return (
|
|
145
|
+
<Card maxWidth={480}>
|
|
146
|
+
<ActionBar
|
|
147
|
+
cancel={
|
|
148
|
+
<Button chromeless disabled>
|
|
149
|
+
Cancel
|
|
150
|
+
</Button>
|
|
151
|
+
}
|
|
152
|
+
primary={
|
|
153
|
+
<Button accent disabled>
|
|
154
|
+
Save changes
|
|
155
|
+
</Button>
|
|
156
|
+
}
|
|
157
|
+
/>
|
|
158
|
+
</Card>
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
};
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
Children,
|
|
13
|
+
Fragment,
|
|
13
14
|
cloneElement,
|
|
14
15
|
isValidElement,
|
|
15
16
|
useEffect,
|
|
@@ -194,10 +195,16 @@ export function ActionBar({
|
|
|
194
195
|
width="100%"
|
|
195
196
|
{...stackProps}
|
|
196
197
|
>
|
|
198
|
+
{/* `secondaryItems` is a plain array (asArray), so rendering it bare made
|
|
199
|
+
React warn "Each child in a list should have a unique key" and name
|
|
200
|
+
the CALLER — the screen that passed a single element into the slot —
|
|
201
|
+
instead of this file. Key it here, where the array is built. */}
|
|
197
202
|
{hasSlots ? (
|
|
198
203
|
<>
|
|
199
204
|
{cancelItems}
|
|
200
|
-
{secondaryItems
|
|
205
|
+
{secondaryItems.map((node, index) => (
|
|
206
|
+
<Fragment key={`mp-action-secondary-${index}`}>{node}</Fragment>
|
|
207
|
+
))}
|
|
201
208
|
{primary}
|
|
202
209
|
</>
|
|
203
210
|
) : null}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XAxis and YAxis are exported from the charts barrel but had no title of
|
|
3
|
+
* their own — only the composed charts had stories (MPO-193). happy-dom runs
|
|
4
|
+
* no layout, so a measured ChartSurface stays at width 0 and draws nothing;
|
|
5
|
+
* these specs render the same axis frame at an explicit pixel size so the
|
|
6
|
+
* marks the stories show are actually asserted.
|
|
7
|
+
*/
|
|
8
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
9
|
+
import { cleanup, screen } from "@testing-library/react";
|
|
10
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
11
|
+
import { ChartSurface } from "./ChartSurface";
|
|
12
|
+
import { XAxis, YAxis } from "./Axis";
|
|
13
|
+
import { computeCartesianPlot } from "./composeChart";
|
|
14
|
+
import { createBandScale, createLinearScale } from "./math";
|
|
15
|
+
import meta, { AxisLineAndTicks, Default, LabelTruncation, WideValueRange } from "./Axis.stories";
|
|
16
|
+
|
|
17
|
+
afterEach(cleanup);
|
|
18
|
+
|
|
19
|
+
const categories = ["Mon", "Tue", "Wed", "Thu", "Fri"];
|
|
20
|
+
|
|
21
|
+
function Frame({ showGrid = true }: { showGrid?: boolean }) {
|
|
22
|
+
return (
|
|
23
|
+
<ChartSurface label="Axis frame" width={400} height={200}>
|
|
24
|
+
{(size) => {
|
|
25
|
+
const plot = computeCartesianPlot(size, {
|
|
26
|
+
showXAxis: true,
|
|
27
|
+
showYAxis: true,
|
|
28
|
+
yTickLabels: ["0", "25", "50", "75", "100"],
|
|
29
|
+
fontSize: 12,
|
|
30
|
+
});
|
|
31
|
+
const xScale = createBandScale({
|
|
32
|
+
domain: categories,
|
|
33
|
+
range: [plot.x, plot.x + plot.width],
|
|
34
|
+
});
|
|
35
|
+
const yScale = createLinearScale({
|
|
36
|
+
domain: [0, 100],
|
|
37
|
+
range: [plot.y + plot.height, plot.y],
|
|
38
|
+
});
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<YAxis plot={plot} scale={yScale} ticks={4} showGrid={showGrid} />
|
|
42
|
+
<XAxis plot={plot} scale={xScale} />
|
|
43
|
+
</>
|
|
44
|
+
);
|
|
45
|
+
}}
|
|
46
|
+
</ChartSurface>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("Axis stories", () => {
|
|
51
|
+
it("declares the Components/Axis title against the component", () => {
|
|
52
|
+
expect(meta.title).toBe("Components/Axis");
|
|
53
|
+
expect(meta.component).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("every exported story mounts", () => {
|
|
57
|
+
for (const story of [Default, AxisLineAndTicks, LabelTruncation, WideValueRange]) {
|
|
58
|
+
renderWithProviders(<>{story.render?.({} as never, {} as never)}</>);
|
|
59
|
+
cleanup();
|
|
60
|
+
}
|
|
61
|
+
expect(true).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("Long labels truncate renders both container widths", () => {
|
|
65
|
+
renderWithProviders(<>{LabelTruncation.render?.({} as never, {} as never)}</>);
|
|
66
|
+
expect(screen.getByText("520px container")).toBeTruthy();
|
|
67
|
+
expect(screen.getByText("280px container")).toBeTruthy();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("draws one x label per category and the y grid lines at a fixed size", () => {
|
|
71
|
+
const { container } = renderWithProviders(<Frame />);
|
|
72
|
+
const labels = Array.from(container.querySelectorAll("text")).map((node) => node.textContent);
|
|
73
|
+
for (const category of categories) expect(labels).toContain(category);
|
|
74
|
+
// 4-tick hint, plus the x baseline and one tick mark per category.
|
|
75
|
+
expect(container.querySelectorAll("line").length).toBeGreaterThan(categories.length);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("showGrid=false drops the horizontal rules", () => {
|
|
79
|
+
const withGrid = renderWithProviders(<Frame />).container.querySelectorAll("line").length;
|
|
80
|
+
cleanup();
|
|
81
|
+
const withoutGrid = renderWithProviders(<Frame showGrid={false} />).container.querySelectorAll(
|
|
82
|
+
"line",
|
|
83
|
+
).length;
|
|
84
|
+
expect(withoutGrid).toBeLessThan(withGrid);
|
|
85
|
+
});
|
|
86
|
+
});
|