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

Use SSR-compatible slot implementation in ActionList/NavList #3173

Merged
merged 11 commits into from
Apr 20, 2023
7 changes: 7 additions & 0 deletions .changeset/lemon-berries-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@primer/react": patch
---

`ActionList` and `NavList` are now SSR-compatible.

Warning: In this new implementation, `ActionList.LeadingVisual`, `ActionList.TrailingVisual,` and `ActionList.Description` must be direct children of `ActionList`. The same applies to `NavList`.
48 changes: 22 additions & 26 deletions src/ActionList/Description.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react'
import Box from '../Box'
import {SxProp, merge} from '../sx'
import Truncate from '../Truncate'
import {Slot, ItemContext} from './shared'
import {SxProp, merge} from '../sx'
import {ItemContext} from './shared'

export type ActionListDescriptionProps = {
/**
Expand All @@ -28,29 +28,25 @@ export const Description: React.FC<React.PropsWithChildren<ActionListDescription
marginLeft: variant === 'block' ? 0 : 2,
}

return (
<Slot name={variant === 'block' ? 'BlockDescription' : 'InlineDescription'}>
{({blockDescriptionId, inlineDescriptionId, disabled}: ItemContext) =>
variant === 'block' ? (
<Box
as="span"
sx={merge({...styles, color: disabled ? 'fg.disabled' : 'fg.muted'}, sx as SxProp)}
id={blockDescriptionId}
>
{props.children}
</Box>
) : (
<Truncate
id={inlineDescriptionId}
sx={merge({...styles, color: disabled ? 'fg.disabled' : 'fg.muted'}, sx as SxProp)}
title={props.children as string}
inline={true}
maxWidth="100%"
>
{props.children}
</Truncate>
)
}
</Slot>
const {blockDescriptionId, inlineDescriptionId, disabled} = React.useContext(ItemContext)

return variant === 'block' ? (
<Box
as="span"
sx={merge({...styles, color: disabled ? 'fg.disabled' : 'fg.muted'}, sx as SxProp)}
id={blockDescriptionId}
>
{props.children}
</Box>
) : (
<Truncate
id={inlineDescriptionId}
sx={merge({...styles, color: disabled ? 'fg.disabled' : 'fg.muted'}, sx as SxProp)}
title={props.children as string}
inline={true}
maxWidth="100%"
>
{props.children}
</Truncate>
)
}
121 changes: 60 additions & 61 deletions src/ActionList/Item.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
import React from 'react'
import styled from 'styled-components'
import Box, {BoxProps} from '../Box'
import {useId} from '../hooks/useId'
import {useSlots} from '../hooks/useSlots'
import sx, {BetterSystemStyleObject, merge, SxProp} from '../sx'
import {useTheme} from '../ThemeProvider'
import {useId} from '../hooks/useId'
import {defaultSxProp} from '../utils/defaultSxProp'
import {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
import {ActionListContainerContext} from './ActionListContainerContext'
import {Description} from './Description'
import {ActionListGroupProps, GroupContext} from './Group'
import {ActionListProps, ListContext} from './List'
import {Selection} from './Selection'
import {ActionListItemProps, Slots, TEXT_ROW_HEIGHT, getVariantStyles} from './shared'
import {defaultSxProp} from '../utils/defaultSxProp'
import {ActionListItemProps, getVariantStyles, ItemContext, TEXT_ROW_HEIGHT} from './shared'
import {LeadingVisual, TrailingVisual} from './Visuals'

const LiBox = styled.li<SxProp>(sx)

Expand All @@ -30,6 +33,11 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(
},
forwardedRef,
): JSX.Element => {
const [slots, childrenWithoutSlots] = useSlots(props.children, {
leadingVisual: LeadingVisual,
trailingVisual: TrailingVisual,
description: Description,
})
const {variant: listVariant, showDividers, selectionVariant: listSelectionVariant} = React.useContext(ListContext)
const {selectionVariant: groupSelectionVariant} = React.useContext(GroupContext)
const {container, afterSelect, selectionAttribute} = React.useContext(ActionListContainerContext)
Expand Down Expand Up @@ -169,66 +177,57 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(

const ItemWrapper = _PrivateItemWrapper || React.Fragment

const menuItemProps = {
onClick: clickHandler,
onKeyPress: keyPressHandler,
'aria-disabled': disabled ? true : undefined,
tabIndex: disabled ? undefined : 0,
'aria-labelledby': `${labelId} ${
slots.description && slots.description.props.variant !== 'block' ? inlineDescriptionId : ''
}`,
'aria-describedby': slots.description?.props.variant === 'block' ? blockDescriptionId : undefined,
...(selectionAttribute && {[selectionAttribute]: selected}),
role: role || itemRole,
}

const containerProps = _PrivateItemWrapper ? {role: role || itemRole ? 'none' : undefined} : menuItemProps

const wrapperProps = _PrivateItemWrapper ? menuItemProps : {}

return (
<Slots context={{variant, disabled, inlineDescriptionId, blockDescriptionId}}>
{slots => {
const menuItemProps = {
onClick: clickHandler,
onKeyPress: keyPressHandler,
'aria-disabled': disabled ? true : undefined,
tabIndex: disabled ? undefined : 0,
'aria-labelledby': `${labelId} ${slots.InlineDescription ? inlineDescriptionId : ''}`,
'aria-describedby': slots.BlockDescription ? blockDescriptionId : undefined,
...(selectionAttribute && {[selectionAttribute]: selected}),
role: role || itemRole,
}
const containerProps = _PrivateItemWrapper
? {
role: role || itemRole ? 'none' : undefined,
}
: menuItemProps
const wrapperProps = _PrivateItemWrapper ? menuItemProps : {}

return (
<LiBox
ref={forwardedRef}
sx={merge<BetterSystemStyleObject>(styles, sxProp)}
{...containerProps}
{...props}
<ItemContext.Provider value={{variant, disabled, inlineDescriptionId, blockDescriptionId}}>
<LiBox ref={forwardedRef} sx={merge<BetterSystemStyleObject>(styles, sxProp)} {...containerProps} {...props}>
<ItemWrapper {...wrapperProps}>
<Selection selected={selected} />
{slots.leadingVisual}
<Box
data-component="ActionList.Item--DividerContainer"
sx={{display: 'flex', flexDirection: 'column', flexGrow: 1, minWidth: 0}}
>
<ItemWrapper {...wrapperProps}>
<Selection selected={selected} />
{slots.LeadingVisual}
<Box
data-component="ActionList.Item--DividerContainer"
sx={{display: 'flex', flexDirection: 'column', flexGrow: 1, minWidth: 0}}
<ConditionalBox if={Boolean(slots.trailingVisual)} sx={{display: 'flex', flexGrow: 1}}>
<ConditionalBox
if={!!slots.description && slots.description.props.variant !== 'block'}
sx={{display: 'flex', flexGrow: 1, alignItems: 'baseline', minWidth: 0}}
>
<ConditionalBox if={Boolean(slots.TrailingVisual)} sx={{display: 'flex', flexGrow: 1}}>
<ConditionalBox
if={Boolean(slots.InlineDescription)}
sx={{display: 'flex', flexGrow: 1, alignItems: 'baseline', minWidth: 0}}
>
<Box
as="span"
id={labelId}
sx={{
flexGrow: slots.InlineDescription ? 0 : 1,
fontWeight: slots.InlineDescription ? 'bold' : 'normal',
}}
>
{props.children}
</Box>
{slots.InlineDescription}
</ConditionalBox>
{slots.TrailingVisual}
</ConditionalBox>
{slots.BlockDescription}
</Box>
</ItemWrapper>
</LiBox>
)
}}
</Slots>
<Box
as="span"
id={labelId}
sx={{
flexGrow: slots.description && slots.description.props.variant !== 'block' ? 0 : 1,
fontWeight: slots.description && slots.description.props.variant !== 'block' ? 'bold' : 'normal',
}}
>
{childrenWithoutSlots}
</Box>
{slots.description?.props.variant !== 'block' ? slots.description : null}
</ConditionalBox>
{slots.trailingVisual}
</ConditionalBox>
{slots.description?.props.variant === 'block' ? slots.description : null}
</Box>
</ItemWrapper>
</LiBox>
</ItemContext.Provider>
)
},
) as PolymorphicForwardRefComponent<'li', ActionListItemProps>
Expand Down
66 changes: 30 additions & 36 deletions src/ActionList/Visuals.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react'
import Box from '../Box'
import {SxProp, merge} from '../sx'
import {get} from '../constants'
import {getVariantStyles, Slot, ItemContext, TEXT_ROW_HEIGHT} from './shared'
import {SxProp, merge} from '../sx'
import {ItemContext, TEXT_ROW_HEIGHT, getVariantStyles} from './shared'

type VisualProps = SxProp & React.HTMLAttributes<HTMLSpanElement>

Expand Down Expand Up @@ -30,48 +30,42 @@ export const LeadingVisualContainer: React.FC<React.PropsWithChildren<VisualProp

export type ActionListLeadingVisualProps = VisualProps
export const LeadingVisual: React.FC<React.PropsWithChildren<VisualProps>> = ({sx = {}, ...props}) => {
const {variant, disabled} = React.useContext(ItemContext)
return (
<Slot name="LeadingVisual">
{({variant, disabled}: ItemContext) => (
<LeadingVisualContainer
sx={merge(
{
color: getVariantStyles(variant, disabled).iconColor,
svg: {fontSize: 0},
},
sx as SxProp,
)}
{...props}
>
{props.children}
</LeadingVisualContainer>
<LeadingVisualContainer
sx={merge(
{
color: getVariantStyles(variant, disabled).iconColor,
svg: {fontSize: 0},
},
sx as SxProp,
)}
</Slot>
{...props}
>
{props.children}
</LeadingVisualContainer>
)
}

export type ActionListTrailingVisualProps = VisualProps
export const TrailingVisual: React.FC<React.PropsWithChildren<VisualProps>> = ({sx = {}, ...props}) => {
const {variant, disabled} = React.useContext(ItemContext)
return (
<Slot name="TrailingVisual">
{({variant, disabled}: ItemContext) => (
<Box
as="span"
sx={merge(
{
height: '20px', // match height of text row
flexShrink: 0,
color: getVariantStyles(variant, disabled).annotationColor,
marginLeft: 2,
fontWeight: 'initial',
},
sx as SxProp,
)}
{...props}
>
{props.children}
</Box>
<Box
as="span"
sx={merge(
{
height: '20px', // match height of text row
flexShrink: 0,
color: getVariantStyles(variant, disabled).annotationColor,
marginLeft: 2,
fontWeight: 'initial',
},
sx as SxProp,
)}
</Slot>
{...props}
>
{props.children}
</Box>
)
}
9 changes: 4 additions & 5 deletions src/ActionList/shared.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react'
import {SxProp} from '../sx'
import createSlots from '../utils/create-slots'
import {AriaRole} from '../utils/types'

export type ActionListItemProps = {
Expand Down Expand Up @@ -56,10 +55,12 @@ type MenuItemProps = {
}

export type ItemContext = Pick<ActionListItemProps, 'variant' | 'disabled'> & {
inlineDescriptionId: string
blockDescriptionId: string
inlineDescriptionId?: string
blockDescriptionId?: string
}

export const ItemContext = React.createContext<ItemContext>({})

export const getVariantStyles = (
variant: ActionListItemProps['variant'],
disabled: ActionListItemProps['disabled'],
Expand Down Expand Up @@ -90,6 +91,4 @@ export const getVariantStyles = (
}
}

export const {Slots, Slot} = createSlots(['LeadingVisual', 'InlineDescription', 'BlockDescription', 'TrailingVisual'])

export const TEXT_ROW_HEIGHT = '20px' // custom value off the scale
12 changes: 6 additions & 6 deletions src/NavList/__snapshots__/NavList.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1278,12 +1278,6 @@ exports[`NavList.Item with NavList.SubNav has active styles if SubNav contains t
list-style: none;
}

.c9 {
padding: 0;
margin: 0;
display: none;
}

.c5 {
display: -webkit-box;
display: -webkit-flex;
Expand All @@ -1295,6 +1289,12 @@ exports[`NavList.Item with NavList.SubNav has active styles if SubNav contains t
flex-grow: 1;
}

.c9 {
padding: 0;
margin: 0;
display: none;
}

.c7 {
height: 20px;
-webkit-flex-shrink: 0;
Expand Down