Skip to content

Commit

Permalink
Disclosure pattern implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
broccolinisoup committed Oct 24, 2022
1 parent 88e4586 commit ef2cb54
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 47 deletions.
139 changes: 97 additions & 42 deletions src/UnderlineNav2/UnderlineNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -129,6 +134,8 @@ export const UnderlineNav = forwardRef(
const navRef = (forwardedRef ?? backupRef) as MutableRefObject<HTMLElement>
const listRef = useRef<HTMLUListElement>(null)
const moreMenuRef = useRef<HTMLLIElement>(null)
const moreMenuBtnRef = useRef<HTMLButtonElement>(null)
const containerRef = React.useRef<HTMLUListElement>(null)

const {theme} = useTheme()

Expand Down Expand Up @@ -194,6 +201,7 @@ export const UnderlineNav = forwardRef(
const afterSelectHandler = (event: React.MouseEvent<HTMLLIElement> | React.KeyboardEvent<HTMLLIElement>) => {
if (!event.defaultPrevented) {
if (typeof afterSelect === 'function') afterSelect(event)
closeOverlay()
}
}

Expand Down Expand Up @@ -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<HTMLButtonElement>) => {
if (event.defaultPrevented || event.button !== 0) {
return
}
toggleOverlay()
},
[toggleOverlay]
)

return (
<UnderlineNavContext.Provider
Expand Down Expand Up @@ -265,46 +313,53 @@ export const UnderlineNav = forwardRef(
{actions.length > 0 && (
<MoreMenuListItem ref={moreMenuRef}>
<Box sx={getDividerStyle(theme)}></Box>
<ActionMenu>
<ActionMenu.Button sx={moreBtnStyles}>More</ActionMenu.Button>
<ActionMenu.Overlay align="end">
<ActionList selectionVariant="single">
{actions.map((action, index) => {
const {children: actionElementChildren, ...actionElementProps} = action.props
return (
<Box key={index} as="li">
<ActionList.Item
sx={menuItemStyles}
as={asNavItem}
{...actionElementProps}
onSelect={(
event: React.MouseEvent<HTMLLIElement> | React.KeyboardEvent<HTMLLIElement>
) => {
swapMenuItemWithListItem(action, index, event, updateListAndMenu)
setSelectEvent(event)
}}
>
<Box
as="span"
sx={{display: 'flex', alignItems: 'center', justifyContent: 'space-between'}}
>
{actionElementChildren}

{loadingCounters ? (
<LoadingCounter />
) : (
actionElementProps.counter !== undefined && (
<CounterLabel>{actionElementProps.counter}</CounterLabel>
)
)}
</Box>
</ActionList.Item>
<Button
ref={moreMenuBtnRef}
sx={moreBtnStyles}
aria-controls="disclosure-widget"
aria-expanded={isWidgetOpen}
onClick={onAnchorClick}
trailingIcon={TriangleDownIcon}
>
More
</Button>
<ActionList
selectionVariant="single"
ref={containerRef}
id="disclosure-widget"
sx={getMenuStyles(theme, isWidgetOpen)}
>
{actions.map((action, index) => {
const {children: actionElementChildren, ...actionElementProps} = action.props
return (
<Box key={index} as="li">
<ActionList.Item
{...actionElementProps}
sx={menuItemStyles}
as={asNavItem}
onSelect={(event: React.MouseEvent<HTMLLIElement> | React.KeyboardEvent<HTMLLIElement>) => {
swapMenuItemWithListItem(action, index, event, updateListAndMenu)
setSelectEvent(event)
closeOverlay()
focusOnMoreMenuBtn()
}}
>
<Box as="span" sx={{display: 'flex', alignItems: 'center', justifyContent: 'space-between'}}>
{actionElementChildren}

{loadingCounters ? (
<LoadingCounter />
) : (
actionElementProps.counter !== undefined && (
<CounterLabel>{actionElementProps.counter}</CounterLabel>
)
)}
</Box>
)
})}
</ActionList>
</ActionMenu.Overlay>
</ActionMenu>
</ActionList.Item>
</Box>
)
})}
</ActionList>
</MoreMenuListItem>
)}
</NavigationList>
Expand Down
25 changes: 21 additions & 4 deletions src/UnderlineNav2/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ export const getNavStyles = (theme?: Theme, props?: Partial<Pick<UnderlineNavPro
borderBottom: '1px solid',
borderBottomColor: `${theme?.colors.border.muted}`,
align: 'row',
alignItems: 'center',
position: 'relative'
alignItems: 'center'
})

export const ulStyles = {
Expand All @@ -52,7 +51,8 @@ export const ulStyles = {
margin: 0,
marginBottom: '-1px',
alignItems: 'center',
gap: `${GAP}px`
gap: `${GAP}px`,
position: 'relative'
}

export const getDividerStyle = (theme?: Theme) => ({
Expand All @@ -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 = (
Expand Down Expand Up @@ -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'
})
2 changes: 1 addition & 1 deletion src/hooks/useOnOutsideClick.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export type TouchOrMouseEvent = MouseEvent | TouchEvent
type TouchOrMouseEventCallback = (event: TouchOrMouseEvent) => boolean | undefined

export type UseOnOutsideClickSettings = {
containerRef: React.RefObject<HTMLDivElement>
containerRef: React.RefObject<HTMLDivElement> | React.RefObject<HTMLUListElement>
ignoreClickRefs?: React.RefObject<HTMLElement>[]
onClickOutside: (e: TouchOrMouseEvent) => void
}
Expand Down

0 comments on commit ef2cb54

Please sign in to comment.