1
0
Files
breadcrumb-the-shire/web/js/core/app-form-utils.js
fs 2b915f5a43 refactor: extract shared form utils, add aria-live regions and reduced-motion support
DRY duplicated helpers (escapeAttr, belongsToForm, isSubmitControl,
isEditableTarget) into web/js/core/app-form-utils.js and update 7
consumers. Add aria-live="polite" to flash messages, async messages,
and search results for screen reader announcements. Add
prefers-reduced-motion support to CSS tooltips.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 22:28:46 +01:00

55 lines
1.9 KiB
JavaScript

/**
* Shared form utility helpers — used by confirm-actions, detail-action-policy,
* and detail-page-factory to avoid duplicated submit/form-ownership logic.
*/
/** Escape a string for safe use inside a CSS attribute selector value. */
export const escapeAttr = (value) => {
const raw = String(value || '');
if (typeof CSS !== 'undefined' && CSS && typeof CSS.escape === 'function') {
return CSS.escape(raw);
}
return raw.replace(/["\\]/g, '\\$&');
};
/** Check whether a control belongs to a given form (by .form, form="" attr, or containment). */
export const belongsToForm = (control, form) => {
if (!(control instanceof HTMLElement) || !(form instanceof HTMLFormElement)) {
return false;
}
const owner = control.form;
if (owner instanceof HTMLFormElement) {
return owner === form;
}
const controlFormAttr = String(control.getAttribute('form') || '').trim();
const formId = String(form.id || '').trim();
if (controlFormAttr !== '' && formId !== '') {
return controlFormAttr === formId;
}
return form.contains(control);
};
/** Check whether an element is a submit button or submit input. */
export const isSubmitControl = (control) => {
if (!(control instanceof HTMLElement)) {
return false;
}
if (control instanceof HTMLButtonElement) {
const type = String(control.getAttribute('type') || 'submit').trim().toLowerCase();
return type === '' || type === 'submit';
}
if (control instanceof HTMLInputElement) {
const type = String(control.type || '').trim().toLowerCase();
return type === 'submit' || type === 'image';
}
return false;
};
/** Check whether the event target is an editable element (input, textarea, select, contentEditable). */
export const isEditableTarget = (target) => {
if (!target) { return false; }
if (target.isContentEditable) { return true; }
const tag = target.tagName;
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
};