diff --git a/.changeset/purple-crabs-matter.md b/.changeset/purple-crabs-matter.md new file mode 100644 index 00000000000..261665de8d8 --- /dev/null +++ b/.changeset/purple-crabs-matter.md @@ -0,0 +1,5 @@ +--- +"@primer/react": patch +--- + +Refactor(ActionList): ActionList.Item should render content as a button if parent is not interactive. diff --git a/src/ActionList/Description.tsx b/src/ActionList/Description.tsx index 4027ec22f58..011c1a800b8 100644 --- a/src/ActionList/Description.tsx +++ b/src/ActionList/Description.tsx @@ -44,7 +44,6 @@ export const Description: React.FC {props.children} diff --git a/src/ActionList/Item.tsx b/src/ActionList/Item.tsx index 56e91d0070c..c0b3eae74b8 100644 --- a/src/ActionList/Item.tsx +++ b/src/ActionList/Item.tsx @@ -9,10 +9,11 @@ import {defaultSxProp} from '../utils/defaultSxProp' import {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' import {ActionListContainerContext} from './ActionListContainerContext' import {Description} from './Description' -import {ActionListProps, ListContext} from './List' +import {ListContext} from './List' import {Selection} from './Selection' import {ActionListItemProps, getVariantStyles, ItemContext, TEXT_ROW_HEIGHT} from './shared' import {LeadingVisual, TrailingVisual} from './Visuals' +import {MenuContext} from '../ActionMenu/ActionMenu' import {GroupContext} from './Group' const LiBox = styled.li(sx) @@ -29,6 +30,8 @@ export const Item = React.forwardRef( id, role, _PrivateItemWrapper, + // @ts-ignore tabIndex is sometimes passed as a prop in dotcom. + tabIndex, ...props }, forwardedRef, @@ -38,10 +41,13 @@ export const Item = React.forwardRef( trailingVisual: TrailingVisual, description: Description, }) + const {variant: listVariant, showDividers, selectionVariant: listSelectionVariant} = React.useContext(ListContext) const {container, afterSelect, selectionAttribute} = React.useContext(ActionListContainerContext) + const menuContext = React.useContext(MenuContext) const {selectionVariant: groupSelectionVariant} = React.useContext(GroupContext) + const selectionVariant = groupSelectionVariant ?? listSelectionVariant const onSelect = React.useCallback( ( event: React.MouseEvent | React.KeyboardEvent, @@ -55,10 +61,6 @@ export const Item = React.forwardRef( [onSelectUser], ) - const selectionVariant: ActionListProps['selectionVariant'] = groupSelectionVariant - ? groupSelectionVariant - : listSelectionVariant - /** Infer item role based on the container */ let itemRole: ActionListItemProps['role'] if (container === 'ActionMenu' || container === 'DropdownMenu') { @@ -84,12 +86,22 @@ export const Item = React.forwardRef( }, } + const isTopLevelInteractive = () => + _PrivateItemWrapper !== undefined || + // @ts-ignore props.as may be defined, may not. + props.as === 'button' || + // @ts-ignore props.as may be defined, may not. + props.as === 'a' || + menuContext.anchorId !== undefined || + role?.match(/menuitem/) || + tabIndex !== undefined + const styles = { position: 'relative', display: 'flex', - paddingX: 2, + paddingX: isTopLevelInteractive() ? 2 : 0, fontSize: 1, - paddingY: '6px', // custom value off the scale + paddingY: isTopLevelInteractive() ? '6px' : 0, // custom value off the scale lineHeight: TEXT_ROW_HEIGHT, minHeight: 5, marginX: listVariant === 'inset' ? 2 : 0, @@ -145,6 +157,10 @@ export const Item = React.forwardRef( borderTopWidth: showDividers ? `1px` : '0', borderColor: 'var(--divider-color, transparent)', }, + 'button[data-component="ActionList.Item--DividerContainer"]': { + textAlign: 'left', + padding: 0, + }, // show between 2 items ':not(:first-of-type)': {'--divider-color': theme?.colors.actionListItem.inlineDivider}, // hide divider after dividers & group header, with higher importance! @@ -182,13 +198,13 @@ export const Item = React.forwardRef( const inlineDescriptionId = useId(id && `${id}--inline-description`) const blockDescriptionId = useId(id && `${id}--block-description`) - const ItemWrapper = _PrivateItemWrapper || React.Fragment + const ItemWrapper = _PrivateItemWrapper || Box const menuItemProps = { onClick: clickHandler, onKeyPress: keyPressHandler, 'aria-disabled': disabled ? true : undefined, - tabIndex: disabled ? undefined : 0, + tabIndex: disabled || !isTopLevelInteractive() ? undefined : 0, 'aria-labelledby': `${labelId} ${ slots.description && slots.description.props.variant !== 'block' ? inlineDescriptionId : '' }`, @@ -199,17 +215,41 @@ export const Item = React.forwardRef( const containerProps = _PrivateItemWrapper ? {role: role || itemRole ? 'none' : undefined} : menuItemProps - const wrapperProps = _PrivateItemWrapper ? menuItemProps : {} + const wrapperProps = _PrivateItemWrapper + ? menuItemProps + : { + sx: { + display: 'flex', + paddingX: isTopLevelInteractive() ? 0 : 2, + paddingY: isTopLevelInteractive() ? 0 : '6px', // custom value off the scale + flexGrow: 1, + }, + } return ( (styles, sxProp)} {...containerProps} {...props}> + {/* @ts-ignore onClick prop is only passed when _PrivateItemWrapper is set by ActionList.LinkItem. */} {slots.leadingVisual} & { - onClose?: (gesture: 'anchor-click' | 'click-outside' | 'escape' | 'tab') => void -} -const MenuContext = React.createContext({renderAnchor: null, open: false}) - -export type ActionMenuProps = { - /** - * Recommended: `ActionMenu.Button` or `ActionMenu.Anchor` with `ActionMenu.Overlay` - */ - children: React.ReactElement[] | React.ReactElement - - /** - * If defined, will control the open/closed state of the overlay. Must be used in conjunction with `onOpenChange`. - */ - open?: boolean - - /** - * If defined, will control the open/closed state of the overlay. Must be used in conjunction with `open`. - */ - onOpenChange?: (s: boolean) => void -} & Pick - -const Menu: React.FC> = ({ - anchorRef: externalAnchorRef, - open, - onOpenChange, - children, -}: ActionMenuProps) => { - const [combinedOpenState, setCombinedOpenState] = useProvidedStateOrCreate(open, onOpenChange, false) - const onOpen = React.useCallback(() => setCombinedOpenState(true), [setCombinedOpenState]) - const onClose = React.useCallback(() => setCombinedOpenState(false), [setCombinedOpenState]) - - const anchorRef = useProvidedRefOrCreate(externalAnchorRef) - const anchorId = useId() - let renderAnchor: AnchoredOverlayProps['renderAnchor'] = null - - // 🚨 Hack for good API! - // we strip out Anchor from children and pass it to AnchoredOverlay to render - // with additional props for accessibility - const contents = React.Children.map(children, child => { - if (child.type === MenuButton || child.type === Anchor) { - renderAnchor = anchorProps => React.cloneElement(child, anchorProps) - return null - } - return child - }) - - return ( - - {contents} - - ) -} - -export type ActionMenuAnchorProps = {children: React.ReactElement} -const Anchor = React.forwardRef(({children, ...anchorProps}, anchorRef) => { - return React.cloneElement(children, {...anchorProps, ref: anchorRef}) -}) - -/** this component is syntactical sugar 🍭 */ -export type ActionMenuButtonProps = ButtonProps -const MenuButton = React.forwardRef(({...props}, anchorRef) => { - return ( - -