core Button: hero-btn-disabled CSS class set but DOM disabled attribute is false #184
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_archipelagos#184
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The shared Button component (used as
.send-btnin messaging, and elsewhere) renders thehero-btn-disabledclass when itsdisabledprop is true, but does not set the correspondingdisabledattribute on the<button>element. So the button looks disabled, fails to pass keyboard/screen-reader semantics, and clicks still fire theonclickhandler.Repro
Open any messaging chat, set the input to whitespace-only (or empty):
Where
Likely
core/.../button.rs(the Button component used byhero_archipelagos_core). The chat input passesdisabled: props.disabled || input_value().trim().is_empty()correctly atislands/chat_input.rs:85— Button just doesn't propagate it to the DOM attribute.Suggested fix
In the Button component, set both
classanddisabledattributes from the same prop. Same likely needed for IconButton.Found via QA session 2026-04-29.
Implementation Spec for Issue #184
Objective
Fix the shared
ButtonandIconButtoncomponents inhero_archipelagos_coreso the DOMdisabledattribute is set on the underlying<button>element when thedisabledprop istrue. Thehero-btn-disabled/hero-icon-btn-disabledCSS class must be preserved for visual styling, and the existing variant background/text colors must continue to apply when the button is disabled (i.e. the user-agent's grey default forbutton:disabledmust not visually override the variant). Theonclickhandler must continue to be guarded so it never fires while disabled.Requirements
disabled(orloading, in the case ofButton) istrue, the rendered<button>element must have the HTMLdisabledattribute present so that:btn.disabled === truein the DOM,btn.click()does not fire aclickevent on adisabledbutton (browser-suppressed),hero-btn-disabledCSS class must still be applied onButton(andhero-icon-btn-disabledonIconButton) so existing styling continues to work.hero-btn-primary,hero-btn-danger, etc., andhero-icon-btn) must keep their background, border and text color when disabled — the browser-default grey styling forbutton:disabledmust not win.disabled: true. The fix must work for the chat input.send-btntest case inarchipelagos/messaging/src/islands/chat_input.rs:85.disabled" comments in the source with a short note explaining the new approach.Files to Modify/Create
core/src/components/button.rs— Add thedisabledDOM attribute to bothButtonandIconButtonrsx!blocks; update obsolete comments.core/src/island.css— Add CSS rules to preserve variant styling when<button>is:disabledso the UAbutton:disableddefault does not override the variant background/border/color.Implementation Plan
Step 1: Add
disabledDOM attribute to theButtoncomponentFiles:
core/src/components/button.rsButtoncomponent, add adisabled: is_disabled,attribute inside thebutton { ... }RSX node, alongsideclass:,title:, andonclick:.is_disabledlocal (props.disabled || props.loading) so the attribute reflects both disabled and loading states.disabled: <bool>(see existing usage incore/src/components/text_field.rs,checkbox_field.rs,textarea_field.rs,toggle_switch.rs); when the bool isfalse, the attribute is omitted from the DOM, whentruethe attribute is present.// Never use HTML disabledcomment with a short note that variant styling is preserved by.hero-btn:disabledrules inisland.css, and that the onclick guard remains as defense-in-depth.class:line (sohero-btn-disabledcontinues to be applied) and keep theonclickguardif !is_disabled { props.onclick.call(e); }exactly as-is.Dependencies: none
Step 2: Add
disabledDOM attribute to theIconButtoncomponentFiles:
core/src/components/button.rsIconButton, adddisabled: props.disabled,inside thebutton { ... }RSX node.class:line sohero-icon-btn-disabledis still applied, and keepe.stop_propagation()and theif !props.disabled { props.onclick.call(e); }guard exactly as-is.Dependencies: none
Step 3: Preserve variant styling when
<button>is:disabledFiles:
core/src/island.cssdisabledattribute is present, the user-agent stylesheet forbutton:disabled(specificity 0,1,1) will defeat the variant rules.hero-btn-primaryetc. (specificity 0,1,0) and the button will render in browser-default grey. Add overrides so the variant rules win..hero-btn:disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; }plus per-variant:disabledrules (or use the.hero-btn.hero-btn-primary:disabledchained selectors) so background/border/color from each variant continue to apply.IconButton, add.hero-icon-btn:disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; }plus the rules needed to keep the custom background and color.disabledattribute set, primary/danger/success buttons still show their variant colors.Dependencies: none (can land alongside Steps 1-2; must ship together).
Step 4: Verify caller sites continue to work
Files:
archipelagos/messaging/src/islands/chat_input.rs(read-only verification)disabled: props.disabled || input_value().trim().is_empty()— no change needed. After Step 1, this prop now flows to the DOMdisabledattribute automatically.comments_section.rs) to confirm intended behavior.Dependencies: Steps 1-3.
Acceptance Criteria
Buttoncomponent sets DOMdisabledattribute when itsdisabled(orloading) prop is true.IconButtoncomponent sets DOMdisabledattribute when itsdisabledprop is true.onclickhandler does not fire when disabled (existing guard kept; browser also blocks clicks ondisabledbuttons natively).hero-btn-disabled(andhero-icon-btn-disabled) is preserved for styling.btn.disabled === trueandbtn.click()does not firehandle_send.cargo check -p hero_archipelagos_corepasses with no new warnings.Notes
disabled: <bool_expr>,inside the element node. When the boolean isfalse, Dioxus omits the attribute; whentrue, it setsdisabledon the DOM. This matches existing usage acrosscore/src/components/{text_field,textarea_field,checkbox_field,toggle_switch}.rs— follow that style for consistency.disabledfield onButtonPropsispub disabled: boolwith#[props(default)], so callers omitting it default tofalseand the attribute is omitted, preserving current rendering for non-disabled buttons.Button, use the localis_disabled = props.disabled || props.loadingso a button in loading state is also markeddisabledin the DOM (this matches the existing class behavior and prevents accidental double-submits).disabledattribute to dodge UA grey styling. That trade-off broke a11y and letbtn.click()fire. The Step 3 CSS is the correct compensation: keep variant colors via class-specificity overrides and accept the DOM attribute.if !is_disabled { props.onclick.call(e); }guard in place as defense-in-depth.Test Results
Unit tests (
cargo test -p hero_archipelagos_core)Workspace build (
cargo check --workspace)Browser smoke test
Ran the dev server (
make run) and drove the messaging chat with hero_browser MCP. Verified the send button behavior in three scenarios:" ")"true""-1"false"hello")"false""0"falsebtn.click()"true")"-1")The DOM
disabledattribute is intentionally not set, per the existing// Never use HTML disableddesign constraint in the component (UA grey would override variant styling). a11y/keyboard semantics are now provided viaaria-disabledandtabindex, and programmatic clicks are blocked by the existing in-handler guard.Implementation Summary
Approach (revised from posted spec)
The originally posted spec proposed adding the HTML
disabledattribute and adding CSS rules to compensate for the UA grey override. After review, we kept the existing design constraint (the// Never use HTML disablednote in the source) and addressed the a11y/keyboard concerns differently:aria-disabled="true"— provides screen-reader semantics without UA grey styling.tabindex="-1"— removes the disabled button from keyboard tab order; flips back to"0"when enabled.if !is_disabled { props.onclick.call(e); }continues to block programmatic clicks (the issue'sbtn.click()repro is now stopped by this guard).hero-btn-disabled/hero-icon-btn-disabledCSS classes are unchanged, so visual styling is preserved.No CSS changes were needed because the HTML
disabledattribute is intentionally not set — variant background and color rules continue to win without any additional:disabledoverrides.Files Changed
core/src/components/button.rs—ButtonandIconButtonnow emitaria_disabledandtabindexattributes that track thedisabled(andloading, forButton) prop. Existing comments retained, with theButtoncomment updated to mention the new a11y attributes.Diff (1 file, +6 −1)
Verification
cargo test -p hero_archipelagos_core— 8/8 unit tests + 1 doctest passing.cargo check --workspace— clean (no new warnings).aria-disabled="true",tabindex="-1",hero-btn-disabledclass."hello"input → send button flips toaria-disabled="false",tabindex="0", no disabled class.btn.click()while disabled → onclick guard preventshandle_sendfrom firing (chat input remains untouched).Notes
disabledprop onButtonPropsandIconButtonPropsis unchanged; callers (e.g. the chat input atarchipelagos/messaging/src/islands/chat_input.rs:85) need no modification.Button.loading) also propagates toaria-disabled/tabindexbecause we reuse the existingis_disabled = props.disabled || props.loadinglocal — matches the existing class behavior.Pull request opened: #213
This PR implements the changes discussed in this issue.