Skip to content
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: 5 additions & 0 deletions .changeset/thirty-mayflies-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

UnderlineNav.Link renamed to UnderlineNav.Item along with updated styles
18 changes: 9 additions & 9 deletions docs/content/drafts/UnderlineNav2.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,29 @@ description: Use an underlined nav to allow tab like navigation with overflow be

```jsx live drafts
<UnderlineNav label="simple nav">
<UnderlineNav.Link selected>Item 1</UnderlineNav.Link>
<UnderlineNav.Link>Item 2</UnderlineNav.Link>
<UnderlineNav.Link>Item 3</UnderlineNav.Link>
<UnderlineNav.Item selected>Item 1</UnderlineNav.Item>
<UnderlineNav.Item>Item 2</UnderlineNav.Item>
<UnderlineNav.Item>Item 3</UnderlineNav.Item>
</UnderlineNav>
```

### With icons

```jsx live drafts
<UnderlineNav label="simple nav with icons">
<UnderlineNav.Link selected leadingIcon={EyeIcon}>
<UnderlineNav.Item selected leadingIcon={EyeIcon}>
Item 1
</UnderlineNav.Link>
<UnderlineNav.Link>Item 2</UnderlineNav.Link>
</UnderlineNav.Item>
<UnderlineNav.Item>Item 2</UnderlineNav.Item>
</UnderlineNav>
```

### Small variant

```jsx live drafts
<UnderlineNav label="small variant" variant="small">
<UnderlineNav.Link selected>Item 1</UnderlineNav.Link>
<UnderlineNav.Link>Item 2</UnderlineNav.Link>
<UnderlineNav.Item selected>Item 1</UnderlineNav.Item>
<UnderlineNav.Item>Item 2</UnderlineNav.Item>
</UnderlineNav>
```

Expand Down Expand Up @@ -65,7 +65,7 @@ description: Use an underlined nav to allow tab like navigation with overflow be
<PropsTableSxRow />
</PropsTable>

### UnderlineNav.Link
### UnderlineNav.Item

<PropsTable>
<PropsTableRow name="leadingIcon" type="Component" description="The leading icon comes before item label" />
Expand Down
20 changes: 10 additions & 10 deletions src/UnderlineNav2/UnderlineNav.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import React from 'react'
import '@testing-library/jest-dom/extend-expect'
import {render} from '@testing-library/react'

import UnderlineNav from '.'
import {UnderlineNav} from '.'

describe('UnderlineNav', () => {
test('selected nav', () => {
const {getByText} = render(
<UnderlineNav label="Test nav">
<UnderlineNav.Link selected>Item 1</UnderlineNav.Link>
<UnderlineNav.Link>Item 2</UnderlineNav.Link>
<UnderlineNav.Link>Item 3</UnderlineNav.Link>
<UnderlineNav.Item selected>Item 1</UnderlineNav.Item>
<UnderlineNav.Item>Item 2</UnderlineNav.Item>
<UnderlineNav.Item>Item 3</UnderlineNav.Item>
</UnderlineNav>
)
const selectedNavLink = getByText('Item 1').closest('a')
Expand All @@ -20,9 +20,9 @@ describe('UnderlineNav', () => {
test('basic nav functionality', () => {
const {container} = render(
<UnderlineNav label="Test nav">
<UnderlineNav.Link selected>Item 1</UnderlineNav.Link>
<UnderlineNav.Link>Item 2</UnderlineNav.Link>
<UnderlineNav.Link>Item 3</UnderlineNav.Link>
<UnderlineNav.Item selected>Item 1</UnderlineNav.Item>
<UnderlineNav.Item>Item 2</UnderlineNav.Item>
<UnderlineNav.Item>Item 3</UnderlineNav.Item>
</UnderlineNav>
)
expect(container.getElementsByTagName('nav').length).toEqual(1)
Expand All @@ -33,9 +33,9 @@ describe('UnderlineNav', () => {
test('respect align prop', () => {
const {container} = render(
<UnderlineNav label="Test nav" align="right">
<UnderlineNav.Link selected>Item 1</UnderlineNav.Link>
<UnderlineNav.Link>Item 2</UnderlineNav.Link>
<UnderlineNav.Link>Item 3</UnderlineNav.Link>
<UnderlineNav.Item selected>Item 1</UnderlineNav.Item>
<UnderlineNav.Item>Item 2</UnderlineNav.Item>
<UnderlineNav.Item>Item 3</UnderlineNav.Item>
</UnderlineNav>
)
const nav = container.getElementsByTagName('nav')[0]
Expand Down
20 changes: 17 additions & 3 deletions src/UnderlineNav2/UnderlineNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {UnderlineNavContext} from './UnderlineNavContext'
import {ActionMenu} from '../ActionMenu'
import {ActionList} from '../ActionList'
import {useResizeObserver, ResizeObserverEntry} from '../hooks/useResizeObserver'
import {useFocusZone} from '../hooks/useFocusZone'
import {FocusKeys} from '@primer/behaviors'

type Overflow = 'auto' | 'menu' | 'scroll'
type ChildWidthArray = Array<{width: number}>
Expand Down Expand Up @@ -84,6 +86,16 @@ export const UnderlineNav = forwardRef(
) => {
const backupRef = useRef<HTMLElement>(null)
const newRef = (forwardedRef ?? backupRef) as MutableRefObject<HTMLElement>

// This might change if we decide tab through the navigation items rather than navigationg with the arrow keys.
// TBD. In the meantime keeping it as a menu with the focus trap.
// ref: https://www.w3.org/WAI/ARIA/apg/example-index/menubar/menubar-navigation.html (Keyboard Support)
useFocusZone({
containerRef: backupRef,
bindKeys: FocusKeys.ArrowHorizontal | FocusKeys.HomeAndEnd,
focusOutBehavior: 'wrap'
})

const styles = {
display: 'flex',
justifyContent: align === 'right' ? 'flex-end' : 'space-between',
Expand All @@ -98,7 +110,8 @@ export const UnderlineNav = forwardRef(
listStyle: 'none',
padding: '0',
margin: '0',
marginBottom: '-1px'
marginBottom: '-1px',
alignItems: 'center'
}

const [selectedLink, setSelectedLink] = useState<RefObject<HTMLElement> | undefined>(undefined)
Expand Down Expand Up @@ -143,14 +156,15 @@ export const UnderlineNav = forwardRef(
<UnderlineNavContext.Provider
value={{setChildrenWidth, selectedLink, setSelectedLink, afterSelect: afterSelectHandler, variant}}
>
<Box as={as} sx={merge(styles, sxProp)} aria-label={label} ref={newRef}>
<Box tabIndex={0} as={as} sx={merge(styles, sxProp)} aria-label={label} ref={newRef}>
<Box as="ul" sx={merge<BetterSystemStyleObject>(overflowStyles, ulStyles)}>
{responsiveProps.items}
</Box>

{actions.length > 0 && (
<ActionMenu>
<ActionMenu.Button>More</ActionMenu.Button>
{/* set margin 0 here because safari puts extra margin around the button */}
<ActionMenu.Button sx={{m: 0}}>More</ActionMenu.Button>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to fix extra margin around the button on safari

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow, can you add a comment here about this?

<ActionMenu.Overlay>
<ActionList>
{actions.map((action, index) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {merge, SxProp, BetterSystemStyleObject} from '../sx'
import {IconProps} from '@primer/octicons-react'
import {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
import {UnderlineNavContext} from './UnderlineNavContext'
import CounterLabel from '../CounterLabel'
import {Theme, useTheme} from '../ThemeProvider'

// adopted from React.AnchorHTMLAttributes
type LinkProps = {
Expand All @@ -18,7 +20,7 @@ type LinkProps = {
referrerPolicy?: React.AnchorHTMLAttributes<HTMLAnchorElement>['referrerPolicy']
}

export type UnderlineNavLinkProps = {
export type UnderlineNavItemProps = {
/**
* Primary content for an NavLink
*/
Expand All @@ -36,16 +38,21 @@ export type UnderlineNavLinkProps = {
*/
leadingIcon?: React.FunctionComponent<IconProps>
as?: React.ElementType
/**
* Counter
*/
counter?: number
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danielguillan - Wondering if we are going to support loading state for counters.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just making a comment here for the types. I added the counter prop with only number type just to make the Counter component redundant. We can add another type for the loading state if we are going to support 😊

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we are going to support loading states. We can add support incrementally in another PR if that makes things easier.

} & SxProp &
LinkProps

export const UnderlineNavLink = forwardRef(
export const UnderlineNavItem = forwardRef(
(
{
sx: sxProp = {},
as: Component = 'a',
href = '#',
children,
counter,
onSelect,
selected: preSelected = false,
leadingIcon: LeadingIcon,
Expand All @@ -56,11 +63,13 @@ export const UnderlineNavLink = forwardRef(
const backupRef = useRef<HTMLElement>(null)
const ref = forwardedRef ?? backupRef
const {setChildrenWidth, selectedLink, setSelectedLink, afterSelect, variant} = useContext(UnderlineNavContext)
const {theme} = useTheme()
useLayoutEffect(() => {
const domRect = (ref as MutableRefObject<HTMLElement>).current.getBoundingClientRect()
setChildrenWidth({width: domRect.width})
preSelected && selectedLink === undefined && setSelectedLink(ref as RefObject<HTMLElement>)
}, [ref, preSelected, selectedLink, setSelectedLink, setChildrenWidth])

const iconWrapStyles = {
alignItems: 'center',
display: 'inline-flex',
Expand All @@ -70,29 +79,72 @@ export const UnderlineNavLink = forwardRef(
const textStyles: BetterSystemStyleObject = {
whiteSpace: 'nowrap'
}

const wrapperStyles = {
display: 'inline-flex',
paddingY: 1,
paddingX: 2,
borderRadius: 2
}
const smallVariantLinkStyles = {
paddingY: 2,
paddingY: 1,
fontSize: 0
}
const defaultVariantLinkStyles = {
paddingY: 3,
paddingY: 2,
fontSize: 1
}

const linkStyles = {
// eslint-disable-next-line no-shadow
const linkStyles = (theme?: Theme) => ({
position: 'relative',
display: 'inline-flex',
color: 'fg.default',
textAlign: 'center',
borderBottom: '2px solid transparent',
borderColor: selectedLink === ref ? 'primer.border.active' : 'transparent',
textDecoration: 'none',
paddingX: 2,
marginRight: 3,
paddingX: 1,
...(variant === 'small' ? smallVariantLinkStyles : defaultVariantLinkStyles),
'&:hover, &:focus': {
borderColor: selectedLink === ref ? 'primer.border.active' : 'neutral.muted',
transition: '0.2s ease'
'&:hover > div[data-component="wrapper"] ': {
backgroundColor: theme?.colors.neutral.muted,
transition: 'background .12s ease-out'
},
'&:focus': {
outline: 0,
'& > div[data-component="wrapper"]': {
boxShadow: `inset 0 0 0 2px ${theme?.colors.accent.fg}`
},
// where focus-visible is supported, remove the focus box-shadow
'&:not(:focus-visible) > div[data-component="wrapper"]': {
boxShadow: 'none'
}
},
'&:focus-visible > div[data-component="wrapper"]': {
boxShadow: `inset 0 0 0 2px ${theme?.colors.accent.fg}`
},
// renders a visibly hidden "copy" of the label in bold, reserving box space for when label becomes bold on selected
'& span[data-content]::before': {
content: 'attr(data-content)',
display: 'block',
height: 0,
fontWeight: '600',
visibility: 'hidden'
},
// selected state styles
'&::after': {
position: 'absolute',
right: '50%',
bottom: 0,
width: `calc(100% - 8px)`,
height: 2,
content: '""',
bg: selectedLink === ref ? theme?.colors.primer.border.active : 'transparent',
borderRadius: 0,
transform: 'translate(50%, -50%)'
}
})

const counterStyles = {
marginLeft: 2
}
const keyPressHandler = React.useCallback(
event => {
Expand All @@ -118,29 +170,41 @@ export const UnderlineNavLink = forwardRef(
[onSelect, afterSelect, ref, setSelectedLink]
)
return (
<Box as="li">
<Box as="li" sx={{display: 'flex', flexDirection: 'column', alignItems: 'center'}}>
<Box
as={Component}
href={href}
onKeyPress={keyPressHandler}
onClick={clickHandler}
{...(selectedLink === ref ? {'aria-current': 'page'} : {})}
sx={merge(linkStyles, sxProp as SxProp)}
sx={merge(linkStyles(theme), sxProp as SxProp)}
{...props}
ref={ref}
>
{LeadingIcon && (
<Box as="span" data-component="leadingIcon" sx={iconWrapStyles}>
<LeadingIcon />
</Box>
)}
{children && (
<Box as="span" data-component="text" sx={textStyles}>
{children}
</Box>
)}
<Box as="div" data-component="wrapper" sx={wrapperStyles}>
{LeadingIcon && (
<Box as="span" data-component="leadingIcon" sx={iconWrapStyles}>
<LeadingIcon />
</Box>
)}
{children && (
<Box
as="span"
data-component="text"
data-content={children}
sx={selectedLink === ref ? {fontWeight: 600, ...{textStyles}} : {textStyles}}
>
{children}
</Box>
)}
{counter && (
<Box as="span" data-component="counter" sx={counterStyles}>
<CounterLabel>{counter}</CounterLabel>
</Box>
)}
</Box>
</Box>
</Box>
)
}
) as PolymorphicForwardRefComponent<'a', UnderlineNavLinkProps>
) as PolymorphicForwardRefComponent<'a', UnderlineNavItemProps>
Loading