mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(auth,settings): clear offline caches on login and tighten settings a11y
Login only wiped the in-memory query cache; the IndexedDB query snapshot and the service-worker data/page caches from the previous account stayed on disk. Logout and the 401/expiry redirect already drop all three. Run the same offline-cache wipe on a successful login too, before the new user's dashboard prefetch warms, so a fresh sign-in on a shared browser inherits nothing from the prior session. The per-account restore guard still stands; this removes the stale data at the source. Settings shell: move focus to the section heading when the active section changes within the mounted shell, so keyboard and screen-reader users are placed in the new content instead of being left on the nav link they activated. The first mount is skipped to avoid an unexpected focus move on a deep link. Raise the inactive sub-tab text from muted-foreground to foreground/70 so it clears the WCAG AA contrast floor over the tinted sub-tab strip.
This commit is contained in:
1 parent
84543ad540
commit
7214b95982
2 files changed
+59
-2
No files matched your search
@@ -22,6 +22,7 @@ import {
|
||||
import { MfaLoginStep, type MfaMethod } from "@/components/auth/mfa-login-step";
|
||||
import { isDashboardSnapshotEnabled } from "@/lib/dashboard/snapshot-flag";
|
||||
import { prefetchDashboardSnapshot } from "@/lib/queries/use-dashboard-snapshot";
|
||||
import { clearOfflineCachesForSessionEnd } from "@/lib/pwa/query-persister";
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
@@ -92,6 +93,16 @@ export default function LoginPage() {
|
||||
// stale `["auth","me"]`, so the shell refetches it fresh on mount and the
|
||||
// former explicit `invalidateQueries({ queryKey: auth() })` is redundant.
|
||||
queryClient.clear();
|
||||
// Belt-and-suspenders to match the session-END wipe (logout / 401-expiry):
|
||||
// also drop the OFFLINE layers — the IndexedDB query snapshot and the SW
|
||||
// data/page caches — so a previous account's persisted dashboard never
|
||||
// lingers on disk for the new session. The restore is already per-account
|
||||
// bound, but a fresh login on a shared browser should leave nothing of the
|
||||
// prior account behind. Order is clear → prefetch → navigate: the wipe is
|
||||
// started here (best-effort, like logout) before the prefetch warms THIS
|
||||
// user's data, and the persister's 1 s debounce gives the single IDB delete
|
||||
// ample headroom to land before the new snapshot is written.
|
||||
void clearOfflineCachesForSessionEnd();
|
||||
const target = getRedirectTarget();
|
||||
if (target === "/" && isDashboardSnapshotEnabled()) {
|
||||
prefetchDashboardSnapshot(queryClient);
|
||||
|
||||
@@ -585,6 +585,39 @@ export function SettingsShell({
|
||||
}
|
||||
: null);
|
||||
|
||||
// v1.25.1 (A11Y-L6) — move focus to the section heading when the active
|
||||
// section changes WITHIN the mounted shell (a sub-tab tap or a rail click;
|
||||
// the shell instance is reused across `/settings/[section]` param changes,
|
||||
// which is also why the mobile-strip effect above keys on `activeSlug`).
|
||||
// Without this, keyboard / screen-reader users stay focused on the nav link
|
||||
// they activated and the new content is never announced. The very first
|
||||
// mount (a deep link or a fresh entry into Settings) is skipped on purpose:
|
||||
// auto-focusing on initial page load is itself a jarring, unexpected focus
|
||||
// move. The heading id is shared by two instances (mobile above the strip,
|
||||
// desktop in the grid) — only one is visible per breakpoint — so we focus
|
||||
// the visible one. `preventScroll` leaves the global scroll-reset in charge.
|
||||
const headingId = resolvedHeading?.headingId;
|
||||
const firstHeadingFocusRef = React.useRef(true);
|
||||
React.useEffect(() => {
|
||||
if (firstHeadingFocusRef.current) {
|
||||
firstHeadingFocusRef.current = false;
|
||||
return;
|
||||
}
|
||||
if (!headingId || typeof document === "undefined") return;
|
||||
const candidates = document.querySelectorAll<HTMLElement>(
|
||||
`[data-settings-heading="${headingId}"]`,
|
||||
);
|
||||
for (const el of candidates) {
|
||||
// `offsetParent === null` ⇒ the element (or an ancestor) is
|
||||
// `display:none` — i.e. the hidden-for-this-breakpoint instance. Skip it
|
||||
// and focus the visible heading.
|
||||
if (el.offsetParent !== null) {
|
||||
el.focus({ preventScroll: true });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [activeSlug, headingId]);
|
||||
|
||||
const headingBlock = resolvedHeading ? (
|
||||
<div className="space-y-6">
|
||||
{resolvedHeading.topSlot ? <div>{resolvedHeading.topSlot}</div> : null}
|
||||
@@ -592,7 +625,14 @@ export function SettingsShell({
|
||||
<div className="min-w-0">
|
||||
<h1
|
||||
id={resolvedHeading.headingId}
|
||||
className="text-2xl font-bold tracking-tight"
|
||||
data-settings-heading={resolvedHeading.headingId}
|
||||
// v1.25.1 (A11Y-L6) — programmatically focusable so an in-shell
|
||||
// section switch can move focus here (see the `activeSlug` effect).
|
||||
// `tabIndex={-1}` keeps it out of the sequential tab order but lets
|
||||
// `.focus()` land, so keyboard / screen-reader users are placed in
|
||||
// the new section's content instead of being left on the nav link.
|
||||
tabIndex={-1}
|
||||
className="text-2xl font-bold tracking-tight outline-none"
|
||||
>
|
||||
{resolvedHeading.title}
|
||||
</h1>
|
||||
@@ -773,7 +813,13 @@ export function SettingsShell({
|
||||
"flex min-h-9 items-center rounded-md px-3 py-1.5 text-sm font-medium whitespace-nowrap transition-colors",
|
||||
isActive
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
: // v1.25.1 (A11Y-L3) — inactive tab text over the
|
||||
// `bg-muted/50` strip. `text-muted-foreground` is tuned
|
||||
// for ~AA on the page background; over the tinted strip
|
||||
// the effective ratio drops below 4.5:1 in light mode.
|
||||
// `text-foreground/70` keeps the de-emphasised look but
|
||||
// clears the WCAG AA floor on the strip.
|
||||
"text-foreground/70 hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{t(`settings.sections.${slug}.title`)}
|
||||
|
||||
Reference in new issue
Block a user