Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Leafdown uses lightweight [Keep a Changelog](https://keepachangelog.com/en/1.1.0

### Changed

- Group the editor's context actions in a compact popup outside tables. Icon controls fill their rows, separators distinguish command groups, and short labels name block movement, footnote actions, image replacement, the code language, and a callout's authored disclosure.
- Rewrite the bundled help guides with numbered window illustrations, task-by-task instructions, menu paths and shortcuts, and Markdown examples pairing source with its rendered result. The guides and illustrations work offline and follow the current appearance.
- Redesign the help pages, keyboard shortcuts, What's new, and Changelog as one family. Each opens larger, taking most of the window's height and as much width as its text needs, with its title in a header and its text at 15 px. A help page with several sections, and the Changelog, list them beside the text in a wide window to jump between. What's new alternates its highlights' drawings from side to side, starts each change with its first sentence in bold, and folds a release's fixes behind a count you can open. The Changelog lays out every release the same way, with its fixes listed, and the shortcuts sit in cards side by side.
- Show shortcuts and key hints as raised keycaps across menus, the command palette, the welcome screen, the shortcuts reference, help, and release notes. Keycaps name the platform's keys: Ctrl on Windows and Linux, and modifier symbols on macOS, with full names for screen readers.
Expand Down
2 changes: 2 additions & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ The context popup is a contextual menu triggered by a pointer or keyboard select

The popup offers the actions of the context it opens in, one row or submenu per group, in this order. A command unavailable within its context stays visible but disabled.

Outside tables, rows with one or two actions show text: `Up` and `Down` for block movement, `Rename` and `Go to` for footnotes, `Language...` for a code block's language, and `Opens expanded` for a callout's authored disclosure. Paste and Replace image keep their command labels. The full command name remains the accessible name.

- **Caret:** Paste; Blockquote, Ordered list, Unordered list, Task list; Block type; Insert.
- **Text selection:** Cut, Copy, Paste, Delete; Strong, Emphasis, Inline code, Link; Block type.
- **Structural block selection:** Cut, Copy, Paste, Delete; Move block up, Move block down; Blockquote, Ordered list, Unordered list, Task list; Block type; Insert.
Expand Down
2 changes: 2 additions & 0 deletions docs/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ A named folder is one ordinary root folder with an editable display name, ordere
- **Open from right-click:** commands act on the editor selection established by the right-click.
- **Open from keyboard:** commands act on the caret or selection the request was made from, and the popup holds focus.

Outside tables, the popup is compact and separates command groups with horizontal rules. Icon controls share each row's full width. Rows with one or two actions show short labels, wrapping longer translations within the popup. Accessible names retain the full command name, and a tooltip supplies it where the visible label is shortened. Submenu rows show their icon and label together.

## Editor Model

The editor is a unified hybrid Markdown surface. Behavior is governed by rendering rules, contextual marker visibility, command transformations, and serialization.
Expand Down
27 changes: 27 additions & 0 deletions e2e/desktop/specs/block-selection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,33 @@ describe("desktop block selection", () => {

await dispatchHandleGesture(first);
await expect($("[data-testid='editor-context-popup']")).toBeDisplayed();
const popupRows = await browser.execute(() => {
const popup = document.querySelector<HTMLElement>("[data-testid='editor-context-popup']")!;
const bounds = popup.getBoundingClientRect();
const groups = new Map<string, DOMRect[]>();

for (const control of popup.querySelectorAll<HTMLElement>("[data-toolbar-row]")) {
const key = control.dataset.toolbarRow!;
const row = groups.get(key) ?? [];
row.push(control.getBoundingClientRect());
groups.set(key, row);
}

return [...groups.values()].map((row) => ({
count: row.length,
leadingSpace: row[0].left - bounds.left,
trailingSpace: bounds.right - row.at(-1)!.right,
widths: row.map((control) => control.width),
}));
});
expect(popupRows.some(({ count }) => count === 2)).toBe(true);
for (const row of popupRows) {
expect(row.leadingSpace).toBeGreaterThanOrEqual(0);
expect(row.trailingSpace).toBeGreaterThanOrEqual(0);
expect(row.leadingSpace).toBeLessThanOrEqual(8);
expect(row.trailingSpace).toBeLessThanOrEqual(8);
expect(Math.max(...row.widths) - Math.min(...row.widths)).toBeLessThan(1);
}
const selectedFirstGeometry = await getGeometryForText("Nested first");
expect(Math.abs(selectedFirstGeometry.blockLeft - firstGeometry.blockLeft)).toBeLessThan(0.5);
await expect($("aria/Bold")).not.toExist();
Expand Down
66 changes: 66 additions & 0 deletions src/features/editor/components/editor-context-popup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,71 @@ describe("editor-context-popup", () => {
expect(controlLabels()).toEqual(labels);
});

it("labels sparse rows while keeping the full command names and actions", async () => {
const onExecute = vi.fn();
const { rerender, user } = renderWithUser(
<EditorContextPopup
request={{ ...POINTER_REQUEST, selectionKind: "caret" }}
commandState={withEnabledCommands("edit.renameFootnote", "edit.jumpToFootnoteDefinition")}
onClose={vi.fn()}
onExecute={onExecute}
onReturnFocus={vi.fn()}
/>,
);

expect(screen.getByRole("button", { name: "Paste" })).toHaveTextContent("Paste");
expect(screen.getByRole("button", { name: "Rename footnote" })).toHaveTextContent("Rename");
expect(screen.getByRole("button", { name: "Jump to footnote definition" })).toHaveTextContent(
"Go to",
);
await user.click(screen.getByRole("button", { name: "Jump to footnote definition" }));
expect(onExecute).toHaveBeenCalledWith("edit.jumpToFootnoteDefinition");

rerender(
<EditorContextPopup
request={{ ...POINTER_REQUEST, selectionKind: "block" }}
commandState={withEnabledCommands(
"edit.moveBlockUp",
"edit.moveBlockDown",
"format.codeBlockLanguage",
)}
onClose={vi.fn()}
onExecute={onExecute}
onReturnFocus={vi.fn()}
/>,
);

expect(screen.getByRole("button", { name: "Move block up" })).toHaveTextContent("Up");
expect(screen.getByRole("button", { name: "Move block down" })).toHaveTextContent("Down");
expect(screen.getByRole("button", { name: "Code block language..." })).toHaveTextContent(
"Language...",
);
expect(screen.getByRole("button", { name: "Copy" }).textContent).toBe("");
await user.click(screen.getByRole("button", { name: "Move block down" }));
expect(onExecute).toHaveBeenCalledWith("edit.moveBlockDown");
});

it("keeps sparse table-context rows as icon controls", () => {
render(
<EditorContextPopup
request={{ ...POINTER_REQUEST, container: "table", selectionKind: "caret" }}
commandState={withEnabledCommands("edit.renameFootnote", "edit.jumpToFootnoteDefinition")}
onClose={vi.fn()}
onExecute={vi.fn()}
onReturnFocus={vi.fn()}
/>,
);

for (const name of [
"Paste",
"Rename footnote",
"Jump to footnote definition",
"Delete table",
]) {
expect(screen.getByRole("button", { name }).textContent).toBe("");
}
});

it("places an object's actions directly under the quick actions", () => {
const { rerender } = render(
<EditorContextPopup
Expand Down Expand Up @@ -492,6 +557,7 @@ describe("editor-context-popup", () => {

expect(controlLabels().slice(0, 2)).toEqual(["Paste", "Callout opens expanded"]);
expect(screen.getByLabelText("Callout opens expanded")).toHaveAttribute("aria-pressed", "true");
expect(screen.getByLabelText("Callout opens expanded")).toHaveTextContent("Opens expanded");

await user.click(screen.getByLabelText("Callout opens expanded"));

Expand Down
Loading