Make server-rendered Pajamas::ButtonComponent accessible when disabled

Summary

Follow-up from review of !247432 (merged) (removal of the accessible_disabled_button feature flag), specifically this discussion.

GlButton (Vue) now renders aria-disabled="true" instead of the native disabled attribute when disabled, keeping the button focusable and announced by assistive technology, and relies on JS to no-op the click so a focusable-but-disabled button does nothing when activated.

Server-rendered Pajamas::ButtonComponent has no equivalent. Pajamas::ButtonComponent#base_attributes (app/components/pajamas/button_component.rb:129-130) already sets both disabled and aria-disabled when disabled/loading:

attributes['disabled'] = 'disabled' if @disabled || @loading
attributes['aria-disabled'] = true if @disabled || @loading

So the aria-disabled attribute is present, but the button also still carries the native disabled attribute, which removes it from the tab order — the opposite of the focusable-when-disabled behavior GlButton now provides. And if we wanted to drop the native disabled to match GlButton, there is no JS layer on server-rendered buttons to prevent the click from doing something.

The decision

Settled in !250688 (merged), and it's a third option rather than either of the two below: make it opt-in.

  • Pajamas::ButtonComponent gains an accessible_disabled option defaulting to false. Server-rendered buttons keep native disabled unless a call site asks for the accessible variant.
  • In @gitlab/ui, GlButton keeps its accessibleDisabled prop and flips the default to true. Vue buttons stay accessible by default, and authors opt out with :accessible-disabled="false".

Why the two sides differ: GlButton owns both markup and behaviour, so it can guarantee a focusable disabled button does nothing when activated. Server-rendered buttons have no such coupling. Rapid Diffs renders a disabled button that JS re-enables at runtime, with clicks routed through a web component, so the markup on its own doesn't tell you how the button behaves. Defaulting to native disabled server-side avoids guessing.

Proposed by @markrian, endorsed by @trevorpierce, @sdejonge and @aferch. Still open: how the click guard for opted-in buttons should be bound, raised by @thutterer.

The two options originally framed here were (1) add shared JS so all server-rendered buttons drop native disabled, or (2) leave them all on native disabled and accept the inconsistency. Opt-in is the middle.

Scope note

Out of scope for !247432 (merged), which is confined to the flag removal and its GlButton test fallout. Raised here so the server-rendered gap is tracked rather than lost.

References

Edited by Adam Ferch