diff --git a/packages/grafana-ui/src/components/Splitter/Splitter.mdx b/packages/grafana-ui/src/components/Splitter/Splitter.mdx deleted file mode 100644 index af7d3fc015b..00000000000 --- a/packages/grafana-ui/src/components/Splitter/Splitter.mdx +++ /dev/null @@ -1,23 +0,0 @@ -import { Meta, Preview, ArgTypes } from '@storybook/blocks'; -import { Box, Splitter, Text } from '@grafana/ui'; - - - -# Splitter - -The splitter creates two resizable panes, either horizontally or vertically. - - -
- - - Primary - - - Secondary - - -
-
- - diff --git a/packages/grafana-ui/src/components/Splitter/Splitter.story.tsx b/packages/grafana-ui/src/components/Splitter/Splitter.story.tsx deleted file mode 100644 index 7248f78f3d2..00000000000 --- a/packages/grafana-ui/src/components/Splitter/Splitter.story.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { css } from '@emotion/css'; -import { Meta, StoryFn } from '@storybook/react'; -import React from 'react'; - -import { Splitter, useTheme2 } from '@grafana/ui'; - -import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas'; - -import mdx from './Splitter.mdx'; - -const meta: Meta = { - title: 'General/Layout/Splitter', - component: Splitter, - parameters: { - docs: { - page: mdx, - }, - controls: { - exclude: [], - }, - }, - argTypes: { - initialSize: { control: { type: 'number', min: 0.1, max: 1 } }, - }, -}; - -export const Basic: StoryFn = (args) => { - const theme = useTheme2(); - const paneStyles = css({ - display: 'flex', - flexGrow: 1, - background: theme.colors.background.primary, - padding: theme.spacing(2), - border: `1px solid ${theme.colors.border.weak}`, - height: '100%', - }); - - return ( - -
- -
Primary
-
Secondary
-
-
-
- ); -}; - -Basic.args = { - direction: 'row', - dragPosition: 'middle', -}; - -export default meta; diff --git a/packages/grafana-ui/src/components/Splitter/useSplitter.mdx b/packages/grafana-ui/src/components/Splitter/useSplitter.mdx new file mode 100644 index 00000000000..332782e1d92 --- /dev/null +++ b/packages/grafana-ui/src/components/Splitter/useSplitter.mdx @@ -0,0 +1,34 @@ +import { Meta, ArgTypes } from '@storybook/blocks'; +import { Box, useSplitter, Text } from '@grafana/ui'; + +# useSplitter + +The splitter creates two resizable panes, either horizontally or vertically. + +### Usage + +```tsx +import { useSplitter } from '@grafana/ui'; + +const { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({ + direction: 'row', + initialSize: 0.5, + dragPosition: 'end', +}); + +return ( +
+
+ + Primary + +
+
+
+ + Secondary + +
+
+); +``` diff --git a/packages/grafana-ui/src/components/Splitter/useSplitter.story.tsx b/packages/grafana-ui/src/components/Splitter/useSplitter.story.tsx new file mode 100644 index 00000000000..ca929b79efc --- /dev/null +++ b/packages/grafana-ui/src/components/Splitter/useSplitter.story.tsx @@ -0,0 +1,71 @@ +import { Meta, StoryFn } from '@storybook/react'; +import React from 'react'; + +import { Box } from '@grafana/ui'; + +import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas'; + +import { UseSplitterOptions, useSplitter } from './useSplitter'; +import mdx from './useSplitter.mdx'; + +const meta: Meta = { + title: 'General/Layout/useSplitter', + parameters: { + docs: { page: mdx }, + controls: { + exclude: [], + }, + }, + argTypes: { + initialSize: { control: { type: 'number', min: 0.1, max: 1 } }, + direction: { control: { type: 'radio' }, options: ['row', 'column'] }, + dragPosition: { control: { type: 'radio' }, options: ['start', 'middle', 'end'] }, + hasSecondPane: { type: 'boolean', options: [true, false] }, + }, +}; + +interface StoryOptions extends UseSplitterOptions { + hasSecondPane: boolean; +} + +export const Basic: StoryFn = (options) => { + const { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({ + ...options, + }); + + if (!options.hasSecondPane) { + primaryProps.style.flexGrow = 1; + } + + return ( + +
+
+
+ + Primary + +
+ {options.hasSecondPane && ( + <> +
+
+ + Secondary + +
+ + )} +
+
+ + ); +}; + +Basic.args = { + direction: 'row', + dragPosition: 'middle', + hasSecondPane: true, +}; + +export default meta; diff --git a/packages/grafana-ui/src/components/Splitter/Splitter.tsx b/packages/grafana-ui/src/components/Splitter/useSplitter.ts similarity index 78% rename from packages/grafana-ui/src/components/Splitter/Splitter.tsx rename to packages/grafana-ui/src/components/Splitter/useSplitter.ts index ded1e13d65f..5f3b0b8e4a8 100644 --- a/packages/grafana-ui/src/components/Splitter/Splitter.tsx +++ b/packages/grafana-ui/src/components/Splitter/useSplitter.ts @@ -7,137 +7,19 @@ import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../themes'; import { DragHandlePosition, getDragStyles } from '../DragHandle/DragHandle'; -export interface Props { +export interface UseSplitterOptions { /** * The initial size of the primary pane between 0-1, defaults to 0.5 */ initialSize?: number; - direction?: 'row' | 'column'; + direction: 'row' | 'column'; dragPosition?: DragHandlePosition; - primaryPaneStyles?: React.CSSProperties; - secondaryPaneStyles?: React.CSSProperties; /** * Called when ever the size of the primary pane changes * @param flexSize (float from 0-1) */ onSizeChanged?: (flexSize: number, pixelSize: number) => void; onResizing?: (flexSize: number, pixelSize: number) => void; - children: [React.ReactNode, React.ReactNode]; -} - -/** - * Splits two children into two resizable panes - * @alpha - */ -export function Splitter(props: Props) { - const { - direction = 'row', - initialSize = 0.5, - primaryPaneStyles, - secondaryPaneStyles, - onSizeChanged, - onResizing, - dragPosition = 'middle', - children, - } = props; - - const { containerRef, firstPaneRef, minDimProp, splitterProps, secondPaneRef } = useSplitter( - direction, - onSizeChanged, - onResizing - ); - - const kids = React.Children.toArray(children); - const styles = useStyles2(getStyles, direction); - const dragStyles = useStyles2(getDragStyles, dragPosition); - const dragHandleStyle = direction === 'column' ? dragStyles.dragHandleHorizontal : dragStyles.dragHandleVertical; - const id = useId(); - - const secondAvailable = kids.length === 2; - const visibilitySecond = secondAvailable ? 'visible' : 'hidden'; - let firstChildSize = initialSize; - - // If second child is missing let first child have all the space - if (!children[1]) { - firstChildSize = 1; - } - - return ( -
-
- {kids[0]} -
- - {kids[1] && ( - <> -
- -
- {kids[1]} -
- - )} -
- ); -} - -function getStyles(theme: GrafanaTheme2, direction: Props['direction']) { - return { - container: css({ - display: 'flex', - flexDirection: direction === 'row' ? 'row' : 'column', - width: '100%', - flexGrow: 1, - overflow: 'hidden', - }), - panel: css({ display: 'flex', position: 'relative', flexBasis: 0 }), - dragEdge: { - second: css({ - top: 0, - left: theme.spacing(-1), - bottom: 0, - position: 'absolute', - zIndex: theme.zIndex.modal, - }), - first: css({ - top: 0, - left: theme.spacing(-1), - bottom: 0, - position: 'absolute', - zIndex: theme.zIndex.modal, - }), - }, - }; } const PIXELS_PER_MS = 0.3 as const; @@ -159,11 +41,9 @@ const propsForDirection = { }, } as const; -function useSplitter( - direction: 'row' | 'column', - onSizeChanged: Props['onSizeChanged'], - onResizing: Props['onResizing'] -) { +export function useSplitter(options: UseSplitterOptions) { + const { direction, initialSize = 0.5, dragPosition = 'middle', onResizing, onSizeChanged } = options; + const handleSize = 16; const splitterRef = useRef(null); const firstPaneRef = useRef(null); @@ -171,7 +51,6 @@ function useSplitter( const containerRef = useRef(null); const containerSize = useRef(null); const primarySizeRef = useRef<'1fr' | number>('1fr'); - const firstPaneMeasurements = useRef(undefined); const savedPos = useRef(undefined); @@ -197,11 +76,7 @@ function useSplitter( const curSize = firstPaneRef.current!.getBoundingClientRect()[measurementProp]; const newDims = measureElement(firstPaneRef.current); - splitterRef.current!.ariaValueNow = `${clamp( - ((curSize - newDims[minDimProp]) / (newDims[maxDimProp] - newDims[minDimProp])) * 100, - 0, - 100 - )}`; + splitterRef.current!.ariaValueNow = ariaValue(curSize, newDims[minDimProp], newDims[maxDimProp]); } }, 500, @@ -239,10 +114,8 @@ function useSplitter( firstPaneRef.current!.style.flexGrow = `${newFlex}`; secondPaneRef.current!.style.flexGrow = `${1 - newFlex}`; + splitterRef.current!.ariaValueNow = ariaValue(newSize, dims[minDimProp], dims[maxDimProp]); - const ariaValueNow = ariaValue(newSize, dims[minDimProp], dims[maxDimProp] - dims[minDimProp]); - - splitterRef.current!.ariaValueNow = `${ariaValueNow}`; onResizing?.(newFlex, newSize); } }, @@ -406,7 +279,7 @@ function useSplitter( const dim = measureElement(firstPaneRef.current); firstPaneMeasurements.current = dim; primarySizeRef.current = firstPaneRef.current!.getBoundingClientRect()[measurementProp]; - splitterRef.current!.ariaValueNow = `${((primarySizeRef.current - dim[minDimProp]) / (dim[maxDimProp] - dim[minDimProp])) * 100}`; + splitterRef.current!.ariaValueNow = `${ariaValue(primarySizeRef.current, dim[minDimProp], dim[maxDimProp])}`; }, [maxDimProp, measurementProp, minDimProp]); const onBlur = useCallback(() => { @@ -421,10 +294,32 @@ function useSplitter( } }, [onSizeChanged]); + const styles = useStyles2(getStyles, direction); + const dragStyles = useStyles2(getDragStyles, dragPosition); + const dragHandleStyle = direction === 'column' ? dragStyles.dragHandleHorizontal : dragStyles.dragHandleVertical; + const id = useId(); + return { - containerRef, - firstPaneRef, - minDimProp, + containerProps: { + ref: containerRef, + className: styles.container, + }, + primaryProps: { + ref: firstPaneRef, + className: styles.panel, + style: { + [minDimProp]: 'min-content', + flexGrow: clamp(initialSize ?? 0.5, 0, 1), + }, + }, + secondaryProps: { + ref: secondPaneRef, + className: styles.panel, + style: { + flexGrow: clamp(1 - initialSize, 0, 1), + [minDimProp]: 'min-content', + }, + }, splitterProps: { onPointerUp, onPointerDown, @@ -435,8 +330,15 @@ function useSplitter( onBlur, ref: splitterRef, style: { [measurementProp]: `${handleSize}px` }, + role: 'separator', + 'aria-valuemin': 0, + 'aria-valuemax': 100, + 'aria-valuenow': initialSize * 100, + 'aria-controls': `start-panel-${id}`, + 'aria-label': 'Pane resize widget', + tabIndex: 0, + className: dragHandleStyle, }, - secondPaneRef, }; } @@ -456,8 +358,10 @@ function measureElement(ref: T): MeasureResult { const savedWidth = ref.style.width; const savedHeight = ref.style.height; const savedFlex = ref.style.flexGrow; + document.body.style.overflow = 'hidden'; ref.style.flexGrow = '0'; + const { width: minWidth, height: minHeight } = ref.getBoundingClientRect(); ref.style.flexGrow = '100'; @@ -491,3 +395,16 @@ function useResizeObserver( // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); } + +function getStyles(theme: GrafanaTheme2, direction: UseSplitterOptions['direction']) { + return { + container: css({ + display: 'flex', + flexDirection: direction === 'row' ? 'row' : 'column', + width: '100%', + flexGrow: 1, + overflow: 'hidden', + }), + panel: css({ display: 'flex', position: 'relative', flexBasis: 0 }), + }; +} diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 435d63f2442..ec094bea02e 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -264,8 +264,8 @@ export { Avatar } from './UsersIndicator/Avatar'; // Export this until we've figured out a good approach to inline form styles. export { InlineFormLabel } from './FormLabel/FormLabel'; export { Divider } from './Divider/Divider'; -export { getDragStyles } from './DragHandle/DragHandle'; -export { Splitter } from './Splitter/Splitter'; +export { getDragStyles, type DragHandlePosition } from './DragHandle/DragHandle'; +export { useSplitter } from './Splitter/useSplitter'; export { LayoutItemContext, type LayoutItemContextProps } from './Layout/LayoutItemContext'; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index 6b2ceaa457e..91840641603 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -21,8 +21,6 @@ export interface PanelEditorState extends SceneObjectState { isDirty?: boolean; panelId: number; optionsPane: PanelOptionsPane; - optionsCollapsed?: boolean; - optionsPaneSize: number; dataPane?: PanelDataPane; vizManager: VizPanelManager; } @@ -102,56 +100,8 @@ export class PanelEditor extends SceneObjectBase { sourcePanel!.parent.setState({ body: this.state.vizManager.state.panel.clone() }); } } - - public toggleOptionsPane() { - this.setState({ optionsCollapsed: !this.state.optionsCollapsed, optionsPaneSize: OPTIONS_PANE_FLEX_DEFAULT }); - } - - public onOptionsPaneResizing = (flexSize: number, pixelSize: number) => { - if (flexSize <= 0 && pixelSize <= 0) { - return; - } - - const optionsPixelSize = (pixelSize / flexSize) * (1 - flexSize); - - if (this.state.optionsCollapsed && optionsPixelSize > OPTIONS_PANE_PIXELS_MIN) { - this.setState({ optionsCollapsed: false }); - } - - if (!this.state.optionsCollapsed && optionsPixelSize < OPTIONS_PANE_PIXELS_MIN) { - this.setState({ optionsCollapsed: true }); - } - }; - - public onOptionsPaneSizeChanged = (flexSize: number, pixelSize: number) => { - if (flexSize <= 0 && pixelSize <= 0) { - return; - } - - const optionsPaneSize = 1 - flexSize; - const isSnappedClosed = this.state.optionsPaneSize === 0; - const fullWidth = pixelSize / flexSize; - const snapWidth = OPTIONS_PANE_PIXELS_SNAP / fullWidth; - - if (this.state.optionsCollapsed) { - if (isSnappedClosed) { - this.setState({ - optionsPaneSize: Math.max(optionsPaneSize, snapWidth), - optionsCollapsed: false, - }); - } else { - this.setState({ optionsPaneSize: 0 }); - } - } else if (isSnappedClosed) { - this.setState({ optionsPaneSize: optionsPaneSize }); - } - }; } -export const OPTIONS_PANE_PIXELS_MIN = 300; -export const OPTIONS_PANE_PIXELS_SNAP = 400; -export const OPTIONS_PANE_FLEX_DEFAULT = 0.25; - export function buildPanelEditScene(panel: VizPanel): PanelEditor { const panelClone = panel.clone(); const vizPanelMgr = new VizPanelManager(panelClone); @@ -160,6 +110,5 @@ export function buildPanelEditScene(panel: VizPanel): PanelEditor { panelId: getPanelIdForVizPanel(panel), optionsPane: new PanelOptionsPane({}), vizManager: vizPanelMgr, - optionsPaneSize: OPTIONS_PANE_FLEX_DEFAULT, }); } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx index a464b0a538b..2b81f59779d 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx @@ -1,56 +1,109 @@ -import { css } from '@emotion/css'; -import React, { useMemo } from 'react'; +import { css, cx } from '@emotion/css'; +import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { SceneComponentProps } from '@grafana/scenes'; -import { Splitter, useStyles2 } from '@grafana/ui'; +import { Button, ToolbarButton, useStyles2 } from '@grafana/ui'; import { NavToolbarActions } from '../scene/NavToolbarActions'; import { getDashboardSceneFor } from '../utils/utils'; import { PanelEditor } from './PanelEditor'; +import { useSnappingSplitter } from './splitter/useSnappingSplitter'; export function PanelEditorRenderer({ model }: SceneComponentProps) { const dashboard = getDashboardSceneFor(model); - const { optionsPane, vizManager, dataPane, optionsPaneSize } = model.useState(); - const { controls } = dashboard.useState(); + const { optionsPane } = model.useState(); const styles = useStyles2(getStyles); - const [vizPaneStyles, optionsPaneStyles] = useMemo(() => { - if (optionsPaneSize > 0) { - return [{ flexGrow: 1 - optionsPaneSize }, { minWidth: 'unset', overflow: 'hidden', flexGrow: optionsPaneSize }]; - } else { - return [{ flexGrow: 1 }, { minWidth: 'unset', flexGrow: 0 }]; - } - }, [optionsPaneSize]); + + const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } = + useSnappingSplitter({ + direction: 'row', + dragPosition: 'end', + initialSize: 0.75, + paneOptions: { + collapseBelowPixels: 250, + snapOpenToPixels: 400, + }, + }); return ( <> - -
-
- {controls && } - - - {dataPane && } - -
+
+
+
- {optionsPane && } - +
+
+ {splitterState.collapsed && ( +
+ +
+ )} + {!splitterState.collapsed && } +
+
+ + ); +} + +function VizAndDataPane({ model }: SceneComponentProps) { + const dashboard = getDashboardSceneFor(model); + const { vizManager, dataPane } = model.useState(); + const { controls } = dashboard.useState(); + const styles = useStyles2(getStyles); + + const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } = + useSnappingSplitter({ + direction: 'column', + dragPosition: 'start', + initialSize: 0.5, + paneOptions: { + collapseBelowPixels: 150, + }, + }); + + if (!dataPane) { + primaryProps.style.flexGrow = 1; + } + + return ( + <> + {controls && } +
+
+ +
+ {dataPane && ( + <> +
+
+ {splitterState.collapsed && ( +
+
+ )} + {!splitterState.collapsed && } +
+ + )} +
); } @@ -70,7 +123,7 @@ function getStyles(theme: GrafanaTheme2) { label: 'body', flexGrow: 1, display: 'flex', - position: 'relative', + flexDirection: 'column', minHeight: 0, gap: '8px', }), @@ -81,5 +134,35 @@ function getStyles(theme: GrafanaTheme2) { gap: theme.spacing(1), padding: theme.spacing(2, 0, 2, 2), }), + optionsPane: css({ + flexDirection: 'column', + borderLeft: `1px solid ${theme.colors.border.weak}`, + background: theme.colors.background.primary, + }), + expandOptionsWrapper: css({ + display: 'flex', + flexDirection: 'column', + padding: theme.spacing(2, 1), + }), + expandDataPane: css({ + display: 'flex', + flexDirection: 'row', + padding: theme.spacing(1), + borderTop: `1px solid ${theme.colors.border.weak}`, + borderRight: `1px solid ${theme.colors.border.weak}`, + background: theme.colors.background.primary, + flexGrow: 1, + justifyContent: 'space-around', + }), + rotate180: css({ + rotate: '180deg', + }), + openDataPaneButton: css({ + width: theme.spacing(8), + justifyContent: 'center', + svg: { + rotate: '-90deg', + }, + }), }; } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx index 98d37e26d2b..0678f10734e 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx @@ -39,47 +39,18 @@ export class PanelOptionsPane extends SceneObjectBase { this.setState({ listMode }); }; - onCollapsePane = () => { - const editor = sceneGraph.getAncestor(this, PanelEditor); - editor.toggleOptionsPane(); - }; - static Component = ({ model }: SceneComponentProps) => { const { isVizPickerOpen, searchQuery, listMode } = model.useState(); - const editor = sceneGraph.getAncestor(model, PanelEditor); - const { optionsCollapsed, vizManager } = editor.useState(); + const vizManager = sceneGraph.getAncestor(model, PanelEditor).state.vizManager; const { pluginId } = vizManager.state.panel.useState(); const styles = useStyles2(getStyles); - if (optionsCollapsed) { - return ( -
-
- -
-
- ); - } - return ( -
+ <> {!isVizPickerOpen && ( <>
- + { )} {isVizPickerOpen && } -
+ ); }; } function getStyles(theme: GrafanaTheme2) { return { - pane: css({ - display: 'flex', - flexDirection: 'column', - flexGrow: '1', - borderLeft: `1px solid ${theme.colors.border.weak}`, - background: theme.colors.background.primary, - }), top: css({ display: 'flex', flexDirection: 'column', @@ -137,11 +101,9 @@ function getStyles(theme: GrafanaTheme2) { interface VisualizationButtonProps { pluginId: string; onOpen: () => void; - isOpen?: boolean; - onTogglePane?: () => void; } -export function VisualizationButton({ pluginId, onOpen, isOpen, onTogglePane }: VisualizationButtonProps) { +export function VisualizationButton({ pluginId, onOpen }: VisualizationButtonProps) { const styles = useStyles2(getVizButtonStyles); const pluginMeta = useMemo(() => getAllPanelPluginMeta().filter((p) => p.id === pluginId)[0], [pluginId]); @@ -160,14 +122,6 @@ export function VisualizationButton({ pluginId, onOpen, isOpen, onTogglePane }: > {pluginMeta.name} - {/* */} ); } diff --git a/public/app/features/dashboard-scene/panel-edit/splitter/useSnappingSplitter.ts b/public/app/features/dashboard-scene/panel-edit/splitter/useSnappingSplitter.ts new file mode 100644 index 00000000000..95e78cac99c --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/splitter/useSnappingSplitter.ts @@ -0,0 +1,104 @@ +import React, { useCallback } from 'react'; + +import { DragHandlePosition, useSplitter } from '@grafana/ui'; + +export interface UseSnappingSplitterOptions { + /** + * The initial size of the primary pane between 0-1, defaults to 0.5 + */ + initialSize?: number; + direction: 'row' | 'column'; + dragPosition?: DragHandlePosition; + paneOptions: PaneOptions; +} + +interface PaneOptions { + collapseBelowPixels: number; + snapOpenToPixels?: number; +} + +interface PaneState { + collapsed: boolean; + snapSize?: number; +} + +export function useSnappingSplitter(options: UseSnappingSplitterOptions) { + const { paneOptions } = options; + + const [state, setState] = React.useState({ collapsed: false }); + + const onResizing = useCallback( + (flexSize: number, pixelSize: number) => { + if (flexSize <= 0 && pixelSize <= 0) { + return; + } + + const optionsPixelSize = (pixelSize / flexSize) * (1 - flexSize); + + if (state.collapsed && optionsPixelSize > paneOptions.collapseBelowPixels) { + setState({ collapsed: false }); + } + + if (!state.collapsed && optionsPixelSize < paneOptions.collapseBelowPixels) { + setState({ collapsed: true }); + } + }, + [state, paneOptions.collapseBelowPixels] + ); + + const onSizeChanged = useCallback( + (flexSize: number, pixelSize: number) => { + if (flexSize <= 0 && pixelSize <= 0) { + return; + } + + const newSecondPaneSize = 1 - flexSize; + const isSnappedClosed = state.snapSize === 0; + const sizeOfBothPanes = pixelSize / flexSize; + const snapOpenToPixels = paneOptions.snapOpenToPixels ?? sizeOfBothPanes / 2; + const snapSize = snapOpenToPixels / sizeOfBothPanes; + + if (state.collapsed) { + if (isSnappedClosed) { + setState({ snapSize: Math.max(newSecondPaneSize, snapSize), collapsed: false }); + } else { + setState({ snapSize: 0, collapsed: true }); + } + } else if (isSnappedClosed) { + setState({ snapSize: newSecondPaneSize, collapsed: false }); + } + }, + [state, paneOptions.snapOpenToPixels] + ); + + const onToggleCollapse = useCallback(() => { + setState({ collapsed: !state.collapsed }); + }, [state.collapsed]); + + const { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({ + ...options, + onResizing, + onSizeChanged, + }); + + // This is to allow resizing it beyond the content dimensions + secondaryProps.style.overflow = 'hidden'; + secondaryProps.style.minWidth = 'unset'; + secondaryProps.style.minHeight = 'unset'; + + if (state.snapSize) { + primaryProps.style = { + ...primaryProps.style, + flexGrow: 1 - state.snapSize, + }; + secondaryProps.style.flexGrow = state.snapSize; + } else if (state.snapSize === 0) { + primaryProps.style.flexGrow = 1; + secondaryProps.style.flexGrow = 0; + secondaryProps.style.minWidth = 'unset'; + secondaryProps.style.minHeight = 'unset'; + secondaryProps.style.overflow = 'unset'; + } + + return { containerProps, primaryProps, secondaryProps, splitterProps, splitterState: state, onToggleCollapse }; +}