Repository navigation
F135: replace window.confirm/prompt with the app's own dialogs - #136
Conversation
The last surfaces still using a browser popup. #131 gave every add/edit form an accessible Dialog; eight one-way and corrective actions kept the natives. Five yes/no confirms (submit day, confirm order, cancel draft, deplete flock, archive flock) and three that collect a required audit reason (void entry, void payment, void order). Why they had to go: - window.prompt can be suppressed by the browser outright — an action that silently no-ops is worse than an ugly one. - Its "reason is required" check ran AFTER the popup closed, so a blank answer threw away everything the user had typed. The dialog validates in place. - Neither can be styled: the night theme stopped at the popup edge. - Both block the JS thread, and on mobile render as system chrome with the origin in the title, next to #131's bottom sheets. useConfirm(): one hook over the existing Dialog, two shapes, so a screen that needs both (Sales needs all four) still renders one element. if (!(await confirm({ ... }))) return; const reason = await askReason({ ... }); // null, or a trimmed non-empty if (reason === null) return; Initial focus falls out of DOM order and lands where it should without any special-casing: Cancel for a yes/no, so a stray Enter cannot deplete a flock; the textarea for a reason, where there is nothing to decide until they type. --danger/--danger-press are themed, unlike --aubergine: no single red clears both 4.5:1 for its white label and 3:1 against the surface behind it in light and dark at once. Light 6.71/5.56, dark 5.53/4.60; press is the lighter step either way, matching --aubergine-press. DailyEntryPage's synchronous re-entry guard ran before the confirm. window.confirm blocked the thread so nothing could slip through; the dialog does not, so the guard is re-checked after the await. Tests: 296 -> 319. Every one of these actions had ZERO coverage while it lived behind a native popup — a stubbed window.confirm tests the stub, not the screen. Coverage 86.9 -> 89.9 lines, 67.3 -> 70.1 functions; gate ratcheted. useConfirm itself is at 100% across the board, including the unmount and ask-over-ask paths that would otherwise strand a caller's promise for ever. Verified at 1280x900 and 390x844, light and dark: no horizontal overflow, no internal scrolling, footer in reach, and the destructive fill resolving to the right token in each theme. GLOSSARY and the Help page carry the new behaviour. Closes #135.
DailyEntryPage: the post-await guard was 3/4 ineffective (codex, Medium). selectedFlock, prefillFailed and prefillPending are React state, so re-reading them after the confirm returns the render-time closure, not what is true now — the check looked like defence it could not provide. Narrowed to inFlight.current (a ref, genuinely fresh) and documented why the other three are left out rather than mirrored: none of them can change while the dialog is up. The flock and date controls sit behind the backdrop, and a prefill cannot start meanwhile because both save buttons are disabled while one is pending. Accessibility, both reviewers: - The consequence text was not the dialog's accessible description. Focus lands on a button, so a screen reader announced the title and the control and never what the action does — the only thing a confirmation is for. Dialog takes an optional describedBy; useConfirm points it at the body. - The reason field carried no validation semantics. Added required, aria-invalid, and aria-describedby to the error, and focus now returns to the field instead of staying on the button that just refused it. That also makes the error announced rather than merely displayed. Corrected a rule this branch contradicted (both reviewers). GLOSSARY and Help said red meant "destroys or freezes", but submit-day freezes the entry and is deliberately not red. The rule is UNDO or RETIRE — void, cancel, deplete, archive. Submitting a day and confirming an order are equally irreversible but are the ordinary path through the week; a red button on the most routine action of all would spend the colour where it says nothing. Reworded in three places. Merged the duplicate `.dialog textarea` rule by taking textarea out of the width group instead of stacking a second block after it — resize is meaningless on an input or a select. Rejected: --on-danger not being re-declared in the dark blocks. It is deliberately theme-invariant. White is the only label that clears AA on all four fills (4.60-6.71); black would fail the dark rest fill at 3.79. Documented so it is not flagged again. Tests 320 -> 321; useConfirm stays at 100% lines/branches/functions.
|
Review complete: codex + two independent agents, per the standing policy. Applied (4)
The guard one is worth calling out, because the PR body claimed it was handled and it was not. The accessible-description one is the most consequential for users. Focus lands on a button, so a screen reader announced the title and the control and never what the action does — the only thing a confirmation exists to say. The reason field gained Both reviewers independently caught the red-button contradiction: GLOSSARY and Help said red meant "destroys or freezes", but submitting a day freezes the entry and is deliberately aubergine. The docs were wrong, not the flag — the rule is undo or retire. A red button on the most routine action in the app would spend the colour where it says nothing. Reworded in three places. Rejected (1)
Confirmed cleanThe third reviewer found no defects in promise lifecycle, races, idempotency-key rotation, or silent behaviour changes, and proved two things the PR body asserted without evidence:
State321 tests. |
Closes #135.
The last surfaces still using a browser popup. #131 (PR #132) gave every add/edit form an accessible
Dialog; eight one-way and corrective actions keptwindow.confirm/window.prompt. The inconsistency ran the wrong way — "add a grade" got a designed sheet, "void a confirmed order" got an OS text box.What moved
Yes/no (5) — submit day, confirm order, cancel draft, deplete flock, archive flock.
Reason required (3) — void daily entry, void payment, void confirmed order. These write an audit reason, so the text itself is the point.
Why the prompts mattered most
The issue was filed for the five confirms. A census for
window.promptturned up three more, and they were the worse offenders:window.promptoutright. An action that silently no-ops is worse than an ugly one.The hook
One hook over the existing
Dialog, two shapes, so a screen needing both (Sales needs all four) still renders one element:Focus falls out of DOM order and lands where it should with no special-casing:
Cancelfor a yes/no — a stray Enter must not deplete a flock — and the textarea for a reason, where there is nothing to decide until they have typed.Two things worth a look in review
The danger tokens are themed, unlike
--aubergine. No single red clears both 4.5:1 for its white label and 3:1 against the surface behind it in light and dark at once — the band that satisfies both in dark is narrow. Light 6.71 rest / 5.56 press, dark 5.53 / 4.60; press is the lighter step either way, matching--aubergine-press.DailyEntryPage's re-entry guard needed re-checking.inFlight.currentis tested before the confirm.window.confirmblocked the thread so nothing could slip through the gap; a dialog does not, so the guard is re-checked after the await.Tests
296 → 319. Worth stating plainly: every one of these actions had zero coverage while it lived behind a native popup — a stubbed
window.confirmtests the stub, not the screen. The threeFlocksPagetests that assertedexpect(window.confirm).toHaveBeenCalled()were the whole of it.useConfirmis at 100% lines/branches/functions, including two paths that would otherwise strand a caller's promise for ever:The first of those caught a real bug in my own code during the build:
confirm/askReasoninstalled the new resolver beforeopen()settled the previous one, soopen()answered the brand-new promise and left the outgoing one hanging.Coverage 86.9 → 89.9 lines, 67.3 → 70.1 functions; gate ratcheted with headroom (branches left at 82 — they moved 83.14 → 83.35, flat).
Verified in a browser
1280×900 and 390×844, light and dark, across all three dialog shapes: no horizontal overflow, no internal scrolling, footer in thumb reach, and the destructive fill resolving to
#a8320fin light /#bc3c14in dark.Also constrained
.dialog textareatoresize: vertical— the default both-axis handle could be dragged wider than the panel.Docs
specs/product/GLOSSARY.mdgains Confirmation and Void reason entries; the Help page's "Adding & correcting" section gains two bullets covering the ask-first behaviour and the reason requirement.Not in scope
#133 (capture screens: Water / expense-add / Sales add-line, plus the Users flock-assign control) stays an independent decision.