diff --git a/packages/leva/src/hooks/useToggle.ts b/packages/leva/src/hooks/useToggle.ts index b5676b03..b9578626 100644 --- a/packages/leva/src/hooks/useToggle.ts +++ b/packages/leva/src/hooks/useToggle.ts @@ -126,5 +126,67 @@ export function useToggle(toggled: boolean) { } }, [toggled]) + // Watch for content size changes when panel is expanded + useEffect(() => { + if (!toggled || !contentRef.current || !wrapperRef.current) return + + const wrapper = wrapperRef.current + let rafId: number | null = null + let currentTransitionHandler: (() => void) | null = null + + const resizeObserver = new ResizeObserver(() => { + const content = contentRef.current + if (!content) return + + // Cancel any pending animation + if (rafId !== null) { + cancelAnimationFrame(rafId) + rafId = null + } + + // Remove any existing transition handler + if (currentTransitionHandler) { + wrapper.removeEventListener('transitionend', currentTransitionHandler) + currentTransitionHandler = null + } + + // Get the current and target heights + const currentHeight = wrapper.getBoundingClientRect().height + const targetHeight = content.getBoundingClientRect().height + + // Only update if there's a meaningful difference + if (Math.abs(currentHeight - targetHeight) > 1) { + // Set explicit height to enable transition + wrapper.style.height = currentHeight + 'px' + + // Use requestAnimationFrame to ensure the height is set before changing it + rafId = requestAnimationFrame(() => { + rafId = null + wrapper.style.height = targetHeight + 'px' + + // Remove fixed height after transition completes + const handleTransitionEnd = () => { + wrapper.style.removeProperty('height') + currentTransitionHandler = null + } + currentTransitionHandler = handleTransitionEnd + wrapper.addEventListener('transitionend', handleTransitionEnd, { once: true }) + }) + } + }) + + resizeObserver.observe(contentRef.current) + + return () => { + resizeObserver.disconnect() + if (rafId !== null) { + cancelAnimationFrame(rafId) + } + if (currentTransitionHandler) { + wrapper.removeEventListener('transitionend', currentTransitionHandler) + } + } + }, [toggled]) + return { wrapperRef, contentRef } } diff --git a/packages/leva/stories/input-options.stories.tsx b/packages/leva/stories/input-options.stories.tsx index d3f69982..fc4d2f40 100644 --- a/packages/leva/stories/input-options.stories.tsx +++ b/packages/leva/stories/input-options.stories.tsx @@ -62,6 +62,51 @@ export const Render = () => { ) } +export const ConditionalRenderingPanelHeight = () => { + const values = useControls({ + showBasicFields: { value: true, label: 'Show Basic Fields' }, + name: { value: 'John Doe', render: (get) => get('showBasicFields') }, + age: { value: 25, render: (get) => get('showBasicFields') }, + + showAdvanced: { value: false, label: 'Show Advanced Settings' }, + advancedSettings: folder( + { + apiEndpoint: { value: 'https://api.example.com', render: (get) => get('advancedSettings.enableAPI') }, + enableAPI: true, + timeout: { value: 5000, min: 1000, max: 30000 }, + retries: { value: 3, min: 0, max: 10 }, + }, + { render: (get) => get('showAdvanced') } + ), + + showDebug: { value: false, label: 'Show Debug Options' }, + debugOptions: folder( + { + verbose: false, + logLevel: { value: 'info', options: ['debug', 'info', 'warn', 'error'] }, + showTimestamps: true, + colorOutput: { value: true, render: (get) => get('debugOptions.verbose') }, + }, + { render: (get) => get('showDebug') } + ), + }) + + return ( +
+

Panel Height Auto-Adjusts Demo

+

+ Toggle the checkboxes to show/hide different sections. + The panel height will smoothly animate to accommodate the content. +

+
+        {JSON.stringify(values, null, 2)}
+      
+
+ ) +} + +ConditionalRenderingPanelHeight.storyName = 'Conditional Rendering - Panel Height Fix' + export const Optional = () => { const values = useControls({ color: { value: '#f00', optional: true },