Remove disabled Pajamas button links from the tab order
What breaks
Passing href together with disabled or loading renders:
<a disabled="disabled" aria-disabled="true" class="gl-button btn disabled btn-md btn-default" href="/foo">disabled isn't valid on <a>, so browsers ignore it. The link stays focusable and in the tab order.
Why this isn't caught everywhere
app/assets/javascripts/main.js:173-175 binds a click guard directly to .btn elements, checking hasClass('disabled') and calling preventDefault() and stopImmediatePropagation(). It doesn't cover markup injected after it runs. Where it's attached, the only defect is tab order. Where it isn't, the link can also be activated.
Traced case
The milestone labels tab renders two disabled link buttons (app/views/shared/milestones/_labels_tab.html.haml:20 and :23) via app/assets/javascripts/milestones/milestone.js:33, after main.js has already bound its guard. DOMPurify's config (app/assets/javascripts/lib/dompurify.js:14-31) keeps the tabindex attribute intact.
The fix
# `disabled` is not valid on `<a>`, so browsers ignore it and the link stays
# focusable. Take it out of the tab order the way GlButton does.
attributes['tabindex'] = '-1' if link? && !form? && (@disabled || @loading)base_attributes is spread after @button_options in the template, so a caller-supplied tabindex is overridden. The !form? guard leaves the button_to path alone, since that renders a real <button> where native disabled already works. GlButton's tabindex() returns -1 for disabled links when not in accessible-disabled mode, and GlLink's click guard is what actually blocks activation. This fix only handles tab order, so it isn't full parity with that Vue behavior. It also fires on loading, which GlButton's tabindex() doesn't, since a loading link is as unusable as a disabled one.
Affected call sites
Ten call sites pass both disabled and href, across five files:
app/views/kaminari/gitlab/_keyset_paginator.html.haml(4)app/views/shared/milestones/_labels_tab.html.haml(2)app/views/shared/integrations/gitlab_slack_application/_slack_integration_form.html.haml(2)ee/app/views/admin/application_settings/_semantic_search_settings.html.haml(1)app/views/admin/users/_head.html.haml(1)
Specs
spec/components/pajamas/button_component_spec.rb: 91 examples, 0 failures. Rubocop clean.
Related
- #612142 — accessible-disabled mechanism for this component
- !250688 (merged) — in-flight MR adding an opt-in
accessible_disabledoption; if it merges first, the condition here becomeslink? && !form? && native_disabled? - #621995 — follow-up: omit
hrefwhen disabled, plus the dead-attribute cleanup