fix(nav): replace static back-link hrefs with history.back() + fallback #303
Reference in New Issue
Block a user
Delete Branch "feat/issue-185-fix-nav-history-back"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #185
Summary
<BackButton>component (src/lib/components/BackButton.svelte) that callshistory.back(), with a fixed "Zurück" label, ≥ 44px touch target, and focus ring<a href>back links across persons, documents, admin, and enrich routespersons/new,documents/edit,admin/users) intentionally left as static links — they are form cancel actions, not back navigationnotifications/+page.sveltedropped from scope — file no longer exists (renamed to/aktivitaeten)<BackButton>form🏛️ Markus Keller — Senior Application Architect
Verdict: ✅ Approved
The extraction is exactly right.
history.back()was duplicated across 7+ files — now it lives in one place. The component is appropriately thin: no props, no state, no abstraction leakage.CLAUDE.mdis updated with the new rule so future developers won't re-introduce static back links.Suggestions
admin/users/[id]inconsistency (+page.svelte:28): The mobile panel back button usesonclick={() => history.back()}inline rather than<BackButton>. The whole point of the component is to own this behavior in one place. Right now there are two implementations. If this button can't use<BackButton>because of layout constraints (md:hidden,mr-3), the fix is to makeBackButtonaccept aclassprop:Then callers can override:
<BackButton class="mr-3 md:hidden" />. The CLAUDE.md note says "do not use a static<a href>for back navigation" — it doesn't yet say "don't inlinehistory.back()either." Worth closing that gap.The pattern is now documented in CLAUDE.md — good. The note about "Label is always 'Zurück' (no contextual suffix)" is the right call given the discussion.
👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ✅ Approved
Clean TDD: unit tests exist for all three behaviors, E2E tests cover real navigation and accessibility. Component is 25 lines and does one thing. The refactor across 6 pages is consistent.
Suggestions
Weak touch-target unit test (
BackButton.svelte.spec.ts:24-27):This checks that the class string is present — not that the element renders at 44px. The E2E test at
back-button.spec.ts:38-40does the real measurement (boundingBox().height). Consider dropping the unit test for touch target to avoid false confidence from a CSS-class check, or upgrade it:admin/users/[id]is an inline duplicate (+page.svelte:26-33): The mobile panel button re-implementshistory.back()inline instead of using<BackButton>. Since the new CLAUDE.md rule says to always use the component, this is a violation of the pattern we just established. The mobile-only style (md:hidden,mr-3) could be handled via aclassprop onBackButton.No test for the admin panel button: Because it's not using
BackButton, it's outsideBackButton.svelte.spec.ts's coverage. If someone removes theonclickfrom that button, nothing fails.🔒 Nora Steiner (NullX) — Application Security Engineer
Verdict: ✅ Approved
history.back()is a read-only browser API. No user input, no dynamic HTML insertion, no external URLs, no attack surface. This PR reduces attack surface by replacing<a href>elements (which could theoretically be influenced by URL manipulation in some routing edge cases) with a pure browser history call.Checked
innerHTML,eval(), ordocument.write()calls ✅aria-label={m.btn_back()}✅ — screen readers and users understand the action before activating itBackButtonhas visible text via{m.btn_back()}— no aria-label needed since text is present ✅Nothing to flag from a security perspective.
🧪 Sara Holt — QA Engineer & Test Strategist
Verdict: ⚠️ Approved with concerns
Test coverage is present at both unit and E2E layers — that's good. But there are a few reliability gaps worth addressing.
Concerns
Touch-target unit test tests markup, not behavior (
BackButton.svelte.spec.ts:24-27):This passes even if the CSS class has no effect (e.g., a Tailwind config issue, a class name typo that compiles silently). The E2E test at
back-button.spec.ts:38-40measures the actual rendered height and is the real gate. This unit test should either be removed (to avoid false confidence) or upgraded to check actual DOM size usingvitest-browser-svelte's browser context.[data-hydrated]as a wait condition in E2E (back-button.spec.ts:8, 33, 45): All three E2E tests doawait page.waitForSelector('[data-hydrated]'). If this attribute is ever removed or renamed in the SvelteKit layout, all three tests will hang at the timeout threshold with a cryptic error. Worth adding a comment pointing to where this attribute is emitted, or switching to a more stable condition like waiting for the back button to be visible:admin/users/[id]inline button has no test: The mobile panel button at+page.svelte:26-33uses inlinehistory.back()— it is not covered byBackButton.svelte.spec.ts. If theonclickis accidentally removed, no test catches it.Axe scan covers only
/persons/new: BackButton appears on 6+ pages. The WCAG scan runs on one. Consider adding a scan on/documents/[id]/edittoo since that page's topbar context is different (BackButton inside a flex snippet) — contrast or focus ring might render differently there.🎨 Leonie Voss — UX Design Lead & Accessibility Strategist
Verdict: ✅ Approved
The component hits all the accessibility fundamentals correctly, and the semantic shift from
<a>to<button>is the right call for a non-navigation action.What's good
min-h-[44px]touch target — meets WCAG 2.2 minimum ✅focus-visible:ring-2 focus-visible:ring-focus-ring— correct project token, keyboard users can see focus ✅aria-hidden="true"on the decorative arrow SVG ✅{m.btn_back()}→ "Zurück" — screen readers read it natively, no separatearia-labelneeded ✅<button>not<a>— correct semantic element for a browser history action ✅group-hover:-translate-x-1) — clear affordance, consistent with existing back links ✅One concern
mb-4is hardcoded into the component (BackButton.svelte:7): The component bakesmb-4into its class string. This works on standalone pages, but in the{#snippet topbar()}contexts (enrich/[id]/+page.svelte,documents/[id]/edit/+page.svelte), the topbar is a flex row —mb-4adds bottom margin in a context where vertical spacing is managed by the parent flex container, which is semantically wrong even if it doesn't visually break anything today.Consider making the outer spacing configurable:
Then standalone pages keep
mb-4by default, and topbar contexts can passclass=""or a different value. This also resolves theadmin/users/[id]layout concern raised by other reviewers.⚙️ Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
No infrastructure, CI, or Docker Compose changes in this PR. The new E2E test file (
frontend/e2e/back-button.spec.ts) will be picked up by the existing Playwright configuration automatically — no pipeline changes needed.Checked
One note
The E2E tests wait on
[data-hydrated]as a readiness signal. That's fine as long as it's consistently emitted — just make sure this pattern is documented somewhere so future test authors don't invent different wait conditions for the same thing. A comment in the test file pointing to wheredata-hydratedis set in the layout would suffice.Review Cycle 1 — Changes
Addressed
BackButtonnow accepts aclassprop (default'mb-4') — removes the hardcoded margin from the component; callers in flex-row topbar contexts now passclass=""— commit367dcc66mb-4is applied by default, and one that asserts theclassprop overrides it — commit367dcc66back-button.spec.tsexplaining what emits[data-hydrated]and why tests wait on it — commit367dcc66Not addressed in this PR (out of scope or by design)
admin/users/[id]mobile panel button remains an inlineonclick={() => history.back()}— the visual design is icon-only,md:hidden, usestext-ink-3, no uppercase/tracking — too different fromBackButtonto force the component here without making BackButton generic. Theclassprop does allow<BackButton class="mr-3 md:hidden" />if the team decides to align it later; that's a follow-on decision./documents/[id]/edit) — requires a logged-in session in E2E; deferred to avoid expanding E2E scope in this PR.Re-running review cycle 2.
🏛️ Markus Keller — Senior Application Architect (Cycle 2)
Verdict: ✅ Approved
The
classprop withmb-4as default closes the hardcoded-margin concern. The abstraction is clean: one canonical implementation, callers opt out of the margin when the layout context demands it. Theadmin/users/[id]exception is documented and the reasoning is sound — the panel button is visually and semantically distinct enough that forcing it throughBackButtonwould make the component generic in the wrong way.No remaining architectural concerns.
👨💻 Felix Brandt — Senior Fullstack Developer (Cycle 2)
Verdict: ✅ Approved
The class prop tests are correct TDD: one test for the default (
mb-4), one for the override (nomb-4, hasmr-3). The weak className-string test for touch target is gone — E2E owns the actual measurement. All four unit tests are behavioral.The
class=""in topbar contexts is idiomatic Svelte 5 — no concerns there.LGTM.
🔒 Nora Steiner (NullX) — Application Security Engineer (Cycle 2)
Verdict: ✅ Approved
The
classprop change introduces no new attack surface. No dynamic content flows through the prop, no user-controlled input, no XSS vectors.No security concerns this cycle.
🧪 Sara Holt — QA Engineer & Test Strategist (Cycle 2)
Verdict: ✅ Approved
My cycle 1 concerns are addressed:
[data-hydrated]comment added, explaining what emits it and why tests wait on itRemaining deferred items (axe on a second page, admin panel unit test) are noted in the PR comment with clear rationale. Acceptable for this scope.
LGTM.
🎨 Leonie Voss — UX Design Lead & Accessibility Strategist (Cycle 2)
Verdict: ✅ Approved
The
mb-4hardcode concern is resolved. Theclassprop withmb-4default means standalone page contexts are unchanged, and the two topbar contexts (documents/[id]/edit,enrich/[id]) correctly passclass=""to suppress the bottom margin inside their flex-row container.The component is semantically clean in all rendering contexts now.
No remaining concerns.
⚙️ Tobias Wendt — DevOps & Platform Engineer (Cycle 2)
Verdict: ✅ Approved
No infrastructure changes. The
[data-hydrated]comment in the E2E spec is a small but useful addition for maintainability.LGTM from infrastructure side.