feat(editor): close the note editor with browser back navigation

The mobile editor is fullscreen with no backdrop to tap, so the Android
back button / iOS back swipe now dismisses it instead of leaving the
page. The editor pushes a shallow-routing history entry on mount and
closes when it is popped; UI closes pop the entry themselves so history
stays clean. NotesView's hash rewriting now goes through SvelteKit's
replaceState so the router keeps handling popstate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C1N5Vi12zTez7LNqRvYjUD
This commit is contained in:
Claude
2026-07-15 10:25:54 +02:00
committed by Fred Cabassut
parent d96b20f244
commit 12f45267f7
4 changed files with 71 additions and 3 deletions
+3 -1
View File
@@ -15,7 +15,9 @@ declare global {
} | null;
}
// interface PageData {}
// interface PageState {}
interface PageState {
noteEditorOpen?: boolean;
}
// interface Platform {}
}
}
+28
View File
@@ -4,6 +4,8 @@
<script lang="ts">
import { onMount } from 'svelte';
import { pushState } from '$app/navigation';
import { page } from '$app/state';
import ColorPicker from './ColorPicker.svelte';
import Checklist from './Checklist.svelte';
import FormattingToolbar from './FormattingToolbar.svelte';
@@ -404,8 +406,34 @@
pointerdownOnOverlay = false;
}
// Close on browser back — Android back button / iOS back swipe. The editor is
// fullscreen on mobile with no backdrop to tap, so back must dismiss it instead of
// leaving the page. On mount, push a shallow-routing history entry; navigating back
// pops it and closes the editor. UI closes pop the entry themselves via
// history.back() so no stale entry is left behind.
let historyEntryPushed = false;
onMount(() => {
// Reuse an existing entry (e.g. back-navigating to a note restored from the
// URL hash) instead of stacking a second one.
if (!page.state.noteEditorOpen) {
pushState('', { noteEditorOpen: true });
}
historyEntryPushed = true;
});
$effect(() => {
if (page.state.noteEditorOpen || !historyEntryPushed || closing) return;
historyEntryPushed = false;
saveAndClose();
});
function dismissOverlay() {
closing = true;
if (historyEntryPushed) {
historyEntryPushed = false;
history.back();
}
setTimeout(() => onClose(), 400);
}
+6 -2
View File
@@ -4,6 +4,7 @@
import TagFilter from '$lib/components/TagFilter.svelte';
import { pinnedNotes, unpinnedNotes, selectedTag, currentFilter, notes, notesLoaded, loadNotes, updateSortOrders } from '$lib/stores/notes.js';
import { onMount } from 'svelte';
import { replaceState } from '$app/navigation';
import type { Note, NoteFilter } from '$lib/types/index.js';
import Plus from 'lucide-svelte/icons/plus';
import SquareCheck from 'lucide-svelte/icons/square-check';
@@ -27,16 +28,19 @@
loadNotes(filter);
});
// Use SvelteKit's replaceState (not the raw history API) so the router keeps
// ownership of the entry — NoteEditor's close-on-back relies on popstate being
// handled by SvelteKit.
function openEditor(note: Note) {
editingNote = note;
history.replaceState(null, '', `#${note.id}`);
replaceState(`#${note.id}`, {});
}
function closeEditor() {
editingNote = null;
showNewNote = false;
newNoteChecklist = false;
history.replaceState(null, '', location.pathname);
replaceState(location.pathname, {});
}
function handleReorder(noteIds: string[]) {
+34
View File
@@ -23,6 +23,40 @@ test.describe('Notes CRUD', () => {
await expect(page.getByText('Updated Title')).toBeVisible();
});
test('Scenario: Browser back closes the open editor instead of leaving the page', async ({ authenticatedPage: page }) => {
// Given a note titled "Back Closes Me" exists
await createNote(page, 'Back Closes Me', 'Some content');
// Given the note is open in the editor
await noteCard(page, 'Back Closes Me').click();
await expect(page.getByTestId('note-editor')).toBeVisible();
// When the user navigates back
await page.goBack();
// Then the editor is closed and the notes list is still shown
await expect(page.getByTestId('note-editor')).not.toBeVisible();
await expect(noteCard(page, 'Back Closes Me')).toBeVisible();
});
test('Scenario: Closing the editor leaves browser history clean', async ({ authenticatedPage: page }) => {
// Given a note titled "Clean History" exists
await createNote(page, 'Clean History');
// Given the user opened and closed the note
await noteCard(page, 'Clean History').click();
await expect(page.getByTestId('note-editor')).toBeVisible();
await page.getByTestId('close-editor-btn').click();
await expect(page.getByTestId('note-editor')).not.toBeVisible();
// When the user navigates back
await page.goBack();
// Then the editor does not reopen and the note's URL is gone from history
await expect(page.getByTestId('note-editor')).not.toBeVisible();
expect(page.url()).not.toContain('#');
});
test('Scenario: Trashed note disappears from the main view', async ({ authenticatedPage: page }) => {
// Given a note titled "Delete Me" exists
await createNote(page, 'Delete Me');