-
Notifications
You must be signed in to change notification settings - Fork 136
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Menu, ClickMenu, and ContextMenu components for solara.lab (#295)
Allows popup menu's for buttons, context menus on right click, or normal click Co-authored-by: Maarten A. Breddels <[email protected]>
- Loading branch information
1 parent
805da82
commit 90e766d
Showing
6 changed files
with
292 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
from .confirmation_dialog import ConfirmationDialog # noqa: F401 | ||
from .menu import ClickMenu, ContextMenu, Menu # noqa: #F401 F403 | ||
from .tabs import Tab, Tabs # noqa: F401 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
from typing import Dict, List, Optional, Union | ||
|
||
import solara | ||
from solara.components.component_vue import component_vue | ||
|
||
|
||
@component_vue("menu.vue") | ||
def MenuWidget( | ||
activator: List[solara.Element], | ||
children: List[solara.Element] = [], | ||
show_menu: bool = False, | ||
style: Optional[str] = None, | ||
context: bool = False, | ||
use_absolute: bool = True, | ||
): | ||
pass | ||
|
||
|
||
@solara.component | ||
def ClickMenu( | ||
activator: Union[solara.Element, List[solara.Element]], | ||
children: List[solara.Element] = [], | ||
style: Optional[Union[str, Dict[str, str]]] = None, | ||
): | ||
""" | ||
Show a pop-up menu by clicking on the `activator` element. The menu appears at the cursor position. | ||
```solara | ||
import solara | ||
@solara.component | ||
def Page(): | ||
image_url = "/static/public/beach.jpeg" | ||
image = solara.Image(image=image_url) | ||
with solara.lab.ClickMenu(activator=image): | ||
with solara.Column(gap="0px"): | ||
[solara.Button(f"Click me {i}!", text=True) for i in range(5)] | ||
``` | ||
## Arguments | ||
* activator: Clicking on this element will open the menu. Accepts either a `solara.Element`, or a list of elements. | ||
* menu_contents: List of Elements to be contained in the menu. | ||
* style: CSS style to apply. Applied directly onto the `v-menu` component. | ||
""" | ||
show = solara.use_reactive(False) | ||
style_flat = solara.util._flatten_style(style) | ||
|
||
if not isinstance(activator, list): | ||
activator = [activator] | ||
|
||
return MenuWidget(activator=activator, children=children, show_menu=show.value, style=style_flat) | ||
|
||
|
||
@solara.component | ||
def ContextMenu( | ||
activator: Union[solara.Element, List[solara.Element]], | ||
children: List[solara.Element] = [], | ||
style: Optional[Union[str, Dict[str, str]]] = None, | ||
): | ||
""" | ||
Show a context menu by triggering the contextmenu event on the `activator` element. The menu appears at the cursor position. | ||
A contextmenu event is typically triggered by clicking the right mouse button, or by pressing the context menu key. | ||
```solara | ||
import solara | ||
@solara.component | ||
def Page(): | ||
image_url = "/static/public/beach.jpeg" | ||
image = solara.Image(image=image_url) | ||
with solara.lab.ContextMenu(activator=image): | ||
with solara.Column(gap="0px"): | ||
[solara.Button(f"Click me {i}!", text=True) for i in range(5)] | ||
``` | ||
## Arguments | ||
* activator: Clicking on this element will open the menu. Accepts either a `solara.Element`, or a list of elements. | ||
* children: List of Elements to be contained in the menu | ||
* style: CSS style to apply. Applied directly onto the `v-menu` component. | ||
""" | ||
show = solara.use_reactive(False) | ||
style_flat = solara.util._flatten_style(style) | ||
|
||
if not isinstance(activator, list): | ||
activator = [activator] | ||
|
||
return MenuWidget(activator=activator, children=children, show_menu=show.value, style=style_flat, context=True) | ||
|
||
|
||
@solara.component | ||
def Menu( | ||
activator: Union[solara.Element, List[solara.Element]], | ||
children: List[solara.Element] = [], | ||
style: Optional[Union[str, Dict[str, str]]] = None, | ||
): | ||
""" | ||
Show a pop-up menu by clicking on the `activator` element. The menu appears below the `activator` element. | ||
```solara | ||
import solara | ||
@solara.component | ||
def Page(): | ||
btn = solara.Button("Show suboptions") | ||
with solara.lab.Menu(activator=btn): | ||
with solara.Column(gap="0px"): | ||
[solara.Button(f"Click me {str(i)}!", text=True) for i in range(5)] | ||
``` | ||
## Arguments | ||
* activator: Clicking on this element will open the menu. Accepts either a `solara.Element`, or a list of elements. | ||
* children: List of Elements to be contained in the menu | ||
* style: CSS style to apply. Applied directly onto the `v-menu` component. | ||
""" | ||
show = solara.use_reactive(False) | ||
style_flat = solara.util._flatten_style(style) | ||
|
||
if not isinstance(activator, list): | ||
activator = [activator] | ||
|
||
return MenuWidget(activator=activator, children=children, show_menu=show.value, style=style_flat, use_absolute=False) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
<template> | ||
<v-menu | ||
v-model="show_menu" | ||
:absolute="use_absolute" | ||
offset-y | ||
> | ||
<template v-if="context" v-slot:activator="{ on }"> | ||
<div v-for="(element, index) in activator" | ||
:key="index" | ||
@contextmenu.prevent="show($event, on)"> | ||
<jupyter-widget :widget="element"></jupyter-widget> | ||
</div> | ||
</template> | ||
<template v-else v-slot:activator="{ on }"> | ||
<div v-for="(element, index) in activator" | ||
:key="index" | ||
v-on="on" | ||
style="width: fit-content;" | ||
> | ||
<jupyter-widget :widget="element"></jupyter-widget> | ||
</div> | ||
</template> | ||
<v-list v-for="(element, index) in children" :key="index" style="padding: 0;"> | ||
<jupyter-widget :widget="element" :style="style" ></jupyter-widget> | ||
</v-list> | ||
</v-menu> | ||
</template> | ||
|
||
<script> | ||
module.exports = { | ||
methods: { | ||
show(e, on) { | ||
// hide menu, and trigger the event on the next tick, otherwise vue does not see | ||
// `show_menu` changing and will no do any animation | ||
this.show_menu = false; | ||
this.$nextTick(() => { | ||
on.click(e) | ||
}) | ||
} | ||
} | ||
} | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
""" | ||
# Menus | ||
This page contains the various kinds of menu elements available to use in solara | ||
# Menu | ||
""" | ||
import solara | ||
from solara.website.utils import apidoc | ||
|
||
from . import NoPage | ||
|
||
title = "Menus" | ||
|
||
|
||
__doc__ += apidoc(solara.lab.components.menu.Menu.f) # type: ignore | ||
__doc__ += "# ClickMenu" | ||
__doc__ += apidoc(solara.lab.components.menu.ClickMenu.f) # type: ignore | ||
__doc__ += "# ContextMenu" | ||
__doc__ += apidoc(solara.lab.components.menu.ContextMenu.f) # type: ignore | ||
|
||
Page = NoPage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import pytest | ||
from playwright.sync_api import Page, expect | ||
|
||
# Tests are only run in solara, since others don't support parametrization. | ||
|
||
|
||
# Tests rendering of menus, as well as click functionality on child element | ||
@pytest.mark.parametrize("test_type", ["Menu", "ClickMenu", "ContextMenu"]) | ||
def test_menus_playwright(test_type, solara_test, page_session: Page, assert_solara_snapshot): | ||
import solara | ||
from solara.lab import ClickMenu, ContextMenu, Menu | ||
|
||
@solara.component | ||
def Page(): | ||
# without height dropdown menu won't be visible in screenshots | ||
with solara.Column(classes=["test-class-container"], style="height:300px; width: 400px;"): | ||
text = solara.use_reactive("pre-test") | ||
|
||
def on_click(): | ||
text.set("Works!") | ||
|
||
solara.Text(text.value, classes=["test-class-text"]) | ||
|
||
menu_activator = solara.Button("activator") | ||
menu_child = solara.Button("child", on_click=on_click, classes=["test-class-child"]) | ||
|
||
if test_type == "Menu": | ||
Menu(activator=menu_activator, children=[menu_child]) | ||
elif test_type == "ClickMenu": | ||
ClickMenu(activator=menu_activator, children=[menu_child]) | ||
else: | ||
ContextMenu(activator=menu_activator, children=[menu_child]) | ||
|
||
solara.display(Page()) | ||
|
||
text_el = page_session.locator(".test-class-text") | ||
expect(text_el).to_contain_text("pre-test") | ||
activator_button = page_session.locator("text=activator") | ||
expect(page_session.locator(".test-class-child")).to_have_count(0) | ||
if test_type == "ContextMenu": | ||
activator_button.click(button="right") | ||
else: | ||
activator_button.click() | ||
|
||
child_button = page_session.locator("text=child") | ||
page_session.wait_for_timeout(350) # Wait for any animations after click | ||
expect(page_session.locator(".test-class-child")).to_be_visible() | ||
child_button.click() | ||
page_session.wait_for_timeout(350) | ||
expect(text_el).to_contain_text("Works!") | ||
|
||
|
||
# Tests successive click behaviour of menus | ||
@pytest.mark.parametrize("test_type", ["Menu", "ClickMenu", "ContextMenu"]) | ||
def test_menus_successive(test_type, solara_test, page_session: Page): | ||
import solara | ||
from solara.lab import ClickMenu, ContextMenu, Menu | ||
|
||
@solara.component | ||
def Page(): | ||
menu_activator = solara.Div(style="height: 200px; width: 400px;", classes=["test-class-activator"]) | ||
menu_child = solara.Div(classes=["test-class-menu"], style="height: 100px; width: 50px;") | ||
|
||
if test_type == "Menu": | ||
Menu(activator=menu_activator, children=[menu_child]) | ||
elif test_type == "ClickMenu": | ||
ClickMenu(activator=menu_activator, children=[menu_child]) | ||
else: | ||
ContextMenu(activator=menu_activator, children=[menu_child]) | ||
|
||
solara.display(Page()) | ||
|
||
activator_el = page_session.locator(".test-class-activator") | ||
expect(page_session.locator(".test-class-menu")).to_have_count(0) # Menu should not exist yet | ||
if test_type == "ContextMenu": | ||
activator_el.click(button="right") | ||
else: | ||
activator_el.click() | ||
|
||
page_session.wait_for_timeout(350) # Wait for any animations after click | ||
expect(page_session.locator(".test-class-menu")).to_be_in_viewport() # Menu should be visible now | ||
|
||
if test_type == "ContextMenu": | ||
activator_el.click(button="right") | ||
page_session.wait_for_timeout(350) | ||
expect(page_session.locator(".test-class-menu")).to_be_in_viewport() | ||
else: | ||
activator_el.click() | ||
page_session.wait_for_timeout(350) | ||
expect(page_session.locator(".test-class-menu")).not_to_be_in_viewport() # After second click, Menu and ClickMenu should disappear |