Gap cursor for block-edge positions (#335)
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m52s
CI / Build container images (pull_request) Successful in 1m13s
CI / Auth e2e pack (pull_request) Successful in 9m25s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CI / Lint, typecheck, test (push) Has been cancelled
CD / Build and push images (push) Has been cancelled
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m52s
CI / Build container images (pull_request) Successful in 1m13s
CI / Auth e2e pack (pull_request) Successful in 9m25s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CI / Lint, typecheck, test (push) Has been cancelled
CD / Build and push images (push) Has been cancelled
A table (or any other block node without a text position of its own) as the page's first, last, or only block was unreachable from before/after: neither mouse nor arrow keys could place the cursor there, so no paragraph could be created around it. - add the prosemirror-gapcursor plugin as a TipTap extension (via @tiptap/pm, no new dependency; schema-neutral, so the editorSchema drift fence is unaffected) - style the gap cursor bar in base.css -- the upstream package does not ship its stylesheet through our import path; the blink animation honors prefers-reduced-motion - e2e: keyboard-only round trip that creates paragraphs before and after a lone table Closes #335 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aoPvnakfBP28nAfijgUY9
This commit is contained in:
parent
7e17a2dba6
commit
69563348ca
@ -60,6 +60,42 @@ test('typing persists across reload and undo/redo work', async ({ browser }) =>
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('gap cursor reaches positions before and after a lone table (issue #335)', async ({
|
||||
browser,
|
||||
}) => {
|
||||
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
|
||||
const { pondSlug, pageSlug } = await createPage(context, `E2E Gapcursor ${Date.now()}`);
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto(`/p/${pondSlug}/${pageSlug}`);
|
||||
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
||||
const status = page.locator('.editor-connection');
|
||||
await expect(status).toHaveAttribute('data-status', 'connected', { timeout: 10000 });
|
||||
|
||||
const content = page.locator('.ProseMirror');
|
||||
await content.click();
|
||||
await page.getByRole('button', { name: /insert table|tabelle einfügen/i }).click();
|
||||
await expect(content.locator('table')).toBeVisible();
|
||||
// Inserting into the empty page replaces the placeholder paragraph — the
|
||||
// table really is the only block, which is the situation of issue #335.
|
||||
await expect(content.locator(':scope > p')).toHaveCount(0);
|
||||
|
||||
// Keyboard only: ArrowUp from the first cell lands on the gap cursor
|
||||
// before the table; typing there materializes a paragraph.
|
||||
await content.locator('th').first().click();
|
||||
await page.keyboard.press('ArrowUp');
|
||||
await page.keyboard.type('above');
|
||||
await expect(content.locator(':scope > :first-child')).toHaveText('above');
|
||||
|
||||
// Same for the position after the table.
|
||||
await content.locator('td').last().click();
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await page.keyboard.type('below');
|
||||
await expect(content.locator(':scope > :last-child')).toHaveText('below');
|
||||
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('edit mode hides the sidebar; leaving edit mode restores it', async ({ browser }) => {
|
||||
const context = await contextForUser(browser, BASE_URL, 'fixture-user');
|
||||
const { pondSlug, pageSlug } = await createPage(context, `E2E Sidebar ${Date.now()}`);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { AnyExtension } from '@tiptap/core';
|
||||
|
||||
import { GapCursor } from './gap-cursor';
|
||||
import { MarkdownClipboard } from './markdown-clipboard';
|
||||
import { Bold, CodeMark, Italic, LinkMark, Strikethrough } from './marks';
|
||||
import { Image } from './nodes/image';
|
||||
@ -63,4 +64,5 @@ export const documentExtensions: AnyExtension[] = [
|
||||
Strikethrough,
|
||||
LinkMark,
|
||||
MarkdownClipboard,
|
||||
GapCursor,
|
||||
];
|
||||
|
||||
20
apps/web/src/editor/gap-cursor.ts
Normal file
20
apps/web/src/editor/gap-cursor.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Extension } from '@tiptap/core';
|
||||
import { gapCursor } from '@tiptap/pm/gapcursor';
|
||||
|
||||
/**
|
||||
* Cursor position adjacent to block nodes that offer no text position of
|
||||
* their own — without it a table (or code block, image, …) as the page's
|
||||
* first, last, or only block is unreachable from before/after, and no
|
||||
* paragraph can be created there (issue #335). Wraps prosemirror-gapcursor,
|
||||
* which also handles the arrow-key navigation into the gap positions; the
|
||||
* bar itself is styled in `styles/base.css` (`.ProseMirror-gapcursor`)
|
||||
* because the upstream package does not ship its stylesheet through this
|
||||
* entry point.
|
||||
*/
|
||||
export const GapCursor = Extension.create({
|
||||
name: 'gapCursor',
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [gapCursor()];
|
||||
},
|
||||
});
|
||||
@ -1732,6 +1732,42 @@ button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Gap cursor (issue #335): the blinking bar prosemirror-gapcursor renders at
|
||||
positions adjacent to block nodes without a text position of their own
|
||||
(e.g. a table as the page's only block). The upstream package does not
|
||||
ship its stylesheet through our import path, so these rules replace it. */
|
||||
.ProseMirror-gapcursor {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ProseMirror-gapcursor::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
width: 20px;
|
||||
border-top: 1px solid var(--color-text);
|
||||
animation: dt-gapcursor-blink 1.1s steps(2, start) infinite;
|
||||
}
|
||||
|
||||
@keyframes dt-gapcursor-blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-focused .ProseMirror-gapcursor {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ProseMirror-gapcursor::after {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-content h1,
|
||||
.editor-content h2,
|
||||
.editor-content h3,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user