Skip to content

Commit 348a5d2

Browse files
feat: press Enter/Tab in page title focuses editor body (#1137) (#1146)
Co-authored-by: Ona <no-reply@ona.com>
1 parent acc4e81 commit 348a5d2

7 files changed

Lines changed: 134 additions & 10 deletions

File tree

.agents/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ src/
546546
│ ├── emoji-picker.tsx # Floating emoji grid with search, used by page icon picker
547547
│ ├── page-cover.tsx # Page cover image: upload, display, change, remove (saves to pages.cover_url)
548548
│ ├── page-icon.tsx # Page icon display + emoji picker trigger (saves to pages.icon)
549-
│ ├── page-title.tsx # Inline-editable page title (saves on blur/Enter)
549+
│ ├── page-title.tsx # Inline-editable page title (saves on blur/Enter/Tab, advances focus to editor via onAdvance)
550550
│ ├── page-breadcrumb.tsx # Server component: breadcrumb nav (workspace → ancestors → current page)
551551
│ ├── page-backlinks.tsx # Server component: backlinks section (queries page_links, shows linking pages)
552552
│ ├── page-content-client.tsx # Client wrapper: lazy-loads DatabaseViewClient, RowPropertiesHeader, PageViewClient via next/dynamic

.agents/quality.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Tracks code quality per domain. Updated by automations as a side effect of featu
1717
| Infrastructure | A | Sentry (client + server + edge), proxy with session refresh, health endpoint with tests (9 tests), PWA manifest, global error boundary, Supabase clients (browser + server + proxy). JetBrains Mono font correctly configured. Dark-only oklch theme tokens. Migration validation tests (33 tests). API route error-handling consistency tests (6 tests). |
1818
| Auth | A | Sign-in, sign-up, invite accept pages. OAuth sign-in with GitHub and Google fully functional. Auth guard in app layout with redirect. Error boundaries on all auth routes (5 error.tsx files, 22 tests). Loading state for invite page. Typography regression test (2 tests). Sign-in unit tests (10 tests): form validation, error handling, redirect logic, loading state. Sign-up unit tests (11 tests): form validation, error handling, redirect logic, loading state. Auth callback route tests (7 tests). Root page tests (4 tests). OAuth buttons unit tests (10 tests). E2E spec covers form rendering, redirect, and sign-in flow (8 tests). |
1919
| Workspaces | A | Workspace home, settings (name/slug/delete), workspace switcher with create dialog. Slug generation utility with unit tests (12 tests). Settings form unit tests (17 tests): validation, save, slug sanitization, delete confirmation flow, error handling. Create workspace dialog unit tests (5 tests). E2E specs cover workspace creation (3 tests), workspace settings (3 tests), and workspace limit enforcement (2 tests). Max 3 workspace limit enforced via DB trigger. |
20-
| Pages | A | Page view with title + editor, page tree with CRUD + drag-and-drop + nest/unnest. Page menu with export/import markdown. Page icon picker with emoji support. Cover images, backlinks, version history, favorites, trash/soft-delete, page duplication, inline page link search. Keyboard shortcuts for duplicate (⌘D) and export (⌘⇧E). Tree logic extracted to `src/lib/page-tree.ts` with 37 unit tests covering build, reorder, nest, unnest, and drop computation. Page tree actions hook tests (35 tests): create, create-database, duplicate (regular + database), delete with descendant cleanup, move up/down, nest/unnest, toggle favorite with optimistic updates and error rollback. Page tree keyboard shortcut tests (5 tests). Page icon design spec tests (1 test). Page visit error handling tests (2 tests). Persisted tree expansion tests (9 tests). 11 E2E specs: page CRUD (5), sidebar drag (2), page icon (4), page cover (5), page duplicate (4), favorites (7), trash (7), version history (5), page link search (4), page shortcuts (4), backlinks (1) — 48 tests total. |
20+
| Pages | A | Page view with title + editor, page tree with CRUD + drag-and-drop + nest/unnest. Page menu with export/import markdown. Page icon picker with emoji support. Cover images, backlinks, version history, favorites, trash/soft-delete, page duplication, inline page link search. Keyboard shortcuts for duplicate (⌘D) and export (⌘⇧E). Tree logic extracted to `src/lib/page-tree.ts` with 37 unit tests covering build, reorder, nest, unnest, and drop computation. Page tree actions hook tests (35 tests): create, create-database, duplicate (regular + database), delete with descendant cleanup, move up/down, nest/unnest, toggle favorite with optimistic updates and error rollback. Page tree keyboard shortcut tests (5 tests). Page icon design spec tests (1 test). Page visit error handling tests (2 tests). Persisted tree expansion tests (9 tests). 12 E2E specs: page CRUD (5), sidebar drag (2), page icon (4), page cover (5), page duplicate (4), favorites (7), trash (7), version history (5), page link search (4), page shortcuts (4), backlinks (1), title advance (3) — 51 tests total. |
2121
| Editor | A | Full Lexical editor: slash commands, floating toolbar (with font family selector), floating link editor, drag-and-drop blocks, code highlighting, image upload, clipboard image paste, callouts, collapsible/toggle blocks, table support. Markdown import/export with shortcuts. Word count and reading time utility (18 tests). 18 unit test files (162 tests): theme mapping (9), markdown utils (8), design spec compliance (11), Node.contains safety (1), Lexical dispatch safety (1), collapsible toggle (12), callout node (5), image plugin (6), markdown shortcuts (12), emoji picker design spec (7), floating image toolbar (21), image node (19), word count (18), auto-link plugin (12), table serialization (6), save error handling (5), version save error handling (3), save debounce (6). 16 E2E specs (editor-auto-link, editor-auto-save, editor-callout-collapsible, editor-code-paste, editor-drag, editor-focus-mode, editor-font-family, editor-image-paste, editor-image-upload, editor-link, editor-list-indent, editor-markdown-shortcuts, editor-slash-commands, editor-table, editor-toolbar, editor-turn-into — 64 tests total). Auto-save with debounce. Sentry error capture on save failures and image uploads. |
2222
| Search | A | Full-text search via PostgreSQL tsvector + tsquery. API route with integration tests (14 tests) including transient network retry regression tests. Sidebar search component with unit tests (12 tests). Sentry error capture. E2E spec covers search flow (`e2e/search.spec.ts`, 5 tests). |
2323
| Import/Export | A | Markdown export (download .md) and import (parse .md, create page) via page menu and workspace home. Shared `useMarkdownImport` hook. Markdown utils with unit tests (8 tests). E2E specs: import-export flow plus edge cases (`e2e/import-export.spec.ts`, 7 tests), workspace home import (`e2e/workspace-home-import.spec.ts`, 3 tests) — 10 E2E tests total. |
@@ -34,15 +34,15 @@ Tracks code quality per domain. Updated by automations as a side effect of featu
3434
| Category | Files | Tests |
3535
|---|---|---|
3636
| Unit/Integration (Vitest) | 144 | 1953 |
37-
| E2E (Playwright) | 84 | 412 |
38-
| **Total** | **228** | **2365** |
37+
| E2E (Playwright) | 85 | 415 |
38+
| **Total** | **229** | **2368** |
3939

4040
### Test files by domain
4141

4242
- **Auth**: `sign-in/page.test.tsx` (10 tests), `sign-in/error.test.ts` (4 tests), `sign-up/page.test.tsx` (11 tests), `sign-up/error.test.ts` (4 tests), `forgot-password/error.test.ts` (4 tests), `reset-password/error.test.ts` (4 tests), `invite/[token]/error.test.ts` (6 tests), `auth-typography.test.ts` (2 tests), `callback/route.test.ts` (7 tests), `page.test.tsx` (4 tests), `oauth-buttons.test.tsx` (10 tests), `e2e/auth.spec.ts` (8 tests)
4343
- **Workspaces**: `workspace.test.ts` (12 tests), `workspace-settings-form.test.tsx` (17 tests), `create-workspace-dialog.test.tsx` (5 tests), `e2e/workspace.spec.ts` (3 tests), `e2e/workspace-settings.spec.ts` (3 tests), `e2e/workspace-limit.spec.ts` (2 tests)
4444
- **Editor**: `theme.test.ts` (9), `markdown-utils.test.ts` (8), `design-spec-compliance.test.ts` (11), `node-contains-safety.test.ts` (1), `lexical-dispatch-safety.test.ts` (1), `callout-node.test.ts` (5), `collapsible-toggle.test.ts` (12), `image-plugin.test.ts` (6), `markdown-shortcuts.test.ts` (12), `emoji-picker-design-spec.test.ts` (7), `floating-image-toolbar.test.ts` (21), `image-node.test.ts` (19), `word-count.test.ts` (18), `auto-link-plugin.test.ts` (12), `table-serialization.test.ts` (6), `save-error-handling.test.ts` (5), `version-save-error-handling.test.ts` (3), `save-debounce.test.ts` (6), `e2e/editor-auto-link.spec.ts` (5), `e2e/editor-auto-save.spec.ts` (2), `e2e/editor-callout-collapsible.spec.ts` (3), `e2e/editor-code-paste.spec.ts` (2), `e2e/editor-drag.spec.ts` (4), `e2e/editor-focus-mode.spec.ts` (5), `e2e/editor-font-family.spec.ts` (5), `e2e/editor-image-upload.spec.ts` (2), `e2e/editor-image-paste.spec.ts` (2), `e2e/editor-link.spec.ts` (3), `e2e/editor-list-indent.spec.ts` (3), `e2e/editor-markdown-shortcuts.spec.ts` (4), `e2e/editor-slash-commands.spec.ts` (6), `e2e/editor-table.spec.ts` (8), `e2e/editor-toolbar.spec.ts` (4), `e2e/editor-turn-into.spec.ts` (6)
45-
- **Pages**: `page-tree.test.ts` (37 tests), `use-page-tree-actions.test.ts` (35 tests), `page-tree-shortcut.test.tsx` (5 tests), `page-icon-design-spec.test.ts` (1 test), `page-visit.test.ts` (2 tests), `use-persisted-expanded.test.ts` (9 tests), `e2e/page-crud.spec.ts` (5), `e2e/page-icon.spec.ts` (4), `e2e/sidebar-drag.spec.ts` (2), `e2e/page-cover.spec.ts` (5), `e2e/page-duplicate.spec.ts` (4), `e2e/favorites.spec.ts` (7), `e2e/trash.spec.ts` (7), `e2e/version-history.spec.ts` (5), `e2e/page-link-search.spec.ts` (4), `e2e/page-shortcuts.spec.ts` (4), `e2e/backlinks.spec.ts` (1)
45+
- **Pages**: `page-tree.test.ts` (37 tests), `use-page-tree-actions.test.ts` (35 tests), `page-tree-shortcut.test.tsx` (5 tests), `page-icon-design-spec.test.ts` (1 test), `page-visit.test.ts` (2 tests), `use-persisted-expanded.test.ts` (9 tests), `e2e/page-crud.spec.ts` (5), `e2e/page-icon.spec.ts` (4), `e2e/sidebar-drag.spec.ts` (2), `e2e/page-cover.spec.ts` (5), `e2e/page-duplicate.spec.ts` (4), `e2e/favorites.spec.ts` (7), `e2e/trash.spec.ts` (7), `e2e/version-history.spec.ts` (5), `e2e/page-link-search.spec.ts` (4), `e2e/page-shortcuts.spec.ts` (4), `e2e/backlinks.spec.ts` (1), `e2e/title-advance.spec.ts` (3)
4646
- **Search**: `search/route.test.ts` (14 tests), `page-search.test.tsx` (12 tests), `e2e/search.spec.ts` (5 tests)
4747
- **Import/Export**: `e2e/import-export.spec.ts` (7 tests), `e2e/workspace-home-import.spec.ts` (3 tests)
4848
- **Database**: `database.test.ts` (55 tests), `column-helpers.test.ts` (65 tests), `database-duplicate.test.ts` (11 tests), `database-filters.test.ts` (54 tests), `formula.test.ts` (80 tests), `csv-export.test.ts` (38 tests), `property-type-picker.test.ts` (14 tests), `property-type-picker-completeness.test.ts` (5 tests), `table-view-value-format.test.ts` (33 tests), `table-defaults.test.ts` (27 tests), `table-cell.test.tsx` (40 tests), `database-width.test.ts` (3 tests), `database-view-client.test.tsx` (6 tests), `filter-bar.test.tsx` (20 tests), `filter-value-editor.test.tsx` (34 tests), `sort-menu.test.tsx` (19 tests), `rename-property-dialog.test.tsx` (14 tests), `row-properties-header.test.tsx` (17 tests), `view-tabs.test.tsx` (24 tests), `use-database-filters.test.ts` (18 tests), `use-database-properties.test.ts` (20 tests), `use-database-rows.test.ts` (23 tests), `use-database-views.test.ts` (24 tests), `checkbox.test.tsx` (8 tests), `computed.test.tsx` (15 tests), `date.test.tsx` (12 tests), `email.test.tsx` (8 tests), `files.test.tsx` (11 tests), `formula.test.tsx` (6 tests), `multi-select.test.tsx` (10 tests), `number.test.tsx` (14 tests), `person.test.tsx` (9 tests), `phone.test.tsx` (10 tests), `relation.test.tsx` (12 tests), `select-dropdown.test.tsx` (32 tests), `select.test.tsx` (11 tests), `status.test.tsx` (14 tests), `text.test.tsx` (10 tests), `url.test.tsx` (10 tests), `board-view-helpers.test.ts` (18 tests), `board-view.test.tsx` (13 tests), `calendar-keyboard.test.ts` (25 tests), `calendar-view-helpers.test.ts` (35 tests), `calendar-view.test.tsx` (15 tests), `database-empty-state.test.tsx` (14 tests), `gallery-view.test.tsx` (17 tests), `list-keyboard.test.ts` (14 tests), `list-view.test.tsx` (22 tests), `row-count-announcer.test.tsx` (7 tests), `row-count-status-bar.test.tsx` (5 tests), `formula-design-spec.test.ts` (2 tests), `person-skeleton-design-spec.test.ts` (1 test), `calendar-view-design-spec.test.ts` (1 test), `gallery-view-design-spec.test.ts` (3 tests), `filter-sort-toolbar-design-spec.test.ts` (2 tests), `e2e/database-crud.spec.ts` (11 tests), `e2e/database-duplicate.spec.ts` (2), `e2e/database-duplicate-row.spec.ts` (3), `e2e/database-add-property-types.spec.ts` (10), `e2e/database-board.spec.ts` (4), `e2e/database-board-keyboard.spec.ts` (6), `e2e/database-calendar.spec.ts` (4), `e2e/database-calendar-keyboard.spec.ts` (7), `e2e/database-column-reorder.spec.ts` (3), `e2e/database-column-resize.spec.ts` (4), `e2e/database-csv-export.spec.ts` (3), `e2e/database-error-recovery.spec.ts` (4), `e2e/database-files.spec.ts` (4), `e2e/database-filter-keyboard.spec.ts` (6), `e2e/database-filter-types.spec.ts` (4), `e2e/database-formula.spec.ts` (3), `e2e/database-gallery.spec.ts` (4), `e2e/database-gallery-keyboard.spec.ts` (5), `e2e/database-inline.spec.ts` (4), `e2e/database-list.spec.ts` (4), `e2e/database-list-keyboard.spec.ts` (6), `e2e/database-person.spec.ts` (5), `e2e/database-relation.spec.ts` (3), `e2e/database-row-page.spec.ts` (7), `e2e/database-search.spec.ts` (2), `e2e/database-select-options.spec.ts` (6), `e2e/database-table-editor-portal.spec.ts` (4), `e2e/database-table-keyboard.spec.ts` (6), `e2e/database-view-config.spec.ts` (4), `e2e/database-views.spec.ts` (10), `use-row-selection.test.ts` (10 tests), `e2e/database-bulk-select.spec.ts` (7)
@@ -162,5 +162,6 @@ Tracks code quality per domain. Updated by automations as a side effect of featu
162162
| 2026-05-17 | E2E test for page backlinks rendering (#1127). Added 1 new E2E spec: `e2e/backlinks.spec.ts` (1 test) — creates two pages, inserts an inline page link from page A to page B via the slash command, waits for auto-save and page_links sync, navigates to page B and verifies the Backlinks section appears with a link to page A, clicks the backlink and verifies navigation back to page A. Test totals: 144 Vitest files (1953 tests), 83 E2E specs (407 tests). |
163163
| 2026-05-18 | Mobile header page title (#1136). Added `mobile-header-title.tsx` component and 1 new E2E spec: `e2e/mobile-header-title.spec.ts` (5 tests) — workspace name on home, title update on navigation, Settings on settings page, hidden on desktop, truncation styles. Added Storybook stories for MobileHeaderTitle (5 stories) and MobileLongTitle story in AppShell. Test totals: 144 Vitest files (1953 tests), 84 E2E specs (412 tests). |
164164
| 2026-05-18 | Fixed 11 E2E test failures (#1142). Fixed account-deletion tests (hydration timing with retry pattern), database-bulk-select tests (optimistic row ID race condition, aria-checked assertions), database-column-reorder tests (checkbox column offset in getColumnOrder), database-csv-export tests (gridcell count mismatch from checkbox column). Added `data-row-id` attribute to table rows for temp-ID detection. Fixed DeleteAccountSection component to avoid AlertDialogTrigger + controlled open pattern. Updated visual regression baselines. Test totals unchanged: 144 Vitest files (1953 tests), 84 E2E specs (412 tests). |
165+
| 2026-05-18 | Title Enter/Tab focuses editor (#1137). Added `onAdvance` callback to `PageTitle`, wired in `PageViewClient` to focus Lexical editor on Enter/Tab. Added 1 new E2E spec: `e2e/title-advance.spec.ts` (3 tests). Added `WithAdvance` Storybook story and visual regression baseline. Test totals: 144 Vitest files (1953 tests), 85 E2E specs (415 tests). |
165166

166167

e2e/title-advance.spec.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { test, expect } from "./fixtures/auth";
2+
import { navigateToEditorPage } from "./fixtures/editor-helpers";
3+
4+
test.describe("Title → Editor focus transfer", () => {
5+
test("pressing Enter in the title focuses the editor", async ({
6+
authenticatedPage: page,
7+
}) => {
8+
await navigateToEditorPage(page);
9+
10+
const titleInput = page.locator('input[aria-label="Page title"]');
11+
await expect(titleInput).toBeVisible({ timeout: 10_000 });
12+
13+
// Focus the title and type something
14+
await titleInput.click();
15+
await expect(titleInput).toBeFocused();
16+
await page.keyboard.type("Enter Test Title");
17+
18+
// Press Enter — should transfer focus to the editor
19+
await page.keyboard.press("Enter");
20+
21+
const editor = page.locator('[data-lexical-editor="true"]');
22+
await expect(editor).toBeFocused({ timeout: 5_000 });
23+
});
24+
25+
test("pressing Tab in the title focuses the editor", async ({
26+
authenticatedPage: page,
27+
}) => {
28+
await navigateToEditorPage(page);
29+
30+
const titleInput = page.locator('input[aria-label="Page title"]');
31+
await expect(titleInput).toBeVisible({ timeout: 10_000 });
32+
33+
// Focus the title and type something
34+
await titleInput.click();
35+
await expect(titleInput).toBeFocused();
36+
await page.keyboard.type("Tab Test Title");
37+
38+
// Press Tab — should transfer focus to the editor
39+
await page.keyboard.press("Tab");
40+
41+
const editor = page.locator('[data-lexical-editor="true"]');
42+
await expect(editor).toBeFocused({ timeout: 5_000 });
43+
});
44+
45+
test("title is saved before focus transfers on Enter", async ({
46+
authenticatedPage: page,
47+
}) => {
48+
await navigateToEditorPage(page);
49+
50+
const titleInput = page.locator('input[aria-label="Page title"]');
51+
await expect(titleInput).toBeVisible({ timeout: 10_000 });
52+
53+
const testTitle = `Save Test ${Date.now()}`;
54+
await titleInput.click();
55+
await page.keyboard.press("Control+a");
56+
await page.keyboard.type(testTitle);
57+
58+
// Press Enter to save and advance
59+
await page.keyboard.press("Enter");
60+
61+
// Editor should be focused
62+
const editor = page.locator('[data-lexical-editor="true"]');
63+
await expect(editor).toBeFocused({ timeout: 5_000 });
64+
65+
// Reload and verify the title was persisted
66+
await page.reload({ waitUntil: "domcontentloaded" });
67+
const reloadedTitle = page.locator('input[aria-label="Page title"]');
68+
await expect(reloadedTitle).toBeVisible({ timeout: 10_000 });
69+
await expect(reloadedTitle).toHaveValue(testTitle, { timeout: 5_000 });
70+
});
71+
});
11.9 KB
Loading

src/components/page-title.stories.tsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Meta, StoryObj } from "@storybook/react";
2+
import { fn } from "@storybook/test";
23

34
const meta: Meta = {
45
title: "Components/PageTitle",
@@ -8,7 +9,7 @@ export { meta as default };
89

910
type Story = StoryObj;
1011

11-
// PageTitle uses Supabase for persistence. This story renders the
12+
// PageTitle uses Supabase for persistence. These stories render the
1213
// visual appearance without the data layer.
1314
export const Default: Story = {
1415
render: () => (
@@ -47,3 +48,44 @@ export const LongTitle: Story = {
4748
</div>
4849
),
4950
};
51+
52+
/**
53+
* Demonstrates the Enter/Tab → editor focus transfer pattern.
54+
* Press Enter or Tab in the title to see the onAdvance callback fire
55+
* and focus move to the mock editor area below.
56+
*/
57+
export const WithAdvance: Story = {
58+
args: {
59+
onAdvance: fn(),
60+
},
61+
render: (args) => {
62+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
63+
if (e.key === "Enter" || e.key === "Tab") {
64+
e.preventDefault();
65+
(args as { onAdvance: () => void }).onAdvance();
66+
const editor = document.getElementById("mock-editor");
67+
editor?.focus();
68+
}
69+
};
70+
71+
return (
72+
<div className="space-y-4">
73+
<input
74+
type="text"
75+
defaultValue="My Page Title"
76+
placeholder="Untitled"
77+
className="w-full bg-transparent text-3xl font-bold text-foreground placeholder:text-muted-foreground outline-none"
78+
aria-label="Page title"
79+
onKeyDown={handleKeyDown}
80+
/>
81+
<div
82+
id="mock-editor"
83+
tabIndex={0}
84+
className="min-h-[200px] rounded border border-border p-4 text-sm text-muted-foreground outline-none focus:ring-2 focus:ring-ring"
85+
>
86+
Editor area (press Enter or Tab in the title to focus here)
87+
</div>
88+
</div>
89+
);
90+
},
91+
};

0 commit comments

Comments
 (0)