Skip to content

Commit

Permalink
[v3] Avoid the sidebar collapse having unintended animations when `si…
Browse files Browse the repository at this point in the history
…debar.autoCollapse` is set to `true` (#3157)

* more

* more

* more

* more

* aa

* fix lint
  • Loading branch information
dimaMachina authored Aug 31, 2024
1 parent c4c4635 commit 0b4d43b
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 37 deletions.
5 changes: 5 additions & 0 deletions .changeset/fair-hounds-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'nextra-theme-docs': patch
---

Avoid the sidebar collapse having unintended animations when `sidebar.autoCollapse` is set to `true`.
2 changes: 1 addition & 1 deletion examples/swr-site/theme.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ const config: DocsThemeConfig = {
},
toc: {
extraContent: (
<img alt="placeholder cat" src="https://placekitten.com/g/300/200" />
<img alt="placeholder cat" src="https://placecats.com/300/200" />
),
float: true
}
Expand Down
2 changes: 1 addition & 1 deletion packages/nextra-theme-docs/src/components/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export function Search({
const handleChange = useCallback(
(e: ChangeEvent<HTMLInputElement> | CompositionEvent<HTMLInputElement>) => {
if (!compositionStateRef.current.compositioning) {
const { value } = e.target as HTMLInputElement
const { value } = e.currentTarget
onChangeProp(value)
setShow(Boolean(value))
compositionStateRef.current.emitted = true
Expand Down
87 changes: 52 additions & 35 deletions packages/nextra-theme-docs/src/components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import type { Heading } from 'nextra'
import { useFSRoute, useMounted } from 'nextra/hooks'
import { ArrowRightIcon, ExpandIcon } from 'nextra/icons'
import type { Item, MenuItem, PageItem } from 'nextra/normalize-pages'
import type { ReactElement } from 'react'
import type { FocusEventHandler, ReactElement } from 'react'
import {
createContext,
memo,
useCallback,
useContext,
useEffect,
useMemo,
Expand All @@ -22,11 +23,9 @@ import { LocaleSwitch } from './locale-switch'

const TreeState: Record<string, boolean> = Object.create(null)

const FocusedItemContext = createContext<null | string>(null)
const FocusedItemContext = createContext('')
FocusedItemContext.displayName = 'FocusedItem'
const OnFocusItemContext = createContext<null | ((item: string | null) => any)>(
null
)
const OnFocusItemContext = createContext<(route: string) => void>(() => {})
OnFocusItemContext.displayName = 'OnFocusItem'
const FolderLevelContext = createContext(0)
FolderLevelContext.displayName = 'FolderLevel'
Expand Down Expand Up @@ -66,16 +65,17 @@ const classes = {
type FolderProps = {
item: PageItem | MenuItem | Item
anchors: Heading[]
onFocus: FocusEventHandler
}

function FolderImpl({ item, anchors }: FolderProps): ReactElement {
function FolderImpl({ item, anchors, onFocus }: FolderProps): ReactElement {
const routeOriginal = useFSRoute()
const [route] = routeOriginal.split('#')
const active = [route, route + '/'].includes(item.route + '/')
const activeRouteInside = active || route.startsWith(item.route + '/')

const focusedRoute = useContext(FocusedItemContext)
const focusedRouteInside = !!focusedRoute?.startsWith(item.route + '/')
const focusedRouteInside = focusedRoute.startsWith(item.route + '/')
const level = useContext(FolderLevelContext)

const { setMenu } = useMenu()
Expand All @@ -95,18 +95,20 @@ function FolderImpl({ item, anchors }: FolderProps): ReactElement {
const rerender = useState({})[1]

useEffect(() => {
const updateTreeState = () => {
function updateTreeState() {
if (activeRouteInside || focusedRouteInside) {
TreeState[item.route] = true
}
}
const updateAndPruneTreeState = () => {

function updateAndPruneTreeState() {
if (activeRouteInside && focusedRouteInside) {
TreeState[item.route] = true
} else {
delete TreeState[item.route]
}
}

if (themeConfig.sidebar.autoCollapse) {
updateAndPruneTreeState()
} else {
Expand Down Expand Up @@ -145,6 +147,7 @@ function FolderImpl({ item, anchors }: FolderProps): ReactElement {
<li className={cn({ open, active })}>
<ComponentToUse
href={isLink ? item.route : undefined}
data-href={isLink ? undefined : item.route}
className={cn(
'_items-center _justify-between _gap-2',
!isLink && '_text-left _w-full',
Expand Down Expand Up @@ -173,6 +176,7 @@ function FolderImpl({ item, anchors }: FolderProps): ReactElement {
TreeState[item.route] = !open
rerender({})
}}
onFocus={onFocus}
>
{item.title}
<ArrowRightIcon
Expand All @@ -183,15 +187,15 @@ function FolderImpl({ item, anchors }: FolderProps): ReactElement {
)}
/>
</ComponentToUse>
<Collapse className="ltr:_pr-0 rtl:_pl-0 _pt-1" isOpen={open}>
{Array.isArray(item.children) ? (
<Collapse className="_pt-1" isOpen={open}>
{Array.isArray(item.children) && (
<Menu
className={cn(classes.border, 'ltr:_ml-3 rtl:_mr-3')}
directories={item.children}
base={item.route}
anchors={anchors}
/>
) : null}
)}
</Collapse>
</li>
)
Expand All @@ -218,13 +222,14 @@ function Separator({ title }: { title: string }): ReactElement {

function File({
item,
anchors
anchors,
onFocus
}: {
item: PageItem | Item
anchors: Heading[]
onFocus: FocusEventHandler
}): ReactElement {
const route = useFSRoute()
const onFocus = useContext(OnFocusItemContext)

// It is possible that the item doesn't have any route - for example an external link.
const active = item.route && [route, route + '/'].includes(item.route + '/')
Expand All @@ -244,12 +249,7 @@ function File({
onClick={() => {
setMenu(false)
}}
onFocus={() => {
onFocus?.(item.route)
}}
onBlur={() => {
onFocus?.(null)
}}
onFocus={onFocus}
>
{item.title}
</Anchor>
Expand Down Expand Up @@ -292,18 +292,39 @@ function Menu({
className,
onlyCurrentDocs
}: MenuProps): ReactElement {
const onFocus = useContext(OnFocusItemContext)

const handleFocus: FocusEventHandler = useCallback(
event => {
const route =
event.target.getAttribute('href') ||
event.target.getAttribute('data-href') ||
''
onFocus(route)
},
[onFocus]
)

return (
<ul className={cn(classes.list, className)}>
{directories.map(item =>
!onlyCurrentDocs || item.isUnderCurrentDocsTree ? (
{directories.map(item => {
if (onlyCurrentDocs && !item.isUnderCurrentDocsTree) return

const ComponentToUse =
item.type === 'menu' ||
(item.children && (item.children.length || !item.withIndexPage)) ? (
<Folder key={item.name} item={item} anchors={anchors} />
) : (
<File key={item.name} item={item} anchors={anchors} />
)
) : null
)}
(item.children && (item.children.length || !item.withIndexPage))
? Folder
: File

return (
<ComponentToUse
key={item.name}
item={item}
anchors={anchors}
onFocus={handleFocus}
/>
)
})}
</ul>
)
}
Expand All @@ -324,7 +345,7 @@ export function Sidebar({
includePlaceholder
}: SideBarProps): ReactElement {
const { menu, setMenu } = useMenu()
const [focused, setFocused] = useState<null | string>(null)
const [focused, setFocused] = useState('')
const [showSidebar, setSidebar] = useState(true)
const [showToggleAnimation, setToggleAnimation] = useState(false)

Expand Down Expand Up @@ -400,11 +421,7 @@ export function Sidebar({
</div>
)}
<FocusedItemContext.Provider value={focused}>
<OnFocusItemContext.Provider
value={item => {
setFocused(item)
}}
>
<OnFocusItemContext.Provider value={setFocused}>
<div
className={cn(
'_overflow-y-auto',
Expand Down

0 comments on commit 0b4d43b

Please sign in to comment.