Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/rare-words-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@primer/react': minor
---

ActionList.Group + ActionList.TrailingAction: add missing className prop
LabelGroup: add missing className prop
2 changes: 1 addition & 1 deletion packages/react/src/ActionBar/ActionBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('ActionBar', () => {

behavesAsComponent({
Component: ActionBar,
options: {skipAs: true, skipSx: true},
options: {skipAs: true, skipSx: true, skipClassName: true},
toRender: () => <SimpleActionBar />,
})

Expand Down
54 changes: 54 additions & 0 deletions packages/react/src/ActionList/ActionList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ describe('ActionList', () => {
toRender: () => <ActionList />,
})

behavesAsComponent({
Component: ActionList.Divider,
options: {skipAs: true, skipSx: true},
toRender: () => <ActionList.Divider />,
})

behavesAsComponent({
Component: ActionList.TrailingAction,
options: {skipAs: true, skipSx: true},
toRender: () => <ActionList.TrailingAction label="Action">Action</ActionList.TrailingAction>,
})

checkExports('ActionList', {
default: undefined,
ActionList,
Expand Down Expand Up @@ -144,4 +156,46 @@ describe('ActionList', () => {
)
expect(HTMLRender(<Element />).container.querySelector('li[aria-hidden="true"]')).toHaveClass('test-class-name')
})

it('list and its sub-components support classname', () => {
const {container} = HTMLRender(
<ActionList className="list">
<ActionList.Heading as="h2" className="heading">
Heading
</ActionList.Heading>
<ActionList.Item className="item">
Item
<ActionList.TrailingAction label="action" className="trailing_action">
Trailing Action
</ActionList.TrailingAction>
</ActionList.Item>
<ActionList.Divider className="divider" />
<ActionList.LinkItem className="link" href="//github.com" title="anchor" aria-keyshortcuts="d">
Link Item
</ActionList.LinkItem>
<ActionList.Group className="group">
<ActionList.GroupHeading as="h2" className="group_heading">
Group Heading
</ActionList.GroupHeading>
<ActionList.Item className="item">
<ActionList.TrailingVisual className="trailing">Trailing Visual</ActionList.TrailingVisual>
<ActionList.LeadingVisual className="leading">Leading Visual</ActionList.LeadingVisual>
<ActionList.Description className="description">Description</ActionList.Description>
</ActionList.Item>
</ActionList.Group>
</ActionList>,
)

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()
})
})
1 change: 1 addition & 0 deletions packages/react/src/ActionList/Description.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,4 @@ export const Description: React.FC<React.PropsWithChildren<ActionListDescription
</Truncate>
)
}
Description.displayName = 'ActionList.Description'
1 change: 1 addition & 0 deletions packages/react/src/ActionList/Divider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ export const Divider: React.FC<React.PropsWithChildren<ActionListDividerProps>>
/>
)
}
Divider.displayName = 'ActionList.Divider'
13 changes: 13 additions & 0 deletions packages/react/src/ActionList/Group.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => <ActionList.Group />,
})

behavesAsComponent({
Component: ActionList.GroupHeading,
options: {skipAs: true, skipSx: true},
toRender: () => <ActionList.GroupHeading />,
})

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(() =>
Expand Down
19 changes: 17 additions & 2 deletions packages/react/src/ActionList/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -86,6 +90,7 @@ export const Group: React.FC<React.PropsWithChildren<ActionListGroupProps>> = ({
auxiliaryText,
selectionVariant,
role,
className,
sx = defaultSxProp,
...props
}) => {
Expand All @@ -112,7 +117,13 @@ export const Group: React.FC<React.PropsWithChildren<ActionListGroupProps>> = ({
if (enabled) {
if (sx !== defaultSxProp) {
return (
<Box as="li" className={groupClasses.Group} role={listRole ? 'none' : undefined} sx={sx} {...props}>
<Box
as="li"
className={clsx(className, groupClasses.Group)}
role={listRole ? 'none' : undefined}
sx={sx}
{...props}
>
<GroupContext.Provider value={{selectionVariant, groupHeadingId}}>
{title && !slots.groupHeading ? (
// Escape hatch: supports old API <ActionList.Group title="group title"> in a non breaking way
Expand All @@ -135,7 +146,7 @@ export const Group: React.FC<React.PropsWithChildren<ActionListGroupProps>> = ({
)
}
return (
<li className={groupClasses.Group} role={listRole ? 'none' : undefined} {...props}>
<li className={clsx(className, groupClasses.Group)} role={listRole ? 'none' : undefined} {...props}>
<GroupContext.Provider value={{selectionVariant, groupHeadingId}}>
{title && !slots.groupHeading ? (
// Escape hatch: supports old API <ActionList.Group title="group title"> in a non breaking way
Expand Down Expand Up @@ -166,6 +177,7 @@ export const Group: React.FC<React.PropsWithChildren<ActionListGroupProps>> = ({
listStyle: 'none', // hide the ::marker inserted by browser's stylesheet
...sx,
}}
className={className}
{...props}
>
<GroupContext.Provider value={{selectionVariant, groupHeadingId}}>
Expand Down Expand Up @@ -292,3 +304,6 @@ export const GroupHeading: React.FC<React.PropsWithChildren<ActionListGroupHeadi
</>
)
}

GroupHeading.displayName = 'ActionList.GroupHeading'
Group.displayName = 'ActionList.Group'
7 changes: 7 additions & 0 deletions packages/react/src/ActionList/Heading.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => <ActionList.Heading as="h1" />,
})

it('should render the ActionList.Heading component as a heading with the given heading level', async () => {
const container = HTMLRender(
<ActionList>
Expand Down
43 changes: 43 additions & 0 deletions packages/react/src/ActionList/Item.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -57,6 +58,48 @@ function SingleSelectListStory(): JSX.Element {
}

describe('ActionList.Item', () => {
behavesAsComponent({
Component: ActionList.Item,
options: {skipAs: true, skipSx: true},
toRender: () => <ActionList.Item />,
})

behavesAsComponent({
Component: ActionList.LinkItem,
options: {skipAs: true, skipSx: true},
toRender: () => <ActionList.LinkItem />,
})

behavesAsComponent({
Component: ActionList.TrailingVisual,
options: {skipAs: true, skipSx: true, skipClassName: true},
toRender: () => (
<ActionList.Item>
<ActionList.TrailingVisual>Trailing Visual</ActionList.TrailingVisual>
</ActionList.Item>
),
})

behavesAsComponent({
Component: ActionList.LeadingVisual,
options: {skipAs: true, skipSx: true, skipClassName: true},
toRender: () => (
<ActionList.Item>
<ActionList.LeadingVisual>Leading Visual</ActionList.LeadingVisual>
</ActionList.Item>
),
})

behavesAsComponent({
Component: ActionList.Description,
options: {skipAs: true, skipSx: true, skipClassName: true},
toRender: () => (
<ActionList.Item>
<ActionList.Description>Description</ActionList.Description>
</ActionList.Item>
),
})

it('should have aria-keyshortcuts applied to the correct element', async () => {
const {container} = HTMLRender(<SimpleActionList />)
const linkOptions = await waitFor(() => container.querySelectorAll('a'))
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/ActionList/LinkItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,5 @@ export const LinkItem = React.forwardRef(
)
},
) as PolymorphicForwardRefComponent<'a', ActionListLinkItemProps>

LinkItem.displayName = 'ActionList.LinkItem'
1 change: 1 addition & 0 deletions packages/react/src/ActionList/TrailingAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const TrailingAction = forwardRef(
sx={{
flexShrink: 0,
}}
className={className}
>
{icon ? (
<IconButton
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/ActionList/Visuals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,6 @@ export const VisualOrIndicator: React.FC<
</VisualComponent>
)
}

LeadingVisual.displayName = 'ActionList.LeadingVisual'
TrailingVisual.displayName = 'ActionList.TrailingVisual'
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('ConfirmationDialog', () => {
behavesAsComponent({
Component: ConfirmationDialog,
toRender: () => <Basic />,
options: {skipAs: true, skipSx: true},
options: {skipAs: true, skipSx: true, skipClassName: true},
})

checkExports('ConfirmationDialog/ConfirmationDialog', {
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/Dialog/Dialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('Dialog', () => {

behavesAsComponent({
Component: Dialog,
options: {skipAs: true, skipSx: true},
options: {skipAs: true, skipSx: true, skipClassName: true},
toRender: () => (
<Dialog onClose={() => {}}>
<div>Hidden when narrow</div>
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/DialogV1/Dialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
11 changes: 10 additions & 1 deletion packages/react/src/LabelGroup/LabelGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<SxProp>`
Expand Down Expand Up @@ -158,6 +159,7 @@ const LabelGroup: React.FC<React.PropsWithChildren<LabelGroupProps>> = ({
overflowStyle = 'overlay',
sx: sxProp,
as = 'ul',
className,
}) => {
const containerRef = React.useRef<HTMLElement>(null)
const collapseButtonRef = React.useRef<HTMLButtonElement>(null)
Expand Down Expand Up @@ -337,6 +339,7 @@ const LabelGroup: React.FC<React.PropsWithChildren<LabelGroupProps>> = ({
data-overflow={overflowStyle === 'inline' && isOverflowShown ? 'inline' : undefined}
data-list={isList || undefined}
sx={sxProp}
className={className}
as={as}
>
{React.Children.map(children, (child, index) => (
Expand Down Expand Up @@ -378,7 +381,13 @@ const LabelGroup: React.FC<React.PropsWithChildren<LabelGroupProps>> = ({
</ToggleWrapper>
</StyledLabelGroupContainer>
) : (
<StyledLabelGroupContainer data-overflow="inline" data-list={isList || undefined} sx={sxProp} as={as}>
<StyledLabelGroupContainer
data-overflow="inline"
data-list={isList || undefined}
sx={sxProp}
as={as}
className={className}
>
{isList
? React.Children.map(children, (child, index) => {
return <li key={index}>{child}</li>
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/__tests__/ActionMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => <Example />,
})

Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/__tests__/AnchoredOverlay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const AnchoredOverlayTestComponent = ({
describe('AnchoredOverlay', () => {
behavesAsComponent({
Component: AnchoredOverlay,
options: {skipAs: true, skipSx: true},
options: {skipAs: true, skipSx: true, skipClassName: true},
toRender: () => <AnchoredOverlayTestComponent />,
})

Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/__tests__/LabelGroup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('LabelGroup', () => {
thresholds: [],
})) as jest.Mock<IntersectionObserver>

behavesAsComponent({Component: LabelGroup, options: {skipAs: true}})
behavesAsComponent({Component: LabelGroup, options: {skipAs: true, skipClassName: true}})

checkExports('LabelGroup', {
default: LabelGroup,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('ActionMenu', () => {

behavesAsComponent({
Component: ActionMenu,
options: {skipAs: true, skipSx: true},
options: {skipAs: true, skipSx: true, skipClassName: true},
toRender: () => <ActionMenu items={[]} />,
})

Expand Down
10 changes: 10 additions & 0 deletions packages/react/src/utils/testing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ export function unloadCSS(path: string) {
interface Options {
skipAs?: boolean
skipSx?: boolean
skipClassName?: boolean
skipDisplayName?: boolean
}

Expand Down Expand Up @@ -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
Expand Down
Loading