feat(consent): require scrolling the terms before enabling "I agree"#226
Conversation
The first-run consent dialog's "I agree — continue" button now starts disabled and
unlocks only once the user scrolls to the bottom of the terms (#consent-body), so
acceptance follows actually seeing the full Privacy Policy + Terms of Use.
- Accept button renders `disabled` (dimmed, not-allowed cursor); a small hint
("Please scroll to the bottom to continue.") shows beneath the actions while locked.
- Unlocks on scroll-to-bottom (4px tolerance for sub-pixel rounding), hiding the hint.
- If the terms already fit without scrolling (tall viewport), enables immediately —
nothing to scroll past.
- Re-checked on resize so rotating to a layout where the content fits also unlocks it.
- The terms region is now focusable (tabindex=0) and receives initial focus, so
keyboard users can scroll to read (and unlock) instead of landing on a disabled
button. Decline stays enabled throughout.
- The window resize listener is cleaned up on close (incl. decline-before-scroll).
Closes #225
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code ReviewPR: feat(consent): require scrolling the terms before enabling "I agree" OverviewThis PR adds a scroll-to-accept gate on the consent dialog so users must scroll to the bottom of the Privacy Policy + Terms before the "I agree" button becomes active. The implementation is clean and well-thought-out — fallback for tall viewports, resize re-check, and keyboard accessibility are all handled. Two issues worth addressing before merge. Bug: scroll listener not removed on early declinesrc/consent.ts — When the user clicks Decline (or presses Escape) before scrolling to the bottom, function cleanup(accepted: boolean): void {
window.removeEventListener('resize', onScrollOrResize);
// body scroll listener is still attached here
overlay.remove();
...
}After Fix: function cleanup(accepted: boolean): void {
window.removeEventListener('resize', onScrollOrResize);
body.removeEventListener('scroll', onScrollOrResize); // add this
overlay.remove();
if (accepted) recordConsent();
resolve(accepted);
}This also makes Minor UX: focus doesn't move to the accept button after unlockAfter const unlock = (): void => {
acceptBtn.disabled = false;
hint.style.display = 'none';
body.removeEventListener('scroll', onScrollOrResize);
window.removeEventListener('resize', onScrollOrResize);
acceptBtn.focus(); // move focus now that the button is interactive
};This is a soft suggestion rather than a blocker — the current behavior is functional — but it improves keyboard flow noticeably. Everything else looks good
Summary: The listener cleanup in 🤖 Generated with Claude Code |
Address review on #226: - cleanup() now also removes the body scroll listener (symmetric with unlock), so decline-before-scroll tears down both listeners. - unlock() moves focus to the now-enabled accept button so a keyboard user who just scrolled to the bottom doesn't have to Tab to reach it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Both addressed: |
Code Review — feat(consent): require scrolling the terms before enabling "I agree"Overall: This is a solid, well-thought-out feature. The symmetric listener teardown, passive scroll listener, fits-without-scrolling fallback, and resize re-check all show good attention to edge cases. One confirmed focus-order bug and two minor accessibility gaps are noted below. Bug:
|
Summary
The first-run consent dialog now disables "I agree — continue" until the user scrolls to the bottom of the terms — so acceptance follows actually viewing the full Privacy Policy + Terms of Use.
Changes
src/consent.ts:disabled; a hint ("Please scroll to the bottom to continue.") shows beneath the actions while it's locked.#consent-bodyis scrolled to the bottom (scrollTop + clientHeight >= scrollHeight - 4), hiding the hint and removing the listeners.tabindex=0and gets initial focus (keyboard users can scroll to read/unlock instead of landing on a disabled button). Decline stays enabled.resizelistener cleaned up on close, including decline-before-scroll.src/style.css:#consent-accept:disabled— dimmed +not-allowedcursor.#consent-scroll-hint— hidden by default; shown via JS only while locked.Test plan
type-check(app + worker),lint,test(96),build— all green; markup/logic verified in the bundleCloses #225
🤖 Generated with Claude Code