From 188d74eff6203245c2ea9c7ae6b180f238d125a8 Mon Sep 17 00:00:00 2001 From: William Jacoby Date: Wed, 7 Oct 2026 12:56:44 -0500 Subject: [PATCH 1/2] Add template_a11y.py to check icon-only links and buttons Scans an extension's or style's templates for links and buttons that show only an icon, and reports any without a title (tooltip) or screen-reader text (an sr-only span or aria-label). A title on the icon itself, phpBB's ACP pattern, counts as the tooltip, and a link around an image with alt text passes. Standard library only; no board needed. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- phpbb-test-board/README.md | 14 +++ phpbb-test-board/template_a11y.py | 157 ++++++++++++++++++++++++++++++ tests/test-phpbb-test-board.sh | 22 +++++ 4 files changed, 194 insertions(+), 1 deletion(-) create mode 100755 phpbb-test-board/template_a11y.py diff --git a/README.md b/README.md index 94d3a71..cc4ddfe 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ The tag is reserved at the pinned commit before the release is created. A failur ### Testing extensions and styles: `phpbb-test-board/` -Builds a local phpBB 3.3 board on SQLite and tests extensions on it: `smoke_test.py` checks that an extension doesn't break any page, `feature_checks.py` exercises its main feature, and `screenshots.py` takes documentation screenshots of extensions and styles. For local testing only. +Builds a local phpBB 3.3 board on SQLite and tests extensions on it: `smoke_test.py` checks that an extension doesn't break any page, `feature_checks.py` exercises its main feature, and `screenshots.py` takes documentation screenshots of extensions and styles, and `template_a11y.py` checks that icon-only links and buttons have a tooltip and screen-reader text. For local testing only. ```bash phpbb-test-board/setup-board.sh -d ~/phpbb-test-board diff --git a/phpbb-test-board/README.md b/phpbb-test-board/README.md index 6d3156b..7a434ad 100644 --- a/phpbb-test-board/README.md +++ b/phpbb-test-board/README.md @@ -89,6 +89,20 @@ phpbb-test-board/screenshots.py path/to/ProMinoDeux -o path/to/ProMinoDeux/docs/ --seed path/to/seed-forum/bin/seed-standard-fixtures.php ``` +### `template_a11y.py`: labels on icon-only links and buttons + +```bash +phpbb-test-board/template_a11y.py path/to/project [more projects...] +``` + +Scans an extension's or style's templates (`styles/`, `adm/style/`, or a style's `template/`) for links and buttons that show only an icon. Each needs a `title` for the tooltip and screen-reader text, a `` inside it or an `aria-label`, both from a language string: + +```html +{L_CHECK} +``` + +A `title` on the icon itself, phpBB's ACP pattern, counts as the tooltip, and a link around an image with alt text passes. Problems are printed as `FILE:LINE: message`, and the exit status is 1 if there are any. It needs no board and only Python's standard library. + ## Extensions that need another extension Pass the other extension's checkout with `--with`. Repeat it for several, in the order they must be enabled: diff --git a/phpbb-test-board/template_a11y.py b/phpbb-test-board/template_a11y.py new file mode 100755 index 0000000..a72b101 --- /dev/null +++ b/phpbb-test-board/template_a11y.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +""" +Check a phpBB extension's or style's templates for icon-only controls without text. + +Scans every .html template under the given checkouts (styles/ and adm/style/, +or a style's own template/ folder) for links and buttons whose only content +is an icon or image. Each such control needs: + + - a title attribute, for the tooltip sighted mouse users see, and + - screen-reader text: a inside it or an aria-label. + +A title on the icon itself, phpBB's ACP pattern, also counts as the tooltip. +A link around an image with alt text is described by that text and passes. + +Both should come from a language string, not hard-coded text. Template +syntax ({L_FOO}, {{ lang('FOO') }}, , {% if %}) counts as text, so +it works on phpBB 3.3 and Twig templates alike. Uses only the standard +library and needs no test board. + +Output: one line per problem, as FILE:LINE: message. +Exit status: 0 no problems, 1 problems found, 2 bad arguments. +""" +import argparse +import re +import sys +from html.parser import HTMLParser +from pathlib import Path + +CONTROLS = {"a", "button"} +ICON_TAGS = {"i", "img", "svg"} +VOID_TAGS = {"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"} +# Template logic that prints nothing: {% ... %}, {# ... #}. +TEMPLATE_LOGIC = re.compile(r"\{%.*?%\}|\{#.*?#\}", re.S) + + +class Control: + """One or ' \ + ' {L_VISIBLE}' \ + '{L_UP}' \ + '{BANNER}' >"$tmp/a11y-good/styles/all/template/good.html" +check "template_a11y.py --help exits 0" exits_with 0 "$tools/template_a11y.py" --help +check "template_a11y.py without arguments exits 2" exits_with 2 "$tools/template_a11y.py" +check "template_a11y.py with a missing directory exits 2" exits_with 2 "$tools/template_a11y.py" "$tmp/no-such-dir" +check "template_a11y.py accepts labelled controls" exits_with 0 "$tools/template_a11y.py" "$tmp/a11y-good" +check "template_a11y.py exits 1 on unlabelled icons" exits_with 1 "$tools/template_a11y.py" "$tmp/a11y-ext" +check "template_a11y.py reports a missing screen-reader text" \ + output_has "bad.html:1: icon-only without screen-reader text" "$tools/template_a11y.py" "$tmp/a11y-ext" +check "template_a11y.py reports a missing tooltip" \ + output_has "bad.html:2: icon-only without a title (tooltip)" "$tools/template_a11y.py" "$tmp/a11y-ext" + if [ "$failures" -eq 0 ]; then echo "All tests passed." else From b8d75ab5ecb381eabbbcd0561b9fbc9244e312b3 Mon Sep 17 00:00:00 2001 From: William Jacoby Date: Wed, 7 Oct 2026 13:04:01 -0500 Subject: [PATCH 2/2] Recommend aria-label for icon-only controls in ACP templates The ACP style has no sr-only class, so a span would be visible there. Co-Authored-By: Claude Opus 5.5 --- phpbb-test-board/README.md | 2 +- phpbb-test-board/template_a11y.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/phpbb-test-board/README.md b/phpbb-test-board/README.md index 7a434ad..19c34ce 100644 --- a/phpbb-test-board/README.md +++ b/phpbb-test-board/README.md @@ -101,7 +101,7 @@ Scans an extension's or style's templates (`styles/`, `adm/style/`, or a style's {L_CHECK} ``` -A `title` on the icon itself, phpBB's ACP pattern, counts as the tooltip, and a link around an image with alt text passes. Problems are printed as `FILE:LINE: message`, and the exit status is 1 if there are any. It needs no board and only Python's standard library. +In ACP templates (`adm/style/`) use `aria-label` instead of the span: the ACP style has no `sr-only` class, so the text would show. A `title` on the icon itself, phpBB's ACP pattern, counts as the tooltip, and a link around an image with alt text passes. Problems are printed as `FILE:LINE: message`, and the exit status is 1 if there are any. It needs no board and only Python's standard library. ## Extensions that need another extension diff --git a/phpbb-test-board/template_a11y.py b/phpbb-test-board/template_a11y.py index a72b101..fac604b 100755 --- a/phpbb-test-board/template_a11y.py +++ b/phpbb-test-board/template_a11y.py @@ -9,7 +9,9 @@ - a title attribute, for the tooltip sighted mouse users see, and - screen-reader text: a inside it or an aria-label. -A title on the icon itself, phpBB's ACP pattern, also counts as the tooltip. +In ACP templates use aria-label: the ACP style has no sr-only class, so a +span would be visible there. A title on the icon itself, phpBB's ACP +pattern, also counts as the tooltip. A link around an image with alt text is described by that text and passes. Both should come from a language string, not hard-coded text. Template @@ -50,8 +52,9 @@ def __init__(self, tag: str, attrs: dict, line: int) -> None: class TemplateChecker(HTMLParser): """Collect icon-only controls that lack a tooltip or screen-reader text.""" - def __init__(self) -> None: + def __init__(self, acp: bool = False) -> None: super().__init__(convert_charrefs=True) + self.acp = acp self.problems: list = [] self.controls: list = [] self.sr_only_depth: list = [] # per open element: inside an sr-only element? @@ -109,7 +112,9 @@ def check(self, control: Control) -> None: if not control.attrs.get("title", "").strip() and not control.child_title: missing.append("a title (tooltip)") if not named: - missing.append('screen-reader text ( or aria-label)') + # The ACP style has no sr-only class, so a span there would show. + missing.append("screen-reader text (aria-label; the ACP has no sr-only class)" if self.acp + else 'screen-reader text ( or aria-label)') if missing: self.problems.append((control.line, f"icon-only <{control.tag}> without " + " or ".join(missing))) @@ -126,7 +131,7 @@ def template_files(root: Path) -> list: def check_file(path: Path) -> list: """Problems in one template, as (line, message) pairs.""" - checker = TemplateChecker() + checker = TemplateChecker(acp="adm/style" in path.as_posix()) checker.feed(path.read_text(encoding="utf-8", errors="replace")) checker.close() return checker.problems