Skip to content

Commit

Permalink
feat(Select): Convert Select component to CSS Modules behind featur…
Browse files Browse the repository at this point in the history
…e 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
  • Loading branch information
randall-krauskopf authored Oct 31, 2024
1 parent 8138dee commit 719def7
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 78 deletions.
5 changes: 5 additions & 0 deletions .changeset/modern-icons-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Migrate `Select` component to css modules
2 changes: 1 addition & 1 deletion packages/react/src/FormControl/FormControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Autocomplete from '../Autocomplete'
import Box from '../Box'
import Checkbox from '../Checkbox'
import Radio from '../Radio'
import Select from '../Select'
import Select from '../Select/Select'
import {SelectPanel} from '../SelectPanel'
import TextInput from '../TextInput'
import TextInputWithTokens from '../TextInputWithTokens'
Expand Down
25 changes: 25 additions & 0 deletions packages/react/src/Select/Select.dev.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react'
import type {Meta} from '@storybook/react'
import {FormControl, Box} from '..'
import Select from './Select'

export default {
title: 'Components/Select/Dev',
component: Select,
} as Meta

export const Default = () => (
<Box as="form">
<FormControl>
<FormControl.Label>Default label</FormControl.Label>
<Select sx={{color: 'danger.fg'}}>
<Select.Option value="one">Choice one</Select.Option>
<Select.Option value="two">Choice two</Select.Option>
<Select.Option value="three">Choice three</Select.Option>
<Select.Option value="four">Choice four</Select.Option>
<Select.Option value="five">Choice five</Select.Option>
<Select.Option value="six">Choice six</Select.Option>
</Select>
</FormControl>
</Box>
)
3 changes: 2 additions & 1 deletion packages/react/src/Select/Select.features.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react'
import {Select, FormControl, Box, Heading} from '..'
import {FormControl, Box, Heading} from '..'
import Select from './Select'

export default {
title: 'Components/Select/Features',
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/Select/Select.figma.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import {Select} from '../../src'
import Select from '.'
import FormControl from '../FormControl'
import figma from '@figma/code-connect'

Expand Down
67 changes: 67 additions & 0 deletions packages/react/src/Select/Select.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
.Select {
width: 100%;
/* stylelint-disable-next-line primer/spacing */
margin-top: 1px;
/* stylelint-disable-next-line primer/spacing */
margin-bottom: 1px;
/* stylelint-disable-next-line primer/spacing */
margin-left: 1px;
font-size: inherit;
color: currentColor;

/* Firefox hacks:
* 1. Makes Firefox's native dropdown menu's background match the theme.
* background-color should be 'transparent', but Firefox uses the background-color on
* <select> to determine the background color used for the dropdown menu.
* 2. Adds 1px margins to the <select> so the background color doesn't hide the focus outline created with an inset box-shadow.
*/
background-color: inherit;
border: 0;
border-radius: inherit;
outline: none;
appearance: none;

/* 2. Prevents visible overlap of partially transparent background colors.
* 'colors.input.disabledBg' happens to be partially transparent in light mode, so we use a
* transparent background-color on a disabled <select>.
*/
&:disabled {
background-color: transparent;
}

/* 3. Maintain dark bg color in Firefox on Windows high-contrast mode
* Firefox makes the <select>'s background color white when setting 'background-color: transparent;'
*/
@media screen and (forced-colors: active) {
&:disabled {
background-color: -moz-combobox;
}
}
}

.TextInputWrapper {
position: relative;
overflow: hidden;

@media screen and (forced-colors: active) {
svg {
fill: 'FieldText';
}
}
}

.disabled {
@media screen and (forced-colors: active) {
svg {
fill: 'GrayText';
}
}
}

.ArrowIndicator {
position: absolute;
top: 50%;
right: var(--base-size-4);
pointer-events: none;
transform: translateY(-50%);
}
5 changes: 3 additions & 2 deletions packages/react/src/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react'
import type {Meta} from '@storybook/react'
import {Select, FormControl, Box} from '..'
import type {SelectProps} from '../Select'
import {FormControl, Box} from '..'
import Select from './Select'
import type {SelectProps} from './Select'
import type {FormControlArgs} from '../utils/form-story-helpers'
import {
formControlArgs,
Expand Down
199 changes: 128 additions & 71 deletions packages/react/src/Select/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import React from 'react'
import styled from 'styled-components'
import {clsx} from 'clsx'
import type {StyledWrapperProps} from '../internal/components/TextInputWrapper'
import TextInputWrapper from '../internal/components/TextInputWrapper'
import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent'
import {useFeatureFlag} from '../FeatureFlags'
import classes from './Select.module.css'

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

const arrowRightOffset = '4px'
const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_team'

const StyledSelect = styled.select`
appearance: none;
border-radius: inherit;
border: 0;
color: currentColor;
font-size: inherit;
outline: none;
width: 100%;
const arrowRightOffset = '4px'

/* Firefox hacks: */
/* 1. Makes Firefox's native dropdown menu's background match the theme.
const StyledSelect = toggleStyledComponent(
CSS_MODULES_FEATURE_FLAG,
'select',
styled.select`
appearance: none;
border-radius: inherit;
border: 0;
color: currentColor;
font-size: inherit;
outline: none;
width: 100%;
/* Firefox hacks: */
/* 1. Makes Firefox's native dropdown menu's background match the theme.
background-color should be 'transparent', but Firefox uses the background-color on
<select> to determine the background color used for the dropdown menu.
2. Adds 1px margins to the <select> so the background color doesn't hide the focus outline created with an inset box-shadow.
*/
background-color: inherit;
margin-top: 1px;
margin-left: 1px;
margin-bottom: 1px;
background-color: inherit;
margin-top: 1px;
margin-left: 1px;
margin-bottom: 1px;
/* 2. Prevents visible overlap of partially transparent background colors.
/* 2. Prevents visible overlap of partially transparent background colors.
'colors.input.disabledBg' happens to be partially transparent in light mode, so we use a
transparent background-color on a disabled <select>. */
&:disabled {
background-color: transparent;
}
&:disabled {
background-color: transparent;
}
/* 3. Maintain dark bg color in Firefox on Windows high-contrast mode
/* 3. Maintain dark bg color in Firefox on Windows high-contrast mode
Firefox makes the <select>'s background color white when setting 'background-color: transparent;' */
@media screen and (forced-colors: active) {
&:disabled {
background-color: -moz-combobox;
@media screen and (forced-colors: active) {
&:disabled {
background-color: -moz-combobox;
}
}
}
`

const ArrowIndicatorSVG: React.FC<React.PropsWithChildren<{className?: string}>> = ({className}) => (
<svg
aria-hidden="true"
width="16"
height="16"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<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" />
</svg>
`,
)

const ArrowIndicator = styled(ArrowIndicatorSVG)`
const ArrowIndicatorSVG: React.FC<React.PropsWithChildren<{className?: string}>> = ({className}) => {
return (
<svg
aria-hidden="true"
width="16"
height="16"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<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" />
</svg>
)
}

const StyledArrowIndicatorSVG = styled(ArrowIndicatorSVG)`
pointer-events: none;
position: absolute;
right: ${arrowRightOffset};
top: 50%;
transform: translateY(-50%);
`

const ArrowIndicator: React.FC<{className?: string}> = ({className}) => {
const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG)
if (enabled) {
return <ArrowIndicatorSVG className={clsx(classes.ArrowIndicator, className)} />
}

return <StyledArrowIndicatorSVG />
}

const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({block, children, contrast, disabled, placeholder, size, required, validationStatus, ...rest}: SelectProps, ref) => (
<TextInputWrapper
sx={{
overflow: 'hidden',
position: 'relative',
'@media screen and (forced-colors: active)': {
svg: {
fill: disabled ? 'GrayText' : 'FieldText',
({block, children, contrast, disabled, placeholder, size, required, validationStatus, ...rest}: SelectProps, ref) => {
const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG)
if (enabled) {
return (
<TextInputWrapper
block={block}
contrast={contrast}
disabled={disabled}
size={size}
validationStatus={validationStatus}
className={classes.TextInputWrapper}
sx={rest.sx}
>
<StyledSelect
ref={ref}
required={required}
disabled={disabled}
aria-invalid={validationStatus === 'error' ? 'true' : 'false'}
data-hasplaceholder={Boolean(placeholder)}
defaultValue={placeholder ?? undefined}
className={clsx(classes.Select, disabled && classes.Disabled)}
{...rest}
>
{placeholder && (
<option value="" disabled={required} hidden={required}>
{placeholder}
</option>
)}
{children}
</StyledSelect>
<ArrowIndicator className={classes.ArrowIndicator} />
</TextInputWrapper>
)
}

return (
<TextInputWrapper
sx={{
overflow: 'hidden',
position: 'relative',
'@media screen and (forced-colors: active)': {
svg: {
fill: disabled ? 'GrayText' : 'FieldText',
},
},
},
}}
block={block}
contrast={contrast}
disabled={disabled}
size={size}
validationStatus={validationStatus}
>
<StyledSelect
ref={ref}
required={required}
}}
block={block}
contrast={contrast}
disabled={disabled}
aria-invalid={validationStatus === 'error' ? 'true' : 'false'}
data-hasplaceholder={Boolean(placeholder)}
defaultValue={placeholder ?? undefined}
{...rest}
size={size}
validationStatus={validationStatus}
>
{placeholder && (
<option value="" disabled={required} hidden={required}>
{placeholder}
</option>
)}
{children}
</StyledSelect>
<ArrowIndicator />
</TextInputWrapper>
),
<StyledSelect
ref={ref}
required={required}
disabled={disabled}
aria-invalid={validationStatus === 'error' ? 'true' : 'false'}
data-hasplaceholder={Boolean(placeholder)}
defaultValue={placeholder ?? undefined}
{...rest}
>
{placeholder && (
<option value="" disabled={required} hidden={required}>
{placeholder}
</option>
)}
{children}
</StyledSelect>
<ArrowIndicator />
</TextInputWrapper>
)
},
)

const Option: React.FC<React.PropsWithChildren<React.HTMLProps<HTMLOptionElement> & {value: string}>> = props => (
Expand Down
5 changes: 3 additions & 2 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,9 @@ export {default as RadioGroup} from './RadioGroup'
export type {RelativeTimeProps} from './RelativeTime'
export {default as RelativeTime} from './RelativeTime'
export {SegmentedControl} from './SegmentedControl'
export {default as Select} from './Select'
export type {SelectProps} from './Select'
// 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
export {default as Select} from './Select/Select'
export type {SelectProps} from './Select/Select'
export {SelectPanel} from './SelectPanel'
export type {SelectPanelProps} from './SelectPanel'
export {default as SideNav} from './SideNav'
Expand Down

0 comments on commit 719def7

Please sign in to comment.