Wall of People Groups: focus returns to the wall when a dialog closes, and the map pin hint stays off the wall

This commit is contained in:
Doxa Maps 2026-09-28 20:02:12 +00:00
parent ef7325990a
commit bee12baea0
4 changed files with 14 additions and 2 deletions

View file

@ -122,6 +122,7 @@ export default {
if (this._wc) { this._wc.setWorld(this.canvasWidth, this.canvasHeight); return; }
this._wc = new WallCanvas(this.$refs.canvas, {
worldW: this.canvasWidth, worldH: this.canvasHeight,
label: this.$t('wall.title'),
spacing: this.cardSpacing, vSpacing: this.cardVerticalSpacing,
labels: (k, p, n) => (n === undefined ? this.$t(k, p) : this.$t(k, p, n)),
prayCount: (d) => Number(d.peoplePraying || 0) + (this.$store?.prayers?.[d.ID] || 0),

View file

@ -10,7 +10,10 @@ const FOCUSABLE = 'button:not([disabled]),[href],input:not([disabled]),textarea:
export function trapDialog(container, onClose) {
if (!container) return () => {}
const root = container.getRootNode ? container.getRootNode() : document
const prev = root.activeElement || document.activeElement
// The opener: the element that had focus, or (a click on a non-focusable surface leaves <body>)
// the wall's canvas, so focus has somewhere sensible to come back to.
let prev = root.activeElement || document.activeElement
if (!prev || prev === document.body) prev = (root.querySelector && root.querySelector('canvas.wall-canvas-el')) || prev
const focusables = () => [...container.querySelectorAll(FOCUSABLE)]
const onKey = (e) => {
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); onClose(); return }

View file

@ -50,6 +50,11 @@ export class WallCanvas {
this.canvas = document.createElement('canvas')
this.canvas.className = 'wall-canvas-el'
this.canvas.style.cssText = 'display:block;width:100%;height:100%;touch-action:none'
// Focusable, with a name: a dialog opened from a click on the wall returns focus HERE on close
// (a non-focusable canvas left focus on <body>), and the wall itself is reachable by keyboard.
this.canvas.tabIndex = 0
if (opts.label) this.canvas.setAttribute('aria-label', opts.label)
this.canvas.setAttribute('role', 'img')
host.appendChild(this.canvas)
this.ctx = this.canvas.getContext('2d')
this.icons = Object.fromEntries(Object.entries(ICON).map(([k, d]) => [k, new Path2D(d)]))

View file

@ -1,5 +1,8 @@
{
"type": "monolithic",
"label": "Wall of People Groups",
"note": "Monolithic composite (capability 3): ONE root .vue nests the zoomable d3 wall, its cards, and the prayer and comments dialogs. index.js registers exactly ONE tag (wall-of-people-groups); none of the inner components is independently embeddable."
"note": "Monolithic composite (capability 3): ONE root .vue nests the zoomable d3 wall, its cards, and the prayer and comments dialogs. index.js registers exactly ONE tag (wall-of-people-groups); none of the inner components is independently embeddable.",
"config": {
"pinHint": false
}
}