diff --git a/packages/react/src/ActionBar/ActionBar.test.tsx b/packages/react/src/ActionBar/ActionBar.test.tsx
index 98fd2d4b777..f5537efde2e 100644
--- a/packages/react/src/ActionBar/ActionBar.test.tsx
+++ b/packages/react/src/ActionBar/ActionBar.test.tsx
@@ -82,6 +82,72 @@ describe('ActionBar', () => {
})
})
+describe('ActionBar.Button', () => {
+ afterEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('renders a text button with its children as the accessible name', () => {
+ render(
+
+ Save
+ ,
+ )
+
+ expect(screen.getByRole('button', {name: 'Save'})).toBeInTheDocument()
+ })
+
+ it('should trigger non-disabled button', () => {
+ const onClick = vi.fn()
+ render(
+
+ Save
+ ,
+ )
+
+ screen.getByRole('button', {name: 'Save'}).click()
+
+ expect(onClick).toHaveBeenCalled()
+ })
+
+ it('should not trigger disabled button', () => {
+ const onClick = vi.fn()
+ render(
+
+
+ Save
+
+ ,
+ )
+
+ screen.getByRole('button', {name: 'Save'}).click()
+
+ expect(onClick).not.toHaveBeenCalled()
+ })
+
+ it('should not trigger disabled button with spacebar or enter', async () => {
+ const user = userEvent.setup()
+ const onClick = vi.fn()
+ render(
+
+
+ Save
+
+ ,
+ )
+
+ const button = screen.getByRole('button', {name: 'Save'})
+
+ act(() => {
+ button.focus()
+ })
+
+ await user.keyboard('{Enter}')
+
+ expect(onClick).not.toHaveBeenCalled()
+ })
+})
+
describe('ActionBar Registry System', () => {
it('should preserve order with deep nesting', () => {
render(
@@ -442,6 +508,17 @@ describe('ActionBar data-component attributes', () => {
expect(iconButton).toBeInTheDocument()
})
+ it('renders ActionBar.Button with a text label', () => {
+ render(
+
+ Save
+ ,
+ )
+
+ const button = screen.getByRole('button', {name: 'Save'})
+ expect(button).toBeInTheDocument()
+ })
+
it('renders ActionBar.VerticalDivider with data-component attribute', () => {
const {container} = render(
diff --git a/packages/react/src/ActionBar/ActionBar.tsx b/packages/react/src/ActionBar/ActionBar.tsx
index 8fd85484606..299ff7ffb6c 100644
--- a/packages/react/src/ActionBar/ActionBar.tsx
+++ b/packages/react/src/ActionBar/ActionBar.tsx
@@ -3,8 +3,8 @@ import React, {useState, useCallback, useRef, forwardRef, useMemo, useSyncExtern
import {KebabHorizontalIcon} from '@primer/octicons-react'
import {ActionList, type ActionListItemProps} from '../ActionList'
-import type {IconButtonProps} from '../Button'
-import {IconButton} from '../Button'
+import type {ButtonProps, IconButtonProps} from '../Button'
+import {Button, IconButton} from '../Button'
import {ActionMenu} from '../ActionMenu'
import {useFocusZone, FocusKeys} from '../hooks/useFocusZone'
import styles from './ActionBar.module.css'
@@ -15,9 +15,9 @@ import {createDescendantRegistry} from '../utils/descendant-registry'
type ChildProps =
| {
type: 'action'
- label: string
+ label: React.ReactNode
disabled: boolean
- icon: ActionBarIconButtonProps['icon']
+ icon?: ActionBarIconButtonProps['icon']
onClick: MouseEventHandler
}
| {type: 'divider' | 'group'}
@@ -84,6 +84,8 @@ export type ActionBarProps = {
export type ActionBarIconButtonProps = {disabled?: boolean} & IconButtonProps
+export type ActionBarButtonProps = {disabled?: boolean} & ButtonProps
+
export type ActionBarMenuItemProps =
| ({
/**
@@ -260,15 +262,17 @@ export const ActionBar: React.FC> = ({
const {onClick, icon: Icon, label, disabled} = menuItem
return (
{
typeof onClick === 'function' && onClick(event as React.MouseEvent)
}}
disabled={disabled}
>
-
-
-
+ {Icon ? (
+
+
+
+ ) : null}
{label}
)
@@ -390,6 +394,51 @@ export const ActionBarIconButton = forwardRef(
},
)
+export const ActionBarButton = forwardRef(({disabled, onClick, ...props}: ActionBarButtonProps, forwardedRef) => {
+ const ref = useRef(null)
+ const mergedRef = useMergedRefs(forwardedRef, ref)
+
+ const {size} = React.useContext(ActionBarContext)
+
+ const {children, leadingVisual} = props
+
+ const {dataOverflowingAttr} = useActionBarItem(
+ ref,
+ useMemo(
+ (): ChildProps => ({
+ type: 'action',
+ label: children,
+ // Only forward the leading visual to the overflow menu when it is a component
+ // that can be rendered as an icon (e.g. an octicon), matching ActionBar.IconButton.
+ icon: typeof leadingVisual === 'function' ? (leadingVisual as ActionBarIconButtonProps['icon']) : undefined,
+ disabled: !!disabled,
+ onClick: onClick as MouseEventHandler,
+ }),
+ [children, leadingVisual, disabled, onClick],
+ ),
+ )
+
+ const clickHandler = useCallback(
+ (event: React.MouseEvent) => {
+ if (disabled) return
+ onClick?.(event)
+ },
+ [disabled, onClick],
+ )
+
+ return (
+
+ )
+})
+
const ActionBarGroupContext = React.createContext<{
isOverflowing: boolean
} | null>(null)
diff --git a/packages/react/src/ActionBar/index.ts b/packages/react/src/ActionBar/index.ts
index 9a7e78b503b..ef72a7a1e1d 100644
--- a/packages/react/src/ActionBar/index.ts
+++ b/packages/react/src/ActionBar/index.ts
@@ -1,8 +1,16 @@
-import {ActionBar as Bar, ActionBarIconButton, VerticalDivider, ActionBarGroup, ActionBarMenu} from './ActionBar'
-export type {ActionBarProps, ActionBarMenuProps, ActionBarMenuItemProps} from './ActionBar'
+import {
+ ActionBar as Bar,
+ ActionBarIconButton,
+ ActionBarButton,
+ VerticalDivider,
+ ActionBarGroup,
+ ActionBarMenu,
+} from './ActionBar'
+export type {ActionBarProps, ActionBarButtonProps, ActionBarMenuProps, ActionBarMenuItemProps} from './ActionBar'
const ActionBar = Object.assign(Bar, {
IconButton: ActionBarIconButton,
+ Button: ActionBarButton,
Divider: VerticalDivider,
Group: ActionBarGroup,
Menu: ActionBarMenu,
diff --git a/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap
index 41075447917..412847b840c 100644
--- a/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap
+++ b/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap
@@ -281,6 +281,7 @@ exports[`@primer/react/deprecated > should not update exports without a semver c
exports[`@primer/react/experimental > should not update exports without a semver change 1`] = `
[
"ActionBar",
+ "type ActionBarButtonProps",
"type ActionBarMenuItemProps",
"type ActionBarMenuProps",
"type ActionBarProps",