Skip to content

Commit 0317607

Browse files
authored
Update card collapsed state handling to change child view model visibility attribute (#8274)
1 parent de8cfbf commit 0317607

5 files changed

Lines changed: 126 additions & 19 deletions

File tree

panel/models/card.ts

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {Container} from "@bokehjs/core/layout/grid"
44
import type * as p from "@bokehjs/core/properties"
55
import {GridAlignmentLayout} from "@bokehjs/models/layouts/alignments"
66
import {LayoutDOMView} from "@bokehjs/models/layouts/layout_dom"
7+
import type {UIElementView} from "@bokehjs/models/ui/ui_element"
78

89
import {Column, ColumnView} from "./column"
910

@@ -22,6 +23,9 @@ export class CardView extends ColumnView {
2223

2324
button_el: HTMLButtonElement
2425
header_el: HTMLElement
26+
visible_child_views: Map<UIElementView, boolean> = new Map()
27+
protected _updating_child_visibility: Set<UIElementView> = new Set()
28+
protected _child_visible_callbacks: Map<UIElementView, () => void> = new Map()
2529

2630
readonly collapsed_style = new DOM.InlineStyleSheet()
2731

@@ -40,6 +44,10 @@ export class CardView extends ColumnView {
4044
this.child_views[0].el.style.backgroundColor = header_background
4145
this.header_el.style.backgroundColor = header_background
4246
})
47+
48+
for (const child_view of this.child_views.slice(1)) {
49+
this._register_child_view(child_view)
50+
}
4351
}
4452

4553
override stylesheets(): StyleSheetLike[] {
@@ -106,11 +114,9 @@ export class CardView extends ColumnView {
106114
header.r_after_render()
107115
}
108116

109-
if (this.model.collapsed) {
110-
return
111-
}
112-
113117
for (const child_view of this.child_views.slice(1)) {
118+
this._register_child_view(child_view)
119+
this._apply_child_visible(child_view)
114120
this.shadow_el.appendChild(child_view.el)
115121
child_view.render()
116122
child_view.r_after_render()
@@ -119,6 +125,15 @@ export class CardView extends ColumnView {
119125

120126
override async update_children(): Promise<void> {
121127
await this.build_child_views()
128+
const child_views = new Set(this.child_views.slice(1))
129+
for (const child_view of this.visible_child_views.keys()) {
130+
if (!child_views.has(child_view)) {
131+
this._unregister_child_view(child_view)
132+
}
133+
}
134+
for (const child_view of child_views) {
135+
this._register_child_view(child_view)
136+
}
122137
this.render()
123138
this.invalidate_layout()
124139
}
@@ -210,14 +225,15 @@ export class CardView extends ColumnView {
210225

211226
_collapse(): void {
212227
for (const child_view of this.child_views.slice(1)) {
228+
this._register_child_view(child_view)
213229
if (this.model.collapsed) {
214230
this.shadow_el.removeChild(child_view.el)
215-
child_view.model.visible = false
231+
this._set_child_visible(child_view, false)
216232
} else {
217233
child_view.render()
218234
child_view.after_render()
219235
this.shadow_el.appendChild(child_view.el)
220-
child_view.model.visible = true
236+
this._apply_child_visible(child_view)
221237
}
222238
}
223239
if (this.model.collapsed) {
@@ -232,6 +248,54 @@ export class CardView extends ColumnView {
232248
this.invalidate_layout()
233249
}
234250

251+
protected _set_child_visible(child_view: UIElementView, visible: boolean): void {
252+
if (child_view.model.visible == visible) {
253+
return
254+
}
255+
this._updating_child_visibility.add(child_view)
256+
try {
257+
child_view.model.visible = visible
258+
} finally {
259+
this._updating_child_visibility.delete(child_view)
260+
}
261+
}
262+
263+
protected _apply_child_visible(child_view: UIElementView): void {
264+
const desired_visible = this.visible_child_views.get(child_view) ?? child_view.model.visible
265+
this.visible_child_views.set(child_view, desired_visible)
266+
this._set_child_visible(child_view, desired_visible && !this.model.collapsed)
267+
}
268+
269+
protected _register_child_view(child_view: UIElementView): void {
270+
if (this.visible_child_views.has(child_view)) {
271+
return
272+
}
273+
this.visible_child_views.set(child_view, child_view.model.visible)
274+
const {visible} = child_view.model.properties
275+
const callback = () => {
276+
if (this._updating_child_visibility.has(child_view)) {
277+
return
278+
}
279+
const desired_visible = child_view.model.visible
280+
this.visible_child_views.set(child_view, desired_visible)
281+
if (this.model.collapsed && desired_visible) {
282+
this._set_child_visible(child_view, false)
283+
}
284+
}
285+
this._child_visible_callbacks.set(child_view, callback)
286+
this.on_change(visible, callback)
287+
}
288+
289+
protected _unregister_child_view(child_view: UIElementView): void {
290+
const callback = this._child_visible_callbacks.get(child_view)
291+
if (callback != null) {
292+
child_view.model.properties.visible.change.disconnect(callback)
293+
this._child_visible_callbacks.delete(child_view)
294+
}
295+
this.visible_child_views.delete(child_view)
296+
this._updating_child_visibility.delete(child_view)
297+
}
298+
235299
protected override _create_element(): HTMLElement {
236300
return DOM.create_element((this.model.tag as any), {class: this.css_classes()})
237301
}

panel/tests/test_server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,7 @@ def cb(event):
839839
wait_until(lambda: bool(called))
840840

841841

842+
@pytest.mark.flaky(max_runs=3)
842843
def test_server_thread_pool_change_event(server_implementation, threads):
843844
button = Button(name='Click')
844845
button2 = Button(name='Click')

panel/tests/ui/layout/test_accordion.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ def accordion_components():
1919
return d0, d1
2020

2121

22-
def is_collapsed(card_object, card_content):
22+
def is_collapsed(card_object, card_content, page):
2323
expect(card_object.locator('svg')).to_have_class("icon icon-tabler icons-tabler-outline icon-tabler-chevron-right")
24-
expect(card_object).not_to_contain_text(card_content)
24+
expect(page.get_by_text(card_content)).to_be_hidden()
2525
return True
2626

2727

@@ -51,8 +51,8 @@ def test_accordion_default(page, accordion_components):
5151
expect(d1_object).to_contain_text(d1.name)
5252

5353
# cards are collapsed and their content is hidden by default
54-
assert is_collapsed(card_object=d0_object, card_content=d0.text)
55-
assert is_collapsed(card_object=d1_object, card_content=d1.text)
54+
assert is_collapsed(card_object=d0_object, card_content=d0.text, page=page)
55+
assert is_collapsed(card_object=d1_object, card_content=d1.text, page=page)
5656

5757
# cards can be expanded simultaneously
5858
d0_object.click()
@@ -94,7 +94,7 @@ def test_accordion_active(page, accordion_components):
9494
# second card is collapsed and its content is hidden
9595
d1_object = accordion_elements.nth(1)
9696
d1_object.wait_for()
97-
assert is_collapsed(card_object=d1_object, card_content=d1.text)
97+
assert is_collapsed(card_object=d1_object, card_content=d1.text, page=page)
9898

9999

100100
def test_accordion_objects(page, accordion_components):
@@ -127,11 +127,11 @@ def test_accordion_toggle(page, accordion_components):
127127
# click to expand the first card, 2nd card is collapsed
128128
d0_object.click()
129129
assert is_expanded(card_object=d0_object, card_content=d0.text)
130-
assert is_collapsed(card_object=d1_object, card_content=d1.text)
130+
assert is_collapsed(card_object=d1_object, card_content=d1.text, page=page)
131131

132132
# click to expand the 2nd card, 1st card is collapsed
133133
d1_object.click()
134-
assert is_collapsed(card_object=d0_object, card_content=d0.text)
134+
assert is_collapsed(card_object=d0_object, card_content=d0.text, page=page)
135135
assert is_expanded(card_object=d1_object, card_content=d1.text)
136136

137137

@@ -150,15 +150,15 @@ def test_accordion_append(page, accordion_components):
150150
expect(accordion_elements).to_have_count(1)
151151
# collapsed by default
152152
d0_object = accordion_elements.nth(0)
153-
assert is_collapsed(card_object=d0_object, card_content=d0.text)
153+
assert is_collapsed(card_object=d0_object, card_content=d0.text, page=page)
154154

155155
# add new element d1
156156
accordion.append(d1)
157157
expect(accordion_elements).to_have_count(2)
158158
d1_object = accordion_elements.nth(1)
159159
# both cards are collapsed
160-
assert is_collapsed(card_object=d0_object, card_content=d0.text)
161-
assert is_collapsed(card_object=d1_object, card_content=d1.text)
160+
assert is_collapsed(card_object=d0_object, card_content=d0.text, page=page)
161+
assert is_collapsed(card_object=d1_object, card_content=d1.text, page=page)
162162

163163

164164
def test_accordion_extend(page, accordion_components):
@@ -179,9 +179,9 @@ def test_accordion_extend(page, accordion_components):
179179
d0_object = accordion_elements.nth(0)
180180
d1_object = accordion_elements.nth(1)
181181
d2_object = accordion_elements.nth(2)
182-
assert is_collapsed(card_object=d0_object, card_content=d0.text)
183-
assert is_collapsed(card_object=d1_object, card_content=d1.text)
184-
assert is_collapsed(card_object=d2_object, card_content=d2.text)
182+
assert is_collapsed(card_object=d0_object, card_content=d0.text, page=page)
183+
assert is_collapsed(card_object=d1_object, card_content=d1.text, page=page)
184+
assert is_collapsed(card_object=d2_object, card_content=d2.text, page=page)
185185

186186

187187
def test_accordion_clear(page, accordion_components):

panel/tests/ui/layout/test_card.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,44 @@ def test_card_widget_not_collapsed(page, card_components):
201201

202202
wait_until(lambda: w2.value == 'F', page)
203203
assert not card.collapsed
204+
205+
206+
def test_card_child_visible(page, card_components):
207+
w1, w2 = card_components
208+
w1.visible = False
209+
card = Card(w1, title="Foo", collapsed=True)
210+
211+
serve_component(page, card)
212+
213+
slider = page.locator('.bk-Slider')
214+
expect(slider).not_to_be_visible()
215+
216+
w1.visible = True
217+
218+
expect(slider).not_to_be_visible()
219+
220+
card.collapsed = False
221+
222+
expect(slider).to_be_visible()
223+
224+
card.collapsed = True
225+
226+
# Ensure newly added component respects visibility
227+
card.append(w2)
228+
229+
text_input = page.locator(".class_w2")
230+
231+
expect(text_input).not_to_be_visible()
232+
233+
# Ensure opening card renders visible component
234+
card.collapsed = False
235+
expect(text_input).to_be_visible()
236+
237+
# Ensure setting visible restores visibility
238+
w2.visible = False
239+
expect(text_input).not_to_be_visible()
240+
241+
# Ensure toggling collapsed doesn't override visible state
242+
card.collapsed = True
243+
card.collapsed = False
244+
expect(text_input).not_to_be_visible()

panel/tests/ui/widgets/test_texteditor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def test_texteditor_enter_value(page):
6262
wait_until(lambda: widget.value == '<p>test</p>', page)
6363

6464

65+
@pytest.mark.flaky(max_runs=3)
6566
def test_texteditor_regression_copy_paste(page, browser):
6667
# https://github.com/holoviz/panel/issues/5545
6768
widget = TextEditor()

0 commit comments

Comments
 (0)