mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
* fix(a11y): accessible names for icon-only buttons and a guard
Icon-only buttons named only by a matTooltip (or by nothing) had no
accessible name: the icon ligature is hidden from assistive technology
and a tooltip only adds a description. 22 buttons on master, in .html
and inline templates, now carry a translated aria-label bound to their
tooltip key. The channel row's favorite star keeps one label
("Favorite") and reports its state through aria-pressed.
New guard `pnpm run a11y:icon-buttons:validate` (CI step) fails on a
<button> whose only content is mat-icons (through control flow,
ng-container and spinners) without aria-label, an aria-label binding
or aria-labelledby. The inline-template lookup moves to a shared
tools/nx/inline-templates.mjs used by the icon-ligature guard too.
web-e2e icon-button-names.e2e.ts runs axe's button-name rule on a
channel list, the channel details dialog, Sources, the playlist info
dialog and an Xtream search. Five new keys are translated in all 18
locales.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(a11y): text hidden from assistive technology never names an icon button
Greptile: a static aria-hidden="true" child's text counted as the
button's name, so <button><mat-icon/><span aria-hidden="true">Close</span>
passed the guard. Hidden subtrees now add only their icons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
48 lines
1.5 KiB
JavaScript
48 lines
1.5 KiB
JavaScript
import ts from 'typescript';
|
|
import { literalLineResolver } from './literal-source-lines.mjs';
|
|
|
|
/**
|
|
* `template` of an `@Component({...})` decorator, quoted or backticked. A
|
|
* template with `${...}` substitutions is built at runtime and skipped.
|
|
*/
|
|
export function isInlineTemplate(node) {
|
|
const call = node.parent?.parent;
|
|
return (
|
|
ts.isPropertyAssignment(node) &&
|
|
(ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) &&
|
|
node.name.text === 'template' &&
|
|
(ts.isNoSubstitutionTemplateLiteral(node.initializer) ||
|
|
ts.isStringLiteral(node.initializer)) &&
|
|
call !== undefined &&
|
|
ts.isCallExpression(call) &&
|
|
ts.isIdentifier(call.expression) &&
|
|
call.expression.text === 'Component' &&
|
|
ts.isDecorator(call.parent)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The inline component templates of a TypeScript source, each with a
|
|
* mapping from a template offset to its zero-based source line.
|
|
*/
|
|
export function inlineTemplates(source, file) {
|
|
const sourceFile = ts.createSourceFile(
|
|
file,
|
|
source,
|
|
ts.ScriptTarget.Latest,
|
|
true
|
|
);
|
|
const found = [];
|
|
const visit = (node) => {
|
|
if (isInlineTemplate(node)) {
|
|
found.push({
|
|
template: node.initializer.text,
|
|
lineAt: literalLineResolver(node.initializer, sourceFile),
|
|
});
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
visit(sourceFile);
|
|
return found;
|
|
}
|