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(Pay attention to me) + + 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