Skip to content

Commit 719def7

Browse files
feat(Select): Convert Select component to CSS Modules behind feature flag (#5194)
* initial commit * changeset and lint * fix css module comments * fix Select import to please the FormControl component's type comparision * fix slot comparison in form control
1 parent 8138dee commit 719def7

File tree

9 files changed

+235
-78
lines changed

9 files changed

+235
-78
lines changed

.changeset/modern-icons-clean.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+
Migrate `Select` component to css modules

packages/react/src/FormControl/FormControl.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Autocomplete from '../Autocomplete'
33
import Box from '../Box'
44
import Checkbox from '../Checkbox'
55
import Radio from '../Radio'
6-
import Select from '../Select'
6+
import Select from '../Select/Select'
77
import {SelectPanel} from '../SelectPanel'
88
import TextInput from '../TextInput'
99
import TextInputWithTokens from '../TextInputWithTokens'
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react'
2+
import type {Meta} from '@storybook/react'
3+
import {FormControl, Box} from '..'
4+
import Select from './Select'
5+
6+
export default {
7+
title: 'Components/Select/Dev',
8+
component: Select,
9+
} as Meta
10+
11+
export const Default = () => (
12+
<Box as="form">
13+
<FormControl>
14+
<FormControl.Label>Default label</FormControl.Label>
15+
<Select sx={{color: 'danger.fg'}}>
16+
<Select.Option value="one">Choice one</Select.Option>
17+
<Select.Option value="two">Choice two</Select.Option>
18+
<Select.Option value="three">Choice three</Select.Option>
19+
<Select.Option value="four">Choice four</Select.Option>
20+
<Select.Option value="five">Choice five</Select.Option>
21+
<Select.Option value="six">Choice six</Select.Option>
22+
</Select>
23+
</FormControl>
24+
</Box>
25+
)

packages/react/src/Select/Select.features.stories.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react'
2-
import {Select, FormControl, Box, Heading} from '..'
2+
import {FormControl, Box, Heading} from '..'
3+
import Select from './Select'
34

45
export default {
56
title: 'Components/Select/Features',

packages/react/src/Select/Select.figma.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react'
2-
import {Select} from '../../src'
2+
import Select from '.'
33
import FormControl from '../FormControl'
44
import figma from '@figma/code-connect'
55

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
.Select {
2+
width: 100%;
3+
/* stylelint-disable-next-line primer/spacing */
4+
margin-top: 1px;
5+
/* stylelint-disable-next-line primer/spacing */
6+
margin-bottom: 1px;
7+
/* stylelint-disable-next-line primer/spacing */
8+
margin-left: 1px;
9+
font-size: inherit;
10+
color: currentColor;
11+
12+
/* Firefox hacks:
13+
* 1. Makes Firefox's native dropdown menu's background match the theme.
14+
* background-color should be 'transparent', but Firefox uses the background-color on
15+
* <select> to determine the background color used for the dropdown menu.
16+
* 2. Adds 1px margins to the <select> so the background color doesn't hide the focus outline created with an inset box-shadow.
17+
*/
18+
background-color: inherit;
19+
border: 0;
20+
border-radius: inherit;
21+
outline: none;
22+
appearance: none;
23+
24+
/* 2. Prevents visible overlap of partially transparent background colors.
25+
* 'colors.input.disabledBg' happens to be partially transparent in light mode, so we use a
26+
* transparent background-color on a disabled <select>.
27+
*/
28+
&:disabled {
29+
background-color: transparent;
30+
}
31+
32+
/* 3. Maintain dark bg color in Firefox on Windows high-contrast mode
33+
* Firefox makes the <select>'s background color white when setting 'background-color: transparent;'
34+
*/
35+
@media screen and (forced-colors: active) {
36+
&:disabled {
37+
background-color: -moz-combobox;
38+
}
39+
}
40+
}
41+
42+
.TextInputWrapper {
43+
position: relative;
44+
overflow: hidden;
45+
46+
@media screen and (forced-colors: active) {
47+
svg {
48+
fill: 'FieldText';
49+
}
50+
}
51+
}
52+
53+
.disabled {
54+
@media screen and (forced-colors: active) {
55+
svg {
56+
fill: 'GrayText';
57+
}
58+
}
59+
}
60+
61+
.ArrowIndicator {
62+
position: absolute;
63+
top: 50%;
64+
right: var(--base-size-4);
65+
pointer-events: none;
66+
transform: translateY(-50%);
67+
}

packages/react/src/Select/Select.stories.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React from 'react'
22
import type {Meta} from '@storybook/react'
3-
import {Select, FormControl, Box} from '..'
4-
import type {SelectProps} from '../Select'
3+
import {FormControl, Box} from '..'
4+
import Select from './Select'
5+
import type {SelectProps} from './Select'
56
import type {FormControlArgs} from '../utils/form-story-helpers'
67
import {
78
formControlArgs,

packages/react/src/Select/Select.tsx

Lines changed: 128 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import React from 'react'
22
import styled from 'styled-components'
3+
import {clsx} from 'clsx'
34
import type {StyledWrapperProps} from '../internal/components/TextInputWrapper'
45
import TextInputWrapper from '../internal/components/TextInputWrapper'
6+
import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent'
7+
import {useFeatureFlag} from '../FeatureFlags'
8+
import classes from './Select.module.css'
59

610
export type SelectProps = Omit<
711
Omit<React.ComponentPropsWithoutRef<'select'>, 'size'> & Omit<StyledWrapperProps, 'variant'>,
@@ -10,106 +14,159 @@ export type SelectProps = Omit<
1014
placeholder?: string
1115
}
1216

13-
const arrowRightOffset = '4px'
17+
const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_team'
1418

15-
const StyledSelect = styled.select`
16-
appearance: none;
17-
border-radius: inherit;
18-
border: 0;
19-
color: currentColor;
20-
font-size: inherit;
21-
outline: none;
22-
width: 100%;
19+
const arrowRightOffset = '4px'
2320

24-
/* Firefox hacks: */
25-
/* 1. Makes Firefox's native dropdown menu's background match the theme.
21+
const StyledSelect = toggleStyledComponent(
22+
CSS_MODULES_FEATURE_FLAG,
23+
'select',
24+
styled.select`
25+
appearance: none;
26+
border-radius: inherit;
27+
border: 0;
28+
color: currentColor;
29+
font-size: inherit;
30+
outline: none;
31+
width: 100%;
32+
33+
/* Firefox hacks: */
34+
/* 1. Makes Firefox's native dropdown menu's background match the theme.
2635
2736
background-color should be 'transparent', but Firefox uses the background-color on
2837
<select> to determine the background color used for the dropdown menu.
2938
3039
2. Adds 1px margins to the <select> so the background color doesn't hide the focus outline created with an inset box-shadow.
3140
*/
32-
background-color: inherit;
33-
margin-top: 1px;
34-
margin-left: 1px;
35-
margin-bottom: 1px;
41+
background-color: inherit;
42+
margin-top: 1px;
43+
margin-left: 1px;
44+
margin-bottom: 1px;
3645
37-
/* 2. Prevents visible overlap of partially transparent background colors.
46+
/* 2. Prevents visible overlap of partially transparent background colors.
3847
3948
'colors.input.disabledBg' happens to be partially transparent in light mode, so we use a
4049
transparent background-color on a disabled <select>. */
41-
&:disabled {
42-
background-color: transparent;
43-
}
50+
&:disabled {
51+
background-color: transparent;
52+
}
4453
45-
/* 3. Maintain dark bg color in Firefox on Windows high-contrast mode
54+
/* 3. Maintain dark bg color in Firefox on Windows high-contrast mode
4655
4756
Firefox makes the <select>'s background color white when setting 'background-color: transparent;' */
48-
@media screen and (forced-colors: active) {
49-
&:disabled {
50-
background-color: -moz-combobox;
57+
@media screen and (forced-colors: active) {
58+
&:disabled {
59+
background-color: -moz-combobox;
60+
}
5161
}
52-
}
53-
`
54-
55-
const ArrowIndicatorSVG: React.FC<React.PropsWithChildren<{className?: string}>> = ({className}) => (
56-
<svg
57-
aria-hidden="true"
58-
width="16"
59-
height="16"
60-
fill="currentColor"
61-
xmlns="http://www.w3.org/2000/svg"
62-
className={className}
63-
>
64-
<path d="m4.074 9.427 3.396 3.396a.25.25 0 0 0 .354 0l3.396-3.396A.25.25 0 0 0 11.043 9H4.251a.25.25 0 0 0-.177.427ZM4.074 7.47 7.47 4.073a.25.25 0 0 1 .354 0L11.22 7.47a.25.25 0 0 1-.177.426H4.251a.25.25 0 0 1-.177-.426Z" />
65-
</svg>
62+
`,
6663
)
6764

68-
const ArrowIndicator = styled(ArrowIndicatorSVG)`
65+
const ArrowIndicatorSVG: React.FC<React.PropsWithChildren<{className?: string}>> = ({className}) => {
66+
return (
67+
<svg
68+
aria-hidden="true"
69+
width="16"
70+
height="16"
71+
fill="currentColor"
72+
xmlns="http://www.w3.org/2000/svg"
73+
className={className}
74+
>
75+
<path d="m4.074 9.427 3.396 3.396a.25.25 0 0 0 .354 0l3.396-3.396A.25.25 0 0 0 11.043 9H4.251a.25.25 0 0 0-.177.427ZM4.074 7.47 7.47 4.073a.25.25 0 0 1 .354 0L11.22 7.47a.25.25 0 0 1-.177.426H4.251a.25.25 0 0 1-.177-.426Z" />
76+
</svg>
77+
)
78+
}
79+
80+
const StyledArrowIndicatorSVG = styled(ArrowIndicatorSVG)`
6981
pointer-events: none;
7082
position: absolute;
7183
right: ${arrowRightOffset};
7284
top: 50%;
7385
transform: translateY(-50%);
7486
`
7587

88+
const ArrowIndicator: React.FC<{className?: string}> = ({className}) => {
89+
const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG)
90+
if (enabled) {
91+
return <ArrowIndicatorSVG className={clsx(classes.ArrowIndicator, className)} />
92+
}
93+
94+
return <StyledArrowIndicatorSVG />
95+
}
96+
7697
const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
77-
({block, children, contrast, disabled, placeholder, size, required, validationStatus, ...rest}: SelectProps, ref) => (
78-
<TextInputWrapper
79-
sx={{
80-
overflow: 'hidden',
81-
position: 'relative',
82-
'@media screen and (forced-colors: active)': {
83-
svg: {
84-
fill: disabled ? 'GrayText' : 'FieldText',
98+
({block, children, contrast, disabled, placeholder, size, required, validationStatus, ...rest}: SelectProps, ref) => {
99+
const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG)
100+
if (enabled) {
101+
return (
102+
<TextInputWrapper
103+
block={block}
104+
contrast={contrast}
105+
disabled={disabled}
106+
size={size}
107+
validationStatus={validationStatus}
108+
className={classes.TextInputWrapper}
109+
sx={rest.sx}
110+
>
111+
<StyledSelect
112+
ref={ref}
113+
required={required}
114+
disabled={disabled}
115+
aria-invalid={validationStatus === 'error' ? 'true' : 'false'}
116+
data-hasplaceholder={Boolean(placeholder)}
117+
defaultValue={placeholder ?? undefined}
118+
className={clsx(classes.Select, disabled && classes.Disabled)}
119+
{...rest}
120+
>
121+
{placeholder && (
122+
<option value="" disabled={required} hidden={required}>
123+
{placeholder}
124+
</option>
125+
)}
126+
{children}
127+
</StyledSelect>
128+
<ArrowIndicator className={classes.ArrowIndicator} />
129+
</TextInputWrapper>
130+
)
131+
}
132+
133+
return (
134+
<TextInputWrapper
135+
sx={{
136+
overflow: 'hidden',
137+
position: 'relative',
138+
'@media screen and (forced-colors: active)': {
139+
svg: {
140+
fill: disabled ? 'GrayText' : 'FieldText',
141+
},
85142
},
86-
},
87-
}}
88-
block={block}
89-
contrast={contrast}
90-
disabled={disabled}
91-
size={size}
92-
validationStatus={validationStatus}
93-
>
94-
<StyledSelect
95-
ref={ref}
96-
required={required}
143+
}}
144+
block={block}
145+
contrast={contrast}
97146
disabled={disabled}
98-
aria-invalid={validationStatus === 'error' ? 'true' : 'false'}
99-
data-hasplaceholder={Boolean(placeholder)}
100-
defaultValue={placeholder ?? undefined}
101-
{...rest}
147+
size={size}
148+
validationStatus={validationStatus}
102149
>
103-
{placeholder && (
104-
<option value="" disabled={required} hidden={required}>
105-
{placeholder}
106-
</option>
107-
)}
108-
{children}
109-
</StyledSelect>
110-
<ArrowIndicator />
111-
</TextInputWrapper>
112-
),
150+
<StyledSelect
151+
ref={ref}
152+
required={required}
153+
disabled={disabled}
154+
aria-invalid={validationStatus === 'error' ? 'true' : 'false'}
155+
data-hasplaceholder={Boolean(placeholder)}
156+
defaultValue={placeholder ?? undefined}
157+
{...rest}
158+
>
159+
{placeholder && (
160+
<option value="" disabled={required} hidden={required}>
161+
{placeholder}
162+
</option>
163+
)}
164+
{children}
165+
</StyledSelect>
166+
<ArrowIndicator />
167+
</TextInputWrapper>
168+
)
169+
},
113170
)
114171

115172
const Option: React.FC<React.PropsWithChildren<React.HTMLProps<HTMLOptionElement> & {value: string}>> = props => (

packages/react/src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,9 @@ export {default as RadioGroup} from './RadioGroup'
137137
export type {RelativeTimeProps} from './RelativeTime'
138138
export {default as RelativeTime} from './RelativeTime'
139139
export {SegmentedControl} from './SegmentedControl'
140-
export {default as Select} from './Select'
141-
export type {SelectProps} from './Select'
140+
// Curently there is a duplicate Select component at the root of the dir, so need to be explicit about exporting from the src/Select dir
141+
export {default as Select} from './Select/Select'
142+
export type {SelectProps} from './Select/Select'
142143
export {SelectPanel} from './SelectPanel'
143144
export type {SelectPanelProps} from './SelectPanel'
144145
export {default as SideNav} from './SideNav'

0 commit comments

Comments
 (0)