Two maps placed their share menu correctly because they were named in a string literal
DRV1650-SHARE-MENU-MOBILE-OFFSCREEN. On mobile the share/embed menu opens off-screen to
the right on Cities with Internet and Gospel Resources, while Simple Research and Prayer
Progress are correct.
THE SPLIT IS NOT A PLUGIN DIFFERENCE. MapControlMenu resolved the area it places inside
with closest('.dsm-map-area, .rm-map-area, .dm-map-area, [data-map-area]') -- three
hardcoded plugin class names. doxa-simple-map and research-map are on that list. Everyone
else resolved `|| null`, and place() answers a null area by returning an EMPTY STYLE,
which leaves .mcm{position:absolute} at its STATIC position: below-and-right of a button
already at the top right. Off the page.
So the two maps that worked worked because they were named in a string in another
component, and the bug was decided by a literal rather than by geometry. It is also the
SAME failure the 2026-09-22 dashboard fix documents twenty lines below -- not "a wrong
side was chosen" but "nothing chose anything" -- returning through a different door.
MEASURED: cities-internet, gospel-resources, key-cities and upg-neighbor carry no
map-area class and no data-map-area, so FOUR plugins were unanchored. The Driver reported
two. A fix aimed at the two he named would have left the other two, and a gate written
around them would have passed.
The named area is still preferred, because it is a real map's walls and a menu must not
outgrow them. When a plugin declares none, the anchor falls back to the toolbar's own
offsetParent -- the containing block this menu is absolutely positioned against, so
placing against it is correct by definition rather than by registration. A plugin that
adopts a class later simply gets the better anchor; nothing has to be registered anywhere
for a menu to be on screen.
check-map-menu-anchor asserts the PROPERTY rather than the list: that the resolution
cannot produce null from a real toolbar, that place() still guards a genuinely absent
one, and that a declared map area is still preferred over the fallback. Negative-tested:
restoring the allowlist-only resolution turns it red.
NEEDS EYES: this is rendered geometry and I work headless. What I can prove is that the
anchor is no longer decided by a name; whether the menu now sits correctly at 390px on
all four is rega's to measure.
This commit is contained in:
parent
713481b330
commit
948a00c7da
3 changed files with 89 additions and 1 deletions
70
internal/check-map-menu-anchor.mjs
Normal file
70
internal/check-map-menu-anchor.mjs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-map-menu-anchor — a map control menu must always have an anchor, so it can never fall to its
|
||||
* static position off the edge of the page.
|
||||
*
|
||||
* THE DEFECT (Driver, 2026-09-28, mobile): the share/embed menu opens OFF-SCREEN to the right on Cities
|
||||
* with Internet and Gospel Resources, while Simple Research and Prayer Progress are correct.
|
||||
*
|
||||
* ★ THE SPLIT WAS AN ALLOWLIST, NOT A PLUGIN DIFFERENCE. MapControlMenu resolved its containing area
|
||||
* with `closest('.dsm-map-area, .rm-map-area, .dm-map-area, [data-map-area]')` — three hardcoded plugin
|
||||
* class names. doxa-simple-map and research-map are on that list; everyone else resolved `|| null`, and
|
||||
* place() answers a null area with an EMPTY STYLE, leaving `.mcm{position:absolute}` at its STATIC
|
||||
* position: below-and-right of a button already at the top right. The two maps that worked were the two
|
||||
* that happened to be named in a string literal in another component.
|
||||
*
|
||||
* MEASURED: cities-internet, gospel-resources, key-cities and upg-neighbor carry NO map-area class and
|
||||
* no data-map-area. The Driver reported two; the allowlist made it four. A gate that only checked the
|
||||
* two he named would have passed the other two.
|
||||
*
|
||||
* SO THIS ASSERTS THE PROPERTY, NOT THE LIST: whatever a plugin calls its map area, the menu resolves
|
||||
* SOMETHING to anchor against. The fallback is the toolbar's own offsetParent — the containing block the
|
||||
* menu is absolutely positioned against — which is correct by definition rather than by registration.
|
||||
*
|
||||
* Exit 0 pass · 1 fail.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const TAG = 'MAP-MENU-ANCHOR'
|
||||
const fails = []
|
||||
const F = join(ROOT, 'library/components/map-controls/MapControlMenu.vue')
|
||||
const src = readFileSync(F, 'utf8')
|
||||
const code = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
|
||||
|
||||
// 1. the area resolution must not be able to produce null from a real toolbar element.
|
||||
const m = code.match(/host\.value\s*=\s*([^\n]+)/)
|
||||
if (!m) {
|
||||
fails.push('the area resolution moved — this gate cannot find `host.value = …` and would pass blind.')
|
||||
} else {
|
||||
const expr = m[1]
|
||||
if (!/offsetParent|parentElement|document\.body/.test(expr)) {
|
||||
fails.push('MapControlMenu resolves its anchor from a class allowlist with no structural fallback:\n'
|
||||
+ ` ${expr.trim()}\n`
|
||||
+ ' A plugin whose map area is not named in that list gets a null area, place() returns an\n'
|
||||
+ ' EMPTY style, and the menu renders at its static position — off the page. That is the\n'
|
||||
+ ' Driver-reported mobile bug, and it is decided by a string literal rather than by geometry.')
|
||||
}
|
||||
}
|
||||
|
||||
// 2. place() must still bail safely when there is genuinely no toolbar (SSR, an unmounted menu).
|
||||
if (!/if\s*\(\s*!tb\s*\|\|\s*!area\s*\)/.test(code)) {
|
||||
fails.push('place() no longer guards a missing toolbar/area; it must still return rather than throw.')
|
||||
}
|
||||
|
||||
// 3. and the named-area behaviour must be PREFERRED, not replaced: a map menu must still be bounded by
|
||||
// the map's own walls where one is declared, or it can overflow a real map.
|
||||
if (!/closest\(\s*AREA\s*\)/.test(code)) {
|
||||
fails.push('the named map area is no longer preferred — a menu must be bounded by a real map\'s walls '
|
||||
+ 'where one is declared, and only fall back to the offset parent when none is.')
|
||||
}
|
||||
|
||||
if (fails.length) {
|
||||
console.error(`${TAG}: FAIL`)
|
||||
for (const f of fails) console.error(` ✗ ${f}`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(`${TAG}: PASS — a named map area is preferred, and a plugin that declares none still resolves an `
|
||||
+ 'anchor structurally, so the menu cannot fall back to its static off-page position.')
|
||||
|
|
@ -71,6 +71,17 @@ const host = ref(null)
|
|||
const el = ref(null)
|
||||
const style = ref({})
|
||||
|
||||
// ★ AN ALLOWLIST OF THREE PLUGIN CLASS NAMES DECIDED WHETHER THIS MENU HAD AN ANCHOR AT ALL, and a
|
||||
// plugin not on the list got no placement rather than a worse one. `.dsm-map-area` is doxa-simple-map,
|
||||
// `.rm-map-area` is research-map, `.dm-map-area` is demo-map -- so Prayer Progress and Simple Research
|
||||
// placed correctly and everyone else fell through to `|| null`, which place() answers with an EMPTY
|
||||
// STYLE, which leaves `.mcm{position:absolute}` at its STATIC position: below-and-right of a button
|
||||
// already at the top right. Off the page.
|
||||
// That is the identical failure the 2026-09-22 dashboard fix describes below, returning through a
|
||||
// different door: not "a wrong side was chosen" but "nothing chose anything".
|
||||
// MEASURED 2026-09-28: cities-internet, gospel-resources, key-cities and upg-neighbor carry NO map-area
|
||||
// class and no data-map-area, so all four were unanchored. The Driver reported two of them; the list is
|
||||
// what made it four.
|
||||
const AREA = '.dsm-map-area, .rm-map-area, .dm-map-area, [data-map-area]'
|
||||
|
||||
function place() {
|
||||
|
|
@ -127,7 +138,13 @@ function onClickOutside(e) {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
host.value = toolbar?.el?.value?.closest(AREA) || null
|
||||
// ★ NEVER null. A named area is still preferred -- it is the MAP's walls, and a menu must not outgrow
|
||||
// them -- but when a plugin has not tagged one, the toolbar's own offsetParent is the containing block
|
||||
// this menu is absolutely positioned against, so placing against it is geometrically correct by
|
||||
// definition rather than by convention. A plugin that adopts a class or data-map-area later simply
|
||||
// gets the better anchor; nothing has to be registered anywhere for the menu to be on screen.
|
||||
const tbEl = toolbar?.el?.value || null
|
||||
host.value = tbEl?.closest(AREA) || tbEl?.offsetParent || tbEl?.parentElement || null
|
||||
host.value?.addEventListener(OPEN_EVENT, onOtherOpened)
|
||||
window.addEventListener('resize', onResize)
|
||||
document.addEventListener('click', onClickOutside, true)
|
||||
|
|
|
|||
|
|
@ -224,6 +224,7 @@
|
|||
"check:scrim-viewport:self-test": "node internal/check-scrim-covers-viewport.mjs --self-test",
|
||||
"check:script-flags": "node internal/check-script-flags-reachable.mjs",
|
||||
"check:settled": "node internal/settled-tree-gate.mjs",
|
||||
"check:map-menu-anchor": "node internal/check-map-menu-anchor.mjs",
|
||||
"check:embed-url-absolute": "node internal/check-embed-url-absolute.mjs",
|
||||
"check:setup-order": "node internal/check-setup-order.mjs",
|
||||
"check:sfc-parse": "node internal/check-sfc-script-parses.mjs",
|
||||
|
|
|
|||
Loading…
Reference in a new issue