LearnNewsExamplesServices
Frontmatter
id15324
titlebutton.Base disabled config omits native disabled semantics
stateOpen
labels
bugaitestingcoreaccessibility
assigneesneo-gpt-emmy
createdAt1:28 AM
updatedAt1:36 AM
githubUrlhttps://github.com/neomjs/neo/issues/15324
authorneo-gpt
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

button.Base disabled config omits native disabled semantics

Open Backlog/active-chunk-6 bugaitestingcoreaccessibility
neo-gpt
neo-gpt commented on 1:28 AM

Context

PR #15312 exposed a framework-core accessibility gap while repairing its paged mailbox controls. The app set button.Base.disabled = true and correctly received the neo-disabled visual class, but the rendered native <button> remained focusable and was announced as enabled because no HTML disabled attribute reached its VDOM root.

The review initially suspected component.Base, but source inspection falsified that prescription: component.Abstract.disabled promises the class plus DOM-event suppression, and manager.DomEvent already stops non-resize events when component.disabled is true. A generic component may render a div, where blindly emitting a native disabled attribute would be meaningless. The missing native semantic belongs to the control that owns a native button root: button.Base.

The Problem

src/button/Base.mjs renders a native <button>, inherits disabled_, and relies on component.Base.afterSetDisabled(). That inherited hook only toggles neo-disabled. The worker-side event manager prevents the handler from receiving routed DOM events, but the browser and assistive technology cannot see that internal guard:

  • the button stays in the tab order;
  • accessibility APIs announce it as enabled;
  • browser-native disabled behavior is absent;
  • every app that needs honest edge semantics must add local ARIA or handler compensation.

PR #15312 now carries exactly that compensation (aria-disabled plus a handler-side refusal). It is a safe local repair, but repeating it for every native Button would leave the framework contract split between visual, worker-routing, and browser semantics.

Empirical anchors on current dev:

  • src/component/Abstract.mjs:72-76 defines the generic contract: neo-disabled plus no DOM events.
  • src/manager/DomEvent.mjs:178-184 enforces the event-routing half.
  • src/component/Base.mjs:433-438 toggles only the class.
  • src/button/Base.mjs owns the native <button> VDOM root but has no afterSetDisabled override.
  • src/component/mwc/Button.mjs:92-100 is a positive precedent for mapping the config to the rendered control with changeVdomRootKey('disabled', value).
  • src/button/Split.mjs:211-219 maps getVdomRoot() to its main native button, while its trigger is another button.Base; a Button-level fix can therefore cover both without teaching generic components HTML-button semantics.

The Architectural Reality

The disabled contract has three distinct owners:

  1. component.Abstract / component.Base: framework-generic state, visual class, serialization.
  2. manager.DomEvent: worker-side event-routing suppression.
  3. button.Base: browser-native semantics for the concrete <button> root.

The first two are already present and should remain. The missing third leg must be added at button.Base, not broadened to all components.

Decision Record impact: none — this aligns existing component/control ownership; it does not introduce or challenge an ADR.

The Fix

Add a documented afterSetDisabled(value, oldValue) override to src/button/Base.mjs that:

  • delegates to super.afterSetDisabled(value, oldValue) so neo-disabled remains the visual state;
  • mirrors the boolean onto the effective native button root via changeVdomRootKey('disabled', value);
  • preserves manager.DomEvent as defense in depth.

Cover the base Button and SplitButton shapes. The test must exercise the rendered DOM, not only the reactive config or CSS class, because the defect is precisely the missing browser-visible attribute.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
button.Base.disabled Native <button> root + inherited component config true renders the native disabled attribute and retains neo-disabled; false removes both Worker-side DomEvent suppression remains defense in depth Button hook JSDoc focused unit delta + real-DOM component/e2e assertion
button.Split.disabled getVdomRoot() main-button mapping + trigger button.Base both native button elements reflect the disabled state existing propagated trigger config focused SplitButton test note rendered-DOM assertion for main + trigger
generic component.Base.disabled component.Abstract contract unchanged: class + routed-event suppression; no universal HTML attribute current behavior no contract widening regression assertion or existing-source audit

Acceptance Criteria

  • Setting button.Base.disabled = true adds the native disabled attribute to its rendered <button> root while retaining neo-disabled.
  • Setting it back to false removes the native attribute and restores native focus/activation behavior.
  • A disabled Button is excluded from normal keyboard focus and is exposed as disabled through the browser accessibility tree.
  • button.Split.disabled applies native disabled semantics to both its main button and trigger button.
  • The generic component.Base.disabled contract and manager.DomEvent guard remain unchanged.
  • Focused tests include a real-DOM assertion; config/class-only evidence is insufficient.

Out of Scope

  • Replacing the generic neo-disabled visual class.
  • Emitting a native disabled attribute on arbitrary component roots.
  • Redesigning disabled semantics for non-native composite widgets; those may require aria-disabled and focus policy per widget role.
  • Removing PR #15312's local defense-in-depth guard in the same change.

Avoided Traps

  • Do not fix component.Base globally. Many components render non-form roots, so a universal HTML disabled attribute would claim semantics browsers do not implement.
  • Do not add only aria-disabled. ARIA communicates state but does not supply native focus and activation behavior.
  • Do not remove manager.DomEvent suppression. Native semantics and worker routing protect different boundaries.

Related

Related: #15312, #15316

Live latest-open sweep: checked latest 20 open issues at 2026-07-16T23:27:36Z; no equivalent found. Recent A2A claim sweep and archived issue/discussion sweep were also clear.

Retrieval Hint: query button.Base disabled native attribute neo-disabled DomEvent PR #15312. Retrieval Hint: inspect 2531e33acbb5dded68e8b92abdc077dfa166c5ab and Grace's disposition on PR #15312.