diff --git a/.changeset/slow-spoons-peel.md b/.changeset/slow-spoons-peel.md new file mode 100644 index 00000000000..83dcb2f5943 --- /dev/null +++ b/.changeset/slow-spoons-peel.md @@ -0,0 +1,5 @@ +--- +"@primer/react": minor +--- + +Update `Token`, `IssueLabelToken`, `AvatarToken` components to use CSS Modules diff --git a/packages/react/src/Token/AvatarToken.module.css b/packages/react/src/Token/AvatarToken.module.css new file mode 100644 index 00000000000..7dc33bcfa68 --- /dev/null +++ b/packages/react/src/Token/AvatarToken.module.css @@ -0,0 +1,37 @@ +:root { + --spacing: calc(var(--base-size-4) * 2); +} + +.AvatarContainer { + display: block; +} + +/* TODO: Remove this once the avatar component is converted to CSS modules */ +.Avatar { + width: 100%; + height: 100%; +} + +.Token { + padding-left: var(--base-size-4) !important; +} + +.AvatarContainer:where([data-size='small']) { + width: calc(16px - var(--spacing)); + height: calc(16px - var(--spacing)); +} + +.AvatarContainer:where([data-size='medium']) { + width: calc(20px - var(--spacing)); + height: calc(20px - var(--spacing)); +} + +.AvatarContainer:where([data-size='large']) { + width: calc(24px - var(--spacing)); + height: calc(24px - var(--spacing)); +} + +.AvatarContainer:where([data-size='xlarge']) { + width: calc(32px - var(--spacing)); + height: calc(32px - var(--spacing)); +} diff --git a/packages/react/src/Token/AvatarToken.tsx b/packages/react/src/Token/AvatarToken.tsx index b03a1059356..c6b70005639 100644 --- a/packages/react/src/Token/AvatarToken.tsx +++ b/packages/react/src/Token/AvatarToken.tsx @@ -6,23 +6,51 @@ import {defaultTokenSize, tokenSizes} from './TokenBase' import Token from './Token' import Avatar from '../Avatar' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' +import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent' +import {useFeatureFlag} from '../FeatureFlags' +import classes from './AvatarToken.module.css' +import {clsx} from 'clsx' // TODO: update props to only accept 'large' and 'xlarge' on the next breaking change export interface AvatarTokenProps extends TokenBaseProps { avatarSrc: string } -const AvatarContainer = styled.span<{avatarSize: TokenSizeKeys}>` - // 'space.1' is used because to match space from the left of the token to the left of the avatar - // '* 2' is done to account for the top and bottom - --spacing: calc(${get('space.1')} * 2); +const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_team' - display: block; - height: ${props => `calc(${tokenSizes[props.avatarSize]} - var(--spacing))`}; - width: ${props => `calc(${tokenSizes[props.avatarSize]} - var(--spacing))`}; -` +const AvatarContainer = toggleStyledComponent( + CSS_MODULES_FEATURE_FLAG, + 'span', + styled.span<{avatarSize: TokenSizeKeys}>` + // 'space.1' is used because to match space from the left of the token to the left of the avatar + // '* 2' is done to account for the top and bottom + --spacing: calc(${get('space.1')} * 2); + + display: block; + height: ${props => `calc(${tokenSizes[props.avatarSize]} - var(--spacing))`}; + width: ${props => `calc(${tokenSizes[props.avatarSize]} - var(--spacing))`}; + `, +) + +const AvatarToken = forwardRef(({avatarSrc, id, size = defaultTokenSize, className, ...rest}, forwardedRef) => { + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) + if (enabled) { + return ( + ( + + + + )} + size={size} + id={id?.toString()} + className={clsx(classes.Token, className)} + {...rest} + ref={forwardedRef} + /> + ) + } -const AvatarToken = forwardRef(({avatarSrc, id, size = defaultTokenSize, ...rest}, forwardedRef) => { return ( ( diff --git a/packages/react/src/Token/IssueLabelToken.module.css b/packages/react/src/Token/IssueLabelToken.module.css new file mode 100644 index 00000000000..5543c107480 --- /dev/null +++ b/packages/react/src/Token/IssueLabelToken.module.css @@ -0,0 +1,8 @@ +.IssueLabel:where([data-has-remove-button='true']) { + padding-right: 0; +} + +.RemoveButton:where([data-has-multiple-action-targets='true']) { + position: relative; + z-index: 1; +} diff --git a/packages/react/src/Token/IssueLabelToken.tsx b/packages/react/src/Token/IssueLabelToken.tsx index 54b0b89ba53..a8b1f409d67 100644 --- a/packages/react/src/Token/IssueLabelToken.tsx +++ b/packages/react/src/Token/IssueLabelToken.tsx @@ -8,6 +8,9 @@ import {parseToHsla, parseToRgba} from 'color2k' import {useTheme} from '../ThemeProvider' import TokenTextContainer from './_TokenTextContainer' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' +import classes from './IssueLabelToken.module.css' +import {useFeatureFlag} from '../FeatureFlags' +import {clsx} from 'clsx' export interface IssueLabelTokenProps extends TokenBaseProps { /** @@ -16,6 +19,7 @@ export interface IssueLabelTokenProps extends TokenBaseProps { fillColor?: string } +const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_team' const tokenBorderWidthPx = 1 const lightModeStyles = { @@ -43,6 +47,8 @@ const darkModeStyles = { } const IssueLabelToken = forwardRef((props, forwardedRef) => { + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) + const { as, fillColor = '#999', @@ -54,6 +60,7 @@ const IssueLabelToken = forwardRef((props, forwardedRef) => { hideRemoveButton, href, onClick, + className, ...rest } = props const interactiveTokenProps = { @@ -61,6 +68,7 @@ const IssueLabelToken = forwardRef((props, forwardedRef) => { href, onClick, } + const {resolvedColorScheme} = useTheme() const hasMultipleActionTargets = isTokenInteractive(props) && Boolean(onRemove) && !hideRemoveButton const onRemoveClick: MouseEventHandler = e => { @@ -133,6 +141,37 @@ const IssueLabelToken = forwardRef((props, forwardedRef) => { } }, [fillColor, resolvedColorScheme, hideRemoveButton, onRemove, isSelected, props]) + if (enabled) { + return ( + + {text} + {!hideRemoveButton && onRemove ? ( + + ) : null} + + ) + } + return ( >> = ({children, size}) => ( @@ -35,6 +40,8 @@ const LeadingVisualContainer: React.FC { + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) + const { as, onRemove, @@ -46,6 +53,8 @@ const Token = forwardRef((props, forwardedRef) => { href, onClick, sx: sxProp = defaultSxProp, + className, + style, ...rest } = props const hasMultipleActionTargets = isTokenInteractive(props) && Boolean(onRemove) && !hideRemoveButton @@ -58,7 +67,8 @@ const Token = forwardRef((props, forwardedRef) => { href, onClick, } - const sx = merge( + + const mergedSx = merge( { backgroundColor: 'neutral.subtle', borderColor: props.isSelected ? 'fg.default' : 'border.subtle', @@ -80,16 +90,56 @@ const Token = forwardRef((props, forwardedRef) => { sxProp, ) + if (enabled) { + return ( + + {LeadingVisual ? ( + + + + ) : null} + + {text} + {onRemove && (press backspace or delete to remove)} + + + {!hideRemoveButton && onRemove ? ( + + ) : null} + + ) + } + return ( {LeadingVisual ? ( diff --git a/packages/react/src/Token/TokenBase.module.css b/packages/react/src/Token/TokenBase.module.css new file mode 100644 index 00000000000..d8da3654770 --- /dev/null +++ b/packages/react/src/Token/TokenBase.module.css @@ -0,0 +1,60 @@ +.TokenBase { + position: relative; + display: inline-flex; + font-family: inherit; + font-weight: var(--base-text-weight-semibold); + text-decoration: none; + white-space: nowrap; + border-radius: var(--borderRadius-full); + align-items: center; +} + +.TokenBase:where([data-cursor-is-interactive='true']) { + cursor: pointer; +} + +.TokenBase:where([data-cursor-is-interactive='false']) { + cursor: auto; +} + +.TokenBase:where([data-size='small']) { + width: auto; + height: 16px; + padding-right: var(--base-size-4); + padding-left: var(--base-size-4); + font-size: var(--text-body-size-small); + /* stylelint-disable-next-line primer/typography */ + line-height: 16px; +} + +.TokenBase:where([data-size='medium']) { + width: auto; + height: 20px; + padding-right: var(--base-size-8); + padding-left: var(--base-size-8); + font-size: var(--text-body-size-small); + /* stylelint-disable-next-line primer/typography */ + line-height: 20px; +} + +.TokenBase[data-size='large'] { + width: auto; + height: 24px; + padding-right: var(--base-size-8); + padding-left: var(--base-size-8); + font-size: var(--text-body-size-small); + /* stylelint-disable-next-line primer/typography */ + line-height: 24px; +} + +.TokenBase[data-size='xlarge'] { + width: auto; + height: 32px; + padding-top: 0; + padding-right: var(--base-size-16); + padding-bottom: 0; + padding-left: var(--base-size-16); + font-size: var(--text-body-size-medium); + /* stylelint-disable-next-line primer/typography */ + line-height: 32px; +} diff --git a/packages/react/src/Token/TokenBase.tsx b/packages/react/src/Token/TokenBase.tsx index 43b314367e8..89b3b28219a 100644 --- a/packages/react/src/Token/TokenBase.tsx +++ b/packages/react/src/Token/TokenBase.tsx @@ -2,10 +2,14 @@ import type {ComponentProps, KeyboardEvent} from 'react' import React from 'react' import styled from 'styled-components' import {variant} from 'styled-system' +import {clsx} from 'clsx' import {get} from '../constants' import type {SxProp} from '../sx' import sx from '../sx' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' +import {useFeatureFlag} from '../FeatureFlags' +import classes from './TokenBase.module.css' +import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent' export type TokenSizeKeys = 'small' | 'medium' | 'large' | 'xlarge' @@ -112,26 +116,54 @@ const variants = variant< }, }) -const StyledTokenBase = styled.span< - { - size?: TokenSizeKeys - } & SxProp ->` - align-items: center; - border-radius: 999px; - cursor: ${props => (isTokenInteractive(props) ? 'pointer' : 'auto')}; - display: inline-flex; - font-weight: ${get('fontWeights.bold')}; - font-family: inherit; - text-decoration: none; - position: relative; - white-space: nowrap; - ${variants} - ${sx} -` +const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_team' + +const StyledTokenBase = toggleStyledComponent( + CSS_MODULES_FEATURE_FLAG, + 'span', + styled.span< + { + size?: TokenSizeKeys + } & SxProp + >` + align-items: center; + border-radius: 999px; + cursor: ${props => (isTokenInteractive(props) ? 'pointer' : 'auto')}; + display: inline-flex; + font-weight: ${get('fontWeights.bold')}; + font-family: inherit; + text-decoration: none; + position: relative; + white-space: nowrap; + ${variants} + ${sx} + `, +) const TokenBase = React.forwardRef( - ({onRemove, onKeyDown, id, size = defaultTokenSize, ...rest}, forwardedRef) => { + ({onRemove, onKeyDown, id, className, size = defaultTokenSize, ...rest}, forwardedRef) => { + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) + + if (enabled) { + return ( + ) => { + onKeyDown && onKeyDown(event) + + if ((event.key === 'Backspace' || event.key === 'Delete') && onRemove) { + onRemove() + } + }} + className={clsx(classes.TokenBase, className)} + data-cursor-is-interactive={isTokenInteractive(rest)} + data-size={size} + id={id?.toString()} + {...rest} + ref={forwardedRef} + /> + ) + } + return ( ) => { @@ -144,7 +176,6 @@ const TokenBase = React.forwardRef )