Skip to content

Commit

Permalink
Add expansion panel component. (#1089)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
richard-to authored Nov 5, 2024
1 parent 69148c9 commit 96a47bb
Show file tree
Hide file tree
Showing 31 changed files with 847 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Mesop is a Python-based UI framework that allows you to rapidly build web apps l

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

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

```python
import time
Expand Down
165 changes: 165 additions & 0 deletions demo/expansion_panel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
from dataclasses import field

import mesop as me


@me.stateclass
class State:
normal_accordion: dict[str, bool] = field(
default_factory=lambda: {"pie": True, "donut": False, "icecream": False}
)
multi_accordion: dict[str, bool] = field(
default_factory=lambda: {"pie": False, "donut": False, "icecream": False}
)


def load(e: me.LoadEvent):
me.set_theme_mode("system")


@me.page(
on_load=load,
security_policy=me.SecurityPolicy(
allowed_iframe_parents=["https://google.github.io"]
),
path="/expansion_panel",
)
def app():
state = me.state(State)
with me.box(
style=me.Style(
display="flex",
flex_direction="column",
gap=15,
margin=me.Margin.all(15),
max_width=500,
)
):
me.text("Normal Accordion", type="headline-5")
with me.accordion():
with me.expansion_panel(
key="pie",
title="Pie",
description="Type of snack",
icon="pie_chart",
disabled=False,
expanded=state.normal_accordion["pie"],
hide_toggle=False,
on_toggle=on_accordion_toggle,
):
me.text(
"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."
)

with me.expansion_panel(
key="donut",
title="Donut",
description="Type of breakfast",
icon="donut_large",
disabled=False,
expanded=state.normal_accordion["donut"],
hide_toggle=False,
on_toggle=on_accordion_toggle,
):
me.text(
"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."
)

with me.expansion_panel(
key="icecream",
title="Ice cream",
description="Type of dessert",
icon="icecream",
disabled=False,
expanded=state.normal_accordion["icecream"],
hide_toggle=False,
on_toggle=on_accordion_toggle,
):
me.text(
"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."
)

me.text("Multi Accordion", type="headline-5")
with me.box(
style=me.Style(display="flex", gap=20, margin=me.Margin(bottom=15)),
):
me.button(
label="Open All", type="flat", on_click=on_multi_accordion_open_all
)
me.button(
label="Close All", type="flat", on_click=on_multi_accordion_close_all
)

with me.accordion():
with me.expansion_panel(
key="pie",
title="Pie",
description="Type of snack",
icon="pie_chart",
expanded=state.multi_accordion["pie"],
on_toggle=on_multi_accordion_toggle,
):
me.text(
"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."
)

with me.expansion_panel(
key="donut",
title="Donut",
description="Type of breakfast",
icon="donut_large",
expanded=state.multi_accordion["donut"],
on_toggle=on_multi_accordion_toggle,
):
me.text(
"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."
)

with me.expansion_panel(
key="icecream",
title="Ice cream",
description="Type of dessert",
icon="icecream",
expanded=state.multi_accordion["icecream"],
on_toggle=on_multi_accordion_toggle,
):
me.text(
"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."
)

me.text("Expansion Panel", type="headline-5")

with me.expansion_panel(
key="pie",
title="Pie",
description="Type of snack",
icon="pie_chart",
):
me.text(
"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."
)


def on_accordion_toggle(e: me.ExpansionPanelToggleEvent):
"""Implements accordion behavior where only one panel can be open at a time"""
state = me.state(State)
state.normal_accordion = {"pie": False, "donut": False, "icecream": False}
state.normal_accordion[e.key] = e.opened


def on_multi_accordion_toggle(e: me.ExpansionPanelToggleEvent):
"""Implements accordion behavior where multiple panels can be open at a time"""
state = me.state(State)
state.multi_accordion[e.key] = e.opened


def on_multi_accordion_open_all(e: me.ClickEvent):
state = me.state(State)
for key in state.multi_accordion:
state.multi_accordion[key] = True


def on_multi_accordion_close_all(e: me.ClickEvent):
state = me.state(State)
for key in state.multi_accordion:
state.multi_accordion[key] = False
2 changes: 2 additions & 0 deletions demo/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import dialog as dialog
import divider as divider
import embed as embed
import expansion_panel as expansion_panel
import fancy_chat as fancy_chat
import feedback as feedback
import form_billing as form_billing
Expand Down Expand Up @@ -186,6 +187,7 @@ class Section:
Example(name="badge"),
Example(name="card"),
Example(name="divider"),
Example(name="expansion_panel"),
Example(name="icon"),
Example(name="progress_bar"),
Example(name="progress_spinner"),
Expand Down
21 changes: 21 additions & 0 deletions docs/components/expansion-panel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## Overview

Expansion panel and is based on the [Angular Material expansion panel component](https://material.angular.io/components/expansion/overview).

This is a useful component for showing a summary header which can be expanded into a more detailed card/panel.

The expansion panels can also be grouped together to create an accordion.

## Examples

<iframe class="component-demo" src="https://google.github.io/mesop/demo/?demo=expansion_panel"></iframe>

```python
--8<-- "demo/expansion_panel.py"
```

## API

::: mesop.components.accordion.accordion.accordion
::: mesop.components.expansion_panel.expansion_panel.expansion_panel
::: mesop.components.expansion_panel.expansion_panel.ExpansionPanelToggleEvent
2 changes: 2 additions & 0 deletions mesop/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ py_library(
deps = [
":version",
# REF(//scripts/scaffold_component.py):insert_component_import
"//mesop/components/accordion:py",
"//mesop/components/expansion_panel:py",
"//mesop/components/card_header:py",
"//mesop/components/card_actions:py",
"//mesop/components/card_content:py",
Expand Down
7 changes: 7 additions & 0 deletions mesop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from mesop.component_helpers.helper import (
slot as slot,
)
from mesop.components.accordion.accordion import accordion as accordion
from mesop.components.audio.audio import audio as audio
from mesop.components.autocomplete.autocomplete import (
AutocompleteEnterEvent as AutocompleteEnterEvent,
Expand Down Expand Up @@ -102,6 +103,12 @@
from mesop.components.datepicker.datepicker import date_picker as date_picker
from mesop.components.divider.divider import divider as divider
from mesop.components.embed.embed import embed as embed
from mesop.components.expansion_panel.expansion_panel import (
ExpansionPanelToggleEvent as ExpansionPanelToggleEvent,
)
from mesop.components.expansion_panel.expansion_panel import (
expansion_panel as expansion_panel,
)
from mesop.components.html.html import html as html
from mesop.components.icon.icon import icon as icon
from mesop.components.image.image import image as image
Expand Down
9 changes: 9 additions & 0 deletions mesop/components/accordion/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
load("//mesop/components:defs.bzl", "mesop_component")

package(
default_visibility = ["//build_defs:mesop_internal"],
)

mesop_component(
name = "accordion",
)
Empty file.
3 changes: 3 additions & 0 deletions mesop/components/accordion/accordion.ng.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<mat-accordion>
<ng-content></ng-content>
</mat-accordion>
7 changes: 7 additions & 0 deletions mesop/components/accordion/accordion.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
syntax = "proto2";

package mesop.components.accordion;

message AccordionType {

}
28 changes: 28 additions & 0 deletions mesop/components/accordion/accordion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import mesop.components.accordion.accordion_pb2 as accordion_pb
from mesop.component_helpers import (
insert_composite_component,
register_native_component,
)


@register_native_component
def accordion(
*,
key: str | None = None,
):
"""
This function creates an accordion.
This is more of a visual component. It is used to style a group of expansion panel
components in a unified and consistent way (as if they were one component -- i.e. an
accordion).
The mechanics of an accordion that only allows one expansion panel to be open at a
time, must be implemented manually, but is easy to do with Mesop state and event
handlers.
"""
return insert_composite_component(
key=key,
type_name="accordion",
proto=accordion_pb.AccordionType(),
)
29 changes: 29 additions & 0 deletions mesop/components/accordion/accordion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {MatAccordion} from '@angular/material/expansion';
import {Component, Input} from '@angular/core';
import {
Key,
Type,
} from 'mesop/mesop/protos/ui_jspb_proto_pb/mesop/protos/ui_pb';
import {AccordionType} from 'mesop/mesop/components/accordion/accordion_jspb_proto_pb/mesop/components/accordion/accordion_pb';

@Component({
selector: 'mesop-accordion',
templateUrl: 'accordion.ng.html',
standalone: true,
imports: [MatAccordion],
})
export class AccordionComponent {
@Input({required: true}) type!: Type;
@Input() key!: Key;
private _config!: AccordionType;

ngOnChanges() {
this._config = AccordionType.deserializeBinary(
this.type.getValue() as unknown as Uint8Array,
);
}

config(): AccordionType {
return this._config;
}
}
17 changes: 17 additions & 0 deletions mesop/components/expansion_panel/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
load("//mesop/components:defs.bzl", "mesop_component")
load("//build_defs:defaults.bzl", "sass_binary")

package(
default_visibility = ["//build_defs:mesop_internal"],
)

mesop_component(
name = "expansion_panel",
assets = [":expansion_panel.css"],
)

sass_binary(
name = "styles",
src = "expansion_panel.scss",
sourcemap = False,
)
Empty file.
13 changes: 13 additions & 0 deletions mesop/components/expansion_panel/e2e/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
load("//build_defs:defaults.bzl", "py_library")

package(
default_visibility = ["//build_defs:mesop_examples"],
)

py_library(
name = "e2e",
srcs = glob(["*.py"]),
deps = [
"//mesop",
],
)
3 changes: 3 additions & 0 deletions mesop/components/expansion_panel/e2e/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import accordion_app as accordion_app
from . import expansion_panel_app as expansion_panel_app
from . import multi_accordion_app as multi_accordion_app
Loading

0 comments on commit 96a47bb

Please sign in to comment.