diff --git a/.changeset/tender-parrots-refuse.md b/.changeset/tender-parrots-refuse.md new file mode 100644 index 00000000000..008900b4bbc --- /dev/null +++ b/.changeset/tender-parrots-refuse.md @@ -0,0 +1,5 @@ +--- +"@primer/react": patch +--- + +Add TrailingAction support to NavList diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-dark-colorblind-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-dark-colorblind-linux.png new file mode 100644 index 00000000000..b77258c9709 Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-dark-dimmed-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-dark-dimmed-linux.png new file mode 100644 index 00000000000..a1e46bf7491 Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-dark-high-contrast-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-dark-high-contrast-linux.png new file mode 100644 index 00000000000..aea34cdbce5 Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-dark-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-dark-linux.png new file mode 100644 index 00000000000..b77258c9709 Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-dark-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-dark-tritanopia-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-dark-tritanopia-linux.png new file mode 100644 index 00000000000..b77258c9709 Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-light-colorblind-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-light-colorblind-linux.png new file mode 100644 index 00000000000..bf8ed147561 Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-light-high-contrast-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-light-high-contrast-linux.png new file mode 100644 index 00000000000..8d8b96a6ec1 Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-light-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-light-linux.png new file mode 100644 index 00000000000..bf8ed147561 Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-light-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-light-tritanopia-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-light-tritanopia-linux.png new file mode 100644 index 00000000000..bf8ed147561 Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-Bad-Example-of-SubNav-and-TrailingAction-light-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-dark-colorblind-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-dark-colorblind-linux.png new file mode 100644 index 00000000000..add3ee54197 Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-dark-dimmed-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-dark-dimmed-linux.png new file mode 100644 index 00000000000..38c654f0313 Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-dark-high-contrast-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-dark-high-contrast-linux.png new file mode 100644 index 00000000000..faff6487adb Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-dark-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-dark-linux.png new file mode 100644 index 00000000000..add3ee54197 Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-dark-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-dark-tritanopia-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-dark-tritanopia-linux.png new file mode 100644 index 00000000000..add3ee54197 Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-light-colorblind-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-light-colorblind-linux.png new file mode 100644 index 00000000000..8c80c2fb60a Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-light-high-contrast-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-light-high-contrast-linux.png new file mode 100644 index 00000000000..a3f72dace82 Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-light-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-light-linux.png new file mode 100644 index 00000000000..8c80c2fb60a Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-light-linux.png differ diff --git a/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-light-tritanopia-linux.png b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-light-tritanopia-linux.png new file mode 100644 index 00000000000..8c80c2fb60a Binary files /dev/null and b/.playwright/snapshots/components/NavList.test.ts-snapshots/NavList-With-TrailingAction-light-tritanopia-linux.png differ diff --git a/e2e/components/NavList.test.ts b/e2e/components/NavList.test.ts new file mode 100644 index 00000000000..09b083aa23e --- /dev/null +++ b/e2e/components/NavList.test.ts @@ -0,0 +1,63 @@ +import {test, expect} from '@playwright/test' +import {visit} from '../test-helpers/storybook' +import {themes} from '../test-helpers/themes' + +test.describe('NavList', () => { + test.describe('With TrailingAction', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-navlist--with-trailing-action', + globals: { + colorScheme: theme, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`NavList.With TrailingAction.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-navlist--with-trailing-action', + globals: { + colorScheme: theme, + }, + }) + await expect(page).toHaveNoViolations() + }) + }) + } + }) + + test.describe('With Bad Example of SubNav and TrailingAction', () => { + for (const theme of themes) { + test.describe(theme, () => { + test('default @vrt', async ({page}) => { + await visit(page, { + id: 'components-navlist--with-bad-example-of-sub-nav-and-trailing-action', + globals: { + colorScheme: theme, + }, + }) + + // Default state + expect(await page.screenshot()).toMatchSnapshot( + `NavList.With Bad Example of SubNav and TrailingAction.${theme}.png`, + ) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: 'components-navlist--with-bad-example-of-sub-nav-and-trailing-action', + globals: { + colorScheme: theme, + }, + }) + await expect(page).toHaveNoViolations() + }) + }) + } + }) +}) diff --git a/packages/react/src/ActionList/Item.tsx b/packages/react/src/ActionList/Item.tsx index 0cf06a981c1..cd95ff830c9 100644 --- a/packages/react/src/ActionList/Item.tsx +++ b/packages/react/src/ActionList/Item.tsx @@ -127,9 +127,10 @@ export const Item = React.forwardRef( } const itemRole = role || inferredItemRole + const menuContext = container === 'ActionMenu' || container === 'SelectPanel' if (slots.trailingAction) { - invariant(!container, `ActionList.TrailingAction can not be used within a ${container}.`) + invariant(!menuContext, `ActionList.TrailingAction can not be used within a ${container}.`) } /** Infer the proper selection attribute based on the item's role */ @@ -455,7 +456,7 @@ export const Item = React.forwardRef( {slots.blockDescription} - {!inactive && Boolean(slots.trailingAction) && !container && slots.trailingAction} + {!inactive && !menuContext && Boolean(slots.trailingAction) && slots.trailingAction} ) diff --git a/packages/react/src/ActionList/index.ts b/packages/react/src/ActionList/index.ts index ad2d03c509d..e3ca7e4e333 100644 --- a/packages/react/src/ActionList/index.ts +++ b/packages/react/src/ActionList/index.ts @@ -16,6 +16,7 @@ export type {ActionListDividerProps} from './Divider' export type {ActionListDescriptionProps} from './Description' export type {ActionListLeadingVisualProps, ActionListTrailingVisualProps} from './Visuals' export type {ActionListHeadingProps} from './Heading' +export type {ActionListTrailingActionProps} from './TrailingAction' /** * Collection of list-related components. diff --git a/packages/react/src/NavList/NavList.docs.json b/packages/react/src/NavList/NavList.docs.json index 98603aee482..efdff7a4d9d 100644 --- a/packages/react/src/NavList/NavList.docs.json +++ b/packages/react/src/NavList/NavList.docs.json @@ -136,6 +136,37 @@ "type": "React.RefObject" } ] + }, + { + "name": "NavList.TrailingAction", + "props": [ + { + "name": "as", + "type": "a | button", + "defaultValue": "button", + "required": false, + "description": "HTML element to render as." + }, + { + "name": "label", + "type": "string", + "defaultValue": "", + "required": true, + "description": "Acccessible name for the control." + }, + { + "name": "icon", + "type": "string", + "defaultValue": "", + "required": true, + "description": "Octicon to pass into IconButton. When this is not set, TrailingAction renders as a `Button` instead of an `IconButton`." + }, + { + "name": "href", + "type": "string", + "description": "href when the TrailingAction is rendered as a link." + } + ] } ] } diff --git a/packages/react/src/NavList/NavList.stories.tsx b/packages/react/src/NavList/NavList.stories.tsx index 400cf8ae3fa..59c018a4ba0 100644 --- a/packages/react/src/NavList/NavList.stories.tsx +++ b/packages/react/src/NavList/NavList.stories.tsx @@ -2,6 +2,7 @@ import type {Meta, StoryFn} from '@storybook/react' import React from 'react' import {PageLayout} from '../PageLayout' import {NavList} from './NavList' +import {ArrowRightIcon, ArrowLeftIcon, BookIcon, FileDirectoryIcon} from '@primer/octicons-react' const meta: Meta = { title: 'Components/NavList', @@ -246,4 +247,91 @@ export const WithGroup = () => ( ) +export const WithTrailingAction = () => { + return ( + + + + + + + + Item 1 + + + + Item 2 + + + + + + ) +} + +export const WithTrailingActionInSubItem = () => { + return ( + + + + + + + + Item 1 + + + + Item 2 + + + + Item 3 + + + Sub item 1 + + + + + + + + ) +} + +export const WithBadExampleOfSubNavAndTrailingAction = () => { + return ( + + + + + + + + Item 1 + + + + Item 2 + + + + Item 3 + + + + Sub item 1 + + + + + + + + ) +} + +WithBadExampleOfSubNavAndTrailingAction.storyName = 'With SubNav and Trailing Action (Bad example, do not copy)' + export default meta diff --git a/packages/react/src/NavList/NavList.test.tsx b/packages/react/src/NavList/NavList.test.tsx index e69aed47ba6..ffcca312d8a 100644 --- a/packages/react/src/NavList/NavList.test.tsx +++ b/packages/react/src/NavList/NavList.test.tsx @@ -2,6 +2,7 @@ import {render, fireEvent} from '@testing-library/react' import React from 'react' import {ThemeProvider, SSRProvider} from '..' import {NavList} from './NavList' +import {FeatureFlags} from '../FeatureFlags' type ReactRouterLikeLinkProps = {to: string; children: React.ReactNode} @@ -65,6 +66,20 @@ describe('NavList', () => { ) expect(container).toMatchSnapshot() }) + + it('supports TrailingAction', async () => { + const {getByRole} = render( + + + Item 1 + + + , + ) + + const trailingAction = getByRole('button', {name: 'Some trailing action'}) + expect(trailingAction).toBeInTheDocument() + }) }) describe('NavList.Item', () => { @@ -334,4 +349,43 @@ describe('NavList.Item with NavList.SubNav', () => { const currentLink = queryByRole('link', {name: 'Current'}) expect(currentLink).toBeVisible() }) + + describe('TrailingAction', () => { + function NavListWithSubNavAndTrailingAction() { + return ( + + + + Item + + + + Sub Item 1 + + + Sub Item 2 + + + + + ) + } + + it('does not render TrailingAction alongside SubNav', async () => { + const {queryByRole} = render() + + const trailingAction = queryByRole('button', {name: 'This should not be rendered'}) + expect(trailingAction).toBeNull() + }) + + it('supports TrailingAction within an Item inside SubNav', async () => { + const {getByRole, queryByRole} = render() + + const itemWithSubNav = getByRole('button', {name: 'Item'}) + fireEvent.click(itemWithSubNav) + + expect(queryByRole('link', {name: 'Sub Item 1'})).toBeVisible() + expect(queryByRole('button', {name: 'Trailing Action for Sub Item 1'})).toBeVisible() + }) + }) }) diff --git a/packages/react/src/NavList/NavList.tsx b/packages/react/src/NavList/NavList.tsx index 908322fb475..2ba2fc0d2c0 100644 --- a/packages/react/src/NavList/NavList.tsx +++ b/packages/react/src/NavList/NavList.tsx @@ -2,7 +2,12 @@ import {ChevronDownIcon} from '@primer/octicons-react' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' import React, {isValidElement} from 'react' import styled from 'styled-components' -import type {ActionListDividerProps, ActionListLeadingVisualProps, ActionListTrailingVisualProps} from '../ActionList' +import type { + ActionListTrailingActionProps, + ActionListDividerProps, + ActionListLeadingVisualProps, + ActionListTrailingVisualProps, +} from '../ActionList' import {ActionList} from '../ActionList' import {ActionListContainerContext} from '../ActionList/ActionListContainerContext' import Box from '../Box' @@ -65,9 +70,9 @@ const Item = React.forwardRef( // Get SubNav from children const subNav = React.Children.toArray(children).find(child => isValidElement(child) && child.type === SubNav) - // Get children without SubNav - const childrenWithoutSubNav = React.Children.toArray(children).filter(child => - isValidElement(child) ? child.type !== SubNav : true, + // Get children without SubNav or TrailingAction + const childrenWithoutSubNavOrTrailingAction = React.Children.toArray(children).filter(child => + isValidElement(child) ? child.type !== SubNav && child.type !== TrailingAction : true, ) if (!isValidElement(subNav) && defaultOpen) @@ -78,7 +83,7 @@ const Item = React.forwardRef( if (subNav && isValidElement(subNav)) { return ( - {childrenWithoutSubNav} + {childrenWithoutSubNavOrTrailingAction} ) } @@ -251,6 +256,14 @@ const Divider = ActionList.Divider Divider.displayName = 'NavList.Divider' +// NavList.TrailingAction + +export type NavListTrailingActionProps = ActionListTrailingActionProps + +const TrailingAction = ActionList.TrailingAction + +TrailingAction.displayName = 'NavList.TrailingAction' + // ---------------------------------------------------------------------------- // NavList.Group @@ -285,6 +298,7 @@ export const NavList = Object.assign(Root, { SubNav, LeadingVisual, TrailingVisual, + TrailingAction, Divider, Group, }) diff --git a/script/generate-e2e-tests.js b/script/generate-e2e-tests.js index 3cd8e77901e..1be868f0652 100644 --- a/script/generate-e2e-tests.js +++ b/script/generate-e2e-tests.js @@ -800,6 +800,25 @@ const components = new Map([ ], }, ], + [ + 'NavList', + { + stories: [ + { + id: 'components-navlist--with-trailing-action', + name: 'With TrailingAction', + }, + { + id: 'components-navlist--with-trailing-action-in-sub-item', + name: 'With TrailingAction in Sub Item', + }, + { + id: 'components-navlist--with-bad-example-of-sub-nav-and-trailing-action', + name: 'With Bad Example of SubNav and TrailingAction', + }, + ], + }, + ], [ 'Pagehead', {