diff --git a/library/components/info/MapInfo.vue b/library/components/info/MapInfo.vue index 81dd7f8af..68c5d1b25 100644 --- a/library/components/info/MapInfo.vue +++ b/library/components/info/MapInfo.vue @@ -27,8 +27,8 @@ * * ★ THE POINTER GOES FOR GOOD, per map, once its popup has been opened AND closed (opening alone * could be a mis-tap). The checkbox in the popup turns the pointer off on EVERY map. A map given - * `pointer-keep` offers "Keep tooltip activated" instead: opening alone retires its pointer, and the box - * keeps it showing on every load (see the prop). All of it lives in + * `pointer-keep` points ONCE PER PAGE LOAD instead, and offers "Keep tooltip turned on" in place of that + * box (see the prop). What is stored lives in * localStorage, read through a guard: private browsing and sandboxed frames throw on access, and a * hint must never be the thing that breaks a page. The button itself always stays. * @@ -78,9 +78,11 @@ const props = defineProps({ */ trigger: { type: String, default: 'floating', validator: (v) => ['floating', 'control'].includes(v) }, /** - * Offer "Keep tooltip activated" in the popup, IN PLACE OF the every-map "don't point again" box. Off by - * default: opening the popup once retires this map's pointer, as before. Checked: the pointer keeps - * showing on every load. Per map (its storageKey), so it answers only for the map that offers it. + * The pointer model of a map that offers "Keep tooltip turned on" (IN PLACE OF the every-map "don't + * point again" box). Its pointer shows ONCE PER PAGE LOAD: the first time the map is on screen in that + * load. Opening the popup hides it for the rest of that load only, and the next full load shows it + * again: page-load memory (pointerDoneThisLoad, below), never storage. The box, off by default, keeps it + * showing after the popup closes and on every appearance; that preference IS stored, per map. */ pointerKeep: { type: Boolean, default: false }, }) @@ -109,16 +111,29 @@ function writeStore(k, v) { try { window.localStorage.setItem(k, v) } catch { /* const pointing = ref(false) const pointerOff = ref(false) const keep = ref(false) -// A kept pointer outranks both "seen" and the every-map off switch: it is this map's own, later answer. -const wantPointer = () => keep.value || (!pointerOff.value && readStore(SEEN + props.storageKey) !== '1') function refreshPointer() { pointerOff.value = readStore(OFF) === 'off' - keep.value = props.pointerKeep && readStore(KEEP + props.storageKey) === '1' - pointing.value = wantPointer() + if (props.pointerKeep) { + keep.value = readStore(KEEP + props.storageKey) === '1' + pointing.value = keep.value || !pointerDoneThisLoad.has(props.storageKey) + return + } + pointing.value = !pointerOff.value && readStore(SEEN + props.storageKey) !== '1' +} +// Another map on the page (same document) or another tab (storage event) turned the pointer off. A +// page-load pointer answers only its own keep box: the every-map switch is not offered beside it, and a +// switch the reader cannot see on this map must not silently take its pointer away. +function onPointerOff() { if (!props.pointerKeep) refreshPointer() } +function onStorage(e) { + if (props.pointerKeep) { + if (!e || e.key === KEEP + props.storageKey) { + keep.value = readStore(KEEP + props.storageKey) === '1' + if (keep.value) pointing.value = true + } + return + } + if (!e || e.key === OFF || e.key === SEEN + props.storageKey) refreshPointer() } -// Another map on the page (same document) or another tab (storage event) turned the pointer off. -function onPointerOff() { refreshPointer() } -function onStorage(e) { if (!e || e.key === OFF || e.key === SEEN + props.storageKey || e.key === KEEP + props.storageKey) refreshPointer() } onMounted(() => { refreshPointer() window.addEventListener(OFF_EVENT, onPointerOff) @@ -131,7 +146,7 @@ onBeforeUnmount(() => { function setPointerOff(off) { pointerOff.value = off writeStore(OFF, off ? 'off' : 'on') - pointing.value = wantPointer() + pointing.value = !off && readStore(SEEN + props.storageKey) !== '1' try { window.dispatchEvent(new CustomEvent(OFF_EVENT)) } catch { /* no window */ } } function setKeep(on) { @@ -148,16 +163,15 @@ const failed = reactive({}) async function show() { opens.value++ open.value = true - // With the keep box on offer, OPENING the popup is what retires the pointer (the box is right there - // to say otherwise), so a reader who opens it and reloads without closing is not pointed at again. - if (props.pointerKeep) writeStore(SEEN + props.storageKey, '1') + // A page-load pointer is done for this load once the popup has been opened (in memory only). + if (props.pointerKeep) pointerDoneThisLoad.add(props.storageKey) await nextTick() closeEl.value?.focus() } async function hide() { open.value = false - writeStore(SEEN + props.storageKey, '1') - pointing.value = keep.value + if (!props.pointerKeep) writeStore(SEEN + props.storageKey, '1') + pointing.value = props.pointerKeep && keep.value await nextTick() // Back to whichever button the reader can see: the map's own control when it is on screen, else ours. const back = props.trigger === 'control' && controlShown.value && control.value ? control.value : btnEl.value @@ -263,6 +277,11 @@ onBeforeUnmount(() => { }) watch(open, (v) => { control.value?.setAttribute('aria-expanded', String(v)) }) const floatingShown = () => props.trigger !== 'control' || !controlShown.value +// The pointer beside the control has been on screen: this page load has pointed once, so a remount (the +// gallery rebuilds a heavy tab on every switch back) does not point again. Marked on SCREEN, not on mount: +// a tab's MapInfo can mount inside a hidden pane. +watch(() => props.pointerKeep && pointing.value && !open.value && !floatingShown() && !!ctlAt.value, + (on) => { if (on) pointerDoneThisLoad.add(props.storageKey) }, { immediate: true }) // ★ aria-modal PROMISES THE READER THAT TAB STAYS INSIDE, so keep it there: Tab from the last control // wraps to the first, Shift+Tab from the first to the last. The active element is read from the shadow // root (document.activeElement would be the host element). Hidden controls are skipped. @@ -445,6 +464,13 @@ useShadowStyles(` `, 'map-info') + +