Skip to content

Licensing alert blocks the CP but has no dialog role, name, inert page, or Escape handler #15393

Description

@bpmore

Bug description

The licensing alert that opens on first load of a trial or unlicensed site covers the page and swallows every click until dismissed, but it has none of the semantics of a dialog.

Measured on first load of /cp/dashboard:

document.querySelector('[role=dialog],[role=alertdialog]')  ->  null
containing chain    ->  div.portal-targets -> div -> body
                        no role, no aria-modal, no aria-label/labelledby
elements with [inert]  ->  0
Tab cycle           ->  Snooze <-> Manage Licenses only
Escape              ->  does not dismiss

Focus is moved into the alert, so a screen reader user is dropped into an unnamed two-button loop with no announcement of what happened or that the page behind is blocked. The page behind is not marked inert, so the accessibility tree still exposes it as available while every click is being swallowed. Escape does nothing, which breaks the convention every other overlay in the CP follows.

The command palette in the same codebase gets this right: role="dialog", focus moved inside, rest of the page inert, Escape closes. The licensing alert should use the same primitive.

Source: https://github.com/statamic/cms/blob/v6.31.0/resources/js/components/LicensingAlert.vue, mounted from https://github.com/statamic/cms/blob/v6.31.0/resources/js/pages/layout/Layout.vue

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. Set STATAMIC_PRO_ENABLED=true with no license key, so the site is in trial mode
  3. Create a super user and log in to /cp
  4. With the "Licensing Alert" showing, run in the console:
document.querySelector('[role=dialog],[role=alertdialog]')  // null
document.querySelectorAll('[inert]').length                 // 0
  1. Press Escape. The alert stays open.

Suggested fix

Render it through the same dialog primitive the command palette uses:

  • role="alertdialog" and aria-modal="true"
  • aria-labelledby pointing at the "Licensing Alert" heading
  • mark the page behind it inert
  • map Escape to Snooze

Additional note, worth flagging separately

Because this overlay is up on first load and is not marked as a dialog, any automated accessibility tooling pointed at a trial Statamic install silently measures the alert instead of the page behind it. The first pass of the audit that found this returned near-identical results for 48 different CP screens before we realised what was being scanned. Anyone benchmarking Statamic's CP accessibility will get wrong numbers until this is fixed or they know to dismiss it first.

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