Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "Add support for nested submenus to ActionMenu" #4472

Merged
merged 1 commit into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .changeset/wild-students-bow.md

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ type ContextProps = {
// eslint-disable-next-line @typescript-eslint/ban-types
afterSelect?: Function
enableFocusZone?: boolean
defaultTrailingVisual?: React.ReactElement
}

export const ActionListContainerContext = React.createContext<ContextProps>({})
14 changes: 3 additions & 11 deletions packages/react/src/ActionList/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,22 +74,14 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(
inlineDescription: [Description, props => props.variant !== 'block'],
})

const {container, afterSelect, selectionAttribute, defaultTrailingVisual} =
React.useContext(ActionListContainerContext)

// Be sure to avoid rendering the container unless there is a default
const wrappedDefaultTrailingVisual = defaultTrailingVisual ? (
<TrailingVisual>{defaultTrailingVisual}</TrailingVisual>
) : null
const trailingVisual = slots.trailingVisual ?? wrappedDefaultTrailingVisual

const {
variant: listVariant,
role: listRole,
showDividers,
selectionVariant: listSelectionVariant,
} = React.useContext(ListContext)
const {selectionVariant: groupSelectionVariant} = React.useContext(GroupContext)
const {container, afterSelect, selectionAttribute} = React.useContext(ActionListContainerContext)
const inactive = Boolean(inactiveText)
const showInactiveIndicator = inactive && container === undefined

Expand Down Expand Up @@ -316,7 +308,7 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(
sx={{display: 'flex', flexDirection: 'column', flexGrow: 1, minWidth: 0}}
>
<ConditionalWrapper
if={Boolean(trailingVisual) || (showInactiveIndicator && !slots.leadingVisual)}
if={Boolean(slots.trailingVisual) || (showInactiveIndicator && !slots.leadingVisual)}
sx={{display: 'flex', flexGrow: 1}}
>
<ConditionalWrapper
Expand Down Expand Up @@ -346,7 +338,7 @@ export const Item = React.forwardRef<HTMLLIElement, ActionListItemProps>(
) : (
// If it's not inactive, or it has a leading visual that can be replaced,
// just render the trailing visual slot.
trailingVisual
slots.trailingVisual
)
}
</ConditionalWrapper>
Expand Down
53 changes: 1 addition & 52 deletions packages/react/src/ActionMenu/ActionMenu.features.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import React from 'react'
import {ActionMenu, ActionList, Box} from '../'
import {
WorkflowIcon,
ArchiveIcon,
GearIcon,
CopyIcon,
RocketIcon,
CommentIcon,
BookIcon,
SparkleFillIcon,
} from '@primer/octicons-react'
import {WorkflowIcon, ArchiveIcon, GearIcon, CopyIcon, RocketIcon, CommentIcon, BookIcon} from '@primer/octicons-react'

export default {
title: 'Components/ActionMenu/Features',
Expand Down Expand Up @@ -190,45 +181,3 @@ export const InactiveItems = () => (
</ActionMenu.Overlay>
</ActionMenu>
)

export const Submenus = () => (
<ActionMenu>
<ActionMenu.Button>Edit</ActionMenu.Button>
<ActionMenu.Overlay>
<ActionList>
<ActionList.Item>Cut</ActionList.Item>
<ActionList.Item>Copy</ActionList.Item>
<ActionList.Item>Paste</ActionList.Item>
<ActionMenu>
<ActionMenu.Anchor>
<ActionList.Item>
<ActionList.LeadingVisual>
<SparkleFillIcon />
</ActionList.LeadingVisual>
Paste special
</ActionList.Item>
</ActionMenu.Anchor>
<ActionMenu.Overlay>
<ActionList>
<ActionList.Item>Paste plain text</ActionList.Item>
<ActionList.Item>Paste formulas</ActionList.Item>
<ActionList.Item>Paste with formatting</ActionList.Item>
<ActionMenu>
<ActionMenu.Anchor>
<ActionList.Item>Paste from</ActionList.Item>
</ActionMenu.Anchor>
<ActionMenu.Overlay>
<ActionList>
<ActionList.Item>Current clipboard</ActionList.Item>
<ActionList.Item>History</ActionList.Item>
<ActionList.Item>Another device</ActionList.Item>
</ActionList>
</ActionMenu.Overlay>
</ActionMenu>
</ActionList>
</ActionMenu.Overlay>
</ActionMenu>
</ActionList>
</ActionMenu.Overlay>
</ActionMenu>
)
96 changes: 14 additions & 82 deletions packages/react/src/ActionMenu/ActionMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {useCallback, useContext, useMemo} from 'react'
import {TriangleDownIcon, ChevronRightIcon} from '@primer/octicons-react'
import React from 'react'
import {TriangleDownIcon} from '@primer/octicons-react'
import type {AnchoredOverlayProps} from '../AnchoredOverlay'
import {AnchoredOverlay} from '../AnchoredOverlay'
import type {OverlayProps} from '../Overlay'
Expand All @@ -13,16 +13,11 @@ import type {MandateProps} from '../utils/types'
import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
import {Tooltip} from '../TooltipV2/Tooltip'

export type MenuCloseHandler = (
gesture: 'anchor-click' | 'click-outside' | 'escape' | 'tab' | 'item-select' | 'arrow-left',
) => void

export type MenuContextProps = Pick<
AnchoredOverlayProps,
'anchorRef' | 'renderAnchor' | 'open' | 'onOpen' | 'anchorId'
> & {
onClose?: MenuCloseHandler
isSubmenu?: boolean
onClose?: (gesture: 'anchor-click' | 'click-outside' | 'escape' | 'tab') => void
}
const MenuContext = React.createContext<MenuContextProps>({renderAnchor: null, open: false})

Expand All @@ -49,23 +44,9 @@ const Menu: React.FC<React.PropsWithChildren<ActionMenuProps>> = ({
onOpenChange,
children,
}: ActionMenuProps) => {
const parentMenuContext = useContext(MenuContext)

const [combinedOpenState, setCombinedOpenState] = useProvidedStateOrCreate(open, onOpenChange, false)
const onOpen = React.useCallback(() => setCombinedOpenState(true), [setCombinedOpenState])
const onClose: MenuCloseHandler = React.useCallback(
gesture => {
setCombinedOpenState(false)

// Close the parent stack when an item is selected or the user tabs out of the menu entirely
switch (gesture) {
case 'tab':
case 'item-select':
parentMenuContext.onClose?.(gesture)
}
},
[setCombinedOpenState, parentMenuContext],
)
const onClose = React.useCallback(() => setCombinedOpenState(false), [setCombinedOpenState])

const menuButtonChild = React.Children.toArray(children).find(
child => React.isValidElement<ActionMenuButtonProps>(child) && (child.type === MenuButton || child.type === Anchor),
Expand Down Expand Up @@ -119,59 +100,15 @@ const Menu: React.FC<React.PropsWithChildren<ActionMenuProps>> = ({
})

return (
<MenuContext.Provider
value={{
anchorRef,
renderAnchor,
anchorId,
open: combinedOpenState,
onOpen,
onClose,
// will be undefined for the outermost level, then false for the top menu, then true inside that
isSubmenu: parentMenuContext.isSubmenu !== undefined,
}}
>
<MenuContext.Provider value={{anchorRef, renderAnchor, anchorId, open: combinedOpenState, onOpen, onClose}}>
{contents}
</MenuContext.Provider>
)
}

export type ActionMenuAnchorProps = {children: React.ReactElement; id?: string}
const Anchor = React.forwardRef<HTMLElement, ActionMenuAnchorProps>(({children, ...anchorProps}, anchorRef) => {
const {onOpen, isSubmenu} = React.useContext(MenuContext)

const openSubmenuOnRightArrow: React.KeyboardEventHandler<HTMLElement> = useCallback(
event => {
children.props.onKeyDown?.(event)
if (isSubmenu && event.key === 'ArrowRight' && !event.defaultPrevented) onOpen?.('anchor-key-press')
},
[children, isSubmenu, onOpen],
)

// Add right chevron icon to submenu anchors rendered using `ActionList.Item`
const parentActionListContext = useContext(ActionListContainerContext)
const thisActionListContext = useMemo(
() =>
isSubmenu
? {
...parentActionListContext,
defaultTrailingVisual: <ChevronRightIcon />,
// Default behavior is to close after selecting; we want to open the submenu instead
afterSelect: () => onOpen?.('anchor-click'),
}
: parentActionListContext,
[isSubmenu, onOpen, parentActionListContext],
)

return (
<ActionListContainerContext.Provider value={thisActionListContext}>
{React.cloneElement(children, {
...anchorProps,
ref: anchorRef,
onKeyDown: openSubmenuOnRightArrow,
})}
</ActionListContainerContext.Provider>
)
return React.cloneElement(children, {...anchorProps, ref: anchorRef})
})

/** this component is syntactical sugar 🍭 */
Expand All @@ -196,24 +133,19 @@ type MenuOverlayProps = Partial<OverlayProps> &
const Overlay: React.FC<React.PropsWithChildren<MenuOverlayProps>> = ({
children,
align = 'start',
side,
side = 'outside-bottom',
'aria-labelledby': ariaLabelledby,
...overlayProps
}) => {
// we typecast anchorRef as required instead of optional
// because we know that we're setting it in context in Menu
const {
anchorRef,
renderAnchor,
anchorId,
open,
onOpen,
onClose,
isSubmenu = false,
} = React.useContext(MenuContext) as MandateProps<MenuContextProps, 'anchorRef'>
const {anchorRef, renderAnchor, anchorId, open, onOpen, onClose} = React.useContext(MenuContext) as MandateProps<
MenuContextProps,
'anchorRef'
>

const containerRef = React.useRef<HTMLDivElement>(null)
useMenuKeyboardNavigation(open, onClose, containerRef, anchorRef, isSubmenu)
useMenuKeyboardNavigation(open, onClose, containerRef, anchorRef)

return (
<AnchoredOverlay
Expand All @@ -224,7 +156,7 @@ const Overlay: React.FC<React.PropsWithChildren<MenuOverlayProps>> = ({
onOpen={onOpen}
onClose={onClose}
align={align}
side={side ?? (isSubmenu ? 'outside-right' : 'outside-bottom')}
side={side}
overlayProps={overlayProps}
focusZoneSettings={{focusOutBehavior: 'wrap'}}
>
Expand All @@ -235,7 +167,7 @@ const Overlay: React.FC<React.PropsWithChildren<MenuOverlayProps>> = ({
listRole: 'menu',
listLabelledBy: ariaLabelledby || anchorId,
selectionAttribute: 'aria-checked', // Should this be here?
afterSelect: () => onClose?.('item-select'),
afterSelect: onClose,
}}
>
{children}
Expand Down
Loading
Loading