Skip to content

Commit 829c417

Browse files
authoredSep 8, 2024··
fix <details /> on mobile (#3206)
* more * more * more * polish * Apply suggestions from code review
1 parent 8867388 commit 829c417

File tree

10 files changed

+117
-111
lines changed

10 files changed

+117
-111
lines changed
 

‎.changeset/forty-bats-design.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'nextra-theme-docs': patch
3+
---
4+
5+
- fix overflow when clicking on `<details>` with open state
6+
7+
- fix animation on mobile when clicking on `<details>` with open state

‎docs/pages/docs/guide/advanced/playground.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Z --> G
4646
const initialRender = useRef(false)
4747

4848
useEffect(() => {
49-
if (!initialRender.current && spanRef.current) {
49+
if (!initialRender.current) {
5050
initialRender.current = true
5151
spanRef.current.textContent = rawMdx
5252
}

‎docs/pages/docs/guide/syntax-highlighting.mdx

+25-36
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { OptionTable } from 'components/table'
22
import { Callout } from 'nextra/components'
3+
import { useEffect, useRef } from 'react'
34

45
# Syntax Highlighting
56

@@ -195,56 +196,44 @@ Since syntax highlighting is done at build time, you can’t use dynamic content
195196
in your code blocks. However, since MDX is very powerful there is a workaround
196197
via client JS. For example:
197198

198-
import { useEffect, useRef } from 'react'
199-
200199
export function DynamicCode({ children }) {
201-
const ref = useRef()
200+
const ref = useRef(null)
202201
const tokenRef = useRef()
203202
// Find the corresponding token from the DOM
204203
useEffect(() => {
205-
if (ref.current) {
206-
const token = [...ref.current.querySelectorAll('code span')].find(
207-
el => el.innerText === '1'
208-
)
209-
tokenRef.current = token
210-
}
204+
tokenRef.current = [
205+
...ref.current.querySelectorAll('code > span > span')
206+
].find(el => el.textContent === '1')
211207
}, [])
212208
return (
213209
<>
214-
<div ref={ref} style={{ marginTop: '1.5rem' }}>
210+
<div ref={ref} className="mt-6">
215211
{children}
216212
</div>
217-
<a
218-
onClick={() => {
219-
tokenRef.current.innerText =
220-
(parseInt(tokenRef.current.innerText) || 0) + 1
221-
}}
222-
style={{
223-
cursor: 'pointer',
224-
userSelect: 'none'
225-
}}
226-
>
227-
Increase the number
228-
</a>
229-
<a
230-
onClick={() => {
231-
tokenRef.current.innerText = '1 + 1'
232-
}}
233-
style={{
234-
marginLeft: '.5rem',
235-
cursor: 'pointer',
236-
userSelect: 'none'
237-
}}
238-
>
239-
Change to `1 + 1`
240-
</a>
213+
<div className="flex _text-primary-600 _underline _decoration-from-font [text-underline-position:from-font] mt-3 gap-3 justify-center">
214+
<button
215+
onClick={() => {
216+
const prev = tokenRef.current.textContent
217+
tokenRef.current.textContent = +prev + 1
218+
}}
219+
>
220+
Increase the number
221+
</button>
222+
<button
223+
onClick={() => {
224+
tokenRef.current.textContent = '1 + 1'
225+
}}
226+
>
227+
Change to `1 + 1`
228+
</button>
229+
</div>
241230
</>
242231
)
243232
}
244233

245234
<DynamicCode>
246-
```js copy=false filename="dynamic_code.js"
247-
function hello () {
235+
```js copy=false filename="dynamic-code.js"
236+
function hello() {
248237
const x = 2 + 3
249238
console.log(1)
250239
}

‎packages/nextra-theme-docs/css/styles.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -192,5 +192,5 @@ body,
192192
@apply _z-20 _rounded-xl _py-2.5 _shadow-xl;
193193
@apply contrast-more:_border contrast-more:_border-gray-900 contrast-more:dark:_border-gray-50;
194194
@apply _backdrop-blur-lg _bg-[rgb(var(--nextra-bg),.8)];
195-
@apply _transition-opacity data-[closed]:_opacity-0 data-[open]:_opacity-100;
195+
@apply motion-reduce:_transition-none _transition-opacity data-[closed]:_opacity-0 data-[open]:_opacity-100;
196196
}

‎packages/nextra-theme-docs/src/components/collapse.tsx

+28-33
Original file line numberDiff line numberDiff line change
@@ -4,55 +4,56 @@ import { useEffect, useRef } from 'react'
44

55
export function Collapse({
66
children,
7-
className,
87
isOpen,
9-
horizontal = false
8+
horizontal = false,
9+
openDuration = 500,
10+
closeDuration = 300
1011
}: {
1112
children: ReactNode
12-
className?: string
1313
isOpen: boolean
1414
horizontal?: boolean
15+
openDuration?: number
16+
closeDuration?: number
1517
}): ReactElement {
16-
const containerRef = useRef<HTMLDivElement>(null)
17-
const innerRef = useRef<HTMLDivElement>(null)
18-
const animationRef = useRef(0)
18+
const containerRef = useRef<HTMLDivElement>(null!)
1919
const initialOpen = useRef(isOpen)
20+
const animationRef = useRef(0)
2021
const initialRender = useRef(true)
21-
2222
useEffect(() => {
23-
const container = containerRef.current
24-
const inner = innerRef.current
2523
const animation = animationRef.current
24+
const container = containerRef.current
2625
if (animation) {
2726
clearTimeout(animation)
27+
animationRef.current = 0
2828
}
29-
if (initialRender.current || !container || !inner) return
3029

31-
container.classList.toggle('_duration-500', !isOpen)
32-
container.classList.toggle('_duration-300', isOpen)
30+
if (initialRender.current) {
31+
return
32+
}
33+
const child = container.children[0] as HTMLDivElement
3334

3435
if (horizontal) {
3536
// save initial width to avoid word wrapping when container width will be changed
36-
inner.style.width = `${inner.clientWidth}px`
37-
container.style.width = `${inner.clientWidth}px`
37+
child.style.width = `${child.clientWidth}px`
38+
container.style.width = `${child.clientWidth}px`
3839
} else {
39-
container.style.height = `${inner.clientHeight}px`
40+
container.style.height = `${child.clientHeight}px`
4041
}
4142
if (isOpen) {
4243
animationRef.current = window.setTimeout(() => {
43-
// should be style property in kebab-case, not css class name
44+
// should be style property in kebab-case, not CSS class name
4445
container.style.removeProperty('height')
45-
}, 300)
46+
}, openDuration)
4647
} else {
4748
setTimeout(() => {
4849
if (horizontal) {
49-
container.style.width = '0px'
50+
container.style.width = '0'
5051
} else {
51-
container.style.height = '0px'
52+
container.style.height = '0'
5253
}
53-
}, 0)
54+
})
5455
}
55-
}, [horizontal, isOpen])
56+
}, [horizontal, isOpen, openDuration])
5657

5758
useEffect(() => {
5859
initialRender.current = false
@@ -63,20 +64,14 @@ export function Collapse({
6364
ref={containerRef}
6465
className={cn(
6566
'_transform-gpu _transition-all _ease-in-out motion-reduce:_transition-none',
66-
!isOpen && '_overflow-hidden'
67+
isOpen ? '_opacity-100' : ['_opacity-0', '_overflow-hidden']
6768
)}
68-
style={initialOpen.current || horizontal ? undefined : { height: 0 }}
69+
style={{
70+
...(initialOpen.current || horizontal ? undefined : { height: 0 }),
71+
transitionDuration: (isOpen ? openDuration : closeDuration) + 'ms'
72+
}}
6973
>
70-
<div
71-
ref={innerRef}
72-
className={cn(
73-
'_transition-opacity _duration-500 _ease-in-out motion-reduce:_transition-none',
74-
isOpen ? '_opacity-100' : '_opacity-0',
75-
className
76-
)}
77-
>
78-
{children}
79-
</div>
74+
<div>{children}</div>
8075
</div>
8176
)
8277
}

‎packages/nextra-theme-docs/src/components/navbar.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ function NavbarMenu({
5959
<MenuItems
6060
transition
6161
className={cn(
62+
'motion-reduce:_transition-none',
6263
'nextra-scrollbar _transition-opacity data-[closed]:_opacity-0 data-[open]:_opacity-100',
6364
'_border _border-black/5 dark:_border-white/20',
6465
'_backdrop-blur-lg _bg-[rgb(var(--nextra-bg),.8)]',

‎packages/nextra-theme-docs/src/components/search.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export function Search({
8585
'max-sm:_hidden'
8686
)}
8787
>
88-
{navigator.userAgent.includes('Macintosh') ? (
88+
{navigator.userAgent.includes('Mac') ? (
8989
<>
9090
<span className="_text-xs"></span>K
9191
</>

‎packages/nextra-theme-docs/src/components/select.tsx

+25-25
Original file line numberDiff line numberDiff line change
@@ -40,32 +40,32 @@ export function Select({
4040
)}
4141
>
4242
{selected.name}
43-
<ListboxOptions
44-
transition
45-
anchor={{ to: 'top start', gap: 10 }}
46-
className="_transition-opacity data-[closed]:_opacity-0 data-[open]:_opacity-100 _min-w-[--button-width] _z-20 _max-h-64 _rounded-md _border _border-black/5 _backdrop-blur-lg _bg-[rgb(var(--nextra-bg),.8)] _py-1 _text-sm _shadow-lg dark:_border-white/20"
47-
>
48-
{options.map(option => (
49-
<ListboxOption
50-
key={option.key}
51-
value={option}
52-
className={cn(
53-
'data-[focus]:_bg-primary-50 data-[focus]:_text-primary-600 data-[focus]:dark:_bg-primary-500/10',
54-
'_text-gray-800 dark:_text-gray-100',
55-
'_relative _cursor-pointer _whitespace-nowrap _py-1.5',
56-
'_transition-colors ltr:_pl-3 ltr:_pr-9 rtl:_pr-3 rtl:_pl-9'
57-
)}
58-
>
59-
{option.name}
60-
{option.key === selected.key && (
61-
<span className="_absolute _inset-y-0 _flex _items-center ltr:_right-3 rtl:_left-3">
62-
<CheckIcon />
63-
</span>
64-
)}
65-
</ListboxOption>
66-
))}
67-
</ListboxOptions>
6843
</ListboxButton>
44+
<ListboxOptions
45+
as="ul"
46+
transition
47+
anchor={{ to: 'top start', gap: 10 }}
48+
className="motion-reduce:_transition-none _transition-opacity data-[closed]:_opacity-0 data-[open]:_opacity-100 _min-w-[--button-width] _z-20 _max-h-64 _rounded-md _border _border-black/5 _backdrop-blur-lg _bg-[rgb(var(--nextra-bg),.8)] _py-1 _text-sm _shadow-lg dark:_border-white/20"
49+
>
50+
{options.map(option => (
51+
<ListboxOption
52+
key={option.key}
53+
value={option}
54+
as="li"
55+
className={cn(
56+
'data-[focus]:_bg-primary-50 data-[focus]:_text-primary-600 data-[focus]:dark:_bg-primary-500/10',
57+
'_text-gray-800 dark:_text-gray-100',
58+
'_cursor-pointer _whitespace-nowrap _py-1.5 _px-3',
59+
'_transition-colors',
60+
option.key === selected.key &&
61+
'_flex _items-center _justify-between _gap-3'
62+
)}
63+
>
64+
{option.name}
65+
{option.key === selected.key && <CheckIcon />}
66+
</ListboxOption>
67+
))}
68+
</ListboxOptions>
6969
</Listbox>
7070
)
7171
}

‎packages/nextra-theme-docs/src/components/sidebar.tsx

+9-9
Original file line numberDiff line numberDiff line change
@@ -184,21 +184,21 @@ function FolderImpl({ item, anchors, onFocus }: FolderProps): ReactElement {
184184
className={cn(
185185
'_shrink-0',
186186
'_rounded-sm _p-0.5 hover:_bg-gray-800/5 dark:hover:_bg-gray-100/5',
187-
'*:_origin-center *:_transition-transform *:rtl:_-rotate-180',
187+
'motion-reduce:*:_transition-none *:_origin-center *:_transition-transform *:rtl:_-rotate-180',
188188
open && '*:ltr:_rotate-90 *:rtl:_rotate-[-270deg]'
189189
)}
190190
/>
191191
</ComponentToUse>
192-
<Collapse className="_pt-1" isOpen={open}>
193-
{Array.isArray(item.children) && (
192+
{Array.isArray(item.children) && (
193+
<Collapse isOpen={open}>
194194
<Menu
195-
className={cn(classes.border, 'ltr:_ml-3 rtl:_mr-3')}
195+
className={cn(classes.border, '_pt-1 ltr:_ml-3 rtl:_mr-3')}
196196
directories={item.children}
197197
base={item.route}
198198
anchors={anchors}
199199
/>
200-
)}
201-
</Collapse>
200+
</Collapse>
201+
)}
202202
</li>
203203
)
204204
}
@@ -352,12 +352,12 @@ export function Sidebar({
352352
const [showToggleAnimation, setToggleAnimation] = useState(false)
353353

354354
const anchors = useMemo(() => toc.filter(v => v.depth === 2), [toc])
355-
const sidebarRef = useRef<HTMLDivElement>(null)
356-
const containerRef = useRef<HTMLDivElement>(null)
355+
const sidebarRef = useRef<HTMLDivElement>(null!)
356+
const containerRef = useRef<HTMLDivElement>(null!)
357357
const mounted = useMounted()
358358

359359
useEffect(() => {
360-
const activeElement = sidebarRef.current?.querySelector('li.active')
360+
const activeElement = sidebarRef.current.querySelector('li.active')
361361

362362
if (activeElement && (window.innerWidth > 767 || menu)) {
363363
const scroll = () => {

‎packages/nextra-theme-docs/src/mdx-components.tsx

+19-5
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,25 @@ function Details({
100100
className,
101101
...props
102102
}: ComponentProps<'details'>): ReactElement {
103-
const [isOpen, setIsOpen] = useState(!!open)
103+
const [isOpen, setIsOpen] = useState(() => !!open)
104104
// To animate the close animation we have to delay the DOM node state here.
105105
const [delayedOpenState, setDelayedOpenState] = useState(isOpen)
106+
const animationRef = useRef(0)
106107

107108
useEffect(() => {
109+
const animation = animationRef.current
110+
if (animation) {
111+
clearTimeout(animation)
112+
animationRef.current = 0
113+
}
108114
if (!isOpen) {
109-
const timeout = setTimeout(() => setDelayedOpenState(isOpen), 500)
110-
return () => clearTimeout(timeout)
115+
animationRef.current = window.setTimeout(
116+
() => setDelayedOpenState(isOpen),
117+
300
118+
)
119+
return () => {
120+
clearTimeout(animationRef.current)
121+
}
111122
}
112123
setDelayedOpenState(true)
113124
}, [isOpen])
@@ -148,10 +159,13 @@ function Details({
148159
<details
149160
className={cn(
150161
'[&:not(:first-child)]:_mt-4 _rounded _border _border-gray-200 _bg-white _p-2 _shadow-sm dark:_border-neutral-800 dark:_bg-neutral-900',
162+
'_overflow-hidden',
151163
className
152164
)}
153165
{...props}
154-
open={delayedOpenState}
166+
// `isOpen ||` fix issue on mobile devices while clicking on details, open attribute is still
167+
// false, and we can't calculate child.clientHeight
168+
open={isOpen || delayedOpenState}
155169
data-expanded={isOpen ? '' : undefined}
156170
>
157171
{summaryElement}
@@ -180,7 +194,7 @@ function Summary({
180194
<ArrowRightIcon
181195
className={cn(
182196
'_order-first', // if prettier formats `summary` it will have unexpected margin-top
183-
'_h-4 _shrink-0 _mx-1.5',
197+
'_h-4 _shrink-0 _mx-1.5 motion-reduce:_transition-none',
184198
'rtl:_rotate-180 [[data-expanded]>summary:first-child>&]:_rotate-90 _transition'
185199
)}
186200
strokeWidth="3"

0 commit comments

Comments
 (0)
Please sign in to comment.