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>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
* Declarative confirm-before-action — intercepts clicks and submits on [data-confirm-message] elements.
|
||||
*/
|
||||
import { confirmDialog, requestSubmitWithFallback } from '../core/app-confirm-dialog.js';
|
||||
import { isSubmitControl, belongsToForm } from '../core/app-form-utils.js';
|
||||
|
||||
const CONFIRM_ATTR = 'data-confirm-message';
|
||||
const confirmedSubmitterByForm = new WeakMap();
|
||||
@@ -9,37 +10,7 @@ const confirmedClickElements = new WeakSet();
|
||||
|
||||
const trimAttr = (element, attr) => String(element?.getAttribute(attr) || '').trim();
|
||||
|
||||
const isSubmitControl = (element) => {
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
if (element instanceof HTMLButtonElement) {
|
||||
const type = String(element.getAttribute('type') || 'submit').trim().toLowerCase();
|
||||
return type === '' || type === 'submit';
|
||||
}
|
||||
if (element instanceof HTMLInputElement) {
|
||||
const type = String(element.type || '').trim().toLowerCase();
|
||||
return type === 'submit' || type === 'image';
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const submitterByForm = new Map();
|
||||
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 = trimAttr(control, 'form');
|
||||
const formId = trimAttr(form, 'id');
|
||||
if (controlFormAttr !== '' && formId !== '') {
|
||||
return controlFormAttr === formId;
|
||||
}
|
||||
return form.contains(control);
|
||||
};
|
||||
|
||||
const readConfirmOption = (primary, fallback, suffix) => {
|
||||
const attr = `data-confirm-${suffix}`;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Global search sidebar — debounced fetch with live result counts and Ctrl+K shortcut.
|
||||
*/
|
||||
import { requireEl, optionalEl, warnOnce } from '../core/app-dom.js';
|
||||
import { isEditableTarget } from '../core/app-form-utils.js';
|
||||
|
||||
const input = requireEl('#side-search', { module: 'global-search' });
|
||||
const resultsEl = requireEl('[data-global-search-results]', { module: 'global-search' });
|
||||
@@ -191,13 +192,6 @@ if (input && resultsEl) {
|
||||
submitSearch();
|
||||
});
|
||||
|
||||
const isEditableTarget = (target) => {
|
||||
if (!target) {return false;}
|
||||
if (target.isContentEditable) {return true;}
|
||||
const tag = target.tagName;
|
||||
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
||||
};
|
||||
|
||||
const focusSearch = () => {
|
||||
if (!input) {return;}
|
||||
window.requestAnimationFrame(() => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* Browser history integration — handles back/forward navigation state.
|
||||
*/
|
||||
import { isEditableTarget } from '../core/app-form-utils.js';
|
||||
|
||||
const updateHistoryButtons = () => {
|
||||
const back = document.querySelector('#global-back');
|
||||
const forward = document.querySelector('#global-forward');
|
||||
@@ -29,13 +31,6 @@ const initHistoryButtons = () => {
|
||||
const back = document.querySelector('#global-back');
|
||||
const forward = document.querySelector('#global-forward');
|
||||
|
||||
const isEditableTarget = (target) => {
|
||||
if (!target) {return false;}
|
||||
if (target.isContentEditable) {return true;}
|
||||
const tag = target.tagName;
|
||||
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (!event.altKey || event.metaKey || event.ctrlKey) {return;}
|
||||
if (isEditableTarget(event.target)) {return;}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Aside panel switcher — opens/closes named sidebar panels.
|
||||
*/
|
||||
import { requireEl, warnOnce } from '../core/app-dom.js';
|
||||
import { isEditableTarget } from '../core/app-form-utils.js';
|
||||
import { initPersistedDetailsGroup } from '../core/app-details-open-state.js';
|
||||
|
||||
export function initAsidePanels(options = {}) {
|
||||
@@ -40,12 +41,6 @@ export function initAsidePanels(options = {}) {
|
||||
const getTabKey = (tab) => tab?.dataset?.asideTarget || '';
|
||||
const getPanelTools = (panel) => panel?.querySelector('[data-aside-panel-tools]') || null;
|
||||
const getTabHref = (tab) => tab?.dataset?.asideHref || '';
|
||||
const isEditableTarget = (target) => {
|
||||
if (!target) {return false;}
|
||||
if (target.isContentEditable) {return true;}
|
||||
const tag = target.tagName;
|
||||
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
||||
};
|
||||
|
||||
const panelDetailsRegistry = new WeakMap();
|
||||
const findActiveDetailsKeys = (panel) => Array.from(panel.querySelectorAll('details[data-details-key]'))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Sidebar collapse/show toggle with localStorage persistence and Ctrl+B shortcut.
|
||||
*/
|
||||
import { requireEl, warnOnce } from '../core/app-dom.js';
|
||||
import { isEditableTarget } from '../core/app-form-utils.js';
|
||||
|
||||
export function initSidebarToggle(options = {}) {
|
||||
const {
|
||||
@@ -150,13 +151,6 @@ export function initSidebarToggle(options = {}) {
|
||||
return controller;
|
||||
}
|
||||
|
||||
const isEditableTarget = (target) => {
|
||||
if (!target) {return false;}
|
||||
if (target.isContentEditable) {return true;}
|
||||
const tag = target.tagName;
|
||||
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (!(event.metaKey || event.ctrlKey)) {return;}
|
||||
if (event.altKey) {return;}
|
||||
|
||||
Reference in New Issue
Block a user