Skip to content

Commit ba0a6c0

Browse files
authored
feat(ActionList + ActionList.Divider) Convert to CSS Modules (#5375)
* copy * Update packages/react/src/ActionList/List.tsx * Create twenty-dogs-sparkle.md * lint
1 parent 6978865 commit ba0a6c0

File tree

8 files changed

+226
-16
lines changed

8 files changed

+226
-16
lines changed

.changeset/twenty-dogs-sparkle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/react": minor
3+
---
4+
5+
Convert ActionList (wrapper) and ActionList.Divider to CSS Modules

packages/react/src/ActionList/ActionList.dev.stories.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,21 @@ export const GroupHeadingCustomClassname = () => (
117117
</ActionList>
118118
)
119119

120+
export const ListCustomClassname = () => (
121+
<ActionList className="testCustomClassnameBorder">
122+
<ActionList.Item>Copy link</ActionList.Item>
123+
<ActionList.Item>Quote reply</ActionList.Item>
124+
</ActionList>
125+
)
126+
127+
export const DividerCustomClassname = () => (
128+
<ActionList>
129+
<ActionList.Item>Edit comment</ActionList.Item>
130+
<ActionList.Divider className="testCustomClassnameBgColor" />
131+
<ActionList.Item>Quote reply</ActionList.Item>
132+
</ActionList>
133+
)
134+
120135
export const HeadingCustomClassname = () => (
121136
<ActionList>
122137
<ActionList.Heading className="testCustomClassnameColor" as="h2">
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/* stylelint-disable selector-max-specificity, selector-max-compound-selectors */
2+
3+
.ActionList {
4+
padding: 0;
5+
margin: 0;
6+
list-style: none;
7+
8+
ul {
9+
padding: 0;
10+
margin: 0;
11+
list-style: none;
12+
}
13+
14+
&:where([data-variant='inset']) {
15+
/* change to padding (all) when Item is converted */
16+
padding-block: var(--base-size-8);
17+
}
18+
19+
&:where([data-dividers='true']) {
20+
/* place dividers on the wrapper that excludes leading visuals/actions */
21+
& .ActionListSubContent::before {
22+
position: absolute;
23+
/* stylelint-disable-next-line primer/spacing */
24+
top: calc(-1 * var(--control-medium-paddingBlock));
25+
display: block;
26+
width: 100%;
27+
height: 1px;
28+
content: '';
29+
/* stylelint-disable-next-line primer/colors */
30+
background: var(--borderColor-muted);
31+
}
32+
33+
/* if inline description, move pseudo divider to description wrapper */
34+
& [data-description-variant='inline'] {
35+
&::before {
36+
position: absolute;
37+
/* stylelint-disable-next-line primer/spacing */
38+
top: calc(-1 * var(--control-medium-paddingBlock));
39+
display: block;
40+
width: 100%;
41+
height: var(--borderWidth-thin);
42+
content: '';
43+
/* stylelint-disable-next-line primer/colors */
44+
background: var(--borderColor-muted);
45+
}
46+
47+
/* remove the default divider */
48+
& .ActionListSubContent::before {
49+
content: unset;
50+
}
51+
}
52+
53+
/* hide if item is first of type with label::before, or is the first item after a sectionDivider */
54+
.ActionListItem:first-of-type .ActionListSubContent::before,
55+
.Divider + .ActionListItem .ActionListSubContent::before {
56+
visibility: hidden;
57+
}
58+
59+
/* hide if item is first of type with label::before, or is the first item after a sectionDivider */
60+
.ActionListItem:first-of-type [data-description-variant='inline']::before,
61+
.Divider + .ActionListItem [data-description-variant='inline']::before {
62+
visibility: hidden;
63+
}
64+
}
65+
}
66+
67+
.Divider {
68+
display: block;
69+
height: var(--borderWidth-thin);
70+
padding: 0;
71+
/* stylelint-disable-next-line primer/spacing */
72+
margin-block-start: calc(var(--base-size-8) - var(--borderWidth-thin));
73+
margin-block-end: var(--base-size-8);
74+
margin-inline: calc(-1 * var(--base-size-8));
75+
list-style: none;
76+
/* stylelint-disable-next-line primer/colors */
77+
background: var(--borderColor-muted);
78+
border: 0;
79+
}

packages/react/src/ActionList/ActionList.test.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,4 +650,57 @@ describe('ActionList', () => {
650650
expect(description.parentElement).toHaveAttribute('data-component', 'ActionList.Item--DividerContainer')
651651
})
652652
})
653+
654+
it('should support a custom `className` on the outermost element', () => {
655+
const Element = () => {
656+
return (
657+
<ActionList className="test-class-name">
658+
<ActionList.Item>Item</ActionList.Item>
659+
</ActionList>
660+
)
661+
}
662+
const FeatureFlagElement = () => {
663+
return (
664+
<FeatureFlags
665+
flags={{
666+
primer_react_css_modules_team: true,
667+
primer_react_css_modules_staff: true,
668+
primer_react_css_modules_ga: true,
669+
}}
670+
>
671+
<Element />
672+
</FeatureFlags>
673+
)
674+
}
675+
expect(HTMLRender(<FeatureFlagElement />).container.querySelector('ul')).toHaveClass('test-class-name')
676+
expect(HTMLRender(<Element />).container.querySelector('ul')).toHaveClass('test-class-name')
677+
})
678+
679+
it('divider should support a custom `className`', () => {
680+
const Element = () => {
681+
return (
682+
<ActionList>
683+
<ActionList.Item>Item</ActionList.Item>
684+
<ActionList.Divider className="test-class-name" />
685+
</ActionList>
686+
)
687+
}
688+
const FeatureFlagElement = () => {
689+
return (
690+
<FeatureFlags
691+
flags={{
692+
primer_react_css_modules_team: true,
693+
primer_react_css_modules_staff: true,
694+
primer_react_css_modules_ga: true,
695+
}}
696+
>
697+
<Element />
698+
</FeatureFlags>
699+
)
700+
}
701+
expect(HTMLRender(<FeatureFlagElement />).container.querySelector('li[aria-hidden="true"]')).toHaveClass(
702+
'test-class-name',
703+
)
704+
expect(HTMLRender(<Element />).container.querySelector('li[aria-hidden="true"]')).toHaveClass('test-class-name')
705+
})
653706
})

packages/react/src/ActionList/Divider.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,34 @@ import {get} from '../constants'
44
import type {Theme} from '../ThemeProvider'
55
import type {SxProp} from '../sx'
66
import {merge} from '../sx'
7+
import {clsx} from 'clsx'
8+
import {useFeatureFlag} from '../FeatureFlags'
9+
import classes from './ActionList.module.css'
10+
import {defaultSxProp} from '../utils/defaultSxProp'
711

8-
export type ActionListDividerProps = SxProp
12+
export type ActionListDividerProps = SxProp & {
13+
className?: string
14+
}
915

1016
/**
1117
* Visually separates `Item`s or `Group`s in an `ActionList`.
1218
*/
13-
export const Divider: React.FC<React.PropsWithChildren<ActionListDividerProps>> = ({sx = {}}) => {
19+
export const Divider: React.FC<React.PropsWithChildren<ActionListDividerProps>> = ({sx = defaultSxProp, className}) => {
20+
const enabled = useFeatureFlag('primer_react_css_modules_team')
21+
if (enabled) {
22+
if (sx !== defaultSxProp) {
23+
return (
24+
<Box
25+
className={clsx(className, classes.Divider)}
26+
as="li"
27+
aria-hidden="true"
28+
sx={sx}
29+
data-component="ActionList.Divider"
30+
/>
31+
)
32+
}
33+
return <li className={clsx(className, classes.Divider)} aria-hidden="true" data-component="ActionList.Divider" />
34+
}
1435
return (
1536
<Box
1637
as="li"
@@ -22,11 +43,12 @@ export const Divider: React.FC<React.PropsWithChildren<ActionListDividerProps>>
2243
marginTop: (theme: Theme) => `calc(${get('space.2')(theme)} - 1px)`,
2344
marginBottom: 2,
2445
listStyle: 'none', // hide the ::marker inserted by browser's stylesheet
25-
marginRight: 'calc(-1 * var(--base-size-8, 8px))',
26-
marginLeft: 'calc(-1 * var(--base-size-8, 8px))',
46+
marginRight: 'calc(-1 * var(--base-size-8))',
47+
marginLeft: 'calc(-1 * var(--base-size-8))',
2748
},
2849
sx as SxProp,
2950
)}
51+
className={className}
3052
data-component="ActionList.Divider"
3153
/>
3254
)

packages/react/src/ActionList/List.tsx

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ import {useId} from '../hooks/useId'
1111
import {ListContext, type ActionListProps} from './shared'
1212
import {useProvidedRefOrCreate} from '../hooks'
1313
import {FocusKeys, useFocusZone} from '../hooks/useFocusZone'
14+
import {clsx} from 'clsx'
15+
import {useFeatureFlag} from '../FeatureFlags'
16+
import classes from './ActionList.module.css'
1417

1518
const ListBox = styled.ul<SxProp>(sx)
1619

1720
export const List = React.forwardRef<HTMLUListElement, ActionListProps>(
1821
(
19-
{variant = 'inset', selectionVariant, showDividers = false, role, sx: sxProp = defaultSxProp, ...props},
22+
{variant = 'inset', selectionVariant, showDividers = false, role, sx: sxProp = defaultSxProp, className, ...props},
2023
forwardedRef,
2124
): JSX.Element => {
2225
const styles = {
@@ -54,6 +57,8 @@ export const List = React.forwardRef<HTMLUListElement, ActionListProps>(
5457
focusOutBehavior: listRole === 'menu' ? 'wrap' : undefined,
5558
})
5659

60+
const enabled = useFeatureFlag('primer_react_css_modules_team')
61+
5762
return (
5863
<ListContext.Provider
5964
value={{
@@ -65,15 +70,45 @@ export const List = React.forwardRef<HTMLUListElement, ActionListProps>(
6570
}}
6671
>
6772
{slots.heading}
68-
<ListBox
69-
sx={merge(styles, sxProp as SxProp)}
70-
role={listRole}
71-
aria-labelledby={ariaLabelledBy}
72-
{...props}
73-
ref={listRef}
74-
>
75-
{childrenWithoutSlots}
76-
</ListBox>
73+
{enabled ? (
74+
sxProp !== defaultSxProp ? (
75+
<ListBox
76+
sx={merge(styles, sxProp as SxProp)}
77+
className={clsx(classes.ActionList, className)}
78+
role={listRole}
79+
aria-labelledby={ariaLabelledBy}
80+
ref={listRef}
81+
data-dividers={showDividers}
82+
data-variant={variant}
83+
{...props}
84+
>
85+
{childrenWithoutSlots}
86+
</ListBox>
87+
) : (
88+
<ul
89+
className={clsx(classes.ActionList, className)}
90+
role={listRole}
91+
aria-labelledby={ariaLabelledBy}
92+
ref={listRef}
93+
data-dividers={showDividers}
94+
data-variant={variant}
95+
{...props}
96+
>
97+
{childrenWithoutSlots}
98+
</ul>
99+
)
100+
) : (
101+
<ListBox
102+
sx={merge(styles, sxProp as SxProp)}
103+
role={listRole}
104+
aria-labelledby={ariaLabelledBy}
105+
{...props}
106+
ref={listRef}
107+
className={className}
108+
>
109+
{childrenWithoutSlots}
110+
</ListBox>
111+
)}
77112
</ListContext.Provider>
78113
)
79114
},

packages/react/src/ActionList/shared.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export type ActionListProps = React.PropsWithChildren<{
131131
* The ARIA role describing the function of `List` component. `listbox` or `menu` are a common values.
132132
*/
133133
role?: AriaRole
134+
className?: string
134135
}> &
135136
SxProp
136137

packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,8 +432,8 @@ exports[`NavList renders with groups 1`] = `
432432
margin-top: calc(8px - 1px);
433433
margin-bottom: 8px;
434434
list-style: none;
435-
margin-right: calc(-1 * var(--base-size-8,8px));
436-
margin-left: calc(-1 * var(--base-size-8,8px));
435+
margin-right: calc(-1 * var(--base-size-8));
436+
margin-left: calc(-1 * var(--base-size-8));
437437
}
438438
439439
.c1:first-child {

0 commit comments

Comments
 (0)