Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 43 additions & 6 deletions src/consent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function showConsentModal(): Promise<boolean> {
panel.innerHTML = `
<h2>webmap.dev — privacy-first GPS mapping, no account required</h2>

<div id="consent-body">
<div id="consent-body" tabindex="0" aria-label="Terms — scroll to read">
<h3>Privacy Policy</h3>
<ul class="consent-legal">
<li><strong>Local only.</strong> All GPS data and app settings stay in your browser. Nothing is sent to our servers.</li>
Expand All @@ -59,22 +59,58 @@ export function showConsentModal(): Promise<boolean> {
</div>

<div id="consent-actions">
<button id="consent-accept" class="rec-btn rec-btn-start">I agree — continue</button>
<p id="consent-scroll-hint">Please scroll to the bottom to continue.</p>
<button id="consent-accept" class="rec-btn rec-btn-start" disabled>I agree — continue</button>
<button id="consent-decline">Decline</button>
</div>
`;

overlay.appendChild(panel);
document.body.appendChild(overlay);

const acceptBtn = panel.querySelector<HTMLButtonElement>('#consent-accept')!;
const body = panel.querySelector<HTMLElement>('#consent-body')!;
const hint = panel.querySelector<HTMLElement>('#consent-scroll-hint')!;

// Gate the accept button on the user having scrolled to the bottom of the terms.
// Enable as soon as the bottom is reached (4px tolerance for sub-pixel rounding),
// or immediately if the terms are short enough not to scroll. Re-checked on resize
// so a rotate that makes the content fit also unlocks it.
const atBottom = (): boolean =>
body.scrollTop + body.clientHeight >= body.scrollHeight - 4;
const unlock = (): void => {
acceptBtn.disabled = false;
hint.style.display = 'none';
body.removeEventListener('scroll', onScrollOrResize);
window.removeEventListener('resize', onScrollOrResize);
// Move focus to the now-interactive button so a keyboard user who just scrolled
// to the bottom doesn't have to Tab forward to reach it.
acceptBtn.focus();
};
const onScrollOrResize = (): void => {
if (atBottom()) unlock();
};

if (atBottom()) {
unlock();
} else {
hint.style.display = 'block';
body.addEventListener('scroll', onScrollOrResize, { passive: true });
window.addEventListener('resize', onScrollOrResize);
}

function cleanup(accepted: boolean): void {
// Symmetric teardown — tear down both listeners regardless of which path closes
// the modal (e.g. decline-before-scroll leaves the scroll listener attached).
body.removeEventListener('scroll', onScrollOrResize);
window.removeEventListener('resize', onScrollOrResize);
overlay.remove();
if (accepted) recordConsent();
resolve(accepted);
}

document.getElementById('consent-accept')!.addEventListener('click', () => cleanup(true));
document.getElementById('consent-decline')!.addEventListener('click', () => cleanup(false));
acceptBtn.addEventListener('click', () => cleanup(true));
panel.querySelector('#consent-decline')!.addEventListener('click', () => cleanup(false));
overlay.addEventListener('click', (e) => {
if (e.target === overlay) cleanup(false);
});
Expand All @@ -87,7 +123,8 @@ export function showConsentModal(): Promise<boolean> {
};
document.addEventListener('keydown', onKey);

// Focus the accept button for keyboard accessibility
document.getElementById('consent-accept')!.focus();
// Focus the scrollable terms so keyboard users can scroll to read (and thereby
// unlock the accept button, which starts disabled and can't take focus yet).
body.focus();
});
}
15 changes: 15 additions & 0 deletions src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -1538,6 +1538,21 @@ body { height: 100%; width: 100%; padding: 0; margin: 0; }
cursor: pointer;
}

#consent-accept:disabled {
opacity: 0.5;
cursor: not-allowed;
}

/* Shown (display:block via JS) only while the accept button is locked, i.e. when the
terms actually overflow and haven't been scrolled to the bottom yet. */
#consent-scroll-hint {
display: none;
margin: 0 0 8px;
font-size: 12px;
color: #888;
text-align: center;
}

#consent-decline {
padding: 8px 16px;
font-size: 13px;
Expand Down
Loading