diff --git a/.changeset/loading-button-disabled-styles.md b/.changeset/loading-button-disabled-styles.md new file mode 100644 index 00000000000..ce49af2eeea --- /dev/null +++ b/.changeset/loading-button-disabled-styles.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Button: Apply disabled styles while loading so the button looks non-interactive but stays focusable diff --git a/packages/react/src/Button/Button.features.stories.tsx b/packages/react/src/Button/Button.features.stories.tsx index 161029a4a88..5df10db14ee 100644 --- a/packages/react/src/Button/Button.features.stories.tsx +++ b/packages/react/src/Button/Button.features.stories.tsx @@ -192,7 +192,20 @@ export const Medium = () => export const Large = () => -export const Loading = () => +export const Loading = () => ( +
+ + + + +
+) export const LoadingCustomAnnouncement = () => ( + + + , + ) + + const loadingButton = screen.getByRole('button', {name: 'Loading'}) + const disabledButton = screen.getByRole('button', {name: 'Disabled'}) + const enabledButton = screen.getByRole('button', {name: 'Enabled'}) + + expect(loadingButton).toHaveAttribute('aria-disabled', 'true') + expect(loadingButton).toHaveAttribute('data-loading', 'true') + expect(loadingButton).not.toBeDisabled() + expect(visualStyles(loadingButton)).toEqual(visualStyles(disabledButton)) + expect(visualStyles(loadingButton).backgroundColor).not.toBe(visualStyles(enabledButton).backgroundColor) + expect(visualStyles(loadingButton).cursor).toBe('not-allowed') + }) + + it('applies disabled styles when loading is combined with aria-disabled', () => { + render( + <> + + + , + ) + + const loadingButton = screen.getByRole('button', {name: 'Loading'}) + const disabledButton = screen.getByRole('button', {name: 'Disabled'}) + + expect(loadingButton).toHaveAttribute('aria-disabled', 'true') + expect(loadingButton).not.toBeDisabled() + expect(visualStyles(loadingButton)).toEqual(visualStyles(disabledButton)) + }) + + it('does not call onClick while loading', () => { + const onClick = vi.fn() + render( + , + ) + + fireEvent.click(screen.getByRole('button', {name: 'Submit'})) + expect(onClick).not.toHaveBeenCalled() + }) + + it('remains focusable while loading', () => { + render( + , + ) + + const button = screen.getByRole('button', {name: 'Submit'}) + button.focus() + expect(button).toHaveFocus() + }) + + it('still applies disabled styles for aria-disabled without loading', () => { + render( + <> + + + , + ) + + expect(visualStyles(screen.getByRole('button', {name: 'Aria disabled'}))).toEqual( + visualStyles(screen.getByRole('button', {name: 'Disabled'})), + ) + }) + + it('does not let inactive override primary the same way disabled styles do', () => { + render( + <> + + + , + ) + + expect(visualStyles(screen.getByRole('button', {name: 'Inactive'}))).not.toEqual( + visualStyles(screen.getByRole('button', {name: 'Disabled'})), + ) + }) + }) + it('should preserve the accessible button name when the button is in a loading state', () => { const container = render()