mirror of
https://github.com/bretzel-app/crumbs.git
synced 2026-10-12 12:06:57 +02:00
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:
Vendored
+3
-1
@@ -15,7 +15,9 @@ declare global {
|
|||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface PageState {}
|
interface PageState {
|
||||||
|
noteEditorOpen?: boolean;
|
||||||
|
}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { pushState } from '$app/navigation';
|
||||||
|
import { page } from '$app/state';
|
||||||
import ColorPicker from './ColorPicker.svelte';
|
import ColorPicker from './ColorPicker.svelte';
|
||||||
import Checklist from './Checklist.svelte';
|
import Checklist from './Checklist.svelte';
|
||||||
import FormattingToolbar from './FormattingToolbar.svelte';
|
import FormattingToolbar from './FormattingToolbar.svelte';
|
||||||
@@ -404,8 +406,34 @@
|
|||||||
pointerdownOnOverlay = false;
|
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() {
|
function dismissOverlay() {
|
||||||
closing = true;
|
closing = true;
|
||||||
|
if (historyEntryPushed) {
|
||||||
|
historyEntryPushed = false;
|
||||||
|
history.back();
|
||||||
|
}
|
||||||
setTimeout(() => onClose(), 400);
|
setTimeout(() => onClose(), 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import TagFilter from '$lib/components/TagFilter.svelte';
|
import TagFilter from '$lib/components/TagFilter.svelte';
|
||||||
import { pinnedNotes, unpinnedNotes, selectedTag, currentFilter, notes, notesLoaded, loadNotes, updateSortOrders } from '$lib/stores/notes.js';
|
import { pinnedNotes, unpinnedNotes, selectedTag, currentFilter, notes, notesLoaded, loadNotes, updateSortOrders } from '$lib/stores/notes.js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { replaceState } from '$app/navigation';
|
||||||
import type { Note, NoteFilter } from '$lib/types/index.js';
|
import type { Note, NoteFilter } from '$lib/types/index.js';
|
||||||
import Plus from 'lucide-svelte/icons/plus';
|
import Plus from 'lucide-svelte/icons/plus';
|
||||||
import SquareCheck from 'lucide-svelte/icons/square-check';
|
import SquareCheck from 'lucide-svelte/icons/square-check';
|
||||||
@@ -27,16 +28,19 @@
|
|||||||
loadNotes(filter);
|
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) {
|
function openEditor(note: Note) {
|
||||||
editingNote = note;
|
editingNote = note;
|
||||||
history.replaceState(null, '', `#${note.id}`);
|
replaceState(`#${note.id}`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeEditor() {
|
function closeEditor() {
|
||||||
editingNote = null;
|
editingNote = null;
|
||||||
showNewNote = false;
|
showNewNote = false;
|
||||||
newNoteChecklist = false;
|
newNoteChecklist = false;
|
||||||
history.replaceState(null, '', location.pathname);
|
replaceState(location.pathname, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReorder(noteIds: string[]) {
|
function handleReorder(noteIds: string[]) {
|
||||||
|
|||||||
@@ -23,6 +23,40 @@ test.describe('Notes CRUD', () => {
|
|||||||
await expect(page.getByText('Updated Title')).toBeVisible();
|
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 }) => {
|
test('Scenario: Trashed note disappears from the main view', async ({ authenticatedPage: page }) => {
|
||||||
// Given a note titled "Delete Me" exists
|
// Given a note titled "Delete Me" exists
|
||||||
await createNote(page, 'Delete Me');
|
await createNote(page, 'Delete Me');
|
||||||
|
|||||||
Reference in New Issue
Block a user