From cbcb00e3e1eee72ceb8366c3144d2f6b34470782 Mon Sep 17 00:00:00 2001 From: Katie Langerman <18661030+langermank@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:31:25 -0800 Subject: [PATCH 01/27] copy --- .../src/ActionList/ActionList.module.css | 79 +++++ packages/react/src/ActionList/Item.tsx | 332 +++++++++++++----- packages/react/src/ActionList/Selection.tsx | 25 +- .../react/src/ActionList/TrailingAction.tsx | 91 +++-- packages/react/src/ActionList/Visuals.tsx | 103 ++++-- packages/react/src/ActionList/shared.ts | 2 + 6 files changed, 488 insertions(+), 144 deletions(-) create mode 100644 packages/react/src/ActionList/ActionList.module.css diff --git a/packages/react/src/ActionList/ActionList.module.css b/packages/react/src/ActionList/ActionList.module.css new file mode 100644 index 00000000000..0f3e563ffa0 --- /dev/null +++ b/packages/react/src/ActionList/ActionList.module.css @@ -0,0 +1,79 @@ +/* stylelint-disable selector-max-specificity, selector-max-compound-selectors */ + +.ActionList { + padding: 0; + margin: 0; + list-style: none; + + ul { + padding: 0; + margin: 0; + list-style: none; + } + + &:where([data-variant='inset']) { + /* change to padding (all) when Item is converted */ + padding-block: var(--base-size-8); + } + + &:where([data-dividers='true']) { + /* place dividers on the wrapper that excludes leading visuals/actions */ + & .ActionListSubContent::before { + position: absolute; + /* stylelint-disable-next-line primer/spacing */ + top: calc(-1 * var(--control-medium-paddingBlock)); + display: block; + width: 100%; + height: 1px; + content: ''; + /* stylelint-disable-next-line primer/colors */ + background: var(--borderColor-muted); + } + + /* if inline description, move pseudo divider to description wrapper */ + & [data-description-variant='inline'] { + &::before { + position: absolute; + /* stylelint-disable-next-line primer/spacing */ + top: calc(-1 * var(--control-medium-paddingBlock)); + display: block; + width: 100%; + height: var(--borderWidth-thin); + content: ''; + /* stylelint-disable-next-line primer/colors */ + background: var(--borderColor-muted); + } + + /* remove the default divider */ + & .ActionListSubContent::before { + content: unset; + } + } + + /* hide if item is first of type with label::before, or is the first item after a sectionDivider */ + .ActionListItem:first-of-type .ActionListSubContent::before, + .Divider + .ActionListItem .ActionListSubContent::before { + visibility: hidden; + } + + /* hide if item is first of type with label::before, or is the first item after a sectionDivider */ + .ActionListItem:first-of-type [data-description-variant='inline']::before, + .Divider + .ActionListItem [data-description-variant='inline']::before { + visibility: hidden; + } + } +} + +.Divider { + display: block; + height: var(--borderWidth-thin); + padding: 0; + /* stylelint-disable-next-line primer/spacing */ + margin-block-start: calc(var(--base-size-8) - var(--borderWidth-thin)); + margin-block-end: var(--base-size-8); + margin-inline: calc(-1 * var(--base-size-8)); + list-style: none; + /* stylelint-disable-next-line primer/colors */ + background: var(--borderColor-muted); + border: 0; +} diff --git a/packages/react/src/ActionList/Item.tsx b/packages/react/src/ActionList/Item.tsx index f51caa7b641..1c8c2e6b95a 100644 --- a/packages/react/src/ActionList/Item.tsx +++ b/packages/react/src/ActionList/Item.tsx @@ -15,20 +15,39 @@ import {GroupContext} from './Group' import type {ActionListItemProps, ActionListProps} from './shared' import {Selection} from './Selection' import {LeadingVisual, TrailingVisual, VisualOrIndicator} from './Visuals' -import {getVariantStyles, ItemContext, TEXT_ROW_HEIGHT, ListContext} from './shared' +import {getVariantStyles, ItemContext, ListContext} from './shared' import {TrailingAction} from './TrailingAction' import {ConditionalWrapper} from '../internal/components/ConditionalWrapper' import {invariant} from '../utils/invariant' import {useFeatureFlag} from '../FeatureFlags' import VisuallyHidden from '../_VisuallyHidden' - +import classes from './ActionList.module.css' +import {clsx} from 'clsx' const LiBox = styled.li(sx) -const ButtonItemContainer = React.forwardRef(({as: Component = 'button', children, styles, ...props}, forwardedRef) => { +interface SubItemProps { + children?: React.ReactNode +} + +const SubItem: React.FC = ({children}) => { + return <>{children} +} + +SubItem.displayName = 'ActionList.SubItem' + +const ButtonItemContainerNoBox = React.forwardRef(({children, style, ...props}, forwardedRef) => { + return ( + + ) +}) as PolymorphicForwardRefComponent + +const DivItemContainerNoBox = React.forwardRef(({children, ...props}, forwardedRef) => { return ( - +
} {...props}> {children} - +
) }) as PolymorphicForwardRefComponent @@ -46,35 +65,43 @@ export const Item = React.forwardRef( role, loading, _PrivateItemWrapper, + className, ...props }, forwardedRef, ): JSX.Element => { - const [slots, childrenWithoutSlots] = useSlots(props.children, { + const enabled = useFeatureFlag('primer_react_css_modules_team') + + const baseSlots = { leadingVisual: LeadingVisual, trailingVisual: TrailingVisual, trailingAction: TrailingAction, - blockDescription: [Description, props => props.variant === 'block'], - inlineDescription: [Description, props => props.variant !== 'block'], - }) + subItem: SubItem, + } + + const [partialSlots, childrenWithoutSlots] = useSlots( + props.children, + enabled + ? {...baseSlots, description: Description} + : { + ...baseSlots, + blockDescription: [Description, props => props.variant === 'block'], + inlineDescription: [Description, props => props.variant !== 'block'], + }, + ) + + const slots = {blockDescription: undefined, inlineDescription: undefined, description: undefined, ...partialSlots} const {container, afterSelect, selectionAttribute, defaultTrailingVisual} = React.useContext(ActionListContainerContext) - const buttonSemanticsFeatureFlag = useFeatureFlag('primer_react_action_list_item_as_button') - // Be sure to avoid rendering the container unless there is a default const wrappedDefaultTrailingVisual = defaultTrailingVisual ? ( {defaultTrailingVisual} ) : null const trailingVisual = slots.trailingVisual ?? wrappedDefaultTrailingVisual - const { - variant: listVariant, - role: listRole, - showDividers, - selectionVariant: listSelectionVariant, - } = React.useContext(ListContext) + const {role: listRole, showDividers, selectionVariant: listSelectionVariant} = React.useContext(ListContext) const {selectionVariant: groupSelectionVariant} = React.useContext(GroupContext) const inactive = Boolean(inactiveText) const showInactiveIndicator = inactive && container === undefined @@ -125,8 +152,11 @@ export const Item = React.forwardRef( const listRoleTypes = ['listbox', 'menu', 'list'] const listSemantics = - (listRole && listRoleTypes.includes(listRole)) || inactive || container === 'NavList' || listItemSemantics - const buttonSemantics = !listSemantics && !_PrivateItemWrapper && buttonSemanticsFeatureFlag + (listRole && listRoleTypes.includes(listRole)) || + inactive || + (container === 'NavList' && !enabled) || + listItemSemantics + const buttonSemantics = !listSemantics && !_PrivateItemWrapper const {theme} = useTheme() @@ -145,44 +175,14 @@ export const Item = React.forwardRef( }, } - const hoverStyles = { - '@media (hover: hover) and (pointer: fine)': { - '&:hover:not([aria-disabled]):not([data-inactive])': { - backgroundColor: `actionListItem.${variant}.hoverBg`, - color: getVariantStyles(variant, disabled, inactive).hoverColor, - boxShadow: `inset 0 0 0 max(1px, 0.0625rem) ${theme?.colors.actionListItem.default.activeBorder}`, - }, - '&:focus-visible, > a.focus-visible, &:focus.focus-visible': { - outline: 'none', - border: `2 solid`, - boxShadow: `0 0 0 2px ${theme?.colors.accent.emphasis}`, - }, - '&:active:not([aria-disabled]):not([data-inactive])': { - backgroundColor: `actionListItem.${variant}.activeBg`, - color: getVariantStyles(variant, disabled, inactive).hoverColor, - }, - }, - } - - const listItemStyles = { - display: 'flex', - // show between 2 items - ':not(:first-of-type)': {'--divider-color': theme?.colors.actionListItem.inlineDivider}, - width: buttonSemantics && listVariant !== 'full' ? 'calc(100% - 16px)' : '100%', - marginX: buttonSemantics && listVariant !== 'full' ? '2' : '0', - borderRadius: 2, - ...(buttonSemantics ? hoverStyles : {}), - } - const styles = { position: 'relative', display: 'flex', paddingX: 2, fontSize: 1, paddingY: '6px', // custom value off the scale - lineHeight: TEXT_ROW_HEIGHT, + lineHeight: '16px', minHeight: 5, - marginX: listVariant === 'inset' && !buttonSemantics ? 2 : 0, borderRadius: 2, transition: 'background 33.333ms linear', color: getVariantStyles(variant, disabled, inactive || loading).color, @@ -206,7 +206,7 @@ export const Item = React.forwardRef( appearance: 'none', background: 'unset', border: 'unset', - width: listVariant === 'inset' && !buttonSemantics ? 'calc(100% - 16px)' : '100%', + width: '100%', fontFamily: 'unset', textAlign: 'unset', marginY: 'unset', @@ -218,6 +218,23 @@ export const Item = React.forwardRef( }, }, + '@media (hover: hover) and (pointer: fine)': { + '&:hover:not([aria-disabled]):not([data-inactive])': { + backgroundColor: `actionListItem.${variant}.hoverBg`, + color: getVariantStyles(variant, disabled, inactive).hoverColor, + boxShadow: `inset 0 0 0 max(1px, 0.0625rem) ${theme?.colors.actionListItem.default.activeBorder}`, + }, + '&:focus-visible, > a.focus-visible, &:focus.focus-visible': { + outline: 'none', + border: `2 solid`, + boxShadow: `0 0 0 2px var(--focus-outlineColor)`, + }, + '&:active:not([aria-disabled]):not([data-inactive])': { + backgroundColor: `actionListItem.${variant}.activeBg`, + color: getVariantStyles(variant, disabled, inactive).hoverColor, + }, + }, + /** Divider styles */ '[data-component="ActionList.Item--DividerContainer"]': { position: 'relative', @@ -227,7 +244,7 @@ export const Item = React.forwardRef( display: 'block', position: 'absolute', width: '100%', - top: '-7px', + top: '-8px', border: '0 solid', borderTopWidth: showDividers ? `1px` : '0', borderColor: 'var(--divider-color, transparent)', @@ -249,8 +266,6 @@ export const Item = React.forwardRef( /** Active styles */ ...(active ? activeStyles : {}), // NavList '&[data-is-active-descendant]': {...activeStyles, fontWeight: 'normal'}, // SelectPanel - - ...(!buttonSemantics ? hoverStyles : {}), } const clickHandler = React.useCallback( @@ -285,16 +300,22 @@ export const Item = React.forwardRef( const inactiveWarningId = inactive && !showInactiveIndicator ? `${itemId}--warning-message` : undefined let DefaultItemWrapper = React.Fragment - if (buttonSemanticsFeatureFlag) { - DefaultItemWrapper = listSemantics ? React.Fragment : ButtonItemContainer + if (enabled) { + DefaultItemWrapper = listSemantics ? DivItemContainerNoBox : ButtonItemContainerNoBox + } else { + DefaultItemWrapper = React.Fragment } const ItemWrapper = _PrivateItemWrapper || DefaultItemWrapper + // const ItemWrapper = + // _PrivateItemWrapper || (props.wrapper === 'button' ? ButtonItemContainerNoBox : DefaultItemWrapper) // only apply aria-selected and aria-checked to selectable items const selectableRoles = ['menuitemradio', 'menuitemcheckbox', 'option'] const includeSelectionAttribute = itemSelectionAttribute && itemRole && selectableRoles.includes(itemRole) + // const blockDescriptionSlot = [Description, (props: any) => props.variant === 'block' || props.variant === undefined] + const menuItemProps = { onClick: clickHandler, onKeyPress: !buttonSemantics ? keyPressHandler : undefined, @@ -313,28 +334,175 @@ export const Item = React.forwardRef( ...(includeSelectionAttribute && {[itemSelectionAttribute]: selected}), role: itemRole, id: itemId, + className, } - let containerProps - let wrapperProps - - if (buttonSemanticsFeatureFlag) { - containerProps = _PrivateItemWrapper - ? {role: itemRole ? 'none' : undefined, ...props} - : // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - (listSemantics && {...menuItemProps, ...props, ref: forwardedRef}) || {} - - wrapperProps = _PrivateItemWrapper - ? menuItemProps - : !listSemantics && { - ...menuItemProps, - ...props, - styles: merge(styles, sxProp), - ref: forwardedRef, - } - } else { - containerProps = _PrivateItemWrapper ? {role: itemRole ? 'none' : undefined} : {...menuItemProps, ...props} - wrapperProps = _PrivateItemWrapper ? menuItemProps : {} + const containerProps = _PrivateItemWrapper + ? {role: itemRole ? 'none' : undefined, ...props} + : // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + (listSemantics && {...menuItemProps, ...props, ref: forwardedRef}) || {} + + const wrapperProps = _PrivateItemWrapper + ? menuItemProps + : !listSemantics && { + ...menuItemProps, + ...props, + // styles: merge(styles, sxProp), + ref: forwardedRef, + } + + // Extract the variant prop value from the description slot component + + const descriptionVariant = slots.description?.props.variant ?? 'inline' + + // console.log(listSemantics) + + if (enabled) { + if (sxProp !== defaultSxProp) { + return ( + + (styles, sxProp)} + ref={listSemantics ? forwardedRef : null} + data-variant={variant === 'danger' ? variant : undefined} + data-active={active ? true : undefined} + data-inactive={inactiveText ? true : undefined} + data-has-subitem={slots.subItem ? true : undefined} + className={clsx(classes.ActionListItem, className)} + > + + + + + {slots.leadingVisual} + + + + + {childrenWithoutSlots} + {/* Loading message needs to be in here so it is read with the label */} + {loading === true && Loading} + + {slots.description} + + + {trailingVisual} + + + { + // If the item is inactive, but it's not in an overlay (e.g. ActionMenu, SelectPanel), + // render the inactive warning message directly in the item. + inactive && container ? ( + + {inactiveText} + + ) : null + } + + + {!inactive && !loading && !menuContext && Boolean(slots.trailingAction) && slots.trailingAction} + {slots.subItem} + + + ) + } + return ( + +
  • + + + + + {slots.leadingVisual} + + + + + {childrenWithoutSlots} + {/* Loading message needs to be in here so it is read with the label */} + {loading === true && Loading} + + {slots.description} + + + {trailingVisual} + + + { + // If the item is inactive, but it's not in an overlay (e.g. ActionMenu, SelectPanel), + // render the inactive warning message directly in the item. + inactive && container ? ( + + {inactiveText} + + ) : null + } + + + {!inactive && !loading && !menuContext && Boolean(slots.trailingAction) && slots.trailingAction} + {slots.subItem} +
  • +
    + ) } return ( @@ -349,15 +517,9 @@ export const Item = React.forwardRef( }} > ( - listSemantics || _PrivateItemWrapper ? styles : listItemStyles, - listSemantics || _PrivateItemWrapper ? sxProp : {}, - ) - : merge(styles, sxProp) - } + ref={listSemantics ? forwardedRef : null} + className={className} + sx={merge(styles, sxProp)} data-variant={variant === 'danger' ? variant : undefined} {...containerProps} > diff --git a/packages/react/src/ActionList/Selection.tsx b/packages/react/src/ActionList/Selection.tsx index 4d705e2faf4..0e106fb005b 100644 --- a/packages/react/src/ActionList/Selection.tsx +++ b/packages/react/src/ActionList/Selection.tsx @@ -3,14 +3,18 @@ import {CheckIcon} from '@primer/octicons-react' import type {ActionListGroupProps} from './Group' import {GroupContext} from './Group' import {type ActionListProps, type ActionListItemProps, ListContext} from './shared' -import {LeadingVisualContainer} from './Visuals' +import {LeadingVisualContainer, VisualContainer} from './Visuals' import Box from '../Box' +import {useFeatureFlag} from '../FeatureFlags' +import classes from './ActionList.module.css' -type SelectionProps = Pick -export const Selection: React.FC> = ({selected}) => { +type SelectionProps = Pick +export const Selection: React.FC> = ({selected, className}) => { const {selectionVariant: listSelectionVariant, role: listRole} = React.useContext(ListContext) const {selectionVariant: groupSelectionVariant} = React.useContext(GroupContext) + const enabled = useFeatureFlag('primer_react_css_modules_team') + /** selectionVariant in Group can override the selectionVariant in List root */ /** fallback to selectionVariant from container menu if any (ActionMenu, SelectPanel ) */ let selectionVariant: ActionListProps['selectionVariant'] | ActionListGroupProps['selectionVariant'] @@ -30,6 +34,14 @@ export const Selection: React.FC> = ({se } if (selectionVariant === 'single' || listRole === 'menu') { + if (enabled) { + return ( + + + {/* {selected && } */} + + ) + } return ( {selected && } ) @@ -60,6 +72,13 @@ export const Selection: React.FC> = ({se }, } + if (enabled) { + return ( + +
    + + ) + } return ( { - if (!icon) { - return ( - - {/* @ts-expect-error TODO: Fix this */} - - - ) - } else { +export const TrailingAction = forwardRef( + ({as = 'button', icon, label, href = null, className, ...props}, forwardedRef) => { + const enabled = useFeatureFlag('primer_react_css_modules_team') + + if (enabled) { + return ( + + {icon ? ( + + ) : ( + // @ts-expect-error shhh + + )} + + ) + } + return ( - + {icon ? ( + + ) : ( + // @ts-expect-error shhh + + )} ) - } -}) as PolymorphicForwardRefComponent<'button' | 'a', ActionListTrailingActionProps> + }, +) as PolymorphicForwardRefComponent<'button' | 'a', ActionListTrailingActionProps> TrailingAction.displayName = 'ActionList.TrailingAction' diff --git a/packages/react/src/ActionList/Visuals.tsx b/packages/react/src/ActionList/Visuals.tsx index 98cca0f77c3..1d3373609c4 100644 --- a/packages/react/src/ActionList/Visuals.tsx +++ b/packages/react/src/ActionList/Visuals.tsx @@ -5,20 +5,39 @@ import Spinner from '../Spinner' import {get} from '../constants' import type {SxProp} from '../sx' import {merge} from '../sx' -import {ItemContext, TEXT_ROW_HEIGHT, getVariantStyles} from './shared' +import {ItemContext, getVariantStyles} from './shared' import {Tooltip, type TooltipProps} from '../TooltipV2' +import {clsx} from 'clsx' +import {useFeatureFlag} from '../FeatureFlags' +import classes from './ActionList.module.css' +import {defaultSxProp} from '../utils/defaultSxProp' export type VisualProps = SxProp & React.HTMLAttributes -export const LeadingVisualContainer: React.FC> = ({sx = {}, ...props}) => { +export const VisualContainer: React.FC> = ({ + sx = defaultSxProp, + className, + ...props +}) => { + if (sx !== defaultSxProp) { + return + } + return +} + +// remove when primer_react_css_modules_X is shipped +export const LeadingVisualContainer: React.FC> = ({ + sx = defaultSxProp, + ...props +}) => { return ( > = ({sx = {}, ...props}) => { +export const LeadingVisual: React.FC> = ({ + sx = defaultSxProp, + className, + ...props +}) => { const {variant, disabled, inactive} = React.useContext(ItemContext) + + const enabled = useFeatureFlag('primer_react_css_modules_team') + + if (enabled) { + if (sx !== defaultSxProp) { + return ( + + {props.children} + + ) + } + return ( + + {props.children} + + ) + } return ( > = ({s } export type ActionListTrailingVisualProps = VisualProps -export const TrailingVisual: React.FC> = ({sx = {}, ...props}) => { +export const TrailingVisual: React.FC> = ({ + sx = defaultSxProp, + className, + ...props +}) => { const {variant, disabled, inactive, trailingVisualId} = React.useContext(ItemContext) + const enabled = useFeatureFlag('primer_react_css_modules_team') + if (enabled) { + if (sx !== defaultSxProp) { + return ( + + {props.children} + + ) + } + return ( + + {props.children} + + ) + } return ( -> = ({children, labelId, loading, inactiveText, itemHasLeadingVisual, position}) => { +> = ({children, labelId, loading, inactiveText, itemHasLeadingVisual, position, className}) => { const VisualComponent = position === 'leading' ? LeadingVisual : TrailingVisual if (!loading && !inactiveText) return children @@ -113,26 +173,17 @@ export const VisualOrIndicator: React.FC< } return inactiveText ? ( - - - - - - - + + + + + ) : ( - + ) diff --git a/packages/react/src/ActionList/shared.ts b/packages/react/src/ActionList/shared.ts index d0d582a804b..7e2c6f4dd09 100644 --- a/packages/react/src/ActionList/shared.ts +++ b/packages/react/src/ActionList/shared.ts @@ -52,6 +52,7 @@ export type ActionListItemProps = { * Private API for use internally only. Used by LinkItem to wrap contents in an anchor */ _PrivateItemWrapper?: React.FC> + className?: string } & SxProp type MenuItemProps = { @@ -62,6 +63,7 @@ type MenuItemProps = { 'aria-labelledby'?: string 'aria-describedby'?: string role?: string + className?: string } export type ItemContext = Pick & { From b4073f2c7d454e6988a8ae73dbbcd367c4527486 Mon Sep 17 00:00:00 2001 From: Katie Langerman <18661030+langermank@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:26:52 -0800 Subject: [PATCH 02/27] copy all --- .../src/ActionList/ActionList.module.css | 580 +++++++++++++++++- packages/react/src/ActionList/Description.tsx | 65 +- 2 files changed, 643 insertions(+), 2 deletions(-) diff --git a/packages/react/src/ActionList/ActionList.module.css b/packages/react/src/ActionList/ActionList.module.css index 0f3e563ffa0..15a588ef6ca 100644 --- a/packages/react/src/ActionList/ActionList.module.css +++ b/packages/react/src/ActionList/ActionList.module.css @@ -1,4 +1,4 @@ -/* stylelint-disable selector-max-specificity, selector-max-compound-selectors */ +/* stylelint-disable max-nesting-depth, selector-max-specificity, selector-max-compound-selectors */ .ActionList { padding: 0; @@ -64,6 +64,542 @@ } } +/* ActionListItem is a li that handles visual state, while ActionListItemContent controls actual state via button or link */ + +.ActionListItem { + position: relative; + list-style: none; + background-color: var(--control-transparent-bgColor-rest); + border-radius: var(--borderRadius-medium); + + /* apply flex if trailing action exists as an immediate child */ + &:has(> .TrailingAction) { + display: flex; + flex-wrap: nowrap; + } + + /* state */ + + &:not(:has([aria-disabled], [disabled]), [aria-disabled='true'], [data-has-subitem='true']) { + @media (hover: hover) { + &:hover, + &:active { + cursor: pointer; + } + + &:hover { + background-color: var(--control-transparent-bgColor-hover); + + &:not([data-active], :focus-visible) { + /* Support for "Windows high contrast mode" https:sarahmhigley.com/writing/whcm-quick-tips/ */ + outline: solid var(--borderWidth-thin) transparent; + outline-offset: calc(-1 * var(--borderWidth-thin)); + box-shadow: var(--boxShadow-thin) var(--control-transparent-borderColor-active); + } + } + } + + &:active { + background-color: var(--control-transparent-bgColor-active); + + &:not([data-active]) { + /* Support for "Windows high contrast mode" https:sarahmhigley.com/writing/whcm-quick-tips/ */ + outline: solid var(--borderWidth-thin) transparent; + outline-offset: calc(-1 * var(--borderWidth-thin)); + box-shadow: var(--boxShadow-thin) var(--control-transparent-borderColor-active); + } + } + + &:focus-visible { + @mixin focusOutline 0; + } + + /* danger */ + &:where([data-variant='danger']) { + & * :not([popover], .TrailingVisual) { + color: var(--control-danger-fgColor-rest); + } + + @media (hover: hover) { + &:hover { + background: var(--control-danger-bgColor-hover); + + & * :not([popover]) { + color: var(--control-danger-fgColor-hover); + } + } + } + + &:active { + background: var(--control-danger-bgColor-active); + + & * :not([popover]) { + color: var(--control-danger-fgColor-hover); + } + } + } + + /* active state [aria-current] */ + &:where([data-active]) { + background: var(--control-transparent-bgColor-selected); + + /* provides a visual indication of the current item for Windows high-contrast mode */ + outline: 2px solid transparent; + + & .ItemLabel { + font-weight: var(--base-text-weight-semibold); + color: var(--control-fgColor-rest); + } + + @media (hover: hover) { + &:hover { + background-color: var(--control-transparent-bgColor-hover); + } + } + + /* hide dividers if showDividers is true and item is active */ + + & .ActionListSubContent::before, + & + .ActionListItem .ActionListSubContent::before { + visibility: hidden; + } + + /* blue accent line */ + &::after { + position: absolute; + top: calc(50% - var(--base-size-12)); + left: calc(-1 * var(--base-size-8)); + width: var(--base-size-4); + height: var(--base-size-24); + content: ''; + background: var(--bgColor-accent-emphasis); + border-radius: var(--borderRadius-medium); + } + } + + /* inactive */ + &:where([data-inactive='true']) { + /* ignore tooltip */ + & * :not([popover], .InactiveWarning) { + color: var(--fgColor-muted); + } + + @media (hover: hover) { + &:hover { + cursor: not-allowed; + background-color: transparent; + + & * :not([popover], .InactiveWarning) { + color: var(--fgColor-muted); + } + } + } + + &:active { + background: transparent; + } + } + + &:where([data-loading='true']), + &:has([data-loading='true']) { + & * { + color: var(--fgColor-muted); + } + } + } + + /* if item has subitem, move hover styles to ActionListContent */ + &[data-has-subitem='true'] { + /* first child */ + & > .ActionListContent { + z-index: 1; + + @media (hover: hover) { + &:hover { + cursor: pointer; + background-color: var(--control-transparent-bgColor-hover); + } + } + + &:active { + background-color: var(--control-transparent-bgColor-active); + } + } + + & .Spacer { + display: block; + } + } + + /* disabled */ + + &[aria-disabled='true'], + &:has([aria-disabled='true'], [disabled]) { + & .ActionListContent * { + color: var(--control-fgColor-disabled); + } + + & .ActionListContent { + @media (hover: hover) { + &:hover { + cursor: not-allowed; + background-color: transparent; + } + } + } + + @media (hover: hover) { + &:hover { + background-color: transparent; + } + } + } + + /* hide dividers */ + @media (hover: hover) { + &:hover { + & .ActionListSubContent::before, + & + .ActionListItem .ActionListSubContent::before { + visibility: hidden; + } + + & [data-description-variant='inline']::before, + & + .ActionListItem [data-description-variant='inline']::before { + visibility: hidden; + } + } + } + + /* Make sure that the first visible item isn't a divider */ + &[aria-hidden] + .Divider { + display: none; + } + + /* + * checkbox item [aria-checked] + * listbox [aria-selected] + */ + + & .MultiSelectCheckbox { + position: relative; + display: grid; + width: var(--base-size-16); + height: var(--base-size-16); + margin: 0; + margin-top: var(--base-size-2); /* 2px to center align with label (20px line-height) */ + cursor: pointer; + background-color: var(--bgColor-default); + border: var(--borderWidth-thin) solid var(--control-borderColor-emphasis); + border-radius: var(--borderRadius-small); + transition: + background-color, + border-color 80ms cubic-bezier(0.33, 1, 0.68, 1); /* checked -> unchecked - add 120ms delay to fully see animation-out */ + + place-content: center; + + &::before { + width: var(--base-size-16); + height: var(--base-size-16); + content: ''; + /* stylelint-disable-next-line primer/colors */ + background-color: var(--control-checked-fgColor-rest); + transition: visibility 0s linear 230ms; + clip-path: inset(var(--base-size-16) 0 0 0); + + /* octicon checkmark image */ + mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOSIgdmlld0JveD0iMCAwIDEyIDkiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTEuNzgwMyAwLjIxOTYyNUMxMS45MjEgMC4zNjA0MjcgMTIgMC41NTEzMDUgMTIgMC43NTAzMTNDMTIgMC45NDkzMjEgMTEuOTIxIDEuMTQwMTkgMTEuNzgwMyAxLjI4MUw0LjUxODYgOC41NDA0MkM0LjM3Nzc1IDguNjgxIDQuMTg2ODIgOC43NiAzLjk4Nzc0IDguNzZDMy43ODg2NyA4Ljc2IDMuNTk3NzMgOC42ODEgMy40NTY4OSA4LjU0MDQyTDAuMjAxNjIyIDUuMjg2MkMwLjA2ODkyNzcgNS4xNDM4MyAtMC4wMDMzMDkwNSA0Ljk1NTU1IDAuMDAwMTE2NDkzIDQuNzYwOThDMC4wMDM1NTIwNSA0LjU2NjQzIDAuMDgyMzg5NCA0LjM4MDgxIDAuMjIwMDMyIDQuMjQzMjFDMC4zNTc2NjUgNC4xMDU2MiAwLjU0MzM1NSA0LjAyNjgxIDAuNzM3OTcgNC4wMjMzOEMwLjkzMjU4NCA0LjAxOTk0IDEuMTIwOTMgNC4wOTIxNyAxLjI2MzM0IDQuMjI0ODJMMy45ODc3NCA2Ljk0ODM1TDEwLjcxODYgMC4yMTk2MjVDMTAuODU5NSAwLjA3ODk5MjMgMTEuMDUwNCAwIDExLjI0OTUgMEMxMS40NDg1IDAgMTEuNjM5NSAwLjA3ODk5MjMgMTEuNzgwMyAwLjIxOTYyNVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo='); + mask-size: 75%; + mask-repeat: no-repeat; + mask-position: center; + animation: checkmarkOut 80ms cubic-bezier(0.65, 0, 0.35, 1); /* forwards; slightly snappier animation out */ + } + } + + &[aria-checked='true'], + &[aria-selected='true'] { + & .MultiSelectCheckbox { + background-color: var(--control-checked-bgColor-rest); + border-color: var(--control-checked-borderColor-rest); + transition: + background-color, + border-color 80ms cubic-bezier(0.32, 0, 0.67, 0) 0ms; /* unchecked -> checked */ + + &::before { + visibility: visible; + transition: visibility 0s linear 0s; + animation: checkmarkIn 80ms cubic-bezier(0.65, 0, 0.35, 1) forwards 80ms; + } + } + + & .SingleSelectCheckmark { + visibility: visible; + } + } + + &[aria-checked='false'], + &[aria-selected='false'] { + & .MultiSelectCheckbox { + &::before { + visibility: hidden; + } + } + + & .SingleSelectCheckmark { + visibility: hidden; + } + } +} + +/* button or a tag */ + +/* [ [spacer] [leadingAction] [leadingVisual] [content] ] */ +.ActionListContent { + --subitem-depth: 0px; + + position: relative; + display: grid; + width: 100%; + color: var(--control-fgColor-rest); + text-align: left; + user-select: none; + background-color: transparent; + border: none; + border-radius: var(--borderRadius-medium); + transition: background 33.333ms linear; + /* stylelint-disable-next-line primer/spacing */ + padding-block: var(--control-medium-paddingBlock); + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--control-medium-paddingInline-condensed); + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; + grid-template-rows: min-content; + grid-template-areas: 'spacer leadingAction leadingVisual content'; + grid-template-columns: min-content min-content min-content minmax(0, auto); + align-items: start; + + /* column-gap persists with empty grid-areas, margin applies only when children exist */ + & > :not(:last-child, .Spacer) { + /* stylelint-disable-next-line primer/spacing */ + margin-right: var(--control-medium-gap); + } + + &:hover { + text-decoration: none; + } + + /* collapsible item [aria-expanded] */ + + /* target items inside expanded subgroups */ + &[aria-expanded] { + & + .SubGroup { + @media screen and (prefers-reduced-motion: no-preference) { + transition: + opacity 160ms cubic-bezier(0.25, 1, 0.5, 1), + transform 160ms cubic-bezier(0.25, 1, 0.5, 1); + } + } + } + + &[aria-expanded='true'] { + & .ExpandIcon { + transition: transform 120ms linear; + transform: scaleY(-1); + } + + & .SubGroup { + height: auto; + overflow: visible; + visibility: visible; + opacity: 1; + transform: translateY(0); + } + + &.ActionListContent--hasActiveSubItem { + & > .ItemLabel { + font-weight: var(--base-text-weight-semibold); + } + } + } + + &[aria-expanded='false'] { + & .ExpandIcon { + transition: transform 120ms linear; + transform: scaleY(1); + } + + & + .SubGroup { + height: 0; + overflow: hidden; + visibility: hidden; + opacity: 0; + transform: translateY(calc(-1 * var(--base-size-16))); + } + + /* show active indicator on parent collapse if child is active */ + &:has(+ .SubGroup [data-active='true']) { + background: var(--control-transparent-bgColor-selected); + + & .ItemLabel { + font-weight: var(--base-text-weight-semibold); + } + + & .ActionListSubContent::before, + & + .ActionListItem .ActionListSubContent::before { + visibility: hidden; + } + + /* blue accent line */ + &::after { + position: absolute; + top: calc(50% - var(--base-size-12)); + left: calc(-1 * var(--base-size-8)); + width: var(--base-size-4); + height: var(--base-size-24); + content: ''; + background: var(--bgColor-accent-emphasis); + border-radius: var(--borderRadius-medium); + } + } + } +} + +/* [ [content] [trailingVisual] [trailingAction] ] */ +.ActionListSubContent { + grid-area: content; + position: relative; + display: grid; + width: 100%; + grid-template-rows: min-content; + grid-template-areas: 'label trailingVisual trailingAction'; + grid-template-columns: minmax(0, auto) min-content min-content; + align-items: start; +} + +/* place children on grid */ + +/* spacer used to create depth for nested lists */ + +.Spacer { + display: none; + width: max(0px, var(--subitem-depth) * 8px); + grid-area: spacer; +} + +.LeadingAction { + grid-area: leadingAction; +} + +.LeadingVisual { + grid-area: leadingVisual; +} + +.TrailingVisual { + grid-area: trailingVisual; + font-size: var(--text-body-size-medium); +} + +.TrailingAction { + grid-area: trailingAction; +} + +/* wrapper span + default block */ +.ItemDescriptionWrap { + grid-area: label; + display: flex; + flex-direction: column; + gap: var(--base-size-4); + + & .ItemLabel { + font-weight: var(--base-text-weight-semibold); + word-break: break-word; + } + + /* inline */ + &:where([data-description-variant='inline']) { + position: relative; + word-break: normal; + flex-direction: row; + align-items: baseline; + gap: var(--base-size-8); + + & .ItemLabel { + word-break: normal; + } + + &:has([data-truncate='true']) { + & .ItemLabel { + flex: 1 0 auto; + } + } + + & .Description { + /* adjust line-height for baseline alignment */ + /* stylelint-disable-next-line primer/typography */ + line-height: calc(var(--control-medium-lineBoxHeight) - var(--base-size-2)); + } + } +} + +/* description */ +.Description { + font-size: var(--text-body-size-small); + font-weight: var(--base-text-weight-normal); + + /* line-height: var(--text-caption-lineHeight); */ + + /* remove after FF ships */ + /* stylelint-disable-next-line primer/typography */ + line-height: 16px; + color: var(--fgColor-muted); +} + +/* helper for grid alignment with multi-line content + span wrapping svg or text */ +.VisualWrap { + display: flex; + min-height: var(--control-medium-lineBoxHeight); + /* stylelint-disable-next-line primer/typography */ + line-height: 20px; /* temporary until we fix line-height rounding in primitives */ + color: var(--fgColor-muted); + pointer-events: none; + fill: var(--fgColor-muted); + align-items: center; +} + +/* text */ +.ItemLabel { + position: relative; + font-size: var(--text-body-size-medium); + font-weight: var(--base-text-weight-normal); + /* stylelint-disable-next-line primer/typography */ + line-height: 20px; /* temporary until we fix line-height rounding in primitives */ + color: var(--fgColor-default); + grid-area: label; + word-break: break-word; +} + +.SubGroup .ItemLabel { + font-size: var(--text-body-size-small); +} + +/* trailing action icon button */ + +.TrailingActionButton { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.InactiveButtonWrap { + &:has(.TrailingVisual) { + grid-area: trailingVisual; + } + + &:has(.LeadingVisual) { + grid-area: leadingVisual; + } +} + .Divider { display: block; height: var(--borderWidth-thin); @@ -77,3 +613,45 @@ background: var(--borderColor-muted); border: 0; } + +.InactiveButtonReset { + display: flex; + padding: 0; + font: inherit; + color: inherit; + cursor: pointer; + background: none; + border: none; +} + +.InactiveWarning { + font-size: var(--text-body-size-small); + + /* line-height: var(--text-caption-lineHeight); */ + + /* use variable when FF removed */ + /* stylelint-disable-next-line primer/typography */ + line-height: 16px; + color: var(--fgColor-attention); + grid-row: 2/2; +} + +@keyframes checkmarkIn { + from { + clip-path: inset(var(--base-size-16) 0 0 0); + } + + to { + clip-path: inset(0 0 0 0); + } +} + +@keyframes checkmarkOut { + from { + clip-path: inset(0 0 0 0); + } + + to { + clip-path: inset(var(--base-size-16) 0 0 0); + } +} diff --git a/packages/react/src/ActionList/Description.tsx b/packages/react/src/ActionList/Description.tsx index 68bc4f661f9..c61f78d2189 100644 --- a/packages/react/src/ActionList/Description.tsx +++ b/packages/react/src/ActionList/Description.tsx @@ -4,6 +4,10 @@ import Truncate from '../Truncate' import type {SxProp} from '../sx' import {merge} from '../sx' import {ItemContext} from './shared' +import {useFeatureFlag} from '../FeatureFlags' +import classes from './ActionList.module.css' +import {clsx} from 'clsx' +import {defaultSxProp} from '../utils/defaultSxProp' export type ActionListDescriptionProps = { /** @@ -22,7 +26,7 @@ export type ActionListDescriptionProps = { export const Description: React.FC> = ({ variant = 'inline', - sx = {}, + sx = defaultSxProp, className, truncate, ...props @@ -42,6 +46,65 @@ export const Description: React.FC + {props.children} + + ) + } else { + return ( + + {props.children} + + ) + } + } + if (variant === 'block' || !truncate) { + return ( + + {props.children} + + ) + } else { + return ( + + {props.children} + + ) + } + } + return variant === 'block' || !truncate ? ( Date: Mon, 9 Dec 2024 11:12:05 -0800 Subject: [PATCH 03/27] fix padding --- .../src/ActionList/ActionList.module.css | 2 +- packages/react/src/ActionList/LinkItem.tsx | 153 +++++++++++++----- packages/react/src/ActionList/List.tsx | 4 +- packages/react/src/ActionList/Selection.tsx | 1 - 4 files changed, 112 insertions(+), 48 deletions(-) diff --git a/packages/react/src/ActionList/ActionList.module.css b/packages/react/src/ActionList/ActionList.module.css index 15a588ef6ca..fec2f690928 100644 --- a/packages/react/src/ActionList/ActionList.module.css +++ b/packages/react/src/ActionList/ActionList.module.css @@ -13,7 +13,7 @@ &:where([data-variant='inset']) { /* change to padding (all) when Item is converted */ - padding-block: var(--base-size-8); + padding: var(--base-size-8); } &:where([data-dividers='true']) { diff --git a/packages/react/src/ActionList/LinkItem.tsx b/packages/react/src/ActionList/LinkItem.tsx index c878a821d1b..49dd6389f54 100644 --- a/packages/react/src/ActionList/LinkItem.tsx +++ b/packages/react/src/ActionList/LinkItem.tsx @@ -6,6 +6,8 @@ import {merge} from '../sx' import {Item} from './Item' import type {ActionListItemProps} from './shared' import Box from '../Box' +import {defaultSxProp} from '../utils/defaultSxProp' +import {useFeatureFlag} from '../FeatureFlags' // adopted from React.AnchorHTMLAttributes type LinkProps = { @@ -18,56 +20,119 @@ type LinkProps = { target?: string type?: string referrerPolicy?: React.AnchorHTMLAttributes['referrerPolicy'] + className?: string } // LinkItem does not support selected, loading, variants, etc. -export type ActionListLinkItemProps = Pick & +export type ActionListLinkItemProps = Pick< + ActionListItemProps, + 'active' | 'children' | 'sx' | 'inactiveText' | 'variant' +> & LinkProps -export const LinkItem = React.forwardRef(({sx = {}, active, inactiveText, as: Component, ...props}, forwardedRef) => { - const styles = { - // occupy full size of Item - paddingX: 2, - paddingY: '6px', // custom value off the scale - display: 'flex', - flexGrow: 1, // full width - borderRadius: 2, +export const LinkItem = React.forwardRef( + ({sx = defaultSxProp, active, inactiveText, variant, as: Component, className, ...props}, forwardedRef) => { + const styles = { + // occupy full size of Item + paddingX: 2, + paddingY: '6px', // custom value off the scale + display: 'flex', + flexGrow: 1, // full width + borderRadius: 2, - // inherit Item styles - color: 'inherit', - '&:hover': {color: 'inherit', textDecoration: 'none'}, - } + // inherit Item styles + color: 'inherit', + '&:hover': {color: 'inherit', textDecoration: 'none'}, + } - return ( - { - const clickHandler = (event: React.MouseEvent) => { - onClick && onClick(event) - props.onClick && props.onClick(event as React.MouseEvent) - } - return inactiveText ? ( - - {children} - - ) : ( - { + const clickHandler = (event: React.MouseEvent) => { + onClick && onClick(event) + props.onClick && props.onClick(event as React.MouseEvent) + } + return inactiveText ? ( + {children} + ) : ( + + {children} + + ) + }} > - {children} - + {props.children} + ) - }} - > - {props.children} - - ) -}) as PolymorphicForwardRefComponent<'a', ActionListLinkItemProps> + } + + return ( + { + const clickHandler = (event: React.MouseEvent) => { + onClick && onClick(event) + props.onClick && props.onClick(event as React.MouseEvent) + } + return inactiveText ? ( + {children} + ) : ( + + {children} + + ) + }} + > + {props.children} + + ) + } + + return ( + { + const clickHandler = (event: React.MouseEvent) => { + onClick && onClick(event) + props.onClick && props.onClick(event as React.MouseEvent) + } + return inactiveText ? ( + + {children} + + ) : ( + + {children} + + ) + }} + > + {props.children} + + ) + }, +) as PolymorphicForwardRefComponent<'a', ActionListLinkItemProps> diff --git a/packages/react/src/ActionList/List.tsx b/packages/react/src/ActionList/List.tsx index 874ce8195ef..4b8ea6af1a6 100644 --- a/packages/react/src/ActionList/List.tsx +++ b/packages/react/src/ActionList/List.tsx @@ -22,7 +22,7 @@ export const List = React.forwardRef( const styles = { margin: 0, paddingInlineStart: 0, // reset ul styles - paddingY: variant === 'inset' ? 2 : 0, + padding: variant === 'inset' ? 2 : 0, } const [slots, childrenWithoutSlots] = useSlots(props.children, { @@ -39,7 +39,7 @@ export const List = React.forwardRef( enableFocusZone: enableFocusZoneFromContainer, } = React.useContext(ActionListContainerContext) - const ariaLabelledBy = slots.heading ? (slots.heading.props.id ?? headingId) : listLabelledBy + const ariaLabelledBy = slots.heading ? slots.heading.props.id ?? headingId : listLabelledBy const listRole = role || listRoleFromContainer const listRef = useProvidedRefOrCreate(forwardedRef as React.RefObject) diff --git a/packages/react/src/ActionList/Selection.tsx b/packages/react/src/ActionList/Selection.tsx index 0e106fb005b..c5f8c4bbb4d 100644 --- a/packages/react/src/ActionList/Selection.tsx +++ b/packages/react/src/ActionList/Selection.tsx @@ -38,7 +38,6 @@ export const Selection: React.FC> = ({se return ( - {/* {selected && } */} ) } From e1e1e8506995b4b6022b80dc2460acad3117ddff Mon Sep 17 00:00:00 2001 From: Katie Langerman <18661030+langermank@users.noreply.github.com> Date: Mon, 9 Dec 2024 18:09:36 -0800 Subject: [PATCH 04/27] add heading --- .../src/ActionList/ActionList.module.css | 35 ++-- .../react/src/ActionList/ActionList.test.tsx | 129 +------------- .../react/src/ActionList/Group.module.css | 40 +++++ packages/react/src/ActionList/Group.test.tsx | 150 ++++++++++++++++ packages/react/src/ActionList/Group.tsx | 160 ++++++++++++------ packages/react/src/ActionList/Item.tsx | 2 +- 6 files changed, 319 insertions(+), 197 deletions(-) create mode 100644 packages/react/src/ActionList/Group.module.css create mode 100644 packages/react/src/ActionList/Group.test.tsx diff --git a/packages/react/src/ActionList/ActionList.module.css b/packages/react/src/ActionList/ActionList.module.css index fec2f690928..59f71a4b3a9 100644 --- a/packages/react/src/ActionList/ActionList.module.css +++ b/packages/react/src/ActionList/ActionList.module.css @@ -206,6 +206,21 @@ color: var(--fgColor-muted); } } + + /* hide dividers */ + @media (hover: hover) { + &:hover { + & .ActionListSubContent::before, + & + .ActionListItem .ActionListSubContent::before { + visibility: hidden; + } + + & [data-description-variant='inline']::before, + & + .ActionListItem [data-description-variant='inline']::before { + visibility: hidden; + } + } + } } /* if item has subitem, move hover styles to ActionListContent */ @@ -255,21 +270,6 @@ } } - /* hide dividers */ - @media (hover: hover) { - &:hover { - & .ActionListSubContent::before, - & + .ActionListItem .ActionListSubContent::before { - visibility: hidden; - } - - & [data-description-variant='inline']::before, - & + .ActionListItem [data-description-variant='inline']::before { - visibility: hidden; - } - } - } - /* Make sure that the first visible item isn't a divider */ &[aria-hidden] + .Divider { display: none; @@ -385,6 +385,7 @@ &:hover { text-decoration: none; + cursor: pointer; } /* collapsible item [aria-expanded] */ @@ -535,8 +536,10 @@ & .Description { /* adjust line-height for baseline alignment */ + + /* line-height: calc(var(--control-medium-lineBoxHeight) - var(--base-size-2)); */ /* stylelint-disable-next-line primer/typography */ - line-height: calc(var(--control-medium-lineBoxHeight) - var(--base-size-2)); + line-height: 16px; } } } diff --git a/packages/react/src/ActionList/ActionList.test.tsx b/packages/react/src/ActionList/ActionList.test.tsx index 11ce02d53fc..b79503c3497 100644 --- a/packages/react/src/ActionList/ActionList.test.tsx +++ b/packages/react/src/ActionList/ActionList.test.tsx @@ -6,7 +6,7 @@ import theme from '../theme' import {ActionList} from '.' import {BookIcon} from '@primer/octicons-react' import {behavesAsComponent, checkExports} from '../utils/testing' -import {BaseStyles, ThemeProvider, ActionMenu} from '..' +import {BaseStyles, ThemeProvider} from '..' import {FeatureFlags} from '../FeatureFlags' function SimpleActionList(): JSX.Element { @@ -237,133 +237,6 @@ describe('ActionList', () => { expect(onClick).toHaveBeenCalled() }) - it('should throw an error when ActionList.GroupHeading has an `as` prop when it is used within ActionMenu context', async () => { - const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn()) - expect(() => - HTMLRender( - - - - Trigger - - - - Group Heading - - - - - - , - ), - ).toThrow( - "Looks like you are trying to set a heading level to a menu role. Group headings for menu type action lists are for representational purposes, and rendered as divs. Therefore they don't need a heading level.", - ) - expect(spy).toHaveBeenCalled() - spy.mockRestore() - }) - - it('should render the ActionList.GroupHeading component as a heading with the given heading level', async () => { - const container = HTMLRender( - - Heading - - Group Heading - - , - ) - const heading = container.getByRole('heading', {level: 2}) - expect(heading).toBeInTheDocument() - expect(heading).toHaveTextContent('Group Heading') - }) - it('should throw an error if ActionList.GroupHeading is used without an `as` prop when no role is specified (for list role)', async () => { - const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn()) - expect(() => - HTMLRender( - - Heading - - Group Heading - Item - - , - ), - ).toThrow( - "You are setting a heading for a list, that requires a heading level. Please use 'as' prop to set a proper heading level.", - ) - expect(spy).toHaveBeenCalled() - spy.mockRestore() - }) - it('should render the ActionList.GroupHeading component as a span (not a heading tag) when role is specified as listbox', async () => { - const container = HTMLRender( - - Heading - - Group Heading - - , - ) - const label = container.getByText('Group Heading') - expect(label).toBeInTheDocument() - expect(label.tagName).toEqual('SPAN') - }) - it('should render the ActionList.GroupHeading component as a span with role="presentation" and aria-hidden="true" when role is specified as listbox', async () => { - const container = HTMLRender( - - Heading - - Group Heading - - , - ) - const label = container.getByText('Group Heading') - const wrapper = label.parentElement - expect(wrapper).toHaveAttribute('role', 'presentation') - expect(wrapper).toHaveAttribute('aria-hidden', 'true') - }) - it('should label the list with the group heading id', async () => { - const {container, getByText} = HTMLRender( - - Heading - - Group Heading - Item - - , - ) - const list = container.querySelector(`li[data-test-id='ActionList.Group'] > ul`) - const heading = getByText('Group Heading') - expect(list).toHaveAttribute('aria-labelledby', heading.id) - }) - it('should NOT label the list with the group heading id when role is specified', async () => { - const {container, getByText} = HTMLRender( - - Heading - - Group Heading - Item - - , - ) - const list = container.querySelector(`li[data-test-id='ActionList.Group'] > ul`) - const heading = getByText('Group Heading') - expect(list).not.toHaveAttribute('aria-labelledby', heading.id) - }) - it('should label the list using aria-label when role is specified', async () => { - const {container, getByText} = HTMLRender( - - Heading - - Group Heading - Item - - , - ) - const list = container.querySelector(`li[data-test-id='ActionList.Group'] > ul`) - const heading = getByText('Group Heading') - expect(list).toHaveAttribute('aria-label', heading.textContent) - }) - it('should render ActionList.Item as button when feature flag is enabled', async () => { const featureFlag = { primer_react_action_list_item_as_button: true, diff --git a/packages/react/src/ActionList/Group.module.css b/packages/react/src/ActionList/Group.module.css new file mode 100644 index 00000000000..9082f68d699 --- /dev/null +++ b/packages/react/src/ActionList/Group.module.css @@ -0,0 +1,40 @@ +.Group:not(:first-child) { + margin-block-start: var(--base-size-8); +} + +.GroupHeadingWrap { + display: flex; + font-size: var(--text-body-size-small); + font-weight: var(--base-text-weight-semibold); + + /* line-height: var(--text-body-lineHeight-small); use when FF rolls out */ + /* stylelint-disable-next-line primer/typography */ + line-height: 18px; + color: var(--fgColor-muted); + flex-direction: column; + padding-inline: var(--base-size-8); + padding-block: var(--base-size-8); + + &:where([data-variant='filled']) { + /* stylelint-disable-next-line primer/spacing */ + margin-block-start: calc(var(--base-size-8) - var(--borderWidth-thin)); + margin-block-end: var(--base-size-8); + margin-inline: calc(-1 * var(--base-size-8)); + background: var(--bgColor-muted); + border-top: solid var(--borderWidth-thin) var(--borderColor-muted); + border-bottom: solid var(--borderWidth-thin) var(--borderColor-muted); + padding-inline: var(--base-size-16); + + &:first-child { + margin-block-start: 0; + } + } +} + +.GroupHeading { + margin: 0; + font-size: var(--text-body-size-small); + font-weight: var(--base-text-weight-semibold); + color: var(--fgColor-muted); + align-self: flex-start; +} diff --git a/packages/react/src/ActionList/Group.test.tsx b/packages/react/src/ActionList/Group.test.tsx new file mode 100644 index 00000000000..742cd50c105 --- /dev/null +++ b/packages/react/src/ActionList/Group.test.tsx @@ -0,0 +1,150 @@ +import {render as HTMLRender} from '@testing-library/react' +import React from 'react' +import theme from '../theme' +import {ActionList} from '.' +import {BaseStyles, ThemeProvider, ActionMenu} from '..' +import {FeatureFlags} from '../FeatureFlags' + +describe('ActionList.Group', () => { + it('should throw an error when ActionList.GroupHeading has an `as` prop when it is used within ActionMenu context', async () => { + const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn()) + expect(() => + HTMLRender( + + + + Trigger + + + + Group Heading + + + + + + , + ), + ).toThrow( + "Looks like you are trying to set a heading level to a menu role. Group headings for menu type action lists are for representational purposes, and rendered as divs. Therefore they don't need a heading level.", + ) + expect(spy).toHaveBeenCalled() + spy.mockRestore() + }) + + it('should render the ActionList.GroupHeading component as a heading with the given heading level', async () => { + const container = HTMLRender( + + Heading + + Group Heading + + , + ) + const heading = container.getByRole('heading', {level: 2}) + expect(heading).toBeInTheDocument() + expect(heading).toHaveTextContent('Group Heading') + }) + it('should throw an error if ActionList.GroupHeading is used without an `as` prop when no role is specified (for list role)', async () => { + const spy = jest.spyOn(console, 'error').mockImplementation(() => jest.fn()) + expect(() => + HTMLRender( + + Heading + + Group Heading + Item + + , + ), + ).toThrow( + "You are setting a heading for a list, that requires a heading level. Please use 'as' prop to set a proper heading level.", + ) + expect(spy).toHaveBeenCalled() + spy.mockRestore() + }) + it('should render the ActionList.GroupHeading component as a span (not a heading tag) when role is specified as listbox', async () => { + const container = HTMLRender( + + Heading + + Group Heading + + , + ) + const label = container.getByText('Group Heading') + expect(label).toBeInTheDocument() + expect(label.tagName).toEqual('SPAN') + }) + it('should render the ActionList.GroupHeading component as a span with role="presentation" and aria-hidden="true" when role is specified as listbox', async () => { + const container = HTMLRender( + + Heading + + Group Heading + + , + ) + const label = container.getByText('Group Heading') + const wrapper = label.parentElement + expect(wrapper).toHaveAttribute('role', 'presentation') + expect(wrapper).toHaveAttribute('aria-hidden', 'true') + }) + it('should label the list with the group heading id', async () => { + const {container, getByText} = HTMLRender( + + Heading + + Group Heading + Item + + , + ) + const list = container.querySelector(`li[data-test-id='ActionList.Group'] > ul`) + const heading = getByText('Group Heading') + expect(list).toHaveAttribute('aria-labelledby', heading.id) + }) + it('should NOT label the list with the group heading id when role is specified', async () => { + const {container, getByText} = HTMLRender( + + Heading + + Group Heading + Item + + , + ) + const list = container.querySelector(`li[data-test-id='ActionList.Group'] > ul`) + const heading = getByText('Group Heading') + expect(list).not.toHaveAttribute('aria-labelledby', heading.id) + }) + + it('should support a custom `className` on the outermost element', () => { + const Element = () => { + return ( + + + + Test + + + + ) + } + const FeatureFlagElement = () => { + return ( + + + + ) + } + expect(HTMLRender().container.querySelector('h2')).toHaveClass('test-class-name') + expect(HTMLRender().container.querySelector('h2')).toHaveClass('test-class-name') + }) +}) diff --git a/packages/react/src/ActionList/Group.tsx b/packages/react/src/ActionList/Group.tsx index d0db9a77ff2..3ea517abc4b 100644 --- a/packages/react/src/ActionList/Group.tsx +++ b/packages/react/src/ActionList/Group.tsx @@ -4,12 +4,29 @@ import Box from '../Box' import type {SxProp} from '../sx' import {ListContext, type ActionListProps} from './shared' import type {AriaRole} from '../utils/types' -import {default as Heading} from '../Heading' import type {ActionListHeadingProps} from './Heading' import {useSlots} from '../hooks/useSlots' import {defaultSxProp} from '../utils/defaultSxProp' import {invariant} from '../utils/invariant' import {clsx} from 'clsx' +import {useFeatureFlag} from '../FeatureFlags' +import classes from './ActionList.module.css' +import groupClasses from './Group.module.css' + +type HeadingProps = { + as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' + className?: string + children: React.ReactNode +} & SxProp + +const Heading: React.FC = ({as: Component = 'h3', className, children, sx = defaultSxProp}) => { + return ( + // Box is temporary to support lingering sx usage + + {children} + + ) +} export type ActionListGroupProps = { /** @@ -18,7 +35,7 @@ export type ActionListGroupProps = { * - `"filled"` - Superimposed on a background, offset from nearby content * - `"subtle"` - Relatively less offset from nearby content */ - variant?: 'subtle' | 'filled' + variant?: 'filled' | 'subtle' /** * @deprecated (Use `ActionList.GroupHeading` instead. i.e. → Group title) */ @@ -50,9 +67,10 @@ export const Group: React.FC> = ({ auxiliaryText, selectionVariant, role, - sx = {}, + sx = defaultSxProp, ...props }) => { + const enabled = useFeatureFlag('primer_react_css_modules_team') const id = useId() const {role: listRole} = React.useContext(ListContext) @@ -72,6 +90,54 @@ export const Group: React.FC> = ({ groupHeadingId = id } + if (enabled) { + if (sx !== defaultSxProp) { + return ( + + + {title && !slots.groupHeading ? ( + // Escape hatch: supports old API in a non breaking way + + ) : null} + {/* Supports new API ActionList.GroupHeading */} + {!title && slots.groupHeading ? React.cloneElement(slots.groupHeading) : null} + + + + ) + } + return ( +
  • + + {title && !slots.groupHeading ? ( + // Escape hatch: supports old API in a non breaking way + + ) : null} + {/* Supports new API ActionList.GroupHeading */} + {!title && slots.groupHeading ? React.cloneElement(slots.groupHeading) : null} + + +
  • + ) + } return ( > = ({ // because the heading is hidden from the accessibility tree and only used for presentation role. // We will instead use aria-label to label the list. See a line below. aria-labelledby={listRole ? undefined : groupHeadingId} - aria-label={listRole ? (title ?? (slots.groupHeading?.props.children as string)) : undefined} + aria-label={listRole ? title ?? (slots.groupHeading?.props.children as string) : undefined} role={role || (listRole && 'group')} > {slots.groupHeading ? childrenWithoutSlots : props.children} @@ -113,6 +179,7 @@ export type ActionListGroupHeadingProps = Pick & { as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' _internalBackwardCompatibleTitle?: string + variant?: 'filled' | 'subtle' } /** @@ -125,7 +192,7 @@ export type ActionListGroupHeadingProps = Pick> = ({ as, - variant, + variant = 'subtle', // We are not recommending this prop to be used, it should only be used internally for incremental rollout. _internalBackwardCompatibleTitle, auxiliaryText, @@ -134,7 +201,7 @@ export const GroupHeading: React.FC { - const {variant: listVariant, role: listRole} = React.useContext(ListContext) + const {role: listRole} = React.useContext(ListContext) const {groupHeadingId} = React.useContext(GroupContext) // for list role, the headings are proper heading tags, for menu and listbox, they are just representational and divs const missingAsForList = (listRole === undefined || listRole === 'list') && children !== undefined && as === undefined @@ -152,58 +219,47 @@ export const GroupHeading: React.FC {/* for listbox (SelectPanel) and menu (ActionMenu) roles, group titles are presentational. */} {listRole && listRole !== 'list' ? ( - + ) : ( // for explicit (role="list" is passed as prop) and implicit list roles (ActionList ins rendered as list by default), group titles are proper heading tags. - - - {_internalBackwardCompatibleTitle ?? children} - - {auxiliaryText &&
    {auxiliaryText}
    } -
    +
    + {sx !== defaultSxProp ? ( + + {_internalBackwardCompatibleTitle ?? children} + + ) : ( + + {_internalBackwardCompatibleTitle ?? children} + + )} + {auxiliaryText &&
    {auxiliaryText}
    } +
    )} ) diff --git a/packages/react/src/ActionList/Item.tsx b/packages/react/src/ActionList/Item.tsx index 1c8c2e6b95a..85cffd6f31b 100644 --- a/packages/react/src/ActionList/Item.tsx +++ b/packages/react/src/ActionList/Item.tsx @@ -244,7 +244,7 @@ export const Item = React.forwardRef( display: 'block', position: 'absolute', width: '100%', - top: '-8px', + top: '-6px', border: '0 solid', borderTopWidth: showDividers ? `1px` : '0', borderColor: 'var(--divider-color, transparent)', From d5e827585d8a7d5b7e8319a8c0e812ec4f45bcd7 Mon Sep 17 00:00:00 2001 From: Katie Langerman <18661030+langermank@users.noreply.github.com> Date: Tue, 10 Dec 2024 11:27:27 -0800 Subject: [PATCH 05/27] fix selectpanel story --- packages/react/src/SelectPanel/SelectPanel.features.stories.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react/src/SelectPanel/SelectPanel.features.stories.tsx b/packages/react/src/SelectPanel/SelectPanel.features.stories.tsx index da4bb2084be..32c707dcfd7 100644 --- a/packages/react/src/SelectPanel/SelectPanel.features.stories.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.features.stories.tsx @@ -347,7 +347,6 @@ export const WithGroups = () => { return ( ( From e6034c26928aadf3be70cee7bbc10878de83ae30 Mon Sep 17 00:00:00 2001 From: Katie Langerman <18661030+langermank@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:43:51 -0800 Subject: [PATCH 06/27] vrt --- .../src/mixins/activeIndicatorLine.css | 10 ++++ .../src/ActionList/ActionList.module.css | 57 +++++++++++++------ .../react/src/ActionList/Group.module.css | 2 +- packages/react/src/ActionList/Item.tsx | 2 +- 4 files changed, 51 insertions(+), 20 deletions(-) create mode 100644 packages/postcss-preset-primer/src/mixins/activeIndicatorLine.css diff --git a/packages/postcss-preset-primer/src/mixins/activeIndicatorLine.css b/packages/postcss-preset-primer/src/mixins/activeIndicatorLine.css new file mode 100644 index 00000000000..7c499f43c11 --- /dev/null +++ b/packages/postcss-preset-primer/src/mixins/activeIndicatorLine.css @@ -0,0 +1,10 @@ +@define-mixin activeIndicatorLine { + position: absolute; + top: calc(50% - var(--base-size-12)); + left: calc(-1 * var(--base-size-8)); + width: var(--base-size-4); + height: var(--base-size-24); + content: ''; + background: var(--bgColor-accent-emphasis); + border-radius: var(--borderRadius-medium); +} diff --git a/packages/react/src/ActionList/ActionList.module.css b/packages/react/src/ActionList/ActionList.module.css index 59f71a4b3a9..889abb2d4f6 100644 --- a/packages/react/src/ActionList/ActionList.module.css +++ b/packages/react/src/ActionList/ActionList.module.css @@ -20,8 +20,12 @@ /* place dividers on the wrapper that excludes leading visuals/actions */ & .ActionListSubContent::before { position: absolute; + + /* use this top size after FF removed */ + + /* top: calc(-1 * var(--control-medium-paddingBlock)); */ /* stylelint-disable-next-line primer/spacing */ - top: calc(-1 * var(--control-medium-paddingBlock)); + top: -7px; display: block; width: 100%; height: 1px; @@ -34,8 +38,12 @@ & [data-description-variant='inline'] { &::before { position: absolute; + + /* use this top size after FF removed */ + + /* top: calc(-1 * var(--control-medium-paddingBlock)); */ /* stylelint-disable-next-line primer/spacing */ - top: calc(-1 * var(--control-medium-paddingBlock)); + top: -7px; display: block; width: 100%; height: var(--borderWidth-thin); @@ -166,14 +174,34 @@ /* blue accent line */ &::after { - position: absolute; - top: calc(50% - var(--base-size-12)); - left: calc(-1 * var(--base-size-8)); - width: var(--base-size-4); - height: var(--base-size-24); - content: ''; - background: var(--bgColor-accent-emphasis); - border-radius: var(--borderRadius-medium); + @mixin activeIndicatorLine; + } + } + + &:where([data-is-active-descendant]) { + background: var(--control-transparent-bgColor-selected); + + /* provides a visual indication of the current item for Windows high-contrast mode */ + outline: 2px solid transparent; + + /* @media (hover: hover) { + &:hover { + background-color: var(--control-transparent-bgColor-hover); + } + } */ + + /* hide dividers if showDividers is true and item is active */ + + /* add back in after FF ship */ + + /* & .ActionListSubContent::before, + & + .ActionListItem .ActionListSubContent::before { + visibility: hidden; + } */ + + /* blue accent line */ + &::after { + @mixin activeIndicatorLine; } } @@ -451,14 +479,7 @@ /* blue accent line */ &::after { - position: absolute; - top: calc(50% - var(--base-size-12)); - left: calc(-1 * var(--base-size-8)); - width: var(--base-size-4); - height: var(--base-size-24); - content: ''; - background: var(--bgColor-accent-emphasis); - border-radius: var(--borderRadius-medium); + @mixin activeIndicatorLine; } } } diff --git a/packages/react/src/ActionList/Group.module.css b/packages/react/src/ActionList/Group.module.css index 9082f68d699..284923552e2 100644 --- a/packages/react/src/ActionList/Group.module.css +++ b/packages/react/src/ActionList/Group.module.css @@ -13,7 +13,7 @@ color: var(--fgColor-muted); flex-direction: column; padding-inline: var(--base-size-8); - padding-block: var(--base-size-8); + padding-block: var(--base-size-6); &:where([data-variant='filled']) { /* stylelint-disable-next-line primer/spacing */ diff --git a/packages/react/src/ActionList/Item.tsx b/packages/react/src/ActionList/Item.tsx index 85cffd6f31b..37cd95fd888 100644 --- a/packages/react/src/ActionList/Item.tsx +++ b/packages/react/src/ActionList/Item.tsx @@ -244,7 +244,7 @@ export const Item = React.forwardRef( display: 'block', position: 'absolute', width: '100%', - top: '-6px', + top: '-7px', border: '0 solid', borderTopWidth: showDividers ? `1px` : '0', borderColor: 'var(--divider-color, transparent)', From 9fe5820240ff7ad671519addd4f97ab67e5a5d84 Mon Sep 17 00:00:00 2001 From: Katie Langerman <18661030+langermank@users.noreply.github.com> Date: Tue, 10 Dec 2024 14:15:23 -0800 Subject: [PATCH 07/27] alignment --- packages/react/src/ActionList/ActionList.module.css | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react/src/ActionList/ActionList.module.css b/packages/react/src/ActionList/ActionList.module.css index 889abb2d4f6..ef671499a1d 100644 --- a/packages/react/src/ActionList/ActionList.module.css +++ b/packages/react/src/ActionList/ActionList.module.css @@ -314,7 +314,6 @@ width: var(--base-size-16); height: var(--base-size-16); margin: 0; - margin-top: var(--base-size-2); /* 2px to center align with label (20px line-height) */ cursor: pointer; background-color: var(--bgColor-default); border: var(--borderWidth-thin) solid var(--control-borderColor-emphasis); From 1f4d117ce70f3cd01c5a47ebc6f5d338e618e1de Mon Sep 17 00:00:00 2001 From: Katie Langerman <18661030+langermank@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:17:32 -0800 Subject: [PATCH 08/27] moar fixes --- packages/react/src/ActionList/Selection.tsx | 4 ++-- packages/react/src/ActionList/Visuals.tsx | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/react/src/ActionList/Selection.tsx b/packages/react/src/ActionList/Selection.tsx index c5f8c4bbb4d..c0f0ac8a41b 100644 --- a/packages/react/src/ActionList/Selection.tsx +++ b/packages/react/src/ActionList/Selection.tsx @@ -82,7 +82,7 @@ export const Selection: React.FC> = ({se > = ({se margin: '0', placeContent: 'center', width: 'var(--base-size-16, 16px)', - backgroundColor: selected ? 'accent.fg' : 'canvas.default', + backgroundColor: selected ? 'var(--control-checked-bgColor-rest)' : 'canvas.default', transition: selected ? 'background-color, border-color 80ms cubic-bezier(0.33, 1, 0.68, 1)' : 'background-color, border-color 80ms cubic-bezier(0.32, 0, 0.67, 0) 0ms', diff --git a/packages/react/src/ActionList/Visuals.tsx b/packages/react/src/ActionList/Visuals.tsx index 1d3373609c4..7ff6b2419b9 100644 --- a/packages/react/src/ActionList/Visuals.tsx +++ b/packages/react/src/ActionList/Visuals.tsx @@ -36,7 +36,6 @@ export const LeadingVisualContainer: React.FC Date: Tue, 10 Dec 2024 15:26:25 -0800 Subject: [PATCH 09/27] dev stories --- .../src/ActionList/ActionList.dev.stories.tsx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/react/src/ActionList/ActionList.dev.stories.tsx b/packages/react/src/ActionList/ActionList.dev.stories.tsx index 35c37cf213f..bf8b7456b7f 100644 --- a/packages/react/src/ActionList/ActionList.dev.stories.tsx +++ b/packages/react/src/ActionList/ActionList.dev.stories.tsx @@ -7,6 +7,7 @@ import {Group} from './Group' import {Divider} from './Divider' import {Description} from './Description' import Avatar from '../Avatar' +import {FileDirectoryIcon} from '@primer/octicons-react' export default { title: 'Components/ActionList/Dev', @@ -144,3 +145,23 @@ export const HeadingCustomClassname = () => (
    ) + +export const DescriptionCustomClassname = () => ( + + + Label + This is a description + + +) + +export const VisualCustomClassname = () => ( + + + Label + + + + + +) From 5c76a851dd7995b47ae6562748f70fab0ab926ae Mon Sep 17 00:00:00 2001 From: Katie Langerman <18661030+langermank@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:22:50 -0800 Subject: [PATCH 10/27] tests --- .../src/ActionList/ActionList.module.css | 607 +++++++++--------- .../react/src/ActionList/ActionList.test.tsx | 66 +- .../react/src/ActionList/Description.test.tsx | 88 +++ 3 files changed, 417 insertions(+), 344 deletions(-) create mode 100644 packages/react/src/ActionList/Description.test.tsx diff --git a/packages/react/src/ActionList/ActionList.module.css b/packages/react/src/ActionList/ActionList.module.css index ef671499a1d..52c8c3fc8ff 100644 --- a/packages/react/src/ActionList/ActionList.module.css +++ b/packages/react/src/ActionList/ActionList.module.css @@ -296,385 +296,404 @@ background-color: transparent; } } - } - /* Make sure that the first visible item isn't a divider */ - &[aria-hidden] + .Divider { - display: none; - } + & .MultiSelectCheckbox { + background-color: var(--control-bgColor-disabled); + border-color: var(--control-borderColor-disabled); + } + + &[aria-checked='true'], + &[aria-selected='true'] { + & .MultiSelectCheckbox { + background-color: var(--control-checked-bgColor-disabled); + /* stylelint-disable-next-line primer/colors */ + border-color: var(--control-checked-bgColor-disabled); + + &::before { + /* stylelint-disable-next-line primer/colors */ + background-color: var(--control-checked-fgColor-disabled); + } + } + } - /* + /* Make sure that the first visible item isn't a divider */ + &[aria-hidden] + .Divider { + display: none; + } + + /* * checkbox item [aria-checked] * listbox [aria-selected] */ - & .MultiSelectCheckbox { - position: relative; - display: grid; - width: var(--base-size-16); - height: var(--base-size-16); - margin: 0; - cursor: pointer; - background-color: var(--bgColor-default); - border: var(--borderWidth-thin) solid var(--control-borderColor-emphasis); - border-radius: var(--borderRadius-small); - transition: - background-color, - border-color 80ms cubic-bezier(0.33, 1, 0.68, 1); /* checked -> unchecked - add 120ms delay to fully see animation-out */ - - place-content: center; - - &::before { + & .MultiSelectCheckbox { + position: relative; + display: grid; width: var(--base-size-16); height: var(--base-size-16); - content: ''; - /* stylelint-disable-next-line primer/colors */ - background-color: var(--control-checked-fgColor-rest); - transition: visibility 0s linear 230ms; - clip-path: inset(var(--base-size-16) 0 0 0); - - /* octicon checkmark image */ - mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOSIgdmlld0JveD0iMCAwIDEyIDkiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTEuNzgwMyAwLjIxOTYyNUMxMS45MjEgMC4zNjA0MjcgMTIgMC41NTEzMDUgMTIgMC43NTAzMTNDMTIgMC45NDkzMjEgMTEuOTIxIDEuMTQwMTkgMTEuNzgwMyAxLjI4MUw0LjUxODYgOC41NDA0MkM0LjM3Nzc1IDguNjgxIDQuMTg2ODIgOC43NiAzLjk4Nzc0IDguNzZDMy43ODg2NyA4Ljc2IDMuNTk3NzMgOC42ODEgMy40NTY4OSA4LjU0MDQyTDAuMjAxNjIyIDUuMjg2MkMwLjA2ODkyNzcgNS4xNDM4MyAtMC4wMDMzMDkwNSA0Ljk1NTU1IDAuMDAwMTE2NDkzIDQuNzYwOThDMC4wMDM1NTIwNSA0LjU2NjQzIDAuMDgyMzg5NCA0LjM4MDgxIDAuMjIwMDMyIDQuMjQzMjFDMC4zNTc2NjUgNC4xMDU2MiAwLjU0MzM1NSA0LjAyNjgxIDAuNzM3OTcgNC4wMjMzOEMwLjkzMjU4NCA0LjAxOTk0IDEuMTIwOTMgNC4wOTIxNyAxLjI2MzM0IDQuMjI0ODJMMy45ODc3NCA2Ljk0ODM1TDEwLjcxODYgMC4yMTk2MjVDMTAuODU5NSAwLjA3ODk5MjMgMTEuMDUwNCAwIDExLjI0OTUgMEMxMS40NDg1IDAgMTEuNjM5NSAwLjA3ODk5MjMgMTEuNzgwMyAwLjIxOTYyNVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo='); - mask-size: 75%; - mask-repeat: no-repeat; - mask-position: center; - animation: checkmarkOut 80ms cubic-bezier(0.65, 0, 0.35, 1); /* forwards; slightly snappier animation out */ - } - } - - &[aria-checked='true'], - &[aria-selected='true'] { - & .MultiSelectCheckbox { - background-color: var(--control-checked-bgColor-rest); - border-color: var(--control-checked-borderColor-rest); + margin: 0; + cursor: pointer; + background-color: var(--bgColor-default); + border: var(--borderWidth-thin) solid var(--control-borderColor-emphasis); + border-radius: var(--borderRadius-small); transition: background-color, - border-color 80ms cubic-bezier(0.32, 0, 0.67, 0) 0ms; /* unchecked -> checked */ + border-color 80ms cubic-bezier(0.33, 1, 0.68, 1); /* checked -> unchecked - add 120ms delay to fully see animation-out */ + + place-content: center; &::before { - visibility: visible; - transition: visibility 0s linear 0s; - animation: checkmarkIn 80ms cubic-bezier(0.65, 0, 0.35, 1) forwards 80ms; + width: var(--base-size-16); + height: var(--base-size-16); + content: ''; + /* stylelint-disable-next-line primer/colors */ + background-color: var(--control-checked-fgColor-rest); + transition: visibility 0s linear 230ms; + clip-path: inset(var(--base-size-16) 0 0 0); + + /* octicon checkmark image */ + mask-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOSIgdmlld0JveD0iMCAwIDEyIDkiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTEuNzgwMyAwLjIxOTYyNUMxMS45MjEgMC4zNjA0MjcgMTIgMC41NTEzMDUgMTIgMC43NTAzMTNDMTIgMC45NDkzMjEgMTEuOTIxIDEuMTQwMTkgMTEuNzgwMyAxLjI4MUw0LjUxODYgOC41NDA0MkM0LjM3Nzc1IDguNjgxIDQuMTg2ODIgOC43NiAzLjk4Nzc0IDguNzZDMy43ODg2NyA4Ljc2IDMuNTk3NzMgOC42ODEgMy40NTY4OSA4LjU0MDQyTDAuMjAxNjIyIDUuMjg2MkMwLjA2ODkyNzcgNS4xNDM4MyAtMC4wMDMzMDkwNSA0Ljk1NTU1IDAuMDAwMTE2NDkzIDQuNzYwOThDMC4wMDM1NTIwNSA0LjU2NjQzIDAuMDgyMzg5NCA0LjM4MDgxIDAuMjIwMDMyIDQuMjQzMjFDMC4zNTc2NjUgNC4xMDU2MiAwLjU0MzM1NSA0LjAyNjgxIDAuNzM3OTcgNC4wMjMzOEMwLjkzMjU4NCA0LjAxOTk0IDEuMTIwOTMgNC4wOTIxNyAxLjI2MzM0IDQuMjI0ODJMMy45ODc3NCA2Ljk0ODM1TDEwLjcxODYgMC4yMTk2MjVDMTAuODU5NSAwLjA3ODk5MjMgMTEuMDUwNCAwIDExLjI0OTUgMEMxMS40NDg1IDAgMTEuNjM5NSAwLjA3ODk5MjMgMTEuNzgwMyAwLjIxOTYyNVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo='); + mask-size: 75%; + mask-repeat: no-repeat; + mask-position: center; + animation: checkmarkOut 80ms cubic-bezier(0.65, 0, 0.35, 1); /* forwards; slightly snappier animation out */ } } - & .SingleSelectCheckmark { - visibility: visible; - } - } + &[aria-checked='true'], + &[aria-selected='true'] { + & .MultiSelectCheckbox { + background-color: var(--control-checked-bgColor-rest); + border-color: var(--control-checked-borderColor-rest); + transition: + background-color, + border-color 80ms cubic-bezier(0.32, 0, 0.67, 0) 0ms; /* unchecked -> checked */ - &[aria-checked='false'], - &[aria-selected='false'] { - & .MultiSelectCheckbox { - &::before { - visibility: hidden; + &::before { + visibility: visible; + transition: visibility 0s linear 0s; + animation: checkmarkIn 80ms cubic-bezier(0.65, 0, 0.35, 1) forwards 80ms; + } } - } - & .SingleSelectCheckmark { - visibility: hidden; + & .SingleSelectCheckmark { + visibility: visible; + } } - } -} - -/* button or a tag */ -/* [ [spacer] [leadingAction] [leadingVisual] [content] ] */ -.ActionListContent { - --subitem-depth: 0px; + &[aria-checked='false'], + &[aria-selected='false'] { + & .MultiSelectCheckbox { + &::before { + visibility: hidden; + } + } - position: relative; - display: grid; - width: 100%; - color: var(--control-fgColor-rest); - text-align: left; - user-select: none; - background-color: transparent; - border: none; - border-radius: var(--borderRadius-medium); - transition: background 33.333ms linear; - /* stylelint-disable-next-line primer/spacing */ - padding-block: var(--control-medium-paddingBlock); - /* stylelint-disable-next-line primer/spacing */ - padding-inline: var(--control-medium-paddingInline-condensed); - touch-action: manipulation; - -webkit-tap-highlight-color: transparent; - grid-template-rows: min-content; - grid-template-areas: 'spacer leadingAction leadingVisual content'; - grid-template-columns: min-content min-content min-content minmax(0, auto); - align-items: start; - - /* column-gap persists with empty grid-areas, margin applies only when children exist */ - & > :not(:last-child, .Spacer) { - /* stylelint-disable-next-line primer/spacing */ - margin-right: var(--control-medium-gap); + & .SingleSelectCheckmark { + visibility: hidden; + } + } } - &:hover { - text-decoration: none; - cursor: pointer; - } + /* button or a tag */ - /* collapsible item [aria-expanded] */ + /* [ [spacer] [leadingAction] [leadingVisual] [content] ] */ + .ActionListContent { + --subitem-depth: 0px; - /* target items inside expanded subgroups */ - &[aria-expanded] { - & + .SubGroup { - @media screen and (prefers-reduced-motion: no-preference) { - transition: - opacity 160ms cubic-bezier(0.25, 1, 0.5, 1), - transform 160ms cubic-bezier(0.25, 1, 0.5, 1); - } + position: relative; + display: grid; + width: 100%; + color: var(--control-fgColor-rest); + text-align: left; + user-select: none; + background-color: transparent; + border: none; + border-radius: var(--borderRadius-medium); + transition: background 33.333ms linear; + /* stylelint-disable-next-line primer/spacing */ + padding-block: var(--control-medium-paddingBlock); + /* stylelint-disable-next-line primer/spacing */ + padding-inline: var(--control-medium-paddingInline-condensed); + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; + grid-template-rows: min-content; + grid-template-areas: 'spacer leadingAction leadingVisual content'; + grid-template-columns: min-content min-content min-content minmax(0, auto); + align-items: start; + + /* column-gap persists with empty grid-areas, margin applies only when children exist */ + & > :not(:last-child, .Spacer) { + /* stylelint-disable-next-line primer/spacing */ + margin-right: var(--control-medium-gap); } - } - &[aria-expanded='true'] { - & .ExpandIcon { - transition: transform 120ms linear; - transform: scaleY(-1); + &:hover { + text-decoration: none; + cursor: pointer; } - & .SubGroup { - height: auto; - overflow: visible; - visibility: visible; - opacity: 1; - transform: translateY(0); - } + /* collapsible item [aria-expanded] */ - &.ActionListContent--hasActiveSubItem { - & > .ItemLabel { - font-weight: var(--base-text-weight-semibold); + /* target items inside expanded subgroups */ + &[aria-expanded] { + & + .SubGroup { + @media screen and (prefers-reduced-motion: no-preference) { + transition: + opacity 160ms cubic-bezier(0.25, 1, 0.5, 1), + transform 160ms cubic-bezier(0.25, 1, 0.5, 1); + } } } - } - &[aria-expanded='false'] { - & .ExpandIcon { - transition: transform 120ms linear; - transform: scaleY(1); - } + &[aria-expanded='true'] { + & .ExpandIcon { + transition: transform 120ms linear; + transform: scaleY(-1); + } - & + .SubGroup { - height: 0; - overflow: hidden; - visibility: hidden; - opacity: 0; - transform: translateY(calc(-1 * var(--base-size-16))); - } + & .SubGroup { + height: auto; + overflow: visible; + visibility: visible; + opacity: 1; + transform: translateY(0); + } - /* show active indicator on parent collapse if child is active */ - &:has(+ .SubGroup [data-active='true']) { - background: var(--control-transparent-bgColor-selected); + &.ActionListContent--hasActiveSubItem { + & > .ItemLabel { + font-weight: var(--base-text-weight-semibold); + } + } + } - & .ItemLabel { - font-weight: var(--base-text-weight-semibold); + &[aria-expanded='false'] { + & .ExpandIcon { + transition: transform 120ms linear; + transform: scaleY(1); } - & .ActionListSubContent::before, - & + .ActionListItem .ActionListSubContent::before { + & + .SubGroup { + height: 0; + overflow: hidden; visibility: hidden; + opacity: 0; + transform: translateY(calc(-1 * var(--base-size-16))); } - /* blue accent line */ - &::after { - @mixin activeIndicatorLine; + /* show active indicator on parent collapse if child is active */ + &:has(+ .SubGroup [data-active='true']) { + background: var(--control-transparent-bgColor-selected); + + & .ItemLabel { + font-weight: var(--base-text-weight-semibold); + } + + & .ActionListSubContent::before, + & + .ActionListItem .ActionListSubContent::before { + visibility: hidden; + } + + /* blue accent line */ + &::after { + @mixin activeIndicatorLine; + } } } } -} - -/* [ [content] [trailingVisual] [trailingAction] ] */ -.ActionListSubContent { - grid-area: content; - position: relative; - display: grid; - width: 100%; - grid-template-rows: min-content; - grid-template-areas: 'label trailingVisual trailingAction'; - grid-template-columns: minmax(0, auto) min-content min-content; - align-items: start; -} -/* place children on grid */ + /* [ [content] [trailingVisual] [trailingAction] ] */ + .ActionListSubContent { + grid-area: content; + position: relative; + display: grid; + width: 100%; + grid-template-rows: min-content; + grid-template-areas: 'label trailingVisual trailingAction'; + grid-template-columns: minmax(0, auto) min-content min-content; + align-items: start; + } -/* spacer used to create depth for nested lists */ + /* place children on grid */ -.Spacer { - display: none; - width: max(0px, var(--subitem-depth) * 8px); - grid-area: spacer; -} + /* spacer used to create depth for nested lists */ -.LeadingAction { - grid-area: leadingAction; -} + .Spacer { + display: none; + width: max(0px, var(--subitem-depth) * 8px); + grid-area: spacer; + } -.LeadingVisual { - grid-area: leadingVisual; -} + .LeadingAction { + grid-area: leadingAction; + } -.TrailingVisual { - grid-area: trailingVisual; - font-size: var(--text-body-size-medium); -} + .LeadingVisual { + grid-area: leadingVisual; + } -.TrailingAction { - grid-area: trailingAction; -} + .TrailingVisual { + grid-area: trailingVisual; + font-size: var(--text-body-size-medium); + } -/* wrapper span - default block */ -.ItemDescriptionWrap { - grid-area: label; - display: flex; - flex-direction: column; - gap: var(--base-size-4); - - & .ItemLabel { - font-weight: var(--base-text-weight-semibold); - word-break: break-word; + .TrailingAction { + grid-area: trailingAction; } - /* inline */ - &:where([data-description-variant='inline']) { - position: relative; - word-break: normal; - flex-direction: row; - align-items: baseline; - gap: var(--base-size-8); + /* wrapper span + default block */ + .ItemDescriptionWrap { + grid-area: label; + display: flex; + flex-direction: column; + gap: var(--base-size-4); & .ItemLabel { - word-break: normal; + font-weight: var(--base-text-weight-semibold); + word-break: break-word; } - &:has([data-truncate='true']) { + /* inline */ + &:where([data-description-variant='inline']) { + position: relative; + word-break: normal; + flex-direction: row; + align-items: baseline; + gap: var(--base-size-8); + & .ItemLabel { - flex: 1 0 auto; + word-break: normal; + } + + &:has([data-truncate='true']) { + & .ItemLabel { + flex: 1 0 auto; + } } - } - & .Description { - /* adjust line-height for baseline alignment */ + & .Description { + /* adjust line-height for baseline alignment */ - /* line-height: calc(var(--control-medium-lineBoxHeight) - var(--base-size-2)); */ - /* stylelint-disable-next-line primer/typography */ - line-height: 16px; + /* line-height: calc(var(--control-medium-lineBoxHeight) - var(--base-size-2)); */ + /* stylelint-disable-next-line primer/typography */ + line-height: 16px; + } } } -} -/* description */ -.Description { - font-size: var(--text-body-size-small); - font-weight: var(--base-text-weight-normal); + /* description */ + .Description { + font-size: var(--text-body-size-small); + font-weight: var(--base-text-weight-normal); - /* line-height: var(--text-caption-lineHeight); */ + /* line-height: var(--text-caption-lineHeight); */ - /* remove after FF ships */ - /* stylelint-disable-next-line primer/typography */ - line-height: 16px; - color: var(--fgColor-muted); -} + /* remove after FF ships */ + /* stylelint-disable-next-line primer/typography */ + line-height: 16px; + color: var(--fgColor-muted); + } -/* helper for grid alignment with multi-line content + /* helper for grid alignment with multi-line content span wrapping svg or text */ -.VisualWrap { - display: flex; - min-height: var(--control-medium-lineBoxHeight); - /* stylelint-disable-next-line primer/typography */ - line-height: 20px; /* temporary until we fix line-height rounding in primitives */ - color: var(--fgColor-muted); - pointer-events: none; - fill: var(--fgColor-muted); - align-items: center; -} + .VisualWrap { + display: flex; + min-height: var(--control-medium-lineBoxHeight); + /* stylelint-disable-next-line primer/typography */ + line-height: 20px; /* temporary until we fix line-height rounding in primitives */ + color: var(--fgColor-muted); + pointer-events: none; + fill: var(--fgColor-muted); + align-items: center; + } -/* text */ -.ItemLabel { - position: relative; - font-size: var(--text-body-size-medium); - font-weight: var(--base-text-weight-normal); - /* stylelint-disable-next-line primer/typography */ - line-height: 20px; /* temporary until we fix line-height rounding in primitives */ - color: var(--fgColor-default); - grid-area: label; - word-break: break-word; -} + /* text */ + .ItemLabel { + position: relative; + font-size: var(--text-body-size-medium); + font-weight: var(--base-text-weight-normal); + /* stylelint-disable-next-line primer/typography */ + line-height: 20px; /* temporary until we fix line-height rounding in primitives */ + color: var(--fgColor-default); + grid-area: label; + word-break: break-word; + } -.SubGroup .ItemLabel { - font-size: var(--text-body-size-small); -} + .SubGroup .ItemLabel { + font-size: var(--text-body-size-small); + } -/* trailing action icon button */ + /* trailing action icon button */ -.TrailingActionButton { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -.InactiveButtonWrap { - &:has(.TrailingVisual) { - grid-area: trailingVisual; + .TrailingActionButton { + border-top-left-radius: 0; + border-bottom-left-radius: 0; } - &:has(.LeadingVisual) { - grid-area: leadingVisual; - } -} + .InactiveButtonWrap { + &:has(.TrailingVisual) { + grid-area: trailingVisual; + } -.Divider { - display: block; - height: var(--borderWidth-thin); - padding: 0; - /* stylelint-disable-next-line primer/spacing */ - margin-block-start: calc(var(--base-size-8) - var(--borderWidth-thin)); - margin-block-end: var(--base-size-8); - margin-inline: calc(-1 * var(--base-size-8)); - list-style: none; - /* stylelint-disable-next-line primer/colors */ - background: var(--borderColor-muted); - border: 0; -} + &:has(.LeadingVisual) { + grid-area: leadingVisual; + } + } -.InactiveButtonReset { - display: flex; - padding: 0; - font: inherit; - color: inherit; - cursor: pointer; - background: none; - border: none; -} + .Divider { + display: block; + height: var(--borderWidth-thin); + padding: 0; + /* stylelint-disable-next-line primer/spacing */ + margin-block-start: calc(var(--base-size-8) - var(--borderWidth-thin)); + margin-block-end: var(--base-size-8); + margin-inline: calc(-1 * var(--base-size-8)); + list-style: none; + /* stylelint-disable-next-line primer/colors */ + background: var(--borderColor-muted); + border: 0; + } -.InactiveWarning { - font-size: var(--text-body-size-small); + .InactiveButtonReset { + display: flex; + padding: 0; + font: inherit; + color: inherit; + cursor: pointer; + background: none; + border: none; + } - /* line-height: var(--text-caption-lineHeight); */ + .InactiveWarning { + font-size: var(--text-body-size-small); - /* use variable when FF removed */ - /* stylelint-disable-next-line primer/typography */ - line-height: 16px; - color: var(--fgColor-attention); - grid-row: 2/2; -} + /* line-height: var(--text-caption-lineHeight); */ -@keyframes checkmarkIn { - from { - clip-path: inset(var(--base-size-16) 0 0 0); + /* use variable when FF removed */ + /* stylelint-disable-next-line primer/typography */ + line-height: 16px; + color: var(--fgColor-attention); + grid-row: 2/2; } - to { - clip-path: inset(0 0 0 0); - } -} + @keyframes checkmarkIn { + from { + clip-path: inset(var(--base-size-16) 0 0 0); + } -@keyframes checkmarkOut { - from { - clip-path: inset(0 0 0 0); + to { + clip-path: inset(0 0 0 0); + } } - to { - clip-path: inset(var(--base-size-16) 0 0 0); + @keyframes checkmarkOut { + from { + clip-path: inset(0 0 0 0); + } + + to { + clip-path: inset(var(--base-size-16) 0 0 0); + } } } diff --git a/packages/react/src/ActionList/ActionList.test.tsx b/packages/react/src/ActionList/ActionList.test.tsx index b79503c3497..62d96ead4c4 100644 --- a/packages/react/src/ActionList/ActionList.test.tsx +++ b/packages/react/src/ActionList/ActionList.test.tsx @@ -397,7 +397,14 @@ describe('ActionList', () => { const mockOnSelect = jest.fn() const user = userEvent.setup() const {getByRole} = HTMLRender( - + Item 1 @@ -415,7 +422,14 @@ describe('ActionList', () => { it('should not render buttons when feature flag is enabled and is specified role', async () => { const {getByRole} = HTMLRender( - + Item 1 Item 2 @@ -476,54 +490,6 @@ describe('ActionList', () => { expect(document.activeElement).toHaveTextContent('Option 4') }) - describe('ActionList.Description', () => { - it('should render the description as inline without truncation by default', () => { - const {getByText} = HTMLRender( - - - Item 1Item 1 description - - , - ) - - const description = getByText('Item 1 description') - expect(description.tagName).toBe('SPAN') - expect(description).toHaveStyleRule('flex-basis', 'auto') - expect(description).not.toHaveStyleRule('overflow', 'ellipsis') - expect(description).not.toHaveStyleRule('white-space', 'nowrap') - }) - it('should render the description as `Truncate` when truncate is true', () => { - const {getByText} = HTMLRender( - - - Item 1Item 1 description - - , - ) - - const description = getByText('Item 1 description') - expect(description.tagName).toBe('DIV') - expect(description).toHaveAttribute('title', 'Item 1 description') - expect(description).toHaveStyleRule('flex-basis', '0') - expect(description).toHaveStyleRule('text-overflow', 'ellipsis') - expect(description).toHaveStyleRule('overflow', 'hidden') - expect(description).toHaveStyleRule('white-space', 'nowrap') - }) - it('should render the description in a new line when variant is block', () => { - const {getByText} = HTMLRender( - - - Item 1Item 1 description - - , - ) - - const description = getByText('Item 1 description') - expect(description.tagName).toBe('SPAN') - expect(description.parentElement).toHaveAttribute('data-component', 'ActionList.Item--DividerContainer') - }) - }) - it('should support a custom `className` on the outermost element', () => { const Element = () => { return ( diff --git a/packages/react/src/ActionList/Description.test.tsx b/packages/react/src/ActionList/Description.test.tsx new file mode 100644 index 00000000000..8763b4ad748 --- /dev/null +++ b/packages/react/src/ActionList/Description.test.tsx @@ -0,0 +1,88 @@ +import {render as HTMLRender, waitFor, fireEvent} from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import axe from 'axe-core' +import React from 'react' +import theme from '../theme' +import {ActionList} from '.' +import {BookIcon} from '@primer/octicons-react' +import {behavesAsComponent, checkExports} from '../utils/testing' +import {BaseStyles, ThemeProvider} from '..' +import {FeatureFlags} from '../FeatureFlags' + +describe('ActionList.Description', () => { + it('should render the description as inline without truncation by default', () => { + const {getByText} = HTMLRender( + + + Item 1Item 1 description + + , + ) + + const description = getByText('Item 1 description') + expect(description.tagName).toBe('SPAN') + expect(description).toHaveStyleRule('flex-basis', 'auto') + expect(description).not.toHaveStyleRule('overflow', 'ellipsis') + expect(description).not.toHaveStyleRule('white-space', 'nowrap') + }) + it('should render the description as `Truncate` when truncate is true', () => { + const {getByText} = HTMLRender( + + + Item 1Item 1 description + + , + ) + + const description = getByText('Item 1 description') + expect(description.tagName).toBe('DIV') + expect(description).toHaveAttribute('title', 'Item 1 description') + expect(description).toHaveStyleRule('flex-basis', '0') + expect(description).toHaveStyleRule('text-overflow', 'ellipsis') + expect(description).toHaveStyleRule('overflow', 'hidden') + expect(description).toHaveStyleRule('white-space', 'nowrap') + }) + it('should render the description in a new line when variant is block', () => { + const {getByText} = HTMLRender( + + + Item 1Item 1 description + + , + ) + + const description = getByText('Item 1 description') + expect(description.tagName).toBe('SPAN') + expect(description.parentElement).toHaveAttribute('data-component', 'ActionList.Item--DividerContainer') + }) + it('should support a custom `className`', () => { + const Element = () => { + return ( + + + Item 1Item 1 description + + + ) + } + const FeatureFlagElement = () => { + return ( + + + + ) + } + expect( + HTMLRender().container.querySelector('span[data-component="ActionList.Description"]'), + ).toHaveClass('test-class-name') + expect( + HTMLRender().container.querySelector('span[data-component="ActionList.Description"]'), + ).toHaveClass('test-class-name') + }) +}) From 3c980ddb28edb75d481907c7d44e595af5a73a47 Mon Sep 17 00:00:00 2001 From: Katie Langerman <18661030+langermank@users.noreply.github.com> Date: Wed, 11 Dec 2024 08:17:05 -0800 Subject: [PATCH 11/27] moving tests around --- .../react/src/ActionList/ActionList.test.tsx | 574 ++++-------------- packages/react/src/ActionList/Item.test.tsx | 350 +++++++++++ .../__snapshots__/NavList.test.tsx.snap | 216 +++---- .../__snapshots__/Autocomplete.test.tsx.snap | 67 +- 4 files changed, 572 insertions(+), 635 deletions(-) create mode 100644 packages/react/src/ActionList/Item.test.tsx diff --git a/packages/react/src/ActionList/ActionList.test.tsx b/packages/react/src/ActionList/ActionList.test.tsx index 62d96ead4c4..c2fc27676a9 100644 --- a/packages/react/src/ActionList/ActionList.test.tsx +++ b/packages/react/src/ActionList/ActionList.test.tsx @@ -75,471 +75,111 @@ describe('ActionList', () => { ActionList, }) - it('should have aria-keyshortcuts applied to the correct element', async () => { - const {container} = HTMLRender() - - const linkOptions = await waitFor(() => container.querySelectorAll('a')) - - expect(linkOptions[0]).toHaveAttribute('aria-keyshortcuts', 'd') - expect(linkOptions[0].parentElement).not.toHaveAttribute('aria-keyshortcuts', 'd') - }) - - it('should have no axe violations', async () => { - const {container} = HTMLRender() - const results = await axe.run(container) - expect(results).toHaveNoViolations() - }) - - it('should fire onSelect on click and keypress', async () => { - const component = HTMLRender() - const options = await waitFor(() => component.getAllByRole('option')) - - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[1]).toHaveAttribute('aria-selected', 'false') - - fireEvent.click(options[1]) - - expect(options[0]).toHaveAttribute('aria-selected', 'false') - expect(options[1]).toHaveAttribute('aria-selected', 'true') - - // We pass keycode here to navigate a implementation detail in react-testing-library - // https://github.com/testing-library/react-testing-library/issues/269#issuecomment-455854112 - fireEvent.keyPress(options[0], {key: 'Enter', charCode: 13}) - - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[1]).toHaveAttribute('aria-selected', 'false') - - fireEvent.keyPress(options[1], {key: ' ', charCode: 32}) - - expect(options[0]).toHaveAttribute('aria-selected', 'false') - expect(options[1]).toHaveAttribute('aria-selected', 'true') - }) - - it('should skip onSelect on disabled items', async () => { - const component = HTMLRender() - const options = await waitFor(() => component.getAllByRole('option')) - - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[2]).toHaveAttribute('aria-selected', 'false') - - fireEvent.click(options[2]) - - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[2]).toHaveAttribute('aria-selected', 'false') - - fireEvent.keyPress(options[2], {key: 'Enter', charCode: 13}) - - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[2]).toHaveAttribute('aria-selected', 'false') - }) - - it('should skip onSelect on inactive items', async () => { - const component = HTMLRender() - const options = await waitFor(() => component.getAllByRole('option')) - - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[3]).toHaveAttribute('aria-selected', 'false') - - fireEvent.click(options[3]) - - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[3]).toHaveAttribute('aria-selected', 'false') - - fireEvent.keyPress(options[3], {key: 'Enter', charCode: 13}) - - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[3]).toHaveAttribute('aria-selected', 'false') - }) - - it('should skip onSelect on loading items', async () => { - const component = HTMLRender() - const options = await waitFor(() => component.getAllByRole('option')) - - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[4]).toHaveAttribute('aria-selected', 'false') - - fireEvent.click(options[4]) - - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[4]).toHaveAttribute('aria-selected', 'false') - - fireEvent.keyPress(options[3], {key: 'Enter', charCode: 13}) - - expect(options[0]).toHaveAttribute('aria-selected', 'true') - expect(options[4]).toHaveAttribute('aria-selected', 'false') - }) - - it('should throw when selected is provided without a selectionVariant on parent', async () => { - // we expect console.error to be called, so we suppress that in the test - const mockError = jest.spyOn(console, 'error').mockImplementation(() => jest.fn()) - - expect(() => { - HTMLRender( - - - Primer React - - , - ) - }).toThrow('For Item to be selected, ActionList or ActionList.Group needs to have a selectionVariant defined') - - mockError.mockRestore() - }) - - it('should not crash when clicking an item without an onSelect', async () => { - const component = HTMLRender( - - Primer React - , - ) - const option = await waitFor(() => component.getByRole('option')) - expect(option).toBeInTheDocument() - - fireEvent.click(option) - fireEvent.keyPress(option, {key: 'Enter', charCode: 13}) - expect(option).toBeInTheDocument() - }) - - it('should focus the button around the leading visual when tabbing to an inactive item', async () => { - const component = HTMLRender() - const inactiveOptionButton = await waitFor(() => component.getByRole('button', {name: projects[3].inactiveText})) - - await userEvent.tab() // get focus on first element - await userEvent.keyboard('{ArrowDown}') - await userEvent.keyboard('{ArrowDown}') - expect(inactiveOptionButton).toHaveFocus() - }) - - it('should behave as inactive if both inactiveText and loading props are passed', async () => { - const component = HTMLRender() - const inactiveOptionButton = await waitFor(() => component.getByRole('button', {name: projects[5].inactiveText})) - - await userEvent.tab() // get focus on first element - await userEvent.keyboard('{ArrowDown}') - await userEvent.keyboard('{ArrowDown}') - await userEvent.keyboard('{ArrowDown}') - await userEvent.keyboard('{ArrowDown}') - - expect(inactiveOptionButton).toHaveFocus() - }) - - it('should call onClick for a link item', async () => { - const onClick = jest.fn() - const component = HTMLRender( - - - Primer React - - , - ) - const link = await waitFor(() => component.getByRole('link')) - fireEvent.click(link) - expect(onClick).toHaveBeenCalled() - }) - - it('should render ActionList.Item as button when feature flag is enabled', async () => { - const featureFlag = { - primer_react_action_list_item_as_button: true, - } - - const {container} = HTMLRender( - - - Item 1 - Item 2 - - , - ) - - const button = container.querySelector('button') - expect(button).toHaveTextContent('Item 1') - - // Ensure passed prop "disabled" is applied to the button - expect(button).toHaveAttribute('aria-disabled', 'true') - - const listItems = container.querySelectorAll('li') - expect(listItems.length).toBe(2) - }) - - it('should render ActionList.Item as li when feature flag is disabled', async () => { - const {container} = HTMLRender( - - - Item 1 - Item 2 - - , - ) - - const listitem = container.querySelector('li') - const button = container.querySelector('button') - - expect(listitem).toHaveTextContent('Item 1') - expect(listitem).toHaveAttribute('tabindex', '0') - expect(button).toBeNull() - - const listItems = container.querySelectorAll('li') - expect(listItems.length).toBe(2) - }) - - it('should apply ref to ActionList.Item when feature flag is disabled', async () => { - const MockComponent = () => { - const ref = React.useRef(null) - - const focusRef = () => { - if (ref.current) ref.current.focus() - } - - return ( - - - - Item 1 - Item 2 - - - ) - } - - const {getByRole} = HTMLRender() - const triggerBtn = getByRole('button', {name: 'Prompt'}) - const focusTarget = getByRole('listitem', {name: 'Item 1'}) - - fireEvent.click(triggerBtn) - - expect(document.activeElement).toBe(focusTarget) - }) - - it('should render ActionList.Item as li when feature flag is enabled and has proper aria role', async () => { - const {container} = HTMLRender( - - - Item 1 - Item 2 - - , - ) - - const listitem = container.querySelector('li') - const button = container.querySelector('button') - - expect(listitem).toHaveTextContent('Item 1') - expect(listitem).toHaveAttribute('tabindex', '0') - expect(button).toBeNull() - - const listItems = container.querySelectorAll('li') - expect(listItems.length).toBe(2) - }) - - it('should render the trailing action as a button (default)', async () => { - const {container} = HTMLRender( - - - Item 1 - - - , - ) - - const action = container.querySelector('button[aria-labelledby]') - expect(action).toHaveAccessibleName('Action') - }) - - it('should render the trailing action as a link', async () => { - const {container} = HTMLRender( - - - Item 1 - - - , - ) - - const action = container.querySelector('a[href="#"][aria-labelledby]') - expect(action).toHaveAccessibleName('Action') - }) - - it('should do action when trailing action is clicked', async () => { - const onClick = jest.fn() - const component = HTMLRender( - - - Item 1 - - - , - ) - - const trailingAction = await waitFor(() => component.getByRole('button', {name: 'Action'})) - fireEvent.click(trailingAction) - expect(onClick).toHaveBeenCalled() - }) - - it('should focus the trailing action', async () => { - HTMLRender( - - - Item 1 - - - , - ) - - await userEvent.tab() - expect(document.activeElement).toHaveTextContent('Item 1') - await userEvent.tab() - expect(document.activeElement).toHaveAccessibleName('Action') - }) - - it('should only trigger a key event once when feature flag is enabled', async () => { - const mockOnSelect = jest.fn() - const user = userEvent.setup() - const {getByRole} = HTMLRender( - - - Item 1 - - , - ) - const item = getByRole('button') - - item.focus() - - expect(document.activeElement).toBe(item) - await user.keyboard('{Enter}') - - expect(mockOnSelect).toHaveBeenCalledTimes(1) - }) - - it('should not render buttons when feature flag is enabled and is specified role', async () => { - const {getByRole} = HTMLRender( - - - Item 1 - Item 2 - Item 3 - Item 4 - Item 5 - - , - ) - - const option = getByRole('option') - expect(option.tagName).toBe('LI') - expect(option.textContent).toBe('Item 1') - - const menuItem = getByRole('menuitem') - expect(menuItem.tagName).toBe('LI') - - const menuItemCheckbox = getByRole('menuitemcheckbox') - expect(menuItemCheckbox.tagName).toBe('LI') - - const menuItemRadio = getByRole('menuitemradio') - expect(menuItemRadio.tagName).toBe('LI') - - const button = getByRole('button') - expect(button.parentElement?.tagName).toBe('LI') - expect(button.textContent).toBe('Item 5') - }) - - it('should be navigatable with arrow keys for certain roles', async () => { - HTMLRender( - - Option 1 - Option 2 - - Option 3 - - Option 4 - - Option 5 - - , - ) - - await userEvent.tab() // tab into the story, this should focus on the first button - expect(document.activeElement).toHaveTextContent('Option 1') - - await userEvent.keyboard('{ArrowDown}') - expect(document.activeElement).toHaveTextContent('Option 2') - - await userEvent.keyboard('{ArrowDown}') - expect(document.activeElement).not.toHaveTextContent('Option 3') // option 3 is disabled - expect(document.activeElement).toHaveTextContent('Option 4') - - await userEvent.keyboard('{ArrowDown}') - expect(document.activeElement).toHaveAccessibleName('Unavailable due to an outage') - - await userEvent.keyboard('{ArrowUp}') - expect(document.activeElement).toHaveTextContent('Option 4') - }) - - it('should support a custom `className` on the outermost element', () => { - const Element = () => { - return ( - - Item - - ) - } - const FeatureFlagElement = () => { - return ( - - - - ) - } - expect(HTMLRender().container.querySelector('ul')).toHaveClass('test-class-name') - expect(HTMLRender().container.querySelector('ul')).toHaveClass('test-class-name') - }) - - it('divider should support a custom `className`', () => { - const Element = () => { - return ( - - Item - - - ) - } - const FeatureFlagElement = () => { - return ( - - - - ) - } - expect(HTMLRender().container.querySelector('li[aria-hidden="true"]')).toHaveClass( - 'test-class-name', - ) - expect(HTMLRender().container.querySelector('li[aria-hidden="true"]')).toHaveClass('test-class-name') - }) + // it('should have no axe violations', async () => { + // const {container} = HTMLRender() + // const results = await axe.run(container) + // expect(results).toHaveNoViolations() + // }) + + // it('should throw when selected is provided without a selectionVariant on parent', async () => { + // // we expect console.error to be called, so we suppress that in the test + // const mockError = jest.spyOn(console, 'error').mockImplementation(() => jest.fn()) + + // expect(() => { + // HTMLRender( + // + // + // Primer React + // + // , + // ) + // }).toThrow('For Item to be selected, ActionList or ActionList.Group needs to have a selectionVariant defined') + + // mockError.mockRestore() + // }) + + // it('should be navigatable with arrow keys for certain roles', async () => { + // HTMLRender( + // + // Option 1 + // Option 2 + // + // Option 3 + // + // Option 4 + // + // Option 5 + // + // , + // ) + + // await userEvent.tab() // tab into the story, this should focus on the first button + // expect(document.activeElement).toHaveTextContent('Option 1') + + // await userEvent.keyboard('{ArrowDown}') + // expect(document.activeElement).toHaveTextContent('Option 2') + + // await userEvent.keyboard('{ArrowDown}') + // expect(document.activeElement).not.toHaveTextContent('Option 3') // option 3 is disabled + // expect(document.activeElement).toHaveTextContent('Option 4') + + // await userEvent.keyboard('{ArrowDown}') + // expect(document.activeElement).toHaveAccessibleName('Unavailable due to an outage') + + // await userEvent.keyboard('{ArrowUp}') + // expect(document.activeElement).toHaveTextContent('Option 4') + // }) + + // it('should support a custom `className` on the outermost element', () => { + // const Element = () => { + // return ( + // + // Item + // + // ) + // } + // const FeatureFlagElement = () => { + // return ( + // + // + // + // ) + // } + // expect(HTMLRender().container.querySelector('ul')).toHaveClass('test-class-name') + // expect(HTMLRender().container.querySelector('ul')).toHaveClass('test-class-name') + // }) + + // it('divider should support a custom `className`', () => { + // const Element = () => { + // return ( + // + // Item + // + // + // ) + // } + // const FeatureFlagElement = () => { + // return ( + // + // + // + // ) + // } + // expect(HTMLRender().container.querySelector('li[aria-hidden="true"]')).toHaveClass( + // 'test-class-name', + // ) + // expect(HTMLRender().container.querySelector('li[aria-hidden="true"]')).toHaveClass('test-class-name') + // }) }) diff --git a/packages/react/src/ActionList/Item.test.tsx b/packages/react/src/ActionList/Item.test.tsx new file mode 100644 index 00000000000..f126436b1e1 --- /dev/null +++ b/packages/react/src/ActionList/Item.test.tsx @@ -0,0 +1,350 @@ +import {render as HTMLRender, waitFor, fireEvent} from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import axe from 'axe-core' +import React from 'react' +import theme from '../theme' +import {ActionList} from '.' +import {BookIcon} from '@primer/octicons-react' +import {behavesAsComponent, checkExports} from '../utils/testing' +import {BaseStyles, ThemeProvider} from '..' +import {FeatureFlags} from '../FeatureFlags' + +function SimpleActionList(): JSX.Element { + return ( + + New file + + Copy link + Edit file + Delete file + + Link Item + + + ) +} + +const projects = [ + {name: 'Primer Backlog', scope: 'GitHub'}, + {name: 'Primer React', scope: 'github/primer'}, + {name: 'Disabled Project', scope: 'github/primer', disabled: true}, + {name: 'Inactive Project', scope: 'github/primer', inactiveText: 'Unavailable due to an outage'}, + {name: 'Loading Project', scope: 'github/primer', loading: true}, + { + name: 'Inactive and Loading Project', + scope: 'github/primer', + loading: true, + inactiveText: 'Unavailable due to an outage, but loading still passed', + }, +] + +function SingleSelectListStory(): JSX.Element { + const [selectedIndex, setSelectedIndex] = React.useState(0) + + return ( + + {projects.map((project, index) => ( + setSelectedIndex(index)} + disabled={project.disabled} + inactiveText={project.inactiveText} + loading={project.loading} + > + {project.name} + + ))} + + ) +} + +describe('ActionList.Item', () => { + it('should have aria-keyshortcuts applied to the correct element', async () => { + const {container} = HTMLRender() + const linkOptions = await waitFor(() => container.querySelectorAll('a')) + expect(linkOptions[0]).toHaveAttribute('aria-keyshortcuts', 'd') + expect(linkOptions[0].parentElement).not.toHaveAttribute('aria-keyshortcuts', 'd') + }) + // it('should fire onSelect on click and keypress', async () => { + // const component = HTMLRender() + // const options = await waitFor(() => component.getAllByRole('option')) + // expect(options[0]).toHaveAttribute('aria-selected', 'true') + // expect(options[1]).toHaveAttribute('aria-selected', 'false') + // fireEvent.click(options[1]) + // expect(options[0]).toHaveAttribute('aria-selected', 'false') + // expect(options[1]).toHaveAttribute('aria-selected', 'true') + // // We pass keycode here to navigate a implementation detail in react-testing-library + // // https://github.com/testing-library/react-testing-library/issues/269#issuecomment-455854112 + // fireEvent.keyPress(options[0], {key: 'Enter', charCode: 13}) + // expect(options[0]).toHaveAttribute('aria-selected', 'true') + // expect(options[1]).toHaveAttribute('aria-selected', 'false') + // fireEvent.keyPress(options[1], {key: ' ', charCode: 32}) + // expect(options[0]).toHaveAttribute('aria-selected', 'false') + // expect(options[1]).toHaveAttribute('aria-selected', 'true') + // }) + // it('should skip onSelect on disabled items', async () => { + // const component = HTMLRender() + // const options = await waitFor(() => component.getAllByRole('option')) + // expect(options[0]).toHaveAttribute('aria-selected', 'true') + // expect(options[2]).toHaveAttribute('aria-selected', 'false') + // fireEvent.click(options[2]) + // expect(options[0]).toHaveAttribute('aria-selected', 'true') + // expect(options[2]).toHaveAttribute('aria-selected', 'false') + // fireEvent.keyPress(options[2], {key: 'Enter', charCode: 13}) + // expect(options[0]).toHaveAttribute('aria-selected', 'true') + // expect(options[2]).toHaveAttribute('aria-selected', 'false') + // }) + // it('should skip onSelect on inactive items', async () => { + // const component = HTMLRender() + // const options = await waitFor(() => component.getAllByRole('option')) + // expect(options[0]).toHaveAttribute('aria-selected', 'true') + // expect(options[3]).toHaveAttribute('aria-selected', 'false') + // fireEvent.click(options[3]) + // expect(options[0]).toHaveAttribute('aria-selected', 'true') + // expect(options[3]).toHaveAttribute('aria-selected', 'false') + // fireEvent.keyPress(options[3], {key: 'Enter', charCode: 13}) + // expect(options[0]).toHaveAttribute('aria-selected', 'true') + // expect(options[3]).toHaveAttribute('aria-selected', 'false') + // }) + // it('should skip onSelect on loading items', async () => { + // const component = HTMLRender() + // const options = await waitFor(() => component.getAllByRole('option')) + // expect(options[0]).toHaveAttribute('aria-selected', 'true') + // expect(options[4]).toHaveAttribute('aria-selected', 'false') + // fireEvent.click(options[4]) + // expect(options[0]).toHaveAttribute('aria-selected', 'true') + // expect(options[4]).toHaveAttribute('aria-selected', 'false') + // fireEvent.keyPress(options[3], {key: 'Enter', charCode: 13}) + // expect(options[0]).toHaveAttribute('aria-selected', 'true') + // expect(options[4]).toHaveAttribute('aria-selected', 'false') + // }) + // it('should not crash when clicking an item without an onSelect', async () => { + // const component = HTMLRender( + // + // Primer React + // , + // ) + // const option = await waitFor(() => component.getByRole('option')) + // expect(option).toBeInTheDocument() + // fireEvent.click(option) + // fireEvent.keyPress(option, {key: 'Enter', charCode: 13}) + // expect(option).toBeInTheDocument() + // }) + // it('should focus the button around the leading visual when tabbing to an inactive item', async () => { + // const component = HTMLRender() + // const inactiveOptionButton = await waitFor(() => component.getByRole('button', {name: projects[3].inactiveText})) + // await userEvent.tab() // get focus on first element + // await userEvent.keyboard('{ArrowDown}') + // await userEvent.keyboard('{ArrowDown}') + // expect(inactiveOptionButton).toHaveFocus() + // }) + // it('should behave as inactive if both inactiveText and loading props are passed', async () => { + // const component = HTMLRender() + // const inactiveOptionButton = await waitFor(() => component.getByRole('button', {name: projects[5].inactiveText})) + // await userEvent.tab() // get focus on first element + // await userEvent.keyboard('{ArrowDown}') + // await userEvent.keyboard('{ArrowDown}') + // await userEvent.keyboard('{ArrowDown}') + // await userEvent.keyboard('{ArrowDown}') + // expect(inactiveOptionButton).toHaveFocus() + // }) + // it('should call onClick for a link item', async () => { + // const onClick = jest.fn() + // const component = HTMLRender( + // + // + // Primer React + // + // , + // ) + // const link = await waitFor(() => component.getByRole('link')) + // fireEvent.click(link) + // expect(onClick).toHaveBeenCalled() + // }) + // it('should render ActionList.Item as button when feature flag is enabled', async () => { + // const featureFlag = { + // primer_react_action_list_item_as_button: true, + // } + // const {container} = HTMLRender( + // + // + // Item 1 + // Item 2 + // + // , + // ) + // const button = container.querySelector('button') + // expect(button).toHaveTextContent('Item 1') + // // Ensure passed prop "disabled" is applied to the button + // expect(button).toHaveAttribute('aria-disabled', 'true') + // const listItems = container.querySelectorAll('li') + // expect(listItems.length).toBe(2) + // }) + // it('should render ActionList.Item as li when feature flag is disabled', async () => { + // const {container} = HTMLRender( + // + // + // Item 1 + // Item 2 + // + // , + // ) + // const listitem = container.querySelector('li') + // const button = container.querySelector('button') + // expect(listitem).toHaveTextContent('Item 1') + // expect(listitem).toHaveAttribute('tabindex', '0') + // expect(button).toBeNull() + // const listItems = container.querySelectorAll('li') + // expect(listItems.length).toBe(2) + // }) + // it('should apply ref to ActionList.Item when feature flag is disabled', async () => { + // const MockComponent = () => { + // const ref = React.useRef(null) + // const focusRef = () => { + // if (ref.current) ref.current.focus() + // } + // return ( + // + // + // + // Item 1 + // Item 2 + // + // + // ) + // } + // const {getByRole} = HTMLRender() + // const triggerBtn = getByRole('button', {name: 'Prompt'}) + // const focusTarget = getByRole('listitem', {name: 'Item 1'}) + // fireEvent.click(triggerBtn) + // expect(document.activeElement).toBe(focusTarget) + // }) + // it('should render ActionList.Item as li when feature flag is enabled and has proper aria role', async () => { + // const {container} = HTMLRender( + // + // + // Item 1 + // Item 2 + // + // , + // ) + // const listitem = container.querySelector('li') + // const button = container.querySelector('button') + // expect(listitem).toHaveTextContent('Item 1') + // expect(listitem).toHaveAttribute('tabindex', '0') + // expect(button).toBeNull() + // const listItems = container.querySelectorAll('li') + // expect(listItems.length).toBe(2) + // }) + // it('should render the trailing action as a button (default)', async () => { + // const {container} = HTMLRender( + // + // + // Item 1 + // + // + // , + // ) + // const action = container.querySelector('button[aria-labelledby]') + // expect(action).toHaveAccessibleName('Action') + // }) + // it('should render the trailing action as a link', async () => { + // const {container} = HTMLRender( + // + // + // Item 1 + // + // + // , + // ) + // const action = container.querySelector('a[href="#"][aria-labelledby]') + // expect(action).toHaveAccessibleName('Action') + // }) + // it('should do action when trailing action is clicked', async () => { + // const onClick = jest.fn() + // const component = HTMLRender( + // + // + // Item 1 + // + // + // , + // ) + // const trailingAction = await waitFor(() => component.getByRole('button', {name: 'Action'})) + // fireEvent.click(trailingAction) + // expect(onClick).toHaveBeenCalled() + // }) + // it('should focus the trailing action', async () => { + // HTMLRender( + // + // + // Item 1 + // + // + // , + // ) + // await userEvent.tab() + // expect(document.activeElement).toHaveTextContent('Item 1') + // await userEvent.tab() + // expect(document.activeElement).toHaveAccessibleName('Action') + // }) + // it('should only trigger a key event once when feature flag is enabled', async () => { + // const mockOnSelect = jest.fn() + // const user = userEvent.setup() + // const {getByRole} = HTMLRender( + // + // + // Item 1 + // + // , + // ) + // const item = getByRole('button') + // item.focus() + // expect(document.activeElement).toBe(item) + // await user.keyboard('{Enter}') + // expect(mockOnSelect).toHaveBeenCalledTimes(1) + // }) + // it('should not render buttons when feature flag is enabled and is specified role', async () => { + // const {getByRole} = HTMLRender( + // + // + // Item 1 + // Item 2 + // Item 3 + // Item 4 + // Item 5 + // + // , + // ) + // const option = getByRole('option') + // expect(option.tagName).toBe('LI') + // expect(option.textContent).toBe('Item 1') + // const menuItem = getByRole('menuitem') + // expect(menuItem.tagName).toBe('LI') + // const menuItemCheckbox = getByRole('menuitemcheckbox') + // expect(menuItemCheckbox.tagName).toBe('LI') + // const menuItemRadio = getByRole('menuitemradio') + // expect(menuItemRadio.tagName).toBe('LI') + // const button = getByRole('button') + // expect(button.parentElement?.tagName).toBe('LI') + // expect(button.textContent).toBe('Item 5') + // }) +}) diff --git a/packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap b/packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap index 0310e28f437..060000313b7 100644 --- a/packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap +++ b/packages/react/src/NavList/__snapshots__/NavList.test.tsx.snap @@ -62,8 +62,7 @@ exports[`NavList renders a simple list 1`] = ` .c0 { margin: 0; padding-inline-start: 0; - padding-top: 8px; - padding-bottom: 8px; + padding: 8px; } .c2 { @@ -77,10 +76,8 @@ exports[`NavList renders a simple list 1`] = ` font-size: 14px; padding-top: 0; padding-bottom: 0; - line-height: 20px; + line-height: 16px; min-height: 5px; - margin-left: 8px; - margin-right: 8px; border-radius: 6px; -webkit-transition: background 33.333ms linear; transition: background 33.333ms linear; @@ -91,7 +88,7 @@ exports[`NavList renders a simple list 1`] = ` appearance: none; background: unset; border: unset; - width: calc(100% - 16px); + width: 100%; font-family: unset; text-align: unset; margin-top: unset; @@ -192,10 +189,8 @@ exports[`NavList renders a simple list 1`] = ` font-size: 14px; padding-top: 0; padding-bottom: 0; - line-height: 20px; + line-height: 16px; min-height: 5px; - margin-left: 8px; - margin-right: 8px; border-radius: 6px; -webkit-transition: background 33.333ms linear; transition: background 33.333ms linear; @@ -206,7 +201,7 @@ exports[`NavList renders a simple list 1`] = ` appearance: none; background: unset; border: unset; - width: calc(100% - 16px); + width: 100%; font-family: unset; text-align: unset; margin-top: unset; @@ -304,7 +299,7 @@ exports[`NavList renders a simple list 1`] = ` .c2:focus.focus-visible { outline: none; border: 2 solid; - box-shadow: 0 0 0 2px var(--bgColor-accent-emphasis,var(--color-accent-emphasis,#0969da)); + box-shadow: 0 0 0 2px var(--focus-outlineColor); } .c2:active:not([aria-disabled]):not([data-inactive]) { @@ -334,7 +329,7 @@ exports[`NavList renders a simple list 1`] = ` .c6:focus.focus-visible { outline: none; border: 2 solid; - box-shadow: 0 0 0 2px var(--bgColor-accent-emphasis,var(--color-accent-emphasis,#0969da)); + box-shadow: 0 0 0 2px var(--focus-outlineColor); } .c6:active:not([aria-disabled]):not([data-inactive]) { @@ -453,34 +448,10 @@ exports[`NavList renders with groups 1`] = ` } .c3 { - padding-top: 6px; - padding-bottom: 6px; - padding-left: 16px; - padding-right: 16px; - font-size: 12px; - font-weight: 600; - color: var(--fgColor-muted,var(--color-fg-muted,#656d76)); -} - -.c3 .ActionListGroupHeading { - font-size: var(--text-body-size-small),12px; - font-weight: var(--base-text-weight-semibold,600); - line-height: var(--text-body-lineHeight-small,1.6666); - color: var(--fgColor-muted); -} - -.c3 .ActionListGroupHeadingDescription { - font-size: var(--text-body-size-small,12px); - font-weight: var(--base-text-weight-normal,400); - line-height: var(--text-body-lineHeight-small,1.6666); - color: var(--fgColor-muted); -} - -.c4 { padding-inline-start: 0; } -.c7 { +.c6 { padding-left: 8px; padding-right: 8px; padding-top: 6px; @@ -497,13 +468,13 @@ exports[`NavList renders with groups 1`] = ` color: inherit; } -.c7:hover { +.c6:hover { color: inherit; -webkit-text-decoration: none; text-decoration: none; } -.c8 { +.c7 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -518,7 +489,7 @@ exports[`NavList renders with groups 1`] = ` min-width: 0; } -.c9 { +.c8 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; @@ -528,7 +499,7 @@ exports[`NavList renders with groups 1`] = ` line-height: 20px; } -.c11 { +.c10 { -webkit-box-flex: 1; -webkit-flex-grow: 1; -ms-flex-positive: 1; @@ -541,11 +512,10 @@ exports[`NavList renders with groups 1`] = ` .c0 { margin: 0; padding-inline-start: 0; - padding-top: 8px; - padding-bottom: 8px; + padding: 8px; } -.c6 { +.c5 { position: relative; display: -webkit-box; display: -webkit-flex; @@ -556,10 +526,8 @@ exports[`NavList renders with groups 1`] = ` font-size: 14px; padding-top: 0; padding-bottom: 0; - line-height: 20px; + line-height: 16px; min-height: 5px; - margin-left: 8px; - margin-right: 8px; border-radius: 6px; -webkit-transition: background 33.333ms linear; transition: background 33.333ms linear; @@ -570,7 +538,7 @@ exports[`NavList renders with groups 1`] = ` appearance: none; background: unset; border: unset; - width: calc(100% - 16px); + width: 100%; font-family: unset; text-align: unset; margin-top: unset; @@ -579,32 +547,32 @@ exports[`NavList renders with groups 1`] = ` background-color: var(--control-transparent-bgColor-selected,var(--color-action-list-item-default-selected-bg,rgba(208,215,222,0.24))); } -.c6[data-loading] { +.c5[data-loading] { cursor: default; } -.c6[aria-disabled], -.c6[data-inactive] { +.c5[aria-disabled], +.c5[data-inactive] { cursor: not-allowed; } -.c6[aria-disabled] [data-component="ActionList.Checkbox"], -.c6[data-inactive] [data-component="ActionList.Checkbox"] { +.c5[aria-disabled] [data-component="ActionList.Checkbox"], +.c5[data-inactive] [data-component="ActionList.Checkbox"] { cursor: not-allowed; background-color: var(--control-bgColor-disabled,rgba(175,184,193,0.2)); border-color: var(--color-input-disabled-bg,rgba(175,184,193,0.2)); } -.c6[aria-disabled] [data-component="ActionList.Selection"], -.c6[data-inactive] [data-component="ActionList.Selection"] { +.c5[aria-disabled] [data-component="ActionList.Selection"], +.c5[data-inactive] [data-component="ActionList.Selection"] { color: var(--fgColor-disabled,var(--color-primer-fg-disabled,#8c959f)); } -.c6 [data-component="ActionList.Item--DividerContainer"] { +.c5 [data-component="ActionList.Item--DividerContainer"] { position: relative; } -.c6 [data-component="ActionList.Item--DividerContainer"]::before { +.c5 [data-component="ActionList.Item--DividerContainer"]::before { content: " "; display: block; position: absolute; @@ -615,25 +583,25 @@ exports[`NavList renders with groups 1`] = ` border-color: var(--divider-color,transparent); } -.c6:not(:first-of-type) { +.c5:not(:first-of-type) { --divider-color: var(--borderColor-muted,var(--color-action-list-item-inline-divider,rgba(208,215,222,0.48))); } -[data-component="ActionList.Divider"] + .c5 { +[data-component="ActionList.Divider"] + .c4 { --divider-color: transparent !important; } -.c6:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]), -.c6[data-focus-visible-added]:not([aria-disabled]):not([data-inactive]) { +.c5:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]), +.c5[data-focus-visible-added]:not([aria-disabled]):not([data-inactive]) { --divider-color: transparent; } -.c6:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c5, -.c6[data-focus-visible-added] + li { +.c5:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c4, +.c5[data-focus-visible-added] + li { --divider-color: transparent; } -.c6::after { +.c5::after { position: absolute; top: calc(50% - 12px); left: -8px; @@ -644,12 +612,12 @@ exports[`NavList renders with groups 1`] = ` border-radius: 6px; } -.c6[data-is-active-descendant] { +.c5[data-is-active-descendant] { font-weight: 400; background-color: var(--control-transparent-bgColor-selected,var(--color-action-list-item-default-selected-bg,rgba(208,215,222,0.24))); } -.c6[data-is-active-descendant]::after { +.c5[data-is-active-descendant]::after { position: absolute; top: calc(50% - 12px); left: -8px; @@ -660,7 +628,7 @@ exports[`NavList renders with groups 1`] = ` border-radius: 6px; } -.c10 { +.c9 { position: relative; display: -webkit-box; display: -webkit-flex; @@ -671,10 +639,8 @@ exports[`NavList renders with groups 1`] = ` font-size: 14px; padding-top: 0; padding-bottom: 0; - line-height: 20px; + line-height: 16px; min-height: 5px; - margin-left: 8px; - margin-right: 8px; border-radius: 6px; -webkit-transition: background 33.333ms linear; transition: background 33.333ms linear; @@ -685,39 +651,39 @@ exports[`NavList renders with groups 1`] = ` appearance: none; background: unset; border: unset; - width: calc(100% - 16px); + width: 100%; font-family: unset; text-align: unset; margin-top: unset; margin-bottom: unset; } -.c10[data-loading] { +.c9[data-loading] { cursor: default; } -.c10[aria-disabled], -.c10[data-inactive] { +.c9[aria-disabled], +.c9[data-inactive] { cursor: not-allowed; } -.c10[aria-disabled] [data-component="ActionList.Checkbox"], -.c10[data-inactive] [data-component="ActionList.Checkbox"] { +.c9[aria-disabled] [data-component="ActionList.Checkbox"], +.c9[data-inactive] [data-component="ActionList.Checkbox"] { cursor: not-allowed; background-color: var(--control-bgColor-disabled,rgba(175,184,193,0.2)); border-color: var(--color-input-disabled-bg,rgba(175,184,193,0.2)); } -.c10[aria-disabled] [data-component="ActionList.Selection"], -.c10[data-inactive] [data-component="ActionList.Selection"] { +.c9[aria-disabled] [data-component="ActionList.Selection"], +.c9[data-inactive] [data-component="ActionList.Selection"] { color: var(--fgColor-disabled,var(--color-primer-fg-disabled,#8c959f)); } -.c10 [data-component="ActionList.Item--DividerContainer"] { +.c9 [data-component="ActionList.Item--DividerContainer"] { position: relative; } -.c10 [data-component="ActionList.Item--DividerContainer"]::before { +.c9 [data-component="ActionList.Item--DividerContainer"]::before { content: " "; display: block; position: absolute; @@ -728,30 +694,30 @@ exports[`NavList renders with groups 1`] = ` border-color: var(--divider-color,transparent); } -.c10:not(:first-of-type) { +.c9:not(:first-of-type) { --divider-color: var(--borderColor-muted,var(--color-action-list-item-inline-divider,rgba(208,215,222,0.48))); } -[data-component="ActionList.Divider"] + .c5 { +[data-component="ActionList.Divider"] + .c4 { --divider-color: transparent !important; } -.c10:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]), -.c10[data-focus-visible-added]:not([aria-disabled]):not([data-inactive]) { +.c9:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]), +.c9[data-focus-visible-added]:not([aria-disabled]):not([data-inactive]) { --divider-color: transparent; } -.c10:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c5, -.c10[data-focus-visible-added] + li { +.c9:hover:not([aria-disabled]):not([data-inactive]):not([data-loading]) + .c4, +.c9[data-focus-visible-added] + li { --divider-color: transparent; } -.c10[data-is-active-descendant] { +.c9[data-is-active-descendant] { font-weight: 400; background-color: var(--control-transparent-bgColor-selected,var(--color-action-list-item-default-selected-bg,rgba(208,215,222,0.24))); } -.c10[data-is-active-descendant]::after { +.c9[data-is-active-descendant]::after { position: absolute; top: calc(50% - 12px); left: -8px; @@ -763,60 +729,60 @@ exports[`NavList renders with groups 1`] = ` } @media (forced-colors:active) { - .c6:focus, - .c6:focus-visible, - .c6 > a.focus-visible, - .c6[data-is-active-descendant] { + .c5:focus, + .c5:focus-visible, + .c5 > a.focus-visible, + .c5[data-is-active-descendant] { outline: solid 1px transparent !important; } } @media (hover:hover) and (pointer:fine) { - .c6:hover:not([aria-disabled]):not([data-inactive]) { + .c5:hover:not([aria-disabled]):not([data-inactive]) { background-color: var(--control-transparent-bgColor-hover,var(--color-action-list-item-default-hover-bg,rgba(208,215,222,0.32))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); box-shadow: inset 0 0 0 max(1px,0.0625rem) var(--control-transparent-borderColor-active,var(--color-action-list-item-default-active-border,transparent)); } - .c6:focus-visible, - .c6 > a.focus-visible, - .c6:focus.focus-visible { + .c5:focus-visible, + .c5 > a.focus-visible, + .c5:focus.focus-visible { outline: none; border: 2 solid; - box-shadow: 0 0 0 2px var(--bgColor-accent-emphasis,var(--color-accent-emphasis,#0969da)); + box-shadow: 0 0 0 2px var(--focus-outlineColor); } - .c6:active:not([aria-disabled]):not([data-inactive]) { + .c5:active:not([aria-disabled]):not([data-inactive]) { background-color: var(--control-transparent-bgColor-active,var(--color-action-list-item-default-active-bg,rgba(208,215,222,0.48))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); } } @media (forced-colors:active) { - .c10:focus, - .c10:focus-visible, - .c10 > a.focus-visible, - .c10[data-is-active-descendant] { + .c9:focus, + .c9:focus-visible, + .c9 > a.focus-visible, + .c9[data-is-active-descendant] { outline: solid 1px transparent !important; } } @media (hover:hover) and (pointer:fine) { - .c10:hover:not([aria-disabled]):not([data-inactive]) { + .c9:hover:not([aria-disabled]):not([data-inactive]) { background-color: var(--control-transparent-bgColor-hover,var(--color-action-list-item-default-hover-bg,rgba(208,215,222,0.32))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); box-shadow: inset 0 0 0 max(1px,0.0625rem) var(--control-transparent-borderColor-active,var(--color-action-list-item-default-active-border,transparent)); } - .c10:focus-visible, - .c10 > a.focus-visible, - .c10:focus.focus-visible { + .c9:focus-visible, + .c9 > a.focus-visible, + .c9:focus.focus-visible { outline: none; border: 2 solid; - box-shadow: 0 0 0 2px var(--bgColor-accent-emphasis,var(--color-accent-emphasis,#0969da)); + box-shadow: 0 0 0 2px var(--focus-outlineColor); } - .c10:active:not([aria-disabled]):not([data-inactive]) { + .c9:active:not([aria-disabled]):not([data-inactive]) { background-color: var(--control-transparent-bgColor-active,var(--color-action-list-item-default-active-bg,rgba(208,215,222,0.48))); color: var(--fgColor-default,var(--color-fg-default,#1F2328)); } @@ -838,37 +804,36 @@ exports[`NavList renders with groups 1`] = ` class="c2" >

    Overview