Skip to content

MEMBERS-DIRECTORY-001M — Restore surviving photo-control focus after rejected removal #643

Description

@daliu

Parent: #504. Connected release remains owned by #507.

Officer impact: In the preserved future My Account interface, a keyboard or screen-reader user whose focused Remove action disappears after a rejected removal and authoritative profile refresh will be returned to the closest surviving photo action. The connected interface remains unavailable on production.

Officer documentation: Update SYSTEM_DESIGN.md, IMPLEMENTATION_PLAN.md, SECURITY.md, and only the optional-directory one-sentence text alternative plus one no-terminal source-evidence procedure in docs/officers/EVENTS_SHOP_MEMBERS.md. No Mermaid change is needed because page topology, permissions, and data movement do not change.

Deployment evidence: None at issue creation. This is source-only accessibility hardening. memberDirectoryAvailability.ts must remain byte-for-byte false; no website, Firebase, provider, account, sign-in, production-data, deployment, or connected-live action is authorized.

Atomic outcome

Restore keyboard focus to the correct surviving photo control after a focused Remove current saved photo receives a definitive rejection and a successful authoritative profile readback.

Defect

On exact released main f3da2f3, a Remove mutation disables the focused Remove action while pending. A definitive rejection then performs an authoritative profile read. If that current read says hasPhoto:false, Remove disappears. Unlike confirmed removal, the definitive-rejection path records no ready-state focus handoff. Browser-disabled or removed-control focus can therefore remain on the document body.

The existing rejection test returns a profile that still has a saved photo and does not assert focus, so it does not exercise the missing-control branch.

Invariants and transitions

  • Create a rejection-focus intent only after request-ID creation and startMutation('remove') admit the exact current operation into pending.
  • Create it only when Remove current saved photo owns focus at that point.
  • Store only the current mounted application/account lifetime and exact mutation-operation identity. Store no photo bytes, profile value, request ID, revision, provider value, or error.
  • Programmatic or outside-focused Remove invocation, request-ID failure, and rejected mutation start create no intent.
  • After definitive Remove rejection and successful current authoritative readback, consume the matching intent after ready controls render.
  • Choose the current rendered destination: surviving Remove; otherwise current enabled Save profile photo when the exact local draft is ready; otherwise the persistent Add/Replace file input, including no draft, reading, or not-yet-ready draft.
  • If the target already owns focus, leave it alone. Restore only from body, the document root element, absent, or disconnected focus. Preserve any other connected focus deliberately selected during pending.
  • Keep the fixed rejection role=alert. If Remove survives, retain its error description. If Remove disappears, leave the alert standalone; do not associate it with Save or the file input.
  • Clear/fence the intent on confirmed success, unknown outcome, failed confirming read, application change, UID change, unmount, or obsolete operation.
  • Preserve confirmed-success focus behavior, MEMBERS-DIRECTORY-001J — Preserve profile-recovery keyboard focus #637 Reload recovery focus, local draft identity/bytes/render state/revision rules, and all mutation/read/render/context fences.
  • Focus creates no request ID, callable, retry, automatic mutation, draft, data URL, confirmation, audit, or provider/data action.
  • Availability stays false; the default branch remains inert and zero-context/zero-ID/zero-call.
  • No photo query, facial recognition, matching, embedding, similarity, biometric processing, roster authority, or membership proof is added.

Exact ownership

  • src/pages/account/MemberDirectoryProfile.tsx: only the exact-operation focused-Remove rejection intent, cleanup guards, and guarded current-state focus handoff.
  • src/pages/account/MemberDirectoryProfile.test.tsx: one separately named MEMBERS-DIRECTORY-001M block plus minimum directly affected helpers/assertions.
  • SYSTEM_DESIGN.md: one separately named 001M source-only paragraph.
  • IMPLEMENTATION_PLAN.md: one separately named 001M current-source paragraph.
  • SECURITY.md: one separately named 001M/RISK-042 row immediately after released 001L; preserve active OAUTH-001A2L — Bound Strava activity/statistics JSON and cancel the unread statistics body #616's future OAUTH-001A2L/RISK-024 hunk.
  • docs/officers/EVENTS_SHOP_MEMBERS.md: only the optional-directory one-sentence text alternative and one no-terminal 001M source-evidence procedure; no Mermaid or unrelated procedure change.

No CSS change: the same native controls and existing focus styles are used.

Trustworthy RED and required tests

  • On unchanged runtime, focus Remove, begin a deferred valid removal, assert disabled, explicitly blur to model native focus eviction, reject definitively, resolve confirming read with hasPhoto:false, and prove focus remains off the surviving file input.
  • Repeat with a current ready replacement and prove old source does not focus Save profile photo.
  • Corrected behavior restores surviving Remove, ready Save, or file input for no/reading/not-ready draft.
  • Surviving Remove retains aria-describedby to the fixed error; vanished Remove leaves a standalone alert and does not attach it to Save/input.
  • Programmatic/outside-focused invocation creates no intent; deliberate outside focus during pending is preserved; already-retained Remove is not redundantly focused.
  • Request-ID failure remains enabled/focused with zero removal call.
  • Failed confirming read retains existing unavailable/Reload focus; ordinary unknown outcome retains existing uncertain/Reload focus; confirmed Remove behavior remains unchanged.
  • Application change, UID change, unmount, and stale operation completion are focus-inert.
  • No extra request ID, remove/upload/visibility call, retry, confirmation, draft, or data URL.
  • Default preview remains zero-context/zero-ID/zero-call.
  • Existing profile, draft, rejection, uncertainty, recovery-focus, response-boundary, accessibility, privacy, race, and mobile suites remain green.

Explicit exclusions

No Account.tsx/test, CSS, service adapter, availability, inbound WebP validation, admin finder, AdminGuard, routes, backend Function/Rule/index/schema/repair/package/lock/workflow/release control, privacy/capability policy, Firebase/provider configuration, deployment, account/sign-in, production data, real name/photo, connected/live action, or #616 path/hunk.

Migration and residual risk

No migration. This focus handoff is accessibility state, not server acknowledgement, deletion proof, reconciliation, authorization, provider evidence, or proof that removal succeeded. A definitive rejection and a concurrent authoritative state with no photo may both be true; the rejection alert therefore remains separate from the current surviving action.

Availability stays false and live #623 remains inert. #507 retains every connection and release gate.

Claim protocol

Assign the issue and post a timestamped CLAIMED marker naming the exact branch and released base before any edit. The claim remains active through reviewed merge, exact-main/deployment audit, closure, and explicit release.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:membershipMembership lifecycle and reconciliationarea:privacyPersonal data, consent, minimization, retention, and privacy operationsarea:webWeb application and hostingbugSomething isn't workingpriority:P1High-priority follow-upsize:SSmall focused issuetype:reliabilityReliability and recoverytype:securitySecurity or privacy boundarytype:testingTest infrastructure and quality gates

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions