You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A11y: valid widget roles for Styles page tabbable elements - #3471
IBM Equal Access flagged element_tabbable_role_valid 8 times on the Styles admin page (tests/cypress/e2e/admin-a11y.cy.js, CI log had ruleId/message only, no selectors — see Strategy11/formidable-pro#6756).
What changed
Field Shape's 4 radio-style labels (field-shape.php): each <label> had its own tabindex="0" (needed because the underlying native <input type="radio"> is display: none) but no ARIA role. A widget role can't go directly on <label> — IBM's aria_role_valid rejects any role there. Moved tabindex/role="radio"/aria-checked onto a new inner <span> wrapping the icon, and added aria-label (Regular / Rounded corners / Circle / Underline) so the new radio role has an accessible name.
Primary Color's colorpicker span (primary-color.php): had a redundant tabindex="0" duplicating the native focusability of its own nested <input type="text">. Removed the redundant tabindex rather than adding a role — role="button" here would wrap a focusable descendant (a new aria_descendant_valid violation) for no behavioral benefit, since the input was already the real interactive control.
No CSS or JS changes — matches the issue's stated scope.
How this was verified
checkIbmAccessibility's assertCompliance(false) means CI never actually fails on this rule, so a green Cypress run proves nothing either way. Reproduced locally instead, per fix-sop's documented exception for this rule: logged into a formidable-preview-env instance (Lite only, matching what cypress.yml checks out), curled the authenticated Styles page, injected <base href>, and ran IBM's own accessibility-checker CLI against the saved HTML.
Red: 8/8 element_tabbable_role_valid violations reproduced, exact XPath/snippet match for the two templates above.
Green: 0 element_tabbable_role_valid violations after the fix.
Compared every rule's violation count before/after to check for regressions: total ARIA violation-level findings actually dropped by 12 (8 target rule + 4 svg_graphics_labelled, fixed as a side effect since the new aria-label now gives the wrapped icon an accessible name too).
A few new potentialviolation-level input_label_visible/element_tabbable_unobscured advisories appeared (aria-label isn't a visible label) — same rule/severity already present elsewhere on this page for other controls, not a new failure class. Making the label visible would need a CSS/layout change, out of scope for this markup-only fix.
element_tabbable_role_valid (IBM Equal Access), 8 instances all on the
Styles admin page:
- Field Shape's 4 radio-style labels had their own tabindex="0" (needed
since the underlying native <input type="radio"> is display:none) but
no ARIA role - and role can't go on <label> itself (IBM's aria_role_valid
rejects any widget role there). Moved tabindex/role="radio"/aria-checked
onto a wrapping <span> around the icon instead, with aria-label naming
each shape so the new radio role still has an accessible name.
- Primary Color's colorpicker span had a redundant tabindex="0" duplicating
the native focusability of its own nested text input, with no valid role
of its own. Removed the redundant tabindex rather than adding a role,
since role="button" here would wrap a focusable descendant (a new
aria_descendant_valid violation) for no behavioral benefit - the input
was already the real interactive control.
Verified via curl + IBM's accessibility-checker CLI against the real
rendered Styles page (login-authenticated, <base href> injected for
relative assets) per fix-sop.md's exception for this specific rule -
CI's own checkIbmAccessibility only logs violations, never fails
(assertCompliance(false)), so a green Cypress run doesn't prove anything.
Confirmed all 8 element_tabbable_role_valid violations before the fix
(exact XPath/snippet match), 0 after, with a live before/after diff of
every rule's violation count to confirm no regression - total ARIA
"violation"-level count actually dropped by 12 (8 target rule +
4 svg_graphics_labelled, fixed as a side effect of the new aria-label
now covering the icon's own accessible name). A few new potentialviolation
advisories appeared (aria-label isn't a visible label) - same rule/severity
already present elsewhere on this page, not a new failure class, and
adding visible text is a CSS layout change out of scope for this markup-only
fix.
Refs #6756
We reviewed changes in cbbdf70...d41891d on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.
AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.
The reason will be displayed to describe this comment to others. Learn more.
Variable $field_value might not be defined
A variable has been used but not defined, which may result in warnings during program execution. This can also cause bugs since the intended usage scope of the variable is not known.
The reason will be displayed to describe this comment to others. Learn more.
Request Changes. Two live-verified defects in this diff's own new code, plus non-blocking generalization notes.
1. aria-checked is set once at PHP render time and never updated when the user picks a different shape. Confirmed live in wp-preview-env (lite, PR branch loaded): clicked "Circle" — the real underlying <input id="frm-field-shape-circle"> correctly became checked: true, but its role="radio" span's aria-checked attribute stayed "false", and the previously-selected span's aria-checked="true" never cleared. radio-component.js's onRadioChange() (the only change handler wired to these inputs) moves the visual tracker and shows/hides extra elements, but never touches aria-checked — grepped the whole diff and radio-component.js for aria-checked, zero hits outside the PHP template. A screen reader user who changes shape and revisits the group is told the previous selection is still checked. Fix belongs in onRadioChange(): after moving the tracker, sync each sibling span's aria-checked to its associated input's .checked.
2. The new role="radio"/tabindex="0" spans are keyboard-focusable but not keyboard-operable. Live-verified: focused the "Regular" span via .focus() (matching real Tab behavior) and pressed both Enter and Space — neither selects the option (confirmed via the underlying input's .checked staying false); a plain mouse/label click does work (native label[for] click-forwarding). Grepped the repo for any keydown/keypress handler on this component — none exists. A keyboard-only user now tabs to an element a screen reader explicitly announces as an operable "radio button" (via the new role + aria-checked + aria-label this PR adds) and finds it does nothing — a more misleading state than the previous plain, roleless tabindex="0" label, which at least didn't claim to be a working widget. The 4 spans also aren't wrapped in a role="radiogroup" container and all carry tabindex="0" simultaneously rather than a roving tabindex — arrow-key navigation between options (expected once role="radio" is declared, per the ARIA APG radio-group pattern) doesn't exist either. Minimum fix: a keydown handler on each span forwarding Enter/Space to click() on its associated native radio input (or programmatically checking it and dispatching change), ideally alongside the roving-tabindex/arrow-key pattern the role implies.
Both confirmed independently by a backgrounded code-review pass, which I cross-verified against the live page and source before including.
Non-blocking generalization notes (same defect classes this PR fixes elsewhere in the same directory, left untouched):
colorpicker.php:5 has the identical redundant tabindex="0" on a <span> wrapping an already-focusable <input type="text"> that this PR removes from primary-color.php — same component family (FrmColorpickerStyleComponent), same fix would apply.
direction.php, align.php, and text-toggle.php still render <label tabindex="0"> with no role at all, wrapping a display:none radio input — the same element_tabbable_role_valid-shaped gap this PR's title ("valid widget roles for Styles page tabbable elements") describes fixing, just outside the specific 8 violations this fixture happened to hit. Given finding #2 above, simply copying this PR's role="radio" pattern onto them isn't sufficient on its own — worth fixing keyboard operability across all of them together rather than one template at a time.
CI: DeepSource: PHP" is red (PHP-W1066, "$field_value might not be defined" on field-shape.php:9") but confirmed a false positive — read FrmStyleComponent::load_view() directly: it sets $field_value = $this->field_value; in local scope immediately before include-ing this exact template, so the variable is always defined at render time. DeepSource's static analyzer doesn't trace include-based variable injection. Not treated as blocking.
…adio spans
Franky's CHANGES_REQUESTED review on this PR found two live defects in the
role="radio" spans this PR added to field-shape.php: aria-checked was only
ever set at PHP render time and never updated when the selection changed,
and the spans were focusable but had no keydown handler, so Enter/Space
did nothing despite being announced as an operable radio widget.
radio-component.js now syncs aria-checked across the group on every change
(syncAriaChecked) and forwards Enter/Space activation from each span to its
associated native input (onSpanKeydown), via a new getRadioSpan lookup.
Compiled js/formidable-settings-components.js hand-patched with only this
function's minified output spliced in (module IDs churn non-deterministically
on a full rebuild, per TOOLS.md) — verified byte-identical to a full rebuild's
functional change before splicing.
Verified live in formidable-preview-env: label-click, Enter, and Space all
correctly select the option and sync aria-checked across the group; no new
console errors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
syncAriaChecked() updates every span's aria-checked in the group on each change, not just at render time.
onSpanKeydown() forwards Enter/Space on a span to its native input (via a new getRadioSpan() lookup), so the role="radio" spans are keyboard-operable, not just focusable.
Verified live in formidable-preview-env: label-click, Enter, and Space all select the option and sync aria-checked across the group; no new console errors.
Left the non-blocking generalization notes (colorpicker.php:5, direction/align/text-toggle) untouched, as suggested — out of this issue's scope.
The reason will be displayed to describe this comment to others. Learn more.
Expected 'this' to be used by class method 'getRadioSpan'
If a class method does not use this, it can sometimes be made into a static function. If you do convert the method into a static function, instances of the class that call that particular method have to be converted to a static call as well (MyClass.callStaticMethod())
The reason will be displayed to describe this comment to others. Learn more.
Expected 'this' to be used by class method 'onSpanKeydown'
If a class method does not use this, it can sometimes be made into a static function. If you do convert the method into a static function, instances of the class that call that particular method have to be converted to a static call as well (MyClass.callStaticMethod())
The reason will be displayed to describe this comment to others. Learn more.
Approve, with one non-blocking note. Both previously-flagged live defects are fixed and live-verified.
Live-verified in formidable-preview-env (lite branch loaded to this PR's head):
aria-checked now stays in sync — clicked through all 4 shapes; the correct span flips to "true" and every sibling flips to "false" on each change, confirmed via getAttribute('aria-checked') on all 4 spans after each click.
Keyboard operability — focused the Regular span and pressed Enter: input became checked, change fired, aria-checked synced. Repeated with Space on the Underline span: same result. Mouse/label click still works unchanged.
Screenshot of the widget in its current (Underline-selected) state:
CI: green except DeepSource: PHP/DeepSource: JavaScript. PHP is the same pre-existing field-shape.php:9 false positive already confirmed last round (FrmStyleComponent::load_view() sets $field_value before the include, DeepSource just doesn't trace it). JavaScript is new — inline note below, non-blocking.
Non-blocking, unchanged from last round: colorpicker.php:5's identical redundant tabindex="0" on an already-focusable input, and direction.php/align.php/text-toggle.php's still-unfixed unlabeled-icon-only-radio + missing keyboard-operability pattern this PR's title describes fixing more broadly — worth a follow-up pass across all of them together.
The reason will be displayed to describe this comment to others. Learn more.
Fixed, took the suggested diff verbatim. Behaviorally identical (radio.labels is either a NodeList or null, same short-circuit either way), confirmed via ESLint against the real repo config plus a syntax check - no other change.
Separately, re-scanning surfaced two pre-existing DeepSource JS-0105 findings on the same file (getRadioSpan/onSpanKeydown "expected this to be used") that were already there before this push, not raised in your review - leaving those out of scope for this round.
Non-blocking notes (colorpicker.php:5 redundant tabindex, direction.php/align.php/text-toggle.php unlabeled-icon-only-radio pattern): leaving both out of scope for this PR, same as last round - framed as a follow-up pass across all of them together, not an ask for this diff.
The reason will be displayed to describe this comment to others. Learn more.
Approve. Only change since the prior approval is a pure syntactic refactor — no behavior change.
Diff since last franky approval (d3052f7 → d41891d):getRadioSpan()'s radio.labels && radio.labels[0] became radio.labels?.[0]. Functionally identical (radio.labels is always a live NodeList on a real <input>, never undefined) — confirms the previously live-verified aria-checked sync and keyboard-operability fixes are untouched by this commit.
Non-blocking notes carried over unchanged from last round (still deliberately out of scope per the PR's own comments): colorpicker.php:5 redundant tabindex, and direction.php/align.php/text-toggle.php's unlabeled-icon-only-radio + missing keyboard-operability pattern — worth a follow-up pass across all of them together.
Approved, no unresolved findings — the non-blocking notes carried over (colorpicker.php tabindex, the direction/align/text-toggle unlabeled-icon-radio pattern) are already deferred from earlier rounds, nothing new to action. Clearing vivi-pickup/vivi-working, ready for a human merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was broken
IBM Equal Access flagged
element_tabbable_role_valid8 times on the Styles admin page (tests/cypress/e2e/admin-a11y.cy.js, CI log had ruleId/message only, no selectors — see Strategy11/formidable-pro#6756).What changed
field-shape.php): each<label>had its owntabindex="0"(needed because the underlying native<input type="radio">isdisplay: none) but no ARIA role. A widget role can't go directly on<label>— IBM'saria_role_validrejects any role there. Movedtabindex/role="radio"/aria-checkedonto a new inner<span>wrapping the icon, and addedaria-label(Regular / Rounded corners / Circle / Underline) so the new radio role has an accessible name.primary-color.php): had a redundanttabindex="0"duplicating the native focusability of its own nested<input type="text">. Removed the redundanttabindexrather than adding a role —role="button"here would wrap a focusable descendant (a newaria_descendant_validviolation) for no behavioral benefit, since the input was already the real interactive control.No CSS or JS changes — matches the issue's stated scope.
How this was verified
checkIbmAccessibility'sassertCompliance(false)means CI never actually fails on this rule, so a green Cypress run proves nothing either way. Reproduced locally instead, per fix-sop's documented exception for this rule: logged into aformidable-preview-envinstance (Lite only, matching whatcypress.ymlchecks out), curled the authenticated Styles page, injected<base href>, and ran IBM's ownaccessibility-checkerCLI against the saved HTML.element_tabbable_role_validviolations reproduced, exact XPath/snippet match for the two templates above.element_tabbable_role_validviolations after the fix.violation-level findings actually dropped by 12 (8 target rule + 4svg_graphics_labelled, fixed as a side effect since the newaria-labelnow gives the wrapped icon an accessible name too).potentialviolation-levelinput_label_visible/element_tabbable_unobscuredadvisories appeared (aria-label isn't a visible label) — same rule/severity already present elsewhere on this page for other controls, not a new failure class. Making the label visible would need a CSS/layout change, out of scope for this markup-only fix.Refs Strategy11/formidable-pro#6756