From a110c64d9b4101ea13675f939f0580cae4bb62e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 1 Jul 2023 20:36:01 +0200 Subject: [PATCH] Skip the lifecycle-related code outside of the browser env --- packages/react-resizable-panels/src/Panel.ts | 143 +-- .../react-resizable-panels/src/PanelGroup.ts | 967 +++++++++--------- .../src/PanelResizeHandle.ts | 167 +-- .../src/hooks/useWindowSplitterBehavior.ts | 7 + 4 files changed, 668 insertions(+), 616 deletions(-) diff --git a/packages/react-resizable-panels/src/Panel.ts b/packages/react-resizable-panels/src/Panel.ts index 26dd1821c..e8c76d195 100644 --- a/packages/react-resizable-panels/src/Panel.ts +++ b/packages/react-resizable-panels/src/Panel.ts @@ -1,3 +1,4 @@ +import { isBrowser } from "#is-browser"; import useIsomorphicLayoutEffect from "./hooks/useIsomorphicEffect"; import useUniqueId from "./hooks/useUniqueId"; import { @@ -81,16 +82,6 @@ function PanelWithForwardedRef({ unregisterPanel, } = context; - // Use a ref to guard against users passing inline props - const callbacksRef = useRef<{ - onCollapse: PanelOnCollapse | null; - onResize: PanelOnResize | null; - }>({ onCollapse, onResize }); - useEffect(() => { - callbacksRef.current.onCollapse = onCollapse; - callbacksRef.current.onResize = onResize; - }); - // Basic props validation if (minSize < 0 || minSize > 100) { throw Error(`Panel minSize must be between 0 and 100, but was ${minSize}`); @@ -114,66 +105,78 @@ function PanelWithForwardedRef({ const style = getPanelStyle(panelId, defaultSize); - const committedValuesRef = useRef<{ - size: number; - }>({ - size: parseSizeFromStyle(style), - }); - const panelDataRef = useRef<{ - callbacksRef: PanelCallbackRef; - collapsedSize: number; - collapsible: boolean; - defaultSize: number | null; - id: string; - maxSize: number; - minSize: number; - order: number | null; - }>({ - callbacksRef, - collapsedSize, - collapsible, - defaultSize, - id: panelId, - maxSize, - minSize, - order, - }); - useIsomorphicLayoutEffect(() => { - committedValuesRef.current.size = parseSizeFromStyle(style); - - panelDataRef.current.callbacksRef = callbacksRef; - panelDataRef.current.collapsedSize = collapsedSize; - panelDataRef.current.collapsible = collapsible; - panelDataRef.current.defaultSize = defaultSize; - panelDataRef.current.id = panelId; - panelDataRef.current.maxSize = maxSize; - panelDataRef.current.minSize = minSize; - panelDataRef.current.order = order; - }); - - useIsomorphicLayoutEffect(() => { - registerPanel(panelId, panelDataRef as PanelData); - - return () => { - unregisterPanel(panelId); - }; - }, [order, panelId, registerPanel, unregisterPanel]); - - useImperativeHandle( - forwardedRef, - () => ({ - collapse: () => collapsePanel(panelId), - expand: () => expandPanel(panelId), - getCollapsed() { - return committedValuesRef.current.size === 0; - }, - getSize() { - return committedValuesRef.current.size; - }, - resize: (percentage: number) => resizePanel(panelId, percentage), - }), - [collapsePanel, expandPanel, panelId, resizePanel] - ); + if (isBrowser) { + // Use a ref to guard against users passing inline props + const callbacksRef = useRef<{ + onCollapse: PanelOnCollapse | null; + onResize: PanelOnResize | null; + }>({ onCollapse, onResize }); + useEffect(() => { + callbacksRef.current.onCollapse = onCollapse; + callbacksRef.current.onResize = onResize; + }); + + const committedValuesRef = useRef<{ + size: number; + }>({ + size: parseSizeFromStyle(style), + }); + const panelDataRef = useRef<{ + callbacksRef: PanelCallbackRef; + collapsedSize: number; + collapsible: boolean; + defaultSize: number | null; + id: string; + maxSize: number; + minSize: number; + order: number | null; + }>({ + callbacksRef, + collapsedSize, + collapsible, + defaultSize, + id: panelId, + maxSize, + minSize, + order, + }); + useIsomorphicLayoutEffect(() => { + committedValuesRef.current.size = parseSizeFromStyle(style); + + panelDataRef.current.callbacksRef = callbacksRef; + panelDataRef.current.collapsedSize = collapsedSize; + panelDataRef.current.collapsible = collapsible; + panelDataRef.current.defaultSize = defaultSize; + panelDataRef.current.id = panelId; + panelDataRef.current.maxSize = maxSize; + panelDataRef.current.minSize = minSize; + panelDataRef.current.order = order; + }); + + useIsomorphicLayoutEffect(() => { + registerPanel(panelId, panelDataRef as PanelData); + + return () => { + unregisterPanel(panelId); + }; + }, [order, panelId, registerPanel, unregisterPanel]); + + useImperativeHandle( + forwardedRef, + () => ({ + collapse: () => collapsePanel(panelId), + expand: () => expandPanel(panelId), + getCollapsed() { + return committedValuesRef.current.size === 0; + }, + getSize() { + return committedValuesRef.current.size; + }, + resize: (percentage: number) => resizePanel(panelId, percentage), + }), + [collapsePanel, expandPanel, panelId, resizePanel] + ); + } return createElement(Type, { children, diff --git a/packages/react-resizable-panels/src/PanelGroup.ts b/packages/react-resizable-panels/src/PanelGroup.ts index 34d51012b..3f3b2dad5 100644 --- a/packages/react-resizable-panels/src/PanelGroup.ts +++ b/packages/react-resizable-panels/src/PanelGroup.ts @@ -174,571 +174,604 @@ function PanelGroupWithForwardedRef({ // 0-1 values representing the relative size of each panel. const [sizes, setSizes] = useState([]); - // Used to support imperative collapse/expand API. - const panelSizeBeforeCollapse = useRef>(new Map()); + let context: React.ContextType = null; - const prevDeltaRef = useRef(0); + if (isBrowser) { + // Used to support imperative collapse/expand API. + const panelSizeBeforeCollapse = useRef>(new Map()); - // Store committed values to avoid unnecessarily re-running memoization/effects functions. - const committedValuesRef = useRef({ - direction, - panels, - sizes, - }); - - useImperativeHandle( - forwardedRef, - () => ({ - getLayout: () => { - const { sizes } = committedValuesRef.current; - return sizes; - }, - setLayout: (sizes: number[]) => { - const total = sizes.reduce( - (accumulated, current) => accumulated + current, - 0 - ); + const prevDeltaRef = useRef(0); - assert(total === 100, "Panel sizes must add up to 100%"); + // Store committed values to avoid unnecessarily re-running memoization/effects functions. + const committedValuesRef = useRef({ + direction, + panels, + sizes, + }); - const { panels } = committedValuesRef.current; - const panelIdToLastNotifiedSizeMap = - panelIdToLastNotifiedSizeMapRef.current; - const panelsArray = panelsMapToSortedArray(panels); + useImperativeHandle( + forwardedRef, + () => ({ + getLayout: () => { + const { sizes } = committedValuesRef.current; + return sizes; + }, + setLayout: (sizes: number[]) => { + const total = sizes.reduce( + (accumulated, current) => accumulated + current, + 0 + ); - setSizes(sizes); + assert(total === 100, "Panel sizes must add up to 100%"); - callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap); - }, - }), - [] - ); + const { panels } = committedValuesRef.current; + const panelIdToLastNotifiedSizeMap = + panelIdToLastNotifiedSizeMapRef.current; + const panelsArray = panelsMapToSortedArray(panels); - useIsomorphicLayoutEffect(() => { - committedValuesRef.current.direction = direction; - committedValuesRef.current.panels = panels; - committedValuesRef.current.sizes = sizes; - }); + setSizes(sizes); - useWindowSplitterPanelGroupBehavior({ - committedValuesRef, - groupId, - panels, - setSizes, - sizes, - panelSizeBeforeCollapse, - }); + callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap); + }, + }), + [] + ); - // Notify external code when sizes have changed. - useEffect(() => { - const { onLayout } = callbacksRef.current!; - const { panels, sizes } = committedValuesRef.current; + useIsomorphicLayoutEffect(() => { + committedValuesRef.current.direction = direction; + committedValuesRef.current.panels = panels; + committedValuesRef.current.sizes = sizes; + }); - // Don't commit layout until all panels have registered and re-rendered with their actual sizes. - if (sizes.length > 0) { - if (onLayout) { - onLayout(sizes); - } + useWindowSplitterPanelGroupBehavior({ + committedValuesRef, + groupId, + panels, + setSizes, + sizes, + panelSizeBeforeCollapse, + }); - const panelIdToLastNotifiedSizeMap = - panelIdToLastNotifiedSizeMapRef.current; + // Notify external code when sizes have changed. + useEffect(() => { + const { onLayout } = callbacksRef.current!; + const { panels, sizes } = committedValuesRef.current; - // When possible, we notify before the next render so that rendering work can be batched together. - // Some cases are difficult to detect though, - // for example– panels that are conditionally rendered can affect the size of neighboring panels. - // In this case, the best we can do is notify on commit. - // The callPanelCallbacks() uses its own memoization to avoid notifying panels twice in these cases. - const panelsArray = panelsMapToSortedArray(panels); - callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap); - } - }, [sizes]); - - // Once all panels have registered themselves, - // Compute the initial sizes based on default weights. - // This assumes that panels register during initial mount (no conditional rendering)! - useIsomorphicLayoutEffect(() => { - const sizes = committedValuesRef.current.sizes; - if (sizes.length === panels.size) { - // Only compute (or restore) default sizes once per panel configuration. - return; - } + // Don't commit layout until all panels have registered and re-rendered with their actual sizes. + if (sizes.length > 0) { + if (onLayout) { + onLayout(sizes); + } - // If this panel has been configured to persist sizing information, - // default size should be restored from local storage if possible. - let defaultSizes: number[] | null = null; - if (autoSaveId) { - const panelsArray = panelsMapToSortedArray(panels); - defaultSizes = loadPanelLayout(autoSaveId, panelsArray, storage); - } + const panelIdToLastNotifiedSizeMap = + panelIdToLastNotifiedSizeMapRef.current; - if (defaultSizes != null) { - setSizes(defaultSizes); - } else { - const panelsArray = panelsMapToSortedArray(panels); + // When possible, we notify before the next render so that rendering work can be batched together. + // Some cases are difficult to detect though, + // for example– panels that are conditionally rendered can affect the size of neighboring panels. + // In this case, the best we can do is notify on commit. + // The callPanelCallbacks() uses its own memoization to avoid notifying panels twice in these cases. + const panelsArray = panelsMapToSortedArray(panels); + callPanelCallbacks(panelsArray, sizes, panelIdToLastNotifiedSizeMap); + } + }, [sizes]); + + // Once all panels have registered themselves, + // Compute the initial sizes based on default weights. + // This assumes that panels register during initial mount (no conditional rendering)! + useIsomorphicLayoutEffect(() => { + const sizes = committedValuesRef.current.sizes; + if (sizes.length === panels.size) { + // Only compute (or restore) default sizes once per panel configuration. + return; + } - let panelsWithNullDefaultSize = 0; - let totalDefaultSize = 0; - let totalMinSize = 0; + // If this panel has been configured to persist sizing information, + // default size should be restored from local storage if possible. + let defaultSizes: number[] | null = null; + if (autoSaveId) { + const panelsArray = panelsMapToSortedArray(panels); + defaultSizes = loadPanelLayout(autoSaveId, panelsArray, storage); + } - // TODO - // Implicit default size calculations below do not account for inferred min/max size values. - // e.g. if Panel A has a maxSize of 40 then Panels A and B can't both have an implicit default size of 50. - // For now, these logic edge cases are left to the user to handle via props. + if (defaultSizes != null) { + setSizes(defaultSizes); + } else { + const panelsArray = panelsMapToSortedArray(panels); - panelsArray.forEach((panel) => { - totalMinSize += panel.current.minSize; + let panelsWithNullDefaultSize = 0; + let totalDefaultSize = 0; + let totalMinSize = 0; - if (panel.current.defaultSize === null) { - panelsWithNullDefaultSize++; - } else { - totalDefaultSize += panel.current.defaultSize; - } - }); + // TODO + // Implicit default size calculations below do not account for inferred min/max size values. + // e.g. if Panel A has a maxSize of 40 then Panels A and B can't both have an implicit default size of 50. + // For now, these logic edge cases are left to the user to handle via props. - if (totalDefaultSize > 100) { - throw new Error(`Default panel sizes cannot exceed 100%`); - } else if ( - panelsArray.length > 1 && - panelsWithNullDefaultSize === 0 && - totalDefaultSize !== 100 - ) { - throw new Error(`Invalid default sizes specified for panels`); - } else if (totalMinSize > 100) { - throw new Error(`Minimum panel sizes cannot exceed 100%`); - } + panelsArray.forEach((panel) => { + totalMinSize += panel.current.minSize; - setSizes( - panelsArray.map((panel) => { if (panel.current.defaultSize === null) { - return (100 - totalDefaultSize) / panelsWithNullDefaultSize; + panelsWithNullDefaultSize++; + } else { + totalDefaultSize += panel.current.defaultSize; } + }); + + if (totalDefaultSize > 100) { + throw new Error(`Default panel sizes cannot exceed 100%`); + } else if ( + panelsArray.length > 1 && + panelsWithNullDefaultSize === 0 && + totalDefaultSize !== 100 + ) { + throw new Error(`Invalid default sizes specified for panels`); + } else if (totalMinSize > 100) { + throw new Error(`Minimum panel sizes cannot exceed 100%`); + } - return panel.current.defaultSize; - }) - ); - } - }, [autoSaveId, panels, storage]); + setSizes( + panelsArray.map((panel) => { + if (panel.current.defaultSize === null) { + return (100 - totalDefaultSize) / panelsWithNullDefaultSize; + } - useEffect(() => { - // If this panel has been configured to persist sizing information, save sizes to local storage. - if (autoSaveId) { - if (sizes.length === 0 || sizes.length !== panels.size) { - return; + return panel.current.defaultSize; + }) + ); } + }, [autoSaveId, panels, storage]); - const panelsArray = panelsMapToSortedArray(panels); + useEffect(() => { + // If this panel has been configured to persist sizing information, save sizes to local storage. + if (autoSaveId) { + if (sizes.length === 0 || sizes.length !== panels.size) { + return; + } - // Limit the frequency of localStorage updates. - if (!debounceMap[autoSaveId]) { - debounceMap[autoSaveId] = debounce(savePanelGroupLayout, 100); + const panelsArray = panelsMapToSortedArray(panels); + + // Limit the frequency of localStorage updates. + if (!debounceMap[autoSaveId]) { + debounceMap[autoSaveId] = debounce(savePanelGroupLayout, 100); + } + debounceMap[autoSaveId](autoSaveId, panelsArray, sizes, storage); } - debounceMap[autoSaveId](autoSaveId, panelsArray, sizes, storage); - } - }, [autoSaveId, panels, sizes, storage]); - - const getPanelStyle = useCallback( - (id: string, defaultSize: number | null): CSSProperties => { - const { panels } = committedValuesRef.current; - - // Before mounting, Panels will not yet have registered themselves. - // This includes server rendering. - // At this point the best we can do is render everything with the same size. - if (panels.size === 0) { - if (isDevelopment && !isBrowser && defaultSize == null) { - console.warn( - `WARNING: Panel defaultSize prop recommended to avoid layout shift after server rendering` - ); + }, [autoSaveId, panels, sizes, storage]); + + const getPanelStyle = useCallback( + (id: string, defaultSize: number | null): CSSProperties => { + const { panels } = committedValuesRef.current; + + // Before mounting, Panels will not yet have registered themselves. + // This includes server rendering. + // At this point the best we can do is render everything with the same size. + if (panels.size === 0) { + if (isDevelopment && !isBrowser && defaultSize == null) { + console.warn( + `WARNING: Panel defaultSize prop recommended to avoid layout shift after server rendering` + ); + } + + return { + flexBasis: 0, + flexGrow: defaultSize != null ? defaultSize : undefined, + flexShrink: 1, + + // Without this, Panel sizes may be unintentionally overridden by their content. + overflow: "hidden", + }; } + const flexGrow = getFlexGrow(panels, id, sizes); + return { flexBasis: 0, - flexGrow: defaultSize != null ? defaultSize : undefined, + flexGrow, flexShrink: 1, // Without this, Panel sizes may be unintentionally overridden by their content. overflow: "hidden", - }; - } - - const flexGrow = getFlexGrow(panels, id, sizes); - - return { - flexBasis: 0, - flexGrow, - flexShrink: 1, - // Without this, Panel sizes may be unintentionally overridden by their content. - overflow: "hidden", + // Disable pointer events inside of a panel during resize. + // This avoid edge cases like nested iframes. + pointerEvents: + disablePointerEventsDuringResize && activeHandleId !== null + ? "none" + : undefined, + }; + }, + [activeHandleId, disablePointerEventsDuringResize, sizes] + ); - // Disable pointer events inside of a panel during resize. - // This avoid edge cases like nested iframes. - pointerEvents: - disablePointerEventsDuringResize && activeHandleId !== null - ? "none" - : undefined, - }; - }, - [activeHandleId, disablePointerEventsDuringResize, sizes] - ); - - const registerPanel = useCallback((id: string, panelRef: PanelData) => { - setPanels((prevPanels) => { - if (prevPanels.has(id)) { - return prevPanels; - } + const registerPanel = useCallback((id: string, panelRef: PanelData) => { + setPanels((prevPanels) => { + if (prevPanels.has(id)) { + return prevPanels; + } - const nextPanels = new Map(prevPanels); - nextPanels.set(id, panelRef); + const nextPanels = new Map(prevPanels); + nextPanels.set(id, panelRef); - return nextPanels; - }); - }, []); + return nextPanels; + }); + }, []); - const registerResizeHandle = useCallback( - (handleId: string) => { - const resizeHandler = (event: ResizeEvent) => { - event.preventDefault(); + const registerResizeHandle = useCallback( + (handleId: string) => { + const resizeHandler = (event: ResizeEvent) => { + event.preventDefault(); - const { - direction, - panels, - sizes: prevSizes, - } = committedValuesRef.current; + const { + direction, + panels, + sizes: prevSizes, + } = committedValuesRef.current; - const panelsArray = panelsMapToSortedArray(panels); + const panelsArray = panelsMapToSortedArray(panels); - const [idBefore, idAfter] = getResizeHandlePanelIds( - groupId, - handleId, - panelsArray - ); - if (idBefore == null || idAfter == null) { - return; - } + const [idBefore, idAfter] = getResizeHandlePanelIds( + groupId, + handleId, + panelsArray + ); + if (idBefore == null || idAfter == null) { + return; + } - let movement = getMovement( - event, - groupId, - handleId, - panelsArray, - direction, - prevSizes, - initialDragStateRef.current - ); - if (movement === 0) { - return; - } + let movement = getMovement( + event, + groupId, + handleId, + panelsArray, + direction, + prevSizes, + initialDragStateRef.current + ); + if (movement === 0) { + return; + } - const groupElement = getPanelGroup(groupId)!; - const rect = groupElement.getBoundingClientRect(); - const isHorizontal = direction === "horizontal"; + const groupElement = getPanelGroup(groupId)!; + const rect = groupElement.getBoundingClientRect(); + const isHorizontal = direction === "horizontal"; - // Support RTL layouts - if (document.dir === "rtl" && isHorizontal) { - movement = -movement; - } + // Support RTL layouts + if (document.dir === "rtl" && isHorizontal) { + movement = -movement; + } - const size = isHorizontal ? rect.width : rect.height; - const delta = (movement / size) * 100; - - const nextSizes = adjustByDelta( - event, - panels, - idBefore, - idAfter, - delta, - prevSizes, - panelSizeBeforeCollapse.current, - initialDragStateRef.current - ); + const size = isHorizontal ? rect.width : rect.height; + const delta = (movement / size) * 100; + + const nextSizes = adjustByDelta( + event, + panels, + idBefore, + idAfter, + delta, + prevSizes, + panelSizeBeforeCollapse.current, + initialDragStateRef.current + ); - const sizesChanged = !areEqual(prevSizes, nextSizes); - - // Don't update cursor for resizes triggered by keyboard interactions. - if (isMouseEvent(event) || isTouchEvent(event)) { - // Watch for multiple subsequent deltas; this might occur for tiny cursor movements. - // In this case, Panel sizes might not change– - // but updating cursor in this scenario would cause a flicker. - if (prevDeltaRef.current != delta) { - if (!sizesChanged) { - // If the pointer has moved too far to resize the panel any further, - // update the cursor style for a visual clue. - // This mimics VS Code behavior. - - if (isHorizontal) { - setGlobalCursorStyle( - movement < 0 ? "horizontal-min" : "horizontal-max" - ); + const sizesChanged = !areEqual(prevSizes, nextSizes); + + // Don't update cursor for resizes triggered by keyboard interactions. + if (isMouseEvent(event) || isTouchEvent(event)) { + // Watch for multiple subsequent deltas; this might occur for tiny cursor movements. + // In this case, Panel sizes might not change– + // but updating cursor in this scenario would cause a flicker. + if (prevDeltaRef.current != delta) { + if (!sizesChanged) { + // If the pointer has moved too far to resize the panel any further, + // update the cursor style for a visual clue. + // This mimics VS Code behavior. + + if (isHorizontal) { + setGlobalCursorStyle( + movement < 0 ? "horizontal-min" : "horizontal-max" + ); + } else { + setGlobalCursorStyle( + movement < 0 ? "vertical-min" : "vertical-max" + ); + } } else { - setGlobalCursorStyle( - movement < 0 ? "vertical-min" : "vertical-max" - ); + // Reset the cursor style to the the normal resize cursor. + setGlobalCursorStyle(isHorizontal ? "horizontal" : "vertical"); } - } else { - // Reset the cursor style to the the normal resize cursor. - setGlobalCursorStyle(isHorizontal ? "horizontal" : "vertical"); } } - } - if (sizesChanged) { - const panelIdToLastNotifiedSizeMap = - panelIdToLastNotifiedSizeMapRef.current; + if (sizesChanged) { + const panelIdToLastNotifiedSizeMap = + panelIdToLastNotifiedSizeMapRef.current; - setSizes(nextSizes); + setSizes(nextSizes); - // If resize change handlers have been declared, this is the time to call them. - // Trigger user callbacks after updating state, so that user code can override the sizes. - callPanelCallbacks( - panelsArray, - nextSizes, - panelIdToLastNotifiedSizeMap - ); - } + // If resize change handlers have been declared, this is the time to call them. + // Trigger user callbacks after updating state, so that user code can override the sizes. + callPanelCallbacks( + panelsArray, + nextSizes, + panelIdToLastNotifiedSizeMap + ); + } - prevDeltaRef.current = delta; - }; + prevDeltaRef.current = delta; + }; - return resizeHandler; - }, - [groupId] - ); + return resizeHandler; + }, + [groupId] + ); - const unregisterPanel = useCallback((id: string) => { - setPanels((prevPanels) => { - if (!prevPanels.has(id)) { - return prevPanels; - } + const unregisterPanel = useCallback((id: string) => { + setPanels((prevPanels) => { + if (!prevPanels.has(id)) { + return prevPanels; + } - const nextPanels = new Map(prevPanels); - nextPanels.delete(id); + const nextPanels = new Map(prevPanels); + nextPanels.delete(id); - return nextPanels; - }); - }, []); + return nextPanels; + }); + }, []); - const collapsePanel = useCallback((id: string) => { - const { panels, sizes: prevSizes } = committedValuesRef.current; + const collapsePanel = useCallback((id: string) => { + const { panels, sizes: prevSizes } = committedValuesRef.current; - const panel = panels.get(id); - if (panel == null) { - return; - } + const panel = panels.get(id); + if (panel == null) { + return; + } - const { collapsedSize, collapsible } = panel.current; - if (!collapsible) { - return; - } + const { collapsedSize, collapsible } = panel.current; + if (!collapsible) { + return; + } - const panelsArray = panelsMapToSortedArray(panels); + const panelsArray = panelsMapToSortedArray(panels); - const index = panelsArray.indexOf(panel); - if (index < 0) { - return; - } + const index = panelsArray.indexOf(panel); + if (index < 0) { + return; + } - const currentSize = prevSizes[index]; - if (currentSize === collapsedSize) { - // Panel is already collapsed. - return; - } + const currentSize = prevSizes[index]; + if (currentSize === collapsedSize) { + // Panel is already collapsed. + return; + } - panelSizeBeforeCollapse.current.set(id, currentSize); + panelSizeBeforeCollapse.current.set(id, currentSize); - const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray); - if (idBefore == null || idAfter == null) { - return; - } - - const isLastPanel = index === panelsArray.length - 1; - const delta = isLastPanel ? currentSize : collapsedSize - currentSize; + const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray); + if (idBefore == null || idAfter == null) { + return; + } - const nextSizes = adjustByDelta( - null, - panels, - idBefore, - idAfter, - delta, - prevSizes, - panelSizeBeforeCollapse.current, - null - ); - if (prevSizes !== nextSizes) { - const panelIdToLastNotifiedSizeMap = - panelIdToLastNotifiedSizeMapRef.current; + const isLastPanel = index === panelsArray.length - 1; + const delta = isLastPanel ? currentSize : collapsedSize - currentSize; + + const nextSizes = adjustByDelta( + null, + panels, + idBefore, + idAfter, + delta, + prevSizes, + panelSizeBeforeCollapse.current, + null + ); + if (prevSizes !== nextSizes) { + const panelIdToLastNotifiedSizeMap = + panelIdToLastNotifiedSizeMapRef.current; - setSizes(nextSizes); + setSizes(nextSizes); - // If resize change handlers have been declared, this is the time to call them. - // Trigger user callbacks after updating state, so that user code can override the sizes. - callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap); - } - }, []); + // If resize change handlers have been declared, this is the time to call them. + // Trigger user callbacks after updating state, so that user code can override the sizes. + callPanelCallbacks( + panelsArray, + nextSizes, + panelIdToLastNotifiedSizeMap + ); + } + }, []); - const expandPanel = useCallback((id: string) => { - const { panels, sizes: prevSizes } = committedValuesRef.current; + const expandPanel = useCallback((id: string) => { + const { panels, sizes: prevSizes } = committedValuesRef.current; - const panel = panels.get(id); - if (panel == null) { - return; - } + const panel = panels.get(id); + if (panel == null) { + return; + } - const { collapsedSize, minSize } = panel.current; + const { collapsedSize, minSize } = panel.current; - const sizeBeforeCollapse = - panelSizeBeforeCollapse.current.get(id) || minSize; - if (!sizeBeforeCollapse) { - return; - } + const sizeBeforeCollapse = + panelSizeBeforeCollapse.current.get(id) || minSize; + if (!sizeBeforeCollapse) { + return; + } - const panelsArray = panelsMapToSortedArray(panels); + const panelsArray = panelsMapToSortedArray(panels); - const index = panelsArray.indexOf(panel); - if (index < 0) { - return; - } + const index = panelsArray.indexOf(panel); + if (index < 0) { + return; + } - const currentSize = prevSizes[index]; - if (currentSize !== collapsedSize) { - // Panel is already expanded. - return; - } + const currentSize = prevSizes[index]; + if (currentSize !== collapsedSize) { + // Panel is already expanded. + return; + } - const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray); - if (idBefore == null || idAfter == null) { - return; - } + const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray); + if (idBefore == null || idAfter == null) { + return; + } - const isLastPanel = index === panelsArray.length - 1; - const delta = isLastPanel - ? collapsedSize - sizeBeforeCollapse - : sizeBeforeCollapse; + const isLastPanel = index === panelsArray.length - 1; + const delta = isLastPanel + ? collapsedSize - sizeBeforeCollapse + : sizeBeforeCollapse; + + const nextSizes = adjustByDelta( + null, + panels, + idBefore, + idAfter, + delta, + prevSizes, + panelSizeBeforeCollapse.current, + null + ); + if (prevSizes !== nextSizes) { + const panelIdToLastNotifiedSizeMap = + panelIdToLastNotifiedSizeMapRef.current; - const nextSizes = adjustByDelta( - null, - panels, - idBefore, - idAfter, - delta, - prevSizes, - panelSizeBeforeCollapse.current, - null - ); - if (prevSizes !== nextSizes) { - const panelIdToLastNotifiedSizeMap = - panelIdToLastNotifiedSizeMapRef.current; + setSizes(nextSizes); - setSizes(nextSizes); + // If resize change handlers have been declared, this is the time to call them. + // Trigger user callbacks after updating state, so that user code can override the sizes. + callPanelCallbacks( + panelsArray, + nextSizes, + panelIdToLastNotifiedSizeMap + ); + } + }, []); - // If resize change handlers have been declared, this is the time to call them. - // Trigger user callbacks after updating state, so that user code can override the sizes. - callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap); - } - }, []); + const resizePanel = useCallback((id: string, nextSize: number) => { + const { panels, sizes: prevSizes } = committedValuesRef.current; - const resizePanel = useCallback((id: string, nextSize: number) => { - const { panels, sizes: prevSizes } = committedValuesRef.current; + const panel = panels.get(id); + if (panel == null) { + return; + } - const panel = panels.get(id); - if (panel == null) { - return; - } + const { collapsedSize, collapsible, maxSize, minSize } = panel.current; - const { collapsedSize, collapsible, maxSize, minSize } = panel.current; + const panelsArray = panelsMapToSortedArray(panels); - const panelsArray = panelsMapToSortedArray(panels); + const index = panelsArray.indexOf(panel); + if (index < 0) { + return; + } - const index = panelsArray.indexOf(panel); - if (index < 0) { - return; - } + const currentSize = prevSizes[index]; + if (currentSize === nextSize) { + return; + } - const currentSize = prevSizes[index]; - if (currentSize === nextSize) { - return; - } + if (collapsible && nextSize === collapsedSize) { + // This is a valid resize state. + } else { + nextSize = Math.min(maxSize, Math.max(minSize, nextSize)); + } - if (collapsible && nextSize === collapsedSize) { - // This is a valid resize state. - } else { - nextSize = Math.min(maxSize, Math.max(minSize, nextSize)); - } + const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray); + if (idBefore == null || idAfter == null) { + return; + } - const [idBefore, idAfter] = getBeforeAndAfterIds(id, panelsArray); - if (idBefore == null || idAfter == null) { - return; - } + const isLastPanel = index === panelsArray.length - 1; + const delta = isLastPanel + ? currentSize - nextSize + : nextSize - currentSize; + + const nextSizes = adjustByDelta( + null, + panels, + idBefore, + idAfter, + delta, + prevSizes, + panelSizeBeforeCollapse.current, + null + ); + if (prevSizes !== nextSizes) { + const panelIdToLastNotifiedSizeMap = + panelIdToLastNotifiedSizeMapRef.current; - const isLastPanel = index === panelsArray.length - 1; - const delta = isLastPanel ? currentSize - nextSize : nextSize - currentSize; + setSizes(nextSizes); - const nextSizes = adjustByDelta( - null, - panels, - idBefore, - idAfter, - delta, - prevSizes, - panelSizeBeforeCollapse.current, - null + // If resize change handlers have been declared, this is the time to call them. + // Trigger user callbacks after updating state, so that user code can override the sizes. + callPanelCallbacks( + panelsArray, + nextSizes, + panelIdToLastNotifiedSizeMap + ); + } + }, []); + + context = useMemo( + () => ({ + activeHandleId, + collapsePanel, + direction, + expandPanel, + getPanelStyle, + groupId, + registerPanel, + registerResizeHandle, + resizePanel, + startDragging: (id: string, event: ResizeEvent) => { + setActiveHandleId(id); + + if (isMouseEvent(event) || isTouchEvent(event)) { + const handleElement = getResizeHandle(id)!; + + initialDragStateRef.current = { + dragHandleRect: handleElement.getBoundingClientRect(), + dragOffset: getDragOffset(event, id, direction), + sizes: committedValuesRef.current.sizes, + }; + } + }, + stopDragging: () => { + resetGlobalCursorStyle(); + setActiveHandleId(null); + + initialDragStateRef.current = null; + }, + unregisterPanel, + }), + [ + activeHandleId, + collapsePanel, + direction, + expandPanel, + getPanelStyle, + groupId, + registerPanel, + registerResizeHandle, + resizePanel, + unregisterPanel, + ] ); - if (prevSizes !== nextSizes) { - const panelIdToLastNotifiedSizeMap = - panelIdToLastNotifiedSizeMapRef.current; - - setSizes(nextSizes); - - // If resize change handlers have been declared, this is the time to call them. - // Trigger user callbacks after updating state, so that user code can override the sizes. - callPanelCallbacks(panelsArray, nextSizes, panelIdToLastNotifiedSizeMap); - } - }, []); - - const context = useMemo( - () => ({ - activeHandleId, - collapsePanel, - direction, - expandPanel, - getPanelStyle, - groupId, - registerPanel, - registerResizeHandle, - resizePanel, - startDragging: (id: string, event: ResizeEvent) => { - setActiveHandleId(id); - - if (isMouseEvent(event) || isTouchEvent(event)) { - const handleElement = getResizeHandle(id)!; - - initialDragStateRef.current = { - dragHandleRect: handleElement.getBoundingClientRect(), - dragOffset: getDragOffset(event, id, direction), - sizes: committedValuesRef.current.sizes, - }; - } - }, - stopDragging: () => { - resetGlobalCursorStyle(); - setActiveHandleId(null); - - initialDragStateRef.current = null; - }, - unregisterPanel, - }), - [ + } else { + context = { activeHandleId, - collapsePanel, + collapsePanel: () => {}, direction, - expandPanel, - getPanelStyle, + expandPanel: () => {}, + getPanelStyle: () => ({}), groupId, - registerPanel, - registerResizeHandle, - resizePanel, - unregisterPanel, - ] - ); + registerPanel: () => {}, + registerResizeHandle: () => () => {}, + resizePanel: () => {}, + startDragging: () => {}, + stopDragging: () => {}, + unregisterPanel: () => {}, + }; + } const style: CSSProperties = { display: "flex", diff --git a/packages/react-resizable-panels/src/PanelResizeHandle.ts b/packages/react-resizable-panels/src/PanelResizeHandle.ts index e08cdee82..a5e209a9b 100644 --- a/packages/react-resizable-panels/src/PanelResizeHandle.ts +++ b/packages/react-resizable-panels/src/PanelResizeHandle.ts @@ -1,3 +1,4 @@ +import { isBrowser } from "#is-browser"; import { createElement, CSSProperties, @@ -76,70 +77,98 @@ export function PanelResizeHandle({ null ); - const stopDraggingAndBlur = useCallback(() => { - // Clicking on the drag handle shouldn't leave it focused; - // That would cause the PanelGroup to think it was still active. - const div = divElementRef.current!; - div.blur(); + let handlers: Record<`on${string}`, (...args: any) => any> = {}; - stopDragging(); + if (isBrowser) { + const stopDraggingAndBlur = useCallback(() => { + // Clicking on the drag handle shouldn't leave it focused; + // That would cause the PanelGroup to think it was still active. + const div = divElementRef.current!; + div.blur(); - const { onDragging } = callbacksRef.current; - if (onDragging) { - onDragging(false); - } - }, [stopDragging]); + stopDragging(); - useEffect(() => { - if (disabled) { - setResizeHandler(null); - } else { - const resizeHandler = registerResizeHandle(resizeHandleId); - setResizeHandler(() => resizeHandler); - } - }, [disabled, resizeHandleId, registerResizeHandle]); - - useEffect(() => { - if (disabled || resizeHandler == null || !isDragging) { - return; - } - - const onMove = (event: ResizeEvent) => { - resizeHandler(event); - }; + const { onDragging } = callbacksRef.current; + if (onDragging) { + onDragging(false); + } + }, [stopDragging]); + + useEffect(() => { + if (disabled) { + setResizeHandler(null); + } else { + const resizeHandler = registerResizeHandle(resizeHandleId); + setResizeHandler(() => resizeHandler); + } + }, [disabled, resizeHandleId, registerResizeHandle]); - const onMouseLeave = (event: MouseEvent) => { - resizeHandler(event); - }; + useEffect(() => { + if (disabled || resizeHandler == null || !isDragging) { + return; + } - const divElement = divElementRef.current!; - const targetDocument = divElement.ownerDocument; - - targetDocument.body.addEventListener("contextmenu", stopDraggingAndBlur); - targetDocument.body.addEventListener("mousemove", onMove); - targetDocument.body.addEventListener("touchmove", onMove); - targetDocument.body.addEventListener("mouseleave", onMouseLeave); - window.addEventListener("mouseup", stopDraggingAndBlur); - window.addEventListener("touchend", stopDraggingAndBlur); - - return () => { - targetDocument.body.removeEventListener( - "contextmenu", - stopDraggingAndBlur - ); - targetDocument.body.removeEventListener("mousemove", onMove); - targetDocument.body.removeEventListener("touchmove", onMove); - targetDocument.body.removeEventListener("mouseleave", onMouseLeave); - window.removeEventListener("mouseup", stopDraggingAndBlur); - window.removeEventListener("touchend", stopDraggingAndBlur); + const onMove = (event: ResizeEvent) => { + resizeHandler(event); + }; + + const onMouseLeave = (event: MouseEvent) => { + resizeHandler(event); + }; + + const divElement = divElementRef.current!; + const targetDocument = divElement.ownerDocument; + + targetDocument.body.addEventListener("contextmenu", stopDraggingAndBlur); + targetDocument.body.addEventListener("mousemove", onMove); + targetDocument.body.addEventListener("touchmove", onMove); + targetDocument.body.addEventListener("mouseleave", onMouseLeave); + window.addEventListener("mouseup", stopDraggingAndBlur); + window.addEventListener("touchend", stopDraggingAndBlur); + + return () => { + targetDocument.body.removeEventListener( + "contextmenu", + stopDraggingAndBlur + ); + targetDocument.body.removeEventListener("mousemove", onMove); + targetDocument.body.removeEventListener("touchmove", onMove); + targetDocument.body.removeEventListener("mouseleave", onMouseLeave); + window.removeEventListener("mouseup", stopDraggingAndBlur); + window.removeEventListener("touchend", stopDraggingAndBlur); + }; + }, [direction, disabled, isDragging, resizeHandler, stopDraggingAndBlur]); + + useWindowSplitterResizeHandlerBehavior({ + disabled, + handleId: resizeHandleId, + resizeHandler, + }); + + handlers = { + onBlur: () => setIsFocused(false), + onFocus: () => setIsFocused(true), + onMouseDown: (event: ReactMouseEvent) => { + startDragging(resizeHandleId, event.nativeEvent); + + const { onDragging } = callbacksRef.current!; + if (onDragging) { + onDragging(true); + } + }, + onMouseUp: stopDraggingAndBlur, + onTouchCancel: stopDraggingAndBlur, + onTouchEnd: stopDraggingAndBlur, + onTouchStart: (event: TouchEvent) => { + startDragging(resizeHandleId, event.nativeEvent); + + const { onDragging } = callbacksRef.current!; + if (onDragging) { + onDragging(true); + } + }, }; - }, [direction, disabled, isDragging, resizeHandler, stopDraggingAndBlur]); - - useWindowSplitterResizeHandlerBehavior({ - disabled, - handleId: resizeHandleId, - resizeHandler, - }); + } const style: CSSProperties = { cursor: getCursorStyle(direction), @@ -159,27 +188,7 @@ export function PanelResizeHandle({ "data-panel-group-id": groupId, "data-panel-resize-handle-enabled": !disabled, "data-panel-resize-handle-id": resizeHandleId, - onBlur: () => setIsFocused(false), - onFocus: () => setIsFocused(true), - onMouseDown: (event: ReactMouseEvent) => { - startDragging(resizeHandleId, event.nativeEvent); - - const { onDragging } = callbacksRef.current!; - if (onDragging) { - onDragging(true); - } - }, - onMouseUp: stopDraggingAndBlur, - onTouchCancel: stopDraggingAndBlur, - onTouchEnd: stopDraggingAndBlur, - onTouchStart: (event: TouchEvent) => { - startDragging(resizeHandleId, event.nativeEvent); - - const { onDragging } = callbacksRef.current!; - if (onDragging) { - onDragging(true); - } - }, + ...handlers, ref: divElementRef, role: "separator", style: { diff --git a/packages/react-resizable-panels/src/hooks/useWindowSplitterBehavior.ts b/packages/react-resizable-panels/src/hooks/useWindowSplitterBehavior.ts index 578081786..5541e1b44 100644 --- a/packages/react-resizable-panels/src/hooks/useWindowSplitterBehavior.ts +++ b/packages/react-resizable-panels/src/hooks/useWindowSplitterBehavior.ts @@ -1,3 +1,4 @@ +import { isBrowser } from "#is-browser"; import { RefObject, useEffect } from "../vendor/react"; import { PRECISION } from "../constants"; @@ -34,6 +35,9 @@ export function useWindowSplitterPanelGroupBehavior({ sizes: number[]; panelSizeBeforeCollapse: RefObject>; }): void { + if (!isBrowser) { + return; + } useEffect(() => { const { direction, panels } = committedValuesRef.current!; @@ -170,6 +174,9 @@ export function useWindowSplitterResizeHandlerBehavior({ handleId: string; resizeHandler: ResizeHandler | null; }): void { + if (!isBrowser) { + return; + } useEffect(() => { if (disabled || resizeHandler == null) { return;