@surrealdb/ui 1.2.12 → 1.2.14
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/.claude/launch.json +17 -0
- package/.oxfmtrc.json +27 -0
- package/.oxlintrc.json +23 -0
- package/.zed/settings.json +34 -34
- package/AGENTS.md +25 -19
- package/CLAUDE.md +111 -0
- package/README.md +15 -10
- package/dist/icons.d.ts +4 -0
- package/dist/icons.js +91 -89
- package/dist/icons.js.map +1 -1
- package/dist/ui.css +1 -1
- package/dist/ui.d.ts +259 -1
- package/dist/ui.js +9807 -12265
- package/dist/ui.js.map +1 -1
- package/doc/code-style.md +264 -0
- package/doc/release.md +102 -0
- package/doc/stacker-plan.md +219 -0
- package/package.json +120 -116
- package/release-notes.md +0 -0
- package/res/_mixins.scss +15 -11
- package/tests/_setup/e2e-helpers.tsx +85 -85
- package/tests/_setup/portable-stories.ts +11 -10
- package/tests/e2e/MarkdownEditor/content-blocks.test.tsx +140 -139
- package/tests/e2e/MarkdownEditor/edits.test.tsx +101 -100
- package/tests/e2e/MarkdownEditor/heading-fold.test.tsx +35 -34
- package/tests/e2e/MarkdownEditor/hybrid-widgets.test.tsx +178 -177
- package/tests/e2e/MarkdownEditor/image-on-image.test.tsx +50 -49
- package/tests/e2e/MarkdownEditor/jsx-block-content.test.tsx +195 -194
- package/tests/e2e/MarkdownEditor/jsx-highlight.test.tsx +47 -46
- package/tests/e2e/MarkdownEditor/jsx-inline-badges.test.tsx +35 -34
- package/tests/e2e/MarkdownEditor/jsx-inline-click.test.tsx +39 -38
- package/tests/e2e/MarkdownEditor/jsx-selection.test.tsx +28 -27
- package/tests/e2e/MarkdownEditor/link-placeholder.test.tsx +49 -48
- package/tests/e2e/MarkdownEditor/media-align.test.tsx +40 -39
- package/tests/e2e/MarkdownEditor/media-edit.test.tsx +49 -48
- package/tests/e2e/MarkdownEditor/media-sizing.test.tsx +108 -107
- package/tests/e2e/MarkdownEditor/modes.test.tsx +79 -78
- package/tests/e2e/MarkdownEditor/onimage-parity.test.tsx +29 -28
- package/tests/e2e/MarkdownEditor/regressions.test.tsx +194 -193
- package/tests/e2e/MarkdownEditor/slash-commands.test.tsx +81 -80
- package/tests/e2e/MarkdownEditor/table-click.test.tsx +36 -35
- package/tests/e2e/MarkdownEditor/table-controls.test.tsx +44 -43
- package/tests/e2e/MarkdownEditor/table-format.test.tsx +28 -27
- package/tests/e2e/MarkdownEditor/undo-redo.test.tsx +22 -21
- package/tests/e2e/MarkdownViewer/parity.test.tsx +144 -143
- package/tests/e2e/MarkdownViewer/render.test.tsx +20 -19
- package/tests/e2e/Stacker/stacker.test.tsx +304 -0
- package/tests/unit/Editor/helpers.test.ts +26 -26
- package/tests/unit/MarkdownConformance/error-isolation.test.tsx +26 -25
- package/tests/unit/MarkdownConformance/indentation.test.tsx +20 -19
- package/tests/unit/MarkdownConformance/inline-code-fuzz.test.tsx +255 -0
- package/tests/unit/MarkdownConformance/jsx-attributes.test.tsx +39 -38
- package/tests/unit/MarkdownConformance/jsx-fuzz.test.tsx +122 -122
- package/tests/unit/MarkdownConformance/links.test.tsx +78 -54
- package/tests/unit/MarkdownConformance/render-helpers.tsx +60 -60
- package/tests/unit/MarkdownEditor/code-info.test.ts +58 -58
- package/tests/unit/MarkdownEditor/decorations.test.ts +423 -421
- package/tests/unit/MarkdownEditor/editor-ready.test.ts +22 -22
- package/tests/unit/MarkdownEditor/html-descriptors.test.ts +70 -70
- package/tests/unit/MarkdownEditor/indented-fence.test.ts +80 -80
- package/tests/unit/MarkdownEditor/jsx-attr-scan.test.ts +87 -87
- package/tests/unit/MarkdownEditor/jsx-block-widget.test.ts +51 -51
- package/tests/unit/MarkdownEditor/jsx-tag-grammar.test.ts +63 -63
- package/tests/unit/MarkdownEditor/list-indent.test.ts +67 -67
- package/tests/unit/MarkdownEditor/slash-commands.test.ts +150 -150
- package/tests/unit/MarkdownEditor/table-format.test.ts +67 -67
- package/tests/unit/MarkdownEditor/table.test.ts +96 -96
- package/tests/unit/MarkdownEditor/triggers.test.ts +167 -167
- package/tests/unit/MarkdownEditor/widget-store.test.ts +90 -90
- package/tests/unit/MarkdownViewer/callout.test.tsx +42 -42
- package/tests/unit/MarkdownViewer/code-highlight.test.tsx +41 -41
- package/tests/unit/MarkdownViewer/code-title.test.tsx +45 -45
- package/tests/unit/MarkdownViewer/features.test.tsx +129 -129
- package/tests/unit/MarkdownViewer/headings.test.tsx +28 -28
- package/tests/unit/MarkdownViewer/indented-jsx-children.test.tsx +53 -53
- package/tests/unit/MarkdownViewer/jsx-block-nesting.test.tsx +137 -137
- package/tests/unit/MarkdownViewer/jsx.test.tsx +188 -188
- package/tests/unit/MarkdownViewer/list-bullets.test.tsx +31 -31
- package/tests/unit/MarkdownViewer/list-code.test.tsx +45 -45
- package/tests/unit/MarkdownViewer/preserve-newlines.test.tsx +109 -109
- package/tests/unit/MarkdownViewer/read-time.test.ts +13 -13
- package/tests/unit/MarkdownViewer/renderers.test.tsx +75 -75
- package/tests/unit/MarkdownViewer/runnable.test.tsx +51 -51
- package/tests/unit/MarkdownViewer/ssr.test.tsx +56 -55
- package/tests/unit/MarkdownViewer/syntax-highlighting.test.tsx +43 -42
- package/tests/unit/MarkdownViewer/tabs-fenced-code.test.tsx +120 -120
- package/tests/unit/MarkdownViewer/tabs.test.tsx +83 -82
- package/tests/unit/Stacker/drag.test.ts +21 -0
- package/tests/unit/Stacker/helpers.test.ts +105 -0
- package/tests/unit/Stacker/reducer.test.ts +107 -0
- package/tools/duplicate-vector-mappings.json +141 -141
- package/tools/glass-export-overrides.json +6 -6
- package/tools/icon-parser.ts +41 -40
- package/tools/integration-gradient-exports.json +55 -55
- package/tools/picto-compare.ts +32 -32
- package/tools/picto-merge-urls.ts +19 -19
- package/tools/picto-sync.ts +543 -543
- package/tools/picto-verify-gradients.ts +50 -50
- package/tools/vector-export-nodes.json +975 -975
|
@@ -1,40 +1,41 @@
|
|
|
1
1
|
import { MantineProvider, v8CssVariablesResolver } from "@mantine/core";
|
|
2
2
|
import type { MarkdownComponents } from "@src/lib/markdown";
|
|
3
|
-
import viewerClasses from "@src/lib/markdown/style/viewer.module.scss";
|
|
4
3
|
import { MarkdownViewer } from "@src/primitives/MarkdownViewer";
|
|
5
4
|
import { MANTINE_THEME } from "@src/theme/mantine";
|
|
6
5
|
import { Children, isValidElement, type ReactElement, type ReactNode } from "react";
|
|
7
6
|
import { renderToString } from "react-dom/server";
|
|
8
7
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
9
8
|
|
|
9
|
+
import viewerClasses from "@src/lib/markdown/style/viewer.module.scss";
|
|
10
|
+
|
|
10
11
|
beforeAll(() => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
12
|
+
if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
|
|
13
|
+
Object.defineProperty(window, "matchMedia", {
|
|
14
|
+
writable: true,
|
|
15
|
+
value: (query: string) => ({
|
|
16
|
+
matches: false,
|
|
17
|
+
media: query,
|
|
18
|
+
onchange: null,
|
|
19
|
+
addListener: () => {},
|
|
20
|
+
removeListener: () => {},
|
|
21
|
+
addEventListener: () => {},
|
|
22
|
+
removeEventListener: () => {},
|
|
23
|
+
dispatchEvent: () => false,
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
function shell(node: ReactElement): string {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
return renderToString(
|
|
31
|
+
<MantineProvider
|
|
32
|
+
theme={MANTINE_THEME}
|
|
33
|
+
cssVariablesResolver={v8CssVariablesResolver}
|
|
34
|
+
forceColorScheme="dark"
|
|
35
|
+
>
|
|
36
|
+
{node}
|
|
37
|
+
</MantineProvider>,
|
|
38
|
+
);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
const TABS_MARKDOWN = `<Tabs>
|
|
@@ -55,61 +56,61 @@ fn main() {}
|
|
|
55
56
|
</Tabs>`;
|
|
56
57
|
|
|
57
58
|
describe("MarkdownViewer / Tabs", () => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
59
|
+
it("renders one tab button per TabItem with the correct labels", () => {
|
|
60
|
+
const html = shell(<MarkdownViewer content={TABS_MARKDOWN} />);
|
|
61
|
+
|
|
62
|
+
const tabButtons = html.match(/mantine-Tabs-tab\b/g) ?? [];
|
|
63
|
+
expect(tabButtons).toHaveLength(2);
|
|
64
|
+
|
|
65
|
+
const labels = [...html.matchAll(/mantine-Tabs-tabLabel">([^<]*)</g)].map((m) => m[1]);
|
|
66
|
+
expect(labels).toEqual(["JavaScript", "Rust"]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("renders the active tab's content as markdown (code block highlighted)", () => {
|
|
70
|
+
const html = shell(<MarkdownViewer content={TABS_MARKDOWN} />);
|
|
71
|
+
// A real fenced code block is rendered (not raw text).
|
|
72
|
+
expect(html).toContain('data-language="javascript"');
|
|
73
|
+
// The code is highlighted into spans, so the plain text survives stripping tags.
|
|
74
|
+
const text = html.replace(/<[^>]+>/g, "");
|
|
75
|
+
expect(text).toContain("const x = 42");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("wraps tab sub-content in the shared content-flow class for top-level rhythm", () => {
|
|
79
|
+
const contentFlow = viewerClasses.contentFlow;
|
|
80
|
+
expect(contentFlow).toBeTruthy();
|
|
81
|
+
|
|
82
|
+
const html = shell(<MarkdownViewer content={TABS_MARKDOWN} />);
|
|
83
|
+
|
|
84
|
+
// The class is shared: the viewer root composes it, and the tab body
|
|
85
|
+
// wrapper applies it — so nested content is spaced like top-level content.
|
|
86
|
+
const occurrences = html.split(contentFlow).length - 1;
|
|
87
|
+
expect(occurrences).toBeGreaterThanOrEqual(2);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("exposes bare child elements with their props for custom introspecting components", () => {
|
|
91
|
+
// Mirrors how docs' <ContentTabs> reads each child's label/content via
|
|
92
|
+
// React.Children — the children must be the bare TabItem elements.
|
|
93
|
+
function Group({ children }: { children?: ReactNode }): ReactElement {
|
|
94
|
+
const tabs = Children.toArray(children)
|
|
95
|
+
.filter(isValidElement)
|
|
96
|
+
.map((el) => (el.props as { label?: string }).label ?? "MISSING");
|
|
97
|
+
return <div data-labels={tabs.join("|")} />;
|
|
98
|
+
}
|
|
99
|
+
function Item({ children }: { children?: ReactNode }): ReactElement {
|
|
100
|
+
return <>{children}</>;
|
|
101
|
+
}
|
|
102
|
+
const components: MarkdownComponents = {
|
|
103
|
+
Group: { component: Group, block: true },
|
|
104
|
+
Item: { component: Item, block: true },
|
|
105
|
+
};
|
|
106
|
+
const html = shell(
|
|
107
|
+
<MarkdownViewer
|
|
108
|
+
content={
|
|
109
|
+
'<Group>\n<Item label="One">\n\na\n\n</Item>\n<Item label="Two">\n\nb\n\n</Item>\n</Group>'
|
|
110
|
+
}
|
|
111
|
+
components={components}
|
|
112
|
+
/>,
|
|
113
|
+
);
|
|
114
|
+
expect(html).toContain('data-labels="One|Two"');
|
|
115
|
+
});
|
|
115
116
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { shouldDismiss } from "@src/lib/stacker";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
describe("shouldDismiss", () => {
|
|
5
|
+
it("dismisses on a fast flick towards the edge regardless of distance", () => {
|
|
6
|
+
expect(shouldDismiss(10, 400, 0.6)).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("does not dismiss a slow drag that stays under halfway", () => {
|
|
10
|
+
expect(shouldDismiss(100, 400, 0.1)).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("dismisses once dragged past halfway", () => {
|
|
14
|
+
expect(shouldDismiss(210, 400, 0.1)).toBe(true);
|
|
15
|
+
expect(shouldDismiss(200, 400, 0.1)).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("never dismisses when the extent is unknown and there is no flick", () => {
|
|
19
|
+
expect(shouldDismiss(0, 0, 0.1)).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
coveredEphemeral,
|
|
3
|
+
drawerDepthStyle,
|
|
4
|
+
resolveKind,
|
|
5
|
+
resolveLiveKind,
|
|
6
|
+
topOpened,
|
|
7
|
+
} from "@src/lib/stacker";
|
|
8
|
+
import type { StackerItem } from "@src/lib/stacker";
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
|
|
11
|
+
describe("resolveKind", () => {
|
|
12
|
+
it("uses the explicit variant when provided", () => {
|
|
13
|
+
expect(resolveKind("drawer", false)).toBe("drawer");
|
|
14
|
+
expect(resolveKind("modal", true)).toBe("modal");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("falls back to the viewport when no variant is set", () => {
|
|
18
|
+
expect(resolveKind(undefined, true)).toBe("drawer");
|
|
19
|
+
expect(resolveKind(undefined, false)).toBe("modal");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("resolveLiveKind", () => {
|
|
24
|
+
const drawerFirst: StackerItem[] = [
|
|
25
|
+
{ id: "a", opened: true, settings: { variant: "drawer" } },
|
|
26
|
+
{ id: "b", opened: true, settings: { variant: "modal" } },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
it("returns null for an empty stack", () => {
|
|
30
|
+
expect(resolveLiveKind([], false)).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("follows the first item's variant hint on desktop", () => {
|
|
34
|
+
expect(resolveLiveKind(drawerFirst, false)).toBe("drawer");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("follows the viewport when the first item has no variant", () => {
|
|
38
|
+
const items: StackerItem[] = [{ id: "a", opened: true, settings: {} }];
|
|
39
|
+
expect(resolveLiveKind(items, true)).toBe("drawer");
|
|
40
|
+
expect(resolveLiveKind(items, false)).toBe("modal");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("prefers a stack-level forced kind over the first item and viewport", () => {
|
|
44
|
+
const items: StackerItem[] = [{ id: "b", opened: true, settings: { variant: "modal" } }];
|
|
45
|
+
expect(resolveLiveKind(items, false, "drawer")).toBe("drawer");
|
|
46
|
+
expect(resolveLiveKind(items, true, "modal")).toBe("modal");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("topOpened", () => {
|
|
51
|
+
const items: StackerItem[] = [
|
|
52
|
+
{ id: "a", opened: true, settings: {} },
|
|
53
|
+
{ id: "b", opened: true, settings: {} },
|
|
54
|
+
{ id: "c", opened: false, settings: {} },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
it("returns the top-most open item, skipping closing ones", () => {
|
|
58
|
+
expect(topOpened(items)?.id).toBe("b");
|
|
59
|
+
expect(topOpened([])).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("coveredEphemeral", () => {
|
|
64
|
+
it("returns open ephemeral items that would be covered by a new top", () => {
|
|
65
|
+
const items: StackerItem[] = [
|
|
66
|
+
{ id: "a", opened: true, settings: { ephemeral: true } },
|
|
67
|
+
{ id: "b", opened: true, settings: {} },
|
|
68
|
+
{ id: "c", opened: false, settings: { ephemeral: true } },
|
|
69
|
+
];
|
|
70
|
+
expect(coveredEphemeral(items, "new").map((i) => i.id)).toEqual(["a"]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("excludes the new top id even if that item is ephemeral", () => {
|
|
74
|
+
const items: StackerItem[] = [{ id: "a", opened: true, settings: { ephemeral: true } }];
|
|
75
|
+
expect(coveredEphemeral(items, "a")).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("drawerDepthStyle", () => {
|
|
80
|
+
it("applies no transform at depth 0", () => {
|
|
81
|
+
expect(drawerDepthStyle("right", 0)).toEqual({
|
|
82
|
+
transform: "none",
|
|
83
|
+
transformOrigin: "center",
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("pushes outward from the free edge and scales down per side", () => {
|
|
88
|
+
expect(drawerDepthStyle("right", 1)).toEqual({
|
|
89
|
+
transform: "translateX(-12px) scale(0.96)",
|
|
90
|
+
transformOrigin: "0% 50%",
|
|
91
|
+
});
|
|
92
|
+
expect(drawerDepthStyle("left", 1)).toEqual({
|
|
93
|
+
transform: "translateX(12px) scale(0.96)",
|
|
94
|
+
transformOrigin: "100% 50%",
|
|
95
|
+
});
|
|
96
|
+
expect(drawerDepthStyle("bottom", 2)).toEqual({
|
|
97
|
+
transform: "translateY(-24px) scale(0.92)",
|
|
98
|
+
transformOrigin: "50% 0%",
|
|
99
|
+
});
|
|
100
|
+
expect(drawerDepthStyle("top", 2)).toEqual({
|
|
101
|
+
transform: "translateY(24px) scale(0.92)",
|
|
102
|
+
transformOrigin: "50% 100%",
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { STACKER_INITIAL_STATE, type StackerState, stackerReducer } from "@src/lib/stacker";
|
|
2
|
+
import type { StackerItem } from "@src/lib/stacker";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
function item(id: string): StackerItem {
|
|
6
|
+
return { id, opened: true, settings: { title: id } };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function open(state: StackerState, id: string): StackerState {
|
|
10
|
+
return stackerReducer(state, { type: "OPEN", item: item(id) });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("stackerReducer", () => {
|
|
14
|
+
it("appends overlays in order", () => {
|
|
15
|
+
let state = open(STACKER_INITIAL_STATE, "a");
|
|
16
|
+
state = open(state, "b");
|
|
17
|
+
expect(state.items.map((i) => i.id)).toEqual(["a", "b"]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("no-ops when reopening an already-open id", () => {
|
|
21
|
+
const state = open(STACKER_INITIAL_STATE, "a");
|
|
22
|
+
expect(open(state, "a")).toBe(state);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("revives a closing overlay in place and merges settings", () => {
|
|
26
|
+
let state = open(STACKER_INITIAL_STATE, "a");
|
|
27
|
+
state = stackerReducer(state, { type: "CLOSE", id: "a" });
|
|
28
|
+
expect(state.items[0].opened).toBe(false);
|
|
29
|
+
|
|
30
|
+
state = stackerReducer(state, {
|
|
31
|
+
type: "OPEN",
|
|
32
|
+
item: { id: "a", opened: true, settings: { title: "revived" } },
|
|
33
|
+
});
|
|
34
|
+
expect(state.items).toHaveLength(1);
|
|
35
|
+
expect(state.items[0].opened).toBe(true);
|
|
36
|
+
expect(state.items[0].settings.title).toBe("revived");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("flips opened on CLOSE and is idempotent", () => {
|
|
40
|
+
const state = open(STACKER_INITIAL_STATE, "a");
|
|
41
|
+
const closed = stackerReducer(state, { type: "CLOSE", id: "a" });
|
|
42
|
+
expect(closed.items[0].opened).toBe(false);
|
|
43
|
+
expect(stackerReducer(closed, { type: "CLOSE", id: "a" })).toBe(closed);
|
|
44
|
+
expect(stackerReducer(state, { type: "CLOSE", id: "missing" })).toBe(state);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("removes an item only on REMOVE", () => {
|
|
48
|
+
let state = open(STACKER_INITIAL_STATE, "a");
|
|
49
|
+
state = open(state, "b");
|
|
50
|
+
|
|
51
|
+
state = stackerReducer(state, { type: "REMOVE", id: "b" });
|
|
52
|
+
expect(state.items.map((i) => i.id)).toEqual(["a"]);
|
|
53
|
+
|
|
54
|
+
state = stackerReducer(state, { type: "REMOVE", id: "a" });
|
|
55
|
+
expect(state.items).toHaveLength(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("locks a founding forced variant until the stack empties", () => {
|
|
59
|
+
let state = stackerReducer(STACKER_INITIAL_STATE, {
|
|
60
|
+
type: "OPEN",
|
|
61
|
+
item: {
|
|
62
|
+
id: "ephemeral",
|
|
63
|
+
opened: true,
|
|
64
|
+
settings: { variant: "drawer", ephemeral: true },
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
expect(state.forcedKind).toBe("drawer");
|
|
68
|
+
|
|
69
|
+
state = stackerReducer(state, {
|
|
70
|
+
type: "OPEN",
|
|
71
|
+
item: { id: "next", opened: true, settings: { variant: "modal" } },
|
|
72
|
+
});
|
|
73
|
+
expect(state.forcedKind).toBe("drawer");
|
|
74
|
+
|
|
75
|
+
state = stackerReducer(state, { type: "REMOVE", id: "ephemeral" });
|
|
76
|
+
expect(state.items.map((i) => i.id)).toEqual(["next"]);
|
|
77
|
+
expect(state.forcedKind).toBe("drawer");
|
|
78
|
+
|
|
79
|
+
state = stackerReducer(state, { type: "REMOVE", id: "next" });
|
|
80
|
+
expect(state.forcedKind).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("does not lock kind when the founding overlay has no variant", () => {
|
|
84
|
+
const state = open(STACKER_INITIAL_STATE, "a");
|
|
85
|
+
expect(state.forcedKind).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("closes every open item with CLOSE_ALL", () => {
|
|
89
|
+
let state = open(STACKER_INITIAL_STATE, "a");
|
|
90
|
+
state = open(state, "b");
|
|
91
|
+
const closed = stackerReducer(state, { type: "CLOSE_ALL" });
|
|
92
|
+
expect(closed.items.every((i) => !i.opened)).toBe(true);
|
|
93
|
+
expect(stackerReducer(closed, { type: "CLOSE_ALL" })).toBe(closed);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("merges settings on UPDATE", () => {
|
|
97
|
+
const state = open(STACKER_INITIAL_STATE, "a");
|
|
98
|
+
const updated = stackerReducer(state, {
|
|
99
|
+
type: "UPDATE",
|
|
100
|
+
id: "a",
|
|
101
|
+
patch: { title: "renamed", withCloseButton: false },
|
|
102
|
+
});
|
|
103
|
+
expect(updated.items[0].settings.title).toBe("renamed");
|
|
104
|
+
expect(updated.items[0].settings.withCloseButton).toBe(false);
|
|
105
|
+
expect(stackerReducer(state, { type: "UPDATE", id: "missing", patch: {} })).toBe(state);
|
|
106
|
+
});
|
|
107
|
+
});
|