diff --git a/changelog.d/1226-first-run-touch-dismiss.md b/changelog.d/1226-first-run-touch-dismiss.md new file mode 100644 index 000000000..3eb802dac --- /dev/null +++ b/changelog.d/1226-first-run-touch-dismiss.md @@ -0,0 +1,4 @@ +- The first-run card can be closed on a phone or tablet. "ESC to dismiss" is + now a button that closes the card the same way ESC does; touch screens read + DISMISS, and phones no longer hide it, so choosing a mission is not the only + way out (KnottyDyes, #1226). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index afbf18c45..d75f3f251 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -2076,10 +2076,18 @@ fetching, trailing-24-hour filtering and partial-success caching are unchanged. > `localStorage['gev:first-run-mission:v1'] === 'suppressed'`, written **only** > by the "Don't show this again" checkbox → the per-session > `sessionStorage['gev:first-run-mission-session:v1'] === 'dismissed'`, written -> by **every** close path (mission, Explore, ESC). So it returns each fresh -> browser session until the visitor ticks the box; clearing storage un-ticks it, -> which is accepted. Both stores fail open — an unreadable store still shows the -> launcher rather than silently swallowing first launch. +> by **every** close path (mission, Explore, ESC, the footer dismiss button). So +> it returns each fresh browser session until the visitor ticks the box; clearing +> storage un-ticks it, which is accepted. Both stores fail open — an unreadable +> store still shows the launcher rather than silently swallowing first launch. +> +> **The footer dismiss button is the keyboardless ESC.** Phones and tablets have +> no ESC key, so the footer hint is a `data-first-run-dismiss` button that calls +> the same `dismiss()` as ESC. It stays visible at every width (phones used to +> hide it, leaving a mission as the only way out) and reads DISMISS on a +> `(hover: none) and (pointer: coarse)` screen, "ESC to dismiss" otherwise. Its +> 44px touch target is a `::before` outside the layout, so the footer row keeps +> its one-line height. > > **What a mission may persist (do not "simplify" this).** Layer enablement is > durable in this app (`gev:layer-state:v2`, written by diff --git a/src/firstRunExperience.js b/src/firstRunExperience.js index 408152295..a3181ec15 100644 --- a/src/firstRunExperience.js +++ b/src/firstRunExperience.js @@ -543,6 +543,10 @@ export function initFirstRunExperience({ }); for (const button of buttons) button.addEventListener('click', onChoice); + // The same dismissal as ESC, for screens without a keyboard. + root + .querySelector('[data-first-run-dismiss]') + ?.addEventListener('click', () => dismiss()); suppressBox?.addEventListener('change', onSuppressChange); // Capture phase: the app binds its own global hotkeys (including bare letters // that cycle detection and styles), and the launcher owns the keyboard first. diff --git a/src/firstRunExperience.test.mjs b/src/firstRunExperience.test.mjs index a02c4391f..603272e82 100644 --- a/src/firstRunExperience.test.mjs +++ b/src/firstRunExperience.test.mjs @@ -913,6 +913,48 @@ test('markup, startup ordering and accessibility remain pinned', () => { assert.match(choicesBlock, /overflow-y: auto;/); }); +test('the card closes without a keyboard: the ESC hint is a button, kept on phones', () => { + const html = expandApplicationHtml( + fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8'), + ); + const module = fs.readFileSync( + new URL('./firstRunExperience.js', import.meta.url), + 'utf8', + ); + // The whole stylesheet, so a hide rule added in any other file is caught too. + const css = readStylesheet(new URL('../style.css', import.meta.url)); + const footer = html.slice( + html.indexOf('class="first-run-footer"'), + html.indexOf('data-first-run-status'), + ); + assert.match( + footer, + /