fix: make quest popup body scrollable to prevent overflow - #84
Conversation
- Modal card capped at max-h-[90vh] with flex-col layout - Body text scrolls independently; title and Continue button always visible - Bottom fade gradient hints at more content below - Padding reduced to p-4 on mobile, p-6 on desktop - 2 new e2e tests: Continue button within viewport, card height <= viewport Formatting constraints verified
The parent overflow-hidden was preventing touch scroll events from reaching the inner overflow-y-auto div on iOS/mobile. Also adds touch-pan-y to explicitly enable vertical touch scroll on the body.
On Android Chrome, overflow-y-auto on a nested div inside a flex-1 min-h-0 wrapper is unreliable and causes text to overflow the modal. Flatten to a single flex child with overflow-y-auto, min-h-0, and overscroll-contain directly on the content div.
- Add web/public/manifest.json with display: fullscreen so the app launches without browser chrome when added to home screen - Add <link rel=manifest> and Apple/Android meta tags to index.html - Theme and background color match the app dark theme (#1a1a2e)
When opened directly in Chrome (not via home screen shortcut), the browser can still go fullscreen on first user gesture. Register a one-time touchstart/click listener that calls requestFullscreen() — only on touch-capable devices.
- icons-only action buttons on handheld (max-md:hidden text labels) - music unlocks on touchstart in addition to click/keydown - reduce base font-size to 15px on mobile (<=767px) - fix HTML entity quoting in inventory/spell descriptions (v-html) - fix ghost click reopening action menu after selection (300ms guard) - fix eslint-disable-next-line for multi-line v-html in QuestModal
There was a problem hiding this comment.
Pull request overview
This PR addresses QuestModal overflow by constraining the modal height and making the body scrollable so the Continue button remains reachable, and adds E2E coverage for the regression. It also includes several additional UI/platform changes (fullscreen, PWA manifest/meta, mobile font sizing, and an entity-menu ghost-click guard) that are not described in the PR title/summary.
Changes:
- Make
QuestModalbody scrollable within a max-height constrained card; pin title and Continue button. - Add Playwright E2E assertions to ensure the Continue button and modal card fit within the viewport for long content.
- Add fullscreen/PWA/mobile UI changes (TopBar fullscreen toggle + auto-fullscreen on first interaction, manifest/meta, mobile font-size) and an entity-menu ghost-click guard.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| web/src/composables/useEntityMenu.ts | Adds a 300ms “ghost click” guard after closing the entity menu. |
| web/src/composables/useEntityMenu.test.ts | Adds unit tests validating the ghost-click guard timing behavior. |
| web/src/components/TopBar.vue | Adds fullscreen toggle UI and tracks fullscreen state. |
| web/src/components/SidePanel.vue | Renders inventory/spell descriptions via v-html (consistent with markup-rendered content). |
| web/src/components/QuestModal.vue | Constrains modal height and makes body scrollable; adds testid for E2E. |
| web/src/components/GameLayout.vue | Adds touchstart to the one-shot audio unlock gesture handling. |
| web/src/components/CommandInput.vue | Hides quick-action text labels on mobile to save space. |
| web/src/assets/main.css | Adjusts root font-size on small screens. |
| web/src/App.vue | Requests fullscreen on first interaction for touch devices. |
| web/public/manifest.json | Adds a web app manifest configured for fullscreen display. |
| web/index.html | Links manifest and adds mobile/PWA-related meta tags. |
| web/e2e/game.spec.ts | Adds E2E coverage ensuring QuestModal doesn’t overflow viewport with long body text. |
| vi.useFakeTimers() | ||
| }) | ||
|
|
||
| afterEach(() => { |
There was a problem hiding this comment.
This describe uses fake timers but leaves the 300ms guard timeout pending in tests that don't advance time. Before calling vi.useRealTimers(), clear or run pending timers (e.g., vi.clearAllTimers()), otherwise the suite can leak timers and become flaky as more tests are added.
| afterEach(() => { | |
| afterEach(() => { | |
| vi.clearAllTimers() |
| const isFullscreen = ref(false) | ||
|
|
||
| document.addEventListener('fullscreenchange', () => { | ||
| isFullscreen.value = !!document.fullscreenElement | ||
| }) |
There was a problem hiding this comment.
document.addEventListener('fullscreenchange', ...) is registered at module evaluation time and is never removed. This can leak listeners across HMR/re-mounts and also makes the component harder to test in non-browser environments. Prefer registering the listener in onMounted() and removing it in onUnmounted(), and initialize isFullscreen from document.fullscreenElement when mounting.
| <button | ||
| class="inline-flex items-center gap-1 px-3.5 py-1.5 rounded-full bg-chip-bg border border-border text-text-primary cursor-pointer text-sm whitespace-nowrap transition-colors hover:bg-chip-hover max-[375px]:min-h-11 max-[375px]:min-w-11 max-[375px]:px-3 max-[375px]:py-2" | ||
| class="inline-flex items-center gap-1 px-3.5 py-1.5 rounded-full bg-chip-bg border border-border text-text-primary cursor-pointer text-sm whitespace-nowrap transition-colors hover:bg-chip-hover max-md:px-2.5 max-md:min-h-11 max-md:min-w-11" | ||
| @click="$emit('submitCommand', 'look')" | ||
| > | ||
| 👀 Look | ||
| 👀<span class="max-md:hidden"> Look</span> | ||
| </button> | ||
| <button | ||
| class="inline-flex items-center gap-1 px-3.5 py-1.5 rounded-full bg-chip-bg border border-border text-text-primary cursor-pointer text-sm whitespace-nowrap transition-colors hover:bg-chip-hover max-[375px]:min-h-11 max-[375px]:min-w-11 max-[375px]:px-3 max-[375px]:py-2" | ||
| class="inline-flex items-center gap-1 px-3.5 py-1.5 rounded-full bg-chip-bg border border-border text-text-primary cursor-pointer text-sm whitespace-nowrap transition-colors hover:bg-chip-hover max-md:px-2.5 max-md:min-h-11 max-md:min-w-11" | ||
| @click="$emit('submitCommand', 'search')" | ||
| > | ||
| 🔍 Search | ||
| 🔍<span class="max-md:hidden"> Search</span> | ||
| </button> | ||
| <button | ||
| class="inline-flex items-center gap-1 px-3.5 py-1.5 rounded-full bg-chip-bg border border-border text-text-primary cursor-pointer text-sm whitespace-nowrap transition-colors hover:bg-chip-hover max-[375px]:min-h-11 max-[375px]:min-w-11 max-[375px]:px-3 max-[375px]:py-2" | ||
| class="inline-flex items-center gap-1 px-3.5 py-1.5 rounded-full bg-chip-bg border border-border text-text-primary cursor-pointer text-sm whitespace-nowrap transition-colors hover:bg-chip-hover max-md:px-2.5 max-md:min-h-11 max-md:min-w-11" | ||
| @click="$emit('submitCommand', 'inventory')" | ||
| > | ||
| 🎒 Inventory | ||
| 🎒<span class="max-md:hidden"> Inventory</span> | ||
| </button> | ||
| <button | ||
| class="inline-flex items-center gap-1 px-3.5 py-1.5 rounded-full bg-chip-bg border border-border text-text-primary cursor-pointer text-sm whitespace-nowrap transition-colors hover:bg-chip-hover max-[375px]:min-h-11 max-[375px]:min-w-11 max-[375px]:px-3 max-[375px]:py-2" | ||
| class="inline-flex items-center gap-1 px-3.5 py-1.5 rounded-full bg-chip-bg border border-border text-text-primary cursor-pointer text-sm whitespace-nowrap transition-colors hover:bg-chip-hover max-md:px-2.5 max-md:min-h-11 max-md:min-w-11" | ||
| @click="$emit('submitCommand', 'spells')" | ||
| > | ||
| ✨ Spells | ||
| ✨<span class="max-md:hidden"> Spells</span> | ||
| </button> | ||
| <button | ||
| class="inline-flex items-center gap-1 px-3.5 py-1.5 rounded-full bg-chip-bg border border-border text-text-primary cursor-pointer text-sm whitespace-nowrap transition-colors hover:bg-chip-hover max-[375px]:min-h-11 max-[375px]:min-w-11 max-[375px]:px-3 max-[375px]:py-2" | ||
| class="inline-flex items-center gap-1 px-3.5 py-1.5 rounded-full bg-chip-bg border border-border text-text-primary cursor-pointer text-sm whitespace-nowrap transition-colors hover:bg-chip-hover max-md:px-2.5 max-md:min-h-11 max-md:min-w-11" | ||
| @click="$emit('submitCommand', 'help')" | ||
| > | ||
| ❓ Help | ||
| ❓<span class="max-md:hidden"> Help</span> | ||
| </button> |
There was a problem hiding this comment.
The quick-action buttons hide their text labels on max-md, leaving only emoji as the accessible name. This makes the controls ambiguous for screen readers/voice control users. Add explicit aria-labels (or keep an sr-only text label) so the buttons remain accessible when the visible text is hidden.
| function requestFullscreenOnFirstInteraction() { | ||
| if ( | ||
| !('ontouchstart' in window) || | ||
| !document.documentElement.requestFullscreen | ||
| ) | ||
| return | ||
|
|
||
| function onFirstInteraction() { | ||
| document.documentElement.requestFullscreen().catch(() => {}) | ||
| window.removeEventListener('touchstart', onFirstInteraction) | ||
| window.removeEventListener('click', onFirstInteraction) | ||
| } | ||
|
|
||
| window.addEventListener('touchstart', onFirstInteraction, { once: true }) | ||
| window.addEventListener('click', onFirstInteraction, { once: true }) | ||
| } | ||
|
|
||
| onMounted(() => { | ||
| store.setBridge(bridge) | ||
| store.initGame() | ||
| requestFullscreenOnFirstInteraction() | ||
| }) |
There was a problem hiding this comment.
The PR title/description focus on the QuestModal overflow fix, but this diff also introduces fullscreen behavior (auto-enter on first interaction + toggle button), a PWA manifest/meta changes, global mobile font-size change, and an entity-menu ghost-click guard. Either update the PR description to cover these additional features/risks or split them into separate PRs so the quest modal fix can be reviewed/rolled back independently.
| <!-- eslint-disable vue/no-v-html --> | ||
| <div | ||
| class="flex-1 min-h-0 overflow-y-auto leading-relaxed pr-1 touch-pan-y overscroll-contain" | ||
| v-html="body" | ||
| ></div> | ||
| <!-- eslint-enable vue/no-v-html --> | ||
| <div class="shrink-0 flex justify-end pt-2"> |
There was a problem hiding this comment.
PR description mentions a “bottom fade gradient” hinting at more content, but QuestModal template doesn’t add any gradient overlay element/class. Either implement the gradient effect or update the PR description so it matches the actual change set.
| /** Close the menu and start the 300 ms ghost-click guard. */ | ||
| function closeMenu(): void { | ||
| visible.value = false | ||
| canOpen = false | ||
| clearTimeout(ghostGuardTimer) | ||
| ghostGuardTimer = setTimeout(() => { | ||
| canOpen = true | ||
| }, 300) |
There was a problem hiding this comment.
closeMenu() now always schedules a setTimeout but the composable never clears that timer when its owning component unmounts. If useEntityMenu() is torn down (navigation/HMR/tests), the pending timer can still fire and mutate stale state. Consider registering an onUnmounted() cleanup (and/or switching to a timestamp-based guard to avoid timers altogether).
|
@copilot apply changes based on the comments in this thread |
… aria-labels, gradient Agent-Logs-Url: https://github.com/rmstdope/retroquest/sessions/24aca89f-0786-47c3-a439-7963699ada80 Co-authored-by: rmstdope <8273479+rmstdope@users.noreply.github.com>
Applied all changes from the review thread in commit
|
Summary
Quest popup was too tall on some screens, hiding the Continue button below the fold.
Changes
max-h-[90vh]withflex flex-collayout — title and Continue button always pinned and visible; body text scrolls independently in a constrained area; bottom fade gradient hints at more content; padding reduced top-4mobile /p-6desktopPre-merge Checklist
Formatting constraints verified