Skip to content

Harden board dialogs against focus and live-update clobber - #2397

Open
Chris0Jeky wants to merge 3 commits into
mainfrom
issue-1975/paper-dialog-residuals
Open

Harden board dialogs against focus and live-update clobber#2397
Chris0Jeky wants to merge 3 commits into
mainfrom
issue-1975/paper-dialog-residuals

Conversation

@Chris0Jeky

@Chris0Jeky Chris0Jeky commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

This reduces board-maintenance friction by preventing dialogs from moving or discarding a user's in-progress edit while preserving the existing explicit-save, review-first interaction model.

Implementation notes

  • extracted the focus lifecycle and Tab trap into useDialogFocusManagement, shared by TdDialog and PaperBoardDialogShell
  • added useRealtimeSafeDialogDraft, which compares the live seed against the last seed snapshot instead of treating a new object reference as a new draft
  • applied the reseed guard to Paper and Legacy board/column settings dialogs, including Paper confirmation/busy states
  • kept arrow navigation available from board buttons; only Enter/Space activation is ceded to native/ARIA controls
  • exposed the keyboard model's selected Paper lane with a visible border and aria-current
  • serialized move-button and drag/drop column reorders under one in-flight lock
  • clarified the mutation-parity guard's discovery limits and the nullable column-update contract comment
  • removed unreachable archive-error copy and matching locale keys

Explicitly retained behavior

The archive navigate/close-before-delete ordering is deliberately retained. #519 established that unmounting before store teardown prevents a load-bearing board-view freeze; boardStore remains the owner of the visible failure toast after navigation. Changing that ordering here would reintroduce the older failure mode rather than resolve #1975.

Tests

Added or extended regressions for:

  • Paper and TdDialog bidirectional Tab trapping and focus lifecycle
  • Paper live visual-viewport updates and fallback behavior
  • same-ID reference replacement preserving Paper and Legacy board/column drafts
  • reopen/entity-change reseeding from the latest live state
  • Enter on dialog buttons bypassing board shortcuts while arrow navigation from board buttons remains available
  • selected Paper lane visibility/ARIA state and single-request serialization across double activation and overlapping drag/button reorder paths
  • locale catalog parity and mutation-capability parity

Review disposition

Verification

At refreshed head 8184d9a193b669a4ee44f8e80a493f1c8fe47213, based on current origin/main fe3a120a49e3c1e4737e802629f838d5eb4394af:

  • npx vitest --run --maxWorkers=2 src/tests/i18n/catalogs.spec.ts src/tests/i18n/lazyLocales.spec.ts src/tests/views/paper/PaperBoardDialogShell.spec.ts src/tests/components/ui/TdDialog.spec.ts src/tests/composables/useKeyboardShortcuts.spec.ts src/tests/components/ColumnEditModal.spec.ts src/tests/components/BoardSettingsModal.spec.ts src/tests/views/paper/PaperBoardManagement.spec.ts src/tests/views/BoardView.keyboardRouting.spec.ts src/tests/views/paper/boardMutationCapabilityParity.spec.ts — 10 files / 136 tests passed
  • changed-file npx eslint --max-warnings=0 ... — passed
  • npm run build — typecheck and production build passed
  • git diff --check origin/main..HEAD — passed

Before the localized review fix and non-overlapping base merge, npx vitest --run --maxWorkers=2 passed all 358 files / 5,339 tests at implementation head 320675393.

Docs and residual verification

  • canonical STATUS, masterplan, and testing-guide docs were intentionally not edited; the issue coordinator owns batch doc synchronization
  • no manual phone/software-keyboard, screen-reader, or browser geometry pass was run
  • the complete frontend suite was not repeated after the localized review fix/base merge; the affected focused suite, lint, typecheck, and build were repeated instead
  • hosted CI and automated review are rerunning from this refreshed ready PR

Closes #1975

Copilot AI lite review requested due to automatic review settings September 2, 2026 12:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T12:46:39.204025Z 8184d9a New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3206753938

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +32 to +33
if (!wasOpen || seededKey !== sourceKey || !options.isDirty()) {
options.seed(source)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Merge untouched live fields before saving

When a same-board refresh arrives after the user edits only one field, this form-wide isDirty() gate skips seed() for every field. In PaperBoardSettingsDialog.save and BoardSettingsModal.handleSave, an untouched description consequently remains at its old value; because Save compares it with the current props.board.description, it is sent as a change and silently overwrites the collaborator's newer description. The column dialogs can similarly resubmit an obsolete WIP limit. Preserve dirty fields individually while adopting fresh untouched fields, or construct the payload from per-field dirty state.

Useful? React with 👍 / 👎.

Comment on lines +38 to +44
const isActivationControl = Boolean(target?.closest(
'button, a[href], [role="button"], [role="menuitem"], [role="option"], [role="tab"]',
))

if (isTyping && event.key !== 'Escape') {
if (
(isTextEntry && event.key !== 'Escape') ||
(isActivationControl && (event.key === 'Enter' || event.key === ' '))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat focused dialog containers as shortcut-owning

When a TdDialog is opened from the Legacy CardModal, the focus manager deliberately focuses the .td-dialog container itself, which does not match this activation-control selector. Because Legacy does not set paperDialogOpen, pressing Enter on that initial focus still reaches BoardView's global Enter binding and calls openSelectedCard behind a discard/delete confirmation; when the card was opened by pointer and the board selection is empty, this can programmatically open another card modal. Suppress board activation whenever an active dialog owns the keyboard, rather than only when the event target is an interactive control.

Useful? React with 👍 / 👎.

@Chris0Jeky Chris0Jeky moved this from Pending to Review in Taskdeck Execution Sep 2, 2026
@Chris0Jeky

Copy link
Copy Markdown
Owner Author

Final-base review at 8184d9a found a confirmed HIGH after the single fix round:

  • useRealtimeSafeDialogDraft skips reseeding the whole form once any field is dirty.
  • If a user edits only one field, a realtime same-entity replacement can update an untouched sibling field in props while the draft retains its old value.
  • The save comparison then sends that stale untouched value explicitly, overwriting the collaborator update. This affects Paper and Legacy board dialogs and the analogous column name/WIP pair.
  • Existing coverage dirties both board fields, so it misses the one-dirty/one-remote-changed reproduction.

The shared button/drag reorder lock was independently checked and is sound. The visualViewport/dvh MEDIUM remains tracked on #2180.

Disposition: parked, not merged. This is the second/final review round, so no further fix cycle is being started during wrap-up.

Exact resume point: start from this pushed head and replace form-wide reseed suppression with per-field seed/draft reconciliation: a locally dirty field keeps the user edit, an untouched field adopts the latest live value, and save must not emit a stale untouched value. Add one-dirty/one-remote-changed regressions for Paper and Legacy board and column dialogs, then rerun the focused suite, lint, typecheck/build, current-base integration, and one bounded final judgment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Blocked

Development

Successfully merging this pull request may close these issues.

[Frontend] Paper board dialog residuals from PR #1959: TdDialog machinery parity, realtime input-clobber (both skins), Enter-swallow in dialogs

2 participants