in src/ActionList/Item.tsx [150:406]
hoverBg: get('colors.actionListItem.default.hoverBg'),
focusBg: get('colors.actionListItem.default.activeBg')
}
}
}
const DividedContent = styled.div`
display: flex;
min-width: 0;
/* Required for dividers */
position: relative;
flex-grow: 1;
`
const MainContent = styled.div`
align-items: baseline;
display: flex;
min-width: 0;
flex-direction: var(--main-content-flex-direction);
flex-grow: 1;
`
const StyledItem = styled.div<
{
variant: ItemProps['variant']
showDivider: ItemProps['showDivider']
item?: ItemInput
} & SxProp
>`
/* 6px vertical padding + 20px line height = 32px total height
*
* TODO: When rem-based spacing on a 4px scale lands, replace
* hardcoded '6px' with 'calc((${get('space.s32')} - ${get('space.20')}) / 2)'.
*/
padding: 6px ${get('space.2')};
display: flex;
border-radius: ${get('radii.2')};
color: ${({variant, item}) => getItemVariant(variant, item?.disabled).color};
// 2 frames on a 60hz monitor
transition: background 33.333ms linear;
text-decoration: none;
@media (hover: hover) and (pointer: fine) {
:hover {
// allow override in case another item in the list is active/focused
background: var(
--item-hover-bg-override,
${({variant, item}) => getItemVariant(variant, item?.disabled).hoverBg}
);
color: ${({variant, item}) => getItemVariant(variant, item?.disabled).hoverText};
cursor: ${({variant, item}) => getItemVariant(variant, item?.disabled).hoverCursor};
}
}
// Item dividers
:not(:first-of-type):not(${StyledDivider} + &):not(${StyledHeader} + &) {
margin-top: ${({showDivider}) => (showDivider ? `1px` : '0')};
${DividedContent}::before {
content: ' ';
display: block;
position: absolute;
width: 100%;
top: -7px;
// NB: This 'get' won’t execute if it’s moved into the arrow function below.
border: 0 solid ${get('colors.border.muted')};
border-top-width: ${({showDivider}) => (showDivider ? `1px` : '0')};
}
}
// Item dividers should not be visible:
// - above Hovered
&:hover ${DividedContent}::before,
// - below Hovered
// '*' instead of '&' because '&' maps to separate class names depending on 'variant'
:hover + * ${DividedContent}::before {
// allow override in case another item in the list is active/focused
border-color: var(--item-hover-divider-border-color-override, transparent) !important;
}
// - above Focused
&:focus ${DividedContent}::before,
// - below Focused
// '*' instead of '&' because '&' maps to separate class names depending on 'variant'
:focus + * ${DividedContent}::before,
// - above Active Descendent
&[${isActiveDescendantAttribute}] ${DividedContent}::before,
// - below Active Descendent
[${isActiveDescendantAttribute}] + & ${DividedContent}::before {
// '!important' because all the ':not's above give higher specificity
border-color: transparent !important;
}
// Active Descendant
&[${isActiveDescendantAttribute}='${activeDescendantActivatedDirectly}'] {
background: ${({variant, item}) => getItemVariant(variant, item?.disabled).focusBg};
}
&[${isActiveDescendantAttribute}='${activeDescendantActivatedIndirectly}'] {
background: ${({variant, item}) => getItemVariant(variant, item?.disabled).hoverBg};
}
&:focus {
background: ${({variant, item}) => getItemVariant(variant, item?.disabled).focusBg};
outline: none;
}
&:active {
background: ${({variant, item}) => getItemVariant(variant, item?.disabled).focusBg};
}
${sx}
`
export const TextContainer = styled.span<{
dangerouslySetInnerHtml?: React.DOMAttributes<HTMLDivElement>['dangerouslySetInnerHTML']
}>``
const BaseVisualContainer = styled.div<{variant?: ItemProps['variant']; disabled?: boolean}>`
// Match visual height to adjacent text line height.
// TODO: When rem-based spacing on a 4px scale lands, replace
// hardcoded '20px' with '${get('space.s20')}'.
height: 20px;
width: ${get('space.3')};
margin-right: ${get('space.2')};
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
`
const ColoredVisualContainer = styled(BaseVisualContainer)`
svg {
fill: ${({variant, disabled}) => getItemVariant(variant, disabled).iconColor};
font-size: ${get('fontSizes.0')};
}
`
const LeadingVisualContainer = styled(ColoredVisualContainer)`
display: flex;
flex-direction: column;
justify-content: center;
`
const TrailingContent = styled(ColoredVisualContainer)`
color: ${({variant, disabled}) => getItemVariant(variant, disabled).annotationColor}};
margin-left: ${get('space.2')};
margin-right: 0;
width: auto;
div:nth-child(2) {
margin-left: ${get('space.2')};
}
`
const DescriptionContainer = styled.span`
color: ${get('colors.fg.muted')};
font-size: ${get('fontSizes.0')};
// TODO: When rem-based spacing on a 4px scale lands, replace
// hardcoded '16px' with '${get('lh-12')}'.
line-height: 16px;
margin-left: var(--description-container-margin-left);
min-width: 0;
flex-grow: 1;
flex-basis: var(--description-container-flex-basis);
`
const MultiSelectIcon = styled.svg<{selected?: boolean}>`
rect {
fill: ${({selected}) => (selected ? get('colors.accent.fg') : get('colors.canvas.default'))};
stroke: ${({selected}) => (selected ? get('colors.accent.fg') : get('colors.border.default'))};
shape-rendering: auto; // this is a workaround to override global style in github/github, see primer/react#1666
}
path {
fill: ${get('colors.fg.onEmphasis')};
boxshadow: ${get('shadow.small')};
opacity: ${({selected}) => (selected ? 1 : 0)};
}
`
/**
* An actionable or selectable `Item` with an optional icon and description.
*/
export const Item = React.forwardRef((itemProps, ref) => {
const {
as: Component,
text,
description,
descriptionVariant = 'inline',
selected,
selectionVariant,
leadingVisual: LeadingVisual,
trailingIcon: TrailingIcon,
trailingVisual: TrailingVisual,
trailingText,
variant = 'default',
showDivider,
disabled,
onAction,
onKeyPress,
children,
onClick,
id,
...props
} = itemProps
const labelId = useSSRSafeId()
const descriptionId = useSSRSafeId()
const keyPressHandler = useCallback(
event => {
if (disabled) {
return
}
onKeyPress?.(event)
if (!event.defaultPrevented && [' ', 'Enter'].includes(event.key)) {
onAction?.(itemProps, event)
}
},
[onAction, disabled, itemProps, onKeyPress]
)
const clickHandler = useCallback(
event => {
if (disabled) {
return
}
onClick?.(event)
if (!event.defaultPrevented) {
onAction?.(itemProps, event)
}
},
[onAction, disabled, itemProps, onClick]
)
const {theme} = useTheme()
return (
<StyledItem
ref={ref}
as={Component}
tabIndex={disabled ? undefined : -1}
variant={variant}
showDivider={showDivider}
aria-selected={selected}
aria-labelledby={text ? labelId : undefined}
aria-describedby={description ? descriptionId : undefined}
{...props}
data-id={id}
onKeyPress={keyPressHandler}
onClick={clickHandler}
>
{!!selected === selected && (
<BaseVisualContainer>
{selectionVariant === 'multiple' ? (
<>
{/**