diff --git a/src/panel_splitjs/base.py b/src/panel_splitjs/base.py index de9bc83..5f56156 100644 --- a/src/panel_splitjs/base.py +++ b/src/panel_splitjs/base.py @@ -33,7 +33,7 @@ def _validate(self, val): return if self.length is not None and isinstance(val, tuple) and len(val) != self.length: raise ValueError(f"Size parameter {self.name!r} must have length {self.length}") - if not (isinstance(val, (int, float)) or (isinstance(val, tuple) and all(isinstance(v, (int, float)) for v in val))): + if not (isinstance(val, (int, float)) or (isinstance(val, tuple) and all(isinstance(v, (int, float)) or v is None for v in val))): raise ValueError(f"Size parameter {self.name!r} only takes int or float values") diff --git a/src/panel_splitjs/dist/css/splitjs.css b/src/panel_splitjs/dist/css/splitjs.css index cbd5171..6e1cbd5 100644 --- a/src/panel_splitjs/dist/css/splitjs.css +++ b/src/panel_splitjs/dist/css/splitjs.css @@ -39,10 +39,6 @@ width: 100%; } -/* Ensure buttons stay visible even when panels are collapsed */ -.split.single-split > div:first-child { - min-width: 0 !important; /* Override any minimum width */ -} .split.single-split > div:nth-child(2) { overflow: visible !important; /* Ensure buttons remain visible */ diff --git a/src/panel_splitjs/models/split.js b/src/panel_splitjs/models/split.js index 3edcc57..64e2b52 100644 --- a/src/panel_splitjs/models/split.js +++ b/src/panel_splitjs/models/split.js @@ -7,10 +7,32 @@ export function render({ model, el }) { split_div.className = `split single-split ${model.orientation}` split_div.classList.add("loading") + const [left_min, right_min] = Array.isArray(model.min_size) ? model.min_size : [model.min_size, model.min_size] + + if (model.orientation === "horizontal") { + split_div.style.minWidth = `${left_min + right_min + 8}px` + } else { + split_div.style.minHeight = `${left_min + right_min + 8}px` + } + const split0 = document.createElement("div") split0.className = "split-panel" + if (left_min) { + if (model.orientation === "horizontal") { + split0.style.minWidth = `${left_min}px` + } else { + split0.style.minHeight = `${left_min}px` + } + } const split1 = document.createElement("div") split1.className = "split-panel" + if (right_min) { + if (model.orientation === "horizontal") { + split1.style.minWidth = `${right_min}px` + } else { + split1.style.minHeight = `${right_min}px` + } + } split_div.append(split0, split1) const left_content_wrapper = document.createElement("div") @@ -63,7 +85,6 @@ export function render({ model, el }) { new_sizes = [0, 100] left_click_count = 0 } - model.collapsed = is_collapsed sync_ui(new_sizes, true) }) @@ -80,7 +101,6 @@ export function render({ model, el }) { new_sizes = [100, 0] right_click_count = 0 } - model.collapsed = is_collapsed sync_ui(new_sizes, true) }) } @@ -123,8 +143,8 @@ export function render({ model, el }) { }) function sync_ui(sizes = null, resize = false) { - const left_panel_hidden = sizes ? sizes[0] <= COLLAPSED_SIZE : false - const right_panel_hidden = sizes ? sizes[1] <= COLLAPSED_SIZE : false + const left_panel_hidden = sizes ? ((sizes[0] <= COLLAPSED_SIZE) && (left_min < COLLAPSED_SIZE)) : false + const right_panel_hidden = sizes ? ((sizes[1] <= COLLAPSED_SIZE) && (right_min < COLLAPSED_SIZE)) : false let [ls, rs] = sizes if (right_panel_hidden) { @@ -143,8 +163,8 @@ export function render({ model, el }) { if (resize) { split_instance.setSizes([ls, rs]) sizes = [ls, rs] - model.sizes = [ls, rs] window.dispatchEvent(new Event('resize')) + requestAnimationFrame(() => { model.sizes = split_instance.getSizes() }) } } @@ -153,6 +173,7 @@ export function render({ model, el }) { return } sizes = model.sizes + model.collapsed = (1-sizes[0]) >= 0 ? 0 : (1-sizes[1]) >= 0 ? 1 : null sync_ui(sizes, true) }) diff --git a/tests/test_ui.py b/tests/test_ui.py index e996705..7cd222b 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -3,6 +3,7 @@ pytest.importorskip('playwright') from panel.tests.util import serve_component, wait_until +from panel.pane import Markdown from panel.widgets import Button from panel_splitjs import MultiSplit, Split from playwright.sync_api import expect @@ -26,6 +27,69 @@ def test_split(page, orientation): expect(page.locator('.split-panel').last).to_have_attribute('style', f'{attr}: calc(50% - 4px);') +@pytest.mark.parametrize('orientation', ['horizontal', 'vertical']) +def test_split_min_size_and_total_width(page, orientation): + split = Split( + Markdown("## Left Panel\nContent here", width=150, margin=25), + Markdown("## Right Panel\nMore content", width=150, margin=25), + sizes=(50, 50), # Equal sizing initially + min_size=300, # Minimum 300px for each panel + show_buttons=True, + orientation=orientation + ) + serve_component(page, split) + + # Check total width/height is 608px + attr = "width" if orientation == "horizontal" else "height" + expect(page.locator(".single-split")).to_have_attribute("style", f"min-{attr}: 608px;") + + # Collapse left panel using the left button + left_button = page.locator(".toggle-button-left,.toggle-button-up").first + left_button.click() + # Wait to ensure UI has updated; the left panel should not collapse below min_size=300px, + # so after collapse, its width/height should be 300px. + expect(page.locator(".split-panel").first).to_have_css(attr, "300px") + + # Collapse right panel using the right button + right_button = page.locator(".toggle-button-right,.toggle-button-down").first + right_button.click() + # Wait, then check right does not collapse below 300px + expect(page.locator(".split-panel").last).to_have_css(attr, "300px") + +@pytest.mark.parametrize('orientation', ['horizontal', 'vertical']) +def test_split_min_size_one_side(page, orientation): + # Only first panel has a min_size of 300px, second is unconstrained + split = Split( + Markdown("## Left", width=150), + Markdown("## Right", width=150), + sizes=(50, 50), + min_size=(300, None), + show_buttons=True, + orientation=orientation, + width=600 if orientation == "horizontal" else None, + height=600 if orientation == "vertical" else None + ) + serve_component(page, split) + + attr = "width" if orientation == "horizontal" else "height" + # The minimum size style is still 308px (because None becomes 0, so 300+0+8) + expect(page.locator(".single-split")).to_have_attribute("style", f"min-{attr}: 308px;") + + # Collapse left panel -- it should not collapse below 300px + left_button = page.locator(".toggle-button-left,.toggle-button-up").first + left_button.click() + expect(page.locator(".split-panel").first).to_have_css(attr, "300px") + + # Collapse right panel -- right can collapse to minimum, which is 5px (default collapsed size) + right_button = page.locator(".toggle-button-right,.toggle-button-down").first + right_button.click() + # The right panel should be collapsed to zero + expect(page.locator(".split-panel").last).to_have_css(attr, "0px") + + # The left panel still stays at its min_size + expect(page.locator(".split-panel").first).to_have_css(attr, "592px") + + def test_split_sizes(page): split = Split(Button(name='Left'), Button(name='Right'), sizes=(40, 60), width=400) serve_component(page, split) @@ -64,13 +128,13 @@ def test_split_collapsed_programmatically(page): split.collapsed = 0 expect(page.locator('.split-panel').first).to_have_attribute('style', 'width: calc(1% - 4px);') expect(page.locator('.split-panel').last).to_have_attribute('style', 'width: calc(99% - 4px);') - wait_until(lambda: split.sizes == (0, 100), page) + wait_until(lambda: split.sizes == (1, 99), page) wait_until(lambda: split.collapsed == 0, page) split.collapsed = 1 expect(page.locator('.split-panel').first).to_have_attribute('style', 'width: calc(99% - 4px);') expect(page.locator('.split-panel').last).to_have_attribute('style', 'width: calc(1% - 4px);') - wait_until(lambda: split.sizes == (100, 0), page) + wait_until(lambda: split.sizes == (99, 1), page) wait_until(lambda: split.collapsed == 1, page) split.collapsed = None @@ -104,7 +168,7 @@ def test_split_click_toggle_button(page, orientation): 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.sizes == (0, 100), page) + wait_until(lambda: split.sizes == (1, 99), page) wait_until(lambda: split.collapsed == 0, page) page.locator(f'.toggle-button-{btn2}').click() @@ -116,7 +180,7 @@ def test_split_click_toggle_button(page, orientation): page.locator(f'.toggle-button-{btn2}').click() expect(page.locator('.split-panel').first).to_have_attribute('style', f'{attr}: calc(99% - 4px);') expect(page.locator('.split-panel').last).to_have_attribute('style', f'{attr}: calc(1% - 4px);') - wait_until(lambda: split.sizes == (100, 0), page) + wait_until(lambda: split.sizes == (99, 1), page) wait_until(lambda: split.collapsed == 1, page)