diff --git a/docs/content/drafts/UnderlineNav2.mdx b/docs/content/drafts/UnderlineNav2.mdx
index 25298112592..50ec7349073 100644
--- a/docs/content/drafts/UnderlineNav2.mdx
+++ b/docs/content/drafts/UnderlineNav2.mdx
@@ -17,9 +17,9 @@ import {UnderlineNav} from '@primer/react/drafts'
```jsx live drafts
- Item 1
- Item 2
- Item 3
+ Code
+ Issues
+ Pull Requests
```
@@ -48,90 +48,156 @@ import {UnderlineNav} from '@primer/react/drafts'
### Overflow Behaviour
-When overflow occurs, the component first hides icons if present to optimize for space and show as many items as possible. (Only for fine pointer devices)
+Component behaves depending on the pointer type in case of an overflow.
-#### Items Without Icons
+#### Fine Pointer Devices
-```jsx live drafts
-
-
- Code
-
-
- Issues
-
-
- Pull Requests
-
- Discussions
-
- Actions
-
-
- Projects
-
- Security
- Insights
-
- Settings
-
-
+Component first hides icons if they present to optimize for space and show as many items as possible. If there is still an overflow, it will display the items that don't fit in the `More` menu.
+
+```javascript noinline live drafts
+const Navigation = () => {
+ const items = [
+ {navigation: 'Code', icon: CodeIcon},
+ {navigation: 'Issues', icon: IssueOpenedIcon, counter: 120},
+ {navigation: 'Pull Requests', icon: GitPullRequestIcon, counter: 13},
+ {navigation: 'Discussions', icon: CommentDiscussionIcon, counter: 5},
+ {navigation: 'Actions', icon: PlayIcon, counter: 4},
+ {navigation: 'Projects', icon: ProjectIcon, counter: 9},
+ {navigation: 'Insights', icon: GraphIcon},
+ {navigation: 'Settings', icon: GearIcon, counter: 10},
+ {navigation: 'Security', icon: ShieldLockIcon}
+ ]
+ const [selectedIndex, setSelectedIndex] = React.useState(0)
+ return (
+
+
+
+ {items.map((item, index) => (
+ {
+ setSelectedIndex(index)
+ e.preventDefault()
+ }}
+ counter={item.counter}
+ >
+ {item.navigation}
+
+ ))}
+
+
+
+ )
+}
+
+render()
```
-#### Display `More` Menu
+#### Coarse Pointer Devices
-If there is still overflow, the component will behave depending on the pointer.
+```javascript noinline live drafts
+const items = [
+ {navigation: 'Code', icon: CodeIcon},
+ {navigation: 'Issues', icon: IssueOpenedIcon, counter: 120},
+ {navigation: 'Pull Requests', icon: GitPullRequestIcon, counter: 13},
+ {navigation: 'Discussions', icon: CommentDiscussionIcon, counter: 5},
+ {navigation: 'Actions', icon: PlayIcon, counter: 4},
+ {navigation: 'Projects', icon: ProjectIcon, counter: 9},
+ {navigation: 'Insights', icon: GraphIcon},
+ {navigation: 'Settings', icon: GearIcon, counter: 10},
+ {navigation: 'Security', icon: ShieldLockIcon}
+]
+const Navigation = () => {
+ const [selectedIndex, setSelectedIndex] = React.useState(0)
+ return (
+
+
+
+ {items.map((item, index) => (
+ {
+ setSelectedIndex(index)
+ e.preventDefault()
+ }}
+ counter={item.counter}
+ >
+ {item.navigation}
+
+ ))}
+
+
+
+ )
+}
-```jsx live drafts
-
-
- Code
-
-
- Issues
-
-
- Pull Requests
-
- Discussions
-
- Actions
-
-
- Projects
-
- Security
-
- Insights
-
-
- Settings
-
- Wiki
-
+render()
```
-### Loading state for counters
+### Loading State For Counters
```jsx live drafts
- Item 1
+ Code
- Item 2
- Item 3
+ Issues
+ Pull Requests
```
+### With React Router
+
+```jsx
+import {Link, useNavigate} from 'react-router-dom'
+import {UnderlineNav} from '@primer/react/drafts'
+
+const Navigation = () => {
+ const navigate = useNavigate()
+ return (
+
+
+ Code
+
+ {
+ navigate('issues')
+ }}
+ >
+ Issues
+
+
+ Pull Requests
+
+
+ )
+}
+```
+
+
+ You can bind the routing with both 'to' and 'onSelect' prop here. However; please note that if an 'href' prop is
+ passed, it will be ignored here.
+
+
## Props
### UnderlineNav
+
+
diff --git a/docs/src/@primer/gatsby-theme-doctocat/live-code-scope.js b/docs/src/@primer/gatsby-theme-doctocat/live-code-scope.js
index 991f401f1fd..b5c891a12f8 100644
--- a/docs/src/@primer/gatsby-theme-doctocat/live-code-scope.js
+++ b/docs/src/@primer/gatsby-theme-doctocat/live-code-scope.js
@@ -4,6 +4,7 @@ import * as primerComponents from '@primer/react'
import * as drafts from '@primer/react/drafts'
import * as deprecated from '@primer/react/deprecated'
import {Placeholder} from '@primer/react/Placeholder'
+import {MatchMedia} from '@primer/react/hooks/useMedia'
import React from 'react'
import State from '../../../components/State'
@@ -24,6 +25,7 @@ export default function resolveScope(metastring) {
...(metastring.includes('deprecated') ? deprecated : {}),
ReactRouterLink,
State,
+ MatchMedia,
Placeholder
}
}
diff --git a/src/UnderlineNav2/UnderlineNav.tsx b/src/UnderlineNav2/UnderlineNav.tsx
index 5b60b502900..6155db5f6a0 100644
--- a/src/UnderlineNav2/UnderlineNav.tsx
+++ b/src/UnderlineNav2/UnderlineNav.tsx
@@ -19,7 +19,8 @@ import {
ulStyles,
scrollStyles,
moreMenuStyles,
- menuItemStyles
+ menuItemStyles,
+ GAP
} from './styles'
import {ArrowButton} from './UnderlineNavArrowButton'
import styled from 'styled-components'
@@ -146,7 +147,8 @@ const calculatePossibleItems = (childWidthArray: ChildWidthArray, navWidth: numb
breakpoint = index - 1
break
} else {
- sumsOfChildWidth = sumsOfChildWidth + childWidth.width
+ // The the gap between items into account when calculating the number of items possible
+ sumsOfChildWidth = sumsOfChildWidth + childWidth.width + GAP
}
}
@@ -224,6 +226,8 @@ export const UnderlineNav = forwardRef(
const isCoarsePointer = useMedia('(pointer: coarse)')
+ const [asNavItem, setAsNavItem] = useState('a')
+
const [selectedLink, setSelectedLink] = useState | undefined>(undefined)
const [focusedLink, setFocusedLink] = useState | null>(null)
@@ -347,6 +351,7 @@ export const UnderlineNav = forwardRef(
selectedLinkText,
setSelectedLinkText,
setFocusedLink,
+ setAsNavItem,
selectEvent,
afterSelect: afterSelectHandler,
variant,
@@ -354,74 +359,82 @@ export const UnderlineNav = forwardRef(
iconsVisible
}}
>
- (getNavStyles(theme, {align}), sxProp)}
- aria-label={ariaLabel}
- ref={navRef}
- >
- {isCoarsePointer && (
- 0}
- onScrollWithButton={onScrollWithButton}
- aria-label={ariaLabel}
- />
- )}
-
- (responsiveProps.overflowStyles, ulStyles)} ref={listRef}>
- {responsiveProps.items}
- {actions.length > 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}
- )}
+
+ (getNavStyles(theme, {align}), sxProp)}
+ aria-label={ariaLabel}
+ ref={navRef}
+ >
+ {isCoarsePointer && (
+ 0}
+ onScrollWithButton={onScrollWithButton}
+ aria-label={ariaLabel}
+ />
+ )}
+
+ (responsiveProps.overflowStyles, ulStyles)} ref={listRef}>
+ {responsiveProps.items}
+ {actions.length > 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}
+ )
+ )}
+
+
-
- )
- })}
-
-
-
-
+ )
+ })}
+
+
+
+
+ )}
+
+
+ {isCoarsePointer && (
+ 0}
+ onScrollWithButton={onScrollWithButton}
+ aria-label={ariaLabel}
+ />
)}
-
-
- {isCoarsePointer && (
- 0}
- onScrollWithButton={onScrollWithButton}
- aria-label={ariaLabel}
- />
- )}
+
)
diff --git a/src/UnderlineNav2/UnderlineNavArrowButton.tsx b/src/UnderlineNav2/UnderlineNavArrowButton.tsx
index 81d5822d592..8ac51ab1bef 100644
--- a/src/UnderlineNav2/UnderlineNavArrowButton.tsx
+++ b/src/UnderlineNav2/UnderlineNavArrowButton.tsx
@@ -56,7 +56,7 @@ const ArrowButton = ({
aria-label={`Scroll ${ariaLabel} navigation ${type}`}
onClick={(e: React.MouseEvent) => onScrollWithButton(e, direction)}
icon={type === 'left' ? ChevronLeftIcon : ChevronRightIcon}
- sx={getArrowBtnStyles(theme, type)}
+ sx={getArrowBtnStyles(theme)}
aria-disabled={!show}
/>
diff --git a/src/UnderlineNav2/UnderlineNavContext.tsx b/src/UnderlineNav2/UnderlineNavContext.tsx
index 285810ba36f..2175eb06834 100644
--- a/src/UnderlineNav2/UnderlineNavContext.tsx
+++ b/src/UnderlineNav2/UnderlineNavContext.tsx
@@ -10,6 +10,7 @@ export const UnderlineNavContext = createContext<{
selectedLinkText: string
setSelectedLinkText: React.Dispatch>
setFocusedLink: React.Dispatch | null>>
+ setAsNavItem: React.Dispatch>
selectEvent: React.MouseEvent | React.KeyboardEvent | null
afterSelect?: (event: React.MouseEvent | React.KeyboardEvent) => void
variant: 'default' | 'small'
@@ -24,6 +25,7 @@ export const UnderlineNavContext = createContext<{
selectedLinkText: '',
setSelectedLinkText: () => null,
setFocusedLink: () => null,
+ setAsNavItem: () => null,
selectEvent: null,
variant: 'default',
loadingCounters: false,
diff --git a/src/UnderlineNav2/UnderlineNavItem.tsx b/src/UnderlineNav2/UnderlineNavItem.tsx
index 4aa9624cfd5..9572fbf3b15 100644
--- a/src/UnderlineNav2/UnderlineNavItem.tsx
+++ b/src/UnderlineNav2/UnderlineNavItem.tsx
@@ -42,7 +42,7 @@ export type UnderlineNavItemProps = {
/**
* Counter
*/
- counter?: number
+ counter?: number | string
} & SxProp &
LinkProps
@@ -72,6 +72,7 @@ export const UnderlineNavItem = forwardRef(
selectedLinkText,
setSelectedLinkText,
setFocusedLink,
+ setAsNavItem,
selectEvent,
afterSelect,
variant,
@@ -107,6 +108,8 @@ export const UnderlineNavItem = forwardRef(
if (typeof onSelect === 'function' && selectEvent !== null) onSelect(selectEvent)
setSelectedLinkText('')
}
+
+ setAsNavItem(Component)
}, [
ref,
preSelected,
@@ -117,7 +120,9 @@ export const UnderlineNavItem = forwardRef(
setChildrenWidth,
setNoIconChildrenWidth,
onSelect,
- selectEvent
+ selectEvent,
+ Component,
+ setAsNavItem
])
const keyPressHandler = React.useCallback(
@@ -127,7 +132,6 @@ export const UnderlineNavItem = forwardRef(
if (typeof afterSelect === 'function') afterSelect(event)
}
setSelectedLink(ref as RefObject)
- event.preventDefault()
},
[onSelect, afterSelect, ref, setSelectedLink]
)
@@ -138,7 +142,6 @@ export const UnderlineNavItem = forwardRef(
if (typeof afterSelect === 'function') afterSelect(event)
}
setSelectedLink(ref as RefObject)
- event.preventDefault()
},
[onSelect, afterSelect, ref, setSelectedLink]
)
@@ -172,10 +175,17 @@ export const UnderlineNavItem = forwardRef(
{children}
)}
- {counter && (
+
+ {loadingCounters ? (
- {loadingCounters ? : {counter}}
+
+ ) : (
+ counter !== undefined && (
+
+ {counter}
+
+ )
)}
diff --git a/src/UnderlineNav2/examples.stories.tsx b/src/UnderlineNav2/examples.stories.tsx
index 04daa680922..5fe1c48e458 100644
--- a/src/UnderlineNav2/examples.stories.tsx
+++ b/src/UnderlineNav2/examples.stories.tsx
@@ -72,10 +72,10 @@ export const withCounterLabels = () => {
)
}
-const items: {navigation: string; icon: React.FC; counter?: number}[] = [
+const items: {navigation: string; icon: React.FC; counter?: number | string}[] = [
{navigation: 'Code', icon: CodeIcon},
- {navigation: 'Issues', icon: IssueOpenedIcon, counter: 120},
- {navigation: 'Pull Requests', icon: GitPullRequestIcon, counter: 13},
+ {navigation: 'Issues', icon: IssueOpenedIcon, counter: 0},
+ {navigation: 'Pull Requests', icon: GitPullRequestIcon, counter: '12K'},
{navigation: 'Discussions', icon: CommentDiscussionIcon, counter: 5},
{navigation: 'Actions', icon: PlayIcon, counter: 4},
{navigation: 'Projects', icon: ProjectIcon, counter: 9},
diff --git a/src/UnderlineNav2/styles.ts b/src/UnderlineNav2/styles.ts
index 6f3510a24f0..8369626804b 100644
--- a/src/UnderlineNav2/styles.ts
+++ b/src/UnderlineNav2/styles.ts
@@ -2,6 +2,13 @@ import {Theme} from '../ThemeProvider'
import {BetterSystemStyleObject} from '../sx'
import {UnderlineNavProps} from './UnderlineNav'
+// The gap between the list items. It is a constant because the gap is used to calculate the possible number of items that can fit in the container.
+export const GAP = 8
+
+const focusRing = (theme?: Theme): BetterSystemStyleObject => ({
+ boxShadow: `inset 0 0 0 2px ${theme?.colors.accent.fg}`
+})
+
export const iconWrapStyles = {
alignItems: 'center',
display: 'inline-flex',
@@ -32,22 +39,24 @@ export const counterStyles = {
export const getNavStyles = (theme?: Theme, props?: Partial>) => ({
display: 'flex',
- paddingX: 2,
justifyContent: props?.align === 'right' ? 'flex-end' : 'flex-start',
borderBottom: '1px solid',
borderBottomColor: `${theme?.colors.border.muted}`,
align: 'row',
alignItems: 'center',
- position: 'relative'
+ position: 'relative',
+ paddingX: 3
})
export const ulStyles = {
display: 'flex',
listStyle: 'none',
- padding: '0',
+ paddingY: 0,
+ paddingX: 0,
margin: '0',
marginBottom: '-1px',
- alignItems: 'center'
+ alignItems: 'center',
+ gap: `${GAP}px`
}
export const getDividerStyle = (theme?: Theme) => ({
@@ -82,6 +91,8 @@ export const btnWrapperStyles = (
bottom: 0,
left: direction === 'left' ? 0 : 'auto',
right: direction === 'right' ? 0 : 'auto',
+ // Min touch target size
+ width: '44px',
alignItems: 'center',
background: show
? `linear-gradient(to ${direction} ,#fff0, ${theme?.colors.canvas.default} 14px, ${theme?.colors.canvas.default} 100%)`
@@ -90,33 +101,27 @@ export const btnWrapperStyles = (
display: `${display}`
})
-export const getArrowBtnStyles = (theme?: Theme, direction = 'left') => ({
- fontWeight: 'normal',
+export const getArrowBtnStyles = (theme?: Theme) => ({
boxShadow: 'none',
- margin: 0,
border: 0,
- borderRadius: 2,
- paddingX: '14px',
- paddingY: 0,
background: 'transparent',
- height: '60%',
-
+ width: '100%',
+ height: '100%',
+ // to reset the hover styles of the button
'&:hover:not([disabled]), &:focus-visible': {
- background: theme?.colors.canvas.default
+ background: 'transparent'
},
+ // To reset global focus styles because it doesn't support safari right now.
'&:focus:not(:disabled)': {
outline: 0,
- boxShadow: `inset 0 0 0 2px ${theme?.colors.accent.fg}`,
- background: `linear-gradient(to ${direction} ,#fff0, ${theme?.colors.canvas.default} 14px, ${theme?.colors.canvas.default} 100%)`
+ ...focusRing(theme)
},
// where focus-visible is supported, remove the focus box-shadow
'&:not(:focus-visible)': {
- boxShadow: 'none',
- background: `linear-gradient(to ${direction} ,#fff0, ${theme?.colors.canvas.default} 14px, ${theme?.colors.canvas.default} 100%)`
+ boxShadow: 'none'
},
'&:focus-visible:not(:disabled)': {
- boxShadow: `inset 0 0 0 2px ${theme?.colors.accent.fg}`,
- background: `linear-gradient(to ${direction} ,#fff0, ${theme?.colors.canvas.default} 14px, ${theme?.colors.canvas.default} 100%)`
+ ...focusRing(theme)
}
})
@@ -131,7 +136,6 @@ export const getLinkStyles = (
color: 'fg.default',
textAlign: 'center',
textDecoration: 'none',
- paddingX: 1,
...(props?.variant === 'small' ? smallVariantLinkStyles : defaultVariantLinkStyles),
'@media (hover:hover)': {
'&:hover > div[data-component="wrapper"] ': {
@@ -142,7 +146,7 @@ export const getLinkStyles = (
'&:focus': {
outline: 0,
'& > div[data-component="wrapper"]': {
- boxShadow: `inset 0 0 0 2px ${theme?.colors.accent.fg}`
+ ...focusRing(theme)
},
// where focus-visible is supported, remove the focus box-shadow
'&:not(:focus-visible) > div[data-component="wrapper"]': {
@@ -150,7 +154,7 @@ export const getLinkStyles = (
}
},
'&:focus-visible > div[data-component="wrapper"]': {
- boxShadow: `inset 0 0 0 2px ${theme?.colors.accent.fg}`
+ ...focusRing(theme)
},
// renders a visibly hidden "copy" of the label in bold, reserving box space for when label becomes bold on selected
'& span[data-content]::before': {
@@ -166,7 +170,7 @@ export const getLinkStyles = (
position: 'absolute',
right: '50%',
bottom: 0,
- width: `calc(100% - 8px)`,
+ width: '100%',
height: 2,
content: '""',
bg: selectedLink === ref ? theme?.colors.primer.border.active : 'transparent',
@@ -200,5 +204,7 @@ export const menuItemStyles = {
// This is needed to hide the selected check icon on the menu item. https://github.com/primer/react/blob/main/src/ActionList/Selection.tsx#L32
'& > span': {
display: 'none'
- }
+ },
+ // To reset the style when the menu items are rendered as react router links
+ textDecoration: 'none'
}