Skip to content

Commit 96a47bb

Browse files
authored
Add expansion panel component. (#1089)
* Add expansion panel component. Also adds support for the accordion component which uses multiple grouped expansion panels. The accordion behavior needs to be manually implemented through event handlers on the expansion panels. Closes #1081
1 parent 69148c9 commit 96a47bb

31 files changed

+847
-1
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Mesop is a Python-based UI framework that allows you to rapidly build web apps l
3030

3131
## Write your first Mesop app in less than 10 lines of code...
3232

33-
[Demo app](https://google.github.io/mesop/demo/?demo=text_to_text)
33+
[Demo app](https://google.github.io/mesop/demo/?demo=text_io)
3434

3535
```python
3636
import time

demo/expansion_panel.py

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
from dataclasses import field
2+
3+
import mesop as me
4+
5+
6+
@me.stateclass
7+
class State:
8+
normal_accordion: dict[str, bool] = field(
9+
default_factory=lambda: {"pie": True, "donut": False, "icecream": False}
10+
)
11+
multi_accordion: dict[str, bool] = field(
12+
default_factory=lambda: {"pie": False, "donut": False, "icecream": False}
13+
)
14+
15+
16+
def load(e: me.LoadEvent):
17+
me.set_theme_mode("system")
18+
19+
20+
@me.page(
21+
on_load=load,
22+
security_policy=me.SecurityPolicy(
23+
allowed_iframe_parents=["https://google.github.io"]
24+
),
25+
path="/expansion_panel",
26+
)
27+
def app():
28+
state = me.state(State)
29+
with me.box(
30+
style=me.Style(
31+
display="flex",
32+
flex_direction="column",
33+
gap=15,
34+
margin=me.Margin.all(15),
35+
max_width=500,
36+
)
37+
):
38+
me.text("Normal Accordion", type="headline-5")
39+
with me.accordion():
40+
with me.expansion_panel(
41+
key="pie",
42+
title="Pie",
43+
description="Type of snack",
44+
icon="pie_chart",
45+
disabled=False,
46+
expanded=state.normal_accordion["pie"],
47+
hide_toggle=False,
48+
on_toggle=on_accordion_toggle,
49+
):
50+
me.text(
51+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
52+
)
53+
54+
with me.expansion_panel(
55+
key="donut",
56+
title="Donut",
57+
description="Type of breakfast",
58+
icon="donut_large",
59+
disabled=False,
60+
expanded=state.normal_accordion["donut"],
61+
hide_toggle=False,
62+
on_toggle=on_accordion_toggle,
63+
):
64+
me.text(
65+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
66+
)
67+
68+
with me.expansion_panel(
69+
key="icecream",
70+
title="Ice cream",
71+
description="Type of dessert",
72+
icon="icecream",
73+
disabled=False,
74+
expanded=state.normal_accordion["icecream"],
75+
hide_toggle=False,
76+
on_toggle=on_accordion_toggle,
77+
):
78+
me.text(
79+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
80+
)
81+
82+
me.text("Multi Accordion", type="headline-5")
83+
with me.box(
84+
style=me.Style(display="flex", gap=20, margin=me.Margin(bottom=15)),
85+
):
86+
me.button(
87+
label="Open All", type="flat", on_click=on_multi_accordion_open_all
88+
)
89+
me.button(
90+
label="Close All", type="flat", on_click=on_multi_accordion_close_all
91+
)
92+
93+
with me.accordion():
94+
with me.expansion_panel(
95+
key="pie",
96+
title="Pie",
97+
description="Type of snack",
98+
icon="pie_chart",
99+
expanded=state.multi_accordion["pie"],
100+
on_toggle=on_multi_accordion_toggle,
101+
):
102+
me.text(
103+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
104+
)
105+
106+
with me.expansion_panel(
107+
key="donut",
108+
title="Donut",
109+
description="Type of breakfast",
110+
icon="donut_large",
111+
expanded=state.multi_accordion["donut"],
112+
on_toggle=on_multi_accordion_toggle,
113+
):
114+
me.text(
115+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
116+
)
117+
118+
with me.expansion_panel(
119+
key="icecream",
120+
title="Ice cream",
121+
description="Type of dessert",
122+
icon="icecream",
123+
expanded=state.multi_accordion["icecream"],
124+
on_toggle=on_multi_accordion_toggle,
125+
):
126+
me.text(
127+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
128+
)
129+
130+
me.text("Expansion Panel", type="headline-5")
131+
132+
with me.expansion_panel(
133+
key="pie",
134+
title="Pie",
135+
description="Type of snack",
136+
icon="pie_chart",
137+
):
138+
me.text(
139+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
140+
)
141+
142+
143+
def on_accordion_toggle(e: me.ExpansionPanelToggleEvent):
144+
"""Implements accordion behavior where only one panel can be open at a time"""
145+
state = me.state(State)
146+
state.normal_accordion = {"pie": False, "donut": False, "icecream": False}
147+
state.normal_accordion[e.key] = e.opened
148+
149+
150+
def on_multi_accordion_toggle(e: me.ExpansionPanelToggleEvent):
151+
"""Implements accordion behavior where multiple panels can be open at a time"""
152+
state = me.state(State)
153+
state.multi_accordion[e.key] = e.opened
154+
155+
156+
def on_multi_accordion_open_all(e: me.ClickEvent):
157+
state = me.state(State)
158+
for key in state.multi_accordion:
159+
state.multi_accordion[key] = True
160+
161+
162+
def on_multi_accordion_close_all(e: me.ClickEvent):
163+
state = me.state(State)
164+
for key in state.multi_accordion:
165+
state.multi_accordion[key] = False

demo/main.py

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import dialog as dialog
4040
import divider as divider
4141
import embed as embed
42+
import expansion_panel as expansion_panel
4243
import fancy_chat as fancy_chat
4344
import feedback as feedback
4445
import form_billing as form_billing
@@ -186,6 +187,7 @@ class Section:
186187
Example(name="badge"),
187188
Example(name="card"),
188189
Example(name="divider"),
190+
Example(name="expansion_panel"),
189191
Example(name="icon"),
190192
Example(name="progress_bar"),
191193
Example(name="progress_spinner"),

docs/components/expansion-panel.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
## Overview
2+
3+
Expansion panel and is based on the [Angular Material expansion panel component](https://material.angular.io/components/expansion/overview).
4+
5+
This is a useful component for showing a summary header which can be expanded into a more detailed card/panel.
6+
7+
The expansion panels can also be grouped together to create an accordion.
8+
9+
## Examples
10+
11+
<iframe class="component-demo" src="https://google.github.io/mesop/demo/?demo=expansion_panel"></iframe>
12+
13+
```python
14+
--8<-- "demo/expansion_panel.py"
15+
```
16+
17+
## API
18+
19+
::: mesop.components.accordion.accordion.accordion
20+
::: mesop.components.expansion_panel.expansion_panel.expansion_panel
21+
::: mesop.components.expansion_panel.expansion_panel.ExpansionPanelToggleEvent

mesop/BUILD

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ py_library(
2323
deps = [
2424
":version",
2525
# REF(//scripts/scaffold_component.py):insert_component_import
26+
"//mesop/components/accordion:py",
27+
"//mesop/components/expansion_panel:py",
2628
"//mesop/components/card_header:py",
2729
"//mesop/components/card_actions:py",
2830
"//mesop/components/card_content:py",

mesop/__init__.py

+7
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from mesop.component_helpers.helper import (
3535
slot as slot,
3636
)
37+
from mesop.components.accordion.accordion import accordion as accordion
3738
from mesop.components.audio.audio import audio as audio
3839
from mesop.components.autocomplete.autocomplete import (
3940
AutocompleteEnterEvent as AutocompleteEnterEvent,
@@ -102,6 +103,12 @@
102103
from mesop.components.datepicker.datepicker import date_picker as date_picker
103104
from mesop.components.divider.divider import divider as divider
104105
from mesop.components.embed.embed import embed as embed
106+
from mesop.components.expansion_panel.expansion_panel import (
107+
ExpansionPanelToggleEvent as ExpansionPanelToggleEvent,
108+
)
109+
from mesop.components.expansion_panel.expansion_panel import (
110+
expansion_panel as expansion_panel,
111+
)
105112
from mesop.components.html.html import html as html
106113
from mesop.components.icon.icon import icon as icon
107114
from mesop.components.image.image import image as image

mesop/components/accordion/BUILD

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
load("//mesop/components:defs.bzl", "mesop_component")
2+
3+
package(
4+
default_visibility = ["//build_defs:mesop_internal"],
5+
)
6+
7+
mesop_component(
8+
name = "accordion",
9+
)

mesop/components/accordion/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<mat-accordion>
2+
<ng-content></ng-content>
3+
</mat-accordion>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
syntax = "proto2";
2+
3+
package mesop.components.accordion;
4+
5+
message AccordionType {
6+
7+
}
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import mesop.components.accordion.accordion_pb2 as accordion_pb
2+
from mesop.component_helpers import (
3+
insert_composite_component,
4+
register_native_component,
5+
)
6+
7+
8+
@register_native_component
9+
def accordion(
10+
*,
11+
key: str | None = None,
12+
):
13+
"""
14+
This function creates an accordion.
15+
16+
This is more of a visual component. It is used to style a group of expansion panel
17+
components in a unified and consistent way (as if they were one component -- i.e. an
18+
accordion).
19+
20+
The mechanics of an accordion that only allows one expansion panel to be open at a
21+
time, must be implemented manually, but is easy to do with Mesop state and event
22+
handlers.
23+
"""
24+
return insert_composite_component(
25+
key=key,
26+
type_name="accordion",
27+
proto=accordion_pb.AccordionType(),
28+
)
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {MatAccordion} from '@angular/material/expansion';
2+
import {Component, Input} from '@angular/core';
3+
import {
4+
Key,
5+
Type,
6+
} from 'mesop/mesop/protos/ui_jspb_proto_pb/mesop/protos/ui_pb';
7+
import {AccordionType} from 'mesop/mesop/components/accordion/accordion_jspb_proto_pb/mesop/components/accordion/accordion_pb';
8+
9+
@Component({
10+
selector: 'mesop-accordion',
11+
templateUrl: 'accordion.ng.html',
12+
standalone: true,
13+
imports: [MatAccordion],
14+
})
15+
export class AccordionComponent {
16+
@Input({required: true}) type!: Type;
17+
@Input() key!: Key;
18+
private _config!: AccordionType;
19+
20+
ngOnChanges() {
21+
this._config = AccordionType.deserializeBinary(
22+
this.type.getValue() as unknown as Uint8Array,
23+
);
24+
}
25+
26+
config(): AccordionType {
27+
return this._config;
28+
}
29+
}
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
load("//mesop/components:defs.bzl", "mesop_component")
2+
load("//build_defs:defaults.bzl", "sass_binary")
3+
4+
package(
5+
default_visibility = ["//build_defs:mesop_internal"],
6+
)
7+
8+
mesop_component(
9+
name = "expansion_panel",
10+
assets = [":expansion_panel.css"],
11+
)
12+
13+
sass_binary(
14+
name = "styles",
15+
src = "expansion_panel.scss",
16+
sourcemap = False,
17+
)

mesop/components/expansion_panel/__init__.py

Whitespace-only changes.
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
load("//build_defs:defaults.bzl", "py_library")
2+
3+
package(
4+
default_visibility = ["//build_defs:mesop_examples"],
5+
)
6+
7+
py_library(
8+
name = "e2e",
9+
srcs = glob(["*.py"]),
10+
deps = [
11+
"//mesop",
12+
],
13+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import accordion_app as accordion_app
2+
from . import expansion_panel_app as expansion_panel_app
3+
from . import multi_accordion_app as multi_accordion_app

0 commit comments

Comments
 (0)