diff --git a/.changeset/mean-terms-bathe.md b/.changeset/mean-terms-bathe.md
new file mode 100644
index 00000000000..9b7ddae6349
--- /dev/null
+++ b/.changeset/mean-terms-bathe.md
@@ -0,0 +1,5 @@
+---
+"@primer/react": patch
+---
+
+ActionBar: Add a few fixes and relevant tests
diff --git a/.changeset/popular-jokes-kiss.md b/.changeset/popular-jokes-kiss.md
new file mode 100644
index 00000000000..cb5f5102995
--- /dev/null
+++ b/.changeset/popular-jokes-kiss.md
@@ -0,0 +1,5 @@
+---
+'@primer/react': minor
+---
+
+`Dialog` and `ConfirmationDialog` can now be closed by clicking on the backdrop surrounding the dialog. This will cause `onClose` to be called with a new `'backdrop'` gesture.
diff --git a/.changeset/quiet-lamps-kiss.md b/.changeset/quiet-lamps-kiss.md
new file mode 100644
index 00000000000..0b5bd73d05f
--- /dev/null
+++ b/.changeset/quiet-lamps-kiss.md
@@ -0,0 +1,5 @@
+---
+"@primer/react": patch
+---
+
+TreeView: Fix toggling subtree via Space key (in addition to Enter key)
diff --git a/.changeset/tiny-ghosts-notice.md b/.changeset/tiny-ghosts-notice.md
new file mode 100644
index 00000000000..bc390eb0811
--- /dev/null
+++ b/.changeset/tiny-ghosts-notice.md
@@ -0,0 +1,5 @@
+---
+'@primer/react': minor
+---
+
+Exports createComponent
diff --git a/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-dark-colorblind-linux.png b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-dark-colorblind-linux.png
new file mode 100644
index 00000000000..4a72d6fa66f
Binary files /dev/null and b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-dark-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-dark-dimmed-linux.png b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-dark-dimmed-linux.png
new file mode 100644
index 00000000000..f2773d1d698
Binary files /dev/null and b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-dark-dimmed-linux.png differ
diff --git a/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-dark-high-contrast-linux.png b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-dark-high-contrast-linux.png
new file mode 100644
index 00000000000..ce5416737d1
Binary files /dev/null and b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-dark-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-dark-linux.png b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-dark-linux.png
new file mode 100644
index 00000000000..4a72d6fa66f
Binary files /dev/null and b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-dark-linux.png differ
diff --git a/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-dark-tritanopia-linux.png b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-dark-tritanopia-linux.png
new file mode 100644
index 00000000000..4a72d6fa66f
Binary files /dev/null and b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-dark-tritanopia-linux.png differ
diff --git a/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-light-colorblind-linux.png b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-light-colorblind-linux.png
new file mode 100644
index 00000000000..ee281093c96
Binary files /dev/null and b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-light-colorblind-linux.png differ
diff --git a/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-light-high-contrast-linux.png b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-light-high-contrast-linux.png
new file mode 100644
index 00000000000..b5ea113e4b1
Binary files /dev/null and b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-light-high-contrast-linux.png differ
diff --git a/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-light-linux.png b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-light-linux.png
new file mode 100644
index 00000000000..ee281093c96
Binary files /dev/null and b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-light-linux.png differ
diff --git a/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-light-tritanopia-linux.png b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-light-tritanopia-linux.png
new file mode 100644
index 00000000000..ee281093c96
Binary files /dev/null and b/.playwright/snapshots/components/drafts/ActionBar.test.ts-snapshots/drafts-ActionBar-CommentBox-light-tritanopia-linux.png differ
diff --git a/docs/content/drafts/Dialog.mdx b/docs/content/drafts/Dialog.mdx
index 791d871f6e8..baccf89d4a8 100644
--- a/docs/content/drafts/Dialog.mdx
+++ b/docs/content/drafts/Dialog.mdx
@@ -157,13 +157,13 @@ render()
### ConfirmationDialogProps
-| Prop name | Type | Default | Description |
-| :------------------- | :-------------------------------------------------------------------- | :--------- | :---------------------------------------------------------------------------------------------------------------------------- |
-| title | `React.ReactNode` | | Required. Sets the title of the dialog, which by default is also used as the `aria-labelledby` attribute. |
-| onClose | `(gesture: 'confirm' │ 'cancel' │ 'close-button' │ 'escape') => void` | | Required. This callback is invoked when a gesture to close the dialog is performed. The first argument indicates the gesture. |
-| cancelButtonContent | `React.ReactNode` | `"Cancel"` | The content to use for the cancel button. |
-| confirmButtonContent | `React.ReactNode` | `"OK"` | The content to use for the confirm button. |
-| confirmButtonType | `"normal" │ "primary" │ "danger"` | `Button` | The type of button to use for the confirm button. |
+| Prop name | Type | Default | Description |
+| :------------------- | :----------------------------------------------- | :----------------------------- | :-------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
+| title | `React.ReactNode` | | Required. Sets the title of the dialog, which by default is also used as the `aria-labelledby` attribute. |
+| onClose | `(gesture: 'confirm' │ 'cancel' │ 'close-button' | 'backdrop │ 'escape') => void` | | Required. This callback is invoked when a gesture to close the dialog is performed. The first argument indicates the gesture. |
+| cancelButtonContent | `React.ReactNode` | `"Cancel"` | The content to use for the cancel button. |
+| confirmButtonContent | `React.ReactNode` | `"OK"` | The content to use for the confirm button. |
+| confirmButtonType | `"normal" │ "primary" │ "danger"` | `Button` | The type of button to use for the confirm button. |
### ConfirmOptions
diff --git a/e2e/components/drafts/ActionBar.test.ts b/e2e/components/drafts/ActionBar.test.ts
new file mode 100644
index 00000000000..f7c74f0f750
--- /dev/null
+++ b/e2e/components/drafts/ActionBar.test.ts
@@ -0,0 +1,54 @@
+import {test, expect} from '@playwright/test'
+import {visit} from '../../test-helpers/storybook'
+import {themes} from '../../test-helpers/themes'
+import {viewports} from '../../test-helpers/viewports'
+
+test.describe('ActionBar', () => {
+ test.describe('Default state', () => {
+ for (const theme of themes) {
+ test.describe(theme, () => {
+ test('default @vrt', async ({page}) => {
+ await visit(page, {
+ id: 'drafts-components-actionbar--comment-box',
+ globals: {
+ colorScheme: theme,
+ },
+ })
+ expect(await page.screenshot()).toMatchSnapshot(`drafts.ActionBar.CommentBox.${theme}.png`)
+ })
+
+ test('axe @aat', async ({page}) => {
+ await visit(page, {
+ id: 'drafts-components-actionbar--comment-box',
+ globals: {
+ colorScheme: theme,
+ },
+ })
+ await expect(page).toHaveNoViolations()
+ })
+ })
+ }
+ })
+
+ test.describe('ActionBar Interactions', () => {
+ for (const theme of themes) {
+ test.describe(theme, () => {
+ test('Overflow interaction @vrt', async ({page}) => {
+ await visit(page, {
+ id: 'drafts-components-actionbar--comment-box',
+ globals: {
+ colorScheme: theme,
+ },
+ })
+ const toolbarButtonSelector = `button[data-component="IconButton"]`
+ await expect(page.locator(toolbarButtonSelector)).toHaveCount(10)
+ await page.setViewportSize({width: viewports['primer.breakpoint.xs'], height: 768})
+ await expect(page.locator(toolbarButtonSelector)).toHaveCount(6)
+ const moreButtonSelector = `button[aria-label="More Comment box toolbar items"]`
+ await page.locator(moreButtonSelector).click()
+ await expect(page.locator('ul[role="menu"]>li')).toHaveCount(5)
+ })
+ })
+ }
+ })
+})
diff --git a/packages/react/src/ConfirmationDialog/ConfirmationDialog.tsx b/packages/react/src/ConfirmationDialog/ConfirmationDialog.tsx
index 73311e348a5..9a5a8a19df7 100644
--- a/packages/react/src/ConfirmationDialog/ConfirmationDialog.tsx
+++ b/packages/react/src/ConfirmationDialog/ConfirmationDialog.tsx
@@ -19,7 +19,7 @@ export interface ConfirmationDialogProps {
* Required. This callback is invoked when a gesture to close the dialog
* is performed. The first argument indicates the gesture.
*/
- onClose: (gesture: 'confirm' | 'close-button' | 'cancel' | 'escape') => void
+ onClose: (gesture: 'confirm' | 'close-button' | 'backdrop' | 'cancel' | 'escape') => void
/**
* Required. The title of the ConfirmationDialog. This is usually a brief
diff --git a/packages/react/src/Dialog/Dialog.test.tsx b/packages/react/src/Dialog/Dialog.test.tsx
index 3c8088542e2..da4e38cf698 100644
--- a/packages/react/src/Dialog/Dialog.test.tsx
+++ b/packages/react/src/Dialog/Dialog.test.tsx
@@ -67,7 +67,22 @@ describe('Dialog', () => {
await user.click(getByLabelText('Close'))
- expect(onClose).toHaveBeenCalled()
+ expect(onClose).toHaveBeenCalledWith('close-button')
+ expect(onClose).toHaveBeenCalledTimes(1) // Ensure it's not called with a backdrop gesture as well
+ })
+
+ it('calls `onClose` when clicking the backdrop', async () => {
+ const user = userEvent.setup()
+ const onClose = jest.fn()
+ const {getByRole} = render()
+
+ expect(onClose).not.toHaveBeenCalled()
+
+ const dialog = getByRole('dialog')
+ const backdrop = dialog.parentElement!
+ await user.click(backdrop)
+
+ expect(onClose).toHaveBeenCalledWith('backdrop')
})
it('calls `onClose` when keying "Escape"', async () => {
@@ -80,7 +95,7 @@ describe('Dialog', () => {
await user.keyboard('{Escape}')
- expect(onClose).toHaveBeenCalled()
+ expect(onClose).toHaveBeenCalledWith('escape')
})
it('changes the
style for `overflow` if it is not set to "hidden"', () => {
diff --git a/packages/react/src/Dialog/Dialog.tsx b/packages/react/src/Dialog/Dialog.tsx
index 81692f9ae45..6744210c1d3 100644
--- a/packages/react/src/Dialog/Dialog.tsx
+++ b/packages/react/src/Dialog/Dialog.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback, useEffect, useRef, useState} from 'react'
+import React, {useCallback, useEffect, useRef, useState, type SyntheticEvent} from 'react'
import styled from 'styled-components'
import type {ButtonProps} from '../Button'
import {Button} from '../Button'
@@ -98,11 +98,11 @@ export interface DialogProps extends SxProp {
/**
* This method is invoked when a gesture to close the dialog is used (either
- * an Escape key press or clicking the "X" in the top-right corner). The
+ * an Escape key press, clicking the backdrop, or clicking the "X" in the top-right corner). The
* gesture argument indicates the gesture that was used to close the dialog
- * (either 'close-button' or 'escape').
+ * ('close-button', 'backdrop', or 'escape').
*/
- onClose: (gesture: 'close-button' | 'escape') => void
+ onClose: (gesture: 'close-button' | 'backdrop' | 'escape') => void
/**
* Default: "dialog". The ARIA role to assign to this dialog.
@@ -414,6 +414,14 @@ const _Dialog = React.forwardRef {
+ if (e.target === e.currentTarget) {
+ onClose('backdrop')
+ }
+ },
+ [onClose],
+ )
const dialogRef = useRef(null)
useRefObjectAsForwardedRef(forwardedRef, dialogRef)
@@ -465,7 +473,7 @@ const _Dialog = React.forwardRef
-
+
{
})
})
+ describe('Space', () => {
+ it('calls onSelect function if provided and checks if the item has been selected', () => {
+ const onSelect = jest.fn()
+ const {getByRole} = renderWithTheme(
+
+
+ Parent 1
+
+
+ Child 1
+
+
+
+
+ Parent 2
+
+
+ Child2
+
+
+
+
+ Parent 3
+
+
+ Child 3
+
+
+
+ ,
+ )
+ const itemChild = getByRole('treeitem', {name: 'Child2'})
+
+ act(() => {
+ // Focus first item
+ itemChild.focus()
+ })
+
+ // Press Enter
+ fireEvent.keyDown(document.activeElement || document.body, {key: ' '})
+
+ // onSelect should have been called
+ expect(onSelect).toHaveBeenCalledTimes(1)
+
+ onSelect.mockClear()
+
+ // Press middle click
+ fireEvent.click(document.activeElement?.firstChild || document.body, {button: 1})
+
+ // onSelect should have been called
+ expect(onSelect).toHaveBeenCalledTimes(1)
+ })
+
+ it('toggles expanded state if no onSelect function is provided', () => {
+ const {getByRole, queryByRole} = renderWithTheme(
+
+
+ Parent
+
+ Child 1
+ Child 2
+
+
+ ,
+ )
+
+ const parent = getByRole('treeitem', {name: 'Parent'})
+
+ act(() => {
+ // Focus first item
+ parent.focus()
+ })
+
+ // aria-expanded should be false
+ expect(parent).toHaveAttribute('aria-expanded', 'false')
+
+ // Press Enter
+ fireEvent.keyDown(document.activeElement || document.body, {key: 'Enter'})
+
+ // aria-expanded should now be true
+ expect(parent).toHaveAttribute('aria-expanded', 'true')
+
+ // Subtree should be visible
+ expect(queryByRole('group')).toBeVisible()
+
+ // Press Enter
+ fireEvent.keyDown(document.activeElement || document.body, {key: 'Enter'})
+
+ // aria-expanded should now be false
+ expect(parent).toHaveAttribute('aria-expanded', 'false')
+
+ // Subtree should no longer be visible
+ expect(queryByRole('group')).not.toBeInTheDocument()
+ })
+ })
+
describe('Typeahead', () => {
it('moves focus to the next item that matches the typed character', () => {
const {getByRole} = renderWithTheme(
diff --git a/packages/react/src/TreeView/TreeView.tsx b/packages/react/src/TreeView/TreeView.tsx
index 234a8115da3..212bb9bf49c 100644
--- a/packages/react/src/TreeView/TreeView.tsx
+++ b/packages/react/src/TreeView/TreeView.tsx
@@ -396,7 +396,7 @@ const Item = React.forwardRef(
(event: React.KeyboardEvent) => {
switch (event.key) {
case 'Enter':
- case 'Space':
+ case ' ':
if (onSelect) {
onSelect(event)
} else {
diff --git a/packages/react/src/TreeView/useTypeahead.ts b/packages/react/src/TreeView/useTypeahead.ts
index fe430c597a3..cfd5f35f6d4 100644
--- a/packages/react/src/TreeView/useTypeahead.ts
+++ b/packages/react/src/TreeView/useTypeahead.ts
@@ -63,7 +63,7 @@ export function useTypeahead({containerRef, onFocusChange}: TypeaheadOptions) {
function onKeyDown(event: KeyboardEvent) {
// Ignore key presses that don't produce a character value
- if (!event.key || event.key.length > 1) return
+ if (!event.key || event.key.length > 1 || event.key === ' ') return
// Ignore key presses that occur with a modifier
if (event.ctrlKey || event.altKey || event.metaKey) return
diff --git a/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap
index c29dc006f94..4dbaf3f35d0 100644
--- a/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap
+++ b/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap
@@ -58,6 +58,7 @@ exports[`@primer/react should not update exports without a semver change 1`] = `
"type ConfirmationDialogProps",
"CounterLabel",
"type CounterLabelProps",
+ "createComponent",
"Details",
"type DetailsProps",
"Dialog",
diff --git a/packages/react/src/drafts/ActionBar/ActionBar.stories.tsx b/packages/react/src/drafts/ActionBar/ActionBar.stories.tsx
index 27d5ce0173e..f2dd0563a0a 100644
--- a/packages/react/src/drafts/ActionBar/ActionBar.stories.tsx
+++ b/packages/react/src/drafts/ActionBar/ActionBar.stories.tsx
@@ -68,7 +68,7 @@ export const CommentBox = (props: CommentBoxProps) => {
const [value, setValue] = React.useState('')
const [isOpen, setIsOpen] = React.useState(false)
const buttonRef = React.useRef(null)
- const toolBarLabel = `${ariaLabel} toolbar`
+ const toolBarLabel = `${ariaLabel ? ariaLabel : 'Comment box'} toolbar`
return (
).current.getBoundingClientRect()
setChildrenWidth({text, width: domRect.width})
}, [ref, setChildrenWidth])
- return
+ return
})
const sizeToHeight = {
diff --git a/packages/react/src/drafts/TabPanels/TabPanels.tsx b/packages/react/src/drafts/TabPanels/TabPanels.tsx
index 85bd5de263b..ba73e72783b 100644
--- a/packages/react/src/drafts/TabPanels/TabPanels.tsx
+++ b/packages/react/src/drafts/TabPanels/TabPanels.tsx
@@ -4,7 +4,7 @@ import React from 'react'
import styled from 'styled-components'
import {get} from '../../constants'
import {TabContainerElement} from '@github/tab-container-element'
-import {createComponent} from '../../utils/custom-element'
+import {createComponent} from '../../utils/create-component'
import sx, {type SxProp} from '../../sx'
import type {ComponentProps} from '../../utils/types'
import getGlobalFocusStyles from '../../internal/utils/getGlobalFocusStyles'
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 16c9d5c7e86..e3148feb8f4 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -45,6 +45,9 @@ export {useRefObjectAsForwardedRef} from './hooks/useRefObjectAsForwardedRef'
export {useResizeObserver} from './hooks/useResizeObserver'
export {useResponsiveValue} from './hooks/useResponsiveValue'
+// Utils
+export {createComponent} from './utils/create-component'
+
// Components
export {default as Radio} from './Radio'
export type {RadioProps} from './Radio'
diff --git a/packages/react/src/utils/custom-element.ts b/packages/react/src/utils/create-component.ts
similarity index 100%
rename from packages/react/src/utils/custom-element.ts
rename to packages/react/src/utils/create-component.ts