mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(directions): keyless A→B directions with turn-by-turn steps and FLY (#564)
Supersedes #219 by @spcpza, ported onto the current component layout with fixes from review. Verified before merge: format, package boundaries, unit suite, build, and the browser tracking suite; a browser harness for the change where one applies; adversarial review applied. Co-authored-by: spcpza <[email protected]>
This commit is contained in:
1 parent
9fc8d1966b
commit
d78696dc17
29 files changed
+4658
-65
No files matched your search
+103
-3
@@ -206,7 +206,23 @@ export class LayerPanel {
|
||||
this.setLayerParams(layer.id, chip.params, { origin: 'user' });
|
||||
});
|
||||
row.appendChild(controls);
|
||||
this._syncRowControls(controls, layer);
|
||||
// An ordered list below the chips, for a layer whose row carries a
|
||||
// sequence (turn-by-turn directions). Its own delegated listener, its
|
||||
// own container — the chip row stays a chip row.
|
||||
const list = document.createElement('ol');
|
||||
list.className = 'data-row-list';
|
||||
list.hidden = true;
|
||||
this._bind(list, 'click', (event) => {
|
||||
const button = event.target?.closest?.('.data-row-list-item');
|
||||
if (!button || button.disabled) return;
|
||||
const item = this._rowControlsFor(layer.id)?.list?.items?.find(
|
||||
(entry) => entry.id === button.dataset.listItemId,
|
||||
);
|
||||
if (item?.params)
|
||||
this.setLayerParams(layer.id, item.params, { origin: 'user' });
|
||||
});
|
||||
row.appendChild(list);
|
||||
this._syncRowControls(controls, layer, list);
|
||||
}
|
||||
|
||||
this._toggleContainer.appendChild(row);
|
||||
@@ -232,12 +248,14 @@ export class LayerPanel {
|
||||
* are replaced freely.
|
||||
* @param {HTMLElement|null} container The row's `.data-toggle-controls` node.
|
||||
* @param {object} layer Registered layer entry.
|
||||
* @param {HTMLElement|null} [listContainer] The row's `.data-row-list` node.
|
||||
*/
|
||||
_syncRowControls(container, layer) {
|
||||
_syncRowControls(container, layer, listContainer = null) {
|
||||
if (!container) return;
|
||||
const controls = layer.enabled ? this._rowControlsFor(layer.id) : null;
|
||||
const chips = controls?.chips || [];
|
||||
const legend = controls?.legend || [];
|
||||
this._syncRowList(listContainer, controls?.list || null);
|
||||
container.hidden = chips.length === 0 && legend.length === 0;
|
||||
|
||||
for (const node of [...container.children]) {
|
||||
@@ -285,6 +303,84 @@ export class LayerPanel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a row's ordered list (turn-by-turn directions).
|
||||
*
|
||||
* Each entry is a real `<button>` inside a real `<li>`, so Tab reaches it and
|
||||
* Enter activates it with no key handling of our own, and the `<ol>` carries
|
||||
* the ordering a screen reader announces. Items are reconciled in place,
|
||||
* keyed by id, for the same reason chips are: this runs on every refresh —
|
||||
* including the one a click on the list triggers — and replacing the node
|
||||
* would drop keyboard focus mid-interaction.
|
||||
* @param {HTMLElement|null} container The row's `.data-row-list` node.
|
||||
* @param {{ariaLabel?: string, items?: Array<object>}|null} list Descriptor.
|
||||
*/
|
||||
_syncRowList(container, list) {
|
||||
if (!container) return;
|
||||
const items = list?.items || [];
|
||||
container.hidden = items.length === 0;
|
||||
if (list?.ariaLabel) container.setAttribute('aria-label', list.ariaLabel);
|
||||
|
||||
const stale = new Map();
|
||||
for (const node of [...container.children]) {
|
||||
if (node.dataset?.listItemId) stale.set(node.dataset.listItemId, node);
|
||||
}
|
||||
let previous = null;
|
||||
let activeButton = null;
|
||||
for (const item of items) {
|
||||
let entry = stale.get(item.id);
|
||||
stale.delete(item.id);
|
||||
let button;
|
||||
if (!entry) {
|
||||
entry = document.createElement('li');
|
||||
entry.dataset.listItemId = item.id;
|
||||
button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'data-row-list-item';
|
||||
button.dataset.listItemId = item.id;
|
||||
const lead = document.createElement('span');
|
||||
lead.className = 'data-row-list-lead';
|
||||
const text = document.createElement('span');
|
||||
text.className = 'data-row-list-text';
|
||||
button.append(lead, text);
|
||||
entry.appendChild(button);
|
||||
} else {
|
||||
button = entry.querySelector('.data-row-list-item');
|
||||
}
|
||||
// Keep DOM order in step with descriptor order without rebuilding.
|
||||
const anchor = previous ? previous.nextSibling : container.firstChild;
|
||||
if (entry !== anchor) container.insertBefore(entry, anchor);
|
||||
previous = entry;
|
||||
if (!button) continue;
|
||||
const lead = button.querySelector('.data-row-list-lead');
|
||||
const text = button.querySelector('.data-row-list-text');
|
||||
const leadText = String(item.lead ?? '');
|
||||
const bodyText = String(item.text ?? '');
|
||||
if (lead && lead.textContent !== leadText) lead.textContent = leadText;
|
||||
if (text && text.textContent !== bodyText) text.textContent = bodyText;
|
||||
button.disabled = Boolean(item.disabled);
|
||||
button.classList.toggle('note', Boolean(item.disabled));
|
||||
button.classList.toggle('active', Boolean(item.active));
|
||||
button.classList.toggle('current', Boolean(item.current));
|
||||
button.setAttribute('aria-current', item.current ? 'step' : 'false');
|
||||
button.setAttribute('aria-pressed', item.active ? 'true' : 'false');
|
||||
button.title = bodyText;
|
||||
if (item.current) activeButton = button;
|
||||
}
|
||||
for (const node of stale.values()) node.remove();
|
||||
// Follow the flight, but never steal a scroll the reader is making
|
||||
// themselves: only when the step actually changed.
|
||||
if (
|
||||
activeButton &&
|
||||
container.dataset.currentId !== activeButton.dataset.listItemId
|
||||
) {
|
||||
container.dataset.currentId = activeButton.dataset.listItemId;
|
||||
activeButton.scrollIntoView?.({ block: 'nearest' });
|
||||
} else if (!activeButton) {
|
||||
delete container.dataset.currentId;
|
||||
}
|
||||
}
|
||||
|
||||
_refreshTogglePanel() {
|
||||
if (this._destroyed || !this._toggleContainer) return;
|
||||
// Skip DOM churn while hidden; visibilitychange (main.js) triggers one
|
||||
@@ -314,7 +410,11 @@ export class LayerPanel {
|
||||
meta.textContent = this._buildMetaText(layer);
|
||||
}
|
||||
|
||||
this._syncRowControls(row.querySelector('.data-toggle-controls'), layer);
|
||||
this._syncRowControls(
|
||||
row.querySelector('.data-toggle-controls'),
|
||||
layer,
|
||||
row.querySelector('.data-row-list'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user