From cdc4da128895f4f63a526748fd4df63c9f27d055 Mon Sep 17 00:00:00 2001 From: Hussam Ghazzi Date: Mon, 25 Nov 2024 10:53:02 -0500 Subject: [PATCH] feat(SelectPanel2): Convert SelectPanel2 to CSS modules (#5325) * feat(SelectPanel): Convert SelectPanel to CSS modules behind feature flag * update changeset * style fixes * fix icon color * readd sx * update order --- .changeset/cyan-boxes-peel.md | 5 + .../SelectPanel2/SelectPanel.module.css | 273 +++++++++++ .../experimental/SelectPanel2/SelectPanel.tsx | 422 ++++++++++++------ 3 files changed, 567 insertions(+), 133 deletions(-) create mode 100644 .changeset/cyan-boxes-peel.md create mode 100644 packages/react/src/experimental/SelectPanel2/SelectPanel.module.css diff --git a/.changeset/cyan-boxes-peel.md b/.changeset/cyan-boxes-peel.md new file mode 100644 index 00000000000..2331fb6e904 --- /dev/null +++ b/.changeset/cyan-boxes-peel.md @@ -0,0 +1,5 @@ +--- +"@primer/react": minor +--- + +Convert SelectPanel2 to CSS modules diff --git a/packages/react/src/experimental/SelectPanel2/SelectPanel.module.css b/packages/react/src/experimental/SelectPanel2/SelectPanel.module.css new file mode 100644 index 00000000000..4a7feda9c3d --- /dev/null +++ b/packages/react/src/experimental/SelectPanel2/SelectPanel.module.css @@ -0,0 +1,273 @@ +.Overlay { + padding: 0; + color: var(--fgColor-default); + border: none; + + /* CSS variables values are passed in via styles */ + --max-height: 0; + --position-top: 0; + --position-left: 0; + + &:where([open]) { + display: flex; /* to fit children */ + } + + &:where([data-variant='anchored']), + &:where([data-variant='full-screen']) { + /* stylelint-disable-next-line primer/spacing */ + top: var(--position-top); + /* stylelint-disable-next-line primer/spacing */ + left: var(--position-left); + margin: 0; + + &::backdrop { + background-color: transparent; + } + } + + &:where([data-variant='modal'])::backdrop { + background-color: var(--overlay-backdrop-bgColor); + } + + &:where([data-variant='full-screen']) { + top: 0; + left: 0; + width: 100%; + max-width: 100vw; + height: 100%; + max-height: 100vh; + margin: 0; + border-radius: unset; + } + + &:where([data-variant='bottom-sheet']) { + top: auto; + bottom: 0; + left: 0; + width: 100%; + max-width: 100vw; + max-height: calc(100vh - 64px); + margin: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } +} + +.Form { + display: flex; + width: 100%; + flex-direction: column; +} + +.Container { + display: flex; + overflow: hidden; + flex-direction: column; + flex-shrink: 1; + flex-grow: 1; + justify-content: space-between; + + ul { + overflow-y: auto; + flex-grow: 1; + } +} + +.HeaderContent { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0; + + &:where([data-description]) { + align-items: flex-start; + } + + &:where([data-search-input]) { + margin-bottom: var(--base-size-8); + } +} + +.TitleWrapper { + margin-top: 0; + margin-left: var(--base-size-8); + + &:where([data-description]) { + /* stylelint-disable-next-line primer/spacing */ + margin-top: 2px; + } + + &:where([data-on-back]) { + margin-left: var(--base-size-4); + } +} + +.TextInput { + padding-left: var(--base-size-8) !important; + + &:has(input:placeholder-shown) :global(.TextInput-action) { + display: none; + } +} + +.Checkbox { + margin-top: 0; +} + +.FlexBox { + display: flex; +} + +.Title { + font-size: var(--text-body-size-medium); + font-weight: var(--base-text-weight-semibold); +} + +.Description { + display: block; + font-size: var(--text-body-size-small); + color: var(--fgColor-muted); +} + +.ClearAction { + color: var(--fgColor-muted); + background: none; +} + +.Footer { + display: flex; + min-height: 44px; + padding: var(--base-size-16); + border-top: var(--borderWidth-thin) solid; + border-top-color: var(--borderColor-default); + justify-content: space-between; + align-items: center; + flex-shrink: 0; + + &:where([data-hide-primary-actions]) { + padding: var(--base-size-8); + } +} + +.FooterContent { + flex-grow: 0; + + &:where([data-hide-primary-actions]) { + flex-grow: 1; + } +} + +.FooterActions { + display: flex; + gap: var(--stack-gap-condensed); +} + +.SecondaryCheckbox { + display: flex; + align-items: center; + gap: var(--stack-gap-condensed); +} + +.SmallText { + font-size: var(--text-body-size-small); +} + +.SelectPanelLoading { + display: flex; + height: 100%; + + /* maxHeight of dialog - (header & footer) */ + min-height: min(calc(var(--max-height) - 150px), 324px); + flex-direction: column; + justify-content: center; + align-items: center; + gap: var(--stack-gap-normal); +} + +.LoadingText { + font-size: var(--text-body-size-medium); + color: var(--fgColor-muted); +} + +.MessageFull { + display: flex; + height: 100%; + + /* maxHeight of dialog - (header & footer) */ + min-height: min(calc(var(--max-height) - 150px), 324px); + padding-right: var(--base-size-24); + padding-left: var(--base-size-24); + text-align: center; + flex-direction: column; + justify-content: center; + align-items: center; + flex-grow: 1; + gap: var(--base-size-4); + + a { + color: inherit; + text-decoration: underline; + } +} + +.Octicon { + margin-bottom: var(--base-size-8); + + &.Error { + color: var(--fgColor-danger); + } + + &.Warning { + color: var(--fgColor-attention); + } +} + +.MessageTitle { + font-size: var(--text-body-size-medium); + font-weight: var(--base-text-weight-medium); +} + +.MessageContent { + display: flex; + font-size: var(--text-body-size-medium); + color: var(--fgColor-muted); + flex-direction: column; + gap: var(--stack-gap-condensed); + align-items: center; +} + +.MessageInline { + display: flex; + padding-top: var(--base-size-12); + padding-right: var(--base-size-16); + padding-bottom: var(--base-size-12); + padding-left: var(--base-size-16); + font-size: var(--text-body-size-small); + border-bottom: var(--borderWidth-thin) solid; + gap: var(--stack-gap-condensed); + + a { + color: inherit; + text-decoration: underline; + } + + &:where([data-variant='error']) { + color: var(--fgColor-danger); + background-color: var(--bgColor-danger-muted); + border-color: var(--borderColor-danger-muted); + } + + &:where([data-variant='warning']) { + color: var(--fgColor-attention); + background-color: var(--bgColor-attention-muted); + border-color: var(--borderColor-attention-muted); + } +} + +.Header { + display: flex; + padding: var(--base-size-8); + flex-direction: column; + border-bottom: var(--borderWidth-thin) solid; + border-bottom-color: var(--borderColor-default); +} diff --git a/packages/react/src/experimental/SelectPanel2/SelectPanel.tsx b/packages/react/src/experimental/SelectPanel2/SelectPanel.tsx index ac53c6a4aac..6cffe9375e3 100644 --- a/packages/react/src/experimental/SelectPanel2/SelectPanel.tsx +++ b/packages/react/src/experimental/SelectPanel2/SelectPanel.tsx @@ -25,6 +25,12 @@ import {invariant} from '../../utils/invariant' import {AriaStatus} from '../../live-region' import {useResponsiveValue} from '../../hooks/useResponsiveValue' import type {ResponsiveValue} from '../../hooks/useResponsiveValue' +import {clsx} from 'clsx' +import {useFeatureFlag} from '../../FeatureFlags' + +import classes from './SelectPanel.module.css' + +const CSS_MODULES_FEATURE_FLAG = 'primer_react_css_modules_team' const SelectPanelContext = React.createContext<{ title: string @@ -93,6 +99,7 @@ const Panel: React.FC = ({ ...props }) => { const [internalOpen, setInternalOpen] = React.useState(defaultOpen) + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) const responsiveVariants = Object.assign( {regular: 'anchored', narrow: 'full-screen'}, // defaults @@ -231,6 +238,13 @@ const Panel: React.FC = ({ */ const onClickOutside = onInternalCancel + let maxHeightValue = heightMap[maxHeight] + if (currentVariant === 'bottom-sheet') { + maxHeightValue = 'calc(100vh - 64px)' + } else if (currentVariant === 'full-screen') { + maxHeightValue = '100vh' + } + return ( <> {Anchor} @@ -244,47 +258,61 @@ const Panel: React.FC = ({ height="fit-content" maxHeight={maxHeight} data-variant={currentVariant} - sx={{ - '--max-height': heightMap[maxHeight], - // reset dialog default styles - border: 'none', - padding: 0, - color: 'fg.default', - '&[open]': {display: 'flex'}, // to fit children - - '&[data-variant="anchored"], &[data-variant="full-screen"]': { - margin: 0, - top: position?.top, - left: position?.left, - '::backdrop': {backgroundColor: 'transparent'}, - }, - '&[data-variant="modal"]': { - '::backdrop': {backgroundColor: 'primer.canvas.backdrop'}, - }, - '&[data-variant="full-screen"]': { - margin: 0, - top: 0, - left: 0, - width: '100%', - maxWidth: '100vw', - height: '100%', - maxHeight: '100vh', - '--max-height': '100vh', - borderRadius: 'unset', - }, - '&[data-variant="bottom-sheet"]': { - margin: 0, - top: 'auto', - bottom: 0, - left: 0, - width: '100%', - maxWidth: '100vw', - maxHeight: 'calc(100vh - 64px)', - '--max-height': 'calc(100vh - 64px)', - borderBottomRightRadius: 0, - borderBottomLeftRadius: 0, - }, - }} + sx={ + enabled + ? undefined + : { + '--max-height': heightMap[maxHeight], + // reset dialog default styles + border: 'none', + padding: 0, + color: 'fg.default', + '&[open]': {display: 'flex'}, // to fit children + + '&[data-variant="anchored"], &[data-variant="full-screen"]': { + margin: 0, + top: position?.top, + left: position?.left, + '::backdrop': {backgroundColor: 'transparent'}, + }, + '&[data-variant="modal"]': { + '::backdrop': {backgroundColor: 'primer.canvas.backdrop'}, + }, + '&[data-variant="full-screen"]': { + margin: 0, + top: 0, + left: 0, + width: '100%', + maxWidth: '100vw', + height: '100%', + maxHeight: '100vh', + '--max-height': '100vh', + borderRadius: 'unset', + }, + '&[data-variant="bottom-sheet"]': { + margin: 0, + top: 'auto', + bottom: 0, + left: 0, + width: '100%', + maxWidth: '100vw', + maxHeight: 'calc(100vh - 64px)', + '--max-height': 'calc(100vh - 64px)', + borderBottomRightRadius: 0, + borderBottomLeftRadius: 0, + }, + } + } + style={ + enabled + ? ({ + '--max-height': maxHeightValue, + '--position-top': `${position?.top ?? 0}px`, + '--position-left': `${position?.left ?? 0}px`, + } as React.CSSProperties) + : undefined + } + className={enabled ? classes.Overlay : undefined} {...props} onClick={event => { if (event.target === event.currentTarget) onClickOutside() @@ -309,21 +337,27 @@ const Panel: React.FC = ({ as="form" method="dialog" onSubmit={onInternalSubmit} - sx={{display: 'flex', flexDirection: 'column', width: '100%'}} + sx={enabled ? undefined : {display: 'flex', flexDirection: 'column', width: '100%'}} + className={enabled ? classes.Form : undefined} > {slots.header ?? /* render default header as fallback */ } ((prop } }) -const SelectPanelHeader: React.FC void}> = ({children, onBack, ...props}) => { +const SelectPanelHeader: React.FC & {onBack?: () => void}> = ({ + children, + onBack, + className, + ...props +}) => { const [slots, childrenWithoutSlots] = useSlots(children, { searchInput: SelectPanelSearchInput, }) const {title, description, panelId, onCancel, onClearSelection} = React.useContext(SelectPanelContext) + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) return ( - + {onBack ? ( void /> ) : null} - + {/* heading element is intentionally hardcoded to h1, it is not customisable see https://github.com/github/primer/issues/2578 for context */} - + {title} {description ? ( - + {description} ) : null} - +
{onClearSelection ? ( void /> ) : null} onCancel()} /> - +
{slots.searchInput} @@ -448,10 +514,12 @@ const SelectPanelHeader: React.FC void const SelectPanelSearchInput: React.FC = ({ onChange: propsOnChange, onKeyDown: propsOnKeyDown, + className, ...props }) => { // TODO: use forwardedRef const inputRef = React.createRef() + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) const {setSearchQuery, moveFocusToList} = React.useContext(SelectPanelContext) @@ -482,7 +550,8 @@ const SelectPanelSearchInput: React.FC = ({ icon={XCircleFillIcon} aria-label="Clear" tooltipDirection="w" - sx={{color: 'fg.subtle', bg: 'none'}} + sx={enabled ? undefined : {color: 'fg.subtle', bg: 'none'}} + className={enabled ? classes.ClearAction : undefined} onClick={() => { if (inputRef.current) inputRef.current.value = '' if (typeof propsOnChange === 'function') { @@ -492,10 +561,15 @@ const SelectPanelSearchInput: React.FC = ({ }} /> } - sx={{ - paddingLeft: 2, // align with list checkboxes - '&:has(input:placeholder-shown) .TextInput-action': {display: 'none'}, - }} + sx={ + enabled + ? undefined + : { + paddingLeft: 2, // align with list checkboxes + '&:has(input:placeholder-shown) .TextInput-action': {display: 'none'}, + } + } + className={clsx(enabled ? classes.TextInput : undefined, className)} onChange={internalOnChange} onKeyDown={internalKeyDown} {...props} @@ -509,6 +583,7 @@ const SelectPanelFooter = ({...props}) => { const hidePrimaryActions = selectionVariant === 'instant' const buttonSize = useResponsiveValue(responsiveButtonSizes, 'small') + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) if (hidePrimaryActions && !props.children) { // nothing to render @@ -519,21 +594,36 @@ const SelectPanelFooter = ({...props}) => { return ( - {props.children} + + {props.children} + {hidePrimaryActions ? null : ( - + @@ -552,19 +642,30 @@ const SecondaryButton: React.FC = props => { return ) } -const SecondaryCheckbox: React.FC = ({id, children, ...props}) => { +const SecondaryCheckbox: React.FC = ({id, children, className, ...props}) => { const checkboxId = useId(id) const {selectionVariant} = React.useContext(SelectPanelContext) + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) // Checkbox should not be used with instant selection invariant( @@ -573,9 +674,21 @@ const SecondaryCheckbox: React.FC = ({id, children, ...props}) => ) return ( - - - + + + {children} @@ -602,22 +715,34 @@ const SelectPanelSecondaryAction: React.FC = ({ } const SelectPanelLoading = ({children = 'Fetching items...'}: React.PropsWithChildren) => { + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) + return ( - {children} + + {children} + ) } @@ -641,31 +766,56 @@ const SelectPanelMessage: React.FC = ({ title, children, }) => { + const enabled = useFeatureFlag(CSS_MODULES_FEATURE_FLAG) + if (size === 'full') { return ( {variant !== 'empty' ? ( - + ) : null} - {title} + {title} + + {children} @@ -689,16 +839,22 @@ const SelectPanelMessage: React.FC = ({ return ( {children}