The dashboard header gains an info button ("What can this map do for you?") to
the left of Share. Both are rounded squares at the vertical tabs' radius
(--vtl-radius), matching the app's own roundedness; map controls stay round.
Built as a reusable, parameterized header-button form so other dashboards adopt
it by prop, not by copy:
- MapControlButton: new shape="rounded-square" (border-radius: var(--vtl-radius,8px)).
- ShareButton / MapInfoButton: a shape prop forwarded to MapControlButton;
ShareButton also squares its rotating ring so a round ring never wraps a square.
Defaults stay 'round' — every existing consumer is unchanged in behaviour.
- research-dashboard DashboardTopBar mounts the ⓘ and Share as one header-button
group, sized to the bar with the 44px tap target kept for both. DOM order
[ⓘ, Share] puts ⓘ at the logical start: left in English, right of Share in
Arabic (the flex row reverses under rtl).
- doxa-maps-spa dashboard-plus-grid tab -> trigger:'control', so the tab's
MapInfo is driven by the header ⓘ and the gallery's floating ⓘ is suppressed:
exactly one ⓘ, fail-open. No new strings (reuses mapInfo.open, 11 locales).
82 lines
4.2 KiB
Vue
82 lines
4.2 KiB
Vue
<!--
|
|
MapInfoButton.vue — the map's "what can this map do for you?" control, IN the map's control column.
|
|
|
|
It opens the host's MapInfo popup and owns no popup of its own. The host (the gallery) mounts one
|
|
MapInfo per tab over the tab's pane, with trigger="control"; this button reaches it with a bubbling,
|
|
COMPOSED event, so the plugin's shadow root is no wall. The popup, its examples and its focus handling
|
|
stay in one place (library/components/info/MapInfo.vue).
|
|
|
|
★ IT SHOWS ONLY WHEN A HOST ANSWERS. A control that opens nothing is worse than no control: a
|
|
standalone leaf or a partner page has no MapInfo. So on mount the control ASKS ('dxmaps:mapinfo-control',
|
|
detail.ack = false), and a MapInfo in control mode on the SAME pane sets ack = true while the event is
|
|
being dispatched (dispatch is synchronous, so the answer is there when dispatchEvent returns). No answer:
|
|
nothing renders (not even a hidden button), and it asks again when a host announces itself later
|
|
('dxmaps:mapinfo-host' on window, for a MapInfo that mounted after the map). Use it inside a MapToolbar.
|
|
|
|
★ HIDING IS NOT THE HOST'S SIGNAL. The host decides whether its floating ⓘ is needed by MEASURING this
|
|
button (connected, a non-zero on-screen rect, not inert), never by an unmount event: a detached element
|
|
cannot bubble anything to the pane. See MapInfo's control mode.
|
|
|
|
Props:
|
|
isDark — theme flag forwarded from parent
|
|
-->
|
|
<script setup>
|
|
import { ref, inject, nextTick, getCurrentInstance, onMounted, onBeforeUnmount } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import MapControlButton from './MapControlButton.vue'
|
|
|
|
const { t } = useI18n()
|
|
|
|
defineProps({
|
|
isDark: { type: Boolean, default: false },
|
|
// 'round' (default) is the map-control form. 'rounded-square' is the dashboard HEADER form (the ⓘ
|
|
// sits in a header bar beside Share, not in a map's control column) — forwarded to MapControlButton.
|
|
shape: { type: String, default: 'round' },
|
|
})
|
|
|
|
// ★ AN UNANSWERED CONTROL ADDS NO CHILD TO THE COLUMN. The button renders only once a host has answered
|
|
// (v-if, not v-show): a display:none child is still a child, so it would take the square toolbar's
|
|
// :last-child corners (MapToolbar's controls="square") from the last button a reader can see. So the
|
|
// QUESTION is asked from the toolbar's own element (provided by MapToolbar as mapToolbar.el), or from this
|
|
// component's placeholder's parent, and once answered the rendered button announces ITSELF, so the host
|
|
// measures and focuses the real element.
|
|
const toolbar = inject('mapToolbar', null)
|
|
const self = getCurrentInstance()
|
|
const hosted = ref(false)
|
|
const btn = ref(null)
|
|
const node = () => (btn.value && btn.value.$el) || null
|
|
const origin = () => node() || (toolbar && toolbar.el && toolbar.el.value) || (self && self.proxy && self.proxy.$el && self.proxy.$el.parentNode) || null
|
|
function announce(from, el) {
|
|
const detail = { el, ack: false }
|
|
try { from.dispatchEvent(new CustomEvent('dxmaps:mapinfo-control', { bubbles: true, composed: true, detail })) } catch { /* no CustomEvent */ }
|
|
return detail.ack === true
|
|
}
|
|
function ask() {
|
|
const from = origin()
|
|
if (!from) return
|
|
const was = hosted.value
|
|
hosted.value = announce(from, node())
|
|
if (hosted.value && !was) nextTick(() => { const el = node(); if (el) announce(el, el) })
|
|
}
|
|
function openInfo() {
|
|
const el = node()
|
|
if (!el) return
|
|
try { el.dispatchEvent(new CustomEvent('dxmaps:mapinfo-open', { bubbles: true, composed: true, detail: { el } })) } catch { /* no CustomEvent */ }
|
|
}
|
|
onMounted(() => {
|
|
nextTick(ask)
|
|
window.addEventListener('dxmaps:mapinfo-host', ask)
|
|
})
|
|
onBeforeUnmount(() => { window.removeEventListener('dxmaps:mapinfo-host', ask) })
|
|
</script>
|
|
|
|
<template>
|
|
<MapControlButton v-if="hosted" ref="btn" data-control="map-info" :is-dark="isDark" :shape="shape" :title="t('mapInfo.open')"
|
|
aria-haspopup="dialog" aria-expanded="false" @click="openInfo">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
|
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" />
|
|
<rect x="11" y="10" width="2" height="7" rx="1" fill="currentColor" />
|
|
<circle cx="12" cy="7" r="1.3" fill="currentColor" />
|
|
</svg>
|
|
</MapControlButton>
|
|
</template>
|