Skip to content

Commit 7400549

Browse files
authoredMar 7, 2025··
ActionBar: Add disabled support to ActionBar (#5666)
1 parent 454ff20 commit 7400549

File tree

5 files changed

+137
-14
lines changed

5 files changed

+137
-14
lines changed
 

‎.changeset/sixty-otters-lie.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/react": minor
3+
---
4+
5+
ActionBar: Improves `disabled` state on `ActionBar.IconButton`; includes `disabled` state in overflow menu

‎packages/react/src/ActionBar/ActionBar.docs.json

+6
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@
7575
"type": "string",
7676
"defaultValue": "",
7777
"description": "Use an aria label to describe the functionality of the button. Please refer to [our guidance on alt text](https://primer.style/guides/accessibility/alternative-text-for-images) for tips on writing good alternative text."
78+
},
79+
{
80+
"name": "disabled",
81+
"type": "boolean",
82+
"defaultValue": "",
83+
"description": "Provides a disabled state for the button. The button will remain focusable, and have `aria-disabled` applied."
7884
}
7985
],
8086
"passthrough": {

‎packages/react/src/ActionBar/ActionBar.examples.stories.tsx

+16
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,22 @@ export const SmallActionBar = () => (
4747
</ActionBar>
4848
)
4949

50+
export const WithDisabledItems = () => (
51+
<ActionBar aria-label="Toolbar">
52+
<ActionBar.IconButton icon={BoldIcon} aria-label="Bold"></ActionBar.IconButton>
53+
<ActionBar.IconButton icon={ItalicIcon} aria-label="Italic"></ActionBar.IconButton>
54+
<ActionBar.IconButton icon={CodeIcon} aria-label="Code"></ActionBar.IconButton>
55+
<ActionBar.IconButton icon={LinkIcon} aria-label="Link"></ActionBar.IconButton>
56+
<ActionBar.Divider />
57+
<ActionBar.IconButton disabled icon={FileAddedIcon} aria-label="File Added"></ActionBar.IconButton>
58+
<ActionBar.IconButton disabled icon={SearchIcon} aria-label="Search"></ActionBar.IconButton>
59+
<ActionBar.IconButton disabled icon={QuoteIcon} aria-label="Insert Quote"></ActionBar.IconButton>
60+
<ActionBar.IconButton icon={ListUnorderedIcon} aria-label="Unordered List"></ActionBar.IconButton>
61+
<ActionBar.IconButton icon={ListOrderedIcon} aria-label="Ordered List"></ActionBar.IconButton>
62+
<ActionBar.IconButton icon={TasklistIcon} aria-label="Task List"></ActionBar.IconButton>
63+
</ActionBar>
64+
)
65+
5066
type CommentBoxProps = {'aria-label': string}
5167

5268
export const CommentBox = (props: CommentBoxProps) => {

‎packages/react/src/ActionBar/ActionBar.test.tsx

+70-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react'
22
import {behavesAsComponent} from '../utils/testing'
3-
import {render as HTMLRender} from '@testing-library/react'
3+
import userEvent from '@testing-library/user-event'
4+
import {render as HTMLRender, act} from '@testing-library/react'
45
import axe from 'axe-core'
56

67
import ActionBar from './'
@@ -32,4 +33,72 @@ describe('ActionBar', () => {
3233
const results = await axe.run(container)
3334
expect(results).toHaveNoViolations()
3435
})
36+
37+
it('should not trigger disabled button', () => {
38+
const onClick = jest.fn()
39+
const {getByRole} = HTMLRender(
40+
<ActionBar aria-label="Toolbar">
41+
<ActionBar.IconButton icon={BoldIcon} aria-label="Default" onClick={onClick} disabled></ActionBar.IconButton>
42+
</ActionBar>,
43+
)
44+
45+
const button = getByRole('button')
46+
button.click()
47+
48+
expect(onClick).not.toHaveBeenCalled()
49+
})
50+
51+
it('should trigger non-disabled button', () => {
52+
const onClick = jest.fn()
53+
const {getByRole} = HTMLRender(
54+
<ActionBar aria-label="Toolbar">
55+
<ActionBar.IconButton icon={BoldIcon} aria-label="Default" onClick={onClick}></ActionBar.IconButton>
56+
</ActionBar>,
57+
)
58+
59+
const button = getByRole('button')
60+
button.click()
61+
62+
expect(onClick).toHaveBeenCalled()
63+
})
64+
65+
it('should not trigger disabled button with spacebar or enter', async () => {
66+
const user = userEvent.setup()
67+
const onClick = jest.fn()
68+
const {getByRole} = HTMLRender(
69+
<ActionBar aria-label="Toolbar">
70+
<ActionBar.IconButton icon={BoldIcon} aria-label="Default" onClick={onClick} disabled></ActionBar.IconButton>
71+
</ActionBar>,
72+
)
73+
74+
const button = getByRole('button')
75+
76+
act(() => {
77+
button.focus()
78+
})
79+
80+
await user.keyboard('{Enter}')
81+
82+
expect(onClick).not.toHaveBeenCalled()
83+
})
84+
85+
it('should trigger non-disabled button with spacebar or enter', async () => {
86+
const user = userEvent.setup()
87+
const onClick = jest.fn()
88+
const {getByRole} = HTMLRender(
89+
<ActionBar aria-label="Toolbar">
90+
<ActionBar.IconButton icon={BoldIcon} aria-label="Default" onClick={onClick}></ActionBar.IconButton>
91+
</ActionBar>,
92+
)
93+
94+
const button = getByRole('button')
95+
96+
act(() => {
97+
button.focus()
98+
})
99+
100+
await user.keyboard('{Enter}')
101+
102+
expect(onClick).toHaveBeenCalled()
103+
})
35104
})

‎packages/react/src/ActionBar/ActionBar.tsx

+40-13
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export type ActionBarProps = {
4646
className?: string
4747
} & A11yProps
4848

49-
export type ActionBarIconButtonProps = IconButtonProps
49+
export type ActionBarIconButtonProps = {disabled?: boolean} & IconButtonProps
5050

5151
const MORE_BTN_WIDTH = 86
5252

@@ -215,7 +215,13 @@ export const ActionBar: React.FC<React.PropsWithChildren<ActionBarProps>> = prop
215215
if (menuItem.type === ActionList.Divider) {
216216
return <ActionList.Divider key={index} />
217217
} else {
218-
const {children: menuItemChildren, onClick, icon: Icon, 'aria-label': ariaLabel} = menuItem.props
218+
const {
219+
children: menuItemChildren,
220+
onClick,
221+
icon: Icon,
222+
'aria-label': ariaLabel,
223+
disabled,
224+
} = menuItem.props
219225
return (
220226
<ActionList.Item
221227
key={menuItemChildren}
@@ -224,6 +230,7 @@ export const ActionBar: React.FC<React.PropsWithChildren<ActionBarProps>> = prop
224230
focusOnMoreMenuBtn()
225231
typeof onClick === 'function' && onClick(event)
226232
}}
233+
disabled={disabled}
227234
>
228235
{Icon ? (
229236
<ActionList.LeadingVisual>
@@ -245,17 +252,37 @@ export const ActionBar: React.FC<React.PropsWithChildren<ActionBarProps>> = prop
245252
)
246253
}
247254

248-
export const ActionBarIconButton = forwardRef((props: ActionBarIconButtonProps, forwardedRef) => {
249-
const backupRef = useRef<HTMLElement>(null)
250-
const ref = (forwardedRef ?? backupRef) as RefObject<HTMLAnchorElement>
251-
const {size, setChildrenWidth} = React.useContext(ActionBarContext)
252-
useIsomorphicLayoutEffect(() => {
253-
const text = props['aria-label'] ? props['aria-label'] : ''
254-
const domRect = (ref as MutableRefObject<HTMLElement>).current.getBoundingClientRect()
255-
setChildrenWidth({text, width: domRect.width})
256-
}, [ref, setChildrenWidth])
257-
return <IconButton ref={ref} size={size} {...props} variant="invisible" />
258-
})
255+
export const ActionBarIconButton = forwardRef(
256+
({disabled, onClick, ...props}: ActionBarIconButtonProps, forwardedRef) => {
257+
const backupRef = useRef<HTMLElement>(null)
258+
const ref = (forwardedRef ?? backupRef) as RefObject<HTMLAnchorElement>
259+
const {size, setChildrenWidth} = React.useContext(ActionBarContext)
260+
useIsomorphicLayoutEffect(() => {
261+
const text = props['aria-label'] ? props['aria-label'] : ''
262+
const domRect = (ref as MutableRefObject<HTMLElement>).current.getBoundingClientRect()
263+
setChildrenWidth({text, width: domRect.width})
264+
}, [ref, setChildrenWidth])
265+
266+
const clickHandler = useCallback(
267+
(event: React.MouseEvent<HTMLButtonElement>) => {
268+
if (disabled) return
269+
onClick?.(event)
270+
},
271+
[disabled, onClick],
272+
)
273+
274+
return (
275+
<IconButton
276+
aria-disabled={disabled}
277+
ref={ref}
278+
size={size}
279+
onClick={clickHandler}
280+
{...props}
281+
variant="invisible"
282+
/>
283+
)
284+
},
285+
)
259286

260287
export const VerticalDivider = () => {
261288
const ref = useRef<HTMLDivElement>(null)

0 commit comments

Comments
 (0)
Please sign in to comment.