Wire the back-to-top on both surfaces, from the side that owns the scroller
The Driver asked for the arrow on the people-group grid and on the regions list. Those needed different wiring, and the reason is worth recording because it is not a preference. The regions dashboard owns its scroller: .rd-scroll has overflow-y:auto, so the plugin mounts the button itself and drives its own element. It deliberately does not touch the page's scroll — this renders inside a shadow root in someone else's page, and their scroll is not ours to move. The people-group grid owns no scroller at all. .pgg-root sets no overflow and .pgg-body has no rule; the grid simply grows and something outside it scrolls. In this shell that something is .vtl-host, which lives in the layout, outside every plugin's shadow root, so the grid could not reach it even if it should. So the arrow for that surface belongs in the layout, beside the circle the layout already owns, driving the pane it already owns. The grid is untouched. .vtl-host is per tab, so the arrow follows `active` rather than binding one ref, or it would scroll whichever pane rendered last. One component, two consumers, and each drives a scroller it actually owns. Checked that this adds nothing to the hardcoded-English census: 256 before, 256 after — the three regions entries there are PosterWall prop literals and predate this.
This commit is contained in:
parent
e5140f0b41
commit
63ce2d372c
2 changed files with 37 additions and 3 deletions
|
|
@ -129,6 +129,12 @@
|
|||
class="vtl-fab" type="button" :aria-expanded="String(navOpen)"
|
||||
@click="navOpen = true" :aria-label="label('openViews')">☰</button>
|
||||
|
||||
<!-- Beside the circle, driving the shell's own scroller. Reads --map-fab-beside, which this
|
||||
layout publishes only while the circle is up, so with no circle it falls back to the
|
||||
plain inset instead of leaving a gap where a button used to be. -->
|
||||
<BackToTopButton v-if="narrow" :target="activeHostEl"
|
||||
:labels="{ backToTop: T('actions.backToTop', 'Back to top') }" />
|
||||
|
||||
<div class="vtl-body">
|
||||
<!-- Rail visibility is CSS-driven via `transform`, never v-show/display:none — a display:none element
|
||||
cannot animate, so the drawer must stay in the DOM and only slide. DESKTOP: the rail is always
|
||||
|
|
@ -233,7 +239,8 @@
|
|||
element into imperatively — Vue never manages the element's own shadow DOM. -->
|
||||
<section
|
||||
v-for="row in mountedElementRows" :key="row.key"
|
||||
class="vtl-host" v-show="row.key === active" :hidden="row.key !== active">
|
||||
class="vtl-host" v-show="row.key === active" :hidden="row.key !== active"
|
||||
:ref="el => { if (el) hostEls[row.key] = el; else delete hostEls[row.key] }">
|
||||
<div class="vtl-host-slot" :ref="el => setHostSlot(row.key, el)"></div>
|
||||
<div v-if="hostState[row.key] === 'loading'" class="vtl-host-hint">{{ T('messages.loadingNamed', 'Loading {name}…').replace('{name}', row.label) }}</div>
|
||||
<div v-else-if="hostState[row.key] === 'error'" class="vtl-host-hint err">
|
||||
|
|
@ -250,6 +257,7 @@
|
|||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
import BackToTopButton from '@map/components/BackToTopButton.vue'
|
||||
import { tStatic } from '../i18n/index.js'
|
||||
|
||||
// This layout's chrome, read out of the catalogue. tStatic and not useI18n: it is the root of a
|
||||
|
|
@ -541,6 +549,7 @@ const VTL_SHADOW_CSS = `
|
|||
|
||||
export default defineComponent({
|
||||
name: 'VerticalTabsLayout',
|
||||
components: { BackToTopButton },
|
||||
props: {
|
||||
// Controlled active key (v-model:active). Matches an item's / row's `key`.
|
||||
active: { type: String, default: '' },
|
||||
|
|
@ -568,6 +577,10 @@ export default defineComponent({
|
|||
return { T: tr }
|
||||
},
|
||||
data() {
|
||||
// _hostEls: the .vtl-host pane per tab key. The shell's scroller is the ACTIVE pane, and
|
||||
// there is one per mounted tab, so the back-to-top has to follow `active` rather than
|
||||
// binding a single ref. Not reactive-deep on purpose: these are DOM nodes.
|
||||
|
||||
// _folderOverride: user toggles keyed by group key (true=open, false=closed). When a folder
|
||||
// key is absent here we fall back to the group's own `open` flag (default closed) — so a
|
||||
// folder authored with open:false starts collapsed and the user can still open it.
|
||||
|
|
@ -581,7 +594,8 @@ export default defineComponent({
|
|||
// listener. It lets us reconcile the two boolean state machines (navOpen = mobile drawer,
|
||||
// railCollapsed = desktop slim rail) at the breakpoint and drive rail visibility correctly.
|
||||
narrow: false, _mql: null, _onMedia: null,
|
||||
activatedKeys: {}, hostState: {}, hostError: {}, _hostSlots: {}, _hostMounted: {}
|
||||
activatedKeys: {}, hostState: {}, hostError: {}, _hostSlots: {}, _hostMounted: {},
|
||||
hostEls: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -594,6 +608,18 @@ export default defineComponent({
|
|||
* test for it.
|
||||
*/
|
||||
fabShown() { return this.narrow && !this.header && !this.navOpen },
|
||||
/**
|
||||
* The element the back-to-top arrow scrolls. .vtl-host is the shell's scroller
|
||||
* (overflow-y:auto) and there is one PER TAB, so this follows `active` rather than binding a
|
||||
* single ref — otherwise the arrow would drive whichever pane happened to render last.
|
||||
*
|
||||
* ★ THE ARROW LIVES HERE, NOT IN THE PLUGINS, and that is forced rather than chosen: the
|
||||
* people-group grid has no scroller of its own (.pgg-root sets no overflow and .pgg-body has
|
||||
* no rule at all), so in this shell the thing that scrolls is OUTSIDE the plugin's shadow
|
||||
* root and the plugin cannot reach it. A plugin that DOES own its scroller — the regions
|
||||
* dashboard's .rd-scroll — mounts the same component itself and drives its own element.
|
||||
*/
|
||||
activeHostEl() { return this.hostEls[this.active] || null },
|
||||
// Is the rail element visible at all? DESKTOP: always (it just shrinks when railCollapsed). MOBILE:
|
||||
// only when the off-canvas drawer is open (navOpen). Binding the rail's v-show to THIS (not raw
|
||||
// navOpen) prevents a mobile-closed drawer from hiding the rail after a resize back to desktop.
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import RegionCountryMap from './src/RegionCountryMap.vue'
|
|||
import StatCard from '@map/components/regions/StatCard.vue'
|
||||
import GoalCard from '@map/components/regions/GoalCard.vue'
|
||||
import ProgressBar from '@map/components/charts/ProgressBar.vue'
|
||||
import BackToTopButton from '@map/components/BackToTopButton.vue'
|
||||
import { SHADOW_BOX_SIZING_RESET } from './src/shadowReset.js'
|
||||
import { summarizeRegions, emptyStats, toNumber, PEOPLE_GROUP_LIST_FIELDS } from '@map/utils/countries-meta.js'
|
||||
|
||||
|
|
@ -160,6 +161,10 @@ const regions = ref([])
|
|||
// pre-parsed once here rather than re-fetched inside the map child, as DX-life's CountryMap
|
||||
// does -- one request per element, and the pins provably describe the same rows as the stats.
|
||||
const points = ref([])
|
||||
// The scroll container the back-to-top arrow drives. It is .rd-scroll rather than the window:
|
||||
// this plugin renders inside a shadow root in someone else's page, so the page's scroll is not
|
||||
// ours to move and may not even be the one the reader is using.
|
||||
const scrollEl = ref(null)
|
||||
const loading = ref(true)
|
||||
const loadFailed = ref(false)
|
||||
// The current selection, as { regionSlug, countrySlug } — seeded from profile-config so a
|
||||
|
|
@ -1017,7 +1022,7 @@ useShadowStyles(
|
|||
<div class="rd-root" :class="{ 'rd-root--all': isAll }" :lang="locale" :dir="isRTL ? 'rtl' : 'ltr'">
|
||||
<!-- a landmark, so a screen reader can jump to the dashboard body; the app probe
|
||||
measured none on the page and it costs nothing to name the region. -->
|
||||
<main class="rd-scroll" :aria-label="t('Region dashboard')">
|
||||
<main ref="scrollEl" class="rd-scroll" :aria-label="t('Region dashboard')">
|
||||
<div v-if="loading" class="rd-status">{{ t('Loading region dashboards…') }}</div>
|
||||
<div v-else-if="loadFailed" class="rd-status rd-failed" role="status">{{ t('Region data could not be loaded. This is a connection problem, not a filter.') }}</div>
|
||||
|
||||
|
|
@ -1216,5 +1221,8 @@ useShadowStyles(
|
|||
/>
|
||||
</template>
|
||||
</main>
|
||||
<!-- Inside .rd-root, which is position:absolute inset:0, so the button's own absolute
|
||||
positioning resolves against the plugin rather than the host page. -->
|
||||
<BackToTopButton :target="scrollEl" :labels="{ backToTop: t('Back to top') }" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
Loading…
Reference in a new issue