Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the navigation wrapping ul component user customisable #9153

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions .changeset/real-numbers-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@keystone-6/core": major
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Technically a breaking change? ... custom Navigation in the Admin UI will render weirdly or have invalid DOM structure if not updated.

"@keystone-6/website": patch
---

Make the navigation wrapping ul component user customisable

The `NavigationContainer` component rendered it's children inside a `ul` meaning if you wanted to render anything other than an `li` you would have to do some gymnastics to make it work. This change makes it the users' responsibility to properly wrap list items in a `ul`.
111 changes: 93 additions & 18 deletions docs/pages/docs/guides/custom-admin-ui-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ For more information on the props, please see the [Navigation Props](#navigation
Next we'll want to import some components that Keystone provides to help us build our custom Navigation.

```tsx
import { NavigationContainer, NavItem, ListNavItems } from '@keystone-6/core/admin-ui/components';
import { NavigationContainer, NavItem, NavItemGroup, ListNavItems } from '@keystone-6/core/admin-ui/components';
```

The `NavigationContainer` component provides a container around your navigation links, as well as the different states of the `authenticatedItem` prop. We'll need this to:
Expand All @@ -71,7 +71,7 @@ The `NavigationContainer` component provides a container around your navigation
- Render out the hamburger menu with additional options should a user session be in progress via the `authenticatedItem` prop.

```tsx
import { NavigationContainer, NavItem, ListNavItems } from '@keystone-6/core/admin-ui/components';
import { NavigationContainer, NavItem, NavItemGroup, ListNavItems } from '@keystone-6/core/admin-ui/components';
import type { NavigationProps } from '@keystone-6/core/admin-ui/components';

export function CustomNavigation({ authenticatedItem, lists }: NavigationProps) {
Expand All @@ -91,15 +91,22 @@ For more information on the `NavigationContainer` see the [NavigationContainer](

The `ListNavItems` component takes the provided Array of `lists` and renders a list of NavItems. We'll use this component to help us easily create NavItems from Keystone lists.

{% hint kind="tip" %}
It's important to wrap all links in a `NavItemGroup` component. This is the `ul` to the `li` produced by `NavItem`.
{% /hint %}


```tsx
import { NavigationContainer, NavItem, ListNavItems } from '@keystone-6/core/admin-ui/components';
import { NavigationContainer, NavItem, NavItemGroup, ListNavItems } from '@keystone-6/core/admin-ui/components';
import type { NavigationProps } from '@keystone-6/core/admin-ui/components';

export function CustomNavigation({ authenticatedItem, lists }: NavigationProps) {
return (
<NavigationContainer authenticatedItem={authenticatedItem}>
<ListNavItems lists={lists}/>
{/* ... */}
<NavItemGroup>
<ListNavItems lists={lists}/>
{/* ... */}
</NavItemGroup>
</NavigationContainer>
)
}
Expand All @@ -120,11 +127,13 @@ import type { NavigationProps } from '@keystone-6/core/admin-ui/components';
export function CustomNavigation({ authenticatedItem, lists }: NavigationProps) {
return (
<NavigationContainer authenticatedItem={authenticatedItem}>
<NavItem href="/">Dashboard</NavItem>
<ListNavItems lists={lists}/>
<NavItem href="https://keystonejs.com/">
Keystone Docs
</NavItem>
<NavItemGroup>
<NavItem href="/">Dashboard</NavItem>
<ListNavItems lists={lists}/>
<NavItem href="https://keystonejs.com/">
Keystone Docs
</NavItem>
</NavItemGroup>
</NavigationContainer>
)
}
Expand All @@ -144,17 +153,19 @@ With all that done, your Custom Navigation component should be good to go, and y

```tsx
// admin/components/CustomNavigation.tsx
import { NavigationContainer, NavItem, ListNavItems } from '@keystone-6/core/admin-ui/components';
import { NavigationContainer, NavItem, NavItemGroup, ListNavItems } from '@keystone-6/core/admin-ui/components';
import type { NavigationProps } from '@keystone-6/core/admin-ui/components';

export function CustomNavigation({ authenticatedItem, lists }: NavigationProps) {
return (
<NavigationContainer authenticatedItem={authenticatedItem}>
<NavItem href="/">Dashboard</NavItem>
<ListNavItems lists={lists}/>
<NavItem href="https://keystonejs.com/">
Keystone Docs
</NavItem>
<NavItemGroup>
<NavItem href="/">Dashboard</NavItem>
<ListNavItems lists={lists}/>
<NavItem href="https://keystonejs.com/">
Keystone Docs
</NavItem>
</NavItemGroup>
</NavigationContainer>
)
}
Expand Down Expand Up @@ -230,7 +241,9 @@ Keystone exposes a variety of helper components to make building out your custom

- [NavigationContainer](#navigation-container)
- [ListNavItems](#list-nav-items)
- [ListNavItem](#list-nav-item)
- [NavItem](#nav-item)
- [NavItemGroup](#nav-item-group)

### NavigationContainer

Expand Down Expand Up @@ -280,7 +293,9 @@ If an `include` prop is supplied, the component will only render out lists that
```tsx
const CustomNavigation = ({ lists }) => (
<NavigationContainer>
<ListNavItems lists={lists} include={["Task"]} />
<NavItemGroup>
<ListNavItems lists={lists} include={["Task"]} />
</NavItemGroup>
</NavigationContainer>
)
```
Expand All @@ -292,13 +307,50 @@ Otherwise, all lists will be added.
```tsx
const CustomNavigation = ({ lists }) => (
<NavigationContainer>
<ListNavItems lists={lists} />
<NavItemGroup>
<ListNavItems lists={lists} />
</NavItemGroup>
</NavigationContainer>
)
```

![example of navigation without include prop values](/assets/guides/custom-admin-ui-navigation/listNavItems-without-include.png)

### ListNavItem

This component will render a single `NavItem` for the given list.

```tsx
import { ListNavItem } from '@keystone-6/core/admin-ui/components'
```

In this example we create groups for our lists.

```tsx
const listGroups = [
[
{ name: 'People', lists: ['User', 'Bio', 'Role']},
{ name: 'Posts', lists: ['Post', 'Category', 'Tag']},
]
];

const CustomNavigation = ({ lists }) => (
<NavigationContainer>
{listGroups.map(group => (
<Box key={group.name}>
<H5 paddingX="xlarge">{group.name}</H5>
<NavItemGroup>
{group.lists.map((key) => {
const list = lists.find((l) => l.key === key);
return list ? <ListNavItem key={key} list={list} /> : null;
})}
</NavItemGroup>
</Box>
))}
</NavigationContainer>
)
```
Comment on lines +319 to +352
Copy link
Contributor Author

@mikehazell mikehazell May 22, 2024

Choose a reason for hiding this comment

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

Kind of a contrived example. You could do something like this1 with the ListNavItems component too but I wanted to show that this component is also exported.

Footnotes

  1. Kind of like this. Using <ListNavItems includes={[...]} you don't have control over the order in which the lists are shown, only which lists.


### NavItem

This component is a thin styling and accessibility wrapper around the `Link` component from Next.js
Expand All @@ -323,6 +375,29 @@ type NavItemProps = {
By default the `isSelected` value will be evaluated by the condition `router.pathname === href`.
Pass in `isSelected` if you have a custom condition or would like more granular control over the "selected" state of Navigation items.

### NavItemGroup

This component is a styled unordered list `<ul>` which should be used to wrap `NavItem`, `ListNavItem` and `comp

```tsx
import { NavItemGroup } from '@keystone-6/core/admin-ui/components'
```

{% hint kind="warn" %}
Versions of `@keystone-6/core` before `1.2.0` wrapped all children of `NavigationContainer` with the `NavItemGroup` component.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've guessed what the version number will be. Also I'm not sure we need to call out that this thing has changed.

{% /hint %}

```tsx
const CustomNavigation = ({ lists }) => (
<NavigationContainer>
<NavItemGroup>
<ListNavItems lists={lists} />
</NavItemGroup>
</NavigationContainer>
)
```


## Related resources

{% related-content %}
Expand Down
32 changes: 19 additions & 13 deletions packages/core/src/admin-ui/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ export const NavItem = ({ href, children, isSelected: _isSelected }: NavItemProp
)
}

export const NavItemGroup = ({ children }: { children: ReactNode }) => (
<ul
css={{
margin: 0,
padding: 0,
li: {
listStyle: 'none',
},
}}
>
{children}
</ul>
);

const AuthenticatedItemDialog = ({ item }: { item: AuthenticatedItem | undefined }) => {
const { apiPath } = useKeystone()
const { spacing, typography } = useTheme()
Expand Down Expand Up @@ -154,17 +168,7 @@ export const NavigationContainer = ({ authenticatedItem, children }: NavigationC
>
<AuthenticatedItemDialog item={authenticatedItem} />
<nav role="navigation" aria-label="Side Navigation" css={{ marginTop: spacing.xlarge }}>
<ul
css={{
padding: 0,
margin: 0,
li: {
listStyle: 'none',
},
}}
>
{children}
</ul>
{children}
</nav>
</div>
)
Expand Down Expand Up @@ -234,8 +238,10 @@ export const Navigation = () => {

return (
<NavigationContainer authenticatedItem={authenticatedItem}>
<NavItem href="/">Dashboard</NavItem>
<ListNavItems lists={renderableLists} />
<NavItemGroup>
<NavItem href="/">Dashboard</NavItem>
<ListNavItems lists={renderableLists} />
</NavItemGroup>
</NavigationContainer>
)
}
2 changes: 1 addition & 1 deletion packages/core/src/admin-ui/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export { ErrorBoundary, ErrorContainer } from './Errors'

// ADMIN-UI CUSTOM COMPONENTS
export { Logo } from './Logo'
export { Navigation, NavigationContainer, NavItem, ListNavItems, ListNavItem } from './Navigation'
export { Navigation, NavigationContainer, NavItem, NavItemGroup, ListNavItems, ListNavItem } from './Navigation'

// co-locating the type with the admin-ui/component for a more a salient mental model.
// importing this type from @keystone-6/core/admin-ui/components is probably intuitive for a user
Expand Down