Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Select): Convert Select component to CSS Modules behind feature flag #5194

Merged
merged 5 commits into from
Oct 31, 2024
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
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'
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the newest Select component

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 '.'
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get Select from the correct location

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'
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joshblack Should I explore deleting the src/Select.tsx file in a follow up PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be very much so appreciated if you have the time! 🙏

export type {SelectProps} from './Select/Select'
export {SelectPanel} from './SelectPanel'
export type {SelectPanelProps} from './SelectPanel'
export {default as SideNav} from './SideNav'
Expand Down
Loading