From 1988ac7e9f07560fb3b1c294c95d31257d162e1f Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 4 Jul 2023 21:47:14 -0400 Subject: [PATCH 01/31] Add scroll log --- panel/__init__.py | 3 +- panel/dist/css/scrolllog.css | 31 +++++++++++ panel/layout/__init__.py | 3 ++ panel/layout/scrolllog.py | 101 +++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 panel/dist/css/scrolllog.css create mode 100644 panel/layout/scrolllog.py diff --git a/panel/__init__.py b/panel/__init__.py index 500b77be5d..157e065263 100644 --- a/panel/__init__.py +++ b/panel/__init__.py @@ -61,7 +61,7 @@ ) from .layout import ( # noqa Accordion, Card, Column, FlexBox, FloatPanel, GridBox, GridSpec, GridStack, - HSpacer, Row, Spacer, Swipe, Tabs, VSpacer, WidgetBox, + HSpacer, Row, ScrollLog, Spacer, Swipe, Tabs, VSpacer, WidgetBox, ) from .pane import panel # noqa from .param import Param # noqa @@ -82,6 +82,7 @@ "Param", "Row", "Spacer", + "ScrollLog", "Tabs", "Template", "VSpacer", diff --git a/panel/dist/css/scrolllog.css b/panel/dist/css/scrolllog.css new file mode 100644 index 0000000000..5082e7363f --- /dev/null +++ b/panel/dist/css/scrolllog.css @@ -0,0 +1,31 @@ +.scroll-log { + overflow-y: scroll; +} + +.scroll-down-arrow { + /* For location */ + position: absolute; + bottom: 50; + left: 95%; + transform: translate(0%, -105%); + /* For icon */ + cursor: pointer; + visibility: hidden; + font-size: 18px; + border-radius: 50%; + background-color: rgba(0, 0, 0, 0.25); + color: white; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + /* For animation */ + opacity: 0; + transition: visibility 0s, opacity 0.2s ease-in-out; +} + +.visible { + visibility: visible; + opacity: 1; +} diff --git a/panel/layout/__init__.py b/panel/layout/__init__.py index 17464dc8ac..b72dc4dcb8 100644 --- a/panel/layout/__init__.py +++ b/panel/layout/__init__.py @@ -37,6 +37,7 @@ from .float import FloatPanel # noqa from .grid import GridBox, GridSpec # noqa from .gridstack import GridStack # noqa +from .scrolllog import ScrollLog # noqa from .spacer import ( # noqa Divider, HSpacer, Spacer, VSpacer, ) @@ -58,7 +59,9 @@ "ListPanel", "Panel", "Row", + "ScrollLog", "Spacer", + "Swipe", "Tabs", "VSpacer", "WidgetBox" diff --git a/panel/layout/scrolllog.py b/panel/layout/scrolllog.py new file mode 100644 index 0000000000..e19746d244 --- /dev/null +++ b/panel/layout/scrolllog.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +from typing import ClassVar, List + +import param + +from ..layout.base import ListLike +from ..reactive import ReactiveHTML + + +class ScrollLog(ListLike, ReactiveHTML): + objects = param.List( + default=[], + doc=""" + The list of child objects that make up the layout.""", + precedence=-1, + ) + + height = param.Integer( + default=250, + doc=""" + The height of the scrollable area in pixels.""", + ) + + _stylesheets: ClassVar[List[str]] = [ + """ + .scroll-log { + overflow-y: scroll; + } + + .scroll-down-arrow { + /* For location */ + position: absolute; + bottom: 0%; + left: 50%; + transform: translate(0%, 0%); + /* For icon */ + cursor: pointer; + visibility: hidden; + font-size: 18px; + border-radius: 50%; + background-color: rgba(0, 0, 0, 0.25); + color: white; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + /* For animation */ + opacity: 0; + transition: visibility 0s, opacity 0.2s ease-in-out; + } + + .visible { + visibility: visible; + opacity: 1; + } + """ + ] + + _template = """ +
+
+ {% for obj in objects %} +
+
${obj}
+
+ {% endfor %} +
+ """ + + _scripts = { + "render": """ + scrollDownArrow.addEventListener("click", () => { + self.scroll_to_bottom(); + }); + + var scrollThreshold = 20; + scrollLog.addEventListener("scroll", () => { + var scrollDistanceFromBottom = ( + scrollLog.scrollHeight - scrollLog.scrollTop - scrollLog.clientHeight + ); + if (scrollDistanceFromBottom < scrollThreshold) { + scrollDownArrow.classList.remove("visible"); + } else { + scrollDownArrow.classList.add("visible"); + } + }); + """, + "after_layout": """ + self.scroll_to_bottom(); + """, + "scroll_to_bottom": """ + scrollLog.scrollTop = scrollLog.scrollHeight; + """ + } + + def __init__(self, *objects, **params): + if "sizing_mode" not in params: + params["sizing_mode"] = "stretch_width" + super().__init__(objects=list(objects), **params) From 80b46e85d7f3071d90f2b493af779bcd4e1b568a Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Sun, 9 Jul 2023 14:49:24 -0400 Subject: [PATCH 02/31] Attempt to convert to Typescript --- panel/dist/css/scrolllog.css | 6 +-- panel/layout/scrolllog.py | 99 +++++------------------------------- panel/models/__init__.py | 2 +- panel/models/layout.py | 6 +++ panel/models/scroll_log.ts | 67 ++++++++++++++++++++++++ 5 files changed, 91 insertions(+), 89 deletions(-) create mode 100644 panel/models/scroll_log.ts diff --git a/panel/dist/css/scrolllog.css b/panel/dist/css/scrolllog.css index 5082e7363f..5d3a347976 100644 --- a/panel/dist/css/scrolllog.css +++ b/panel/dist/css/scrolllog.css @@ -5,9 +5,9 @@ .scroll-down-arrow { /* For location */ position: absolute; - bottom: 50; - left: 95%; - transform: translate(0%, -105%); + bottom: 0%; + left: 50%; + transform: translate(0%, 0%); /* For icon */ cursor: pointer; visibility: hidden; diff --git a/panel/layout/scrolllog.py b/panel/layout/scrolllog.py index e19746d244..d3b71bff5e 100644 --- a/panel/layout/scrolllog.py +++ b/panel/layout/scrolllog.py @@ -1,99 +1,28 @@ from __future__ import annotations -from typing import ClassVar, List +from typing import ( + TYPE_CHECKING, ClassVar, List, Type, +) -import param +from ..io.resources import CDN_DIST +from ..layout.base import Column +from ..models import ScrollLog as BkScrollLog -from ..layout.base import ListLike -from ..reactive import ReactiveHTML +if TYPE_CHECKING: + from bokeh.model import Model -class ScrollLog(ListLike, ReactiveHTML): - objects = param.List( - default=[], - doc=""" - The list of child objects that make up the layout.""", - precedence=-1, - ) - - height = param.Integer( - default=250, - doc=""" - The height of the scrollable area in pixels.""", - ) - - _stylesheets: ClassVar[List[str]] = [ - """ - .scroll-log { - overflow-y: scroll; - } - - .scroll-down-arrow { - /* For location */ - position: absolute; - bottom: 0%; - left: 50%; - transform: translate(0%, 0%); - /* For icon */ - cursor: pointer; - visibility: hidden; - font-size: 18px; - border-radius: 50%; - background-color: rgba(0, 0, 0, 0.25); - color: white; - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - /* For animation */ - opacity: 0; - transition: visibility 0s, opacity 0.2s ease-in-out; - } - - .visible { - visibility: visible; - opacity: 1; - } +class ScrollLog(Column): """ - ] + A `ScrollLog` layout allows arranging multiple panel objects in a + scrollable, vertical container with a header bar. - _template = """ -
-
- {% for obj in objects %} -
-
${obj}
-
- {% endfor %} -
+ Reference: https://panel.holoviz.org/reference/layouts/ScrollLog.html """ - _scripts = { - "render": """ - scrollDownArrow.addEventListener("click", () => { - self.scroll_to_bottom(); - }); + _bokeh_model: ClassVar[Type[Model]] = BkScrollLog - var scrollThreshold = 20; - scrollLog.addEventListener("scroll", () => { - var scrollDistanceFromBottom = ( - scrollLog.scrollHeight - scrollLog.scrollTop - scrollLog.clientHeight - ); - if (scrollDistanceFromBottom < scrollThreshold) { - scrollDownArrow.classList.remove("visible"); - } else { - scrollDownArrow.classList.add("visible"); - } - }); - """, - "after_layout": """ - self.scroll_to_bottom(); - """, - "scroll_to_bottom": """ - scrollLog.scrollTop = scrollLog.scrollHeight; - """ - } + _stylesheets: ClassVar[List[str]] = [f"{CDN_DIST}/layout/scrolllog.css"] def __init__(self, *objects, **params): if "sizing_mode" not in params: diff --git a/panel/models/__init__.py b/panel/models/__init__.py index 2966af014e..7e0dc09559 100644 --- a/panel/models/__init__.py +++ b/panel/models/__init__.py @@ -7,7 +7,7 @@ from .datetime_picker import DatetimePicker # noqa from .ipywidget import IPyWidget # noqa -from .layout import Card # noqa +from .layout import Card, ScrollLog # noqa from .location import Location # noqa from .markup import HTML, JSON, PDF # noqa from .reactive_html import ReactiveHTML # noqa diff --git a/panel/models/layout.py b/panel/models/layout.py index 14755e0ac3..4f0e265b6f 100644 --- a/panel/models/layout.py +++ b/panel/models/layout.py @@ -7,6 +7,7 @@ __all__ = ( "Card", "HTMLBox", + "ScrollLog", ) class HTMLBox(LayoutDOM): @@ -33,3 +34,8 @@ class Card(Column): hide_header = Bool(False, help="Whether to hide the Card header") tag = String('tag', help="CSS class to use for the Card as a whole.") + + +class ScrollLog(Column): + + autoscroll = Bool(True, help="Whether to scroll to the bottom on update.") diff --git a/panel/models/scroll_log.ts b/panel/models/scroll_log.ts new file mode 100644 index 0000000000..b3e7851d93 --- /dev/null +++ b/panel/models/scroll_log.ts @@ -0,0 +1,67 @@ +import { Column, ColumnView } from "@bokehjs/models/layouts/column"; +import * as p from "@bokehjs/core/properties"; + +export class ScrollLogView extends ColumnView { + model: ScrollLog; + scroll_down_arrow: HTMLElement; + + connect_signals(): void { + super.connect_signals(); + + const { autoscroll } = this.model.properties; + + if (autoscroll) { + this.on_change(this.model.properties.children, () => { + this.scroll_to_bottom(); + }); + } + + this.el.addEventListener("scroll", () => { + const scrollThreshold = 20; + const scrollDistanceFromBottom = + this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight; + + if (scrollDistanceFromBottom < scrollThreshold) { + this.scroll_down_arrow.classList.remove("visible"); + } else { + this.scroll_down_arrow.classList.add("visible"); + } + }); + + this.scroll_down_arrow.addEventListener("click", () => { + this.scroll_to_bottom(); + }); + } + + scroll_to_bottom(): void { + this.el.scrollTop = this.el.scrollHeight; + } +} + +export namespace ScrollLog { + export type Attrs = p.AttrsOf; + + export type Props = Column.Props & { + autoscroll: p.Property; + }; +} + +export interface ScrollLog extends ScrollLog.Attrs {} + +export class ScrollLog extends Column { + properties: ScrollLog.Props; + + constructor(attrs?: Partial) { + super(attrs); + } + + static __module__ = "panel.models.layout"; + + static { + this.prototype.default_view = ScrollLogView; + + this.define(({ Boolean }) => ({ + autoscroll: [Boolean, true], + })); + } +} From ea2df97be1153bb30cd30ca067c1e0b3bff852f6 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Sun, 9 Jul 2023 15:28:39 -0400 Subject: [PATCH 03/31] Add to index.ts --- panel/dist/css/scrolllog.css | 4 +++- panel/models/index.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/panel/dist/css/scrolllog.css b/panel/dist/css/scrolllog.css index 5d3a347976..099529d869 100644 --- a/panel/dist/css/scrolllog.css +++ b/panel/dist/css/scrolllog.css @@ -22,7 +22,9 @@ justify-content: center; /* For animation */ opacity: 0; - transition: visibility 0s, opacity 0.2s ease-in-out; + transition: + visibility 0s, + opacity 0.2s ease-in-out; } .visible { diff --git a/panel/models/index.ts b/panel/models/index.ts index bc8ba021a1..6b25bd4d74 100644 --- a/panel/models/index.ts +++ b/panel/models/index.ts @@ -26,6 +26,7 @@ export {ReactiveHTML} from "./reactive_html" export {SingleSelect} from "./singleselect" export {SpeechToText} from "./speech_to_text" export {State} from "./state" +export {ScrollLog} from "./scroll_log" export {Tabs} from "./tabs" export {Terminal} from "./terminal" export {TextToSpeech} from "./text_to_speech" From 210a37bd882d13e312db4443c6d85c2db9123080 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 10 Jul 2023 12:27:17 -0400 Subject: [PATCH 04/31] Address comments --- panel/layout/scrolllog.py | 2 +- panel/models/scroll_log.ts | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/panel/layout/scrolllog.py b/panel/layout/scrolllog.py index d3b71bff5e..ee6b9d6203 100644 --- a/panel/layout/scrolllog.py +++ b/panel/layout/scrolllog.py @@ -22,7 +22,7 @@ class ScrollLog(Column): _bokeh_model: ClassVar[Type[Model]] = BkScrollLog - _stylesheets: ClassVar[List[str]] = [f"{CDN_DIST}/layout/scrolllog.css"] + _stylesheets: ClassVar[List[str]] = [f"{CDN_DIST}css/scrolllog.css"] def __init__(self, *objects, **params): if "sizing_mode" not in params: diff --git a/panel/models/scroll_log.ts b/panel/models/scroll_log.ts index b3e7851d93..ca32161554 100644 --- a/panel/models/scroll_log.ts +++ b/panel/models/scroll_log.ts @@ -21,11 +21,9 @@ export class ScrollLogView extends ColumnView { const scrollDistanceFromBottom = this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight; - if (scrollDistanceFromBottom < scrollThreshold) { - this.scroll_down_arrow.classList.remove("visible"); - } else { - this.scroll_down_arrow.classList.add("visible"); - } + this.scroll_down_arrow.classList.toggle( + "visible", scrollDistanceFromBottom >= scrollThreshold + ) }); this.scroll_down_arrow.addEventListener("click", () => { @@ -46,7 +44,7 @@ export namespace ScrollLog { }; } -export interface ScrollLog extends ScrollLog.Attrs {} +export interface ScrollLog extends ScrollLog.Attrs { } export class ScrollLog extends Column { properties: ScrollLog.Props; From ba9411c4657b37be73998d30a92ad606f25d3cab Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 10 Jul 2023 13:48:25 -0400 Subject: [PATCH 05/31] Rename --- panel/layout/scrolllog.py | 7 +------ panel/models/{scroll_log.ts => scrolllog.ts} | 0 2 files changed, 1 insertion(+), 6 deletions(-) rename panel/models/{scroll_log.ts => scrolllog.ts} (100%) diff --git a/panel/layout/scrolllog.py b/panel/layout/scrolllog.py index ee6b9d6203..5f536499a2 100644 --- a/panel/layout/scrolllog.py +++ b/panel/layout/scrolllog.py @@ -15,7 +15,7 @@ class ScrollLog(Column): """ A `ScrollLog` layout allows arranging multiple panel objects in a - scrollable, vertical container with a header bar. + scrollable, vertical container. Reference: https://panel.holoviz.org/reference/layouts/ScrollLog.html """ @@ -23,8 +23,3 @@ class ScrollLog(Column): _bokeh_model: ClassVar[Type[Model]] = BkScrollLog _stylesheets: ClassVar[List[str]] = [f"{CDN_DIST}css/scrolllog.css"] - - def __init__(self, *objects, **params): - if "sizing_mode" not in params: - params["sizing_mode"] = "stretch_width" - super().__init__(objects=list(objects), **params) diff --git a/panel/models/scroll_log.ts b/panel/models/scrolllog.ts similarity index 100% rename from panel/models/scroll_log.ts rename to panel/models/scrolllog.ts From 1676b223f2733480a171f059ff3ce815f381e7c9 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 10 Jul 2023 16:59:46 -0400 Subject: [PATCH 06/31] Create workable version --- panel/dist/css/scrolllog.css | 13 ++++------ panel/layout/scrolllog.py | 14 +++++++++++ panel/models/index.ts | 2 +- panel/models/scrolllog.ts | 46 +++++++++++++++++++++++++++++------- 4 files changed, 57 insertions(+), 18 deletions(-) diff --git a/panel/dist/css/scrolllog.css b/panel/dist/css/scrolllog.css index 099529d869..f72efbfd38 100644 --- a/panel/dist/css/scrolllog.css +++ b/panel/dist/css/scrolllog.css @@ -1,13 +1,12 @@ -.scroll-log { +:host(.scroll-log) { overflow-y: scroll; } .scroll-down-arrow { /* For location */ - position: absolute; - bottom: 0%; - left: 50%; - transform: translate(0%, 0%); + position: sticky; + top: calc(100% - 35px); + left: calc(100% - 60px); /* For icon */ cursor: pointer; visibility: hidden; @@ -22,9 +21,7 @@ justify-content: center; /* For animation */ opacity: 0; - transition: - visibility 0s, - opacity 0.2s ease-in-out; + transition: visibility 0s, opacity 0.2s ease-in-out; } .visible { diff --git a/panel/layout/scrolllog.py b/panel/layout/scrolllog.py index 5f536499a2..c7478771a6 100644 --- a/panel/layout/scrolllog.py +++ b/panel/layout/scrolllog.py @@ -4,6 +4,8 @@ TYPE_CHECKING, ClassVar, List, Type, ) +import param + from ..io.resources import CDN_DIST from ..layout.base import Column from ..models import ScrollLog as BkScrollLog @@ -20,6 +22,18 @@ class ScrollLog(Column): Reference: https://panel.holoviz.org/reference/layouts/ScrollLog.html """ + css_classes = param.List( + default=["scroll-log"], + doc=""" + CSS classes to apply to the overall ScrollLog.""", + ) + _bokeh_model: ClassVar[Type[Model]] = BkScrollLog _stylesheets: ClassVar[List[str]] = [f"{CDN_DIST}css/scrolllog.css"] + + def __init__(self, *objects, **params): + if "sizing_mode" not in params: + params["sizing_mode"] = "stretch_width" + + super().__init__(*objects, **params) diff --git a/panel/models/index.ts b/panel/models/index.ts index 6b25bd4d74..75ccfe0b2f 100644 --- a/panel/models/index.ts +++ b/panel/models/index.ts @@ -26,7 +26,7 @@ export {ReactiveHTML} from "./reactive_html" export {SingleSelect} from "./singleselect" export {SpeechToText} from "./speech_to_text" export {State} from "./state" -export {ScrollLog} from "./scroll_log" +export {ScrollLog} from "./scrolllog" export {Tabs} from "./tabs" export {Terminal} from "./terminal" export {TextToSpeech} from "./text_to_speech" diff --git a/panel/models/scrolllog.ts b/panel/models/scrolllog.ts index ca32161554..f932c84021 100644 --- a/panel/models/scrolllog.ts +++ b/panel/models/scrolllog.ts @@ -1,9 +1,14 @@ import { Column, ColumnView } from "@bokehjs/models/layouts/column"; +import * as DOM from "@bokehjs/core/dom" import * as p from "@bokehjs/core/properties"; export class ScrollLogView extends ColumnView { model: ScrollLog; - scroll_down_arrow: HTMLElement; + scroll_down_arrow_el: HTMLElement; + + scroll_to_bottom(): void { + this.el.scrollTop = this.el.scrollHeight; + } connect_signals(): void { super.connect_signals(); @@ -15,24 +20,47 @@ export class ScrollLogView extends ColumnView { this.scroll_to_bottom(); }); } + } + + render(): void { + super.render() + this.empty() + this._update_stylesheets() + this._update_css_classes() + this._apply_styles() + this._apply_visible() + + this.class_list.add(...this.css_classes()) + this.el.style.overflowY = "auto" + this.el.style.height = "300px" + + this.scroll_down_arrow_el = DOM.createElement('div', { class: 'scroll-down-arrow' }); + this.scroll_down_arrow_el.textContent = '⬇'; + this.shadow_el.appendChild(this.scroll_down_arrow_el); + + this.scroll_down_arrow_el.addEventListener("click", () => { + this.scroll_to_bottom(); + }); this.el.addEventListener("scroll", () => { const scrollThreshold = 20; - const scrollDistanceFromBottom = - this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight; + const scrollDistanceFromBottom = this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight; - this.scroll_down_arrow.classList.toggle( + this.scroll_down_arrow_el.classList.toggle( "visible", scrollDistanceFromBottom >= scrollThreshold ) }); - this.scroll_down_arrow.addEventListener("click", () => { - this.scroll_to_bottom(); - }); + for (const child_view of this.child_views.slice(1)) { + this.shadow_el.appendChild(child_view.el) + child_view.render() + child_view.after_render() + } } - scroll_to_bottom(): void { - this.el.scrollTop = this.el.scrollHeight; + after_render(): void { + super.after_render() + this.scroll_to_bottom(); } } From 1effcee8c12d7c621f06e229872728da9973c69b Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 11 Jul 2023 09:53:51 -0400 Subject: [PATCH 07/31] Extend BkColumn --- panel/dist/css/listpanel.css | 28 +++++++++++++ panel/dist/css/scrolllog.css | 30 -------------- panel/layout/base.py | 7 ++-- panel/layout/scrolllog.py | 39 ------------------ panel/models/__init__.py | 2 +- panel/models/{scrolllog.ts => column.ts} | 52 ++++++++++++++---------- panel/models/index.ts | 2 +- panel/models/layout.py | 36 +++++++++++----- 8 files changed, 90 insertions(+), 106 deletions(-) delete mode 100644 panel/dist/css/scrolllog.css delete mode 100644 panel/layout/scrolllog.py rename panel/models/{scrolllog.ts => column.ts} (54%) diff --git a/panel/dist/css/listpanel.css b/panel/dist/css/listpanel.css index 5f19b8cc9d..6739ea49d0 100644 --- a/panel/dist/css/listpanel.css +++ b/panel/dist/css/listpanel.css @@ -1,3 +1,31 @@ :host(.scrollable) { overflow: scroll; + overflow-y: scroll; +} + +.scroll-down-arrow { + /* For location */ + position: sticky; + top: calc(100% - 35px); + left: calc(100% - 60px); + /* For icon */ + cursor: pointer; + visibility: hidden; + font-size: 18px; + border-radius: 50%; + background-color: rgba(0, 0, 0, 0.25); + color: white; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + /* For animation */ + opacity: 0; + transition: visibility 0s, opacity 0.2s ease-in-out; +} + +.visible { + visibility: visible; + opacity: 1; } diff --git a/panel/dist/css/scrolllog.css b/panel/dist/css/scrolllog.css deleted file mode 100644 index f72efbfd38..0000000000 --- a/panel/dist/css/scrolllog.css +++ /dev/null @@ -1,30 +0,0 @@ -:host(.scroll-log) { - overflow-y: scroll; -} - -.scroll-down-arrow { - /* For location */ - position: sticky; - top: calc(100% - 35px); - left: calc(100% - 60px); - /* For icon */ - cursor: pointer; - visibility: hidden; - font-size: 18px; - border-radius: 50%; - background-color: rgba(0, 0, 0, 0.25); - color: white; - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - /* For animation */ - opacity: 0; - transition: visibility 0s, opacity 0.2s ease-in-out; -} - -.visible { - visibility: visible; - opacity: 1; -} diff --git a/panel/layout/base.py b/panel/layout/base.py index 60619420c4..9aa7746a38 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -12,11 +12,12 @@ import param -from bokeh.models import Column as BkColumn, Row as BkRow +from bokeh.models import Row as BkRow from ..io.model import hold from ..io.resources import CDN_DIST from ..io.state import state +from ..models import Column as PnColumn from ..reactive import Reactive from ..util import param_name, param_reprs @@ -879,7 +880,7 @@ class Column(ListPanel): >>> pn.Column(some_widget, some_pane, some_python_object) """ - _bokeh_model: ClassVar[Type[Model]] = BkColumn + _bokeh_model: ClassVar[Type[Model]] = PnColumn _direction = 'vertical' @@ -929,7 +930,7 @@ class WidgetBox(ListPanel): @property def _bokeh_model(self) -> Type[Model]: # type: ignore - return BkRow if self.horizontal else BkColumn + return BkRow if self.horizontal else PnColumn @property def _direction(self): diff --git a/panel/layout/scrolllog.py b/panel/layout/scrolllog.py deleted file mode 100644 index c7478771a6..0000000000 --- a/panel/layout/scrolllog.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import annotations - -from typing import ( - TYPE_CHECKING, ClassVar, List, Type, -) - -import param - -from ..io.resources import CDN_DIST -from ..layout.base import Column -from ..models import ScrollLog as BkScrollLog - -if TYPE_CHECKING: - from bokeh.model import Model - - -class ScrollLog(Column): - """ - A `ScrollLog` layout allows arranging multiple panel objects in a - scrollable, vertical container. - - Reference: https://panel.holoviz.org/reference/layouts/ScrollLog.html - """ - - css_classes = param.List( - default=["scroll-log"], - doc=""" - CSS classes to apply to the overall ScrollLog.""", - ) - - _bokeh_model: ClassVar[Type[Model]] = BkScrollLog - - _stylesheets: ClassVar[List[str]] = [f"{CDN_DIST}css/scrolllog.css"] - - def __init__(self, *objects, **params): - if "sizing_mode" not in params: - params["sizing_mode"] = "stretch_width" - - super().__init__(*objects, **params) diff --git a/panel/models/__init__.py b/panel/models/__init__.py index 7e0dc09559..5aa3938601 100644 --- a/panel/models/__init__.py +++ b/panel/models/__init__.py @@ -7,7 +7,7 @@ from .datetime_picker import DatetimePicker # noqa from .ipywidget import IPyWidget # noqa -from .layout import Card, ScrollLog # noqa +from .layout import Card, Column, ScrollLog # noqa from .location import Location # noqa from .markup import HTML, JSON, PDF # noqa from .reactive_html import ReactiveHTML # noqa diff --git a/panel/models/scrolllog.ts b/panel/models/column.ts similarity index 54% rename from panel/models/scrolllog.ts rename to panel/models/column.ts index f932c84021..7997a9d5f3 100644 --- a/panel/models/scrolllog.ts +++ b/panel/models/column.ts @@ -1,21 +1,34 @@ -import { Column, ColumnView } from "@bokehjs/models/layouts/column"; +import { Column as BkColumn, ColumnView as BkColumnView } from "@bokehjs/models/layouts/Column"; import * as DOM from "@bokehjs/core/dom" import * as p from "@bokehjs/core/properties"; -export class ScrollLogView extends ColumnView { - model: ScrollLog; +export class ColumnView extends BkColumnView { + model: Column; scroll_down_arrow_el: HTMLElement; scroll_to_bottom(): void { this.el.scrollTop = this.el.scrollHeight; } + toggle_scroll_arrow(): void { + const scrollThreshold = this.model.properties.scroll_arrow_threshold; + const scrollDistanceFromBottom = this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight; + + this.scroll_down_arrow_el.classList.toggle( + "visible", scrollDistanceFromBottom >= scrollThreshold.get_value() + ) + } + connect_signals(): void { super.connect_signals(); - const { autoscroll } = this.model.properties; + const { scroll_arrow_threshold, auto_scroll } = this.model.properties; - if (autoscroll) { + this.on_change(scroll_arrow_threshold, () => { + this.toggle_scroll_arrow(); + }); + + if (auto_scroll) { this.on_change(this.model.properties.children, () => { this.scroll_to_bottom(); }); @@ -43,12 +56,7 @@ export class ScrollLogView extends ColumnView { }); this.el.addEventListener("scroll", () => { - const scrollThreshold = 20; - const scrollDistanceFromBottom = this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight; - - this.scroll_down_arrow_el.classList.toggle( - "visible", scrollDistanceFromBottom >= scrollThreshold - ) + this.toggle_scroll_arrow(); }); for (const child_view of this.child_views.slice(1)) { @@ -64,30 +72,32 @@ export class ScrollLogView extends ColumnView { } } -export namespace ScrollLog { +export namespace Column { export type Attrs = p.AttrsOf; - export type Props = Column.Props & { - autoscroll: p.Property; + export type Props = BkColumn.Props & { + scroll_arrow_threshold: p.Property; + auto_scroll: p.Property; }; } -export interface ScrollLog extends ScrollLog.Attrs { } +export interface Column extends Column.Attrs { } -export class ScrollLog extends Column { - properties: ScrollLog.Props; +export class Column extends BkColumn { + properties: Column.Props; - constructor(attrs?: Partial) { + constructor(attrs?: Partial) { super(attrs); } static __module__ = "panel.models.layout"; static { - this.prototype.default_view = ScrollLogView; + this.prototype.default_view = ColumnView; - this.define(({ Boolean }) => ({ - autoscroll: [Boolean, true], + this.define(({ Int, Boolean }) => ({ + scroll_arrow_threshold: [Int, 20], + auto_scroll: [Boolean, true], })); } } diff --git a/panel/models/index.ts b/panel/models/index.ts index 75ccfe0b2f..f70308850c 100644 --- a/panel/models/index.ts +++ b/panel/models/index.ts @@ -26,7 +26,7 @@ export {ReactiveHTML} from "./reactive_html" export {SingleSelect} from "./singleselect" export {SpeechToText} from "./speech_to_text" export {State} from "./state" -export {ScrollLog} from "./scrolllog" +export {Column} from "./column" export {Tabs} from "./tabs" export {Terminal} from "./terminal" export {TextToSpeech} from "./text_to_speech" diff --git a/panel/models/layout.py b/panel/models/layout.py index 4f0e265b6f..0f6ddef962 100644 --- a/panel/models/layout.py +++ b/panel/models/layout.py @@ -1,27 +1,34 @@ from bokeh.core.properties import ( - Bool, List, Nullable, String, + Bool, Float, List, Nullable, String, ) -from bokeh.models import Column +from bokeh.models import Column as BkColumn from bokeh.models.layouts import LayoutDOM __all__ = ( "Card", "HTMLBox", - "ScrollLog", + "Column", ) + class HTMLBox(LayoutDOM): """ """ -class Card(Column): - active_header_background = Nullable(String, help="Background color of active Card header.") +class Card(BkColumn): + active_header_background = Nullable( + String, help="Background color of active Card header." + ) - button_css_classes = List(String, help="CSS classes to add to the Card collapse button.") + button_css_classes = List( + String, help="CSS classes to add to the Card collapse button." + ) collapsed = Bool(True, help="Whether the Card is collapsed.") - collapsible = Bool(True, help="Whether the Card should have a button to collapse it.") + collapsible = Bool( + True, help="Whether the Card should have a button to collapse it." + ) header_background = Nullable(String, help="Background color of the Card header.") @@ -29,13 +36,20 @@ class Card(Column): header_css_classes = List(String, help="CSS classes to add to the Card header.") - header_tag = String('div', help="HTML tag to use for the Card header.") + header_tag = String("div", help="HTML tag to use for the Card header.") hide_header = Bool(False, help="Whether to hide the Card header") - tag = String('tag', help="CSS class to use for the Card as a whole.") + tag = String("tag", help="CSS class to use for the Card as a whole.") -class ScrollLog(Column): +class Column(BkColumn): + scroll_arrow_threshold = Nullable( + Float, + help=""" + Threshold for showing scroll arrow that scrolls to the latest on click. + The arrow will always be shown if this is set to 0. + """, + ) - autoscroll = Bool(True, help="Whether to scroll to the bottom on update.") + auto_scroll = Bool(True, help="Whether to scroll to the latest row on update.") From 1ed31ee4025bb150771ba6efacf90550545679e4 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 11 Jul 2023 09:54:56 -0400 Subject: [PATCH 08/31] Remove scroll log --- panel/__init__.py | 3 +-- panel/layout/__init__.py | 2 -- panel/models/__init__.py | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/panel/__init__.py b/panel/__init__.py index 157e065263..500b77be5d 100644 --- a/panel/__init__.py +++ b/panel/__init__.py @@ -61,7 +61,7 @@ ) from .layout import ( # noqa Accordion, Card, Column, FlexBox, FloatPanel, GridBox, GridSpec, GridStack, - HSpacer, Row, ScrollLog, Spacer, Swipe, Tabs, VSpacer, WidgetBox, + HSpacer, Row, Spacer, Swipe, Tabs, VSpacer, WidgetBox, ) from .pane import panel # noqa from .param import Param # noqa @@ -82,7 +82,6 @@ "Param", "Row", "Spacer", - "ScrollLog", "Tabs", "Template", "VSpacer", diff --git a/panel/layout/__init__.py b/panel/layout/__init__.py index b72dc4dcb8..9cc2a26135 100644 --- a/panel/layout/__init__.py +++ b/panel/layout/__init__.py @@ -37,7 +37,6 @@ from .float import FloatPanel # noqa from .grid import GridBox, GridSpec # noqa from .gridstack import GridStack # noqa -from .scrolllog import ScrollLog # noqa from .spacer import ( # noqa Divider, HSpacer, Spacer, VSpacer, ) @@ -59,7 +58,6 @@ "ListPanel", "Panel", "Row", - "ScrollLog", "Spacer", "Swipe", "Tabs", diff --git a/panel/models/__init__.py b/panel/models/__init__.py index 5aa3938601..9edea61bc2 100644 --- a/panel/models/__init__.py +++ b/panel/models/__init__.py @@ -7,7 +7,7 @@ from .datetime_picker import DatetimePicker # noqa from .ipywidget import IPyWidget # noqa -from .layout import Card, Column, ScrollLog # noqa +from .layout import Card, Column # noqa from .location import Location # noqa from .markup import HTML, JSON, PDF # noqa from .reactive_html import ReactiveHTML # noqa From b503a8c3d085ae8f9226bdc841d631483b6d9b85 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 11 Jul 2023 18:19:36 +0200 Subject: [PATCH 09/31] Minor fixes --- panel/dist/css/listpanel.css | 1 + panel/models/column.ts | 11 ++++------- panel/models/index.ts | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/panel/dist/css/listpanel.css b/panel/dist/css/listpanel.css index 6739ea49d0..5fe8fbb1ce 100644 --- a/panel/dist/css/listpanel.css +++ b/panel/dist/css/listpanel.css @@ -20,6 +20,7 @@ display: flex; align-items: center; justify-content: center; + z-index: 9999; /* For animation */ opacity: 0; transition: visibility 0s, opacity 0.2s ease-in-out; diff --git a/panel/models/column.ts b/panel/models/column.ts index 7997a9d5f3..596cdb0635 100644 --- a/panel/models/column.ts +++ b/panel/models/column.ts @@ -1,4 +1,4 @@ -import { Column as BkColumn, ColumnView as BkColumnView } from "@bokehjs/models/layouts/Column"; +import { Column as BkColumn, ColumnView as BkColumnView } from "@bokehjs/models/layouts/column"; import * as DOM from "@bokehjs/core/dom" import * as p from "@bokehjs/core/properties"; @@ -44,13 +44,10 @@ export class ColumnView extends BkColumnView { this._apply_visible() this.class_list.add(...this.css_classes()) - this.el.style.overflowY = "auto" - this.el.style.height = "300px" this.scroll_down_arrow_el = DOM.createElement('div', { class: 'scroll-down-arrow' }); this.scroll_down_arrow_el.textContent = '⬇'; this.shadow_el.appendChild(this.scroll_down_arrow_el); - this.scroll_down_arrow_el.addEventListener("click", () => { this.scroll_to_bottom(); }); @@ -76,12 +73,12 @@ export namespace Column { export type Attrs = p.AttrsOf; export type Props = BkColumn.Props & { - scroll_arrow_threshold: p.Property; auto_scroll: p.Property; + scroll_arrow_threshold: p.Property; }; } -export interface Column extends Column.Attrs { } +export interface Column extends BkColumn.Attrs { } export class Column extends BkColumn { properties: Column.Props; @@ -96,8 +93,8 @@ export class Column extends BkColumn { this.prototype.default_view = ColumnView; this.define(({ Int, Boolean }) => ({ - scroll_arrow_threshold: [Int, 20], auto_scroll: [Boolean, true], + scroll_arrow_threshold: [Int, 20], })); } } diff --git a/panel/models/index.ts b/panel/models/index.ts index f70308850c..5dfd7cf86b 100644 --- a/panel/models/index.ts +++ b/panel/models/index.ts @@ -2,6 +2,7 @@ export {AcePlot} from "./ace" export {Audio} from "./audio" export {BrowserInfo} from "./browser" export {Card} from "./card" +export {Column} from "./column" export {CommManager} from "./comm_manager" export {CustomSelect} from "./customselect" export {DataTabulator} from "./tabulator" @@ -26,7 +27,6 @@ export {ReactiveHTML} from "./reactive_html" export {SingleSelect} from "./singleselect" export {SpeechToText} from "./speech_to_text" export {State} from "./state" -export {Column} from "./column" export {Tabs} from "./tabs" export {Terminal} from "./terminal" export {TextToSpeech} from "./text_to_speech" From 01d2f5df18483373d73e697b0bdff69bc983d199 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 11 Jul 2023 14:20:20 -0400 Subject: [PATCH 10/31] Address comments --- panel/dist/css/listpanel.css | 13 ++++++++- panel/layout/base.py | 22 +++++++++++++++- panel/layout/card.py | 2 +- panel/models/column.ts | 51 ++++++++++++++++++------------------ panel/models/layout.py | 10 +++---- 5 files changed, 65 insertions(+), 33 deletions(-) diff --git a/panel/dist/css/listpanel.css b/panel/dist/css/listpanel.css index 5fe8fbb1ce..deee8d9625 100644 --- a/panel/dist/css/listpanel.css +++ b/panel/dist/css/listpanel.css @@ -1,8 +1,15 @@ :host(.scrollable) { - overflow: scroll; + overflow: auto; +} + +:host(.scrollable-vertical) { overflow-y: scroll; } +:host(.scrollable-horizontal) { + overflow-x: scroll; +} + .scroll-down-arrow { /* For location */ position: sticky; @@ -30,3 +37,7 @@ visibility: visible; opacity: 1; } + +.scroll-down-arrow:before { + content: '⬇'; +} diff --git a/panel/layout/base.py b/panel/layout/base.py index 9aa7746a38..4eff2fa214 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -829,7 +829,10 @@ def _process_param_change(self, params: Dict[str, Any]) -> Dict[str, Any]: scroll = params.pop('scroll') css_classes = list(self.css_classes or []) if scroll: - css_classes += ['scrollable'] + if hasattr(self, "_direction"): + css_classes += [f'scrollable-{self._direction}'] + else: + css_classes += ['scrollable'] params['css_classes'] = css_classes return super()._process_param_change(params) @@ -880,12 +883,29 @@ class Column(ListPanel): >>> pn.Column(some_widget, some_pane, some_python_object) """ + auto_scroll = param.Boolean( + default=False, + doc="Whether to scroll to the latest row on update." + ) + + scroll_arrow_threshold = param.Number( + doc=""" + Threshold for showing scroll arrow that scrolls to the latest on click. + The arrow will be hidden if set to 0. + """, + ) + _bokeh_model: ClassVar[Type[Model]] = PnColumn _direction = 'vertical' _stylesheets: ClassVar[list[str]] = [f'{CDN_DIST}css/listpanel.css'] + def __init__(self, *objects: Any, **params: Any): + if "auto_scroll" in params or "scroll_arrow_threshold" in params: + params["scroll"] = True + super().__init__(*objects, **params) + class WidgetBox(ListPanel): """ diff --git a/panel/layout/card.py b/panel/layout/card.py index daa459c534..e690a77ace 100644 --- a/panel/layout/card.py +++ b/panel/layout/card.py @@ -99,7 +99,7 @@ def _process_param_change(self, params): scroll = params.pop('scroll', None) css_classes = self.css_classes or [] if scroll: - params['css_classes'] = css_classes + ['scrollable'] + params['css_classes'] = css_classes + ['scrollable-y'] elif scroll == False: params['css_classes'] = css_classes return super(ListPanel, self)._process_param_change(params) diff --git a/panel/models/column.ts b/panel/models/column.ts index 596cdb0635..a7839071a9 100644 --- a/panel/models/column.ts +++ b/panel/models/column.ts @@ -6,14 +6,14 @@ export class ColumnView extends BkColumnView { model: Column; scroll_down_arrow_el: HTMLElement; - scroll_to_bottom(): void { + scroll_to_latest(): void { this.el.scrollTop = this.el.scrollHeight; } toggle_scroll_arrow(): void { const scrollThreshold = this.model.properties.scroll_arrow_threshold; - const scrollDistanceFromBottom = this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight; + const scrollDistanceFromBottom = this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight; this.scroll_down_arrow_el.classList.toggle( "visible", scrollDistanceFromBottom >= scrollThreshold.get_value() ) @@ -22,15 +22,18 @@ export class ColumnView extends BkColumnView { connect_signals(): void { super.connect_signals(); - const { scroll_arrow_threshold, auto_scroll } = this.model.properties; + const { auto_scroll, scroll_arrow_threshold } = this.model.properties; - this.on_change(scroll_arrow_threshold, () => { - this.toggle_scroll_arrow(); - }); + if (scroll_arrow_threshold.get_value() > 0) { + this.on_change(scroll_arrow_threshold, () => { + this.toggle_scroll_arrow(); + }); + } - if (auto_scroll) { + if (auto_scroll.get_value()) { + console.log(auto_scroll.get_value()) this.on_change(this.model.properties.children, () => { - this.scroll_to_bottom(); + this.scroll_to_latest(); }); } } @@ -45,16 +48,19 @@ export class ColumnView extends BkColumnView { this.class_list.add(...this.css_classes()) - this.scroll_down_arrow_el = DOM.createElement('div', { class: 'scroll-down-arrow' }); - this.scroll_down_arrow_el.textContent = '⬇'; - this.shadow_el.appendChild(this.scroll_down_arrow_el); - this.scroll_down_arrow_el.addEventListener("click", () => { - this.scroll_to_bottom(); - }); + const scrollThreshold = this.model.properties.scroll_arrow_threshold; + if (scrollThreshold.get_value() > 0) { + this.scroll_down_arrow_el = DOM.createElement('div', { class: 'scroll-down-arrow' }); + this.shadow_el.appendChild(this.scroll_down_arrow_el); - this.el.addEventListener("scroll", () => { - this.toggle_scroll_arrow(); - }); + this.el.addEventListener("scroll", () => { + this.toggle_scroll_arrow(); + }); + + this.scroll_down_arrow_el.addEventListener("click", () => { + this.scroll_to_latest(); + }); + } for (const child_view of this.child_views.slice(1)) { this.shadow_el.appendChild(child_view.el) @@ -62,11 +68,6 @@ export class ColumnView extends BkColumnView { child_view.after_render() } } - - after_render(): void { - super.after_render() - this.scroll_to_bottom(); - } } export namespace Column { @@ -92,9 +93,9 @@ export class Column extends BkColumn { static { this.prototype.default_view = ColumnView; - this.define(({ Int, Boolean }) => ({ - auto_scroll: [Boolean, true], - scroll_arrow_threshold: [Int, 20], + this.define(({ Boolean, Int }) => ({ + auto_scroll: [Boolean, false], + scroll_arrow_threshold: [Int, 0], })); } } diff --git a/panel/models/layout.py b/panel/models/layout.py index 0f6ddef962..efee46b425 100644 --- a/panel/models/layout.py +++ b/panel/models/layout.py @@ -44,12 +44,12 @@ class Card(BkColumn): class Column(BkColumn): - scroll_arrow_threshold = Nullable( - Float, + + auto_scroll = Bool(False, help="Whether to scroll to the latest row on update.") + + scroll_arrow_threshold = Float( help=""" Threshold for showing scroll arrow that scrolls to the latest on click. - The arrow will always be shown if this is set to 0. + The arrow will be hidden if set to 0. """, ) - - auto_scroll = Bool(True, help="Whether to scroll to the latest row on update.") From 408306413390baf5ac32dca975d500ff3507a7ef Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 13 Jul 2023 11:48:12 -0700 Subject: [PATCH 11/31] Fix scrolling too early --- panel/models/column.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/panel/models/column.ts b/panel/models/column.ts index a7839071a9..09b450289a 100644 --- a/panel/models/column.ts +++ b/panel/models/column.ts @@ -7,7 +7,10 @@ export class ColumnView extends BkColumnView { scroll_down_arrow_el: HTMLElement; scroll_to_latest(): void { - this.el.scrollTop = this.el.scrollHeight; + // Waits for the child to be rendered before scrolling + requestAnimationFrame(() => { + this.el.scrollTop = this.el.scrollHeight; + }); } toggle_scroll_arrow(): void { @@ -24,18 +27,16 @@ export class ColumnView extends BkColumnView { const { auto_scroll, scroll_arrow_threshold } = this.model.properties; + if (auto_scroll) { + this.on_change(this.model.properties.children, () => { + this.scroll_to_latest(); + }); + } if (scroll_arrow_threshold.get_value() > 0) { this.on_change(scroll_arrow_threshold, () => { this.toggle_scroll_arrow(); }); } - - if (auto_scroll.get_value()) { - console.log(auto_scroll.get_value()) - this.on_change(this.model.properties.children, () => { - this.scroll_to_latest(); - }); - } } render(): void { From d275ba0a37949c83d36ca1327867c197997027a1 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 13 Jul 2023 17:10:14 -0700 Subject: [PATCH 12/31] Add tests + docs --- panel/dist/css/listpanel.css | 4 +- panel/layout/base.py | 17 +++++--- panel/layout/card.py | 2 +- panel/models/column.ts | 2 +- panel/tests/layout/test_base.py | 11 +++++ panel/tests/ui/layout/test_card.py | 8 ++++ panel/tests/ui/layout/test_column.py | 64 +++++++++++++++++++++++++++- panel/tests/ui/layout/test_row.py | 5 ++- 8 files changed, 100 insertions(+), 13 deletions(-) diff --git a/panel/dist/css/listpanel.css b/panel/dist/css/listpanel.css index deee8d9625..3707eee6c9 100644 --- a/panel/dist/css/listpanel.css +++ b/panel/dist/css/listpanel.css @@ -3,11 +3,11 @@ } :host(.scrollable-vertical) { - overflow-y: scroll; + overflow-y: auto; } :host(.scrollable-horizontal) { - overflow-x: scroll; + overflow-x: auto; } .scroll-down-arrow { diff --git a/panel/layout/base.py b/panel/layout/base.py index 4eff2fa214..a55fd51012 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -793,11 +793,14 @@ def _linked_properties(self): ) def _process_param_change(self, params: Dict[str, Any]) -> Dict[str, Any]: - scroll = params.pop('scroll', None) - css_classes = self.css_classes or [] - if scroll: - params['css_classes'] = css_classes + ['scrollable'] - elif scroll == False: + if 'scroll' in params: + scroll = params.pop('scroll') + css_classes = list(self.css_classes or []) + if scroll: + if self._direction is not None: + css_classes += [f'scrollable-{self._direction}'] + else: + css_classes += ['scrollable'] params['css_classes'] = css_classes return super()._process_param_change(params) @@ -829,7 +832,7 @@ def _process_param_change(self, params: Dict[str, Any]) -> Dict[str, Any]: scroll = params.pop('scroll') css_classes = list(self.css_classes or []) if scroll: - if hasattr(self, "_direction"): + if self._direction is not None: css_classes += [f'scrollable-{self._direction}'] else: css_classes += ['scrollable'] @@ -885,7 +888,7 @@ class Column(ListPanel): auto_scroll = param.Boolean( default=False, - doc="Whether to scroll to the latest row on update." + doc="Whether to scroll to the latest object on update." ) scroll_arrow_threshold = param.Number( diff --git a/panel/layout/card.py b/panel/layout/card.py index e690a77ace..b7cd0b5564 100644 --- a/panel/layout/card.py +++ b/panel/layout/card.py @@ -99,7 +99,7 @@ def _process_param_change(self, params): scroll = params.pop('scroll', None) css_classes = self.css_classes or [] if scroll: - params['css_classes'] = css_classes + ['scrollable-y'] + params['css_classes'] = css_classes + ['scrollable-vertical'] elif scroll == False: params['css_classes'] = css_classes return super(ListPanel, self)._process_param_change(params) diff --git a/panel/models/column.ts b/panel/models/column.ts index 09b450289a..30938fc460 100644 --- a/panel/models/column.ts +++ b/panel/models/column.ts @@ -51,7 +51,7 @@ export class ColumnView extends BkColumnView { const scrollThreshold = this.model.properties.scroll_arrow_threshold; if (scrollThreshold.get_value() > 0) { - this.scroll_down_arrow_el = DOM.createElement('div', { class: 'scroll-down-arrow' }); + this.scroll_down_arrow_el = DOM.createElement('div', { class: 'scroll-arrow' }); this.shadow_el.appendChild(this.scroll_down_arrow_el); this.el.addEventListener("scroll", () => { diff --git a/panel/tests/layout/test_base.py b/panel/tests/layout/test_base.py index 8aac8d133a..0e559fed2e 100644 --- a/panel/tests/layout/test_base.py +++ b/panel/tests/layout/test_base.py @@ -567,3 +567,14 @@ def test_no_expand_fixed(panel, document, comm): model = layout.get_root(document, comm) assert model.sizing_mode == 'fixed' + + +@pytest.mark.parametrize('scroll_param', ["auto_scroll", "scroll", "scroll_arrow_threshold"]) +def test_column_scroll_params_sets_scroll(scroll_param, document, comm): + if scroll_param != "scroll_arrow_threshold": + params = {scroll_param: True} + else: + params = {scroll_param: 1} + col = Column(**params) + assert getattr(col, scroll_param) + assert col.scroll diff --git a/panel/tests/ui/layout/test_card.py b/panel/tests/ui/layout/test_card.py index 58c7409a20..8209214bff 100644 --- a/panel/tests/ui/layout/test_card.py +++ b/panel/tests/ui/layout/test_card.py @@ -191,3 +191,11 @@ def test_card_custom_css(page, port): card_button = page.locator(f'.card-button.{additional_button_css_class}') expect(card_button).to_have_count(1) + + +def test_card_scrollable(page, port): + card = Card(scroll=True) + serve_panel_widget(page, port, card) + + card_widget = page.locator('.card') + assert 'scrollable-vertical' in card_widget.get_attribute('class') diff --git a/panel/tests/ui/layout/test_column.py b/panel/tests/ui/layout/test_column.py index 47a8202c17..2bd376f3f9 100644 --- a/panel/tests/ui/layout/test_column.py +++ b/panel/tests/ui/layout/test_column.py @@ -21,7 +21,69 @@ def test_column_scroll(page, port): page.goto(f"http://localhost:{port}") - bbox = page.locator(".bk-Column").bounding_box() + col_el = page.locator(".bk-panel-models-layout-Column") + bbox = col_el.bounding_box() assert bbox['width'] in (200, 215) # Ignore if browser hides empty scrollbar assert bbox['height'] == 420 + + assert 'scrollable-vertical' in col_el.get_attribute('class') + + +def test_column_auto_scroll(page, port): + col = Column( + Spacer(styles=dict(background='red'), width=200, height=200), + Spacer(styles=dict(background='green'), width=200, height=200), + Spacer(styles=dict(background='blue'), width=200, height=200), + auto_scroll=True, height=420 + ) + + serve(col, port=port, threaded=True, show=False) + + time.sleep(0.5) + + page.goto(f"http://localhost:{port}") + + column = page.locator(".bk-panel-models-layout-Column") + bbox = column.bounding_box() + + assert bbox['width'] in (200, 215) # Ignore if browser hides empty scrollbar + assert bbox['height'] == 420 + + assert 'scrollable-vertical' in column.get_attribute('class') + + scroll_loc = column.scrollTop + assert scroll_loc == 0 + + col.append(Spacer(styles=dict(background='yellow'), width=200, height=200)) + new_scroll_loc = column.scrollTop + assert new_scroll_loc > scroll_loc + + +def test_column_scroll_arrow_threshold(page, port): + col = Column( + Spacer(styles=dict(background='red'), width=200, height=200), + Spacer(styles=dict(background='green'), width=200, height=200), + Spacer(styles=dict(background='blue'), width=200, height=200), + scroll_arrow_threshold=0.5, height=420 + ) + + serve(col, port=port, threaded=True, show=False) + + time.sleep(0.5) + + page.goto(f"http://localhost:{port}") + + column = page.locator(".bk-panel-models-layout-Column") + bbox = column.bounding_box() + + assert bbox['width'] in (200, 215) # Ignore if browser hides empty scrollbar + assert bbox['height'] == 420 + + assert 'scrollable-vertical' in column.get_attribute('class') + + # trigger scroll event + column.scrollTop = 1 + scroll_arrow = page.locator(".scroll-arrow") + assert scroll_arrow.get_attribute('class') == 'scroll-arrow' + assert scroll_arrow.is_visible() diff --git a/panel/tests/ui/layout/test_row.py b/panel/tests/ui/layout/test_row.py index c16e18309f..287a75ed79 100644 --- a/panel/tests/ui/layout/test_row.py +++ b/panel/tests/ui/layout/test_row.py @@ -21,7 +21,10 @@ def test_row_scroll(page, port): page.goto(f"http://localhost:{port}") - bbox = page.locator(".bk-Row").bounding_box() + row_el = page.locator(".bk-Row").bounding_box() + bbox = row_el.bounding_box() assert bbox['width'] == 420 assert bbox['height'] in (200, 215) # Ignore if browser hides empty scrollbar + + assert 'scrollable-horizontal' in row_el.get_attribute('class') From 5c35b8b2ebee436947102b205891de3d0736703e Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 13 Jul 2023 17:12:22 -0700 Subject: [PATCH 13/31] Precommit --- panel/dist/css/listpanel.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/panel/dist/css/listpanel.css b/panel/dist/css/listpanel.css index 3707eee6c9..7b766b1c44 100644 --- a/panel/dist/css/listpanel.css +++ b/panel/dist/css/listpanel.css @@ -30,7 +30,9 @@ z-index: 9999; /* For animation */ opacity: 0; - transition: visibility 0s, opacity 0.2s ease-in-out; + transition: + visibility 0s, + opacity 0.2s ease-in-out; } .visible { From ccd50d8fe1941383617b5cf176564abf08fdc5f0 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 13 Jul 2023 17:13:49 -0700 Subject: [PATCH 14/31] precommit --- panel/dist/css/listpanel.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/dist/css/listpanel.css b/panel/dist/css/listpanel.css index 7b766b1c44..e4b290f432 100644 --- a/panel/dist/css/listpanel.css +++ b/panel/dist/css/listpanel.css @@ -30,7 +30,7 @@ z-index: 9999; /* For animation */ opacity: 0; - transition: + transition: visibility 0s, opacity 0.2s ease-in-out; } From f7c391684fd16e2f8ce81a0893feeacc2ff37e67 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Fri, 14 Jul 2023 05:58:56 -0700 Subject: [PATCH 15/31] Address comments --- panel/layout/base.py | 16 +++++++--------- panel/models/column.ts | 16 ++++++++-------- panel/models/layout.py | 2 +- panel/tests/layout/test_base.py | 4 ++-- panel/tests/ui/layout/test_column.py | 8 ++++---- 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/panel/layout/base.py b/panel/layout/base.py index a55fd51012..34a9c5412a 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -887,15 +887,14 @@ class Column(ListPanel): """ auto_scroll = param.Boolean( - default=False, - doc="Whether to scroll to the latest object on update." + default=False, doc=""" + Whether to scroll to the latest object on update.""" ) - scroll_arrow_threshold = param.Number( + scroll_button_threshold = param.Number( doc=""" Threshold for showing scroll arrow that scrolls to the latest on click. - The arrow will be hidden if set to 0. - """, + The arrow will be hidden if set to 0.""" ) _bokeh_model: ClassVar[Type[Model]] = PnColumn @@ -904,10 +903,9 @@ class Column(ListPanel): _stylesheets: ClassVar[list[str]] = [f'{CDN_DIST}css/listpanel.css'] - def __init__(self, *objects: Any, **params: Any): - if "auto_scroll" in params or "scroll_arrow_threshold" in params: - params["scroll"] = True - super().__init__(*objects, **params) + @param.depends("auto_scroll", "scroll_button_threshold", watch=True, on_init=True) + def _set_scrollable(self): + self.scroll = self.scroll or self.auto_scroll or self.scroll_button_threshold class WidgetBox(ListPanel): diff --git a/panel/models/column.ts b/panel/models/column.ts index 30938fc460..fa27636381 100644 --- a/panel/models/column.ts +++ b/panel/models/column.ts @@ -14,7 +14,7 @@ export class ColumnView extends BkColumnView { } toggle_scroll_arrow(): void { - const scrollThreshold = this.model.properties.scroll_arrow_threshold; + const scrollThreshold = this.model.properties.scroll_button_threshold; const scrollDistanceFromBottom = this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight; this.scroll_down_arrow_el.classList.toggle( @@ -25,15 +25,15 @@ export class ColumnView extends BkColumnView { connect_signals(): void { super.connect_signals(); - const { auto_scroll, scroll_arrow_threshold } = this.model.properties; + const { auto_scroll, scroll_button_threshold } = this.model.properties; if (auto_scroll) { this.on_change(this.model.properties.children, () => { this.scroll_to_latest(); }); } - if (scroll_arrow_threshold.get_value() > 0) { - this.on_change(scroll_arrow_threshold, () => { + if (scroll_button_threshold.get_value() > 0) { + this.on_change(scroll_button_threshold, () => { this.toggle_scroll_arrow(); }); } @@ -49,9 +49,9 @@ export class ColumnView extends BkColumnView { this.class_list.add(...this.css_classes()) - const scrollThreshold = this.model.properties.scroll_arrow_threshold; + const scrollThreshold = this.model.properties.scroll_button_threshold; if (scrollThreshold.get_value() > 0) { - this.scroll_down_arrow_el = DOM.createElement('div', { class: 'scroll-arrow' }); + this.scroll_down_arrow_el = DOM.createElement('div', { class: 'scroll-button' }); this.shadow_el.appendChild(this.scroll_down_arrow_el); this.el.addEventListener("scroll", () => { @@ -76,7 +76,7 @@ export namespace Column { export type Props = BkColumn.Props & { auto_scroll: p.Property; - scroll_arrow_threshold: p.Property; + scroll_button_threshold: p.Property; }; } @@ -96,7 +96,7 @@ export class Column extends BkColumn { this.define(({ Boolean, Int }) => ({ auto_scroll: [Boolean, false], - scroll_arrow_threshold: [Int, 0], + scroll_button_threshold: [Int, 0], })); } } diff --git a/panel/models/layout.py b/panel/models/layout.py index efee46b425..b64316121d 100644 --- a/panel/models/layout.py +++ b/panel/models/layout.py @@ -47,7 +47,7 @@ class Column(BkColumn): auto_scroll = Bool(False, help="Whether to scroll to the latest row on update.") - scroll_arrow_threshold = Float( + scroll_button_threshold = Float( help=""" Threshold for showing scroll arrow that scrolls to the latest on click. The arrow will be hidden if set to 0. diff --git a/panel/tests/layout/test_base.py b/panel/tests/layout/test_base.py index 0e559fed2e..fabe3f73f0 100644 --- a/panel/tests/layout/test_base.py +++ b/panel/tests/layout/test_base.py @@ -569,9 +569,9 @@ def test_no_expand_fixed(panel, document, comm): assert model.sizing_mode == 'fixed' -@pytest.mark.parametrize('scroll_param', ["auto_scroll", "scroll", "scroll_arrow_threshold"]) +@pytest.mark.parametrize('scroll_param', ["auto_scroll", "scroll", "scroll_button_threshold"]) def test_column_scroll_params_sets_scroll(scroll_param, document, comm): - if scroll_param != "scroll_arrow_threshold": + if scroll_param != "scroll_button_threshold": params = {scroll_param: True} else: params = {scroll_param: 1} diff --git a/panel/tests/ui/layout/test_column.py b/panel/tests/ui/layout/test_column.py index 2bd376f3f9..11df5f7e56 100644 --- a/panel/tests/ui/layout/test_column.py +++ b/panel/tests/ui/layout/test_column.py @@ -60,12 +60,12 @@ def test_column_auto_scroll(page, port): assert new_scroll_loc > scroll_loc -def test_column_scroll_arrow_threshold(page, port): +def test_column_scroll_button_threshold(page, port): col = Column( Spacer(styles=dict(background='red'), width=200, height=200), Spacer(styles=dict(background='green'), width=200, height=200), Spacer(styles=dict(background='blue'), width=200, height=200), - scroll_arrow_threshold=0.5, height=420 + scroll_button_threshold=0.5, height=420 ) serve(col, port=port, threaded=True, show=False) @@ -84,6 +84,6 @@ def test_column_scroll_arrow_threshold(page, port): # trigger scroll event column.scrollTop = 1 - scroll_arrow = page.locator(".scroll-arrow") - assert scroll_arrow.get_attribute('class') == 'scroll-arrow' + scroll_arrow = page.locator(".scroll-button") + assert scroll_arrow.get_attribute('class') == 'scroll-button' assert scroll_arrow.is_visible() From ca0a877d396011e8cae20818622db1eabe9f2076 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Fri, 14 Jul 2023 05:59:48 -0700 Subject: [PATCH 16/31] Styling --- panel/layout/base.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/panel/layout/base.py b/panel/layout/base.py index 34a9c5412a..b729656fa9 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -886,15 +886,13 @@ class Column(ListPanel): >>> pn.Column(some_widget, some_pane, some_python_object) """ - auto_scroll = param.Boolean( - default=False, doc=""" + auto_scroll = param.Boolean(default=False, doc=""" Whether to scroll to the latest object on update.""" ) - scroll_button_threshold = param.Number( - doc=""" - Threshold for showing scroll arrow that scrolls to the latest on click. - The arrow will be hidden if set to 0.""" + scroll_button_threshold = param.Number(bounds=(0, None), doc=""" + Threshold for showing scroll button that scrolls to the latest on click. + The button will be hidden if set to 0.""" ) _bokeh_model: ClassVar[Type[Model]] = PnColumn From 20d0cccf3d2d9669781493a3701a953887b7e400 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Fri, 14 Jul 2023 09:25:43 -0700 Subject: [PATCH 17/31] Fix inheritance --- panel/layout/base.py | 2 +- panel/models/card.ts | 2 +- panel/models/layout.py | 26 +++++++++++++------------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/panel/layout/base.py b/panel/layout/base.py index b729656fa9..84e44c2aa5 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -903,7 +903,7 @@ class Column(ListPanel): @param.depends("auto_scroll", "scroll_button_threshold", watch=True, on_init=True) def _set_scrollable(self): - self.scroll = self.scroll or self.auto_scroll or self.scroll_button_threshold + self.scroll = self.scroll or self.auto_scroll or bool(self.scroll_button_threshold) class WidgetBox(ListPanel): diff --git a/panel/models/card.ts b/panel/models/card.ts index a9ae5d5e71..fb2c70409f 100644 --- a/panel/models/card.ts +++ b/panel/models/card.ts @@ -1,4 +1,4 @@ -import {Column, ColumnView} from "@bokehjs/models/layouts/column" +import {Column, ColumnView} from "./column" import * as DOM from "@bokehjs/core/dom" import * as p from "@bokehjs/core/properties" diff --git a/panel/models/layout.py b/panel/models/layout.py index b64316121d..bec95cd286 100644 --- a/panel/models/layout.py +++ b/panel/models/layout.py @@ -15,7 +15,19 @@ class HTMLBox(LayoutDOM): """ """ -class Card(BkColumn): +class Column(BkColumn): + + auto_scroll = Bool(False, help="Whether to scroll to the latest row on update.") + + scroll_button_threshold = Float( + help=""" + Threshold for showing scroll arrow that scrolls to the latest on click. + The arrow will be hidden if set to 0. + """, + ) + + +class Card(Column): active_header_background = Nullable( String, help="Background color of active Card header." ) @@ -41,15 +53,3 @@ class Card(BkColumn): hide_header = Bool(False, help="Whether to hide the Card header") tag = String("tag", help="CSS class to use for the Card as a whole.") - - -class Column(BkColumn): - - auto_scroll = Bool(False, help="Whether to scroll to the latest row on update.") - - scroll_button_threshold = Float( - help=""" - Threshold for showing scroll arrow that scrolls to the latest on click. - The arrow will be hidden if set to 0. - """, - ) From 216a097d4df7969cecbc5ffd5390d6fb1ee1a120 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 19 Jul 2023 12:33:45 +0200 Subject: [PATCH 18/31] Various fixes --- panel/dist/css/listpanel.css | 8 ++--- panel/layout/base.py | 18 +++++------ panel/layout/card.py | 11 +------ panel/models/column.ts | 58 +++++++++++++++--------------------- 4 files changed, 36 insertions(+), 59 deletions(-) diff --git a/panel/dist/css/listpanel.css b/panel/dist/css/listpanel.css index e4b290f432..12a2486623 100644 --- a/panel/dist/css/listpanel.css +++ b/panel/dist/css/listpanel.css @@ -10,7 +10,7 @@ overflow-x: auto; } -.scroll-down-arrow { +.scroll-button { /* For location */ position: sticky; top: calc(100% - 35px); @@ -30,9 +30,7 @@ z-index: 9999; /* For animation */ opacity: 0; - transition: - visibility 0s, - opacity 0.2s ease-in-out; + transition: visibility 0s, opacity 0.2s ease-in-out; } .visible { @@ -40,6 +38,6 @@ opacity: 1; } -.scroll-down-arrow:before { +.scroll-button:before { content: '⬇'; } diff --git a/panel/layout/base.py b/panel/layout/base.py index 84e44c2aa5..7194706700 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -794,8 +794,8 @@ def _linked_properties(self): def _process_param_change(self, params: Dict[str, Any]) -> Dict[str, Any]: if 'scroll' in params: - scroll = params.pop('scroll') - css_classes = list(self.css_classes or []) + scroll = params['scroll'] + css_classes = params.get('css_classes', self.css_classes) if scroll: if self._direction is not None: css_classes += [f'scrollable-{self._direction}'] @@ -829,8 +829,8 @@ class NamedListPanel(NamedListLike, Panel): def _process_param_change(self, params: Dict[str, Any]) -> Dict[str, Any]: if 'scroll' in params: - scroll = params.pop('scroll') - css_classes = list(self.css_classes or []) + scroll = params['scroll'] + css_classes = params.get('css_classes', self.css_classes) if scroll: if self._direction is not None: css_classes += [f'scrollable-{self._direction}'] @@ -887,13 +887,11 @@ class Column(ListPanel): """ auto_scroll = param.Boolean(default=False, doc=""" - Whether to scroll to the latest object on update.""" - ) + Whether to scroll to the last object on update.""") - scroll_button_threshold = param.Number(bounds=(0, None), doc=""" - Threshold for showing scroll button that scrolls to the latest on click. - The button will be hidden if set to 0.""" - ) + scroll_button_threshold = param.Integer(bounds=(0, None), doc=""" + Threshold for showing button that scrolls to the bottom on click. + The button will be hidden if set to 0.""") _bokeh_model: ClassVar[Type[Model]] = PnColumn diff --git a/panel/layout/card.py b/panel/layout/card.py index b7cd0b5564..2c6a020563 100644 --- a/panel/layout/card.py +++ b/panel/layout/card.py @@ -8,7 +8,7 @@ from ..io.resources import CDN_DIST from ..models import Card as BkCard -from .base import Column, ListPanel, Row +from .base import Column, Row if TYPE_CHECKING: from bokeh.model import Model @@ -95,15 +95,6 @@ def _cleanup(self, root: Model | None = None) -> None: super()._cleanup(root) self._header_layout._cleanup(root) - def _process_param_change(self, params): - scroll = params.pop('scroll', None) - css_classes = self.css_classes or [] - if scroll: - params['css_classes'] = css_classes + ['scrollable-vertical'] - elif scroll == False: - params['css_classes'] = css_classes - return super(ListPanel, self)._process_param_change(params) - def _update_header(self, *events): from ..pane import HTML, panel if self.header is None: diff --git a/panel/models/column.ts b/panel/models/column.ts index fa27636381..f9f11a625c 100644 --- a/panel/models/column.ts +++ b/panel/models/column.ts @@ -6,7 +6,19 @@ export class ColumnView extends BkColumnView { model: Column; scroll_down_arrow_el: HTMLElement; + connect_signals(): void { + super.connect_signals(); + + const { children, scroll_button_threshold } = this.model.properties; + + this.on_change(children, () => this.scroll_to_latest()); + this.on_change(scroll_button_threshold, () => this.toggle_scroll_arrow()) + } + scroll_to_latest(): void { + if (!this.model.auto_scroll) + return + // Waits for the child to be rendered before scrolling requestAnimationFrame(() => { this.el.scrollTop = this.el.scrollHeight; @@ -14,31 +26,14 @@ export class ColumnView extends BkColumnView { } toggle_scroll_arrow(): void { - const scrollThreshold = this.model.properties.scroll_button_threshold; - + const threshold = this.model.scroll_button_threshold const scrollDistanceFromBottom = this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight; + console.log(scrollDistanceFromBottom, (threshold !== 0) && (scrollDistanceFromBottom >= threshold)) this.scroll_down_arrow_el.classList.toggle( - "visible", scrollDistanceFromBottom >= scrollThreshold.get_value() + "visible", (threshold !== 0) && (scrollDistanceFromBottom >= threshold) ) } - connect_signals(): void { - super.connect_signals(); - - const { auto_scroll, scroll_button_threshold } = this.model.properties; - - if (auto_scroll) { - this.on_change(this.model.properties.children, () => { - this.scroll_to_latest(); - }); - } - if (scroll_button_threshold.get_value() > 0) { - this.on_change(scroll_button_threshold, () => { - this.toggle_scroll_arrow(); - }); - } - } - render(): void { super.render() this.empty() @@ -48,22 +43,17 @@ export class ColumnView extends BkColumnView { this._apply_visible() this.class_list.add(...this.css_classes()) + this.scroll_down_arrow_el = DOM.createElement('div', { class: 'scroll-button' }); + this.shadow_el.appendChild(this.scroll_down_arrow_el); - const scrollThreshold = this.model.properties.scroll_button_threshold; - if (scrollThreshold.get_value() > 0) { - this.scroll_down_arrow_el = DOM.createElement('div', { class: 'scroll-button' }); - this.shadow_el.appendChild(this.scroll_down_arrow_el); - - this.el.addEventListener("scroll", () => { - this.toggle_scroll_arrow(); - }); - - this.scroll_down_arrow_el.addEventListener("click", () => { - this.scroll_to_latest(); - }); - } + this.el.addEventListener("scroll", () => { + this.toggle_scroll_arrow(); + }); + this.scroll_down_arrow_el.addEventListener("click", () => { + this.scroll_to_latest(); + }); - for (const child_view of this.child_views.slice(1)) { + for (const child_view of this.child_views) { this.shadow_el.appendChild(child_view.el) child_view.render() child_view.after_render() From e901c1fe2eaa10d6a44e2b5df4e7472e14c9051b Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 19 Jul 2023 11:22:42 -0400 Subject: [PATCH 19/31] Fix compile issues --- panel/models/column.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/panel/models/column.ts b/panel/models/column.ts index f9f11a625c..0d2d327c16 100644 --- a/panel/models/column.ts +++ b/panel/models/column.ts @@ -16,7 +16,7 @@ export class ColumnView extends BkColumnView { } scroll_to_latest(): void { - if (!this.model.auto_scroll) + if (!this.model.properties.auto_scroll) return // Waits for the child to be rendered before scrolling @@ -26,9 +26,8 @@ export class ColumnView extends BkColumnView { } toggle_scroll_arrow(): void { - const threshold = this.model.scroll_button_threshold + const threshold = this.model.properties.scroll_button_threshold.get_value() const scrollDistanceFromBottom = this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight; - console.log(scrollDistanceFromBottom, (threshold !== 0) && (scrollDistanceFromBottom >= threshold)) this.scroll_down_arrow_el.classList.toggle( "visible", (threshold !== 0) && (scrollDistanceFromBottom >= threshold) ) From 99508228d5200d8a0f0be90d2ea3c6d0b3f65c3b Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 19 Jul 2023 17:48:26 +0200 Subject: [PATCH 20/31] Fix and revert --- panel/models/column.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/panel/models/column.ts b/panel/models/column.ts index 0d2d327c16..a15720c9dd 100644 --- a/panel/models/column.ts +++ b/panel/models/column.ts @@ -16,7 +16,7 @@ export class ColumnView extends BkColumnView { } scroll_to_latest(): void { - if (!this.model.properties.auto_scroll) + if (!this.model.auto_scroll) return // Waits for the child to be rendered before scrolling @@ -26,7 +26,7 @@ export class ColumnView extends BkColumnView { } toggle_scroll_arrow(): void { - const threshold = this.model.properties.scroll_button_threshold.get_value() + const threshold = this.model.scroll_button_threshold const scrollDistanceFromBottom = this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight; this.scroll_down_arrow_el.classList.toggle( "visible", (threshold !== 0) && (scrollDistanceFromBottom >= threshold) @@ -69,7 +69,7 @@ export namespace Column { }; } -export interface Column extends BkColumn.Attrs { } +export interface Column extends Column.Attrs { } export class Column extends BkColumn { properties: Column.Props; From cd6c396a2aa8f558f5106940a9212fa8693489a1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 19 Jul 2023 15:49:38 +0000 Subject: [PATCH 21/31] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- panel/dist/css/listpanel.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/panel/dist/css/listpanel.css b/panel/dist/css/listpanel.css index 12a2486623..2cb41f8d00 100644 --- a/panel/dist/css/listpanel.css +++ b/panel/dist/css/listpanel.css @@ -30,7 +30,9 @@ z-index: 9999; /* For animation */ opacity: 0; - transition: visibility 0s, opacity 0.2s ease-in-out; + transition: + visibility 0s, + opacity 0.2s ease-in-out; } .visible { From 3954d9a7648299fea582ecf4b5ed0e0580207219 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 19 Jul 2023 12:08:58 -0400 Subject: [PATCH 22/31] Fix subtle bug :D --- panel/models/column.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/panel/models/column.ts b/panel/models/column.ts index a15720c9dd..5065e86046 100644 --- a/panel/models/column.ts +++ b/panel/models/column.ts @@ -62,7 +62,6 @@ export class ColumnView extends BkColumnView { export namespace Column { export type Attrs = p.AttrsOf; - export type Props = BkColumn.Props & { auto_scroll: p.Property; scroll_button_threshold: p.Property; From 79c3c64e7d65b30bd581b29da93004b775cac090 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 19 Jul 2023 12:28:50 -0400 Subject: [PATCH 23/31] Negative margin --- panel/dist/css/listpanel.css | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/panel/dist/css/listpanel.css b/panel/dist/css/listpanel.css index 2cb41f8d00..7727e133cb 100644 --- a/panel/dist/css/listpanel.css +++ b/panel/dist/css/listpanel.css @@ -24,15 +24,14 @@ color: white; width: 36px; height: 36px; + margin-bottom: -36px; /* Remove space taken */ display: flex; align-items: center; justify-content: center; z-index: 9999; /* For animation */ opacity: 0; - transition: - visibility 0s, - opacity 0.2s ease-in-out; + transition: visibility 0s, opacity 0.2s ease-in-out; } .visible { From 32da87a920f8e6d1bf70cb5aa72b80e1041dd1f6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 19 Jul 2023 16:31:31 +0000 Subject: [PATCH 24/31] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- panel/dist/css/listpanel.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/panel/dist/css/listpanel.css b/panel/dist/css/listpanel.css index 7727e133cb..f939d2419a 100644 --- a/panel/dist/css/listpanel.css +++ b/panel/dist/css/listpanel.css @@ -31,7 +31,9 @@ z-index: 9999; /* For animation */ opacity: 0; - transition: visibility 0s, opacity 0.2s ease-in-out; + transition: + visibility 0s, + opacity 0.2s ease-in-out; } .visible { From 07c44e433e9d264781911c09c5223224ed986a98 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 19 Jul 2023 16:38:28 -0400 Subject: [PATCH 25/31] Update CSS --- panel/dist/css/listpanel.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/panel/dist/css/listpanel.css b/panel/dist/css/listpanel.css index f939d2419a..15d5c77d55 100644 --- a/panel/dist/css/listpanel.css +++ b/panel/dist/css/listpanel.css @@ -13,7 +13,7 @@ .scroll-button { /* For location */ position: sticky; - top: calc(100% - 35px); + top: calc(100% - 38px); left: calc(100% - 60px); /* For icon */ cursor: pointer; @@ -23,7 +23,7 @@ background-color: rgba(0, 0, 0, 0.25); color: white; width: 36px; - height: 36px; + min-height: 36px; margin-bottom: -36px; /* Remove space taken */ display: flex; align-items: center; From 189b4a3777c91efadf1cec7e77b31131f976b86c Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Fri, 21 Jul 2023 12:28:43 -0400 Subject: [PATCH 26/31] Fix tests --- panel/tests/ui/layout/test_column.py | 16 +++++++++++----- panel/tests/ui/layout/test_row.py | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/panel/tests/ui/layout/test_column.py b/panel/tests/ui/layout/test_column.py index 11df5f7e56..31cfcfb1b1 100644 --- a/panel/tests/ui/layout/test_column.py +++ b/panel/tests/ui/layout/test_column.py @@ -52,11 +52,14 @@ def test_column_auto_scroll(page, port): assert 'scrollable-vertical' in column.get_attribute('class') - scroll_loc = column.scrollTop + scroll_loc = column.evaluate('(el) => el.scrollTop') assert scroll_loc == 0 col.append(Spacer(styles=dict(background='yellow'), width=200, height=200)) - new_scroll_loc = column.scrollTop + + time.sleep(0.5) + + new_scroll_loc = column.evaluate('(el) => el.scrollTop') assert new_scroll_loc > scroll_loc @@ -65,7 +68,7 @@ def test_column_scroll_button_threshold(page, port): Spacer(styles=dict(background='red'), width=200, height=200), Spacer(styles=dict(background='green'), width=200, height=200), Spacer(styles=dict(background='blue'), width=200, height=200), - scroll_button_threshold=0.5, height=420 + scroll_button_threshold=10, height=420 ) serve(col, port=port, threaded=True, show=False) @@ -83,7 +86,10 @@ def test_column_scroll_button_threshold(page, port): assert 'scrollable-vertical' in column.get_attribute('class') # trigger scroll event - column.scrollTop = 1 + column.evaluate('(el) => el.scrollTop = 5') + + time.sleep(0.5) + scroll_arrow = page.locator(".scroll-button") - assert scroll_arrow.get_attribute('class') == 'scroll-button' + assert scroll_arrow.get_attribute('class') == 'scroll-button visible' assert scroll_arrow.is_visible() diff --git a/panel/tests/ui/layout/test_row.py b/panel/tests/ui/layout/test_row.py index 287a75ed79..9c12eb78ab 100644 --- a/panel/tests/ui/layout/test_row.py +++ b/panel/tests/ui/layout/test_row.py @@ -21,7 +21,7 @@ def test_row_scroll(page, port): page.goto(f"http://localhost:{port}") - row_el = page.locator(".bk-Row").bounding_box() + row_el = page.locator(".bk-Row") bbox = row_el.bounding_box() assert bbox['width'] == 420 From 469aa9c7f0f4cde926bdb0b5b435dc9663f9046a Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 24 Jul 2023 08:41:35 -0400 Subject: [PATCH 27/31] Maybe fix UI test --- panel/tests/ui/layout/test_widgetbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panel/tests/ui/layout/test_widgetbox.py b/panel/tests/ui/layout/test_widgetbox.py index 4caed1108b..6191c9a278 100644 --- a/panel/tests/ui/layout/test_widgetbox.py +++ b/panel/tests/ui/layout/test_widgetbox.py @@ -21,7 +21,7 @@ def test_widgetbox_vertical_scroll(page, port): page.goto(f"http://localhost:{port}") - bbox = page.locator(".bk-Column").bounding_box() + bbox = page.locator(".bk-panel-models-layout-Column").bounding_box() assert bbox['width'] in (202, 217) # Ignore if browser hides empty scrollbar assert bbox['height'] == 420 From 4b6f89017e1b4db3157c0f96d53d99ee3a84c01f Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 24 Jul 2023 10:50:32 -0400 Subject: [PATCH 28/31] Fix data types and logic issues, enhance auto_scroll with limit, make button render on init --- panel/layout/base.py | 15 +++-- panel/models/column.ts | 32 +++++++--- panel/models/layout.py | 19 ++++-- panel/tests/layout/test_base.py | 2 +- panel/tests/ui/layout/test_column.py | 96 ++++++++++++++++++++++++++-- 5 files changed, 135 insertions(+), 29 deletions(-) diff --git a/panel/layout/base.py b/panel/layout/base.py index 7194706700..7e69bb5faf 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -886,12 +886,15 @@ class Column(ListPanel): >>> pn.Column(some_widget, some_pane, some_python_object) """ - auto_scroll = param.Boolean(default=False, doc=""" - Whether to scroll to the last object on update.""") + auto_scroll_limit = param.Integer(bounds=(0, None), doc=""" + Max pixel distance from the latest object in the Column to + activate automatic scrolling upon update. Setting to 0 + disables auto-scrolling.""") scroll_button_threshold = param.Integer(bounds=(0, None), doc=""" - Threshold for showing button that scrolls to the bottom on click. - The button will be hidden if set to 0.""") + Min pixel distance from the latest object in the Column to + display the scroll button. Setting to 0 + disables the scroll button.""") _bokeh_model: ClassVar[Type[Model]] = PnColumn @@ -899,9 +902,9 @@ class Column(ListPanel): _stylesheets: ClassVar[list[str]] = [f'{CDN_DIST}css/listpanel.css'] - @param.depends("auto_scroll", "scroll_button_threshold", watch=True, on_init=True) + @param.depends("auto_scroll_limit", "scroll_button_threshold", watch=True, on_init=True) def _set_scrollable(self): - self.scroll = self.scroll or self.auto_scroll or bool(self.scroll_button_threshold) + self.scroll = self.scroll or bool(self.auto_scroll_limit) or bool(self.scroll_button_threshold) class WidgetBox(ListPanel): diff --git a/panel/models/column.ts b/panel/models/column.ts index 5065e86046..61d06d6ac3 100644 --- a/panel/models/column.ts +++ b/panel/models/column.ts @@ -11,25 +11,35 @@ export class ColumnView extends BkColumnView { const { children, scroll_button_threshold } = this.model.properties; - this.on_change(children, () => this.scroll_to_latest()); + this.on_change(children, () => this.trigger_auto_scroll()); this.on_change(scroll_button_threshold, () => this.toggle_scroll_arrow()) } - scroll_to_latest(): void { - if (!this.model.auto_scroll) - return + get distance_from_latest(): number { + return this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight; + } + scroll_to_latest(): void { // Waits for the child to be rendered before scrolling requestAnimationFrame(() => { this.el.scrollTop = this.el.scrollHeight; }); } + trigger_auto_scroll(): void { + const limit = this.model.auto_scroll_limit + const within_limit = this.distance_from_latest <= limit + if (limit == 0 || !within_limit) + return + + this.scroll_to_latest() + } + toggle_scroll_arrow(): void { const threshold = this.model.scroll_button_threshold - const scrollDistanceFromBottom = this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight; + const exceeds_threshold = this.distance_from_latest >= threshold this.scroll_down_arrow_el.classList.toggle( - "visible", (threshold !== 0) && (scrollDistanceFromBottom >= threshold) + "visible", threshold !== 0 && exceeds_threshold ) } @@ -57,13 +67,17 @@ export class ColumnView extends BkColumnView { child_view.render() child_view.after_render() } + + requestAnimationFrame(() => { + this.toggle_scroll_arrow(); + }); } } export namespace Column { export type Attrs = p.AttrsOf; export type Props = BkColumn.Props & { - auto_scroll: p.Property; + auto_scroll_limit: p.Property; scroll_button_threshold: p.Property; }; } @@ -82,8 +96,8 @@ export class Column extends BkColumn { static { this.prototype.default_view = ColumnView; - this.define(({ Boolean, Int }) => ({ - auto_scroll: [Boolean, false], + this.define(({ Int }) => ({ + auto_scroll_limit: [Int, 0], scroll_button_threshold: [Int, 0], })); } diff --git a/panel/models/layout.py b/panel/models/layout.py index bec95cd286..698bcc7a84 100644 --- a/panel/models/layout.py +++ b/panel/models/layout.py @@ -1,5 +1,5 @@ from bokeh.core.properties import ( - Bool, Float, List, Nullable, String, + Bool, Int, List, Nullable, String, ) from bokeh.models import Column as BkColumn from bokeh.models.layouts import LayoutDOM @@ -17,14 +17,19 @@ class HTMLBox(LayoutDOM): class Column(BkColumn): - auto_scroll = Bool(False, help="Whether to scroll to the latest row on update.") + auto_scroll_limit = Int( + default=0, + help=""" + Max pixel distance from the latest object in the Column to + activate automatic scrolling upon update. Setting to 0 + disables auto-scrolling.""") - scroll_button_threshold = Float( + scroll_button_threshold = Int( + default=0, help=""" - Threshold for showing scroll arrow that scrolls to the latest on click. - The arrow will be hidden if set to 0. - """, - ) + Min pixel distance from the latest object in the Column to + display the scroll button. Setting to 0 + disables the scroll button.""") class Card(Column): diff --git a/panel/tests/layout/test_base.py b/panel/tests/layout/test_base.py index fabe3f73f0..b13814fe52 100644 --- a/panel/tests/layout/test_base.py +++ b/panel/tests/layout/test_base.py @@ -569,7 +569,7 @@ def test_no_expand_fixed(panel, document, comm): assert model.sizing_mode == 'fixed' -@pytest.mark.parametrize('scroll_param', ["auto_scroll", "scroll", "scroll_button_threshold"]) +@pytest.mark.parametrize('scroll_param', ["auto_scroll_limit", "scroll", "scroll_button_threshold"]) def test_column_scroll_params_sets_scroll(scroll_param, document, comm): if scroll_param != "scroll_button_threshold": params = {scroll_param: True} diff --git a/panel/tests/ui/layout/test_column.py b/panel/tests/ui/layout/test_column.py index 31cfcfb1b1..906e1d0625 100644 --- a/panel/tests/ui/layout/test_column.py +++ b/panel/tests/ui/layout/test_column.py @@ -30,12 +30,12 @@ def test_column_scroll(page, port): assert 'scrollable-vertical' in col_el.get_attribute('class') -def test_column_auto_scroll(page, port): +def test_column_auto_scroll_limit(page, port): col = Column( Spacer(styles=dict(background='red'), width=200, height=200), Spacer(styles=dict(background='green'), width=200, height=200), Spacer(styles=dict(background='blue'), width=200, height=200), - auto_scroll=True, height=420 + auto_scroll_limit=100, height=420 ) serve(col, port=port, threaded=True, show=False) @@ -55,12 +55,54 @@ def test_column_auto_scroll(page, port): scroll_loc = column.evaluate('(el) => el.scrollTop') assert scroll_loc == 0 + # assert scroll location is still at top col.append(Spacer(styles=dict(background='yellow'), width=200, height=200)) + time.sleep(1) + new_scroll_loc = column.evaluate('(el) => el.scrollTop') + assert new_scroll_loc == scroll_loc + + # scroll to close to bottom + column.evaluate('(el) => el.scrollTop = el.scrollHeight') + + # assert auto scroll works; i.e. distance from bottom is 0 + col.append(Spacer(styles=dict(background='yellow'), width=200, height=200)) + time.sleep(1) + distance_from_bottom = column.evaluate( + '(el) => el.scrollHeight - el.scrollTop - el.clientHeight' + ) + assert distance_from_bottom == 0 + + +def test_column_auto_scroll_limit_disabled(page, port): + col = Column( + Spacer(styles=dict(background='red'), width=200, height=200), + Spacer(styles=dict(background='green'), width=200, height=200), + Spacer(styles=dict(background='blue'), width=200, height=200), + auto_scroll_limit=0, height=420, scroll=True + ) + + serve(col, port=port, threaded=True, show=False) time.sleep(0.5) + page.goto(f"http://localhost:{port}") + + column = page.locator(".bk-panel-models-layout-Column") + bbox = column.bounding_box() + + assert bbox['width'] in (200, 215) # Ignore if browser hides empty scrollbar + assert bbox['height'] == 420 + + assert 'scrollable-vertical' in column.get_attribute('class') + + scroll_loc = column.evaluate('(el) => el.scrollTop') + assert scroll_loc == 0 + + # assert scroll location is still at top + col.append(Spacer(styles=dict(background='yellow'), width=200, height=200)) + time.sleep(1) new_scroll_loc = column.evaluate('(el) => el.scrollTop') - assert new_scroll_loc > scroll_loc + assert new_scroll_loc == scroll_loc def test_column_scroll_button_threshold(page, port): @@ -85,11 +127,53 @@ def test_column_scroll_button_threshold(page, port): assert 'scrollable-vertical' in column.get_attribute('class') - # trigger scroll event - column.evaluate('(el) => el.scrollTop = 5') + # assert scroll button is visible on render + scroll_arrow = page.locator(".scroll-button") + assert scroll_arrow.get_attribute('class') == 'scroll-button visible' + assert scroll_arrow.is_visible() + # assert scroll button is invisible at bottom of page + column.evaluate('(el) => el.scrollTop = el.scrollHeight') time.sleep(0.5) + assert scroll_arrow.get_attribute('class') == 'scroll-button' + assert not scroll_arrow.is_visible() - scroll_arrow = page.locator(".scroll-button") + # assert scroll button is visible beyond threshold + column.evaluate('(el) => el.scrollTop = 5') + time.sleep(0.5) assert scroll_arrow.get_attribute('class') == 'scroll-button visible' assert scroll_arrow.is_visible() + + +def test_column_scroll_button_threshold_disabled(page, port): + col = Column( + Spacer(styles=dict(background='red'), width=200, height=200), + Spacer(styles=dict(background='green'), width=200, height=200), + Spacer(styles=dict(background='blue'), width=200, height=200), + scroll=True, scroll_button_threshold=0, height=420 + ) + + serve(col, port=port, threaded=True, show=False) + + time.sleep(0.5) + + page.goto(f"http://localhost:{port}") + + column = page.locator(".bk-panel-models-layout-Column") + bbox = column.bounding_box() + + assert bbox['width'] in (200, 215) # Ignore if browser hides empty scrollbar + assert bbox['height'] == 420 + + assert 'scrollable-vertical' in column.get_attribute('class') + + # assert scroll button is invisible on render + scroll_arrow = page.locator(".scroll-button") + assert scroll_arrow.get_attribute('class') == 'scroll-button' + assert not scroll_arrow.is_visible() + + # assert scroll button is visible beyond threshold + column.evaluate('(el) => el.scrollTop = 5') + time.sleep(0.5) + assert scroll_arrow.get_attribute('class') == 'scroll-button' + assert not scroll_arrow.is_visible() From 0aa5ec40796ea96d2666ea46690f16b82d1f4d93 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 24 Jul 2023 12:18:10 -0400 Subject: [PATCH 29/31] Add view latest --- examples/reference/layouts/Column.ipynb | 4 +++- panel/layout/base.py | 4 ++++ panel/models/column.ts | 10 ++++++++- panel/models/layout.py | 5 +++++ panel/tests/layout/test_base.py | 4 ++-- panel/tests/ui/layout/test_column.py | 27 +++++++++++++++++++++++++ 6 files changed, 50 insertions(+), 4 deletions(-) diff --git a/examples/reference/layouts/Column.ipynb b/examples/reference/layouts/Column.ipynb index 9ab8730153..de9bdb7a4a 100644 --- a/examples/reference/layouts/Column.ipynb +++ b/examples/reference/layouts/Column.ipynb @@ -22,7 +22,9 @@ "\n", "* **``objects``** (list): The list of objects to display in the Column, should not generally be modified directly except when replaced in its entirety.\n", "* **``scroll``** (boolean): Enable scrollbars if the content overflows the size of the container.\n", - "\n", + "* **``auto_scroll_limit``** (int): Max pixel distance from the latest object in the Column to activate automatic scrolling upon update. Setting to 0 disables auto-scrolling\n", + "* **``scroll_button_threshold``** (int): Min pixel distance from the latest object in the Column to display the scroll button. Setting to 0 disables the scroll button.\"\"\"\n", + "* **``view_latest``** (bool): Whether to scroll to the latest object on init. If not enabled the view will be on the first object.\"\"\"\n", "___" ] }, diff --git a/panel/layout/base.py b/panel/layout/base.py index 7e69bb5faf..78dfdf7ae0 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -896,6 +896,10 @@ class Column(ListPanel): display the scroll button. Setting to 0 disables the scroll button.""") + view_latest = param.Boolean(default=False, doc=""" + Whether to scroll to the latest object on init. If not + enabled the view will be on the first object.""") + _bokeh_model: ClassVar[Type[Model]] = PnColumn _direction = 'vertical' diff --git a/panel/models/column.ts b/panel/models/column.ts index 61d06d6ac3..a7c6430907 100644 --- a/panel/models/column.ts +++ b/panel/models/column.ts @@ -67,8 +67,14 @@ export class ColumnView extends BkColumnView { child_view.render() child_view.after_render() } + } + after_render(): void { + super.after_render() requestAnimationFrame(() => { + if (this.model.view_latest) { + this.scroll_to_latest(); + } this.toggle_scroll_arrow(); }); } @@ -79,6 +85,7 @@ export namespace Column { export type Props = BkColumn.Props & { auto_scroll_limit: p.Property; scroll_button_threshold: p.Property; + view_latest: p.Property; }; } @@ -96,9 +103,10 @@ export class Column extends BkColumn { static { this.prototype.default_view = ColumnView; - this.define(({ Int }) => ({ + this.define(({ Int, Boolean }) => ({ auto_scroll_limit: [Int, 0], scroll_button_threshold: [Int, 0], + view_latest: [Boolean, false], })); } } diff --git a/panel/models/layout.py b/panel/models/layout.py index 698bcc7a84..b0bfd4738d 100644 --- a/panel/models/layout.py +++ b/panel/models/layout.py @@ -31,6 +31,11 @@ class Column(BkColumn): display the scroll button. Setting to 0 disables the scroll button.""") + view_latest = Bool( + default=False, + help=""" + Whether to scroll to the latest object on init. If not + enabled the view will be on the first object.""") class Card(Column): active_header_background = Nullable( diff --git a/panel/tests/layout/test_base.py b/panel/tests/layout/test_base.py index b13814fe52..e0f7c893ac 100644 --- a/panel/tests/layout/test_base.py +++ b/panel/tests/layout/test_base.py @@ -569,9 +569,9 @@ def test_no_expand_fixed(panel, document, comm): assert model.sizing_mode == 'fixed' -@pytest.mark.parametrize('scroll_param', ["auto_scroll_limit", "scroll", "scroll_button_threshold"]) +@pytest.mark.parametrize('scroll_param', ["auto_scroll_limit", "scroll", "scroll_button_threshold", "view_latest"]) def test_column_scroll_params_sets_scroll(scroll_param, document, comm): - if scroll_param != "scroll_button_threshold": + if scroll_param in ["auto_scroll_limit", "scroll_button_threshold"]: params = {scroll_param: True} else: params = {scroll_param: 1} diff --git a/panel/tests/ui/layout/test_column.py b/panel/tests/ui/layout/test_column.py index 906e1d0625..6b49bed2b9 100644 --- a/panel/tests/ui/layout/test_column.py +++ b/panel/tests/ui/layout/test_column.py @@ -177,3 +177,30 @@ def test_column_scroll_button_threshold_disabled(page, port): time.sleep(0.5) assert scroll_arrow.get_attribute('class') == 'scroll-button' assert not scroll_arrow.is_visible() + + +def test_column_view_latest(page, port): + col = Column( + Spacer(styles=dict(background='red'), width=200, height=200), + Spacer(styles=dict(background='green'), width=200, height=200), + Spacer(styles=dict(background='blue'), width=200, height=200), + view_latest=True, scroll=True, height=420 + ) + + serve(col, port=port, threaded=True, show=False) + + time.sleep(0.5) + + page.goto(f"http://localhost:{port}") + + column = page.locator(".bk-panel-models-layout-Column") + bbox = column.bounding_box() + + assert bbox['width'] in (200, 215) # Ignore if browser hides empty scrollbar + assert bbox['height'] == 420 + + assert 'scrollable-vertical' in column.get_attribute('class') + + # assert scroll location does not start at top + scroll_loc = column.evaluate('(el) => el.scrollTop') + assert scroll_loc != 0 From 3de5c2e07164fc71abeaa1957d16fccd787fe1ff Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 24 Jul 2023 12:26:52 -0400 Subject: [PATCH 30/31] Reverse logic in test and fix scroll --- panel/layout/base.py | 9 +++++++-- panel/tests/layout/test_base.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/panel/layout/base.py b/panel/layout/base.py index 78dfdf7ae0..17a68ac31d 100644 --- a/panel/layout/base.py +++ b/panel/layout/base.py @@ -906,9 +906,14 @@ class Column(ListPanel): _stylesheets: ClassVar[list[str]] = [f'{CDN_DIST}css/listpanel.css'] - @param.depends("auto_scroll_limit", "scroll_button_threshold", watch=True, on_init=True) + @param.depends("auto_scroll_limit", "scroll_button_threshold", "view_latest", watch=True, on_init=True) def _set_scrollable(self): - self.scroll = self.scroll or bool(self.auto_scroll_limit) or bool(self.scroll_button_threshold) + self.scroll = ( + self.scroll or + bool(self.auto_scroll_limit) or + bool(self.scroll_button_threshold) or + self.view_latest + ) class WidgetBox(ListPanel): diff --git a/panel/tests/layout/test_base.py b/panel/tests/layout/test_base.py index e0f7c893ac..f37395b410 100644 --- a/panel/tests/layout/test_base.py +++ b/panel/tests/layout/test_base.py @@ -571,7 +571,7 @@ def test_no_expand_fixed(panel, document, comm): @pytest.mark.parametrize('scroll_param', ["auto_scroll_limit", "scroll", "scroll_button_threshold", "view_latest"]) def test_column_scroll_params_sets_scroll(scroll_param, document, comm): - if scroll_param in ["auto_scroll_limit", "scroll_button_threshold"]: + if scroll_param not in ["auto_scroll_limit", "scroll_button_threshold"]: params = {scroll_param: True} else: params = {scroll_param: 1} From 23e3d4e2580c69c41af773260c5dd89f61672cf0 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Sun, 23 Jul 2023 22:29:13 -0400 Subject: [PATCH 31/31] Inherit new scroll func in chatlog --- panel/widgets/chatbox.py | 55 +++++++++++++++------------------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/panel/widgets/chatbox.py b/panel/widgets/chatbox.py index cb3564df96..6d8d84c407 100644 --- a/panel/widgets/chatbox.py +++ b/panel/widgets/chatbox.py @@ -73,7 +73,7 @@ def __init__( ): bubble_styles = { "overflow-x": "auto", - "overflow-y": "auto", + "overflow-y": "scroll", "box-shadow": "rgba(0, 0, 0, 0.15) 1.95px 1.95px 2.6px;", "padding": "0.5em", } @@ -108,6 +108,7 @@ def __init__( align="center", margin=8, styles=bubble_styles, + sizing_mode="stretch_width", ) # create heart icon next to chat @@ -136,8 +137,9 @@ def __init__( row_objects = row_objects[::-1] container_params = dict( - sizing_mode="fixed", + styles={"overflow-y": "auto", "max-height": "300px"}, align=(horizontal_align, "center"), + sizing_mode="fixed", ) row = Row(*row_objects, **container_params) if show_name: @@ -173,6 +175,8 @@ def _serialize_obj( stylesheets = ["p { margin-block-start: 0.2em; margin-block-end: 0.2em;}"] text_styles = {"color": self._bubble_styles.get("color")} try: + if isinstance(obj, str): + panel_obj = StaticText(value=obj, stylesheets=stylesheets, styles=text_styles) if self.default_message_callable is None or issubclass( self.default_message_callable, PaneBase ): @@ -183,12 +187,11 @@ def _serialize_obj( panel_obj = self.default_message_callable(value=obj) except ValueError: panel_obj = _panel(obj, stylesheets=stylesheets, styles=text_styles) - - if panel_obj.sizing_mode is None: - panel_obj.sizing_mode = "stretch_width" + panel_obj.sizing_mode = "stretch_width" if "overflow-wrap" not in panel_obj.styles: panel_obj.styles.update({"overflow-wrap": "break-word"}) + return panel_obj def _update_like(self, event: param.parameterized.Event): @@ -281,21 +284,24 @@ def __init__(self, **params): "flex-direction": "column" if self.ascending else "column-reverse", }, ) + + sizing_mode = chat_layout.get("sizing_mode", None) + if sizing_mode == "fixed" and "height" not in chat_layout: + chat_layout["height"] = 500 + elif sizing_mode is None or sizing_mode == "stretch_width": + chat_layout["height"] = 500 + self._chat_title = StaticText( value=f"{self.name}", styles={"font-size": "1.5em"}, align="center", ) - self._chat_log = Column(**chat_layout) - self._scroll_button = Button( - name="Scroll to latest", - align="center", - sizing_mode="fixed", - width=115, - height=35, - margin=0, + self._chat_log = Column( + scroll=True, + auto_scroll=True, + scroll_button_threshold=20, + **chat_layout ) - self._add_scroll_callback(self._scroll_button, "clicks") self._current_hue = self.message_hue if self._current_hue: self._default_colors = self._generate_default_hsl(self._current_hue) @@ -304,10 +310,6 @@ def __init__(self, **params): box_objects = [self._chat_title] if self.name else [] box_objects.append(self._chat_log) - if self.ascending: - box_objects.insert(0, self._scroll_button) - else: - box_objects.append(self._scroll_button) if self.allow_input: self._attach_input(box_objects, layout) @@ -334,21 +336,6 @@ def _generate_default_hsl(self, hue: int | None, increment: int = 0) -> List[str (f"hsl({hue}, 15%, 60%)", "white"), ] - def _add_scroll_callback(self, obj: Viewable, what: str): - code = """ - const outerColumn = document.querySelector(".bk-Column") - const column = outerColumn.shadowRoot.querySelector(".bk-Column") - """ - if self.ascending: - code += "\ncolumn.scrollTop = column.scrollHeight" - else: - code += "\ncolumn.scrollTop = -column.scrollHeight" - - obj.jscallback( - args={"chat_log": self._chat_log}, - **{what: code}, - ) - def _link_disabled_loading(self, obj: Viewable): """ Link the disabled and loading attributes of the chat box to the @@ -374,7 +361,6 @@ def _attach_input(self, box_objects: List, layout: Dict[str, str]) -> None: button_type="default", sizing_mode="stretch_width", max_width=100, - height=35, ) self._send_button.on_click(self._enter_message) @@ -392,7 +378,6 @@ def _attach_input(self, box_objects: List, layout: Dict[str, str]) -> None: # the send button if isinstance(message_input, TextInput): message_input.param.watch(self._enter_message, "value") - self._add_scroll_callback(message_input, "value") send_button = self._send_button.clone() self._link_disabled_loading(send_button) message_row = Row(message_input, send_button, **row_layout)