Skip to content

Commit

Permalink
Allow up to 4 levels of nesting for the NavList (#3343)
Browse files Browse the repository at this point in the history
* allows up to 4 levels of nesting for the NavList

* adds changeset

* Update src/NavList/NavList.tsx

Co-authored-by: Cole Bemis <[email protected]>

* updates snapshots

---------

Co-authored-by: Cole Bemis <[email protected]>
  • Loading branch information
mperrotti and colebemis authored Jun 6, 2023
1 parent a6b4169 commit 786013e
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 25 deletions.
5 changes: 5 additions & 0 deletions .changeset/little-hairs-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Allows up to 4 levels of nesting in the NavList component.
30 changes: 30 additions & 0 deletions docs/content/NavList.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,36 @@ If a `NavList.Item` contains a `NavList.SubNav`, the `NavList.Item` will render

</Note>

### With nested sub-items

```jsx live drafts
<NavList>
<NavList.Item href="/branches">Branches</NavList.Item>
<NavList.Item href="/tags">Tags</NavList.Item>
<NavList.Item>
Actions
<NavList.SubNav>
<NavList.Item href="/actions" aria-current="page">
General
</NavList.Item>
<NavList.Item href="/actions/runners">
Runners
<NavList.SubNav>
<NavList.Item href="/actions/runners/runner-1">Runner 1</NavList.Item>
<NavList.Item href="/actions/runners/runner-2">Runner 2</NavList.Item>
</NavList.SubNav>
</NavList.Item>
</NavList.SubNav>
</NavList.Item>
</NavList>
```

<Note variant="warning">

We only allow for up to 4 levels of nested subnavs. If more than 4 levels is required, it's a good sign that the navigation design needs to be rethought.

</Note>

### With a divider

```jsx live drafts
Expand Down
39 changes: 39 additions & 0 deletions src/NavList/NavList.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,45 @@ export const WithSubItems: Story = () => (
</PageLayout>
)

export const WithNestedSubItems: Story = () => (
<PageLayout>
<PageLayout.Pane position="start">
<NavList>
<NavList.Item href="#">Item 1</NavList.Item>
<NavList.Item href="#">
Item 2{/* NOTE: Don't nest SubNavs. For testing purposes only */}
<NavList.SubNav>
<NavList.Item href="#">
Sub item 1
<NavList.SubNav>
<NavList.Item href="#">
Sub item 1.1
<NavList.SubNav>
<NavList.Item href="#">Sub item 1.1.1</NavList.Item>
<NavList.Item href="#">
Sub item 1.1.2
<NavList.SubNav>
<NavList.Item href="#">Sub item 1.1.2.1</NavList.Item>
<NavList.Item href="#" aria-current="page">
Sub item 1.1.2.2
</NavList.Item>
</NavList.SubNav>
</NavList.Item>
</NavList.SubNav>
</NavList.Item>
<NavList.Item href="#">Sub item 1.2</NavList.Item>
</NavList.SubNav>
</NavList.Item>
<NavList.Item href="#">Sub item 2</NavList.Item>
</NavList.SubNav>
</NavList.Item>
<NavList.Item href="#">Item 3</NavList.Item>
</NavList>
</PageLayout.Pane>
<PageLayout.Content></PageLayout.Content>
</PageLayout>
)

type ReactRouterLikeLinkProps = {to: string; children: React.ReactNode}
const ReactRouterLikeLink = React.forwardRef<HTMLAnchorElement, ReactRouterLikeLinkProps>(({to, ...props}, ref) => {
// eslint-disable-next-line jsx-a11y/anchor-has-content
Expand Down
39 changes: 31 additions & 8 deletions src/NavList/NavList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,30 +249,53 @@ describe('NavList.Item with NavList.SubNav', () => {
expect(container).toMatchSnapshot()
})

it('prevents multiple levels of nested SubNavs', () => {
it('prevents more than 4 levels of nested SubNavs', () => {
const consoleSpy = jest
.spyOn(console, 'error')
// Suppress error message in test output
.mockImplementation(() => null)

const {getByRole} = render(
<NavList>
<NavList.Item>
Item
<NavList.Item href="#">Item 1</NavList.Item>
<NavList.Item href="#">
Item 2{/* NOTE: Don't nest SubNavs. For testing purposes only */}
<NavList.SubNav>
<NavList.Item>
Sub item
{/* NOTE: Don't nest SubNavs. For testing purposes only */}
<NavList.Item href="#">
Sub item 1
<NavList.SubNav>
<NavList.Item href="#">Sub sub item</NavList.Item>
<NavList.Item href="#">
Sub item 1.1
<NavList.SubNav>
<NavList.Item href="#">Sub item 1.1.1</NavList.Item>
<NavList.Item href="#">
Sub item 1.1.2
<NavList.SubNav>
<NavList.Item href="#">Sub item 1.1.2.1</NavList.Item>
<NavList.Item href="#">
Sub item 1.1.2.2
<NavList.SubNav>
<NavList.Item href="#" aria-current="page">
Sub item 1.1.2.2.1
</NavList.Item>
<NavList.Item href="#">Sub item 1.1.2.2.2</NavList.Item>
</NavList.SubNav>
</NavList.Item>
</NavList.SubNav>
</NavList.Item>
</NavList.SubNav>
</NavList.Item>
<NavList.Item href="#">Sub item 1.2</NavList.Item>
</NavList.SubNav>
</NavList.Item>
<NavList.Item href="#">Sub item 2</NavList.Item>
</NavList.SubNav>
</NavList.Item>
<NavList.Item href="#">Item 3</NavList.Item>
</NavList>,
)

const item = getByRole('button', {name: 'Item'})
const item = getByRole('button', {name: 'Item 2'})
fireEvent.click(item)

expect(consoleSpy).toHaveBeenCalled()
Expand Down
30 changes: 17 additions & 13 deletions src/NavList/NavList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ import {defaultSxProp} from '../utils/defaultSxProp'
import {useId} from '../hooks/useId'
import useIsomorphicLayoutEffect from '../utils/useIsomorphicLayoutEffect'

const getSubnavStyles = (depth: number) => {
return {
paddingLeft: depth > 0 ? depth + 2 : null, // Indent sub-items
fontSize: depth > 0 ? 0 : null, // Reduce font size of sub-items
fontWeight: depth > 0 ? 'normal' : null, // Sub-items don't get bolded
}
}

// ----------------------------------------------------------------------------
// NavList

Expand Down Expand Up @@ -57,9 +65,9 @@ const Item = React.forwardRef<HTMLAnchorElement, NavListItemProps>(
)

// Render ItemWithSubNav if SubNav is present
if (subNav && isValidElement(subNav) && depth < 1) {
if (subNav && isValidElement(subNav)) {
return (
<ItemWithSubNav subNav={subNav} sx={sxProp}>
<ItemWithSubNav subNav={subNav} depth={depth} sx={sxProp}>
{childrenWithoutSubNav}
</ItemWithSubNav>
)
Expand All @@ -70,14 +78,7 @@ const Item = React.forwardRef<HTMLAnchorElement, NavListItemProps>(
ref={ref}
aria-current={ariaCurrent}
active={Boolean(ariaCurrent) && ariaCurrent !== 'false'}
sx={merge<SxProp['sx']>(
{
paddingLeft: depth > 0 ? 5 : null, // Indent sub-items
fontSize: depth > 0 ? 0 : null, // Reduce font size of sub-items
fontWeight: depth > 0 ? 'normal' : null, // Sub-items don't get bolded
},
sxProp,
)}
sx={merge<SxProp['sx']>(getSubnavStyles(depth), sxProp)}
{...props}
>
{children}
Expand All @@ -94,6 +95,7 @@ Item.displayName = 'NavList.Item'
type ItemWithSubNavProps = {
children: React.ReactNode
subNav: React.ReactNode
depth: number
} & SxProp

const ItemWithSubNavContext = React.createContext<{buttonId: string; subNavId: string; isOpen: boolean}>({
Expand All @@ -104,7 +106,7 @@ const ItemWithSubNavContext = React.createContext<{buttonId: string; subNavId: s

// TODO: ref prop
// TODO: Animate open/close transition
function ItemWithSubNav({children, subNav, sx: sxProp = defaultSxProp}: ItemWithSubNavProps) {
function ItemWithSubNav({children, subNav, depth, sx: sxProp = defaultSxProp}: ItemWithSubNavProps) {
const buttonId = useId()
const subNavId = useId()
const [isOpen, setIsOpen] = React.useState(false)
Expand Down Expand Up @@ -135,6 +137,7 @@ function ItemWithSubNav({children, subNav, sx: sxProp = defaultSxProp}: ItemWith
onClick={() => setIsOpen(open => !open)}
sx={merge<SxProp['sx']>(
{
...getSubnavStyles(depth),
fontWeight: containsCurrentItem ? 'bold' : null, // Parent item is bold if any of it's sub-items are current
},
sxProp,
Expand Down Expand Up @@ -178,9 +181,10 @@ const SubNav = ({children, sx: sxProp = defaultSxProp}: NavListSubNavProps) => {
console.error('NavList.SubNav must be a child of a NavList.Item')
}

if (depth > 0) {
if (depth > 3) {
// TODO: write a more informative error message that directs people to rethink their IA
// eslint-disable-next-line no-console
console.error('NavList.SubNav only supports one level of nesting')
console.error('NavList.SubNav only supports four levels of nesting')
return null
}

Expand Down
6 changes: 2 additions & 4 deletions src/NavList/__snapshots__/NavList.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,6 @@ exports[`NavList.Item with NavList.SubNav does not have active styles if SubNav
display: flex;
padding-left: 8px;
padding-right: 8px;
font-size: 14px;
padding-top: 6px;
padding-bottom: 6px;
line-height: 20px;
Expand Down Expand Up @@ -1079,7 +1078,7 @@ exports[`NavList.Item with NavList.SubNav does not have active styles if SubNav
color: #0969da;
-webkit-text-decoration: none;
text-decoration: none;
padding-left: 32px;
padding-left: 16px;
padding-right: 8px;
padding-top: 6px;
padding-bottom: 6px;
Expand Down Expand Up @@ -1480,7 +1479,6 @@ exports[`NavList.Item with NavList.SubNav has active styles if SubNav contains t
display: flex;
padding-left: 8px;
padding-right: 8px;
font-size: 14px;
padding-top: 6px;
padding-bottom: 6px;
line-height: 20px;
Expand Down Expand Up @@ -1560,7 +1558,7 @@ exports[`NavList.Item with NavList.SubNav has active styles if SubNav contains t
color: #0969da;
-webkit-text-decoration: none;
text-decoration: none;
padding-left: 32px;
padding-left: 16px;
padding-right: 8px;
padding-top: 6px;
padding-bottom: 6px;
Expand Down

0 comments on commit 786013e

Please sign in to comment.