Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/panel_splitjs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ class Split(SplitBase):
collapsed = param.Integer(default=None, doc="""
Whether the first or second panel is collapsed. 0 for first panel, 1 for second panel, None for not collapsed.""")

collapse_threshold = param.Number(default=5, bounds=(0, None), doc="""
When set to a value > 0, clicking a collapse button will collapse the panel
directly (instead of first snapping to expanded_sizes) when the panel's
current size is within this many percentage points of its expanded size.
Setting to 0 disables the behavior and preserves the two-step
expand-then-collapse interaction.""")

expanded_sizes = Size(default=(50, 50), allow_None=True, length=2, doc="""
The sizes of the two panels when expanded (as percentages).
Default is (50, 50) .
Expand Down
14 changes: 12 additions & 2 deletions src/panel_splitjs/models/split.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@ export function render({ model, el }) {
right_click_count = 0

let new_sizes
if (left_click_count === 1 && model.sizes[1] < model.expanded_sizes[1]) {
const other_collapsed = model.sizes[1] <= COLLAPSED_SIZE
const diff = Math.abs(model.sizes[0] - model.expanded_sizes[0])
const at_expanded = model.collapse_threshold > 0
? diff <= model.collapse_threshold
: diff < 1
if (left_click_count === 1 && (other_collapsed || !at_expanded)) {
new_sizes = model.expanded_sizes
is_collapsed = null
} else {
Expand All @@ -103,7 +108,12 @@ export function render({ model, el }) {
left_click_count = 0

let new_sizes
if (right_click_count === 1 && model.sizes[0] < model.expanded_sizes[0]) {
const other_collapsed = model.sizes[0] <= COLLAPSED_SIZE
const diff = Math.abs(model.sizes[1] - model.expanded_sizes[1])
const at_expanded = model.collapse_threshold > 0
? diff <= model.collapse_threshold
: diff < 1
if (right_click_count === 1 && (other_collapsed || !at_expanded)) {
new_sizes = model.expanded_sizes
is_collapsed = null
} else {
Expand Down
126 changes: 126 additions & 0 deletions tests/test_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,132 @@ def test_multi_split_replace_panel(page):
expect(page.locator(".markdown").nth(1)).to_have_text("MIDDLE")
expect(page.locator(".markdown").last).to_have_text("RIGHT")

@pytest.mark.parametrize('orientation', ['horizontal', 'vertical'])
def test_split_collapse_threshold_near_expanded(page, orientation):
"""When within collapse_threshold of expanded_sizes, a single click collapses directly."""
kwargs = {'width': 400} if orientation == 'horizontal' else {'height': 400}
split = Split(
Button(name='Left'), Button(name='Right'),
orientation=orientation,
sizes=(38, 62),
expanded_sizes=(40, 60),
collapse_threshold=5,
show_buttons=True,
**kwargs
)
serve_component(page, split)

attr = "width" if orientation == "horizontal" else "height"
btn1 = "left" if orientation == "horizontal" else "up"

# sizes=(38, 62): diff from expanded is 2, which is <= threshold 5.
# A single click on the left button should collapse directly (skip expand step).
page.locator(f'.toggle-button-{btn1}').click()
expect(page.locator('.split-panel').first).to_have_attribute('style', f'{attr}: calc(1% - 4px);')
expect(page.locator('.split-panel').last).to_have_attribute('style', f'{attr}: calc(99% - 4px);')
wait_until(lambda: split.collapsed == 0, page)


@pytest.mark.parametrize('orientation', ['horizontal', 'vertical'])
def test_split_collapse_threshold_far_from_expanded(page, orientation):
"""When outside collapse_threshold, first click restores to expanded_sizes."""
kwargs = {'width': 400} if orientation == 'horizontal' else {'height': 400}
split = Split(
Button(name='Left'), Button(name='Right'),
orientation=orientation,
sizes=(25, 75),
expanded_sizes=(40, 60),
collapse_threshold=5,
show_buttons=True,
**kwargs
)
serve_component(page, split)

attr = "width" if orientation == "horizontal" else "height"
btn1 = "left" if orientation == "horizontal" else "up"

# sizes=(25, 75): diff from expanded is 15, which is > threshold 5.
# First click should restore to expanded_sizes (40, 60), not collapse.
page.locator(f'.toggle-button-{btn1}').click()
expect(page.locator('.split-panel').first).to_have_attribute('style', f'{attr}: calc(40% - 4px);')
expect(page.locator('.split-panel').last).to_have_attribute('style', f'{attr}: calc(60% - 4px);')
wait_until(lambda: split.collapsed is None, page)


@pytest.mark.parametrize('orientation', ['horizontal', 'vertical'])
def test_split_no_threshold_restores_when_above_expanded(page, orientation):
"""With collapse_threshold=0, clicking button when sizes differ from expanded
(even above) should restore to expanded_sizes first."""
kwargs = {'width': 400} if orientation == 'horizontal' else {'height': 400}
split = Split(
Button(name='Left'), Button(name='Right'),
orientation=orientation,
sizes=(50, 50),
expanded_sizes=(40, 60),
collapse_threshold=0,
show_buttons=True,
**kwargs
)
serve_component(page, split)

attr = "width" if orientation == "horizontal" else "height"
btn1 = "left" if orientation == "horizontal" else "up"

# sizes=(50, 50) with expanded=(40, 60) and threshold=0.
# diff=10 >= 1, so not at expanded. First click should restore to (40, 60).
page.locator(f'.toggle-button-{btn1}').click()
expect(page.locator('.split-panel').first).to_have_attribute('style', f'{attr}: calc(40% - 4px);')
expect(page.locator('.split-panel').last).to_have_attribute('style', f'{attr}: calc(60% - 4px);')
wait_until(lambda: split.collapsed is None, page)

# Second click at expanded should now collapse.
page.locator(f'.toggle-button-{btn1}').click()
expect(page.locator('.split-panel').first).to_have_attribute('style', f'{attr}: calc(1% - 4px);')
expect(page.locator('.split-panel').last).to_have_attribute('style', f'{attr}: calc(99% - 4px);')
wait_until(lambda: split.collapsed == 0, page)


@pytest.mark.parametrize('orientation', ['horizontal', 'vertical'])
def test_split_button_restores_when_other_panel_collapsed(page, orientation):
"""When one panel is collapsed, clicking its collapse button should restore to expanded_sizes."""
kwargs = {'width': 400} if orientation == 'horizontal' else {'height': 400}
split = Split(
Button(name='Left'), Button(name='Right'),
orientation=orientation,
expanded_sizes=(40, 60),
show_buttons=True,
**kwargs
)
serve_component(page, split)

attr = "width" if orientation == "horizontal" else "height"
btn1 = "left" if orientation == "horizontal" else "up"
btn2 = "right" if orientation == "horizontal" else "down"

# Collapse the right panel via right button (two clicks: expand then collapse)
page.locator(f'.toggle-button-{btn2}').click()
page.locator(f'.toggle-button-{btn2}').click()
wait_until(lambda: split.collapsed == 1, page)

# Now left panel is ~100%, right is ~0%.
# Clicking left '<' should restore to expanded_sizes, NOT collapse the left panel.
page.locator(f'.toggle-button-{btn1}').click()
expect(page.locator('.split-panel').first).to_have_attribute('style', f'{attr}: calc(40% - 4px);')
expect(page.locator('.split-panel').last).to_have_attribute('style', f'{attr}: calc(60% - 4px);')
wait_until(lambda: split.collapsed is None, page)

# Now do the mirror: collapse left panel
page.locator(f'.toggle-button-{btn1}').click()
wait_until(lambda: split.collapsed == 0, page)

# Right panel is ~100%, left is ~0%.
# Clicking right '>' should restore to expanded_sizes, NOT collapse the right panel.
page.locator(f'.toggle-button-{btn2}').click()
expect(page.locator('.split-panel').first).to_have_attribute('style', f'{attr}: calc(40% - 4px);')
expect(page.locator('.split-panel').last).to_have_attribute('style', f'{attr}: calc(60% - 4px);')
wait_until(lambda: split.collapsed is None, page)


def test_multi_split_append_panel(page):
split = MultiSplit(Markdown("LEFT"), Markdown("MIDDLE"), Markdown("RIGHT"))
serve_component(page, split)
Expand Down
Loading