UNPKG

gov-gui

Version:

Gov UI Component Library Demo ready Build

209 lines (205 loc) 11.9 kB
// Helper to throw in dev, warn in prod function a11yLintError(message) { if (typeof process !== 'undefined' && process.env && "production" !== 'production') { throw new Error(`[A11Y-LINT] ${message}`); } else { console.warn(`[A11Y] ${message}`); } } function runA11yCheck(el, config = {}) { if (!el || !(el instanceof HTMLElement)) return; const { role, ariaHidden = false, requiresLabel = false, isKeyboardNavigable = false, } = config; const tag = el.tagName.toLowerCase(); // Handle aria-hidden if (ariaHidden) { if (!el.hasAttribute('aria-hidden')) { a11yLintError(`<${tag}>: ariaHidden is true but 'aria-hidden' not set.`); el.setAttribute('aria-hidden', 'true'); } return; // Hidden components don't need further checks } // Warn if role is missing or uncommon for tag const allowedRoles = { button: ['button', 'a', 'div', 'span'], link: ['a', 'div', 'span'], checkbox: ['input', 'div', 'span'], dialog: ['div'], alert: ['div'], heading: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], }; if (role) { if (!el.hasAttribute('role')) { a11yLintError(`<${tag}>: Missing 'role'. Setting '${role}'.`); el.setAttribute('role', role); } if (allowedRoles[role] && !allowedRoles[role].includes(tag)) { a11yLintError(`<${tag}>: Role '${role}' is uncommon on this tag.`); } } // Check for accessible label const hasLabel = el.hasAttribute('aria-label') || el.hasAttribute('aria-labelledby') || el.querySelector('[aria-label], [aria-labelledby], label'); if (requiresLabel && !hasLabel) { a11yLintError(`<${tag}>: Missing accessible label. Applying fallback label.`); el.setAttribute('aria-label', 'Label'); } // Keyboard navigation check if (isKeyboardNavigable) { if (!el.hasAttribute('tabindex')) { el.setAttribute('tabindex', '0'); } // if (!el.style.outline) { // el.style.outline = '2px solid #005fcc'; // } if ((role === 'button' || role === 'link') && typeof el['handleKeyDown'] !== 'function') { a11yLintError(`<${tag}>: Expected 'handleKeyDown()' method for keyboard navigation.`); } } // Contrast check const fg = getComputedStyle(el).color; const bg = getComputedStyle(el).backgroundColor; const fgHex = toHex(fg); const bgHex = toHex(bg); if (!hasSufficientContrast(fgHex, bgHex)) { a11yLintError(`<${tag}>: Insufficient contrast: ${fgHex} on ${bgHex}`); } } // --- Helpers below --- function luminance(r, g, b) { const a = [r, g, b].map((v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }); return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2]; } function contrast(hex1, hex2) { const rgb = (hex) => { var _a, _b; return (_b = (_a = hex.match(/\w\w/g)) === null || _a === void 0 ? void 0 : _a.map((c) => parseInt(c, 16))) !== null && _b !== void 0 ? _b : [0, 0, 0]; }; const [r1, g1, b1] = rgb(hex1); const [r2, g2, b2] = rgb(hex2); const lum1 = luminance(r1, g1, b1); const lum2 = luminance(r2, g2, b2); return (Math.max(lum1, lum2) + 0.05) / (Math.min(lum1, lum2) + 0.05); } function hasSufficientContrast(fg, bg) { return contrast(fg, bg) >= 4.5; } function toHex(rgb) { const parts = rgb.match(/\d+/g); return parts ? '#' + parts.slice(0, 3).map((v) => (+v).toString(16).padStart(2, '0')).join('') : '#000'; } // src/utils/a11y-config.ts const a11yMap = { 'gov-accordion': { role: 'region', ariaRole: 'region', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'section' }, 'gov-alert': { role: 'alert', ariaRole: 'alert', ariaHidden: false, isKeyboardNavigable: false, requiresLabel: true, elementSelector: 'div' }, 'gov-avatar': { role: 'img', ariaRole: 'img', ariaHidden: false, isKeyboardNavigable: false, requiresLabel: true, elementSelector: 'img' }, 'gov-badge': { role: 'status', ariaRole: 'status', ariaHidden: false, isKeyboardNavigable: false, requiresLabel: false, elementSelector: 'span' }, 'gov-box': { ariaHidden: true }, 'gov-breadcrumb': { role: 'navigation', ariaRole: 'navigation', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: false, elementSelector: 'nav' }, 'gov-button': { role: 'button', ariaRole: 'button', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'button' }, 'gov-card': { role: 'group', ariaRole: 'group', ariaHidden: false, isKeyboardNavigable: false, requiresLabel: false, elementSelector: 'div' }, 'gov-calendar': { role: 'grid', ariaRole: 'grid', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'table' }, 'gov-chart': { role: 'img', ariaRole: 'img', ariaHidden: false, isKeyboardNavigable: false, requiresLabel: true, elementSelector: 'svg' }, 'gov-checkbox': { role: 'checkbox', ariaRole: 'checkbox', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'input[type="checkbox"]' }, 'gov-chips': { role: 'listbox', ariaRole: 'listbox', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'ul' }, 'gov-choice-chips': { role: 'radiogroup', ariaRole: 'radiogroup', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'div' }, 'gov-combo-box': { role: 'combobox', ariaRole: 'combobox', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'input' }, 'gov-container': { ariaHidden: true }, 'gov-date-time-picker': { role: 'group', ariaRole: 'group', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'div' }, 'gov-drop': { role: 'menu', ariaRole: 'menu', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'ul' }, 'gov-dropdown': { role: 'listbox', ariaRole: 'listbox', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'select' }, 'gov-form': { role: 'form', ariaRole: 'form', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'form' }, 'gov-icon': { role: 'img', ariaRole: 'img', ariaHidden: false, isKeyboardNavigable: false, requiresLabel: false, elementSelector: 'svg' }, 'gov-input': { role: 'textbox', ariaRole: 'textbox', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'input' }, 'gov-layout': { ariaHidden: true }, 'gov-list': { role: 'list', ariaRole: 'list', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'ul' }, 'gov-menubar': { role: 'menubar', ariaRole: 'menubar', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'nav' }, 'gov-modal': { role: 'dialog', ariaRole: 'dialog', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'div' }, 'gov-pagination': { role: 'navigation', ariaRole: 'navigation', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: false, elementSelector: 'nav' }, 'gov-popups': { role: 'dialog', ariaRole: 'dialog', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'div' }, 'gov-progress-bar': { role: 'progressbar', ariaRole: 'progressbar', ariaHidden: false, isKeyboardNavigable: false, requiresLabel: true, elementSelector: 'progress' }, 'gov-radiobutton': { role: 'radio', ariaRole: 'radio', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'input[type="radio"]' }, 'gov-row': { role: 'row', ariaRole: 'row', ariaHidden: false, isKeyboardNavigable: false, requiresLabel: false, elementSelector: 'div' }, 'gov-segmented-chips': { role: 'tablist', ariaRole: 'tablist', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'div' }, 'gov-sidebar': { role: 'complementary', ariaRole: 'complementary', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: false, elementSelector: 'aside' }, 'gov-slider': { role: 'slider', ariaRole: 'slider', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'input[type="range"]' }, 'gov-stepper': { role: 'list', ariaRole: 'list', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'ul' }, 'gov-switcher': { role: 'switch', ariaRole: 'switch', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'input[type="checkbox"]' }, 'gov-table': { role: 'table', ariaRole: 'table', ariaHidden: false, isKeyboardNavigable: true, requiresLabel: true, elementSelector: 'table' }, }; // export abstract class A11yComponent { // protected a11y: A11yConfig = {}; // protected setA11yConfig(tagName: string) { // this.a11y = a11yMap[tagName] || {}; // } // } // src/global/decorators/accessible.ts // export function accessible(tagName: string) { // return function <T extends { new (...args: any[]): {} }>(constructor: T) { // return class extends constructor { // a11y: A11yConfig = a11yMap[tagName] || {}; // Auto-load config // componentWillLoad() { // if (super['componentWillLoad']) super['componentWillLoad'](); // const el: HTMLElement = this['el']; // if (el) runA11yCheck(el, this.a11y); // } // }; // }; // } /** * Automatically runs accessibility checks on the component in connectedCallback. * Only works on classes that define standard accessibility props. */ // export function accessible() { // return function (constructor: any) { // const originalConnectedCallback = constructor.prototype.connectedCallback; // constructor.prototype.connectedCallback = function () { // // Run original logic if present // if (originalConnectedCallback) { // originalConnectedCallback.call(this); // } // // Run A11Y check // runA11yCheck(this); // }; // }; // } function accessibleLifecycle(tagName, selector) { return function (_target, _propertyKey, descriptor) { const original = descriptor.value; descriptor.value = function (...args) { var _a, _b, _c, _d; if (original) original.apply(this, args); const config = (_a = a11yMap[tagName]) !== null && _a !== void 0 ? _a : {}; let el = (_b = this.el) !== null && _b !== void 0 ? _b : (_c = this.shadowRoot) === null || _c === void 0 ? void 0 : _c.host; // Try to find the best internal element for accessibility if ((_d = this.el) === null || _d === void 0 ? void 0 : _d.shadowRoot) { if (selector) { el = this.el.shadowRoot.querySelector(selector); } else if (config.elementSelector) { el = this.el.shadowRoot.querySelector(config.elementSelector); } else if (config.role) { el = this.el.shadowRoot.querySelector(`[role="${config.role}"]`); } // Fallback to common tags for interactive/visible elements if (!el) { el = this.el.shadowRoot.querySelector('button,input,div,section,main,form,span'); } } // Run accessibility check on the selected element if (el) runA11yCheck(el, config); else if (this.el) runA11yCheck(this.el, config); // fallback }; return descriptor; }; } export { accessibleLifecycle as a }; //# sourceMappingURL=p-e89af057.js.map