Skip to content

a11y(2.2.1): ui-main — warn the user before session expiry and offer an extend affordance#3531

Draft
rosanusi wants to merge 2 commits into
mainfrom
wcag/2.2.1-session-timeout-warning
Draft

a11y(2.2.1): ui-main — warn the user before session expiry and offer an extend affordance#3531
rosanusi wants to merge 2 commits into
mainfrom
wcag/2.2.1-session-timeout-warning

Conversation

@rosanusi

@rosanusi rosanusi commented Jun 10, 2026

Copy link
Copy Markdown

Description & motivation 💭

The ui-main auth flow silently refreshes the access token on every 401. When the refresh token itself expires, refreshTokens() returns false and handleError hard-redirected the user to the login page via window.location.assign() — with no warning, no opportunity to extend, and any in-flight form state (Start Workflow, Cancel/Terminate confirmations, Schedule create/edit) destroyed.

SC 2.2.1 (Timing Adjustable, Level A) requires at least one of: turn off the limit, allow adjustment to ≥10×, or warn with ≥20s to extend. None of the three was met.

This PR implements the warn-with-extend path:

  1. JWT exp decodingsetAuthUser now decodes the exp claim from the access token and schedules a warning timer 60 s before expiry. The timer is cancelled and rescheduled on every successful token refresh.
  2. Session warning storesessionWarningState (idle | warning | expired) is the single source of truth for the modal, avoids coupling UI to auth internals.
  3. SessionWarningModal component — a role="alertdialog" <dialog> that opens on warning state with a live countdown (aria-live="polite"), a Stay signed in primary button (calls refreshTokens(); on failure shifts to expired state), and a Sign out secondary button. On expired state the modal shifts to "Your session has expired" with a Sign in again button. No auto-redirect — the user controls the navigation so form state in the background remains intact.
  4. handleError no longer hard-redirects on 401 — calls triggerSessionExpired() instead, making the modal the single session-end decision point. 403 Forbidden still redirects.

Files changed (6):

  • src/lib/stores/session-warning.ts — new store (sessionWarningState, triggerSessionExpired, dismissSessionWarning)
  • src/lib/stores/auth-user.ts — JWT exp decoding, warning timer scheduling/cancellation in setAuthUser/clearAuthUser
  • src/lib/utilities/handle-error.ts — 401 path calls triggerSessionExpired() instead of window.location.assign()
  • src/lib/components/session-warning-modal.svelte — new alertdialog modal
  • src/lib/i18n/locales/en/common.ts — 7 new i18n keys
  • src/routes/(app)/+layout.svelte — mounts SessionWarningModal

SC 2.2.1 Timing Adjustable (Level A) — current verdict: Fails → Supports

Screenshots (if applicable) 📸

New modal appears 60 s before access-token expiry with a countdown, Stay signed in, and Sign out. If no action is taken the countdown reaches zero and the modal shifts to the expired state.

Design Considerations 🎨

Copy for modal title, body, and button labels is a placeholder — needs designer review before merge, especially the countdown phrasing which affects screen-reader announcement cadence. i18n keys are in common.ts so copy can be updated without a code change.

Testing 🧪

How was this tested 👻

  • Manual testing

Steps for others to test: 🚶🏽‍♂️🚶🏽‍♀️

  1. Configure the Go server with a short refresh-token TTL (e.g., 5 min). Log in. Confirm the session-warning modal appears 60 s before the access token expires.
  2. Confirm the modal is announced as alertdialog by VoiceOver/NVDA. Confirm the countdown is announced via aria-live="polite".
  3. Click Stay signed in inside the warning window. Confirm the modal closes and re-arms for the next expiry.
  4. Click Stay signed in more than 10 times across multiple cycles. Confirm each extension succeeds.
  5. Let the countdown reach zero with no action. Confirm the modal shifts to "Your session has expired" — no auto-redirect before user clicks.
  6. Open Start Workflow, fill fields, let the session expire. Confirm form fields remain visible behind the modal so values can be copied before re-authenticating.
  7. Active-user regression: continuous API activity keeps the warning from appearing (silent refresh keeps exp > 60 s ahead).
  8. Confirm handleError no longer issues window.location.assign on 401.
  9. Keyboard-only: Tab cycles between the two buttons; focus is trapped inside the modal.
  10. axe-core on a page with the modal open: zero new violations.

Checklists

Draft Checklist

  • Blocked on design review — modal copy (title, body, button labels) needs designer sign-off before merge
  • Backend coordination — confirm refresh-token TTL with auth team; if TTL < 5 min, discuss whether 60 s lead is appropriate
  • VoiceOver + NVDA smoke test on modal
  • Short-TTL integration test

Merge Checklist

  • Design copy approved
  • Auth team has confirmed refresh-token TTL
  • Screen reader announcement verified (countdown aria-live cadence)
  • axe-core: zero new violations
  • Active-user regression: warning never fires under normal load

Issue(s) closed

Closes finding from audit-output/issues/2.2.1-timing-adjustable-verification.md Section 3.

Docs

Any docs updates needed?

No doc changes needed.

A11y-Audit-Ref: 2.2.1-ui-main-session-timeout-warning

Silent 401-on-refresh-token-expiry hard-redirected to login, destroying
in-flight form state and giving users no warning or extension opportunity.
Add JWT exp decoding to schedule a 60s warning modal (alertdialog), a
Stay-signed-in button that calls refreshTokens(), and replace handleError's
hard redirect on 401 with the modal so form state is preserved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
holocene Ready Ready Preview, Comment Jun 11, 2026 4:00pm

Request Review

@github-actions github-actions Bot added a11y Accessibility audit PR a11y:bucket-4 Bucket 4: engineer + product a11y:sc-2.2.1 labels Jun 10, 2026
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a11y:bucket-4 Bucket 4: engineer + product a11y:sc-2.2.1 a11y Accessibility audit PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant