From 980abf2b2412c0c828c9892cc4e92ca235b52a6d Mon Sep 17 00:00:00 2001 From: Yevhenii Hyzyla Date: Fri, 3 Apr 2026 05:40:38 +0200 Subject: [PATCH] [DevTools] Add resizable sidebar to Profiler tab and toolbar compaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add drag-to-resize columns to the Profiler panel — the sidebar was previously fixed at max 300px and is now user-resizable with persisted state, matching the Components tab behavior. The existing resize logic is extracted into a shared useResizableColumns hook as part of this. Add progressive toolbar compaction for the Profiler — when the left column becomes too narrow, tab labels collapse to icons only, then the root selector and snapshot selector wrap to a second row. --- .../devtools/views/Components/Components.css | 1 + .../devtools/views/Components/Components.js | 187 +-------------- .../src/devtools/views/DevTools.css | 1 + .../views/Profiler/CommitFlamegraph.css | 1 + .../devtools/views/Profiler/CommitRanked.css | 1 + .../src/devtools/views/Profiler/Profiler.css | 57 ++++- .../src/devtools/views/Profiler/Profiler.js | 100 +++++++- .../devtools/views/Profiler/RootSelector.css | 7 + .../devtools/views/Profiler/RootSelector.js | 2 +- .../src/devtools/views/TabBar.css | 4 + .../src/devtools/views/TabBar.js | 6 +- .../src/devtools/views/useResizableColumns.js | 223 ++++++++++++++++++ 12 files changed, 389 insertions(+), 201 deletions(-) create mode 100644 packages/react-devtools-shared/src/devtools/views/useResizableColumns.js diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Components.css b/packages/react-devtools-shared/src/devtools/views/Components/Components.css index b977a368af14..b7b6f5fb207f 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Components.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/Components.css @@ -45,6 +45,7 @@ cursor: ew-resize; } +/* Must match VERTICAL_MODE_MAX_WIDTH in useResizableColumns.js */ @container devtools (width < 600px) { .Components { flex-direction: column; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/Components.js b/packages/react-devtools-shared/src/devtools/views/Components/Components.js index 80b6d450331d..d0b2be9b64c5 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/Components.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/Components.js @@ -8,142 +8,30 @@ */ import * as React from 'react'; -import {Fragment, useEffect, useLayoutEffect, useReducer, useRef} from 'react'; +import {Fragment} from 'react'; import Tree from './Tree'; import {OwnersListContextController} from './OwnersListContext'; import portaledContent from '../portaledContent'; import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext'; -import { - localStorageGetItem, - localStorageSetItem, -} from 'react-devtools-shared/src/storage'; import InspectedElementErrorBoundary from './InspectedElementErrorBoundary'; import InspectedElement from './InspectedElement'; import {ModalDialog} from '../ModalDialog'; import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal'; import {NativeStyleContextController} from './NativeStyleEditor/context'; +import useResizableColumns from '../useResizableColumns'; import styles from './Components.css'; -import typeof {SyntheticPointerEvent} from 'react-dom-bindings/src/events/SyntheticEvent'; -type Orientation = 'horizontal' | 'vertical'; - -type ResizeActionType = - | 'ACTION_SET_DID_MOUNT' - | 'ACTION_SET_HORIZONTAL_PERCENTAGE' - | 'ACTION_SET_VERTICAL_PERCENTAGE'; - -type ResizeAction = { - type: ResizeActionType, - payload: any, -}; - -type ResizeState = { - horizontalPercentage: number, - verticalPercentage: number, -}; +const LOCAL_STORAGE_KEY = 'React::DevTools::createResizeReducer'; function Components(_: {}) { - const wrapperElementRef = useRef(null); - const resizeElementRef = useRef(null); - - const [state, dispatch] = useReducer( - resizeReducer, - null, - initResizeState, - ); - - const {horizontalPercentage, verticalPercentage} = state; - - useLayoutEffect(() => { - const resizeElement = resizeElementRef.current; - - setResizeCSSVariable( - resizeElement, - 'horizontal', - horizontalPercentage * 100, - ); - setResizeCSSVariable(resizeElement, 'vertical', verticalPercentage * 100); - }, []); - - useEffect(() => { - const timeoutID = setTimeout(() => { - localStorageSetItem( - LOCAL_STORAGE_KEY, - JSON.stringify({ - horizontalPercentage, - verticalPercentage, - }), - ); - }, 500); - - return () => clearTimeout(timeoutID); - }, [horizontalPercentage, verticalPercentage]); - - const onResizeStart = (event: SyntheticPointerEvent) => { - const element = event.currentTarget; - element.setPointerCapture(event.pointerId); - }; - - const onResizeEnd = (event: SyntheticPointerEvent) => { - const element = event.currentTarget; - element.releasePointerCapture(event.pointerId); - }; - - const onResize = (event: SyntheticPointerEvent) => { - const element = event.currentTarget; - const isResizing = element.hasPointerCapture(event.pointerId); - if (!isResizing) { - return; - } - - const resizeElement = resizeElementRef.current; - const wrapperElement = wrapperElementRef.current; - - if (wrapperElement === null || resizeElement === null) { - return; - } - - event.preventDefault(); - - const orientation = getOrientation(wrapperElement); - - const {height, width, left, top} = wrapperElement.getBoundingClientRect(); - - const currentMousePosition = - orientation === 'horizontal' ? event.clientX - left : event.clientY - top; - - const boundaryMin = MINIMUM_SIZE; - const boundaryMax = - orientation === 'horizontal' - ? width - MINIMUM_SIZE - : height - MINIMUM_SIZE; - - const isMousePositionInBounds = - currentMousePosition > boundaryMin && currentMousePosition < boundaryMax; - - if (isMousePositionInBounds) { - const resizedElementDimension = - orientation === 'horizontal' ? width : height; - const actionType = - orientation === 'horizontal' - ? 'ACTION_SET_HORIZONTAL_PERCENTAGE' - : 'ACTION_SET_VERTICAL_PERCENTAGE'; - const percentage = (currentMousePosition / resizedElementDimension) * 100; - - setResizeCSSVariable(resizeElement, orientation, percentage); - - dispatch({ - type: actionType, - payload: currentMousePosition / resizedElementDimension, - }); - } - }; + const {wrapperRef, resizeElementRef, onResizeStart, onResizeEnd, onResize} = + useResizableColumns(LOCAL_STORAGE_KEY); return ( -
+
@@ -176,67 +64,4 @@ function Components(_: {}) { ); } -const LOCAL_STORAGE_KEY = 'React::DevTools::createResizeReducer'; -const VERTICAL_MODE_MAX_WIDTH = 600; -const MINIMUM_SIZE = 100; - -function initResizeState(): ResizeState { - let horizontalPercentage = 0.65; - let verticalPercentage = 0.5; - - try { - let data = localStorageGetItem(LOCAL_STORAGE_KEY); - if (data != null) { - data = JSON.parse(data); - horizontalPercentage = data.horizontalPercentage; - verticalPercentage = data.verticalPercentage; - } - } catch (error) {} - - return { - horizontalPercentage, - verticalPercentage, - }; -} - -function resizeReducer(state: ResizeState, action: ResizeAction): ResizeState { - switch (action.type) { - case 'ACTION_SET_HORIZONTAL_PERCENTAGE': - return { - ...state, - horizontalPercentage: action.payload, - }; - case 'ACTION_SET_VERTICAL_PERCENTAGE': - return { - ...state, - verticalPercentage: action.payload, - }; - default: - return state; - } -} - -function getOrientation( - wrapperElement: null | HTMLElement, -): null | Orientation { - if (wrapperElement != null) { - const {width} = wrapperElement.getBoundingClientRect(); - return width > VERTICAL_MODE_MAX_WIDTH ? 'horizontal' : 'vertical'; - } - return null; -} - -function setResizeCSSVariable( - resizeElement: null | HTMLElement, - orientation: null | Orientation, - percentage: number, -): void { - if (resizeElement !== null && orientation !== null) { - resizeElement.style.setProperty( - `--${orientation}-resize-percentage`, - `${percentage}%`, - ); - } -} - export default (portaledContent(Components): component()); diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.css b/packages/react-devtools-shared/src/devtools/views/DevTools.css index 0230860b9454..3b9ba1546f19 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.css +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.css @@ -14,6 +14,7 @@ display: flex; align-items: center; padding: 0 0.5rem; + overflow-x: auto; background-color: var(--color-background); border-top: 1px solid var(--color-border); font-family: var(--font-family-sans); diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitFlamegraph.css b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitFlamegraph.css index 143f83b04325..f5e5aec542fb 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitFlamegraph.css +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitFlamegraph.css @@ -2,6 +2,7 @@ width: 100%; flex: 1; padding: 0.5rem; + overflow: hidden; } .PatternPath { diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitRanked.css b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitRanked.css index c226f30486bf..4c26a977e35b 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitRanked.css +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitRanked.css @@ -2,4 +2,5 @@ width: 100%; flex: 1; padding: 0.5rem; + overflow: hidden; } diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.css b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.css index a317e3fcf429..dc17dfca822c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.css +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.css @@ -19,20 +19,34 @@ .LeftColumn { display: flex; flex-direction: column; - flex: 2 1 200px; + flex: 0 0 var(--horizontal-resize-percentage); + min-width: 0; + overflow: hidden; border-top: 1px solid var(--color-border); } .RightColumn { display: flex; flex-direction: column; - flex: 1 1 100px; - max-width: 300px; + flex: 1 1 35%; overflow-x: hidden; border-left: 1px solid var(--color-border); border-top: 1px solid var(--color-border); } +.ResizeBarWrapper { + flex: 0 0 0px; + position: relative; +} + +.ResizeBar { + position: absolute; + left: 1px; + width: 5px; + height: 100%; + cursor: ew-resize; +} + .Content { position: relative; flex: 1 1 auto; @@ -81,6 +95,16 @@ border-bottom: 1px solid var(--color-border); } +.SnapshotSelectorWrapper { + width: 100%; + height: 2.25rem; + padding: 0 0.25rem; + flex: 0 0 auto; + display: flex; + align-items: center; + border-bottom: 1px solid var(--color-border); +} + .VRule { height: 20px; width: 1px; @@ -132,4 +156,29 @@ color: var(--color-link); margin-left: 0.25rem; margin-right: 0.25rem; -} \ No newline at end of file +} + +/* Must match VERTICAL_MODE_MAX_WIDTH in useResizableColumns.js */ +@container devtools (width < 600px) { + .Profiler { + flex-direction: column; + } + + .LeftColumn { + flex: 0 0 var(--vertical-resize-percentage); + } + + .RightColumn { + flex: 1 1 50%; + border-left: none; + } + + .ResizeBar { + top: 1px; + left: 0; + width: 100%; + height: 5px; + cursor: ns-resize; + } +} + diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js index 6254be06d832..35425292cd65 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/Profiler.js @@ -8,7 +8,14 @@ */ import * as React from 'react'; -import {Fragment, useContext, useEffect, useRef, useEffectEvent} from 'react'; +import { + useContext, + useEffect, + useLayoutEffect, + useRef, + useState, + useEffectEvent, +} from 'react'; import {ModalDialog} from '../ModalDialog'; import {ProfilerContext} from './ProfilerContext'; import TabBar from '../TabBar'; @@ -32,13 +39,32 @@ import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/Set import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle'; import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext'; import portaledContent from '../portaledContent'; +import useResizableColumns from '../useResizableColumns'; import {StoreContext} from '../context'; import {TimelineContext} from 'react-devtools-timeline/src/TimelineContext'; import styles from './Profiler.css'; +const LOCAL_STORAGE_KEY = 'React::DevTools::profilerResizeState'; + +// Toolbar compaction levels: +// 0 = full labels, everything on one row +// 1 = tab icons only, everything on one row +// 2 = tab icons only, root selector + snapshot on second row +const COMPACT_NONE = 0; +const COMPACT_ICONS = 1; +const COMPACT_WRAP = 2; + function Profiler(_: {}) { - const profilerRef = useRef(null); + const {wrapperRef, resizeElementRef, onResizeStart, onResizeEnd, onResize} = + useResizableColumns(LOCAL_STORAGE_KEY); + const toolbarRef = useRef(null); + const [compactLevel, setCompactLevel] = useState(COMPACT_NONE); + // Remembers how wide the toolbar content was when it overflowed. + // We only expand back when the container grows past that width, + // so we don't get stuck in a compact → expand → overflow → compact loop. + const expandThresholdsRef = useRef>([0, 0]); + const isMac = typeof navigator !== 'undefined' && navigator.platform.toUpperCase().indexOf('MAC') >= 0; @@ -98,7 +124,7 @@ function Profiler(_: {}) { }); useEffect(() => { - const div = profilerRef.current; + const div = wrapperRef.current; if (!div) { return; } @@ -109,6 +135,40 @@ function Profiler(_: {}) { }; }, []); + useLayoutEffect(() => { + const toolbar = toolbarRef.current; + if (toolbar == null) { + return; + } + + const checkOverflow = () => { + if (toolbar.scrollWidth > toolbar.clientWidth) { + // Content doesn't fit — remember how wide it was, then compact more. + if (compactLevel < COMPACT_WRAP) { + expandThresholdsRef.current[compactLevel] = toolbar.scrollWidth; + setCompactLevel(compactLevel + 1); + } + } else if (compactLevel > COMPACT_NONE) { + // Content fits — but only expand back if the container is now + // at least as wide as the content was before we compacted. + const prevThreshold = expandThresholdsRef.current[compactLevel - 1]; + if (prevThreshold > 0 && toolbar.clientWidth >= prevThreshold) { + setCompactLevel(compactLevel - 1); + } + } + }; + + const observer = new ResizeObserver(checkOverflow); + observer.observe(toolbar); + + // Check immediately for content changes (e.g. profile data loaded) + checkOverflow(); + + return () => observer.disconnect(); + // Re-check when didRecordCommits changes because new elements + // (e.g. SnapshotSelector) appear and may cause overflow. + }, [compactLevel, didRecordCommits]); + let view = null; if (didRecordCommits || selectedTabID === 'timeline') { switch (selectedTabID) { @@ -165,22 +225,23 @@ function Profiler(_: {}) { return ( -
-
-
+
+
+
= COMPACT_ICONS} currentTab={selectedTabID} id="Profiler" selectTab={selectTab} tabs={supportsTimeline ? tabsWithTimeline : tabs} type="profiler" /> - + {compactLevel < COMPACT_WRAP && }
{!isLegacyProfilerSelected && (
)} - {isLegacyProfilerSelected && didRecordCommits && ( - -
- - - )} + {isLegacyProfilerSelected && + didRecordCommits && + compactLevel < COMPACT_WRAP && }
+ {compactLevel >= COMPACT_WRAP && ( +
+ + {isLegacyProfilerSelected && didRecordCommits && ( + + )} +
+ )}
{view}
+
+
+
{sidebar}
diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/RootSelector.css b/packages/react-devtools-shared/src/devtools/views/Profiler/RootSelector.css index ff1a970d46ce..fa540cc74d78 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/RootSelector.css +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/RootSelector.css @@ -1,3 +1,10 @@ .Spacer { flex: 1; } + +.Select { + min-width: 50px; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/RootSelector.js b/packages/react-devtools-shared/src/devtools/views/Profiler/RootSelector.js index 00e8952fedf0..cf474602dd4e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/RootSelector.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/RootSelector.js @@ -42,7 +42,7 @@ export default function RootSelector(_: {}): React.Node { return (
- {options} diff --git a/packages/react-devtools-shared/src/devtools/views/TabBar.css b/packages/react-devtools-shared/src/devtools/views/TabBar.css index 96b30c3dbf55..ab988809a70f 100644 --- a/packages/react-devtools-shared/src/devtools/views/TabBar.css +++ b/packages/react-devtools-shared/src/devtools/views/TabBar.css @@ -93,6 +93,10 @@ margin-left: 0.25rem; } +.IconCompact { + margin-right: 0; +} + @media screen and (max-width: 525px) { .IconSizeNavigation { margin-right: 0; diff --git a/packages/react-devtools-shared/src/devtools/views/TabBar.js b/packages/react-devtools-shared/src/devtools/views/TabBar.js index a277082ab264..9df47d89ae78 100644 --- a/packages/react-devtools-shared/src/devtools/views/TabBar.js +++ b/packages/react-devtools-shared/src/devtools/views/TabBar.js @@ -24,6 +24,7 @@ type TabInfo = { }; export type Props = { + compact?: boolean, currentTab: any, disabled?: boolean, id: string, @@ -33,6 +34,7 @@ export type Props = { }; export default function TabBar({ + compact = false, currentTab, disabled = false, id: groupName, @@ -118,10 +120,10 @@ export default function TabBar({ - {label} + {!compact && {label}} ); diff --git a/packages/react-devtools-shared/src/devtools/views/useResizableColumns.js b/packages/react-devtools-shared/src/devtools/views/useResizableColumns.js new file mode 100644 index 000000000000..536c5c78f349 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/useResizableColumns.js @@ -0,0 +1,223 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {useEffect, useLayoutEffect, useReducer, useRef} from 'react'; +import { + localStorageGetItem, + localStorageSetItem, +} from 'react-devtools-shared/src/storage'; + +import typeof {SyntheticPointerEvent} from 'react-dom-bindings/src/events/SyntheticEvent'; + +type Orientation = 'horizontal' | 'vertical'; + +type ResizeAction = { + type: 'ACTION_SET_HORIZONTAL_PERCENTAGE' | 'ACTION_SET_VERTICAL_PERCENTAGE', + payload: number, +}; + +type ResizeState = { + horizontalPercentage: number, + verticalPercentage: number, +}; + +// Must match the CSS @container query breakpoint in +// Components/Components.css and Profiler/Profiler.css. +const VERTICAL_MODE_MAX_WIDTH = 600; +const MINIMUM_SIZE = 100; + +export default function useResizableColumns(localStorageKey: string): { + wrapperRef: {current: null | HTMLElement}, + resizeElementRef: {current: null | HTMLElement}, + onResizeStart: (event: SyntheticPointerEvent) => void, + onResizeEnd: (event: SyntheticPointerEvent) => void, + onResize: (event: SyntheticPointerEvent) => void, +} { + const wrapperRef = useRef(null); + const resizeElementRef = useRef(null); + const isFirstRenderRef = useRef(true); + + const [state, dispatch] = useReducer( + resizeReducer, + localStorageKey, + initResizeState, + ); + + const {horizontalPercentage, verticalPercentage} = state; + + // Set CSS variables once on mount from stored state. + // After this, onResize updates them directly during drag. + useLayoutEffect(() => { + const resizeElement = resizeElementRef.current; + + setResizeCSSVariable( + resizeElement, + 'horizontal', + horizontalPercentage * 100, + ); + setResizeCSSVariable(resizeElement, 'vertical', verticalPercentage * 100); + }, []); + + // Skip the first run — initial state already came from localStorage. + useEffect(() => { + if (isFirstRenderRef.current) { + isFirstRenderRef.current = false; + return; + } + + const timeoutID = setTimeout(() => { + localStorageSetItem( + localStorageKey, + JSON.stringify({ + horizontalPercentage, + verticalPercentage, + }), + ); + }, 500); + + return () => clearTimeout(timeoutID); + }, [localStorageKey, horizontalPercentage, verticalPercentage]); + + const onResizeStart = (event: SyntheticPointerEvent) => { + const element = event.currentTarget; + element.setPointerCapture(event.pointerId); + }; + + const onResizeEnd = (event: SyntheticPointerEvent) => { + const element = event.currentTarget; + element.releasePointerCapture(event.pointerId); + }; + + const onResize = (event: SyntheticPointerEvent) => { + const element = event.currentTarget; + const isResizing = element.hasPointerCapture(event.pointerId); + if (!isResizing) { + return; + } + + const resizeElement = resizeElementRef.current; + const wrapperElement = wrapperRef.current; + + if (wrapperElement === null || resizeElement === null) { + return; + } + + event.preventDefault(); + + const orientation = getOrientation(wrapperElement); + + const {height, width, left, top} = wrapperElement.getBoundingClientRect(); + + const currentMousePosition = + orientation === 'horizontal' ? event.clientX - left : event.clientY - top; + + const boundaryMin = MINIMUM_SIZE; + const boundaryMax = + orientation === 'horizontal' + ? width - MINIMUM_SIZE + : height - MINIMUM_SIZE; + + const isMousePositionInBounds = + currentMousePosition > boundaryMin && currentMousePosition < boundaryMax; + + if (isMousePositionInBounds) { + const resizedElementDimension = + orientation === 'horizontal' ? width : height; + const actionType = + orientation === 'horizontal' + ? 'ACTION_SET_HORIZONTAL_PERCENTAGE' + : 'ACTION_SET_VERTICAL_PERCENTAGE'; + const percentage = (currentMousePosition / resizedElementDimension) * 100; + + setResizeCSSVariable(resizeElement, orientation, percentage); + + dispatch({ + type: actionType, + payload: currentMousePosition / resizedElementDimension, + }); + } + }; + + return { + wrapperRef, + resizeElementRef, + onResizeStart, + onResizeEnd, + onResize, + }; +} + +function initResizeState(localStorageKey: string): ResizeState { + let horizontalPercentage = 0.65; + let verticalPercentage = 0.5; + + try { + let data = localStorageGetItem(localStorageKey); + if (data != null) { + data = JSON.parse(data); + if (typeof data.horizontalPercentage === 'number') { + horizontalPercentage = Math.min( + Math.max(data.horizontalPercentage, 0.1), + 0.9, + ); + } + if (typeof data.verticalPercentage === 'number') { + verticalPercentage = Math.min( + Math.max(data.verticalPercentage, 0.1), + 0.9, + ); + } + } + } catch (error) {} + + return { + horizontalPercentage, + verticalPercentage, + }; +} + +function resizeReducer(state: ResizeState, action: ResizeAction): ResizeState { + switch (action.type) { + case 'ACTION_SET_HORIZONTAL_PERCENTAGE': + return { + ...state, + horizontalPercentage: action.payload, + }; + case 'ACTION_SET_VERTICAL_PERCENTAGE': + return { + ...state, + verticalPercentage: action.payload, + }; + default: + return state; + } +} + +function getOrientation( + wrapperElement: null | HTMLElement, +): null | Orientation { + if (wrapperElement != null) { + const {width} = wrapperElement.getBoundingClientRect(); + return width > VERTICAL_MODE_MAX_WIDTH ? 'horizontal' : 'vertical'; + } + return null; +} + +function setResizeCSSVariable( + resizeElement: null | HTMLElement, + orientation: null | Orientation, + percentage: number, +): void { + if (resizeElement !== null && orientation !== null) { + resizeElement.style.setProperty( + `--${orientation}-resize-percentage`, + `${percentage}%`, + ); + } +}