From e77411090aa3912baddc11c7ba115b8870cb1860 Mon Sep 17 00:00:00 2001 From: Hussam Ghazzi Date: Fri, 21 Mar 2025 20:47:12 +0000 Subject: [PATCH 1/2] add missing className props and update behavesAsComponent test check --- .changeset/rare-words-sin.md | 5 ++ .../react/src/ActionBar/ActionBar.test.tsx | 2 +- .../react/src/ActionList/ActionList.test.tsx | 54 +++++++++++++++++++ packages/react/src/ActionList/Description.tsx | 1 + packages/react/src/ActionList/Divider.tsx | 1 + packages/react/src/ActionList/Group.test.tsx | 13 +++++ packages/react/src/ActionList/Group.tsx | 19 ++++++- .../react/src/ActionList/Heading.test.tsx | 7 +++ packages/react/src/ActionList/Item.test.tsx | 43 +++++++++++++++ packages/react/src/ActionList/LinkItem.tsx | 2 + .../react/src/ActionList/TrailingAction.tsx | 1 + packages/react/src/ActionList/Visuals.tsx | 3 ++ .../ConfirmationDialog.test.tsx | 2 +- packages/react/src/Dialog/Dialog.test.tsx | 2 +- packages/react/src/DialogV1/Dialog.test.tsx | 4 +- packages/react/src/LabelGroup/LabelGroup.tsx | 11 +++- .../react/src/__tests__/ActionMenu.test.tsx | 2 +- .../src/__tests__/AnchoredOverlay.test.tsx | 2 +- .../react/src/__tests__/LabelGroup.test.tsx | 2 +- .../__tests__/deprecated/ActionMenu.test.tsx | 2 +- packages/react/src/utils/testing.tsx | 10 ++++ 21 files changed, 176 insertions(+), 12 deletions(-) create mode 100644 .changeset/rare-words-sin.md diff --git a/.changeset/rare-words-sin.md b/.changeset/rare-words-sin.md new file mode 100644 index 00000000000..4f617ec48ce --- /dev/null +++ b/.changeset/rare-words-sin.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +chore(className): add missing className props and update behavesAsComponent test check diff --git a/packages/react/src/ActionBar/ActionBar.test.tsx b/packages/react/src/ActionBar/ActionBar.test.tsx index 44a0c25bcf6..135af92ef14 100644 --- a/packages/react/src/ActionBar/ActionBar.test.tsx +++ b/packages/react/src/ActionBar/ActionBar.test.tsx @@ -24,7 +24,7 @@ describe('ActionBar', () => { behavesAsComponent({ Component: ActionBar, - options: {skipAs: true, skipSx: true}, + options: {skipAs: true, skipSx: true, skipClassName: true}, toRender: () => , }) diff --git a/packages/react/src/ActionList/ActionList.test.tsx b/packages/react/src/ActionList/ActionList.test.tsx index c8d891dec20..7723425a724 100644 --- a/packages/react/src/ActionList/ActionList.test.tsx +++ b/packages/react/src/ActionList/ActionList.test.tsx @@ -34,6 +34,18 @@ describe('ActionList', () => { toRender: () => , }) + behavesAsComponent({ + Component: ActionList.Divider, + options: {skipAs: true, skipSx: true}, + toRender: () => , + }) + + behavesAsComponent({ + Component: ActionList.TrailingAction, + options: {skipAs: true, skipSx: true}, + toRender: () => Action, + }) + checkExports('ActionList', { default: undefined, ActionList, @@ -144,4 +156,46 @@ describe('ActionList', () => { ) expect(HTMLRender().container.querySelector('li[aria-hidden="true"]')).toHaveClass('test-class-name') }) + + it('list and its sub-components support classname', () => { + const {container} = HTMLRender( + + + Heading + + + Item + + Trailing Action + + + + + Link Item + + + + Group Heading + + + Trailing Visual + Leading Visual + Description + + + , + ) + + expect(container.querySelector('.list')).toBeInTheDocument() + expect(container.querySelector('.heading')).toBeInTheDocument() + expect(container.querySelector('.item')).toBeInTheDocument() + expect(container.querySelector('.trailing_action')).toBeInTheDocument() + expect(container.querySelector('.divider')).toBeInTheDocument() + expect(container.querySelector('.link')).toBeInTheDocument() + expect(container.querySelector('.group')).toBeInTheDocument() + expect(container.querySelector('.group_heading')).toBeInTheDocument() + expect(container.querySelector('.trailing')).toBeInTheDocument() + expect(container.querySelector('.leading')).toBeInTheDocument() + expect(container.querySelector('.description')).toBeInTheDocument() + }) }) diff --git a/packages/react/src/ActionList/Description.tsx b/packages/react/src/ActionList/Description.tsx index 15b2e19c3f4..623f7c25ff3 100644 --- a/packages/react/src/ActionList/Description.tsx +++ b/packages/react/src/ActionList/Description.tsx @@ -130,3 +130,4 @@ export const Description: React.FC ) } +Description.displayName = 'ActionList.Description' diff --git a/packages/react/src/ActionList/Divider.tsx b/packages/react/src/ActionList/Divider.tsx index 5de6785ca75..e9897530406 100644 --- a/packages/react/src/ActionList/Divider.tsx +++ b/packages/react/src/ActionList/Divider.tsx @@ -52,3 +52,4 @@ export const Divider: React.FC> /> ) } +Divider.displayName = 'ActionList.Divider' diff --git a/packages/react/src/ActionList/Group.test.tsx b/packages/react/src/ActionList/Group.test.tsx index 0c036d3df58..881aa5045bc 100644 --- a/packages/react/src/ActionList/Group.test.tsx +++ b/packages/react/src/ActionList/Group.test.tsx @@ -4,8 +4,21 @@ import theme from '../theme' import {ActionList} from '.' import {BaseStyles, ThemeProvider, ActionMenu} from '..' import {FeatureFlags} from '../FeatureFlags' +import {behavesAsComponent} from '../utils/testing' describe('ActionList.Group', () => { + behavesAsComponent({ + Component: ActionList.Group, + options: {skipAs: true, skipSx: true}, + toRender: () => , + }) + + behavesAsComponent({ + Component: ActionList.GroupHeading, + options: {skipAs: true, skipSx: true}, + toRender: () => , + }) + it('should throw an error when ActionList.GroupHeading has an `as` prop when it is used within ActionMenu context', async () => { const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn()) expect(() => diff --git a/packages/react/src/ActionList/Group.tsx b/packages/react/src/ActionList/Group.tsx index 44be2fa68d4..52b61e4d36d 100644 --- a/packages/react/src/ActionList/Group.tsx +++ b/packages/react/src/ActionList/Group.tsx @@ -67,6 +67,10 @@ export type ActionListGroupProps = { * The ARIA role describing the function of the list inside `Group` component. `listbox` or `menu` are a common values. */ role?: AriaRole + /** + * Custom class name to apply to the `Group`. + */ + className?: string } & SxProp & { /** * Whether multiple Items or a single Item can be selected in the Group. Overrides value on ActionList root. @@ -86,6 +90,7 @@ export const Group: React.FC> = ({ auxiliaryText, selectionVariant, role, + className, sx = defaultSxProp, ...props }) => { @@ -112,7 +117,13 @@ export const Group: React.FC> = ({ if (enabled) { if (sx !== defaultSxProp) { return ( - + {title && !slots.groupHeading ? ( // Escape hatch: supports old API in a non breaking way @@ -135,7 +146,7 @@ export const Group: React.FC> = ({ ) } return ( -
  • +
  • {title && !slots.groupHeading ? ( // Escape hatch: supports old API in a non breaking way @@ -166,6 +177,7 @@ export const Group: React.FC> = ({ listStyle: 'none', // hide the ::marker inserted by browser's stylesheet ...sx, }} + className={className} {...props} > @@ -292,3 +304,6 @@ export const GroupHeading: React.FC ) } + +GroupHeading.displayName = 'ActionList.GroupHeading' +Group.displayName = 'ActionList.Group' diff --git a/packages/react/src/ActionList/Heading.test.tsx b/packages/react/src/ActionList/Heading.test.tsx index aff9a56923a..e87cd4133de 100644 --- a/packages/react/src/ActionList/Heading.test.tsx +++ b/packages/react/src/ActionList/Heading.test.tsx @@ -4,8 +4,15 @@ import theme from '../theme' import {ActionList} from '.' import {BaseStyles, ThemeProvider, ActionMenu} from '..' import {FeatureFlags} from '../FeatureFlags' +import {behavesAsComponent} from '../utils/testing' describe('ActionList.Heading', () => { + behavesAsComponent({ + Component: ActionList.Heading, + options: {skipAs: true, skipSx: true}, + toRender: () => , + }) + it('should render the ActionList.Heading component as a heading with the given heading level', async () => { const container = HTMLRender( diff --git a/packages/react/src/ActionList/Item.test.tsx b/packages/react/src/ActionList/Item.test.tsx index 5751126aa93..0570f62cd61 100644 --- a/packages/react/src/ActionList/Item.test.tsx +++ b/packages/react/src/ActionList/Item.test.tsx @@ -4,6 +4,7 @@ import React from 'react' import {ActionList} from '.' import {BookIcon} from '@primer/octicons-react' import {FeatureFlags} from '../FeatureFlags' +import {behavesAsComponent} from '../utils/testing' function SimpleActionList(): JSX.Element { return ( @@ -57,6 +58,48 @@ function SingleSelectListStory(): JSX.Element { } describe('ActionList.Item', () => { + behavesAsComponent({ + Component: ActionList.Item, + options: {skipAs: true, skipSx: true}, + toRender: () => , + }) + + behavesAsComponent({ + Component: ActionList.LinkItem, + options: {skipAs: true, skipSx: true}, + toRender: () => , + }) + + behavesAsComponent({ + Component: ActionList.TrailingVisual, + options: {skipAs: true, skipSx: true, skipClassName: true}, + toRender: () => ( + + Trailing Visual + + ), + }) + + behavesAsComponent({ + Component: ActionList.LeadingVisual, + options: {skipAs: true, skipSx: true, skipClassName: true}, + toRender: () => ( + + Leading Visual + + ), + }) + + behavesAsComponent({ + Component: ActionList.Description, + options: {skipAs: true, skipSx: true, skipClassName: true}, + toRender: () => ( + + Description + + ), + }) + it('should have aria-keyshortcuts applied to the correct element', async () => { const {container} = HTMLRender() const linkOptions = await waitFor(() => container.querySelectorAll('a')) diff --git a/packages/react/src/ActionList/LinkItem.tsx b/packages/react/src/ActionList/LinkItem.tsx index fd9d908f292..d495e9c7b3f 100644 --- a/packages/react/src/ActionList/LinkItem.tsx +++ b/packages/react/src/ActionList/LinkItem.tsx @@ -137,3 +137,5 @@ export const LinkItem = React.forwardRef( ) }, ) as PolymorphicForwardRefComponent<'a', ActionListLinkItemProps> + +LinkItem.displayName = 'ActionList.LinkItem' diff --git a/packages/react/src/ActionList/TrailingAction.tsx b/packages/react/src/ActionList/TrailingAction.tsx index 70c931efb03..efaee69bdec 100644 --- a/packages/react/src/ActionList/TrailingAction.tsx +++ b/packages/react/src/ActionList/TrailingAction.tsx @@ -67,6 +67,7 @@ export const TrailingAction = forwardRef( sx={{ flexShrink: 0, }} + className={className} > {icon ? ( ) } + +LeadingVisual.displayName = 'ActionList.LeadingVisual' +TrailingVisual.displayName = 'ActionList.TrailingVisual' diff --git a/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx b/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx index bd9de14a80b..2aad3a23dda 100644 --- a/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx +++ b/packages/react/src/ConfirmationDialog/ConfirmationDialog.test.tsx @@ -73,7 +73,7 @@ describe('ConfirmationDialog', () => { behavesAsComponent({ Component: ConfirmationDialog, toRender: () => , - options: {skipAs: true, skipSx: true}, + options: {skipAs: true, skipSx: true, skipClassName: true}, }) checkExports('ConfirmationDialog/ConfirmationDialog', { diff --git a/packages/react/src/Dialog/Dialog.test.tsx b/packages/react/src/Dialog/Dialog.test.tsx index 39e9dd71704..9195ca85c53 100644 --- a/packages/react/src/Dialog/Dialog.test.tsx +++ b/packages/react/src/Dialog/Dialog.test.tsx @@ -21,7 +21,7 @@ describe('Dialog', () => { behavesAsComponent({ Component: Dialog, - options: {skipAs: true, skipSx: true}, + options: {skipAs: true, skipSx: true, skipClassName: true}, toRender: () => ( {}}>
    Hidden when narrow
    diff --git a/packages/react/src/DialogV1/Dialog.test.tsx b/packages/react/src/DialogV1/Dialog.test.tsx index b23058956ee..92fbe88650d 100644 --- a/packages/react/src/DialogV1/Dialog.test.tsx +++ b/packages/react/src/DialogV1/Dialog.test.tsx @@ -107,11 +107,11 @@ describe('Dialog', () => { behavesAsComponent({ Component: Dialog, toRender: () => comp, - options: {skipAs: true, skipSx: true}, + options: {skipAs: true, skipSx: true, skipClassName: true}, }) describe('Dialog.Header', () => { - behavesAsComponent({Component: Dialog.Header}) + behavesAsComponent({Component: Dialog.Header, options: {skipClassName: true}}) }) it('should support `className` on the Dialog element', () => { diff --git a/packages/react/src/LabelGroup/LabelGroup.tsx b/packages/react/src/LabelGroup/LabelGroup.tsx index 5aaf6f8edd8..1cef8c2e3a5 100644 --- a/packages/react/src/LabelGroup/LabelGroup.tsx +++ b/packages/react/src/LabelGroup/LabelGroup.tsx @@ -18,6 +18,7 @@ export type LabelGroupProps = { overflowStyle?: 'inline' | 'overlay' /** How many tokens to show. `'auto'` truncates the tokens to fit in the parent container. Passing a number will truncate after that number tokens. If this is undefined, tokens will never be truncated. */ visibleChildCount?: 'auto' | number + className?: string } & SxProp const StyledLabelGroupContainer = styled.div` @@ -158,6 +159,7 @@ const LabelGroup: React.FC> = ({ overflowStyle = 'overlay', sx: sxProp, as = 'ul', + className, }) => { const containerRef = React.useRef(null) const collapseButtonRef = React.useRef(null) @@ -337,6 +339,7 @@ const LabelGroup: React.FC> = ({ data-overflow={overflowStyle === 'inline' && isOverflowShown ? 'inline' : undefined} data-list={isList || undefined} sx={sxProp} + className={className} as={as} > {React.Children.map(children, (child, index) => ( @@ -378,7 +381,13 @@ const LabelGroup: React.FC> = ({ ) : ( - + {isList ? React.Children.map(children, (child, index) => { return
  • {child}
  • diff --git a/packages/react/src/__tests__/ActionMenu.test.tsx b/packages/react/src/__tests__/ActionMenu.test.tsx index 64b488b2679..b82750ec0b9 100644 --- a/packages/react/src/__tests__/ActionMenu.test.tsx +++ b/packages/react/src/__tests__/ActionMenu.test.tsx @@ -131,7 +131,7 @@ function ExampleWithSubmenus(): JSX.Element { describe('ActionMenu', () => { behavesAsComponent({ Component: ActionList, - options: {skipAs: true, skipSx: true}, + options: {skipAs: true, skipSx: true, skipClassName: true}, toRender: () => , }) diff --git a/packages/react/src/__tests__/AnchoredOverlay.test.tsx b/packages/react/src/__tests__/AnchoredOverlay.test.tsx index e79514a04ef..31ed65a5d71 100644 --- a/packages/react/src/__tests__/AnchoredOverlay.test.tsx +++ b/packages/react/src/__tests__/AnchoredOverlay.test.tsx @@ -56,7 +56,7 @@ const AnchoredOverlayTestComponent = ({ describe('AnchoredOverlay', () => { behavesAsComponent({ Component: AnchoredOverlay, - options: {skipAs: true, skipSx: true}, + options: {skipAs: true, skipSx: true, skipClassName: true}, toRender: () => , }) diff --git a/packages/react/src/__tests__/LabelGroup.test.tsx b/packages/react/src/__tests__/LabelGroup.test.tsx index dc48c8fe1d3..56705cd28ca 100644 --- a/packages/react/src/__tests__/LabelGroup.test.tsx +++ b/packages/react/src/__tests__/LabelGroup.test.tsx @@ -32,7 +32,7 @@ describe('LabelGroup', () => { thresholds: [], })) as jest.Mock - behavesAsComponent({Component: LabelGroup, options: {skipAs: true}}) + behavesAsComponent({Component: LabelGroup, options: {skipAs: true, skipClassName: true}}) checkExports('LabelGroup', { default: LabelGroup, diff --git a/packages/react/src/__tests__/deprecated/ActionMenu.test.tsx b/packages/react/src/__tests__/deprecated/ActionMenu.test.tsx index adc4aa25293..a8b824487d7 100644 --- a/packages/react/src/__tests__/deprecated/ActionMenu.test.tsx +++ b/packages/react/src/__tests__/deprecated/ActionMenu.test.tsx @@ -38,7 +38,7 @@ describe('ActionMenu', () => { behavesAsComponent({ Component: ActionMenu, - options: {skipAs: true, skipSx: true}, + options: {skipAs: true, skipSx: true, skipClassName: true}, toRender: () => , }) diff --git a/packages/react/src/utils/testing.tsx b/packages/react/src/utils/testing.tsx index 7497d038b55..521d906ba45 100644 --- a/packages/react/src/utils/testing.tsx +++ b/packages/react/src/utils/testing.tsx @@ -199,6 +199,7 @@ export function unloadCSS(path: string) { interface Options { skipAs?: boolean skipSx?: boolean + skipClassName?: boolean skipDisplayName?: boolean } @@ -233,6 +234,15 @@ export function behavesAsComponent({Component, toRender, options}: BehavesAsComp expect(Component.displayName).toMatch(COMPONENT_DISPLAY_NAME_REGEX) }) } + + if (!options.skipClassName) { + it('supports className prop', () => { + const className = 'test-class' + const elem = React.cloneElement(getElement(), {className}) + const {container} = HTMLRender(elem) + expect(container.querySelector('.test-class')).not.toBeNull() + }) + } } // eslint-disable-next-line @typescript-eslint/no-explicit-any From 368ed0216dc654c269e873d2963aeac973984d11 Mon Sep 17 00:00:00 2001 From: Hussam Ghazzi Date: Sat, 22 Mar 2025 15:21:43 -0400 Subject: [PATCH 2/2] Update .changeset/rare-words-sin.md Co-authored-by: Siddharth Kshetrapal --- .changeset/rare-words-sin.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/rare-words-sin.md b/.changeset/rare-words-sin.md index 4f617ec48ce..19fb5d70ecb 100644 --- a/.changeset/rare-words-sin.md +++ b/.changeset/rare-words-sin.md @@ -2,4 +2,5 @@ '@primer/react': minor --- -chore(className): add missing className props and update behavesAsComponent test check +ActionList.Group + ActionList.TrailingAction: add missing className prop +LabelGroup: add missing className prop