diff --git a/.changeset/button-action-spinner-delay.md b/.changeset/button-action-spinner-delay.md new file mode 100644 index 0000000000000..b094ec3d37ef6 --- /dev/null +++ b/.changeset/button-action-spinner-delay.md @@ -0,0 +1,6 @@ +--- +'@astryxdesign/core': patch +--- + +[feat] Button: add `isInterruptible` to keep the button clickable while a `clickAction` is pending — the spinner and `aria-busy` still show, but the button is not disabled or deduped, so a re-click interrupts the in-flight action. ToggleButton's async toggle now runs through this path, staying interruptible. +@cixzhang diff --git a/.changeset/togglebutton-onpressedchange-event.md b/.changeset/togglebutton-onpressedchange-event.md new file mode 100644 index 0000000000000..a124d264311cd --- /dev/null +++ b/.changeset/togglebutton-onpressedchange-event.md @@ -0,0 +1,13 @@ +--- +'@astryxdesign/core': patch +--- + +[fix] ToggleButton onPressedChange receives the click event for preventDefault opt-out +@cixzhang + +`onPressedChange` now receives the originating click event as a second +argument. Calling `event.preventDefault()` skips `pressedChangeAction`, so a +consumer can handle the toggle entirely in `onPressedChange` without firing the +action — matching how `Switch`'s `onChange` and `Button`'s `onClick` already +gate their action props. Existing `(isPressed) => void` handlers keep working; +the event is an added trailing argument. diff --git a/packages/core/src/Button/Button.doc.mjs b/packages/core/src/Button/Button.doc.mjs index ceb030e51c236..e73382749ca38 100644 --- a/packages/core/src/Button/Button.doc.mjs +++ b/packages/core/src/Button/Button.doc.mjs @@ -78,6 +78,12 @@ export const docs = { description: 'Shows a loading spinner and disables interaction. Announces "Loading" via a live region.', default: 'false', }, + { + name: 'isInterruptible', + type: 'boolean', + description: 'Keep the button clickable while a clickAction is pending: the spinner and aria-busy still show, but the button is not disabled and the action is not deduped, so a re-click lands and interrupts the in-flight action with a fresh one.', + default: 'false', + }, { name: 'isDisabled', type: 'boolean', diff --git a/packages/core/src/Button/Button.test.tsx b/packages/core/src/Button/Button.test.tsx index 6b70636c5b8d7..52829ca8ba56e 100644 --- a/packages/core/src/Button/Button.test.tsx +++ b/packages/core/src/Button/Button.test.tsx @@ -71,6 +71,32 @@ describe('Button', () => { expect(button).toBeDisabled(); }); + it('sets aria-busy synchronously while clickAction is pending', async () => { + // The spinner reveal is visually delayed (CSS animation-delay), but the + // loading DOM state — aria-busy and disabled — must not be delayed. + const user = userEvent.setup(); + let resolveAction: (() => void) | undefined; + const clickAction = vi.fn( + async () => + new Promise(resolve => { + resolveAction = resolve; + }), + ); + render(