Test and deploy / test-and-deploy (push) Successful in 1m25s
Highest severity: injected "?" buttons were hijacking the implicit <label> association on 14 fields, so clicking the label text opened the help dialog instead of focusing the input and screen readers lost the input's accessible name. ensureLabelAssociation() now gives each affected input an explicit id/for pairing before the button lands. Also: milestones-ror/actuators topic placement is now text-matched with a warning fallback instead of blind array indexing; every topic warns if it resolves zero containers; dialog temperatures (milestone table, RoR budget, worked example, both charge-setting tables) now respect the °C/°F toggle instead of being stuck in °C; the print worksheet's duplicate curve chart was missing the data-tempc attributes its live counterpart had, so only its title/caption (not its axis numbers) converted; the "?" trigger's touch target grew from 15px to a WCAG-conformant ~25px without changing the visual dot; two genuine coverage gaps closed (a roast-log checklist topic, a curve-reading topic); a worked feasibility example was restored; two claims that didn't trace back to the source worksheet were corrected (an invented "±15 seconds" sweet-spot tolerance, a wrong moisture-to-time mapping); three cultivar cells regained detail lost in the original transcription; a data-transcription footnote no longer leaks into an unrelated topic; checklist items keep their list semantics under VoiceOver. Co-Authored-By: Fable <[email protected]> Co-Authored-By: Opus <[email protected]>
93 lines
3.7 KiB
JavaScript
93 lines
3.7 KiB
JavaScript
// Field-level "?" reference dialogs. Sibling to why-panels.js: why-panels are always-visible
|
|
// section prose ("why this works"); this is an on-demand, per-field reference library ("what are
|
|
// my options"). See FIELD_HELP_SPEC.md (deleted after implementation) for the design rationale.
|
|
|
|
import { HELP_TOPICS } from "./field-help-content.js";
|
|
|
|
export function initFieldHelp({ getPlan, getTempUnit }) {
|
|
const dialog = document.getElementById("field-help-dialog");
|
|
if (!dialog) return;
|
|
const titleEl = document.getElementById("help-dialog-title");
|
|
const bodyEl = document.getElementById("help-dialog-body");
|
|
let lastTrigger = null;
|
|
|
|
function openTopic(key, trigger) {
|
|
const topic = HELP_TOPICS[key];
|
|
if (!topic) return;
|
|
lastTrigger = trigger;
|
|
titleEl.textContent = topic.title;
|
|
bodyEl.replaceChildren(topic.render(getPlan(), getTempUnit?.()));
|
|
bodyEl.scrollTop = 0;
|
|
dialog.showModal();
|
|
}
|
|
|
|
// A button appended into a `.field-label` span is a labelable element, and a <label>
|
|
// without an explicit `for` labels its FIRST labelable descendant in tree order — so an
|
|
// unlabeled wrapping <label> would silently make the help button, not the input, the
|
|
// label's control. Give the input an explicit id/for pairing before that can happen; a
|
|
// no-op wherever the container isn't inside a <label> or is already explicitly paired.
|
|
function ensureLabelAssociation(container) {
|
|
const label = container.closest("label");
|
|
if (!label || label.hasAttribute("for")) return;
|
|
const control = label.querySelector("input, select, textarea");
|
|
if (!control) return;
|
|
if (!control.id)
|
|
control.id = `fh-${(control.name || `${Math.random()}`).replace(/[^\w-]/g, "-")}`;
|
|
label.setAttribute("for", control.id);
|
|
}
|
|
|
|
function makeButton(key, title) {
|
|
const btn = document.createElement("button");
|
|
btn.type = "button";
|
|
btn.className = "help-btn";
|
|
btn.dataset.helpTopic = key;
|
|
btn.setAttribute("aria-label", `Field guide: ${title.split(" — ")[0]}`);
|
|
btn.setAttribute("aria-haspopup", "dialog");
|
|
btn.textContent = "?";
|
|
// Several triggers live inside <label> elements — stop the click from also
|
|
// focusing/toggling the label's control (radio chips especially).
|
|
btn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
openTopic(key, btn);
|
|
});
|
|
return btn;
|
|
}
|
|
|
|
for (const [key, topic] of Object.entries(HELP_TOPICS)) {
|
|
const containers = [];
|
|
for (const sel of topic.targets ?? [])
|
|
containers.push(...document.querySelectorAll(`#plan-form ${sel}`));
|
|
if (topic.place) containers.push(...topic.place());
|
|
if (containers.length === 0)
|
|
console.warn(`field-help: topic "${key}" resolved zero containers — its "?" button is missing.`);
|
|
for (const container of containers) {
|
|
ensureLabelAssociation(container);
|
|
container.append(makeButton(key, topic.title));
|
|
}
|
|
}
|
|
|
|
// Deferred: a click that closes the dialog (the ✕ button, or a backdrop click) also runs
|
|
// the browser's own click-focuses-the-target step, which can otherwise land after — and
|
|
// override — a synchronous focus() call made while handling that same click.
|
|
function returnFocus() {
|
|
const trigger = lastTrigger;
|
|
setTimeout(() => trigger?.focus(), 0);
|
|
}
|
|
|
|
dialog.querySelector("[data-close-help]")?.addEventListener("click", () => {
|
|
dialog.close();
|
|
returnFocus();
|
|
});
|
|
dialog.addEventListener("click", (e) => {
|
|
if (e.target === dialog) {
|
|
dialog.close();
|
|
returnFocus();
|
|
}
|
|
});
|
|
// Covers every other close path (Esc, or dialog.close() called directly) — native <dialog>
|
|
// already restores focus to the pre-showModal() element on close in current browsers; this
|
|
// listener is the explicit fallback for engines where that isn't reliable.
|
|
dialog.addEventListener("close", returnFocus);
|
|
}
|