UNPKG

@atlaskit/navigation-system

Version:

The latest navigation system for Atlassian apps.

679 lines (656 loc) • 29.9 kB
/* panel-splitter.tsx generated by @compiled/babel-plugin v3.0.2 */ import _extends from "@babel/runtime/helpers/extends"; import "./panel-splitter.compiled.css"; import * as React from 'react'; import { ax, ix } from "@compiled/react/runtime"; import { forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal, flushSync } from 'react-dom'; import { bind } from 'bind-event-listener'; import invariant from 'tiny-invariant'; import { useId } from '@atlaskit/ds-lib/use-id'; import useStableRef from '@atlaskit/ds-lib/use-stable-ref'; import { useOpenLayerObserver } from '@atlaskit/layering/use-open-layer-observer'; import { fg } from '@atlaskit/platform-feature-flags/fg'; import { draggable } from '@atlaskit/pragmatic-drag-and-drop/adapter/element-adapter'; import { blockDraggingToIFrames } from '@atlaskit/pragmatic-drag-and-drop/utils/block-dragging-to-iframes'; import { combine } from '@atlaskit/pragmatic-drag-and-drop/utils/combine'; import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/utils/disable-native-drag-preview'; import { preventUnhandled } from '@atlaskit/pragmatic-drag-and-drop/utils/prevent-unhandled'; import Tooltip from '@atlaskit/tooltip/Tooltip'; import TooltipContainer from '@atlaskit/tooltip/TooltipContainer'; import { requestClosePopovers } from '@atlaskit/top-layer/page-surface/request-close-popovers'; import VisuallyHidden from '@atlaskit/visually-hidden/visually-hidden'; import { contentInsetBlockStart } from '../constants'; import { LayoutAreaSizingContext } from '../layout-area-sizing-context'; import { convertResizeBoundToPixels } from './convert-resize-bound-to-pixels'; import { getPanelGrowthSide } from './get-panel-growth-side'; import { getPercentageWithinPixelBounds } from './get-percentage-within-pixel-bounds'; import { getPixelWidth } from './get-pixel-width'; import { getWidthFromDragLocation } from './get-width-from-drag-location'; import { isPanelSplitterDragData } from './is-panel-splitter-drag-data'; import { createKeyboardResizeManager } from './keyboard-resize-manager'; import { OnDoubleClickContext } from './on-double-click-context'; import { PanelSplitterContext } from './panel-splitter-context'; import { panelSplitterDragDataSymbol } from './panel-splitter-drag-symbol'; function signPanelSplitterDragData(data) { return { ...data, [panelSplitterDragDataSymbol]: true }; } const containerStyles = { root: "_12ji1r31 _1qu2glyw _12y31o36 _1e0cglyw _kqswstnw _u7coidpf _152tidpf _1pbykb7n _181n1ule", positionEnd: "_rjxpftgi", positionStart: "_1e02zeo2" }; const grabAreaStyles = { root: "_1bsbl52n _4t3i1osq _kqswstnw _1q51ze3t _y4tize3t _85i5ze3t _bozgze3t _syaz1j28 _bfhk1j28 _1oec6ebc _1lww188d _6fl41ytf _k8m01ylx _d0aluf7j _vp7g6x5g _30l36x5g _1gglglyw _9h8h12zz", rootFinesse: "_vp7g1ihb _30l31ihb _9h8hq98m" }; const lineStyles = { root: "_kqswstnw _1e0c1ule _1bsb1l7b _4t3i1osq _syaz1kw7 _bfhk1r31 _1e021v6z" }; const tooltipStyles = { root: "_ahbq196n _1bsb1ris", placedLeft: "_ahbqze3t _12l2196n" }; const PanelSplitterTooltip = /*#__PURE__*/forwardRef(({ children, className, ...props }, ref) => { const style = useMemo(() => { if (!props.style || !props.style.transform) { return props.style; } // We cannot just match against integers safely, as browsers may introduce non-integer values due to scaling // In the future we can probably use `CSSStyleValue.parse()` to get the browser to handle parsing for us, // but there's no Firefox support yet. const [translateX, translateY] = props.style.transform.matchAll(/(?:-|\+)?\d*\.?\d+px/g); if (!translateY) { // If we can't extract the translateY value we bail out and return the original style return props.style; } /** * Adjusts the translate Y to keep the tooltip within the main content area, * so that it does not appear over the banner or top navigation. */ const newTranslateY = `max(calc(${contentInsetBlockStart} + ${"var(--ds-space-100, 8px)"}), ${translateY})`; const newTransform = `translate3d(${translateX}, ${newTranslateY}, 0)`; return { ...props.style, transform: newTransform }; }, [props.style]); return /*#__PURE__*/React.createElement(TooltipContainer, _extends({}, props, { ref: ref // Must be statically passed // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop, @atlaskit/design-system/no-unsafe-style-overrides , className: ax([tooltipStyles.root, props.placement === 'left' && tooltipStyles.placedLeft, className]) // eslint-disable-next-line @atlaskit/design-system/no-unsafe-style-overrides , // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop style: style }), children); }); /** * A wrapper component that renders a tooltip if the tooltipContent or shortcut is provided. */ const MaybeTooltip = ({ tooltipContent, shortcut, mousePosition, children, testId }) => { if (tooltipContent) { return /*#__PURE__*/React.createElement(Tooltip, { testId: testId, content: tooltipContent, shortcut: shortcut, position: "mouse-y", mousePosition: mousePosition, isScreenReaderAnnouncementDisabled: true, component: PanelSplitterTooltip, shouldAlwaysFadeIn: true, shouldRenderToParent: true }, children); } return children; }; // Browsers increase an RTL range input's value on ArrowLeft, so the arrow key pointing towards the // panel's growth side widens it. const rangeInputDirs = { left: 'rtl', right: 'ltr' }; function getTextDirection(element) { const { direction } = window.getComputedStyle(element); return direction === 'rtl' ? 'rtl' : 'ltr'; } const PortaledPanelSplitter = ({ label, onResizeStart, onResizeEnd, testId, panelId, panelWidth, onCompleteResize, onResizeStartInternal, onResizeInternal, prepareResize, getResizeBounds, panel, portal, resizingCssVar, resizingElementRef, position, tooltipContent, shortcut }) => { var _useContext; const splitterRef = useRef(null); const registerSplitter = (_useContext = useContext(LayoutAreaSizingContext)) === null || _useContext === void 0 ? void 0 : _useContext.registerSplitter; const unregisterSplitter = useRef(undefined); const setSplitterRef = useCallback(element => { var _unregisterSplitter$c; (_unregisterSplitter$c = unregisterSplitter.current) === null || _unregisterSplitter$c === void 0 ? void 0 : _unregisterSplitter$c.call(unregisterSplitter); splitterRef.current = element; unregisterSplitter.current = element && prepareResize ? registerSplitter === null || registerSplitter === void 0 ? void 0 : registerSplitter(element) : undefined; }, [prepareResize, registerSplitter]); const labelId = useId(); const panelGrowthSide = useMemo(() => fg('platform-dst-panel-splitter-resize-direction') ? getPanelGrowthSide({ position, textDirection: getTextDirection(panel) }) : undefined, [position, panel]); // Separate state used for the input range width to remove the UI's dependency on the "persisted" layout state value being updated const [rangeInputValue, setRangeInputValue] = useState(panelWidth); /** * We are using state to store the resize bounds _converted into pixel units_, so that we can update them when the window is resized. * Otherwise they can become out of sync with the actual viewport size, and cause a broken experience (inaccurate width percentage) * when the user tries to resize using the keyboard. */ const [rangeInputBounds, setRangeInputBounds] = useState({ // Using placeholder values here for the initial render. These will be calculated and updated when the range input is focused. min: 200, max: 500 }); const openLayerObserver = useOpenLayerObserver({ isEnabled: !fg('platform-dst-top-layer') }); /** * This is a temporary workaround to enable the `SideNavPanelSplitter` component * to collapse the side nav on double click, without exposing the `onDoubleClick` prop on `PanelSplitter`. * Once `PanelSplitter` supports an `onDoubleClick` prop directly, this context can be removed. */ const onDoubleClick = useContext(OnDoubleClickContext); // Storing the initial `clientX` on `mousedown` events, to workaround a bug caused by some browser extensions // where the `dragstart` event incorrectly returns `0` for the `clientX` location. const initialClientXRef = useRef(null); // Live allocation changes panelWidth on every frame. Only the legacy path should // rebind the drag listeners when this value changes. const legacyPanelWidth = fg('platform-dst-chat-panel-layout') || onResizeInternal ? undefined : panelWidth; useEffect(() => { const splitter = splitterRef.current; invariant(splitter, 'Splitter ref must be set'); let lastManagedResizeWidth; return combine(blockDraggingToIFrames({ element: splitter }), /** * Capturing the initial `clientX` from the mousedown, before the drag starts. * * ⚠️ Note: We are not using pragmatic-drag-and-drop's `onDragStart` for this as some browser * extensions can cause the client location (e.g. clientX) to incorrectly return 0 during the `dragstart` event. * * We are also not using pragmatic-drag-and-drop's `onDrag` here as it is throttled, which means * fast mouse movements can cause the real first drag event to be missed, causing a different clientX * to be captured. * * I also tried only binding an event listener inside pragmatic-drag-and-drop's `onDragStart`, which seemd to work * but did not feel as robust, and might have timing issues as it happens slightly later. */ bind(splitter, { type: 'mousedown', listener: event => { initialClientXRef.current = event.clientX; } }), draggable({ element: splitter, onGenerateDragPreview: ({ nativeSetDragImage }) => { // We will be moving the line to indicate a drag. We can disable the native drag preview disableNativeDragPreview({ nativeSetDragImage }); // We don't want any native drop animation for when the user does not drop on a drop target. We want the drag to finish immediately preventUnhandled.start(); }, getInitialData() { const initialWidth = getPixelWidth(panel); /** * The drag calculations require the actual computed width of the element * For example, if the panel has loaded with a width of 2000px, but the max bound is 1000px and is visually 1000px (due to the CSS `clamp()`), * the drag calculations should use the actual width of 1000px, not the width in state of 2000px. */ return signPanelSplitterDragData({ panelId, initialWidth, resizingWidth: `${initialWidth}px`, resizeBounds: getResizeBounds(), // Only computing text direction when we need it, just as the drag is starting. // Recomputing text direction on each new drag in case the text direction // has changed. This is unlikely, but being safe. direction: getTextDirection(panel) }); }, onDragStart({ source }) { /** * ⚠️ Note: We are not using the client locations (e.g. clientX) during `onDragStart` * because some browser extensions can cause the event properties to incorrectly return 0. */ invariant(isPanelSplitterDragData(source.data)); const resizeStartData = { initialWidth: source.data.initialWidth }; // The layout allocation system needs to know which area is actively being // resized before the first drag update. Committing this synchronously keeps // the actively dragged area protected from automatic compression. if (onResizeStartInternal) { flushSync(() => onResizeStartInternal(resizeStartData)); } onResizeStart === null || onResizeStart === void 0 ? void 0 : onResizeStart(resizeStartData); // Close any open layers when the user starts resizing // Not gated. Top-layer tooltips (`platform-dst-top-layer-tooltip`) are not known to the observer. requestClosePopovers(); openLayerObserver === null || openLayerObserver === void 0 ? void 0 : openLayerObserver.closeLayers(); }, onDrag({ location, source }) { var _initialClientXRef$cu; /** * ⚠️ Note: We are not using the location.initial.input client locations because some browser extensions * can cause the client locations (e.g. clientX) in the `dragstart` event to incorrectly return 0. */ invariant(isPanelSplitterDragData(source.data)); const { initialWidth, resizeBounds, direction } = source.data; invariant(initialClientXRef.current !== null, 'initialClientX must be set'); /** * How wide the element would be if there were no width constraints, * based on the pointer's position. */ const targetWidth = getWidthFromDragLocation({ initialWidth, location, // The fallback of 0 won't be used due to the invariant, however we require one to satisfy the type. initialClientX: (_initialClientXRef$cu = initialClientXRef.current) !== null && _initialClientXRef$cu !== void 0 ? _initialClientXRef$cu : 0, direction, position }); const resizingWidth = `clamp(${resizeBounds.min}, ${targetWidth}px, ${resizeBounds.max})`; if (onResizeInternal) { // The allocator needs below-minimum requests for the local-to-chat cascade. const currentBounds = getResizeBounds(); const width = Math.max(0, Math.min(targetWidth, prepareResize ? window.innerWidth : convertResizeBoundToPixels(currentBounds.max))); if (width !== lastManagedResizeWidth) { lastManagedResizeWidth = width; onResizeInternal(width); } } else { var _resizingElementRef$c; panel.style.setProperty(resizingCssVar, resizingWidth); resizingElementRef === null || resizingElementRef === void 0 ? void 0 : (_resizingElementRef$c = resizingElementRef.current) === null || _resizingElementRef$c === void 0 ? void 0 : _resizingElementRef$c.style.setProperty(resizingCssVar, resizingWidth); } source.data.resizingWidth = resizingWidth; }, async onDrop({ source }) { var _resizingElementRef$c2; invariant(isPanelSplitterDragData(source.data)); preventUnhandled.stop(); if (prepareResize) { if (!(await prepareResize()) || !panel.isConnected) { return; } } // PDD may deliver its final throttled drag update immediately before drop. // Commit that update once before measuring; do not persist a previous frame. if (onResizeInternal && lastManagedResizeWidth !== undefined) { const width = lastManagedResizeWidth; flushSync(() => onResizeInternal(width)); lastManagedResizeWidth = undefined; } const finalWidth = getPixelWidth(panel); // Persist the final preferred width before removing the imperative drag // variable, so the rendered width is continuous across pointer release. if (fg('platform-dst-chat-panel-layout') || onResizeInternal) { flushSync(() => onCompleteResize(finalWidth)); } else { onCompleteResize(finalWidth); } onResizeEnd === null || onResizeEnd === void 0 ? void 0 : onResizeEnd({ initialWidth: source.data.initialWidth, finalWidth }); panel.style.removeProperty(resizingCssVar); resizingElementRef === null || resizingElementRef === void 0 ? void 0 : (_resizingElementRef$c2 = resizingElementRef.current) === null || _resizingElementRef$c2 === void 0 ? void 0 : _resizingElementRef$c2.style.removeProperty(resizingCssVar); } })); }, [onCompleteResize, onResizeStartInternal, onResizeInternal, prepareResize, onResizeStart, onResizeEnd, panel, resizingCssVar, resizingElementRef, legacyPanelWidth, position, openLayerObserver, panelId, getResizeBounds]); const onResizeStartStableRef = useStableRef(onResizeStart); const onResizeEndStableRef = useStableRef(onResizeEnd); const [keyboardResizeManager] = useState(() => createKeyboardResizeManager({ onResizeStart: (...args) => { var _onResizeStartStableR; // Close any open layers when the user starts resizing with keyboard // Not gated. Top-layer tooltips (`platform-dst-top-layer-tooltip`) are not known to the observer. requestClosePopovers(); openLayerObserver === null || openLayerObserver === void 0 ? void 0 : openLayerObserver.closeLayers(); (_onResizeStartStableR = onResizeStartStableRef.current) === null || _onResizeStartStableR === void 0 ? void 0 : _onResizeStartStableR.call(onResizeStartStableRef, ...args); }, onResizeEnd: (...args) => { var _onResizeEndStableRef; return (_onResizeEndStableRef = onResizeEndStableRef.current) === null || _onResizeEndStableRef === void 0 ? void 0 : _onResizeEndStableRef.call(onResizeEndStableRef, ...args); } })); const focusSequence = useRef(0); const keyboardBoundsReady = useRef(false); const pendingKeyboardWidth = useRef(null); const handleSliderInputChange = useCallback(async (event, pendingKey) => { const isManagedResize = onResizeInternal; const sequence = focusSequence.current; let value = isManagedResize ? parseFloat(event.currentTarget.value) : parseInt(event.currentTarget.value); if (prepareResize) { if (!(await prepareResize()) || !panel.isConnected || sequence !== focusSequence.current) { return; } } if (isManagedResize) { // The available region may have changed since focus, including without a // window resize. Use the allocator's current bounds for the public payload. const bounds = getResizeBounds(); const min = convertResizeBoundToPixels(bounds.min); const max = convertResizeBoundToPixels(bounds.max); if (pendingKey) { var _pendingKeyboardWidth; // Native range inputs would apply these keys to the placeholder bounds. // Preserve the intent until the chunk provides the real bounds instead. const current = (_pendingKeyboardWidth = pendingKeyboardWidth.current) !== null && _pendingKeyboardWidth !== void 0 ? _pendingKeyboardWidth : getPixelWidth(panel); const doesArrowRightIncrease = panelGrowthSide ? panelGrowthSide === 'right' : getTextDirection(panel) === 'ltr'; const increase = pendingKey === 'ArrowUp' || pendingKey === (doesArrowRightIncrease ? 'ArrowRight' : 'ArrowLeft') || pendingKey === 'PageUp'; const step = pendingKey.startsWith('Page') ? Math.max(20, (max - min) / 10) : 20; value = pendingKey === 'End' ? max : pendingKey === 'Home' ? min : current + (increase ? step : -step); } value = Math.max(min, Math.min(value, max)); setRangeInputBounds({ min, max }); } pendingKeyboardWidth.current = value; setRangeInputValue(value); /** * We are intentionally _not_ debouncing `onCompleteResize` as we want it to be called anytime the user changes the range input value. * It is an internal-only callback - it's provided by `PanelSplitterProvider`, and used by layout slots to update their layout width state. */ onCompleteResize(value); keyboardResizeManager.onResize({ initialWidth: panelWidth, finalWidth: value }); }, [getResizeBounds, onCompleteResize, onResizeInternal, panelWidth, keyboardResizeManager, panel, prepareResize, panelGrowthSide]); const handleSliderKeyDown = useCallback(event => { if (prepareResize && !keyboardBoundsReady.current && ['Home', 'End', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'PageUp', 'PageDown'].includes(event.key)) { event.preventDefault(); void handleSliderInputChange(event, event.key); } }, [handleSliderInputChange, prepareResize]); const resizeEventListenerCleanupFn = useRef(null); const handleSliderFocus = useCallback(async () => { var _resizeEventListenerC; const sequence = ++focusSequence.current; keyboardBoundsReady.current = false; pendingKeyboardWidth.current = null; if (prepareResize) { if (!(await prepareResize()) || !panel.isConnected || sequence !== focusSequence.current) { return; } } /** * We are only updating the range input's properties when the element is focused, as a performance optimisation. * It isn't necessary to recalculate these values until the element is going to be used. */ // Clean up any left over listeners in case `focus` was triggered again without `blur`. (_resizeEventListenerC = resizeEventListenerCleanupFn.current) === null || _resizeEventListenerC === void 0 ? void 0 : _resizeEventListenerC.call(resizeEventListenerCleanupFn); // Update the range input value to match the current panel width. const width = prepareResize ? getPixelWidth(panel) : panelWidth; pendingKeyboardWidth.current = width; setRangeInputValue(width); const resizeBounds = getResizeBounds(); // Update the range input bounds to ensure they are accurate (in case the viewport was previously resized) setRangeInputBounds({ min: convertResizeBoundToPixels(resizeBounds.min), max: convertResizeBoundToPixels(resizeBounds.max) }); keyboardBoundsReady.current = true; /** * _While the slider element is focused_, add a window resize event listener to update the range input bounds * when the window is resized. This is needed to ensure the min and max widths accurately reflect the viewport * size - in case they are provided in vw units. * * This listener is cleaned up when the element is blurred. */ resizeEventListenerCleanupFn.current = bind(window, { type: 'resize', listener: function updateState() { const resizeBounds = getResizeBounds(); setRangeInputBounds({ min: convertResizeBoundToPixels(resizeBounds.min), max: convertResizeBoundToPixels(resizeBounds.max) }); } }); }, [panelWidth, getResizeBounds, panel, prepareResize]); const handleSliderBlur = useCallback(() => { var _resizeEventListenerC2; focusSequence.current++; keyboardBoundsReady.current = false; // Remove the resize event listener when the range input is blurred. (_resizeEventListenerC2 = resizeEventListenerCleanupFn.current) === null || _resizeEventListenerC2 === void 0 ? void 0 : _resizeEventListenerC2.call(resizeEventListenerCleanupFn); }, []); useEffect(() => { return function cleanup() { var _resizeEventListenerC3; // Cleanup the resize event listener when the component is unmounted. (_resizeEventListenerC3 = resizeEventListenerCleanupFn.current) === null || _resizeEventListenerC3 === void 0 ? void 0 : _resizeEventListenerC3.call(resizeEventListenerCleanupFn); }; }, []); const ariaValueText = useMemo(() => `${getPercentageWithinPixelBounds({ currentWidth: rangeInputValue, resizeBounds: rangeInputBounds })}% width`, [rangeInputValue, rangeInputBounds]); return /*#__PURE__*/createPortal( /*#__PURE__*/React.createElement("div", { "data-testid": testId ? `${testId}-container` : undefined, className: ax([containerStyles.root, position === 'start' && containerStyles.positionStart, position === 'end' && containerStyles.positionEnd]) }, /*#__PURE__*/React.createElement(MaybeTooltip, { tooltipContent: tooltipContent, shortcut: shortcut, mousePosition: panelGrowthSide !== null && panelGrowthSide !== void 0 ? panelGrowthSide : 'right', testId: testId ? `${testId}-tooltip` : undefined }, /*#__PURE__*/React.createElement("div", { ref: setSplitterRef, "data-testid": testId, onDoubleClick: onDoubleClick, className: ax([grabAreaStyles.root, fg('platform-dst-tokens-finesse') && grabAreaStyles.rootFinesse]) }, /*#__PURE__*/React.createElement(VisuallyHidden, null, /*#__PURE__*/React.createElement("input", { type: "range", dir: panelGrowthSide ? rangeInputDirs[panelGrowthSide] : undefined, value: rangeInputValue, step: 20, min: rangeInputBounds.min, max: rangeInputBounds.max, "aria-valuetext": ariaValueText, "aria-labelledby": labelId, onChange: handleSliderInputChange, onKeyDown: prepareResize ? handleSliderKeyDown : undefined, onFocus: handleSliderFocus, onBlur: handleSliderBlur }), /*#__PURE__*/React.createElement("span", { id: labelId }, label)), /*#__PURE__*/React.createElement("span", { className: ax([lineStyles.root]) })))), portal); }; /** * _PanelSplitter_ * * A component that allows the user to resize a layout area. * It can be used within layout areas like `Panel`, and `Aside`. * For the `SideNav`, use the `SideNavPanelSplitter` component instead, as it provides additional functionality * such as double clicking to collapse the side nav. * * Example usage: * ```tsx * <Aside> * <!-- other side nav content --> * <PanelSplitter label="Resize Side Nav" /> * </Aside> * ``` */ export const PanelSplitter = ({ label, onResizeStart, onResizeEnd, testId, tooltipContent }) => { const [panel, setPanel] = useState(null); const [portal, setPortal] = useState(null); const context = useContext(PanelSplitterContext); invariant(context, 'Panel splitter context not set'); const { panelRef, portalRef, isEnabled, panelId, panelWidth, onCompleteResize, onResizeStartInternal, onResizeInternal, getResizeBounds, prepareResize, resizingCssVar, resizingElementRef, position, shortcut } = context; /** * **Explanation** * * _How React Suspense works_ * * When a component is suspended: * * - it is _not_ "unmounted" and effects are _not_ cleaned up * - refs to react managed elements are cleared * - react adds `display:none !important` as an inline style to hide the element * - state is preserved * * When a suspended component is resumed: * - `display:none !important` inline style is removed * - it will re-render (without the refs set) * - refs will be set * - `useEffect` will _not_ re-run * * [More details](https://x.com/alexandereardon/status/1944617494569992440) * * _What does this mean for the panel splitter?_ * * - The panel splitter renders into content into a react managed element (eg managed by Aside) * - When UI is suspended the portal ref is cleared * - After a suspense, we will get a render with cleared refs * - 🔥 This is problematic if we want to portal into that ref during the render * * _Approach: initial render_ * * - We cannot portal the panel splitter until after the refs are initially set * - Effects run after refs being set. In an effect we set the ref values (which are populated), * trigger a re-render and then portal into the refs. * * _Approach: suspense_ * * - We put the refs into react `state` after every render * - The refs in `state` will not be cleared by suspending as effects are not run, or cleaned up, * when components are suspended / resumed. * - After the post suspense render, we can use the `portalRef` (which was captured in an effect) * to continue to portal into. * * _Why not store `portalRef` in `state` in `<PanelSplitterProvider>`?_ * * - The `portalRef` is set high in the tree, so a re-render will cause a * large amount of components to re-render (eg the whole sidebar) * - After a suspense, we would need to do _another_ large re-render to propagate the new refs * - With our current approach, any changes to `portalRef` will cause a re-render low in the tree * (just in the `<PanelSplitter>`) */ // We want this effect to run after each render (see above) // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { // Don't need to do anything if (!isEnabled) { return; } // It is safe to access refs in useEffect, even with Suspense const panelEl = panelRef.current; const portalEl = portalRef.current; invariant(panelEl, 'panelRef not set'); invariant(portalEl, 'portal element not set'); // Doing our own check, as this results in fewer re-renders than react // (which also does it's own checks to see if a re-render should occur) if (panelEl === panel && portalEl === portal) { return; } // If refs don't change, react won't cause infinite loops setPanel(panelEl); setPortal(portalEl); }); if (!isEnabled) { return null; } if (!portal || !panel) { return null; } return /*#__PURE__*/React.createElement(PortaledPanelSplitter, { label: label, onResizeStart: onResizeStart, onResizeEnd: onResizeEnd, testId: testId, panelId: panelId, panel: panel, portal: portal, panelWidth: panelWidth, onCompleteResize: onCompleteResize, onResizeStartInternal: onResizeStartInternal, onResizeInternal: onResizeInternal, prepareResize: prepareResize, getResizeBounds: getResizeBounds, resizingCssVar: resizingCssVar, resizingElementRef: resizingElementRef, position: position, tooltipContent: tooltipContent, shortcut: shortcut }); };