diff --git a/src/UnderlineNav2/UnderlineNav.tsx b/src/UnderlineNav2/UnderlineNav.tsx index cf256f9409a..fce52563426 100644 --- a/src/UnderlineNav2/UnderlineNav.tsx +++ b/src/UnderlineNav2/UnderlineNav.tsx @@ -2,16 +2,21 @@ import React, {useRef, forwardRef, useCallback, useState, MutableRefObject, RefO import Box from '../Box' import sx, {merge, BetterSystemStyleObject, SxProp} from '../sx' import {UnderlineNavContext} from './UnderlineNavContext' -import {ActionMenu} from '../ActionMenu' -import {ActionList} from '../ActionList' import {useResizeObserver, ResizeObserverEntry} from '../hooks/useResizeObserver' import CounterLabel from '../CounterLabel' import {useTheme} from '../ThemeProvider' import {ChildWidthArray, ResponsiveProps} from './types' -import {moreBtnStyles, getDividerStyle, getNavStyles, ulStyles, menuItemStyles, GAP} from './styles' +import {moreBtnStyles, getDividerStyle, getNavStyles, ulStyles, getMenuStyles, menuItemStyles, GAP} from './styles' import styled from 'styled-components' import {LoadingCounter} from './LoadingCounter' +import {Button} from '../Button' +import {useFocusZone} from '../hooks/useFocusZone' +import {FocusKeys} from '@primer/behaviors' +import {TriangleDownIcon} from '@primer/octicons-react' +import {useOnEscapePress} from '../hooks/useOnEscapePress' +import {useOnOutsideClick} from '../hooks/useOnOutsideClick' +import {ActionList} from '../ActionList' export type UnderlineNavProps = { 'aria-label'?: React.AriaAttributes['aria-label'] @@ -129,6 +134,8 @@ export const UnderlineNav = forwardRef( const navRef = (forwardedRef ?? backupRef) as MutableRefObject const listRef = useRef(null) const moreMenuRef = useRef(null) + const moreMenuBtnRef = useRef(null) + const containerRef = React.useRef(null) const {theme} = useTheme() @@ -194,6 +201,7 @@ export const UnderlineNav = forwardRef( const afterSelectHandler = (event: React.MouseEvent | React.KeyboardEvent) => { if (!event.defaultPrevented) { if (typeof afterSelect === 'function') afterSelect(event) + closeOverlay() } } @@ -235,6 +243,46 @@ export const UnderlineNav = forwardRef( // eslint-disable-next-line no-console console.warn('Use the `aria-label` prop to provide an accessible label for assistive technology') } + const [isWidgetOpen, setIsWidgetOpen] = useState(false) + + const closeOverlay = React.useCallback(() => { + setIsWidgetOpen(false) + }, [setIsWidgetOpen]) + + const toggleOverlay = React.useCallback(() => { + setIsWidgetOpen(!isWidgetOpen) + }, [setIsWidgetOpen, isWidgetOpen]) + + const focusOnMoreMenuBtn = React.useCallback(() => { + moreMenuBtnRef.current?.focus() + }, []) + + useFocusZone({ + containerRef: backupRef, + bindKeys: FocusKeys.ArrowVertical | FocusKeys.ArrowHorizontal | FocusKeys.HomeAndEnd | FocusKeys.Tab + }) + + useOnEscapePress( + (event: KeyboardEvent) => { + if (isWidgetOpen) { + event.preventDefault() + closeOverlay() + focusOnMoreMenuBtn() + } + }, + [isWidgetOpen] + ) + + useOnOutsideClick({onClickOutside: closeOverlay, containerRef, ignoreClickRefs: [moreMenuBtnRef]}) + const onAnchorClick = useCallback( + (event: React.MouseEvent) => { + if (event.defaultPrevented || event.button !== 0) { + return + } + toggleOverlay() + }, + [toggleOverlay] + ) return ( 0 && ( - - More - - - {actions.map((action, index) => { - const {children: actionElementChildren, ...actionElementProps} = action.props - return ( - - | React.KeyboardEvent - ) => { - swapMenuItemWithListItem(action, index, event, updateListAndMenu) - setSelectEvent(event) - }} - > - - {actionElementChildren} - - {loadingCounters ? ( - - ) : ( - actionElementProps.counter !== undefined && ( - {actionElementProps.counter} - ) - )} - - + + + {actions.map((action, index) => { + const {children: actionElementChildren, ...actionElementProps} = action.props + return ( + + | React.KeyboardEvent) => { + swapMenuItemWithListItem(action, index, event, updateListAndMenu) + setSelectEvent(event) + closeOverlay() + focusOnMoreMenuBtn() + }} + > + + {actionElementChildren} + + {loadingCounters ? ( + + ) : ( + actionElementProps.counter !== undefined && ( + {actionElementProps.counter} + ) + )} - ) - })} - - - + + + ) + })} + )} diff --git a/src/UnderlineNav2/styles.ts b/src/UnderlineNav2/styles.ts index 5b82b8190ce..de949690580 100644 --- a/src/UnderlineNav2/styles.ts +++ b/src/UnderlineNav2/styles.ts @@ -39,8 +39,7 @@ export const getNavStyles = (theme?: Theme, props?: Partial ({ @@ -71,7 +71,10 @@ export const moreBtnStyles = { fontWeight: 'normal', boxShadow: 'none', paddingY: 1, - paddingX: 2 + paddingX: 2, + '& > span[data-component="trailingIcon"]': { + marginLeft: 0 + } } export const getLinkStyles = ( @@ -142,3 +145,17 @@ export const menuItemStyles = { // To reset the style when the menu items are rendered as react router links textDecoration: 'none' } + +export const getMenuStyles = (theme?: Theme, isWidgetOpen?: boolean) => ({ + position: 'absolute', + top: '90%', + right: '0', + boxShadow: '0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24)', + borderRadius: '12px', + backgroundColor: `${theme?.colors.canvas.overlay}`, + listStyle: 'none', + // Values are from ActionMenu + minWidth: '192px', + maxWidth: '640px', + display: isWidgetOpen ? 'block' : 'none' +}) diff --git a/src/hooks/useOnOutsideClick.tsx b/src/hooks/useOnOutsideClick.tsx index 9cc3d38c404..58af086f1d8 100644 --- a/src/hooks/useOnOutsideClick.tsx +++ b/src/hooks/useOnOutsideClick.tsx @@ -4,7 +4,7 @@ export type TouchOrMouseEvent = MouseEvent | TouchEvent type TouchOrMouseEventCallback = (event: TouchOrMouseEvent) => boolean | undefined export type UseOnOutsideClickSettings = { - containerRef: React.RefObject + containerRef: React.RefObject | React.RefObject ignoreClickRefs?: React.RefObject[] onClickOutside: (e: TouchOrMouseEvent) => void }