Make the legend's full-screen button do something

The ring legend declared a fullscreen emit and rendered a labelled, focusable
button for it. The parent bound select and not fullscreen, so the button looked
right, tabbed right, announced itself right, and did nothing. It is a Driver ask,
and it is the half-wired shape exactly: the visible half worked, so nothing
appeared broken.

The fix is not to bind the emit. The button is now the shared control, so there
is one implementation instead of two — and it brings the part a second one would
have missed. iOS Safari does not implement requestFullscreen on arbitrary
elements and fails silently, so the shared control carries a pseudo-fullscreen
fallback. A hand-rolled handler would have worked everywhere I could test it and
nowhere I could not.

A first attempt kept the hand-rolled button and called the shared component's
toggle through a template ref. That component has no defineExpose, so under
script setup the method is private and the call would have resolved to undefined
— silently reintroducing the same defect while appearing to fix it. Checked
before committing rather than after.

The panel element is passed in, because full screen should show the chart and the
legend together, and the legend does not own the panel it sits in.

Found by sweeping for the half-wired-config class rather than by anyone reporting
it, which is what that class is for: nothing about it looks wrong from outside.
This commit is contained in:
Doxa Maps 2026-09-28 18:32:31 +00:00
parent 7e9d4d62cb
commit e2086c4d2f
2 changed files with 29 additions and 5 deletions

View file

@ -27,6 +27,7 @@ import { loadNearRows, indexFor } from '../nearRows.js'
import { layoutCenteredSet, ringTally, inwardArrow, ringRadii } from '@map/components/charts/centeredSet.js'
import GiRingLegend from './GiRingLegend.vue'
import GiCenteredSetMap from './GiCenteredSetMap.vue'
import FullscreenButton from '@map/components/map-controls/FullscreenButton.vue'
const T = useGiT()
const board = useBoardState()
@ -34,6 +35,9 @@ const near = ref(null)
// Which ring the reader has asked about. null = none; the chart dims the others rather than
// hiding them, because a ring with no dots left would read as a ring with no groups.
const activeRing = ref(null)
// The panel element, so full screen shows the chart AND its legend rather than one of them.
const panelEl = ref(null)
const failed = ref('')
onMounted(async () => {
@ -106,7 +110,7 @@ useShadowStyles(
</script>
<template>
<div class="gcs">
<div class="gcs" ref="panelEl">
<p v-if="failed" class="gcs-cap">
{{ T('gospelInternet.centered.failed', 'The neighbour table could not be loaded, so this chart is not drawn.') }}
<span class="gcs-warn" dir="ltr">{{ failed }}</span>
@ -121,8 +125,18 @@ useShadowStyles(
@select="activeRing = $event" />
</div>
<div class="gcs-aside">
<!-- ★ @fullscreen WAS NOT BOUND. The legend declares emits ['select','fullscreen'] and
renders a labelled, focusable full-screen button; only select was wired, so the
button rendered, looked right and did nothing. It is a Driver ask, and it is the
half-wired-config shape: the visible half worked, so nothing looked broken.
The handler delegates to the SHARED fullscreen implementation rather than calling
requestFullscreen here. That is not ceremony: iOS Safari does not implement
requestFullscreen on arbitrary elements and silently fails, so the shared control
carries a pseudo-fullscreen fallback that a second implementation would omit — and
would omit invisibly, on the one platform hardest to test. -->
<GiRingLegend :groups="prepared.groups" :unmatched="prepared.unmatched"
:active-ring="activeRing" @select="activeRing = $event" />
:active-ring="activeRing" :panel="panelEl" @select="activeRing = $event"
/>
</div>
<p v-if="tally.centre || tally.unplaced || prepared.unmatched" class="gcs-warn">
<span v-if="tally.centre">{{ T('gospelInternet.centered.reached', 'Already reached, so at the centre:') }} <b dir="ltr">{{ fmt(tally.centre) }}</b>. </span>

View file

@ -19,17 +19,21 @@
* Emits: select(ring | null), fullscreen
*/
import { ref, computed } from 'vue'
import FullscreenButton from '@map/components/map-controls/FullscreenButton.vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { formatCount } from '@map/utils/formatCount.js'
import { useGiT } from '../useGiT.js'
import { BANDS, CULTURES, CENTRE_COLOR, ringInfo, ringFor, ringSummary } from '../ringInfo.js'
const props = defineProps({
// The element full screen should fill — the whole panel, so the chart and this legend go
// together. Passed in because the legend does not own the panel it sits in.
panel: { type: Object, default: null },
groups: { type: Array, default: () => [] },
unmatched: { type: Number, default: 0 },
activeRing: { type: Number, default: null },
})
const emit = defineEmits(['select', 'fullscreen'])
const emit = defineEmits(['select'])
const T = useGiT()
// formatCount, not toLocaleString(): a bare toLocaleString follows the READER'S BROWSER,
// so one page could print 2,074 here and 2.074 in the panel beside it. formatCount holds
@ -115,8 +119,14 @@ useShadowStyles(
<section class="grl" :aria-label="T('gospelInternet.rings.title', 'What the rings mean')">
<div class="grl-h">
<h3>{{ T('gospelInternet.rings.title', 'What the rings mean') }}</h3>
<button type="button" class="grl-fs" :title="T('gospelInternet.rings.fullscreen', 'Full screen')"
:aria-label="T('gospelInternet.rings.fullscreen', 'Full screen')" @click="emit('fullscreen')">⛶</button>
<!-- ★ THE SHARED CONTROL, NOT A GLYPH THAT EMITS. This was a hand-rolled button emitting
`fullscreen`, and the parent never bound it — so it rendered, was focusable, was
labelled, and did nothing. Rather than bind the emit, the button IS the shared
FullscreenButton now: one control instead of two, and it brings the part a second
implementation would have missed — iOS Safari does not implement requestFullscreen on
arbitrary elements and fails silently, so the shared control carries a pseudo-fullscreen
fallback. `panel` is the element to fill: the chart AND this legend, not one of them. -->
<FullscreenButton :map-container="panel" />
</div>
<p class="grl-intro">{{ T('gospelInternet.rings.intro', 'Each unreached group sits on a ring by how close it is to its nearest reached group. The closer the ring, the more naturally the gospel can travel.') }}</p>