bvaughn/react-resizable-panels

Feature Request: Greedy Panels

BrodyRas opened this issue ยท 14 comments

Problem

I'm creating a list of collapsible hierarchies (similar to the left panel in VS Code), and this package is working well for it! There is one slight issue in functionality, which I want to discuss:

Seemingly, the "preference" for which panel takes up the remaining space when a panel collapsed is the next panel in order i.e. if <Panel order=3> collapses, then <Panel order=4> takes the remaining space. I'm wondering if there's a way to customize this behavior. Giving preference to the previous panel, instead of the next one, would provide the functionality I'm looking for: collapsing a panel shoves it to the bottom of the PanelGroup.

I tried to do some trickery to make this work (i.e. giving the PanelGroup flexDirection: column-reverse, reversing the order values, etc), but that didn't quite work (reversed the dragging direction ๐Ÿ˜…). If there's a quick fix I'm missing, I'll be happy to learn what it is โค๏ธ Thanks for the help!

Feature Request

Create an option for PanelGroups which can reverse the direction of preferred panels on collapse/expansion, or provide the Panels a "greediness" option, which controls how much of the new space they take

Current Behavior

Collapsing a panel "pulls up" the panel below it, giving it's space to the next panel.
current

Desired Behavior

Collapsing a panel "pushes" it down, giving it's space to the previous panel.
desired

Collapsing a panel "pushes" it down, giving it's space to the previous panel.

Seems like the video you showed from VS Code should already be possible using panelRef.collapse?

I am using panelRef.collapse, it works great! But the issue is how the remaining space is filled. VS Code "pushes down", giving space to the panel above. This package seems to "pull up", giving space to the panel below.

Watch the first GIF, while I'm toggling the "Tab Hierarchy" panel: I'd expect that tab to go down to the bottom, but instead it pulls up the "Third Hierarchy" tab.

Ah, I see what you're saying.

Have you consider using panelGroup.setLayout() for this? I think that would let you do what you're describing.

Oh, that makes sense! On click, I'll add the current panel's percentage to the previous panel's percentage, and zero out the current panel. Thanks!

Yup! :)

Sorry to reopen, I think the layout is working, but for some reason, it seems like the panels are 1 render behind after using setLayout(). Is there another step to trigger the render?

See the video: clicking on the "Tab Hierarchy" calls setLayout() with the updated layout (as described in the previous comment). But nothing happens visually. Then, when I attempt to resize the panel, it jumps to where it should've been: 0.

lateRender

Huh. Can you share a repro?

Here's a reproduction with the simplest case:

const DummyPanels = () => {
    const groupRef = useRef(null)

    // Give the clicked panel's percentage to the previous panel
    const handleClick = (order: number) => {
        const anyGroupRef = groupRef as any
        const layout = anyGroupRef.current.getLayout()
        const currentSize = layout[order]
        layout[order] = 0
        layout[order - 1] += currentSize
        anyGroupRef.current.setLayout(layout)
    }

    return <PanelGroup ref={groupRef} direction='vertical'>
        <Panel order={0} collapsible><></></Panel>
        <CollapseOrDragHandle title='First Panel' onMouseDown={() => { handleClick(1) }} />
        <Panel order={1} collapsible>First Panel's content</Panel>
        <CollapseOrDragHandle title='Second Panel' onMouseDown={() => { handleClick(2) }} />
        <Panel order={2} collapsible>Second Panel's content</Panel>
        <CollapseOrDragHandle title='Third Panel' onMouseDown={() => { handleClick(3) }} />
        <Panel order={3} collapsible>Third Panel's content</Panel>
    </PanelGroup>
}

interface CollapseOrDragHandleProps {
    title: string
    onMouseDown: any
}

const CollapseOrDragHandle = ({ title, onMouseDown }: CollapseOrDragHandleProps) => {
    return <>
        {/* Drag to Resize */}
        <PanelResizeHandle>
            <Box sx={{ height: '10px', backgroundColor: 'gray' }} />
        </PanelResizeHandle>
        {/* Click to Collapse */}
        <Box sx={{ backgroundColor: 'gray', cursor: 'pointer' }} onMouseDown={onMouseDown}>{title}</Box>
    </>
}

The "handle" is two Boxes: dragging the top will resize the panel, and clicking the bottom will collapse the panel. Notice how clicking will technically trigger a new layout, but you don't see it until making another change (i.e. dragging that panel, or another panel)

repro

If there's a better way to structure the clickable handles w/ title text, I'm happy to hear it too โค๏ธ should the clickable title be a part of the panel itself, and the resize handle only manages resizing?

@BrodyRas setLayout does an equality check see

if (!areEqual(prevLayout, safeLayout)) {

So I believe you might need to pass a new array to setLayout ie

let nextLayout = [...group.getLayout()];
// modify nextLayout
nextLayout[0] = 20;
group.setLayout(nextLayout);

Yeah, mutating state is always a good idea to avoid because of equality checks like this. Should work okay if you pass it a new layout array!

D'oh, that makes sense. Silly mistake, I definitely thought I was just grabbing the values of the array, not mutating the group's state ๐Ÿ˜…

That fixed it ๐Ÿ‘ Thanks so much for the help!

Glad you got it sorted out!