fix(first-run): make the dismiss hint a button so touch screens can close the card (#1226)

Allow touch users to dismiss the welcome card through the existing dismissal path.

Contribution by @KnottyDyes.
This commit is contained in:
KnottyDyes authored and GitHub committed 2026-10-25 08:14:52 -07:00
1 parent 91144e88ad
commit 6fee8382ce
6 files changed
+115 -10

No files matched your search

@@ -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).
+12 -4
View File
@@ -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
+4
View File
@@ -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.
+42
View File
@@ -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,
/<button type="button" class="first-run-dismiss" data-first-run-dismiss>/,
);
assert.match(footer, /data-i18n>ESC to dismiss</);
assert.match(footer, /data-i18n>DISMISS</);
// It closes the way ESC does, through the one dismiss path, so the session
// flag and focus handling are the same.
assert.match(
module,
/querySelector\('\[data-first-run-dismiss\]'\)\s*\?\.addEventListener\('click', \(\) => dismiss\(\)\)/,
);
// Phones used to hide the hint, leaving no way out but picking a mission.
assert.doesNotMatch(css, /first-run-footer > span:last-child/);
assert.doesNotMatch(css, /\.first-run-dismiss\s*\{[^}]*display:\s*none/);
// A 44px touch target that does not add to the footer row's height.
assert.match(
css,
/\.first-run-dismiss::before \{[^}]*position: absolute;[^}]*height: 44px;/,
);
assert.doesNotMatch(css, /\.first-run-dismiss \{[^}]*min-height/);
// Touch screens read DISMISS rather than a key they do not have.
assert.match(
css,
/@media \(hover: none\) and \(pointer: coarse\) \{\s*\.first-run-dismiss-key \{\s*display: none;/,
);
});
test('the launcher keeps focus, restores it, and never disables the focused button', () => {
const module = fs.readFileSync(
new URL('./firstRunExperience.js', import.meta.url),
+46 -5
View File
@@ -209,6 +209,49 @@
letter-spacing: 0.055rem;
}
.first-run-dismiss {
position: relative;
padding: 0 0 0 0.75rem;
border: 0;
background: none;
color: inherit;
font: inherit;
letter-spacing: inherit;
text-transform: inherit;
cursor: pointer;
}
/* A 44px touch target drawn outside the layout, so the footer row keeps the
height of its one line of text; a 44px button box would nearly treble it. */
.first-run-dismiss::before {
content: '';
position: absolute;
top: 50%;
right: 0;
left: 0;
height: 44px;
transform: translateY(-50%);
}
.first-run-dismiss:hover,
.first-run-dismiss:focus-visible {
color: rgba(54, 220, 255, 0.86);
}
.first-run-dismiss-touch {
display: none;
}
/* No hover and a coarse pointer: a phone or tablet, where ESC means nothing. */
@media (hover: none) and (pointer: coarse) {
.first-run-dismiss-key {
display: none;
}
.first-run-dismiss-touch {
display: inline;
}
}
.first-run-suppress {
display: flex;
align-items: center;
@@ -270,8 +313,9 @@ body.scene-playback-mode #first-run-launcher {
#first-run-launcher.visible {
transform: translate(-50%, 0) scale(1);
}
/* Five tiles have to clear the dock on a 375 x 812 phone, so the rows tighten
and the redundant "ESC to dismiss" hint (no ESC key on a phone) drops. */
/* Five tiles have to clear the dock on a 375 x 812 phone, so the rows
tighten. The dismiss button stays: it is the only way to close the card
without choosing a mission on a phone, which has no ESC key. */
#first-run-launcher h2 {
margin-top: 0.45rem;
}
@@ -289,9 +333,6 @@ body.scene-playback-mode #first-run-launcher {
.first-run-choices small {
font-size: 0.62rem;
}
.first-run-footer > span:last-child {
display: none;
}
}
/* Short viewports — landscape phones, small laptop windows. The chrome tightens
+7 -1
View File
@@ -44,7 +44,13 @@
<input type="checkbox" data-first-run-suppress />
<span data-i18n>Don't show this again</span>
</label>
<span data-i18n>ESC to dismiss</span>
<!-- A button, not just a hint: phones and tablets have no ESC key. It
closes exactly as ESC does, for this session only. Touch screens
see DISMISS; a keyboard keeps the ESC hint (first-run.css). -->
<button type="button" class="first-run-dismiss" data-first-run-dismiss>
<span class="first-run-dismiss-key" data-i18n>ESC to dismiss</span>
<span class="first-run-dismiss-touch" data-i18n>DISMISS</span>
</button>
</div>
<!-- Kept out of the footer row: it swaps to mission progress and errors, and
a live region that also holds layout makes the row jump as it speaks. -->