Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/button-busy-no-native-disabled.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@astryxdesign/core': patch
---

[fix] Button: the busy state (a pending `clickAction`, or `isLoading`) no longer sets the native `disabled` attribute, which dropped keyboard focus to `<body>` for the duration of the action. A busy button stays focusable and is announced via `aria-busy` + `aria-disabled`; re-activation (click, Enter, Space) is blocked by the existing handler guards, so fire-once actions still fire once. `isDisabled` is unchanged and still uses native `disabled`. With `href`, a busy button now stays an anchor instead of swapping to a disabled `<button>` mid-action. Consumers that checked the `disabled` attribute to detect a busy button should check `aria-busy` instead.

Also fixed: a rejected `clickAction` no longer strands the button in the busy state (previously the transition never settled, so the spinner stayed and every retry was blocked until remount). The rejection now clears the loading state and is reported via `devError`.

@AKnassa
12 changes: 6 additions & 6 deletions packages/core/src/Button/Button.doc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const docs = {
{
name: 'isLoading',
type: 'boolean',
description: 'Shows a loading spinner and disables interaction. Announces "Loading" via a live region.',
description: 'Shows a loading spinner and guards against re-activation while keeping the button focusable (aria-busy + aria-disabled, never native disabled). Announces "Loading" via a live region.',
default: 'false',
},
{
Expand Down Expand Up @@ -148,7 +148,7 @@ export const docs = {
name: 'clickAction',
type: '(e: MouseEvent) => void | Promise<void>',
description:
'Async click handler. Shows loading state while the returned promise is pending.',
'Async click handler. Shows loading state while the returned promise is pending. A rejected promise clears the loading state (so the user can retry) and is reported via devError; handle expected failures inside the action.',
},
],
playground: {
Expand Down Expand Up @@ -209,7 +209,7 @@ export const docsZh = {
{name: 'name', type: 'string', description: '表单提交的 HTML name 属性。'},
{name: 'value', type: 'string | number | readonly string[]', description: '表单提交的 HTML value 属性。'},
{name: 'form', type: 'string', description: '通过 ID 将按钮与表单元素关联。'},
{name: 'isLoading', type: 'boolean', description: '显示加载旋转器并禁用交互。通过实时区域播报"Loading"。', default: 'false'},
{name: 'isLoading', type: 'boolean', description: '显示加载旋转器并阻止重复触发,同时保持按钮可聚焦(aria-busy + aria-disabled,而非原生 disabled)。通过实时区域播报"Loading"。', default: 'false'},
{
name: 'isDisabled',
type: 'boolean',
Expand All @@ -230,7 +230,7 @@ export const docsZh = {
{
name: 'clickAction',
type: '(e: MouseEvent) => void | Promise<void>',
description: '异步点击处理函数。返回的 Promise 处于 pending 状态时显示加载状态。',
description: '异步点击处理函数。返回的 Promise 处于 pending 状态时显示加载状态。Promise 被拒绝时清除加载状态(用户可重试),并通过 devError 上报;预期内的失败请在 action 内部处理。',
},
],
theming: {
Expand Down Expand Up @@ -281,15 +281,15 @@ export const docsDense = {
displayName: 'HTML name for form submission',
value: 'HTML value for form submission',
form: 'associates button with form element by ID',
isLoading: 'shows spinner+disables interaction; announces via live region',
isLoading: 'shows spinner, guards re-activation, stays focusable (aria-busy/aria-disabled, no native disabled); announces via live region',
icon: 'icon element rendered before label text',
isIconOnly: 'when true, renders square icon-only button; label becomes aria-label',
width: "Width of button. Numbers=pixels, strings=as-is (e.g. '100%' for full-width).",
children: 'optional visible override; label is still required for a11y. Prefer <Button label="Save" /> over using children',
endContent: 'trailing icon/badge after label; ignored when isIconOnly; color inherited',
tooltip: 'tooltip on hover',
onClick: 'standard click handler; fires before clickAction',
clickAction: 'async click handler; shows loading while promise pending',
clickAction: 'async click handler; shows loading while promise pending; rejection clears loading (retry works) + devError report',
isDisabled: 'disables button; uses aria-disabled when tooltip present',
},
};
Loading
Loading