Skip to content

gate-38 (skip-link): templates/settings/*.php is a FRAGMENT — the skip link is emitted by NC core, and 'fixing' it is a WCAG 2.4.1 regression #214

Description

@rubenvdlinde

Summary

gate-38 puts templates/settings/*.php in scope and requires each to carry a skip-to-content affordance. Those files are not page roots — they are fragments the Nextcloud Settings framework injects into core/templates/layout.user.php, which already emits the skip links. The gate is reporting "I cannot see it" as "it is absent", which is the exact reasoning the gate's own source already rejects for CnAppRoot twelve lines above the check.

Worse than a nuisance: the only way to make it green is to add a second "Skip to main content" to the fragment. That is a real WCAG 2.4.1 (Bypass Blocks) regression — a duplicate bypass affordance in tab order, whose target is inside a fragment the app does not control — introduced to satisfy an accessibility gate.

Measured

nldesign, full-tree run at origin/development 7b0fade, hydra-gates at main 756fe89:

[gate-38] skip-link: FAIL — 1 root component(s) without skip-link / <NcContent>
$ cat hydra-gate-skip-link.log
templates/settings/admin.php: no <NcContent> shell, no skip-link affordance

templates/ in nldesign contains exactly one file, settings/admin.php. This is a settings-only app: it has no page of its own, no App.vue, no route. The finding is 100% of the gate's output for this repo.

The affordance, in Nextcloud core

Nextcloud 32.0.12.1, read out of a running container:

$ grep -n "skip-" /var/www/html/core/templates/layout.user.php
60:		<div id="skip-actions">
61:			... <a href="<?php p($_['id-app-content']); ?>" class="button primary skip-navigation skip-content">Skip to main content</a> ...
62:			... <a href="<?php p($_['id-app-navigation']); ?>" class="button primary skip-navigation">Skip to navigation of app</a> ...

/settings/admin/theming renders through that layout. The admin section template is included into #app-content, i.e. into the skip link's own target. The bypass-block affordance is present, correct, and localised — in code no app owns.

The gate already knows this argument

run-hydra-gates.sh, immediately above the check:

All 18 fleet apps root on CnAppRoot, so this gate reported every one of them as shipping no skip link. Same principle already written down for the AppHost generics in gate-5/gate-14: "I cannot see it" is not "it is absent", and only the first of those is true here.

templates/settings/*.php is the same situation one layer further out: the shell is not a component this grep can reach, it is the platform's page template.

Proposed fix

Drop templates/settings/*.php from gate-38's scope, or pass it unconditionally with a comment recording that NC core owns the affordance for anything the Settings framework renders.

Keeping some signal, if that is preferred: a settings template that renders its own full-page shell (its own <div id="content">, its own layout) is a different animal and could still be checked. Nothing in the fleet does that today, so the narrower rule and the outright exemption measure the same thing right now.

What should NOT be done

Adding <a href="#app-content" class="skip-link"> to the fragments. It would turn 18 repos green and give every settings page two "Skip to main content" links. The first is core's, correct and localised; the second is the app's, duplicated and pointing into a fragment. axe-core's own bypass rule treats the page as having one landmark structure, so this is not even a wash — it is a net loss for the screen-reader user the gate exists to protect.

Repro

git clone https://github.com/ConductionNL/nldesign && cd nldesign
git checkout development
bash /path/to/hydra-gates/scripts/run-hydra-gates.sh      # full repo, NOT --scope-to-diff
# [gate-38] skip-link: FAIL — 1 root component(s) ...

Related, same family: #160 (gate-7 anti-correlated), #200 (gate-57 misses the attribute-scanner seam), #191 (gate-48 matches a removed comment).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions