Skip to content

fix: make quest popup body scrollable to prevent overflow - #84

Merged
rmstdope merged 8 commits into
mainfrom
quest-popup-scrollable-body
Apr 21, 2026
Merged

rmstdope merged 8 commits into
mainfrom
quest-popup-scrollable-body

Conversation

@rmstdope

Copy link
Copy Markdown
Owner

Summary

Quest popup was too tall on some screens, hiding the Continue button below the fold.

Changes

  • QuestModal.vue: cap modal at max-h-[90vh] with flex flex-col layout — 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 to p-4 mobile / p-6 desktop
  • game.spec.ts: 2 new e2e tests — Continue button within viewport bounds, modal card height ≤ viewport height (both with a very long body)

Pre-merge Checklist

  • Lint passes
  • Prettier passes
  • vue-tsc passes
  • Unit tests pass (254 tests)
  • E2E tests pass (quest modal suite)
  • Build passes

Formatting constraints verified

- 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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 QuestModal body 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(() => {

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
afterEach(() => {
afterEach(() => {
vi.clearAllTimers()

Copilot uses AI. Check for mistakes.
Comment thread web/src/components/TopBar.vue Outdated
Comment on lines +21 to +25
const isFullscreen = ref(false)

document.addEventListener('fullscreenchange', () => {
isFullscreen.value = !!document.fullscreenElement
})

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 96 to 125
<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>

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread web/src/App.vue
Comment on lines +10 to 31
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()
})

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +48
<!-- 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">

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +148
/** 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)

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 uses AI. Check for mistakes.
@rmstdope

Copy link
Copy Markdown
Owner Author

@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>

Copilot AI commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

@copilot apply changes based on the comments in this thread

Applied all changes from the review thread in commit 7eac5f8:

  • useEntityMenu.test.ts: Added vi.clearAllTimers() before vi.useRealTimers() in afterEach to prevent timer leaks.
  • TopBar.vue: Moved fullscreenchange listener to onMounted/onUnmounted and initialized isFullscreen from document.fullscreenElement on mount.
  • CommandInput.vue: Added explicit aria-label attributes to all 5 quick-action buttons (Look, Search, Inventory, Spells, Help).
  • QuestModal.vue: Implemented the bottom fade gradient (bg-gradient-to-t from-bg-card to-transparent) that was mentioned in the PR description but missing.
  • useEntityMenu.ts: Added onUnmounted() cleanup for the ghost-guard timer, guarded with getCurrentInstance() so it only registers within a component context (avoids lifecycle warnings in tests).

@rmstdope
rmstdope merged commit d218391 into main Apr 21, 2026
6 of 7 checks passed
@rmstdope
rmstdope deleted the quest-popup-scrollable-body branch April 21, 2026 11:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants