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:
component.Abstract / component.Base: framework-generic state, visual class, serialization.
manager.DomEvent: worker-side event-routing suppression.
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
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.
Context
PR #15312 exposed a framework-core accessibility gap while repairing its paged mailbox controls. The app set
button.Base.disabled = trueand correctly received theneo-disabledvisual class, but the rendered native<button>remained focusable and was announced as enabled because no HTMLdisabledattribute reached its VDOM root.The review initially suspected
component.Base, but source inspection falsified that prescription:component.Abstract.disabledpromises the class plus DOM-event suppression, andmanager.DomEventalready stops non-resize events whencomponent.disabledis true. A generic component may render adiv, where blindly emitting a nativedisabledattribute would be meaningless. The missing native semantic belongs to the control that owns a native button root:button.Base.The Problem
src/button/Base.mjsrenders a native<button>, inheritsdisabled_, and relies oncomponent.Base.afterSetDisabled(). That inherited hook only togglesneo-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:PR #15312 now carries exactly that compensation (
aria-disabledplus 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-76defines the generic contract:neo-disabledplus no DOM events.src/manager/DomEvent.mjs:178-184enforces the event-routing half.src/component/Base.mjs:433-438toggles only the class.src/button/Base.mjsowns the native<button>VDOM root but has noafterSetDisabledoverride.src/component/mwc/Button.mjs:92-100is a positive precedent for mapping the config to the rendered control withchangeVdomRootKey('disabled', value).src/button/Split.mjs:211-219mapsgetVdomRoot()to its main native button, while its trigger is anotherbutton.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:
component.Abstract/component.Base: framework-generic state, visual class, serialization.manager.DomEvent: worker-side event-routing suppression.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 tosrc/button/Base.mjsthat:super.afterSetDisabled(value, oldValue)soneo-disabledremains the visual state;changeVdomRootKey('disabled', value);manager.DomEventas 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
button.Base.disabled<button>root + inherited component configtruerenders the nativedisabledattribute and retainsneo-disabled;falseremoves bothDomEventsuppression remains defense in depthbutton.Split.disabledgetVdomRoot()main-button mapping + triggerbutton.Basecomponent.Base.disabledcomponent.AbstractcontractAcceptance Criteria
button.Base.disabled = trueadds the nativedisabledattribute to its rendered<button>root while retainingneo-disabled.falseremoves the native attribute and restores native focus/activation behavior.button.Split.disabledapplies native disabled semantics to both its main button and trigger button.component.Base.disabledcontract andmanager.DomEventguard remain unchanged.Out of Scope
neo-disabledvisual class.disabledattribute on arbitrary component roots.aria-disabledand focus policy per widget role.Avoided Traps
component.Baseglobally. Many components render non-form roots, so a universal HTMLdisabledattribute would claim semantics browsers do not implement.aria-disabled. ARIA communicates state but does not supply native focus and activation behavior.manager.DomEventsuppression. 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: inspect2531e33acbb5dded68e8b92abdc077dfa166c5aband Grace's disposition on PR #15312.