Skip to content

Commit 177ab6b

Browse files
Disclosure pattern initial implementation
1 parent 9391d0d commit 177ab6b

File tree

2 files changed

+121
-46
lines changed

2 files changed

+121
-46
lines changed

src/UnderlineNav2/UnderlineNav.tsx

Lines changed: 114 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import React, {useRef, forwardRef, useCallback, useState, MutableRefObject, RefObject} from 'react'
1+
import React, {useRef, forwardRef, useCallback, useState, MutableRefObject, RefObject, useEffect} from 'react'
22
import Box from '../Box'
33
import sx, {merge, BetterSystemStyleObject, SxProp} from '../sx'
44
import {UnderlineNavContext} from './UnderlineNavContext'
5-
import {ActionMenu} from '../ActionMenu'
6-
import {ActionList} from '../ActionList'
75
import {useResizeObserver, ResizeObserverEntry} from '../hooks/useResizeObserver'
86
import CounterLabel from '../CounterLabel'
97
import {useTheme} from '../ThemeProvider'
@@ -12,6 +10,13 @@ import {ChildWidthArray, ResponsiveProps} from './types'
1210
import {moreBtnStyles, getDividerStyle, getNavStyles, ulStyles, moreMenuStyles, menuItemStyles, GAP} from './styles'
1311
import styled from 'styled-components'
1412
import {LoadingCounter} from './LoadingCounter'
13+
import {Button} from '../Button'
14+
import Link from '../Link'
15+
import {useFocusZone} from '../hooks/useFocusZone'
16+
import {FocusKeys, getAnchoredPosition} from '@primer/behaviors'
17+
import {ChevronDownIcon, TriangleDownIcon} from '@primer/octicons-react'
18+
import {useOnEscapePress} from '../hooks/useOnEscapePress'
19+
import {useOnOutsideClick} from '../hooks/useOnOutsideClick'
1520

1621
export type UnderlineNavProps = {
1722
'aria-label'?: React.AriaAttributes['aria-label']
@@ -131,6 +136,8 @@ export const UnderlineNav = forwardRef(
131136
const navRef = (forwardedRef ?? backupRef) as MutableRefObject<HTMLElement>
132137
const listRef = useRef<HTMLUListElement>(null)
133138
const moreMenuRef = useRef<HTMLLIElement>(null)
139+
const moreMenuBtnRef = useRef<HTMLButtonElement>(null)
140+
const containerRef = React.useRef<HTMLUListElement>(null)
134141

135142
const {theme} = useTheme()
136143

@@ -241,6 +248,49 @@ export const UnderlineNav = forwardRef(
241248
// eslint-disable-next-line no-console
242249
console.warn('Use the `aria-label` prop to provide an accessible label for assistive technology')
243250
}
251+
const [isWidgetOpen, setIsWidgetOpen] = useState(false)
252+
253+
const closeOverlay = React.useCallback(() => {
254+
setIsWidgetOpen(false)
255+
}, [setIsWidgetOpen])
256+
257+
const toggleOverlay = React.useCallback(() => {
258+
setIsWidgetOpen(!isWidgetOpen)
259+
}, [setIsWidgetOpen, isWidgetOpen])
260+
261+
useOnOutsideClick({onClickOutside: closeOverlay, containerRef, ignoreClickRefs: [moreMenuBtnRef]})
262+
263+
useFocusZone({
264+
containerRef: backupRef,
265+
bindKeys: FocusKeys.ArrowVertical | FocusKeys.ArrowHorizontal | FocusKeys.HomeAndEnd | FocusKeys.Tab
266+
})
267+
268+
useFocusZone({
269+
containerRef,
270+
bindKeys: FocusKeys.ArrowVertical | FocusKeys.ArrowHorizontal | FocusKeys.HomeAndEnd,
271+
focusInStrategy: 'first'
272+
})
273+
274+
useOnEscapePress(
275+
(event: KeyboardEvent) => {
276+
if (isWidgetOpen) {
277+
event.preventDefault()
278+
closeOverlay()
279+
}
280+
// onClose('escape')
281+
// event.preventDefault()
282+
},
283+
[isWidgetOpen]
284+
)
285+
const onAnchorClick = useCallback(
286+
(event: React.MouseEvent<HTMLButtonElement>) => {
287+
if (event.defaultPrevented || event.button !== 0) {
288+
return
289+
}
290+
toggleOverlay()
291+
},
292+
[toggleOverlay]
293+
)
244294

245295
return (
246296
<UnderlineNavContext.Provider
@@ -271,46 +321,68 @@ export const UnderlineNav = forwardRef(
271321
{actions.length > 0 && (
272322
<MoreMenuListItem ref={moreMenuRef}>
273323
<Box sx={getDividerStyle(theme)}></Box>
274-
<ActionMenu>
275-
<ActionMenu.Button sx={moreBtnStyles}>More</ActionMenu.Button>
276-
<ActionMenu.Overlay align="end">
277-
<ActionList selectionVariant="single">
278-
{actions.map((action, index) => {
279-
const {children: actionElementChildren, ...actionElementProps} = action.props
280-
return (
281-
<Box key={index} as="li">
282-
<ActionList.Item
283-
sx={menuItemStyles}
284-
as={asNavItem}
285-
{...actionElementProps}
286-
onSelect={(
287-
event: React.MouseEvent<HTMLLIElement> | React.KeyboardEvent<HTMLLIElement>
288-
) => {
289-
swapMenuItemWithListItem(action, index, event, updateListAndMenu)
290-
setSelectEvent(event)
291-
}}
292-
>
293-
<Box
294-
as="span"
295-
sx={{display: 'flex', alignItems: 'center', justifyContent: 'space-between'}}
296-
>
297-
{actionElementChildren}
298-
299-
{loadingCounters ? (
300-
<LoadingCounter />
301-
) : (
302-
actionElementProps.counter !== undefined && (
303-
<CounterLabel>{actionElementProps.counter}</CounterLabel>
304-
)
305-
)}
306-
</Box>
307-
</ActionList.Item>
324+
<Button
325+
ref={moreMenuBtnRef}
326+
sx={moreBtnStyles}
327+
aria-controls="disclosure-widget"
328+
aria-expanded={isWidgetOpen}
329+
onClick={onAnchorClick}
330+
trailingIcon={TriangleDownIcon}
331+
>
332+
More
333+
</Button>
334+
335+
<Box
336+
as="ul"
337+
// @ts-ignore Box doesn't have type support for `ref` used in combination with `as`
338+
ref={containerRef}
339+
id="disclosure-widget"
340+
sx={{
341+
position: 'absolute',
342+
top: '90%',
343+
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24)',
344+
borderRadius: '12px',
345+
right: '0',
346+
backgroundColor: `${theme?.colors.canvas.overlay}`,
347+
listStyle: 'none',
348+
padding: '8px',
349+
minWidth: '192px',
350+
maxWidth: '640px',
351+
display: isWidgetOpen ? 'block' : 'none'
352+
}}
353+
// onEscape={() => setIsWidgetOpen(false)}
354+
>
355+
{actions.map((action, index) => {
356+
const {children: actionElementChildren, ...actionElementProps} = action.props
357+
return (
358+
<Box key={index} as="li">
359+
<a
360+
href="#hello"
361+
// sx={menuItemStyles}
362+
// as={asNavItem}
363+
onClick={(event: React.MouseEvent<HTMLAnchorElement>) => {
364+
swapMenuItemWithListItem(action, index, event, updateListAndMenu)
365+
setSelectEvent(event)
366+
closeOverlay()
367+
moreMenuBtnRef.current?.focus()
368+
}}
369+
>
370+
<Box as="span" sx={{display: 'flex', alignItems: 'center', justifyContent: 'space-between'}}>
371+
{actionElementChildren}
372+
373+
{loadingCounters ? (
374+
<LoadingCounter />
375+
) : (
376+
actionElementProps.counter !== undefined && (
377+
<CounterLabel>{actionElementProps.counter}</CounterLabel>
378+
)
379+
)}
308380
</Box>
309-
)
310-
})}
311-
</ActionList>
312-
</ActionMenu.Overlay>
313-
</ActionMenu>
381+
</a>
382+
</Box>
383+
)
384+
})}
385+
</Box>
314386
</MoreMenuListItem>
315387
)}
316388
</NavigationList>

src/UnderlineNav2/styles.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ export const getNavStyles = (theme?: Theme, props?: Partial<Pick<UnderlineNavPro
4040
borderBottom: '1px solid',
4141
borderBottomColor: `${theme?.colors.border.muted}`,
4242
align: 'row',
43-
alignItems: 'center',
44-
position: 'relative'
43+
alignItems: 'center'
4544
})
4645

4746
export const ulStyles = {
@@ -52,7 +51,8 @@ export const ulStyles = {
5251
margin: 0,
5352
marginBottom: '-1px',
5453
alignItems: 'center',
55-
gap: `${GAP}px`
54+
gap: `${GAP}px`,
55+
position: 'relative'
5656
}
5757

5858
export const getDividerStyle = (theme?: Theme) => ({
@@ -71,7 +71,10 @@ export const moreBtnStyles = {
7171
fontWeight: 'normal',
7272
boxShadow: 'none',
7373
paddingY: 1,
74-
paddingX: 2
74+
paddingX: 2,
75+
'& > span[data-component="trailingIcon"]': {
76+
marginLeft: 0
77+
}
7578
}
7679

7780
export const getLinkStyles = (

0 commit comments

Comments
 (0)