Skip to content

Commit

Permalink
Refactor(ActionList): ActionList.Item should render content as a butt…
Browse files Browse the repository at this point in the history
…on if parent is not interactive. (#3284)

* Using button as the underlying tag for ActionList.Item content.

* Fix subnav layout.

* Changing ActionList content to button if the top-level is not a button. ActionMenu is busted.

* Fix issue where ActionList.Item content would render as a button inside ActionMenu, breaking keyboard navigation.

* Create purple-crabs-matter.md

* test(vrt): update snapshots

* Fix bug with MenuContext not being exported properly.

* Fix color violation in ActionList.Item.

* Formatting, update snapshots.

* test(vrt): update snapshots

* Fix a11y violation in ActionList story.

* Formatting.

* Fix ts-ignore comment.

* Adjust line-height when button is rendered.

* Revert "test(vrt): update snapshots"

This reverts commit ba6d863.

* Revert "test(vrt): update snapshots"

This reverts commit 31d5358.

* Update snapshots.

* Remove flexGrow from ActionList.Item to remove extra 1px vertical height.

* Set padding to 0 and put flexGrow back to fix padding issue for ActionList buttons.

* Fix ActionList text wrap story.

* Fix underlinenav story by checking ActionList.Item if as prop is a.

* Update snapshots and formatting.

* Update snapshots.

* If ActionList.Item content is rendered as a button, remove tabIndex from li and fix padding.

* Fix various layout edge cases.

* ts-ignore event handlers on ItemWrapper.

* Update snapshots.

* Revert fontWeight config that differed from production docs.

* Pass selectionVariant prop illegally in UnderlineNav story to fix issue with Selection spans showing up.

* fix: prevent closing menu when `event.preventDefault()` is called on `ActionList.Item`'s `onSelect` handler by @gr2m (#3346)

* fix: prevent closing menu when `event.preventDefault()` is called on `ActionList.Item`'s `onSelect` handler (#3163)

* test: prevent closing menu when `event.preventDefault()` is called on `ActionList.Item`'s `onSelect` handler

Failing test for primer/react#3162

* fix: prevent closing menu when `event.preventDefault()` is called on `ActionList.Item`ߴs `onSelect` handler

* add storybook example: Delayed Menu Close

* update docs

* docs: changeset

* Update changelog

---------

Co-authored-by: Siddharth Kshetrapal <siddharthkp@github.com>

* Update generated/components.json

---------

Co-authored-by: Gregor Martynus <39992+gr2m@users.noreply.github.com>
Co-authored-by: siddharthkp <siddharthkp@users.noreply.github.com>

* Check for tabIndex value in isTopLevelInteractive.

* Reference styles in updated menuProps.

* Updated snapshots.

* Fix padding setting instead of using values from styles, which are inverted.

* Update snapshots.

---------

Co-authored-by: radglob <radglob@users.noreply.github.com>
Co-authored-by: Gregor Martynus <39992+gr2m@users.noreply.github.com>
Co-authored-by: siddharthkp <siddharthkp@users.noreply.github.com>
  • Loading branch information
4 people authored Jun 6, 2023
1 parent 786013e commit 08d7d5d
Show file tree
Hide file tree
Showing 14 changed files with 297 additions and 280 deletions.
5 changes: 5 additions & 0 deletions .changeset/purple-crabs-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": patch
---

Refactor(ActionList): ActionList.Item should render content as a button if parent is not interactive.
1 change: 0 additions & 1 deletion src/ActionList/Description.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export const Description: React.FC<React.PropsWithChildren<ActionListDescription
sx={merge({...styles, color: disabled ? 'fg.disabled' : 'fg.muted'}, sx as SxProp)}
title={props.children as string}
inline={true}
maxWidth="100%"
>
{props.children}
</Truncate>
Expand Down
62 changes: 51 additions & 11 deletions src/ActionList/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<SxProp>(sx)
Expand All @@ -29,6 +30,8 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(
id,
role,
_PrivateItemWrapper,
// @ts-ignore tabIndex is sometimes passed as a prop in dotcom.
tabIndex,
...props
},
forwardedRef,
Expand All @@ -38,10 +41,13 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(
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<HTMLLIElement> | React.KeyboardEvent<HTMLLIElement>,
Expand All @@ -55,10 +61,6 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(
[onSelectUser],
)

const selectionVariant: ActionListProps['selectionVariant'] = groupSelectionVariant
? groupSelectionVariant
: listSelectionVariant

/** Infer item role based on the container */
let itemRole: ActionListItemProps['role']
if (container === 'ActionMenu' || container === 'DropdownMenu') {
Expand All @@ -84,12 +86,22 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(
},
}

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,
Expand Down Expand Up @@ -145,6 +157,10 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(
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!
Expand Down Expand Up @@ -182,13 +198,13 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(
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 : ''
}`,
Expand All @@ -199,17 +215,41 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(

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 (
<ItemContext.Provider value={{variant, disabled, inlineDescriptionId, blockDescriptionId}}>
<LiBox ref={forwardedRef} sx={merge<BetterSystemStyleObject>(styles, sxProp)} {...containerProps} {...props}>
{/* @ts-ignore onClick prop is only passed when _PrivateItemWrapper is set by ActionList.LinkItem. */}
<ItemWrapper {...wrapperProps}>
<Selection selected={selected} />
{slots.leadingVisual}
<Box
data-component="ActionList.Item--DividerContainer"
sx={{display: 'flex', flexDirection: 'column', flexGrow: 1, minWidth: 0}}
sx={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
minWidth: 0,
borderStyle: 'none',
backgroundColor: 'transparent',
cursor: 'inherit',
fontSize: 'inherit',
color: getVariantStyles(variant, disabled).color,
lineHeight: '20px',
}}
// @ts-ignore `as` prop may be passed to ActionList.Item, even if it isn't defined in ActionListItemProps.
// If this item is inside an ActionMenu, don't render an interactive button.
as={isTopLevelInteractive() ? 'div' : 'button'}
>
<ConditionalBox if={Boolean(slots.trailingVisual)} sx={{display: 'flex', flexGrow: 1}}>
<ConditionalBox
Expand Down
138 changes: 0 additions & 138 deletions src/ActionMenu.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'
import {axe, toHaveNoViolations} from 'jest-axe'
import React from 'react'
import theme from '../theme'
import {ActionMenu, ActionList, BaseStyles, ThemeProvider, SSRProvider} from '..'
import {ActionMenu, MenuContext, ActionList, BaseStyles, ThemeProvider, SSRProvider} from '..'
import {behavesAsComponent, checkExports} from '../utils/testing'
import {SingleSelect} from '../ActionMenu/ActionMenu.features.stories'
import {MixedSelection} from '../ActionMenu/ActionMenu.examples.stories'
Expand Down Expand Up @@ -47,6 +47,7 @@ describe('ActionMenu', () => {
checkExports('ActionMenu', {
default: undefined,
ActionMenu,
MenuContext,
})

it('should open Menu on MenuButton click', async () => {
Expand Down
3 changes: 2 additions & 1 deletion src/ActionMenu/ActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export type MenuContextProps = Pick<
> & {
onClose?: (gesture: 'anchor-click' | 'click-outside' | 'escape' | 'tab') => void
}
const MenuContext = React.createContext<MenuContextProps>({renderAnchor: null, open: false})

export const MenuContext = React.createContext<MenuContextProps>({renderAnchor: null, open: false})

export type ActionMenuProps = {
/**
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/NavList/NavList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ describe('NavList.Item with NavList.SubNav', () => {
</ThemeProvider>,
)

const button = getByRole('button')
const button = getByRole('button', {name: 'Item'})

// Starts open
expect(queryByRole('list', {name: 'Item'})).toBeVisible()
Expand Down
Loading

0 comments on commit 08d7d5d

Please sign in to comment.