Skip to content

Commit

Permalink
feat(SegmentedControl): convert to CSS modules behind feature flag (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
hussam-i-am authored Dec 3, 2024
1 parent 82bf850 commit 59a6654
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/empty-crews-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": minor
---

Convert SegmentedControl to use CSS modules behind feature flag
199 changes: 199 additions & 0 deletions packages/react/src/SegmentedControl/SegmentedControl.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
.SegmentedControl {
display: inline-flex;

/* TODO: use primitive `control.{small|medium}.size` when it is available */
height: 32px;
padding: 0;
margin: 0;
font-size: var(--text-body-size-medium);
background-color: var(--controlTrack-bgColor-rest);
border: var(--borderWidth-thin) solid var(--controlTrack-borderColor-rest, transparent);
border-radius: var(--borderRadius-medium);

&:where([data-full-width]) {
display: flex;
width: 100%;
}

&:where([data-size='small']) {
/* TODO: use primitive `control.{small|medium}.size` when it is available */
height: 28px;
font-size: var(--text-body-size-small);
}
}

.Item {
position: relative;
display: block;
/* stylelint-disable-next-line primer/spacing */
margin-top: -1px;
/* stylelint-disable-next-line primer/spacing */
margin-bottom: -1px;
flex-grow: 1;

&:not(:last-child) {
/* stylelint-disable-next-line primer/spacing */
margin-right: 1px;

&::after {
position: absolute;
top: var(--base-size-8);
right: calc(-1 * var(--base-size-2));
bottom: var(--base-size-8);
width: 1px;
content: '';
/* stylelint-disable-next-line primer/colors */
background-color: var(--borderColor-default);
}

&:has(+ [data-selected])::after,
&:where([data-selected])::after {
background-color: transparent;
}
}

&:focus-within:has(:focus-visible) {
background-color: transparent;
}

&:first-child {
/* stylelint-disable-next-line primer/spacing */
margin-left: -1px;
}

&:last-child {
/* stylelint-disable-next-line primer/spacing */
margin-right: -1px;
}
}

.Button {
/* TODO: use primitive `primer.control.medium.paddingInline.normal` when it is available */
--segmented-control-button-inner-padding: 12px;
--segmented-control-button-bg-inset: 4px;
--segmented-control-outer-radius: var(--borderRadius-medium);

width: 100%;
height: 100%;
/* stylelint-disable-next-line primer/spacing */
padding: var(--segmented-control-button-bg-inset);
font-family: inherit;
font-size: inherit;
font-weight: var(--base-text-weight-normal);
color: currentColor;
cursor: pointer;
background-color: transparent;
border-color: transparent;
border-width: 0;
/* stylelint-disable-next-line primer/borders */
border-radius: var(--segmented-control-outer-radius);

& svg {
fill: var(--fgColor-muted);
}

/* fallback :focus state */
&:focus:not(:disabled) {
outline: var(--base-size-2) solid var(--fgColor-accent);
outline-offset: -1px;
box-shadow: none;

/* remove fallback :focus if :focus-visible is supported */
&:not(:focus-visible) {
outline: solid 1px transparent;
}
}

/* default focus state */
&:focus-visible:not(:disabled) {
outline: var(--base-size-2) solid var(--fgColor-accent);
outline-offset: -1px;
box-shadow: none;
}

/* stylelint-disable-next-line selector-max-specificity */
&:focus:focus-visible:not(:last-child)::after {
/* fixes an issue where the focus outline shows over the pseudo-element */
width: 0;
}

@media (pointer: coarse) {
&::before {
position: absolute;
top: 50%;
right: 0;
left: 0;
min-height: 44px;
content: '';
transform: translateY(-50%);
}
}
}

.IconButton {
/* TODO: use primitive `control.medium.size` when it is available instead of '32px' */
width: 32px;

.SegmentedControl:where([data-full-width]) & {
width: 100%;
}
}

.Content {
display: flex;
height: 100%;
/* stylelint-disable-next-line primer/spacing */
padding-right: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset));
/* stylelint-disable-next-line primer/spacing */
padding-left: calc(var(--segmented-control-button-inner-padding) - var(--segmented-control-button-bg-inset));
background-color: transparent;
border-color: transparent;
border-style: solid;
border-width: var(--borderWidth-thin);

/*
innerRadius = outerRadius - distance/2
https://stackoverflow.com/questions/2932146/math-problem-determine-the-corner-radius-of-an-inner-border-based-on-outer-corn
*/
/* stylelint-disable-next-line primer/borders */
border-radius: calc(var(--segmented-control-outer-radius) - var(--segmented-control-button-bg-inset) / 2);
align-items: center;
justify-content: center;
}

.Button[aria-current='true'] {
padding: 0;
font-weight: var(--base-text-weight-semibold);

.Content {
/* stylelint-disable-next-line primer/spacing */
padding-right: var(--segmented-control-button-inner-padding);
/* stylelint-disable-next-line primer/spacing */
padding-left: var(--segmented-control-button-inner-padding);
background-color: var(--controlKnob-bgColor-rest);
border-color: var(--controlKnob-borderColor-rest);
/* stylelint-disable-next-line primer/borders */
border-radius: var(--segmented-control-outer-radius);
}
}

.Button:not([aria-current='true']) {
&:hover .Content {
background-color: var(--controlTrack-bgColor-hover);
}

&:active .Content {
background-color: var(--controlTrack-bgColor-active);
}
}

.Text::after {
display: block;
height: 0;
overflow: hidden;
font-weight: var(--base-text-weight-semibold);
pointer-events: none;
visibility: hidden;
content: attr(data-text);
user-select: none;
}
25 changes: 21 additions & 4 deletions packages/react/src/SegmentedControl/SegmentedControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,21 @@ import styled from 'styled-components'
import {defaultSxProp} from '../utils/defaultSxProp'
import {isElement} from 'react-is'

import classes from './SegmentedControl.module.css'

import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent'
import {useFeatureFlag} from '../FeatureFlags'
import {clsx} from 'clsx'
import {SEGMENTED_CONTROL_CSS_MODULES_FEATURE_FLAG} from './getSegmentedControlStyles'

// Needed because passing a ref to `Box` causes a type error
const SegmentedControlList = styled.ul`
${sx};
`
const SegmentedControlList = toggleStyledComponent(
SEGMENTED_CONTROL_CSS_MODULES_FEATURE_FLAG,
'ul',
styled.ul`
${sx};
`,
)

type SegmentedControlProps = {
'aria-label'?: string
Expand Down Expand Up @@ -57,6 +68,7 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
size,
sx: sxProp = defaultSxProp,
variant = 'default',
className,
...rest
}) => {
const segmentedControlContainerRef = useRef<HTMLUListElement>(null)
Expand Down Expand Up @@ -117,7 +129,9 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({

return React.isValidElement<SegmentedControlIconButtonProps>(childArg) ? childArg.props['aria-label'] : null
}
const listSx = merge(getSegmentedControlStyles({isFullWidth, size}), sxProp as SxProp)

const enabled = useFeatureFlag(SEGMENTED_CONTROL_CSS_MODULES_FEATURE_FLAG)
const listSx = enabled ? sxProp : merge(getSegmentedControlStyles({isFullWidth, size}), sxProp as SxProp)

if (!ariaLabel && !ariaLabelledby) {
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -174,6 +188,9 @@ const Root: React.FC<React.PropsWithChildren<SegmentedControlProps>> = ({
aria-label={ariaLabel}
aria-labelledby={ariaLabelledby}
ref={segmentedControlContainerRef}
className={clsx(enabled && classes.SegmentedControl, className)}
data-full-width={isFullWidth || undefined}
data-size={size}
{...rest}
>
{React.Children.map(children, (child, index) => {
Expand Down
38 changes: 28 additions & 10 deletions packages/react/src/SegmentedControl/SegmentedControlButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@ import styled from 'styled-components'
import Box from '../Box'
import type {SxProp} from '../sx'
import sx, {merge} from '../sx'
import {getSegmentedControlButtonStyles, getSegmentedControlListItemStyles} from './getSegmentedControlStyles'
import {
getSegmentedControlButtonStyles,
getSegmentedControlListItemStyles,
SEGMENTED_CONTROL_CSS_MODULES_FEATURE_FLAG,
} from './getSegmentedControlStyles'
import {defaultSxProp} from '../utils/defaultSxProp'
import {isElement} from 'react-is'
import getGlobalFocusStyles from '../internal/utils/getGlobalFocusStyles'
import {useFeatureFlag} from '../FeatureFlags'

import classes from './SegmentedControl.module.css'
import {clsx} from 'clsx'
import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent'

export type SegmentedControlButtonProps = {
/** The visible label rendered in the button */
Expand All @@ -22,31 +31,40 @@ export type SegmentedControlButtonProps = {
} & SxProp &
ButtonHTMLAttributes<HTMLButtonElement | HTMLLIElement>

const SegmentedControlButtonStyled = styled.button`
${getGlobalFocusStyles('-1px')};
${sx};
`
const SegmentedControlButtonStyled = toggleStyledComponent(
SEGMENTED_CONTROL_CSS_MODULES_FEATURE_FLAG,
'button',
styled.button`
${getGlobalFocusStyles('-1px')};
${sx};
`,
)

const SegmentedControlButton: React.FC<React.PropsWithChildren<SegmentedControlButtonProps>> = ({
children,
leadingIcon: LeadingIcon,
selected,
sx: sxProp = defaultSxProp,
className,
...rest
}) => {
const mergedSx = merge(getSegmentedControlListItemStyles(), sxProp as SxProp)
const enabled = useFeatureFlag(SEGMENTED_CONTROL_CSS_MODULES_FEATURE_FLAG)
const mergedSx = enabled ? sxProp : merge(getSegmentedControlListItemStyles(), sxProp as SxProp)

return (
<Box as="li" sx={mergedSx}>
<Box as="li" sx={mergedSx} className={clsx(enabled && classes.Item)} data-selected={selected || undefined}>
<SegmentedControlButtonStyled
aria-current={selected}
sx={getSegmentedControlButtonStyles({selected, children})}
sx={enabled ? undefined : getSegmentedControlButtonStyles({selected, children})}
className={clsx(enabled && classes.Button, className)}
type="button"
{...rest}
>
<span className="segmentedControl-content">
<span className={clsx(enabled ? classes.Content : 'segmentedControl-content')}>
{LeadingIcon && <Box mr={1}>{isElement(LeadingIcon) ? LeadingIcon : <LeadingIcon />}</Box>}
<Box className="segmentedControl-text">{children}</Box>
<Box className={clsx(enabled ? classes.Text : 'segmentedControl-text')} data-text={children}>
{children}
</Box>
</span>
</SegmentedControlButtonStyled>
</Box>
Expand Down
Loading

0 comments on commit 59a6654

Please sign in to comment.