Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add scroll position to control the location of Column's scroll #5403

Merged
merged 2 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/reference/layouts/Column.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"\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",
"* **``scroll_position``** (int): Current scroll position of the Column. Setting this value will update the scroll position of the Column. Setting to 0 will scroll to the top.\"\"\"\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",
Expand Down
15 changes: 14 additions & 1 deletion panel/layout/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,11 @@ class Column(ListPanel):
>>> pn.Column(some_widget, some_pane, some_python_object)
"""

scroll_position = param.Integer(default=None, doc="""
Current scroll position of the Column. Setting this value
will update the scroll position of the Column. Setting to
0 will scroll to the top.""")

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
Expand All @@ -906,10 +911,18 @@ class Column(ListPanel):

_stylesheets: ClassVar[list[str]] = [f'{CDN_DIST}css/listpanel.css']

@param.depends("auto_scroll_limit", "scroll_button_threshold", "view_latest", watch=True, on_init=True)
@param.depends(
"scroll_position",
"auto_scroll_limit",
"scroll_button_threshold",
"view_latest",
watch=True,
on_init=True
)
def _set_scrollable(self):
self.scroll = (
self.scroll or
bool(self.scroll_position) or
bool(self.auto_scroll_limit) or
bool(self.scroll_button_threshold) or
self.view_latest
Expand Down
19 changes: 18 additions & 1 deletion panel/models/column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,23 @@ export class ColumnView extends BkColumnView {
connect_signals(): void {
super.connect_signals();

const { children, scroll_button_threshold } = this.model.properties;
const { children, scroll_position, scroll_button_threshold } = this.model.properties;

this.on_change(children, () => this.trigger_auto_scroll());
this.on_change(scroll_position, () => this.scroll_to_position());
this.on_change(scroll_button_threshold, () => this.toggle_scroll_button())
}

get distance_from_latest(): number {
return this.el.scrollHeight - this.el.scrollTop - this.el.clientHeight;
}

scroll_to_position(): void {
requestAnimationFrame(() => {
this.el.scrollTop = this.model.scroll_position;
});
}

scroll_to_latest(): void {
// Waits for the child to be rendered before scrolling
requestAnimationFrame(() => {
Expand All @@ -35,6 +42,10 @@ export class ColumnView extends BkColumnView {
this.scroll_to_latest()
}

record_scroll_position(): void {
this.model.scroll_position = this.el.scrollTop;
}

toggle_scroll_button(): void {
const threshold = this.model.scroll_button_threshold
const exceeds_threshold = this.distance_from_latest >= threshold
Expand All @@ -58,6 +69,7 @@ export class ColumnView extends BkColumnView {
this.shadow_el.appendChild(this.scroll_down_button_el);

this.el.addEventListener("scroll", () => {
this.record_scroll_position();
this.toggle_scroll_button();
});
this.scroll_down_button_el.addEventListener("click", () => {
Expand All @@ -74,6 +86,9 @@ export class ColumnView extends BkColumnView {
after_render(): void {
super.after_render()
requestAnimationFrame(() => {
if (this.model.scroll_position) {
this.scroll_to_position();
}
if (this.model.view_latest) {
this.scroll_to_latest();
}
Expand All @@ -85,6 +100,7 @@ export class ColumnView extends BkColumnView {
export namespace Column {
export type Attrs = p.AttrsOf<Props>;
export type Props = BkColumn.Props & {
scroll_position: p.Property<number>;
auto_scroll_limit: p.Property<number>;
scroll_button_threshold: p.Property<number>;
view_latest: p.Property<boolean>;
Expand All @@ -106,6 +122,7 @@ export class Column extends BkColumn {
this.prototype.default_view = ColumnView;

this.define<Column.Props>(({ Int, Boolean }) => ({
scroll_position: [Int, 0],
auto_scroll_limit: [Int, 0],
scroll_button_threshold: [Int, 0],
view_latest: [Boolean, false],
Expand Down
9 changes: 9 additions & 0 deletions panel/models/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ class HTMLBox(LayoutDOM):

class Column(BkColumn):

scroll_position = Int(
default=0,
help="""
Current scroll position of the Column. Setting this value
will update the scroll position of the Column. Setting to
0 will scroll to the top."""
)


auto_scroll_limit = Int(
default=0,
help="""
Expand Down
69 changes: 69 additions & 0 deletions panel/tests/ui/layout/test_column.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,72 @@ def test_column_view_latest(page, port):
# assert scroll location does not start at top
scroll_loc = column.evaluate('(el) => el.scrollTop')
assert scroll_loc != 0

def test_column_scroll_position_init(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_position=100, 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")

# assert scroll position can be used to initialize scroll location
scroll_loc = column.evaluate('(el) => el.scrollTop')
assert scroll_loc == 100


def test_column_scroll_position_recorded(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, 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")

# change scroll location thru scrolling
column.evaluate('(el) => el.scrollTop = 150')
time.sleep(0.5)

# assert scroll position is synced and recorded at 150
assert col.scroll_position == 150


def test_column_scroll_position_param_updated(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, 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")

# change scroll location thru param
col.scroll_position = 175
time.sleep(0.5)

# assert scroll location is synced and recorded at 175
scroll_loc = column.evaluate('(el) => el.scrollTop')
assert scroll_loc == 175