Skip to content

Commit c55429a

Browse files
allisonkingkalilsn
andauthored
Link menu (#1177)
* Add id and class to link attributes * Add link menu based on #1147 Co-authored-by: Kalil Smith-Nuevelle <kalilsn@gmail.com> * Make attribute panel open/close a bit more intuitively * Make link menu update attributes * Add form errors * Add advanced options * Attribute panel opening and closing tests * Add some link tests * Add state to blank story * Fix locators * Fix type * Add failing test * Fix some browser differences * Only autofocus when there is no url * Change the link form to submit on blur so that we always have valid data in the pm doc * Clean up * Clean up some more * Make scroll work kinda * Make panel appear on right properly * Fix browser clicking discrepencies * Update selector for hard breaks test * await expect 🤦 * useRef for container and restore `relative` * Remove extra prop * Add exception in test for webkit CI * Properly pass ref. should make tests more stable --------- Co-authored-by: Kalil Smith-Nuevelle <kalilsn@gmail.com>
1 parent 0951e72 commit c55429a

21 files changed

Lines changed: 703 additions & 49 deletions

packages/context-editor/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"@codemirror/state": "^6.5.2",
7878
"@codemirror/view": "^6.36.4",
7979
"@handlewithcare/react-prosemirror": "catalog:",
80+
"@hookform/resolvers": "catalog:",
8081
"@lezer/cpp": "^1.1.2",
8182
"@lezer/css": "^1.1.10",
8283
"@lezer/html": "^1.3.10",
@@ -88,6 +89,7 @@
8889
"@lezer/python": "^1.1.15",
8990
"@lezer/rust": "^1.0.2",
9091
"@lezer/xml": "^1.0.6",
92+
"@sinclair/typebox": "catalog:",
9193
"deepmerge": "^4.3.1",
9294
"fuzzy": "^0.1.3",
9395
"install": "^0.13.0",
@@ -110,6 +112,8 @@
110112
"react": "catalog:react19",
111113
"react-csv-to-table": "^0.0.4",
112114
"react-dom": "catalog:react19",
115+
"react-hook-form": "catalog:",
116+
"schemas": "workspace:*",
113117
"ui": "workspace:*",
114118
"utils": "workspace:*",
115119
"uuid": "^11.0.4"

packages/context-editor/playwright.config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ export default defineConfig({
4646
projects: [
4747
{
4848
name: "chromium",
49-
use: { ...devices["Desktop Chrome"] },
49+
use: {
50+
...devices["Desktop Chrome"],
51+
contextOptions: { permissions: ["clipboard-write"] },
52+
},
5053
},
5154

5255
{
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import type { Page } from "@playwright/test";
2+
3+
import { expect, test } from "@playwright/test";
4+
5+
import { BLANK_EDITOR_STORY } from "./constants";
6+
7+
const clickNode = async (page: Page, name: string, nth: number = 0) => {
8+
await page.locator(".ProseMirror").getByRole("button", { name }).nth(nth).click();
9+
};
10+
11+
test.describe("attribute panel", () => {
12+
test.beforeEach(async ({ page }) => {
13+
await page.goto(BLANK_EDITOR_STORY);
14+
const editor = page.locator(".ProseMirror");
15+
await editor.click();
16+
});
17+
18+
test.describe("nodes", () => {
19+
test("can open and close the panel on a node", async ({ page }) => {
20+
await test.step("add a paragraph and open panel", async () => {
21+
await page.getByRole("button", { name: "paragraph" }).click();
22+
await page.getByTestId("attribute-panel").waitFor();
23+
});
24+
25+
await test.step("fill out panel while keeping it open", async () => {
26+
const id = "test id";
27+
const className = "test class";
28+
await page.getByRole("textbox", { name: "id" }).fill(id);
29+
await page.getByRole("textbox", { name: "class" }).fill(className);
30+
});
31+
32+
await test.step("can close the panel by clicking the button again", async () => {
33+
await page.getByRole("button", { name: "paragraph" }).click();
34+
await page.getByTestId("attribute-panel").waitFor({ state: "hidden" });
35+
});
36+
});
37+
38+
test("can close a node panel by clicking on text content", async ({ page }) => {
39+
const text = "example";
40+
await test.step("add a paragraph and open panel", async () => {
41+
await page.locator(".ProseMirror").pressSequentially(text);
42+
await clickNode(page, "paragraph");
43+
await page.getByTestId("attribute-panel").waitFor();
44+
});
45+
46+
await test.step("click on other text", async () => {
47+
await page
48+
.locator(".ProseMirror")
49+
.getByText(text)
50+
.click({ position: { x: 0, y: 0 } });
51+
await page.getByTestId("attribute-panel").waitFor({ state: "hidden" });
52+
});
53+
});
54+
55+
test("can switch to a mark attribute panel", async ({ page }) => {
56+
const text = "example";
57+
await test.step("add a paragraph fill in node attributes", async () => {
58+
await page.getByRole("button", { name: "Bold" }).click();
59+
await page.locator(".ProseMirror").pressSequentially(text);
60+
await clickNode(page, "paragraph");
61+
await page.getByTestId("attribute-panel").waitFor();
62+
await expect(page.getByTestId("attribute-panel")).not.toContainText("strong");
63+
const id = "paragraph-id";
64+
await page.getByRole("textbox", { name: "id" }).fill(id);
65+
await expect(page.getByRole("textbox", { name: "id" })).toHaveValue(id);
66+
});
67+
68+
await test.step("click on other text", async () => {
69+
// Add position to make sure we click inside the text
70+
await page
71+
.locator(".ProseMirror")
72+
.getByText(text)
73+
.click({ position: { x: 20, y: 0 } });
74+
await expect(page.getByTestId("attribute-panel")).toContainText("strong");
75+
await expect(page.getByRole("textbox", { name: "id" })).toHaveValue("");
76+
});
77+
});
78+
79+
test("can switch between paragraph nodes", async ({ page }) => {
80+
const editor = page.locator(".ProseMirror");
81+
82+
await test.step("create two paragraphs", async () => {
83+
await editor.pressSequentially("first");
84+
await editor.press("Enter");
85+
await editor.pressSequentially("second");
86+
await expect(page.getByRole("button", { name: "paragraph" })).toHaveCount(2);
87+
});
88+
89+
await test.step("set attrs on first paragraph", async () => {
90+
await clickNode(page, "paragraph", 0);
91+
await page.getByTestId("attribute-panel").waitFor();
92+
const id = "id1";
93+
const className = "class1";
94+
await page.getByRole("textbox", { name: "id" }).fill(id);
95+
await page.getByRole("textbox", { name: "class" }).fill(className);
96+
});
97+
98+
await test.step("open panel on second paragraph", async () => {
99+
await clickNode(page, "paragraph", 1);
100+
// Make sure the second paragraph does not have the same attrs as the first
101+
await expect(page.getByRole("textbox", { name: "id" })).toHaveValue("");
102+
await expect(page.getByRole("textbox", { name: "class" })).toHaveValue("");
103+
});
104+
});
105+
});
106+
107+
test.describe("marks", () => {
108+
test("can open and close the marks panel via keyboard cursor", async ({ page }) => {
109+
const editor = page.locator(".ProseMirror");
110+
await test.step("cursor into a mark", async () => {
111+
await page.getByRole("button", { name: "Bold" }).click();
112+
await editor.pressSequentially("bold");
113+
await expect(page.getByTestId("attribute-panel")).toHaveCount(0);
114+
await editor.press("ArrowLeft");
115+
await page.getByTestId("attribute-panel").waitFor();
116+
});
117+
118+
const suffix = "suffix";
119+
await test.step("add more non-mark'd text", async () => {
120+
await editor.press("ArrowRight");
121+
await page.getByTestId("attribute-panel").waitFor({ state: "hidden" });
122+
await page.getByRole("button", { name: "Bold" }).click();
123+
await editor.pressSequentially(suffix);
124+
});
125+
126+
await test.step("cursor back to mark to open the panel", async () => {
127+
for (let i = 0; i < suffix.length + 1; i++) {
128+
await page.keyboard.press("ArrowLeft");
129+
}
130+
await page.getByTestId("attribute-panel").waitFor();
131+
});
132+
});
133+
134+
test("can close panel by clicking outside of the mark", async ({ page }) => {
135+
const editor = page.locator(".ProseMirror");
136+
const firstParagraph = "first";
137+
const secondParagraph = "second";
138+
await test.step("add bold mark and new (unbolded) node", async () => {
139+
await page.getByRole("button", { name: "Bold" }).click();
140+
await editor.pressSequentially(firstParagraph);
141+
await editor.press("Enter");
142+
await editor.pressSequentially(secondParagraph);
143+
});
144+
145+
await test.step("click between the two to open and close the panel", async () => {
146+
await editor.getByText(firstParagraph).click({ position: { x: 20, y: 0 } });
147+
await page.getByTestId("attribute-panel").waitFor();
148+
await editor.getByText(secondParagraph).click({ position: { x: 20, y: 0 } });
149+
await page.getByTestId("attribute-panel").waitFor({ state: "hidden" });
150+
});
151+
});
152+
153+
test("can click between marks without overwriting attrs", async ({ page, browserName }) => {
154+
const editor = page.locator(".ProseMirror");
155+
156+
await test.step("add bold mark and italic", async () => {
157+
await page.getByRole("button", { name: "Bold" }).click();
158+
await editor.pressSequentially("bold");
159+
await page.getByRole("button", { name: "Bold" }).click();
160+
await editor.pressSequentially(" gap ");
161+
await page.getByRole("button", { name: "Italic" }).click();
162+
await editor.pressSequentially("italic");
163+
});
164+
165+
await test.step("add attrs to italic", async () => {
166+
await editor.press("ArrowLeft");
167+
await page.getByTestId("attribute-panel").waitFor();
168+
await page.getByRole("textbox", { name: "id" }).fill("italic-id");
169+
});
170+
171+
await test.step("add attrs to bold", async () => {
172+
await editor.getByText("bold").click({ position: { x: 20, y: 0 } });
173+
await page.getByTestId("attribute-panel").waitFor();
174+
await page.getByRole("textbox", { name: "id" }).fill("bold-id");
175+
});
176+
177+
await test.step("click between marks", async () => {
178+
const clickOptions =
179+
browserName === "chromium" ? { position: { x: 20, y: 0 } } : undefined;
180+
await editor.getByText("italic").click(clickOptions);
181+
await page.getByTestId("attribute-panel").getByText("em").waitFor();
182+
await expect(page.getByRole("textbox", { name: "id" })).toHaveValue("italic-id");
183+
await editor.getByText("bold").click(clickOptions);
184+
await page.getByTestId("attribute-panel").getByText("strong").waitFor();
185+
await expect(page.getByRole("textbox", { name: "id" })).toHaveValue("bold-id");
186+
});
187+
});
188+
189+
/** This bug has been around for a while. when it is fixed, this test should pass */
190+
test.skip("marks of the same type do not have the same attributes", async ({ page }) => {
191+
const editor = page.locator(".ProseMirror");
192+
193+
await test.step("add two separate bold instances", async () => {
194+
await page.getByRole("button", { name: "Bold" }).click();
195+
await editor.pressSequentially("first");
196+
await page.getByRole("button", { name: "Bold" }).click();
197+
await editor.pressSequentially(" gap ");
198+
await page.getByRole("button", { name: "Bold" }).click();
199+
await editor.pressSequentially("second");
200+
});
201+
202+
await test.step("add attrs to second", async () => {
203+
await editor.press("ArrowLeft");
204+
await page.getByTestId("attribute-panel").waitFor();
205+
await page.getByRole("textbox", { name: "id" }).fill("second-id");
206+
});
207+
208+
await test.step("make sure first does not have the same attr", async () => {
209+
await editor.getByText("first").click({ position: { x: 20, y: 0 } });
210+
await page.getByTestId("attribute-panel").waitFor();
211+
await expect(page.getByRole("textbox", { name: "id" })).toHaveValue("");
212+
});
213+
});
214+
});
215+
});

packages/context-editor/playwright/blocks.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,9 @@ test.describe("hard breaks", () => {
160160
await page.keyboard.type("hello");
161161
await page.keyboard.press("Enter");
162162
await page.keyboard.type("world");
163-
await expect(page.getByText("helloworld")).not.toBeVisible();
164-
await expect(page.getByText("hello")).toBeVisible();
165-
await expect(page.getByText("world")).toBeVisible();
163+
await expect(editor.getByText("helloworld")).not.toBeVisible();
164+
await expect(editor.getByText("hello")).toBeVisible();
165+
await expect(editor.getByText("world")).toBeVisible();
166166
});
167167

168168
await page.keyboard.press("Enter");
@@ -172,7 +172,7 @@ test.describe("hard breaks", () => {
172172
await page.keyboard.press("Shift+Enter");
173173
await page.keyboard.type("earth");
174174
// indicates that the hard break worked, rather than creating a new paragraph
175-
await expect(page.getByText("welcomeearth")).toBeVisible();
175+
await expect(editor.getByText("welcomeearth")).toBeVisible();
176176
});
177177
});
178178
});

0 commit comments

Comments
 (0)