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

fix: we did not have a good chain of height: 100% divs #178

Merged
merged 2 commits into from
Jul 20, 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
29 changes: 20 additions & 9 deletions solara/autorouting.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,12 @@ def get_args(f):
content = solara.Markdown(path.read_text(), unsafe_solara_execute=True)

main = solara.Div(
classes=["solara-autorouter-content"],
children=[
solara.Title(route_current.label or "No title"),
content,
navigation,
]
],
)
main = wrap_in_layouts(main, layouts)
else:
Expand All @@ -218,17 +219,25 @@ def get_args(f):
title = route_current.label or "No title"
title_element = solara.Title(title)
module = None
if route_current.module is not None:
Page = route_current.component
# translate the default RenderPage as no value given (None)
if Page is RenderPage:
Page = None
if route_current.module is not None and (Page is None):
# if not a custom component is given, we try to find a Page component
# in the module
assert route_current.module is not None
module = route_current.module
namespace = module.__dict__
Page = nested_get(namespace, main_name, None)
Page = nested_get(namespace, main_name, Page)
if Page is None:
# app is for backwards compatibility
Page = namespace.get("page", namespace.get("app"))
Page = namespace.get("page", namespace.get("app", Page))
Page = nested_get(namespace, main_name, Page)
else:
Page = route_current.component

if Page is None and route_current.children:
# we we did not get a component, but we recursively render
Page = RenderPage
if isinstance(Page, ipywidgets.Widget):
# If we have a widget, we need to execute this again for each
# connection, since we cannot share widgets between connections/users.
Expand All @@ -253,10 +262,11 @@ def get_args(f):
Page = getattr(modules[route_current.path], "app", None)
Page = getattr(modules[route_current.path], "page", Page)
main = solara.Div(
classes=["solara-autorouter-content"],
children=[
title_element,
Page,
]
],
)
main = wrap_in_layouts(main, layouts)
elif Page is not None:
Expand All @@ -268,10 +278,11 @@ def get_args(f):
else:
page = solara.Error(f"{Page} is not a component or element, but {type(Page)}")
main = solara.Div(
classes=["solara-autorouter-content"],
children=[
title_element,
page,
]
],
)
main = wrap_in_layouts(main, layouts)
else:
Expand Down Expand Up @@ -393,7 +404,7 @@ def generate_routes(module: ModuleType) -> List[solara.Route]:
continue
else:
# skip empty modules
if get_renderable(submod) is None:
if get_renderable(submod) is None and not hasattr(submod, "routes"):
continue
children = getattr(submod, "routes", [])
if subfile:
Expand Down
16 changes: 13 additions & 3 deletions solara/components/applayout.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,12 @@ def set_path(index):
with v.Row(no_gutters=False, class_="solara-content-main"):
v.Col(cols=12, children=children_content)
else:
with v.Html(tag="div", style_="min-height: 100vh") as main:
# this limits the height of the app to the height of the screen
# and further down we use overflow: auto to add scrollbars to the main content
# the navigation drawer adds it own scrollbars
# NOTE: while developing this we added overflow: hidden, but this does not seem
# to be necessary anymore
with v.Html(tag="div", style_="height: 100vh") as main:
with solara.HBox():
if use_drawer:
with v.NavigationDrawer(
Expand Down Expand Up @@ -340,8 +345,13 @@ def set_path(index):
if fullscreen:
solara.Button(icon_name="mdi-fullscreen-exit", on_click=lambda: set_fullscreen(False), icon=True, dark=False)

with v.Content(class_="solara-content-main"):
v.Col(cols=12, children=children_content)
with v.Content(class_="solara-content-main", style_="height: 100%;"):
# make sure the scrollbar does no go under the appbar by adding overflow: auto
# to a child of content, because content has padding-top: 64px (set by vuetify)
# the padding: 12px is needed for backward compatibility with the previously used
# v.Col which has this by default. If we do not use this, a solara.Column will
# use a margin: -12px which will make a horizontal scrollbar appear
solara.Div(style_="height: 100%; overflow: auto; padding: 12px;", children=children_content)
if fullscreen:
with v.Dialog(v_model=True, children=[], fullscreen=True, hide_overlay=True, persistent=True, no_click_animation=True) as dialog:
v.Sheet(class_="overflow-y-auto overflow-x-auto", children=[main])
Expand Down
5 changes: 4 additions & 1 deletion solara/components/columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ def Page():
style_flat = solara.util._flatten_style(style)
with rv.Row(class_=class_, no_gutters=not gutters, dense=gutters_dense, style_=style_flat) as main:
for child, width in zip(children, cycle(widths)):
with rv.Col(children=[child], style_=f"flex-grow: {width}; overflow: auto" if width != 0 else "flex-grow: 0;"):
# we add height: 100% because this will trigger a chain of height set if it is set on the parent
# via the style. If we do not set the height, it will have no effect. Furthermore, we only have
# a single child, so this cannot interfere with other siblings.
with rv.Col(children=[child], style_=f"height: 100%; flex-grow: {width}; overflow: auto" if width != 0 else "flex-grow: 0"):
pass
return main

Expand Down
4 changes: 4 additions & 0 deletions solara/server/assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,7 @@ div.highlight {
.solara-content-main {
animation: solara-fade-in 0.5s ease;
}

.solara-autorouter-content {
height: 100%;
}
63 changes: 63 additions & 0 deletions solara/website/pages/apps/scrolling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import solara

github_url = solara.util.github_url(__file__)

# list of nice soft colors
colors = [
"#e6194b",
"#3cb44b",
"#ffe119",
"#4363d8",
"#f58231",
"#911eb4",
]


@solara.component
def Page1():
with solara.Sidebar():
solara.Button(label="View source", icon_name="mdi-github-circle", attributes={"href": github_url, "target": "_blank"}, text=True, outlined=True)
solara.Markdown("The sidebar will get scrollbars automatically, independently of the main content.")
for i in range(10):
with solara.Card(f"Card {i}"):
solara.Info(f"Text {i}", color=colors[i % len(colors)])

solara.Markdown("The main content will get scrollbars automatically, independently of the sidebar.")
for i in range(10):
with solara.Card(f"Card {i}"):
solara.Info(f"Text {i}", color=colors[i % len(colors)])


@solara.component
def Page2():
# it is important we do not interrupt the height 100% chain
limit_content_height = solara.use_reactive(True)
# warning: if we add "margin": "10px" to the style, we will trigger
# scrollbars in the parent div, if you really need to add margins,
# you should correct for that in the height using "calc(100% - 20px)"
with solara.Column(style={"height": "100%"}):
with solara.Sidebar():
solara.Button(label="View source", icon_name="mdi-github-circle", attributes={"href": github_url, "target": "_blank"}, text=True, outlined=True)
solara.Markdown("Main content columns will scroll together, or independently if we limit height to 100%")
solara.Checkbox(label="Limit content height", value=limit_content_height)

# if we do not limit height to 100%, solara's AppLayout will add a scroll bar
# to the main content, which will be independent of the sidebar's scroll bar
# but both columns will scroll together
with solara.Columns([2, 4], style={"height": "100%"} if limit_content_height.value else {}):
with solara.Card("I have my own scroll bar"):
solara.Markdown("")
for i in range(10):
with solara.Card(f"Card {i}"):
solara.Info(f"Text {i}", color=colors[i % len(colors)])
with solara.Card("I also have my own scroll bar"):
solara.Markdown("")
for i in range(20):
with solara.Card(f"Card {i}"):
solara.Info(f"Text {i}", color=colors[i % len(colors)])


routes = [
solara.Route(path="/", component=Page1, label="Scrolling"),
solara.Route(path="custom", component=Page2, label="Custom scrolling"),
]
3 changes: 3 additions & 0 deletions solara/website/pages/examples/fullscreen/scrolling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
redirect = "/apps/scrolling"

Page = True
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 22 additions & 5 deletions tests/unit/autorouting_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def test_routes_examples_docs():

def test_routes_directory():
routes = solara.autorouting.generate_routes_directory(HERE.parent / "solara_test_apps" / "multipage")
assert len(routes) == 7
assert len(routes) == 8
assert routes[0].path == "/"
assert routes[0].label == "Home"

Expand All @@ -152,11 +152,18 @@ def test_routes_directory():
assert routes[4].path == "and-notebooks"
assert routes[4].label == "And Notebooks"

assert routes[5].path == "single-file-directory"
assert routes[5].label == "Single File Directory"
assert routes[5].path == "custom-routes"
assert routes[5].label == "Custom Routes"
assert routes[5].children[0].path == "/"
assert routes[5].children[0].label == "Hi1"
assert routes[5].children[1].path == "page2"
assert routes[5].children[1].label == "Hi2"

assert routes[6].path == "some-other-python-script"
assert routes[6].label == "Some Other Python Script"
assert routes[6].path == "single-file-directory"
assert routes[6].label == "Single File Directory"

assert routes[7].path == "some-other-python-script"
assert routes[7].label == "Some Other Python Script"

main_object = solara.autorouting.RenderPage()
solara_context = solara.RoutingProvider(children=[main_object], routes=routes, pathname="/")
Expand Down Expand Up @@ -201,6 +208,16 @@ def test_routes_directory():
nav.location = "/a-directory/wrong-path"
assert "Page not found" in rc._find(v.Alert).widget.children[0]

# custom routes in a single file

nav.location = "/custom-routes"
button = rc._find(v.Btn, children=["hi1"]).widget
assert button.children[0] == "hi1"

nav.location = "/custom-routes/page2"
button = rc._find(v.Btn, children=["hi2"]).widget
assert button.children[0] == "hi2"


def test_routes_regular_widgets():
# routes = solara.autorouting.generate_routes_directory(HERE.parent / "solara_test_apps" / "multipage")
Expand Down
17 changes: 17 additions & 0 deletions tests/unit/solara_test_apps/multipage/06-custom-routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import solara


@solara.component
def Page1():
solara.Button("hi1")


@solara.component
def Page2():
solara.Button("hi2")


routes = [
solara.Route(path="/", component=Page1, label="Hi1"),
solara.Route(path="page2", component=Page2, label="Hi2"),
]