@multiplatform.one/rich-text 7.6.3 → 7.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/RichEditor.native.js +20 -0
- package/dist/cjs/RichEditor.native.js.map +1 -1
- package/dist/cjs/editor/TentapEditor.native.js +18 -0
- package/dist/cjs/editor/TentapEditor.native.js.map +1 -1
- package/dist/esm/RichEditor.native.js +21 -1
- package/dist/esm/RichEditor.native.js.map +1 -1
- package/dist/esm/editor/TentapEditor.native.js +18 -0
- package/dist/esm/editor/TentapEditor.native.js.map +1 -1
- package/dist/jsx/RichEditor.native.js +20 -0
- package/dist/jsx/RichEditor.native.js.map +1 -1
- package/dist/jsx/editor/TentapEditor.native.js +18 -0
- package/dist/jsx/editor/TentapEditor.native.js.map +1 -1
- package/package.json +8 -9
- package/src/RichEditor.native.tsx +19 -1
- package/src/RichEditor.parts.stories.tsx +139 -0
- package/src/editor/SlashCommands.spec.tsx +210 -0
- package/src/editor/TentapEditor.native.tsx +35 -0
- package/src/richEditorPartsStories.spec.tsx +150 -0
- package/src/toolbar/CodeBlockBubble.spec.tsx +219 -0
- package/src/toolbar/FixedToolbar.stories.tsx +224 -0
- package/src/toolbar/ImageBubble.spec.tsx +140 -0
- package/src/toolbar/LinkBubble.spec.tsx +159 -0
- package/src/toolbar/ResizeHandle.spec.tsx +118 -0
- package/src/toolbar/StatusBar.spec.tsx +100 -0
- package/src/toolbar/StatusBar.stories.tsx +146 -0
- package/src/toolbar/TableBubble.spec.tsx +189 -0
- package/src/toolbar/ToolbarButton.stories.tsx +193 -0
- package/src/toolbar/ToolbarGroup.stories.tsx +148 -0
- package/src/toolbar/fixedToolbarStories.spec.tsx +200 -0
- package/src/toolbar/statusBarStories.spec.tsx +86 -0
- package/src/toolbar/toolbarButtonStories.spec.tsx +122 -0
- package/src/toolbar/toolbarGroupStories.spec.tsx +108 -0
- package/types/RichEditor.native.d.ts.map +1 -1
- package/types/editor/TentapEditor.native.d.ts +4 -0
- package/types/editor/TentapEditor.native.d.ts.map +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { fireEvent } from "@testing-library/react";
|
|
3
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
4
|
+
import { RichEditorContext } from "../context";
|
|
5
|
+
import type { EditorCapabilities } from "../types";
|
|
6
|
+
|
|
7
|
+
const bubble = vi.hoisted(() => ({ props: null as Record<string, any> | null }));
|
|
8
|
+
|
|
9
|
+
vi.mock("@tiptap/react/menus", () => ({
|
|
10
|
+
BubbleMenu: (props: any) => {
|
|
11
|
+
bubble.props = props;
|
|
12
|
+
return <div data-testid="bubble-menu">{props.children}</div>;
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// The table menu lives inside a Popover, which never mounts its content in a
|
|
17
|
+
// headless DOM. Flatten the popover so the menu is inspectable; nothing else
|
|
18
|
+
// about tamagui is replaced.
|
|
19
|
+
vi.mock("tamagui", async (importOriginal) => {
|
|
20
|
+
const actual = await importOriginal<Record<string, any>>();
|
|
21
|
+
const Popover: any = ({ children }: any) => <>{children}</>;
|
|
22
|
+
Popover.Trigger = ({ children }: any) => <>{children}</>;
|
|
23
|
+
return { ...actual, Popover };
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
vi.mock("@multiplatform.one/components", async (importOriginal) => ({
|
|
27
|
+
...(await importOriginal<Record<string, unknown>>()),
|
|
28
|
+
Tooltip: ({ children }: any) => children,
|
|
29
|
+
PopoverContent: ({ children }: any) => <div data-testid="table-menu">{children}</div>,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock("@multiplatform.one/forms", async (importOriginal) => ({
|
|
33
|
+
...(await importOriginal<Record<string, unknown>>()),
|
|
34
|
+
Button: (props: any) => (
|
|
35
|
+
<button role="button" aria-label={props["aria-label"]} onClick={props.onPress}>
|
|
36
|
+
{props.children}
|
|
37
|
+
</button>
|
|
38
|
+
),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const { TableBubble } = await import("./TableBubble");
|
|
42
|
+
|
|
43
|
+
function makeEditor(active = true) {
|
|
44
|
+
const calls: Array<[string, ...unknown[]]> = [];
|
|
45
|
+
const chain: any = new Proxy(
|
|
46
|
+
{},
|
|
47
|
+
{
|
|
48
|
+
get:
|
|
49
|
+
(_target, prop: string) =>
|
|
50
|
+
(...args: unknown[]) => {
|
|
51
|
+
calls.push([prop, ...args]);
|
|
52
|
+
return chain;
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
const editor = {
|
|
57
|
+
chain: () => chain,
|
|
58
|
+
getAttributes: () => ({}),
|
|
59
|
+
isActive: (name: string) => name === "table" && active,
|
|
60
|
+
} as any;
|
|
61
|
+
return { editor, calls, names: () => calls.map((c) => c[0]) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function render(editor: any, capabilities?: EditorCapabilities) {
|
|
65
|
+
return renderWithProviders(
|
|
66
|
+
<RichEditorContext.Provider size="$true" compact={false} disabled={false}>
|
|
67
|
+
<TableBubble editor={editor} capabilities={capabilities} />
|
|
68
|
+
</RichEditorContext.Provider>,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Labels of the items inside the Table dropdown, in document order. */
|
|
73
|
+
function menuLabels(container: HTMLElement) {
|
|
74
|
+
const menu = container.querySelector('[data-testid="table-menu"]');
|
|
75
|
+
if (!menu) throw new Error("table menu did not render");
|
|
76
|
+
return Array.from(menu.querySelectorAll("[aria-label]")).map((el) =>
|
|
77
|
+
el.getAttribute("aria-label"),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function pressInMenu(container: HTMLElement, label: string) {
|
|
82
|
+
const menu = container.querySelector('[data-testid="table-menu"]') as HTMLElement;
|
|
83
|
+
const el = menu.querySelector(`[aria-label="${label}"]`);
|
|
84
|
+
if (!el) throw new Error(`no menu item labelled "${label}"`);
|
|
85
|
+
fireEvent.click(el);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const ROW_AND_COLUMN = [
|
|
89
|
+
"Add row above",
|
|
90
|
+
"Add row below",
|
|
91
|
+
"Delete row",
|
|
92
|
+
"Add column before",
|
|
93
|
+
"Add column after",
|
|
94
|
+
"Delete column",
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
describe("TableBubble", () => {
|
|
98
|
+
it("renders nothing at all without an editor", () => {
|
|
99
|
+
const { container } = render(null);
|
|
100
|
+
expect(container.textContent).toBe("");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("shows itself only while the caret is inside a table", () => {
|
|
104
|
+
const inTable = makeEditor(true);
|
|
105
|
+
render(inTable.editor);
|
|
106
|
+
expect(bubble.props?.shouldShow({ editor: inTable.editor })).toBe(true);
|
|
107
|
+
|
|
108
|
+
const outside = makeEditor(false);
|
|
109
|
+
render(outside.editor);
|
|
110
|
+
expect(bubble.props?.shouldShow({ editor: outside.editor })).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("anchors to the table rect, not the caret, so it cannot cover the first row", () => {
|
|
114
|
+
const { editor } = makeEditor();
|
|
115
|
+
render(editor);
|
|
116
|
+
expect(typeof bubble.props?.getReferencedVirtualElement).toBe("function");
|
|
117
|
+
expect(bubble.props?.options).toMatchObject({ placement: "top-start" });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// LC-52: GFM pipe tables are plain grids, so merge and split have no
|
|
121
|
+
// markdown form. The markdown capability must drop exactly those two.
|
|
122
|
+
it("markdown capability removes exactly mergeCells and splitCell", () => {
|
|
123
|
+
const { container } = render(makeEditor().editor, "markdown");
|
|
124
|
+
const labels = menuLabels(container);
|
|
125
|
+
|
|
126
|
+
expect(labels).not.toContain("Merge cells");
|
|
127
|
+
expect(labels).not.toContain("Split cell");
|
|
128
|
+
expect(labels).toContain("Toggle header row");
|
|
129
|
+
expect(labels).toContain("Delete table");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("full capability keeps all four table actions", () => {
|
|
133
|
+
const { container } = render(makeEditor().editor, "full");
|
|
134
|
+
const labels = menuLabels(container);
|
|
135
|
+
|
|
136
|
+
expect(labels).toContain("Toggle header row");
|
|
137
|
+
expect(labels).toContain("Merge cells");
|
|
138
|
+
expect(labels).toContain("Split cell");
|
|
139
|
+
expect(labels).toContain("Delete table");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("defaults to the full surface when no capability is passed", () => {
|
|
143
|
+
const { container } = render(makeEditor().editor);
|
|
144
|
+
expect(menuLabels(container)).toEqual(
|
|
145
|
+
menuLabels(render(makeEditor().editor, "full").container),
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("the capability filter touches table actions only — every row and column action survives", () => {
|
|
150
|
+
const full = menuLabels(render(makeEditor().editor, "full").container);
|
|
151
|
+
const markdown = menuLabels(render(makeEditor().editor, "markdown").container);
|
|
152
|
+
|
|
153
|
+
for (const label of ROW_AND_COLUMN) {
|
|
154
|
+
expect(full).toContain(label);
|
|
155
|
+
expect(markdown).toContain(label);
|
|
156
|
+
}
|
|
157
|
+
expect(full.length - markdown.length).toBe(2);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("the quick buttons add after the current row and column, not before", () => {
|
|
161
|
+
const { editor, names } = makeEditor();
|
|
162
|
+
const { container } = render(editor);
|
|
163
|
+
|
|
164
|
+
fireEvent.click(container.querySelector('[aria-label="Add row"]')!);
|
|
165
|
+
expect(names()).toEqual(["focus", "addRowAfter", "run"]);
|
|
166
|
+
|
|
167
|
+
fireEvent.click(container.querySelector('[aria-label="Add column"]')!);
|
|
168
|
+
expect(names()).toEqual(["focus", "addRowAfter", "run", "focus", "addColumnAfter", "run"]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("each menu item runs its own command", () => {
|
|
172
|
+
const cases: Array<[string, string]> = [
|
|
173
|
+
["Add row above", "addRowBefore"],
|
|
174
|
+
["Delete row", "deleteRow"],
|
|
175
|
+
["Add column before", "addColumnBefore"],
|
|
176
|
+
["Delete column", "deleteColumn"],
|
|
177
|
+
["Toggle header row", "toggleHeaderRow"],
|
|
178
|
+
["Merge cells", "mergeCells"],
|
|
179
|
+
["Split cell", "splitCell"],
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
for (const [label, command] of cases) {
|
|
183
|
+
const { editor, names } = makeEditor();
|
|
184
|
+
const { container } = render(editor, "full");
|
|
185
|
+
pressInMenu(container, label);
|
|
186
|
+
expect(names()).toEqual(["focus", command, "run"]);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storybook stories for ToolbarButton and ToolbarDropdownButton — the two
|
|
3
|
+
* pressables every toolbar cluster is built from.
|
|
4
|
+
*
|
|
5
|
+
* Stories: Main, Active, Unavailable, WithTooltip, DropdownButton, Row
|
|
6
|
+
*
|
|
7
|
+
* Two things are worth seeing here and nowhere else. The pressable is a 44x44
|
|
8
|
+
* target (Axiom 12) while the visible chrome is a 30x30 box inside it, so the
|
|
9
|
+
* strip stays dense without shrinking the hit area. And an unavailable control
|
|
10
|
+
* is never bare-`disabled` (DG-ST-03): it keeps focus, reports `aria-disabled`,
|
|
11
|
+
* dims, drops its hover surface, and loses its tooltip (DG-OVL-05).
|
|
12
|
+
*/
|
|
13
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
14
|
+
import { action } from "@multiplatform.one/storybook";
|
|
15
|
+
import { XStack } from "tamagui";
|
|
16
|
+
import { RichEditorContext } from "../context";
|
|
17
|
+
import { ToolbarFrame } from "../styled";
|
|
18
|
+
import { BoldIcon, ItalicIcon, RedoIcon, StrikeIcon, UndoIcon } from "./icons";
|
|
19
|
+
import { ToolbarButton, ToolbarDropdownButton } from "./ToolbarButton";
|
|
20
|
+
import { ToolbarGroup } from "./ToolbarGroup";
|
|
21
|
+
|
|
22
|
+
export interface ToolbarButtonStoryArgs {
|
|
23
|
+
/** Format is on: the chrome box takes the active surface */
|
|
24
|
+
active: boolean;
|
|
25
|
+
/** Unavailable: aria-disabled, dimmed, no hover surface, no tooltip */
|
|
26
|
+
disabled: boolean;
|
|
27
|
+
/** Shortcut hint on hover; suppressed while unavailable */
|
|
28
|
+
tooltip: string;
|
|
29
|
+
/** Accessible name — required, since the glyph carries no text */
|
|
30
|
+
"aria-label": string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const meta: Meta<ToolbarButtonStoryArgs> = {
|
|
34
|
+
title: "Forms/RichTextEditor/ToolbarButton",
|
|
35
|
+
component: ToolbarButton as never,
|
|
36
|
+
parameters: {
|
|
37
|
+
layout: "padded",
|
|
38
|
+
},
|
|
39
|
+
argTypes: {
|
|
40
|
+
active: { control: "boolean" },
|
|
41
|
+
disabled: { control: "boolean" },
|
|
42
|
+
tooltip: { control: "text" },
|
|
43
|
+
"aria-label": { control: "text" },
|
|
44
|
+
},
|
|
45
|
+
args: {
|
|
46
|
+
active: false,
|
|
47
|
+
disabled: false,
|
|
48
|
+
tooltip: "Bold (⌘B)",
|
|
49
|
+
"aria-label": "Bold",
|
|
50
|
+
},
|
|
51
|
+
decorators: [
|
|
52
|
+
(Story) => (
|
|
53
|
+
<RichEditorContext.Provider size="$true" compact={false} disabled={false}>
|
|
54
|
+
<Story />
|
|
55
|
+
</RichEditorContext.Provider>
|
|
56
|
+
),
|
|
57
|
+
],
|
|
58
|
+
render: (args) => (
|
|
59
|
+
<ToolbarFrame width={80} justifyContent="center">
|
|
60
|
+
<ToolbarButton
|
|
61
|
+
active={args.active}
|
|
62
|
+
disabled={args.disabled}
|
|
63
|
+
tooltip={args.tooltip}
|
|
64
|
+
aria-label={args["aria-label"]}
|
|
65
|
+
onPress={() => action("onPress")(args["aria-label"])}
|
|
66
|
+
>
|
|
67
|
+
<BoldIcon>B</BoldIcon>
|
|
68
|
+
</ToolbarButton>
|
|
69
|
+
</ToolbarFrame>
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default meta;
|
|
74
|
+
type Story = StoryObj<ToolbarButtonStoryArgs>;
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Main (with controls)
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
export const Main: Story = {};
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Active — the format is on, so the chrome box takes the active surface
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
export const Active: Story = {
|
|
87
|
+
args: {
|
|
88
|
+
active: true,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Unavailable — DG-ST-03: aria-disabled and washed, never bare `disabled`
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
export const Unavailable: Story = {
|
|
97
|
+
args: {
|
|
98
|
+
disabled: true,
|
|
99
|
+
"aria-label": "Undo",
|
|
100
|
+
tooltip: "Undo (⌘Z)",
|
|
101
|
+
},
|
|
102
|
+
render: (args) => (
|
|
103
|
+
<ToolbarFrame width={80} justifyContent="center">
|
|
104
|
+
<ToolbarButton
|
|
105
|
+
disabled={args.disabled}
|
|
106
|
+
tooltip={args.tooltip}
|
|
107
|
+
aria-label={args["aria-label"]}
|
|
108
|
+
onPress={() => action("onPress")(args["aria-label"])}
|
|
109
|
+
>
|
|
110
|
+
<UndoIcon />
|
|
111
|
+
</ToolbarButton>
|
|
112
|
+
</ToolbarFrame>
|
|
113
|
+
),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// WithTooltip — the shortcut hint that returns with availability
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
export const WithTooltip: Story = {
|
|
121
|
+
name: "With tooltip",
|
|
122
|
+
args: {
|
|
123
|
+
tooltip: "Strikethrough (⌘⇧X)",
|
|
124
|
+
"aria-label": "Strikethrough",
|
|
125
|
+
},
|
|
126
|
+
render: (args) => (
|
|
127
|
+
<ToolbarFrame width={80} justifyContent="center">
|
|
128
|
+
<ToolbarButton
|
|
129
|
+
tooltip={args.tooltip}
|
|
130
|
+
aria-label={args["aria-label"]}
|
|
131
|
+
onPress={() => action("onPress")(args["aria-label"])}
|
|
132
|
+
>
|
|
133
|
+
<StrikeIcon>S</StrikeIcon>
|
|
134
|
+
</ToolbarButton>
|
|
135
|
+
</ToolbarFrame>
|
|
136
|
+
),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// DropdownButton — the caret variant: fixed height, width follows its label
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
export const DropdownButton: Story = {
|
|
144
|
+
name: "Dropdown button",
|
|
145
|
+
render: () => (
|
|
146
|
+
<ToolbarFrame width={220} justifyContent="center">
|
|
147
|
+
<XStack alignItems="center" gap="$1">
|
|
148
|
+
<ToolbarDropdownButton aria-label="Heading" onPress={() => action("onPress")("Heading")}>
|
|
149
|
+
H1
|
|
150
|
+
</ToolbarDropdownButton>
|
|
151
|
+
<ToolbarDropdownButton active aria-label="Lists" onPress={() => action("onPress")("Lists")}>
|
|
152
|
+
Bullets
|
|
153
|
+
</ToolbarDropdownButton>
|
|
154
|
+
<ToolbarDropdownButton
|
|
155
|
+
disabled
|
|
156
|
+
aria-label="Table"
|
|
157
|
+
onPress={() => action("onPress")("Table")}
|
|
158
|
+
>
|
|
159
|
+
Table
|
|
160
|
+
</ToolbarDropdownButton>
|
|
161
|
+
</XStack>
|
|
162
|
+
</ToolbarFrame>
|
|
163
|
+
),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Row — the four states side by side, in the bar they live in
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
export const Row: Story = {
|
|
171
|
+
render: () => (
|
|
172
|
+
<ToolbarFrame width={320}>
|
|
173
|
+
<XStack alignItems="center" gap="$1" paddingHorizontal="$2">
|
|
174
|
+
<ToolbarGroup>
|
|
175
|
+
<ToolbarButton disabled onPress={() => {}} aria-label="Undo" tooltip="Undo (⌘Z)">
|
|
176
|
+
<UndoIcon />
|
|
177
|
+
</ToolbarButton>
|
|
178
|
+
<ToolbarButton onPress={() => action("onPress")("Redo")} aria-label="Redo">
|
|
179
|
+
<RedoIcon />
|
|
180
|
+
</ToolbarButton>
|
|
181
|
+
</ToolbarGroup>
|
|
182
|
+
<ToolbarGroup showSeparator={false}>
|
|
183
|
+
<ToolbarButton active onPress={() => action("onPress")("Bold")} aria-label="Bold">
|
|
184
|
+
<BoldIcon>B</BoldIcon>
|
|
185
|
+
</ToolbarButton>
|
|
186
|
+
<ToolbarButton onPress={() => action("onPress")("Italic")} aria-label="Italic">
|
|
187
|
+
<ItalicIcon>I</ItalicIcon>
|
|
188
|
+
</ToolbarButton>
|
|
189
|
+
</ToolbarGroup>
|
|
190
|
+
</XStack>
|
|
191
|
+
</ToolbarFrame>
|
|
192
|
+
),
|
|
193
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storybook stories for ToolbarGroup — the toolbar's segmenting wrapper: it
|
|
3
|
+
* lays its buttons out in a row and, unless told otherwise, draws the vertical
|
|
4
|
+
* rule that separates one cluster of controls from the next.
|
|
5
|
+
*
|
|
6
|
+
* Stories: Main, NoSeparator, Hidden, TwoGroups
|
|
7
|
+
*
|
|
8
|
+
* The group has no state of its own, so the controls are its whole surface:
|
|
9
|
+
* `visible` decides whether anything renders at all, `showSeparator` decides
|
|
10
|
+
* whether the trailing rule does.
|
|
11
|
+
*/
|
|
12
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
13
|
+
import { XStack } from "tamagui";
|
|
14
|
+
import { RichEditorContext } from "../context";
|
|
15
|
+
import { ToolbarFrame } from "../styled";
|
|
16
|
+
import { BoldIcon, ItalicIcon, RedoIcon, StrikeIcon, UndoIcon } from "./icons";
|
|
17
|
+
import { ToolbarButton } from "./ToolbarButton";
|
|
18
|
+
import { ToolbarGroup } from "./ToolbarGroup";
|
|
19
|
+
|
|
20
|
+
export interface ToolbarGroupStoryArgs {
|
|
21
|
+
/** Renders nothing at all when false — how the toolbar drops a section */
|
|
22
|
+
visible: boolean;
|
|
23
|
+
/** Draws the trailing vertical rule that ends the cluster */
|
|
24
|
+
showSeparator: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A history cluster: the pair the real toolbar opens with. */
|
|
28
|
+
function HistoryButtons() {
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<ToolbarButton onPress={() => {}} aria-label="Undo">
|
|
32
|
+
<UndoIcon />
|
|
33
|
+
</ToolbarButton>
|
|
34
|
+
<ToolbarButton onPress={() => {}} aria-label="Redo">
|
|
35
|
+
<RedoIcon />
|
|
36
|
+
</ToolbarButton>
|
|
37
|
+
</>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A formatting cluster: the group that follows history in the real toolbar. */
|
|
42
|
+
function FormattingButtons() {
|
|
43
|
+
return (
|
|
44
|
+
<>
|
|
45
|
+
<ToolbarButton active onPress={() => {}} aria-label="Bold">
|
|
46
|
+
<BoldIcon>B</BoldIcon>
|
|
47
|
+
</ToolbarButton>
|
|
48
|
+
<ToolbarButton onPress={() => {}} aria-label="Italic">
|
|
49
|
+
<ItalicIcon>I</ItalicIcon>
|
|
50
|
+
</ToolbarButton>
|
|
51
|
+
<ToolbarButton onPress={() => {}} aria-label="Strikethrough">
|
|
52
|
+
<StrikeIcon>S</StrikeIcon>
|
|
53
|
+
</ToolbarButton>
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const meta: Meta<ToolbarGroupStoryArgs> = {
|
|
59
|
+
title: "Forms/RichTextEditor/ToolbarGroup",
|
|
60
|
+
component: ToolbarGroup as never,
|
|
61
|
+
parameters: {
|
|
62
|
+
layout: "padded",
|
|
63
|
+
},
|
|
64
|
+
argTypes: {
|
|
65
|
+
visible: { control: "boolean" },
|
|
66
|
+
showSeparator: { control: "boolean" },
|
|
67
|
+
},
|
|
68
|
+
args: {
|
|
69
|
+
visible: true,
|
|
70
|
+
showSeparator: true,
|
|
71
|
+
},
|
|
72
|
+
decorators: [
|
|
73
|
+
(Story) => (
|
|
74
|
+
<RichEditorContext.Provider size="$true" compact={false} disabled={false}>
|
|
75
|
+
<Story />
|
|
76
|
+
</RichEditorContext.Provider>
|
|
77
|
+
),
|
|
78
|
+
],
|
|
79
|
+
// The group only reads as a group inside the bar it segments, so every
|
|
80
|
+
// story renders it in a real ToolbarFrame with a neighbour after it.
|
|
81
|
+
render: (args) => (
|
|
82
|
+
<ToolbarFrame maxWidth={420}>
|
|
83
|
+
<XStack alignItems="center" gap="$1" paddingHorizontal="$2">
|
|
84
|
+
<ToolbarGroup visible={args.visible} showSeparator={args.showSeparator}>
|
|
85
|
+
<HistoryButtons />
|
|
86
|
+
</ToolbarGroup>
|
|
87
|
+
<ToolbarGroup showSeparator={false}>
|
|
88
|
+
<FormattingButtons />
|
|
89
|
+
</ToolbarGroup>
|
|
90
|
+
</XStack>
|
|
91
|
+
</ToolbarFrame>
|
|
92
|
+
),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export default meta;
|
|
96
|
+
type Story = StoryObj<ToolbarGroupStoryArgs>;
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Main (with controls) — a group with its trailing rule
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
export const Main: Story = {};
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// NoSeparator — the last group in a bar ends without a rule
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
export const NoSeparator: Story = {
|
|
109
|
+
name: "No separator",
|
|
110
|
+
args: {
|
|
111
|
+
showSeparator: false,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Hidden — visible={false} removes the buttons AND the rule
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
export const Hidden: Story = {
|
|
120
|
+
args: {
|
|
121
|
+
visible: false,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// TwoGroups — the shape the toolbar is actually built from
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
export const TwoGroups: Story = {
|
|
130
|
+
name: "Two groups",
|
|
131
|
+
render: () => (
|
|
132
|
+
<ToolbarFrame maxWidth={420}>
|
|
133
|
+
<XStack alignItems="center" gap="$1" paddingHorizontal="$2">
|
|
134
|
+
<ToolbarGroup>
|
|
135
|
+
<HistoryButtons />
|
|
136
|
+
</ToolbarGroup>
|
|
137
|
+
<ToolbarGroup>
|
|
138
|
+
<FormattingButtons />
|
|
139
|
+
</ToolbarGroup>
|
|
140
|
+
<ToolbarGroup showSeparator={false}>
|
|
141
|
+
<ToolbarButton onPress={() => {}} aria-label="Blockquote">
|
|
142
|
+
<BoldIcon>Q</BoldIcon>
|
|
143
|
+
</ToolbarButton>
|
|
144
|
+
</ToolbarGroup>
|
|
145
|
+
</XStack>
|
|
146
|
+
</ToolbarFrame>
|
|
147
|
+
),
|
|
148
|
+
};
|