Skip to content

Commit 0d2d313

Browse files
authored
fix(admin): support heading levels four through six (#2200)
1 parent 3aabb7b commit 0d2d313

8 files changed

Lines changed: 117 additions & 7 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@emdash-cms/admin": patch
3+
---
4+
5+
Adds heading levels 4 through 6 to the content editor menus and stored content.

packages/admin/src/components/PortableTextEditor.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ import {
5252
TextHOne,
5353
TextHTwo,
5454
TextHThree,
55+
TextHFour,
56+
TextHFive,
57+
TextHSix,
5558
List,
5659
ListNumbers,
5760
Quotes,
@@ -1134,6 +1137,36 @@ const defaultSlashCommands: SlashCommandItem[] = [
11341137
editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
11351138
},
11361139
},
1140+
{
1141+
id: "heading4",
1142+
title: msg`Heading 4`,
1143+
description: msg`Smaller section heading`,
1144+
icon: TextHFour,
1145+
aliases: ["h4"],
1146+
command: ({ editor, range }) => {
1147+
editor.chain().focus().deleteRange(range).setNode("heading", { level: 4 }).run();
1148+
},
1149+
},
1150+
{
1151+
id: "heading5",
1152+
title: msg`Heading 5`,
1153+
description: msg`Minor section heading`,
1154+
icon: TextHFive,
1155+
aliases: ["h5"],
1156+
command: ({ editor, range }) => {
1157+
editor.chain().focus().deleteRange(range).setNode("heading", { level: 5 }).run();
1158+
},
1159+
},
1160+
{
1161+
id: "heading6",
1162+
title: msg`Heading 6`,
1163+
description: msg`Smallest section heading`,
1164+
icon: TextHSix,
1165+
aliases: ["h6"],
1166+
command: ({ editor, range }) => {
1167+
editor.chain().focus().deleteRange(range).setNode("heading", { level: 6 }).run();
1168+
},
1169+
},
11371170
{
11381171
id: "bulletList",
11391172
title: msg`Bullet List`,
@@ -2493,7 +2526,7 @@ export function PortableTextEditor({
24932526
() => [
24942527
StarterKit.configure({
24952528
heading: {
2496-
levels: [1, 2, 3],
2529+
levels: [1, 2, 3, 4, 5, 6],
24972530
},
24982531
dropcursor: {
24992532
color: "#3b82f6",
@@ -3651,7 +3684,7 @@ function EditorToolbar({
36513684

36523685
{/* Headings */}
36533686
<ToolbarGroup>
3654-
<HeadingDropdownMenu editor={editor} levels={[1, 2, 3]} />
3687+
<HeadingDropdownMenu editor={editor} />
36553688
</ToolbarGroup>
36563689

36573690
<ToolbarSeparator />

packages/admin/src/components/editor/BlockMenu.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import {
2121
TextHOne,
2222
TextHTwo,
2323
TextHThree,
24+
TextHFour,
25+
TextHFive,
26+
TextHSix,
2427
Quotes,
2528
Code,
2629
List,
@@ -81,6 +84,30 @@ const blockTransforms: BlockTransform[] = [
8184
editor.chain().focus().setNode("heading", { level: 3 }).run();
8285
},
8386
},
87+
{
88+
id: "heading4",
89+
label: msg`Heading 4`,
90+
icon: TextHFour,
91+
transform: (editor) => {
92+
editor.chain().focus().setNode("heading", { level: 4 }).run();
93+
},
94+
},
95+
{
96+
id: "heading5",
97+
label: msg`Heading 5`,
98+
icon: TextHFive,
99+
transform: (editor) => {
100+
editor.chain().focus().setNode("heading", { level: 5 }).run();
101+
},
102+
},
103+
{
104+
id: "heading6",
105+
label: msg`Heading 6`,
106+
icon: TextHSix,
107+
transform: (editor) => {
108+
editor.chain().focus().setNode("heading", { level: 6 }).run();
109+
},
110+
},
84111
{
85112
id: "blockquote",
86113
label: msg`Quote`,

packages/admin/src/styles.css

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,10 @@ body {
307307
margin-bottom: 0.4em !important;
308308
}
309309

310-
.ProseMirror.prose > h3 {
310+
.ProseMirror.prose > h3,
311+
.ProseMirror.prose > h4,
312+
.ProseMirror.prose > h5,
313+
.ProseMirror.prose > h6 {
311314
margin-top: 0.3em !important;
312315
margin-bottom: 0.3em !important;
313316
}

packages/admin/tests/editor/PortableTextEditor.test.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ function typeIntoEditor(editor: Editor, text: string) {
152152
function textBlock(
153153
text: string,
154154
opts: {
155-
style?: "normal" | "h1" | "h2" | "h3" | "blockquote";
155+
style?: "normal" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "blockquote";
156156
marks?: string[];
157157
listItem?: "bullet" | "number";
158158
level?: number;
@@ -313,6 +313,14 @@ describe("Portable Text ↔ ProseMirror conversion", () => {
313313
expect(h1!.textContent).toBe("Title");
314314
});
315315

316+
it("renders an h6 heading", async () => {
317+
await render(<PortableTextEditor value={[textBlock("Detail", { style: "h6" })]} />);
318+
const pm = await waitForEditor();
319+
const h6 = pm.querySelector("h6");
320+
expect(h6).toBeTruthy();
321+
expect(h6!.textContent).toBe("Detail");
322+
});
323+
316324
it("renders bold text", async () => {
317325
await render(<PortableTextEditor value={[textBlock("Bold text", { marks: ["strong"] })]} />);
318326
const pm = await waitForEditor();
@@ -691,6 +699,9 @@ describe("Toolbar", () => {
691699
await expect.element(screen.getByRole("menuitem", { name: "Heading 1" })).toBeInTheDocument();
692700
await expect.element(screen.getByRole("menuitem", { name: "Heading 2" })).toBeInTheDocument();
693701
await expect.element(screen.getByRole("menuitem", { name: "Heading 3" })).toBeInTheDocument();
702+
await expect.element(screen.getByRole("menuitem", { name: "Heading 4" })).toBeInTheDocument();
703+
await expect.element(screen.getByRole("menuitem", { name: "Heading 5" })).toBeInTheDocument();
704+
await expect.element(screen.getByRole("menuitem", { name: "Heading 6" })).toBeInTheDocument();
694705
});
695706

696707
it("has list buttons", async () => {

packages/admin/tests/editor/block-menu.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,9 @@ describe("BlockMenu", () => {
243243
expect(findButtonByText(updatedMenu, "Heading 1")).toBeTruthy();
244244
expect(findButtonByText(updatedMenu, "Heading 2")).toBeTruthy();
245245
expect(findButtonByText(updatedMenu, "Heading 3")).toBeTruthy();
246+
expect(findButtonByText(updatedMenu, "Heading 4")).toBeTruthy();
247+
expect(findButtonByText(updatedMenu, "Heading 5")).toBeTruthy();
248+
expect(findButtonByText(updatedMenu, "Heading 6")).toBeTruthy();
246249
expect(findButtonByText(updatedMenu, "Quote")).toBeTruthy();
247250
expect(findButtonByText(updatedMenu, "Code Block")).toBeTruthy();
248251
expect(findButtonByText(updatedMenu, "Bullet List")).toBeTruthy();

packages/admin/tests/editor/slash-menu.test.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ describe("Slash Command Menu", () => {
455455
const menu = await waitForSlashMenu();
456456
const items = getSlashMenuItems(menu);
457457

458-
// Default commands: heading1-3, bullet/numbered list, quote, code block, divider, image, section
458+
// Default commands: heading1-6, bullet/numbered list, quote, code block, divider, image, section
459459
expect(items.length).toBeGreaterThanOrEqual(8);
460460
});
461461

@@ -471,6 +471,9 @@ describe("Slash Command Menu", () => {
471471
expect(titles).toContain("Heading 1");
472472
expect(titles).toContain("Heading 2");
473473
expect(titles).toContain("Heading 3");
474+
expect(titles).toContain("Heading 4");
475+
expect(titles).toContain("Heading 5");
476+
expect(titles).toContain("Heading 6");
474477
expect(titles).toContain("Bullet List");
475478
expect(titles).toContain("Numbered List");
476479
expect(titles).toContain("Quote");

packages/admin/tests/editor/toolbar.test.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ function expectAlignmentState(
177177

178178
async function getHeadingMenuItem(
179179
screen: Awaited<ReturnType<typeof render>>,
180-
name: "Heading 1" | "Heading 2" | "Heading 3",
180+
name: "Heading 1" | "Heading 2" | "Heading 3" | "Heading 4" | "Heading 5" | "Heading 6",
181181
) {
182182
const trigger = getToolbarButton(screen, "Headings");
183183
trigger.element().click();
@@ -224,6 +224,9 @@ describe("Toolbar Presence and Structure", () => {
224224
await expect.element(screen.getByRole("menuitem", { name: "Heading 1" })).toBeVisible();
225225
await expect.element(screen.getByRole("menuitem", { name: "Heading 2" })).toBeVisible();
226226
await expect.element(screen.getByRole("menuitem", { name: "Heading 3" })).toBeVisible();
227+
await expect.element(screen.getByRole("menuitem", { name: "Heading 4" })).toBeVisible();
228+
await expect.element(screen.getByRole("menuitem", { name: "Heading 5" })).toBeVisible();
229+
await expect.element(screen.getByRole("menuitem", { name: "Heading 6" })).toBeVisible();
227230
expect(
228231
screen
229232
.getByRole("menuitem", { name: "Heading 1" })
@@ -235,7 +238,16 @@ describe("Toolbar Presence and Structure", () => {
235238
document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
236239
(item) => item.textContent?.trim(),
237240
);
238-
expect(headingLabels).not.toContain("Heading 4");
241+
expect(headingLabels).toEqual(
242+
expect.arrayContaining([
243+
"Heading 1",
244+
"Heading 2",
245+
"Heading 3",
246+
"Heading 4",
247+
"Heading 5",
248+
"Heading 6",
249+
]),
250+
);
239251
});
240252

241253
it("has all list buttons", async () => {
@@ -499,6 +511,19 @@ describe("Formatting Button Toggle States", () => {
499511
});
500512
});
501513

514+
it("Heading 6: click changes to h6", async () => {
515+
const { screen, editor } = await renderEditor();
516+
editor.commands.focus();
517+
518+
const { trigger, item } = await getHeadingMenuItem(screen, "Heading 6");
519+
item.element().click();
520+
521+
await vi.waitFor(() => {
522+
expect(trigger.element().hasAttribute("aria-pressed")).toBe(false);
523+
expect(editor.isActive("heading", { level: 6 })).toBe(true);
524+
});
525+
});
526+
502527
it("Bullet List: click toggles aria-pressed to true", async () => {
503528
const { screen, editor } = await renderEditor();
504529
editor.commands.focus();

0 commit comments

Comments
 (0)