|
|
|
|
@ -68,9 +68,9 @@ const props = defineProps({
|
|
|
|
|
/**
|
|
|
|
|
* The map's STRATEGY, as a catalogue key PREFIX (e.g. 'mapInfo.tab.upg-100.strategy'): <prefix>.bottom is
|
|
|
|
|
* the bottom line, <prefix>.p1, .p2 … the points, read in order until the first one that is missing. So
|
|
|
|
|
* the number of points is the catalogue's business, never a count kept here. With a strategy the popup
|
|
|
|
|
* shows the intro (the lead), then the strategy; a map that also has examples gets two tabs, EXAMPLES
|
|
|
|
|
* first. The takeaway is not shown beside a strategy, which supersedes it.
|
|
|
|
|
* the number of points is the catalogue's business, never a count kept here. The popup shows the short
|
|
|
|
|
* description (the lead) first, then the strategy as a collapsed disclosure, then the examples. The
|
|
|
|
|
* takeaway is not shown beside a strategy, which supersedes it.
|
|
|
|
|
*/
|
|
|
|
|
strategyKey: { type: String, default: '' },
|
|
|
|
|
note: { type: String, default: '' },
|
|
|
|
|
@ -130,19 +130,7 @@ function strategyPoints() {
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
const hasStrategy = () => !!strategyBottom()
|
|
|
|
|
// Two tabs only where there is something on both sides: examples AND a strategy.
|
|
|
|
|
const hasTabs = () => hasStrategy() && (props.examples || []).length > 0
|
|
|
|
|
const view = ref('examples')
|
|
|
|
|
// The ARIA tabs pattern: Left/Right move between the tabs and focus follows. With two tabs either key simply
|
|
|
|
|
// switches, so a right-to-left reader needs no mirrored mapping.
|
|
|
|
|
const tabIds = () => ({ ex: 'mi-tab-ex-' + props.storageKey, st: 'mi-tab-st-' + props.storageKey, panel: 'mi-panel-' + props.storageKey })
|
|
|
|
|
function onTabKey(e) {
|
|
|
|
|
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
const list = e.currentTarget // null once dispatch ends, so take it now, not inside nextTick
|
|
|
|
|
view.value = view.value === 'examples' ? 'strategy' : 'examples'
|
|
|
|
|
nextTick(() => { const btn = list && list.querySelector('[aria-selected="true"]'); if (btn) btn.focus() })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const num = (n) => { try { return new Intl.NumberFormat(lang()).format(n) } catch { return String(n) } }
|
|
|
|
|
const isRtl = () => lang() === 'ar'
|
|
|
|
|
|
|
|
|
|
@ -199,7 +187,6 @@ const failed = reactive({})
|
|
|
|
|
async function show() {
|
|
|
|
|
opens.value++
|
|
|
|
|
open.value = true
|
|
|
|
|
view.value = 'examples'
|
|
|
|
|
// A page-load pointer is done for this load once the popup has been opened (in memory only).
|
|
|
|
|
if (pageLoad()) pointerDoneThisLoad.add(props.storageKey)
|
|
|
|
|
await nextTick()
|
|
|
|
|
@ -449,17 +436,24 @@ useShadowStyles(`
|
|
|
|
|
.mi-close:focus-visible, .mi-toggle:focus-visible, .mi-hit:focus-visible, .mi-full:focus-visible{
|
|
|
|
|
outline:3px solid #2563eb; outline-offset:2px; }
|
|
|
|
|
.mi-body{ overflow-y:auto; padding:16px 20px 20px; -webkit-overflow-scrolling:touch; }
|
|
|
|
|
.mi-lead{ margin:0 0 6px; font-size:16px; }
|
|
|
|
|
/* The short description is the first thing the eye should find: the Rudder blue (#38bdf8) as the ACCENT
|
|
|
|
|
(a left bar on a pale band of the same hue), and the text in a darker shade of that hue (#0369a1), since
|
|
|
|
|
the accent itself is only ~2.1:1 on white and would be hard to read as text. */
|
|
|
|
|
.mi-bluf{ margin:0 0 12px; padding:10px 12px; background:#f0f9ff; border-inline-start:4px solid #38bdf8; border-radius:6px; }
|
|
|
|
|
.mi-bluf-h{ margin:0 0 4px; font-size:13px; font-weight:700; color:#0c4a6e; }
|
|
|
|
|
.mi-lead{ margin:0; font-size:16px; font-weight:500; color:#0369a1; }
|
|
|
|
|
.mi-takeaway{ margin:0 0 14px; color:#3b4256; }
|
|
|
|
|
.mi-takeaway b{ color:#1b1f2a; }
|
|
|
|
|
.mi-ex{ margin:18px 0 0; }
|
|
|
|
|
.mi-tabs{ display:flex; gap:4px; margin:14px 0 0; border-bottom:1px solid #e6e8ee; }
|
|
|
|
|
.mi-tab{ min-height:44px; padding:0 16px; border:none; background:none; color:#3b4256; font:inherit; font-weight:600;
|
|
|
|
|
cursor:pointer; border-bottom:3px solid transparent; margin-bottom:-1px; }
|
|
|
|
|
.mi-tab[aria-selected="true"]{ color:#1b1f2a; border-bottom-color:#3b463d; }
|
|
|
|
|
.mi-tab:focus-visible{ outline:3px solid #2563eb; outline-offset:-3px; }
|
|
|
|
|
.mi-strategy{ margin:16px 0 0; }
|
|
|
|
|
.mi-strategy h3{ margin:0 0 6px; }
|
|
|
|
|
.mi-strategy{ margin:14px 0 0; border:1px solid #e6e8ee; border-radius:10px; }
|
|
|
|
|
.mi-strategy-sum{ display:flex; align-items:center; justify-content:space-between; gap:8px; min-height:44px;
|
|
|
|
|
padding:0 14px; cursor:pointer; font-weight:700; color:#1b1f2a; list-style:none; }
|
|
|
|
|
.mi-strategy-sum::-webkit-details-marker{ display:none; }
|
|
|
|
|
.mi-strategy-sum:focus-visible{ outline:3px solid #2563eb; outline-offset:2px; border-radius:10px; }
|
|
|
|
|
.mi-chev{ transition:transform .15s; }
|
|
|
|
|
.mi-strategy[open] .mi-chev{ transform:rotate(180deg); }
|
|
|
|
|
.mi-strategy > .mi-bottom, .mi-strategy > .mi-points{ margin-inline:14px; }
|
|
|
|
|
.mi-strategy > :last-child{ margin-bottom:14px; }
|
|
|
|
|
.mi-bottom{ margin:0 0 8px; font-weight:600; color:#1b1f2a; }
|
|
|
|
|
.mi-points{ margin:0; padding-inline-start:20px; }
|
|
|
|
|
.mi-points li{ margin:0 0 6px; }
|
|
|
|
|
@ -554,6 +548,7 @@ useShadowStyles(`
|
|
|
|
|
styles are injected: a page-level rule cannot reach into a shadow root. */
|
|
|
|
|
@media (prefers-reduced-motion: reduce){
|
|
|
|
|
.mi-pointer, .mi .mi-pointer.mi-ctl{ animation:none; }
|
|
|
|
|
.mi-chev{ transition:none; }
|
|
|
|
|
}
|
|
|
|
|
`, 'map-info')
|
|
|
|
|
</script>
|
|
|
|
|
@ -594,27 +589,22 @@ useShadowStyles(`
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="mi-body">
|
|
|
|
|
<p v-if="leadKey || lead" class="mi-lead">{{ text(leadKey, lead) }}</p>
|
|
|
|
|
<!-- BOTTOM LINE FIRST: the map's short description, marked, in blue, so the eye lands there. -->
|
|
|
|
|
<section v-if="leadKey || lead" class="mi-bluf" :aria-labelledby="'mi-bluf-' + storageKey">
|
|
|
|
|
<h3 :id="'mi-bluf-' + storageKey" class="mi-bluf-h">{{ t('mapInfo.shortDescription') }}</h3>
|
|
|
|
|
<p class="mi-lead">{{ text(leadKey, lead) }}</p>
|
|
|
|
|
</section>
|
|
|
|
|
<p v-if="(takeawayKey || takeaway) && !hasStrategy()" class="mi-takeaway"><b>{{ t('mapInfo.takeaway') }}</b> {{ text(takeawayKey, takeaway) }}</p>
|
|
|
|
|
|
|
|
|
|
<div v-if="hasTabs()" class="mi-tabs" role="tablist" @keydown="onTabKey">
|
|
|
|
|
<button :id="tabIds().ex" type="button" role="tab" class="mi-tab" :aria-selected="String(view === 'examples')" :aria-controls="tabIds().panel"
|
|
|
|
|
:tabindex="view === 'examples' ? 0 : -1" @click="view = 'examples'">{{ t('mapInfo.tabExamples') }}</button>
|
|
|
|
|
<button :id="tabIds().st" type="button" role="tab" class="mi-tab" :aria-selected="String(view === 'strategy')" :aria-controls="tabIds().panel"
|
|
|
|
|
:tabindex="view === 'strategy' ? 0 : -1" @click="view = 'strategy'">{{ t('mapInfo.tabStrategy') }}</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div :id="hasTabs() ? tabIds().panel : null" :role="hasTabs() ? 'tabpanel' : null"
|
|
|
|
|
:aria-labelledby="hasTabs() ? (view === 'examples' ? tabIds().ex : tabIds().st) : null">
|
|
|
|
|
|
|
|
|
|
<section v-if="hasStrategy() && (!hasTabs() || view === 'strategy')" class="mi-strategy">
|
|
|
|
|
<h3 v-if="!hasTabs()">{{ t('mapInfo.tabStrategy') }}</h3>
|
|
|
|
|
<!-- The strategy, collapsed until asked for (a native disclosure: keyboard and screen readers for free).
|
|
|
|
|
Keyed by the open count, so every open starts collapsed. -->
|
|
|
|
|
<details v-if="hasStrategy()" :key="'st-' + opens" class="mi-strategy">
|
|
|
|
|
<summary class="mi-strategy-sum"><span>{{ t('mapInfo.tabStrategy') }}</span><span class="mi-chev" aria-hidden="true">▾</span></summary>
|
|
|
|
|
<p class="mi-bottom">{{ strategyBottom() }}</p>
|
|
|
|
|
<ul v-if="strategyPoints().length" class="mi-points">
|
|
|
|
|
<li v-for="(p, k) in strategyPoints()" :key="k">{{ p }}</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<template v-if="!hasTabs() || view === 'examples'">
|
|
|
|
|
</details>
|
|
|
|
|
|
|
|
|
|
<section v-for="(ex, i) in examples" v-show="!failed[i]" :key="i" class="mi-ex">
|
|
|
|
|
<h3>{{ titleOf(ex) }}</h3>
|
|
|
|
|
@ -681,9 +671,6 @@ useShadowStyles(`
|
|
|
|
|
</template>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p v-if="noteKey || note" class="mi-note">{{ text(noteKey, note) }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|