Skip to content
Draft
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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@emotion/react": "^11.10.5",
"@preconstruct/cli": "^2.3.0",
"@radix-ui/react-icons": "^1.1.1",
"@react-three/fiber": "^9.3.0",
"@size-limit/preset-big-lib": "^8.1.0",
"@stitches/react": "^1.2.8",
"@storybook/addon-docs": "10.0.2",
Expand Down Expand Up @@ -91,6 +92,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-storybook": "10.0.2",
"husky": "^8.0.3",
"jsdom": "^27.1.0",
"playwright": "^1.56.1",
"playwright-core": "^1.56.1",
"pnpm": "^7.25.0",
Expand All @@ -100,9 +102,9 @@
"react-dom": "^18.2.0",
"size-limit": "^8.1.0",
"storybook": "^10.0.2",
"three": "^0.169.0",
"tsd": "^0.25.0",
"typescript": "catalog:",
"jsdom": "^27.1.0",
"vitest": "^4.0.8"
},
"prettier": {
Expand Down
12 changes: 8 additions & 4 deletions packages/leva/src/hooks/useToggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ import { useRef, useEffect, useLayoutEffect } from 'react'
export function useToggle(toggled: boolean) {
const wrapperRef = useRef<HTMLDivElement>(null)
const contentRef = useRef<HTMLDivElement>(null)
const firstRender = useRef(true)
const prevToggledRef = useRef<boolean | null>(null)

// this should be fine for SSR since the store is set in useEffect and
// therefore the pane doesn't show on first render.
Expand All @@ -94,9 +94,11 @@ export function useToggle(toggled: boolean) {
}, [])

useEffect(() => {
// prevents first animation
if (firstRender.current) {
firstRender.current = false
// On initial render (including StrictMode's double-invocation),
// prevToggledRef.current is null, so we skip animation.
// We only animate when toggled actually changes from a previous value.
if (prevToggledRef.current === null || prevToggledRef.current === toggled) {
prevToggledRef.current = toggled
return
}

Expand All @@ -120,6 +122,8 @@ export function useToggle(toggled: boolean) {
timeout = window.setTimeout(() => (ref.style.height = '0px'), 50)
}

prevToggledRef.current = toggled

return () => {
ref.removeEventListener('transitionend', fixHeight)
clearTimeout(timeout)
Expand Down
79 changes: 77 additions & 2 deletions packages/leva/stories/bug-repros.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState } from 'react'
import React, { StrictMode, useState } from 'react'
import Reset from './components/decorator-reset'
import { Meta } from '@storybook/react'
import { Leva, LevaPanel, useControls, useCreateStore } from '../src'
import { Leva, LevaPanel, useControls, useCreateStore, folder } from '../src'

export default {
title: 'Dev/BugRepro',
Expand Down Expand Up @@ -102,3 +102,78 @@ export const ConditionalControls = () => {
}

ConditionalControls.storyName = '540 / conditional controls should work'

// repro for https://github.com/pmndrs/leva/issues/552
// Dynamic import to avoid build errors if @react-three/fiber is not installed
let Canvas: any = null
let Box: any = null

try {
const fiber = require('@react-three/fiber')
Canvas = fiber.Canvas

// Simple box component that uses leva controls
Box = ({ position }: { position: [number, number, number] }) => {
const { scale, color, wireframe, rotation } = useControls('Mesh Settings', {
scale: { value: 1, min: 0.1, max: 3, step: 0.1 },
color: '#ff6b6b',
rotation: { value: { x: 0, y: 0, z: 0 }, step: 0.01 },
wireframe: false,
})

return (
<mesh position={position} scale={scale} rotation={[rotation.x, rotation.y, rotation.z]}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={color} wireframe={wireframe} />
</mesh>
)
}
} catch (e) {
// R3F not installed, story will show error message
}

export const StrictModeWithR3FCanvas = () => {
if (!Canvas) {
return (
<div style={{ padding: 20, background: '#fff3cd', border: '2px solid #ffc107' }}>
<h3>@react-three/fiber not installed</h3>
<p>
This story requires @react-three/fiber and three to be installed as dev dependencies.
<br />
Run: <code>npm install --save-dev @react-three/fiber three</code>
</p>
</div>
)
}

return (
<StrictMode>
<div style={{ width: '100%', height: '100vh' }}>
<div
style={{
position: 'absolute',
top: 10,
left: 10,
zIndex: 1,
padding: 10,
background: '#fff3cd',
border: '2px solid #ffc107',
borderRadius: 4,
}}>
<strong>StrictMode Enabled</strong>
<p style={{ margin: '5px 0 0 0', fontSize: '12px' }}>
Controls should render correctly despite StrictMode's double-invocation.
</p>
</div>
<Canvas>
<ambientLight intensity={0.5} />
<spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} />
<pointLight position={[-10, -10, -10]} />
<Box position={[0, 0, 0]} />
</Canvas>
</div>
</StrictMode>
)
}

StrictModeWithR3FCanvas.storyName = '552 / StrictMode with R3F Canvas should render controls'
Loading