Skip to content

Commit

Permalink
Add scroll_into_view command (#315)
Browse files Browse the repository at this point in the history
  • Loading branch information
wwwillchen authored May 28, 2024
1 parent 60c788a commit a10f247
Show file tree
Hide file tree
Showing 14 changed files with 135 additions and 9 deletions.
33 changes: 33 additions & 0 deletions docs/guides/commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Commands

Commands are actions that you typically call within an event handler.

## Navigate

To navigate to another page, you can use `me.navigate`. This is particularly useful for navigating across a [multi-page](./pages.md) app.

### Example

```py

me.navigate('/path/to/navigate')

```

### API

::: mesop.commands.navigate.navigate

## Scroll into view

If you want to scroll a component into the viewport, you can use `me.scroll_into_view` which scrolls the component with the specified key into the viewport.

### Example

```python
--8<-- "mesop/examples/scroll_into_view.py"
```

### API

::: mesop.commands.scroll_into_view.scroll_into_view
1 change: 1 addition & 0 deletions mesop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from mesop.colab.colab_run import colab_run as colab_run
from mesop.colab.colab_show import colab_show as colab_show
from mesop.commands.navigate import navigate as navigate
from mesop.commands.scroll_into_view import scroll_into_view as scroll_into_view
from mesop.component_helpers import (
Border as Border,
)
Expand Down
6 changes: 6 additions & 0 deletions mesop/commands/navigate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@


def navigate(url: str) -> None:
"""
Navigates to the given URL.
Args:
url: The URL to navigate to.
"""
runtime().context().navigate(url)
14 changes: 14 additions & 0 deletions mesop/commands/scroll_into_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from mesop.runtime import runtime


def scroll_into_view(key: str) -> None:
"""
Scrolls so the component specified by the key is in the viewport.
Args:
key: The unique identifier of the component to scroll to.
This key should be globally unique to prevent unexpected behavior.
If multiple components share the same key, the first component
instance found in the component tree will be scrolled to.
"""
runtime().context().scroll_into_view(key)
1 change: 1 addition & 0 deletions mesop/examples/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from mesop.examples import playground as playground
from mesop.examples import playground_critic as playground_critic
from mesop.examples import readme_app as readme_app
from mesop.examples import scroll_into_view as scroll_into_view
from mesop.examples import sxs as sxs
from mesop.examples import testing as testing

Expand Down
21 changes: 21 additions & 0 deletions mesop/examples/scroll_into_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import mesop as me


@me.page(path="/scroll_into_view")
def app():
me.button("Scroll to middle line", on_click=scroll_to_middle)
me.button("Scroll to bottom line", on_click=scroll_to_bottom)
for _ in range(100):
me.text("Filler line")
me.text("middle_line", key="middle_line")
for _ in range(100):
me.text("Filler line")
me.text("bottom_line", key="bottom_line")


def scroll_to_middle(e: me.ClickEvent):
me.scroll_into_view(key="middle_line")


def scroll_to_bottom(e: me.ClickEvent):
me.scroll_into_view(key="bottom_line")
6 changes: 6 additions & 0 deletions mesop/protos/ui.proto
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ message RenderEvent {
message Command {
oneof command {
NavigateCommand navigate = 1;
ScrollIntoViewCommand scroll_into_view = 2;
}
}

Expand All @@ -165,6 +166,11 @@ message NavigateCommand {
optional string url = 1;
}

message ScrollIntoViewCommand {
// Key of the component to scroll into view
optional string key = 1;
}

message States {
repeated State states = 1;
}
Expand Down
5 changes: 5 additions & 0 deletions mesop/runtime/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ def commands(self) -> list[pb.Command]:
def navigate(self, url: str) -> None:
self._commands.append(pb.Command(navigate=pb.NavigateCommand(url=url)))

def scroll_into_view(self, key: str) -> None:
self._commands.append(
pb.Command(scroll_into_view=pb.ScrollIntoViewCommand(key=key))
)

def register_event_handler(self, fn_id: str, handler: Handler) -> None:
if self._trace_mode:
self._handlers[fn_id] = handler
Expand Down
12 changes: 12 additions & 0 deletions mesop/tests/e2e/scroll_into_view_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {test, expect} from '@playwright/test';

test('scroll_into_view', async ({page}) => {
await page.goto('/scroll_into_view');
await page.setViewportSize({width: 200, height: 200});

await expect(page.getByText('bottom_line')).not.toBeInViewport();

await page.getByRole('button', {name: 'Scroll to bottom line'}).click();

await expect(page.getByText('bottom_line')).toBeInViewport();
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<!-- This DOM node is a hack to get a reference to the ViewContainerRef; need
to *not* render it, otherwise it causes weird layout issues (e.g. flexbox) -->
<span style="display: none" #insertion></span>
<span style="display: none" [attr.data-key]="getKey()" #insertion></span>

<ng-template #childrenTemplate>
<ng-container
Expand Down
4 changes: 4 additions & 0 deletions mesop/web/src/component_renderer/component_renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ export class ComponentRenderer {
}
}

getKey() {
return this.component.getKey()?.getKey();
}

trackByFn(index: any, item: ComponentProto) {
const key = item.getKey()?.getKey();
if (key) {
Expand Down
10 changes: 4 additions & 6 deletions mesop/web/src/services/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
NavigationEvent,
ComponentConfig,
EditorEvent,
Command,
} from 'mesop/mesop/protos/ui_jspb_proto_pb/mesop/protos/ui_pb';
import {Logger} from '../dev_tools/services/logger';
import {Title} from '@angular/platform-browser';
Expand All @@ -26,7 +27,7 @@ interface InitParams {
componentConfigs: readonly ComponentConfig[],
) => void;
onError: (error: ServerError) => void;
onNavigate: (route: string) => void;
onCommand: (command: Command) => void;
}

export enum ChannelStatus {
Expand Down Expand Up @@ -81,7 +82,7 @@ export class Channel {
this.status = ChannelStatus.OPEN;
this.logger.log({type: 'StreamStart'});

const {zone, onRender, onError, onNavigate} = initParams;
const {zone, onRender, onError, onCommand} = initParams;
this.initParams = initParams;

this.eventSource.addEventListener('message', (e) => {
Expand Down Expand Up @@ -110,10 +111,7 @@ export class Channel {
const componentDiff = uiResponse.getRender()!.getComponentDiff()!;

for (const command of uiResponse.getRender()!.getCommandsList()) {
const navigate = command.getNavigate();
if (navigate) {
onNavigate(navigate.getUrl()!);
}
onCommand(command);
}
const title = uiResponse.getRender()!.getTitle();
if (title) {
Expand Down
28 changes: 26 additions & 2 deletions mesop/web/src/shell/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,32 @@ export class Shell {
}
this.error = undefined;
},
onNavigate: (route) => {
this.router.navigateByUrl(route);
onCommand: (command) => {
if (command.hasNavigate()) {
this.router.navigateByUrl(command.getNavigate()!.getUrl()!);
} else if (command.hasScrollIntoView()) {
// Scroll into view
const key = command.getScrollIntoView()!.getKey();
const targetElements = document.querySelectorAll(
`[data-key="${key}"]`,
);
if (!targetElements.length) {
console.error(
`Could not scroll to component with key ${key} because no component found`,
);
return;
}
if (targetElements.length > 1) {
console.warn(
'Found multiple components',
targetElements,
'to potentially scroll to for key',
key,
'. This is probably a bug and you should use a unique key identifier.',
);
}
targetElements[0].parentElement!.scrollIntoView({behavior: 'smooth'});
}
},
onError: (error) => {
this.error = error;
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ nav:
- Getting Started: getting_started.md
- Guides:
- Components: guides/components.md
- Commands: guides/commands.md
- Interactivity: guides/interactivity.md
- Pages: guides/pages.md
- Deployment: guides/deployment.md
Expand Down

0 comments on commit a10f247

Please sign in to comment.