Skip to content

Interactive controls nested inside other interactive controls in login and blueprint tabs #15401

Description

@bpmore

Bug description

Three places in the CP put a focusable element inside a control that already claims an interactive role. Assistive technology can reach the inner element even when it is meant to be hidden, producing a focus stop that announces nothing.

1. Login "Remember me"

<button role="checkbox" aria-label="Remember me" tabindex="4" aria-checked="false">
  <input type="checkbox" name="remember" tabindex="-1" style="position:absolute; …">
</button>

axe: nested-interactive"Using a negative tabindex on an element inside an interactive control does not prevent assistive technologies from focusing the element." The inner input additionally trips the label rule, since it has no associated label of its own.

https://github.com/statamic/cms/blob/v6.31.0/resources/js/pages/auth/Login.vue#L115
https://github.com/statamic/cms/blob/v6.31.0/resources/js/components/ui/Checkbox/Item.vue#L113-L124

2. Blueprint builder tab trigger

<button role="tab" id="reka-tabs-v-6-trigger-main" aria-selected="true" tabindex="-1">

axe: nested-interactive — element has focusable descendants.

3. Blueprint builder tablist

<div role="tablist" aria-orientation="horizontal" tabindex="0">
  … contains div[tabindex] …
</div>

axe: aria-required-children (critical) — "Element has children which are not allowed: div[tabindex]".

WCAG 2.1 SC 4.1.2 Name, Role, Value and SC 1.3.1 Info and Relationships (both Level A).

How to reproduce

  1. composer create-project statamic/statamic
  2. For (1): visit /cp/auth/login and inspect the Remember me control
  3. For (2) and (3): visit /cp/fields/blueprints/collections/{handle}/{blueprint}/edit and inspect the tab strip
  4. Or run axe-core against either page

Suggested fix

  • Render the hidden native checkbox as a sibling of the role="checkbox" button rather than a child.
  • Remove the stray tabindex from the tablist's non-tab children so the tablist only contains role="tab" elements, and drop the focusable descendant inside the tab trigger.

While on the login form: it also uses positive tabindex values 1–6, which pull those controls ahead of the skip links in the tab order. The DOM is already in the intended order, so those can just be removed.

Environment

Environment
Laravel Version: 13.30.1
PHP Version: 8.4.23
Composer Version: 2.10.2
Environment: local
Debug Mode: ENABLED
Maintenance Mode: OFF
Timezone: UTC
Locale: en

Cache
Config: NOT CACHED
Events: NOT CACHED
Routes: NOT CACHED
Views: CACHED

Drivers
Broadcasting: log
Cache: file
Database: sqlite
Logs: stack / single
Mail: log
Queue: sync
Session: file

Storage
public/storage: NOT LINKED

Statamic
Addons: 0
License Key: Not set
Sites: 1
Stache Watcher: Enabled (auto)
Static Caching: Disabled
Version: 6.31.0 PRO

Installation

Fresh statamic/statamic site via CLI

Additional details

Found during a WCAG 2.1 AA audit of a vanilla composer create-project statamic/statamic install with no addons and no custom code. Tested with axe-core 4.x on Chromium 153 at 1440x1000, plus manual keyboard traversal. Source references point at v6.31.0.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions