gov-gui
Version:
Gov UI Component Library Demo ready Build
209 lines (205 loc) • 11.9 kB
JavaScript
// 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