From a25cf00acb9805d6537adc93cde4e25e8e8abdc7 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Sun, 17 Dec 2023 00:13:22 -0800 Subject: [PATCH] Add progress spinner & slide toggle component --- docs/components/progress_spinner.md | 1 + generator/generate_ng_ts.py | 15 +- generator/generate_py_component.py | 15 +- .../output_data/progress_spinner.binarypb | 11 ++ generator/output_data/progress_spinner.json | 77 ++++++++ generator/output_data/slide_toggle.binarypb | 22 +++ generator/output_data/slide_toggle.json | 168 ++++++++++++++++++ generator/spec_generator.ts | 23 ++- mesop/BUILD | 2 + mesop/__init__.py | 9 + mesop/components/progress_spinner/BUILD | 9 + mesop/components/progress_spinner/__init__.py | 0 mesop/components/progress_spinner/e2e/BUILD | 13 ++ .../progress_spinner/e2e/__init__.py | 1 + .../e2e/progress_spinner_app.py | 7 + .../e2e/progress_spinner_test.ts | 8 + .../progress_spinner/progress_spinner.ng.html | 8 + .../progress_spinner/progress_spinner.proto | 12 ++ .../progress_spinner/progress_spinner.py | 41 +++++ .../progress_spinner/progress_spinner.ts | 36 ++++ mesop/components/slide_toggle/BUILD | 9 + mesop/components/slide_toggle/__init__.py | 0 mesop/components/slide_toggle/e2e/BUILD | 13 ++ mesop/components/slide_toggle/e2e/__init__.py | 1 + .../slide_toggle/e2e/slide_toggle_app.py | 19 ++ .../slide_toggle/e2e/slide_toggle_test.ts | 8 + .../slide_toggle/slide_toggle.ng.html | 17 ++ .../slide_toggle/slide_toggle.proto | 21 +++ mesop/components/slide_toggle/slide_toggle.py | 87 +++++++++ mesop/components/slide_toggle/slide_toggle.ts | 49 +++++ mesop/testing/BUILD | 2 + mesop/testing/index.py | 2 + mesop/web/src/component_renderer/BUILD | 2 + .../component_renderer/type_to_component.ts | 4 + mkdocs.yml | 1 + 35 files changed, 704 insertions(+), 9 deletions(-) create mode 100644 docs/components/progress_spinner.md create mode 100644 generator/output_data/progress_spinner.binarypb create mode 100644 generator/output_data/progress_spinner.json create mode 100644 generator/output_data/slide_toggle.binarypb create mode 100644 generator/output_data/slide_toggle.json create mode 100644 mesop/components/progress_spinner/BUILD create mode 100644 mesop/components/progress_spinner/__init__.py create mode 100644 mesop/components/progress_spinner/e2e/BUILD create mode 100644 mesop/components/progress_spinner/e2e/__init__.py create mode 100644 mesop/components/progress_spinner/e2e/progress_spinner_app.py create mode 100644 mesop/components/progress_spinner/e2e/progress_spinner_test.ts create mode 100644 mesop/components/progress_spinner/progress_spinner.ng.html create mode 100644 mesop/components/progress_spinner/progress_spinner.proto create mode 100644 mesop/components/progress_spinner/progress_spinner.py create mode 100644 mesop/components/progress_spinner/progress_spinner.ts create mode 100644 mesop/components/slide_toggle/BUILD create mode 100644 mesop/components/slide_toggle/__init__.py create mode 100644 mesop/components/slide_toggle/e2e/BUILD create mode 100644 mesop/components/slide_toggle/e2e/__init__.py create mode 100644 mesop/components/slide_toggle/e2e/slide_toggle_app.py create mode 100644 mesop/components/slide_toggle/e2e/slide_toggle_test.ts create mode 100644 mesop/components/slide_toggle/slide_toggle.ng.html create mode 100644 mesop/components/slide_toggle/slide_toggle.proto create mode 100644 mesop/components/slide_toggle/slide_toggle.py create mode 100644 mesop/components/slide_toggle/slide_toggle.ts diff --git a/docs/components/progress_spinner.md b/docs/components/progress_spinner.md new file mode 100644 index 000000000..55a71c217 --- /dev/null +++ b/docs/components/progress_spinner.md @@ -0,0 +1 @@ +::: mesop.components.progress_spinner.progress_spinner diff --git a/generator/generate_ng_ts.py b/generator/generate_ng_ts.py index 9acc0785a..da68c7c74 100644 --- a/generator/generate_ng_ts.py +++ b/generator/generate_ng_ts.py @@ -66,15 +66,18 @@ def generate_ts_getter_method(prop: pb.Prop) -> str: def generate_ts_event_method(prop: pb.OutputProp) -> str: - arg = ( - "event" - if prop.event_js_type.is_primitive - else f"event.{prop.event_props[0].name}" - ) + set_value = "" + if len(prop.event_props): + arg = ( + "event" + if prop.event_js_type.is_primitive + else f"event.{prop.event_props[0].name}" + ) + set_value = f"userEvent.set{format_proto_xtype(prop.event_props[0].type).capitalize()}({arg})" return f""" on{prop.event_name}(event: {prop.event_js_type.type_name}): void {{ const userEvent = new UserEvent(); - userEvent.set{format_proto_xtype(prop.event_props[0].type).capitalize()}({arg}) + {set_value} userEvent.setHandlerId(this.config().getOn{prop.event_name}HandlerId()) userEvent.setKey(this.key); this.channel.dispatch(userEvent); diff --git a/generator/generate_py_component.py b/generator/generate_py_component.py index f0d297b1f..99dcb1956 100644 --- a/generator/generate_py_component.py +++ b/generator/generate_py_component.py @@ -79,7 +79,8 @@ def generate_py_events(spec: pb.ComponentSpec) -> str: def generate_py_event(output_prop: pb.OutputProp) -> str: - return f""" + if len(output_prop.event_props): + return f""" @dataclass class {output_prop.event_name}(MesopEvent): {output_prop.event_props[0].name}: {format_py_xtype(output_prop.event_props[0].type)} @@ -90,6 +91,18 @@ class {output_prop.event_name}(MesopEvent): key=key, {output_prop.event_props[0].name}=event.{format_proto_xtype(output_prop.event_props[0].type)}, ), +) + """ + return f""" +@dataclass +class {output_prop.event_name}(MesopEvent): + pass + +register_event_mapper( + {output_prop.event_name}, + lambda event, key: {output_prop.event_name}( + key=key, + ), ) """ diff --git a/generator/output_data/progress_spinner.binarypb b/generator/output_data/progress_spinner.binarypb new file mode 100644 index 000000000..f83c296a6 --- /dev/null +++ b/generator/output_data/progress_spinner.binarypb @@ -0,0 +1,11 @@ + +| +progress_spinner"MatProgressSpinner*mat-progress-spinner2> +"@angular/material/progress-spinnerMatProgressSpinnerModule9 +color2,Theme palette color of the progress spinner.é +mode+) + determinate + indeterminate determinateProgressSpinnerMode2žMode of the progress bar. Input must be one of these values: determinate, indeterminate, buffer, query, defaults to 'determinate'. Mirrored to mode attribute.\ +valuenumber2GValue of the progress bar. Defaults to zero. Mirrored to aria-valuenow.` +diameternumber2HThe diameter of the progress spinner (will set width and height of svg).@ + strokeWidthnumber2%Stroke width of the progress spinner. \ No newline at end of file diff --git a/generator/output_data/progress_spinner.json b/generator/output_data/progress_spinner.json new file mode 100644 index 000000000..78623d29b --- /dev/null +++ b/generator/output_data/progress_spinner.json @@ -0,0 +1,77 @@ +{ + "input": { + "name": "progress_spinner", + "targetClass": "MatProgressSpinner", + "hasContent": false, + "elementName": "mat-progress-spinner", + "tsFilename": "", + "directiveNamesList": [], + "nativeEventsList": [], + "skipPropertyNamesList": [], + "ngModulesList": [ + { + "importPath": "@angular/material/progress-spinner", + "moduleName": "MatProgressSpinnerModule", + "otherSymbolsList": [] + } + ], + "isFormField": false + }, + "inputPropsList": [ + { + "name": "color", + "alias": "", + "type": { + "simpleType": 1 + }, + "debugType": "", + "target": 0, + "docs": "Theme palette color of the progress spinner." + }, + { + "name": "mode", + "alias": "", + "type": { + "simpleType": 0, + "stringLiterals": { + "stringLiteralList": ["determinate", "indeterminate"], + "defaultValue": "determinate" + } + }, + "debugType": "ProgressSpinnerMode", + "target": 0, + "docs": "Mode of the progress bar. Input must be one of these values: determinate, indeterminate, buffer, query, defaults to 'determinate'. Mirrored to mode attribute." + }, + { + "name": "value", + "alias": "", + "type": { + "simpleType": 3 + }, + "debugType": "number", + "target": 0, + "docs": "Value of the progress bar. Defaults to zero. Mirrored to aria-valuenow." + }, + { + "name": "diameter", + "alias": "", + "type": { + "simpleType": 3 + }, + "debugType": "number", + "target": 0, + "docs": "The diameter of the progress spinner (will set width and height of svg)." + }, + { + "name": "strokeWidth", + "alias": "", + "type": { + "simpleType": 3 + }, + "debugType": "number", + "target": 0, + "docs": "Stroke width of the progress spinner." + } + ], + "outputPropsList": [] +} diff --git a/generator/output_data/slide_toggle.binarypb b/generator/output_data/slide_toggle.binarypb new file mode 100644 index 000000000..c2f571933 --- /dev/null +++ b/generator/output_data/slide_toggle.binarypb @@ -0,0 +1,22 @@ + +Œ + slide_toggle"MatSlideToggle*mat-slide-toggleR toggleChange2L +@angular/material/slide-toggleMatSlideToggleModuleMatSlideToggleChangeV +name string | null2;Name value will be applied to the input element if present.i +idstring2WA unique id for the slide-toggle input. If none is supplied, it will be auto-generated.• + labelPosition +before +afterafter'before' | 'after'2VWhether the label should appear after or before the slide-toggle. Defaults to 'after'.q + ariaLabel" +aria-label string | null2EUsed to set the aria-label attribute on the underlying input element.€ +ariaLabelledby"aria-labelledby string | null2JUsed to set the aria-labelledby attribute on the underlying input element.| +ariaDescribedby"aria-describedbystring2KUsed to set the aria-describedby attribute on the underlying input element.> +requiredboolean2%Whether the slide-toggle is required.? +colorstring | undefined2Palette color of slide toggle.> +disabledboolean2%Whether the slide toggle is disabled.D + disableRippleboolean2&Whether the slide toggle has a ripple.1 +tabIndexnumber2Tabindex of slide toggle.K +checkedboolean23Whether the slide-toggle element is checked or not.M +hideIconboolean24Whether to hide the icon inside of the slide toggle.ƒ +changeSlideToggleChangeEvent" +MatSlideToggleChange*IAn event will be dispatched each time the slide-toggle changes its value. \ No newline at end of file diff --git a/generator/output_data/slide_toggle.json b/generator/output_data/slide_toggle.json new file mode 100644 index 000000000..c96c4afb1 --- /dev/null +++ b/generator/output_data/slide_toggle.json @@ -0,0 +1,168 @@ +{ + "input": { + "name": "slide_toggle", + "targetClass": "MatSlideToggle", + "hasContent": false, + "elementName": "mat-slide-toggle", + "tsFilename": "", + "directiveNamesList": [], + "nativeEventsList": [], + "skipPropertyNamesList": ["toggleChange"], + "ngModulesList": [ + { + "importPath": "@angular/material/slide-toggle", + "moduleName": "MatSlideToggleModule", + "otherSymbolsList": ["MatSlideToggleChange"] + } + ], + "isFormField": false + }, + "inputPropsList": [ + { + "name": "name", + "alias": "", + "type": { + "simpleType": 1 + }, + "debugType": "string | null", + "target": 0, + "docs": "Name value will be applied to the input element if present." + }, + { + "name": "id", + "alias": "", + "type": { + "simpleType": 1 + }, + "debugType": "string", + "target": 0, + "docs": "A unique id for the slide-toggle input. If none is supplied, it will be auto-generated." + }, + { + "name": "labelPosition", + "alias": "", + "type": { + "simpleType": 0, + "stringLiterals": { + "stringLiteralList": ["before", "after"], + "defaultValue": "after" + } + }, + "debugType": "'before' | 'after'", + "target": 0, + "docs": "Whether the label should appear after or before the slide-toggle. Defaults to 'after'." + }, + { + "name": "ariaLabel", + "alias": "aria-label", + "type": { + "simpleType": 1 + }, + "debugType": "string | null", + "target": 0, + "docs": "Used to set the aria-label attribute on the underlying input element." + }, + { + "name": "ariaLabelledby", + "alias": "aria-labelledby", + "type": { + "simpleType": 1 + }, + "debugType": "string | null", + "target": 0, + "docs": "Used to set the aria-labelledby attribute on the underlying input element." + }, + { + "name": "ariaDescribedby", + "alias": "aria-describedby", + "type": { + "simpleType": 1 + }, + "debugType": "string", + "target": 0, + "docs": "Used to set the aria-describedby attribute on the underlying input element." + }, + { + "name": "required", + "alias": "", + "type": { + "simpleType": 2 + }, + "debugType": "boolean", + "target": 0, + "docs": "Whether the slide-toggle is required." + }, + { + "name": "color", + "alias": "", + "type": { + "simpleType": 1 + }, + "debugType": "string | undefined", + "target": 0, + "docs": "Palette color of slide toggle." + }, + { + "name": "disabled", + "alias": "", + "type": { + "simpleType": 2 + }, + "debugType": "boolean", + "target": 0, + "docs": "Whether the slide toggle is disabled." + }, + { + "name": "disableRipple", + "alias": "", + "type": { + "simpleType": 2 + }, + "debugType": "boolean", + "target": 0, + "docs": "Whether the slide toggle has a ripple." + }, + { + "name": "tabIndex", + "alias": "", + "type": { + "simpleType": 3 + }, + "debugType": "number", + "target": 0, + "docs": "Tabindex of slide toggle." + }, + { + "name": "checked", + "alias": "", + "type": { + "simpleType": 2 + }, + "debugType": "boolean", + "target": 0, + "docs": "Whether the slide-toggle element is checked or not." + }, + { + "name": "hideIcon", + "alias": "", + "type": { + "simpleType": 2 + }, + "debugType": "boolean", + "target": 0, + "docs": "Whether to hide the icon inside of the slide toggle." + } + ], + "outputPropsList": [ + { + "name": "change", + "eventName": "SlideToggleChangeEvent", + "eventPropsList": [], + "eventJsType": { + "typeName": "MatSlideToggleChange", + "isPrimitive": false + }, + "docs": "An event will be dispatched each time the slide-toggle changes its value." + } + ] +} diff --git a/generator/spec_generator.ts b/generator/spec_generator.ts index facf46bc4..ff43b3bce 100644 --- a/generator/spec_generator.ts +++ b/generator/spec_generator.ts @@ -85,6 +85,20 @@ const progressBarSpecInput = (() => { return i; })(); +const progressSpinnerSpecInput = (() => { + const i = new pb.ComponentSpecInput(); + i.setName('progress_spinner'); + return i; +})(); + +const slideToggleSpecInput = (() => { + const i = new pb.ComponentSpecInput(); + i.setName('slide_toggle'); + // This adds a very confusing event that doesn't seem to have a use case for us. + i.addSkipPropertyNames('toggleChange'); + return i; +})(); + const SYSTEM_IMPORT_PREFIX = '@angular/material/'; const SYSTEM_PREFIX = 'Mat'; const SPEC_INPUTS = [ @@ -97,6 +111,8 @@ const SPEC_INPUTS = [ dividerSpecInput, iconSpecInput, progressBarSpecInput, + progressSpinnerSpecInput, + slideToggleSpecInput, ].map(preprocessSpecInput); function preprocessSpecInput( @@ -300,7 +316,9 @@ class NgParser { } else if ( !member.type && name === 'color' && - ['icon', 'progress_bar'].includes(this.input.getName()) + ['icon', 'progress_bar', 'progress_spinner'].includes( + this.input.getName(), + ) ) { // Technically this is a union between string and ThemePalette (which is a union of string literal) // string is the more flexible type. @@ -380,7 +398,7 @@ class NgParser { xType.setSimpleType(simpleType); eventProp.setType(xType); outputProp.addEventProps(eventProp); - } else { + } else if (type !== 'void') { // Need to import complex type. this.input.getNgModulesList()[0].addOtherSymbols(type); outputProp.setEventPropsList(this.getEventProps(type)); @@ -390,6 +408,7 @@ class NgParser { } getEventProps(type: string): pb.Prop[] { + if (type === 'void') return []; for (const statement of this.sourceFile.statements) { if (ts.isClassDeclaration(statement)) { const cls = statement; diff --git a/mesop/BUILD b/mesop/BUILD index 4bc2b2be4..f2d6843cf 100644 --- a/mesop/BUILD +++ b/mesop/BUILD @@ -12,6 +12,8 @@ py_library( ], deps = [ # REF(//scripts/scaffold_component.py):insert_component_import + "//mesop/components/slide_toggle:py", + "//mesop/components/progress_spinner:py", "//mesop/components/progress_bar:py", "//mesop/components/icon:py", "//mesop/components/divider:py", diff --git a/mesop/__init__.py b/mesop/__init__.py index 36ed1e0b5..edc7e98d5 100644 --- a/mesop/__init__.py +++ b/mesop/__init__.py @@ -21,6 +21,15 @@ from mesop.components.progress_bar.progress_bar import ( progress_bar as progress_bar, ) +from mesop.components.progress_spinner.progress_spinner import ( + progress_spinner as progress_spinner, +) +from mesop.components.slide_toggle.slide_toggle import ( + SlideToggleChangeEvent as SlideToggleChangeEvent, +) +from mesop.components.slide_toggle.slide_toggle import ( + slide_toggle as slide_toggle, +) from mesop.components.text.text import text as text from mesop.components.text_input.text_input import text_input as text_input from mesop.components.tooltip.tooltip import tooltip as tooltip diff --git a/mesop/components/progress_spinner/BUILD b/mesop/components/progress_spinner/BUILD new file mode 100644 index 000000000..7253e68ba --- /dev/null +++ b/mesop/components/progress_spinner/BUILD @@ -0,0 +1,9 @@ +load("//mesop/components:defs.bzl", "mesop_component") + +package( + default_visibility = ["//build_defs:mesop_internal"], +) + +mesop_component( + name = "progress_spinner", +) diff --git a/mesop/components/progress_spinner/__init__.py b/mesop/components/progress_spinner/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mesop/components/progress_spinner/e2e/BUILD b/mesop/components/progress_spinner/e2e/BUILD new file mode 100644 index 000000000..6d31bd3e2 --- /dev/null +++ b/mesop/components/progress_spinner/e2e/BUILD @@ -0,0 +1,13 @@ +load("//build_defs:defaults.bzl", "py_library") + +package( + default_visibility = ["//build_defs:mesop_internal"], +) + +py_library( + name = "e2e", + srcs = glob(["*.py"]), + deps = [ + "//mesop", + ], +) diff --git a/mesop/components/progress_spinner/e2e/__init__.py b/mesop/components/progress_spinner/e2e/__init__.py new file mode 100644 index 000000000..dd382b9fd --- /dev/null +++ b/mesop/components/progress_spinner/e2e/__init__.py @@ -0,0 +1 @@ +from . import progress_spinner_app as progress_spinner_app diff --git a/mesop/components/progress_spinner/e2e/progress_spinner_app.py b/mesop/components/progress_spinner/e2e/progress_spinner_app.py new file mode 100644 index 000000000..1e69d6e34 --- /dev/null +++ b/mesop/components/progress_spinner/e2e/progress_spinner_app.py @@ -0,0 +1,7 @@ +import mesop as me + + +@me.page(path="/components/progress_spinner/e2e/progress_spinner_app") +def app(): + me.text(text="Hello, world!") + me.progress_spinner(mode="indeterminate", diameter=40, stroke_width=4) diff --git a/mesop/components/progress_spinner/e2e/progress_spinner_test.ts b/mesop/components/progress_spinner/e2e/progress_spinner_test.ts new file mode 100644 index 000000000..97edfc142 --- /dev/null +++ b/mesop/components/progress_spinner/e2e/progress_spinner_test.ts @@ -0,0 +1,8 @@ +import {test, expect} from '@playwright/test'; + +test('test', async ({page}) => { + await page.goto('/components/progress_spinner/e2e/progress_spinner_app'); + expect(await page.getByText('Hello, world!').textContent()).toContain( + 'Hello, world!', + ); +}); diff --git a/mesop/components/progress_spinner/progress_spinner.ng.html b/mesop/components/progress_spinner/progress_spinner.ng.html new file mode 100644 index 000000000..2ebc27d99 --- /dev/null +++ b/mesop/components/progress_spinner/progress_spinner.ng.html @@ -0,0 +1,8 @@ + + diff --git a/mesop/components/progress_spinner/progress_spinner.proto b/mesop/components/progress_spinner/progress_spinner.proto new file mode 100644 index 000000000..a61511c38 --- /dev/null +++ b/mesop/components/progress_spinner/progress_spinner.proto @@ -0,0 +1,12 @@ + +syntax = "proto3"; + +package mesop.components.progress_spinner; + +message ProgressSpinnerType { + string color = 1; + string mode = 2; + double value = 3; + double diameter = 4; + double stroke_width = 5; +} diff --git a/mesop/components/progress_spinner/progress_spinner.py b/mesop/components/progress_spinner/progress_spinner.py new file mode 100644 index 000000000..dcb5f44b7 --- /dev/null +++ b/mesop/components/progress_spinner/progress_spinner.py @@ -0,0 +1,41 @@ +from typing import Literal + +from pydantic import validate_arguments + +import mesop.components.progress_spinner.progress_spinner_pb2 as progress_spinner_pb +from mesop.component_helpers import ( + insert_component, +) + + +@validate_arguments +def progress_spinner( + *, + key: str | None = None, + color: str = "", + mode: Literal["determinate", "indeterminate"] = "determinate", + value: float = 0, + diameter: float = 0, + stroke_width: float = 0, +): + """Creates a Progress spinner component. + + Args: + key (str|None): Unique identifier for this component instance. + color (str): Theme palette color of the progress spinner. + mode (Literal['determinate','indeterminate']): Mode of the progress bar. Input must be one of these values: determinate, indeterminate, buffer, query, defaults to 'determinate'. Mirrored to mode attribute. + value (float): Value of the progress bar. Defaults to zero. Mirrored to aria-valuenow. + diameter (float): The diameter of the progress spinner (will set width and height of svg). + stroke_width (float): Stroke width of the progress spinner. + """ + insert_component( + key=key, + type_name="progress_spinner", + proto=progress_spinner_pb.ProgressSpinnerType( + color=color, + mode=mode, + value=value, + diameter=diameter, + stroke_width=stroke_width, + ), + ) diff --git a/mesop/components/progress_spinner/progress_spinner.ts b/mesop/components/progress_spinner/progress_spinner.ts new file mode 100644 index 000000000..f513df84e --- /dev/null +++ b/mesop/components/progress_spinner/progress_spinner.ts @@ -0,0 +1,36 @@ +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {Component, Input} from '@angular/core'; +import { + UserEvent, + Key, + Type, +} from 'mesop/mesop/protos/ui_jspb_proto_pb/mesop/protos/ui_pb'; +import {ProgressSpinnerType} from 'mesop/mesop/components/progress_spinner/progress_spinner_jspb_proto_pb/mesop/components/progress_spinner/progress_spinner_pb'; +import {Channel} from '../../web/src/services/channel'; + +@Component({ + templateUrl: 'progress_spinner.ng.html', + standalone: true, + imports: [MatProgressSpinnerModule], +}) +export class ProgressSpinnerComponent { + @Input({required: true}) type!: Type; + @Input() key!: Key; + private _config!: ProgressSpinnerType; + + constructor(private readonly channel: Channel) {} + + ngOnChanges() { + this._config = ProgressSpinnerType.deserializeBinary( + this.type.getValue() as unknown as Uint8Array, + ); + } + + config(): ProgressSpinnerType { + return this._config; + } + + getMode(): 'determinate' | 'indeterminate' { + return this.config().getMode() as 'determinate' | 'indeterminate'; + } +} diff --git a/mesop/components/slide_toggle/BUILD b/mesop/components/slide_toggle/BUILD new file mode 100644 index 000000000..4f9118d3f --- /dev/null +++ b/mesop/components/slide_toggle/BUILD @@ -0,0 +1,9 @@ +load("//mesop/components:defs.bzl", "mesop_component") + +package( + default_visibility = ["//build_defs:mesop_internal"], +) + +mesop_component( + name = "slide_toggle", +) diff --git a/mesop/components/slide_toggle/__init__.py b/mesop/components/slide_toggle/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mesop/components/slide_toggle/e2e/BUILD b/mesop/components/slide_toggle/e2e/BUILD new file mode 100644 index 000000000..6d31bd3e2 --- /dev/null +++ b/mesop/components/slide_toggle/e2e/BUILD @@ -0,0 +1,13 @@ +load("//build_defs:defaults.bzl", "py_library") + +package( + default_visibility = ["//build_defs:mesop_internal"], +) + +py_library( + name = "e2e", + srcs = glob(["*.py"]), + deps = [ + "//mesop", + ], +) diff --git a/mesop/components/slide_toggle/e2e/__init__.py b/mesop/components/slide_toggle/e2e/__init__.py new file mode 100644 index 000000000..e7061b8b5 --- /dev/null +++ b/mesop/components/slide_toggle/e2e/__init__.py @@ -0,0 +1 @@ +from . import slide_toggle_app as slide_toggle_app diff --git a/mesop/components/slide_toggle/e2e/slide_toggle_app.py b/mesop/components/slide_toggle/e2e/slide_toggle_app.py new file mode 100644 index 000000000..21f575cbb --- /dev/null +++ b/mesop/components/slide_toggle/e2e/slide_toggle_app.py @@ -0,0 +1,19 @@ +import mesop as me + + +@me.stateclass +class State: + toggled: bool = False + + +@me.on(me.SlideToggleChangeEvent) +def on_change(event: me.SlideToggleChangeEvent): + s = me.state(State) + s.toggled = not s.toggled + + +@me.page(path="/components/slide_toggle/e2e/slide_toggle_app") +def app(): + me.slide_toggle(name="Hello, world!", on_change=on_change) + s = me.state(State) + me.text(text=f"Toggled: {s.toggled}") diff --git a/mesop/components/slide_toggle/e2e/slide_toggle_test.ts b/mesop/components/slide_toggle/e2e/slide_toggle_test.ts new file mode 100644 index 000000000..8ff8b0c8f --- /dev/null +++ b/mesop/components/slide_toggle/e2e/slide_toggle_test.ts @@ -0,0 +1,8 @@ +import {test, expect} from '@playwright/test'; + +test('test', async ({page}) => { + await page.goto('/components/slide_toggle/e2e/slide_toggle_app'); + expect(await page.getByText('Toggled:').textContent()).toContain( + 'Toggled: False', + ); +}); diff --git a/mesop/components/slide_toggle/slide_toggle.ng.html b/mesop/components/slide_toggle/slide_toggle.ng.html new file mode 100644 index 000000000..66224eb1a --- /dev/null +++ b/mesop/components/slide_toggle/slide_toggle.ng.html @@ -0,0 +1,17 @@ + + diff --git a/mesop/components/slide_toggle/slide_toggle.proto b/mesop/components/slide_toggle/slide_toggle.proto new file mode 100644 index 000000000..f5edf3d1d --- /dev/null +++ b/mesop/components/slide_toggle/slide_toggle.proto @@ -0,0 +1,21 @@ + +syntax = "proto3"; + +package mesop.components.slide_toggle; + +message SlideToggleType { + string name = 1; + string id = 2; + string label_position = 3; + string aria_label = 4; + string aria_labelledby = 5; + string aria_describedby = 6; + bool required = 7; + string color = 8; + bool disabled = 9; + bool disable_ripple = 10; + double tab_index = 11; + bool checked = 12; + bool hide_icon = 13; + string on_slide_toggle_change_event_handler_id = 14; +} diff --git a/mesop/components/slide_toggle/slide_toggle.py b/mesop/components/slide_toggle/slide_toggle.py new file mode 100644 index 000000000..11621bb14 --- /dev/null +++ b/mesop/components/slide_toggle/slide_toggle.py @@ -0,0 +1,87 @@ +from dataclasses import dataclass +from typing import Any, Callable, Literal + +from pydantic import validate_arguments + +import mesop.components.slide_toggle.slide_toggle_pb2 as slide_toggle_pb +from mesop.component_helpers import ( + handler_type, + insert_component, + register_event_mapper, +) +from mesop.events import MesopEvent + + +@dataclass +class SlideToggleChangeEvent(MesopEvent): + pass + + +register_event_mapper( + SlideToggleChangeEvent, + lambda event, key: SlideToggleChangeEvent( + key=key, + ), +) + + +@validate_arguments +def slide_toggle( + *, + key: str | None = None, + name: str = "", + id: str = "", + label_position: Literal["before", "after"] = "after", + aria_label: str = "", + aria_labelledby: str = "", + aria_describedby: str = "", + required: bool = False, + color: str = "", + disabled: bool = False, + disable_ripple: bool = False, + tab_index: float = 0, + checked: bool = False, + hide_icon: bool = False, + on_change: Callable[[SlideToggleChangeEvent], Any] | None = None, +): + """Creates a Slide toggle component. + + Args: + key (str|None): Unique identifier for this component instance. + name (str): Name value will be applied to the input element if present. + id (str): A unique id for the slide-toggle input. If none is supplied, it will be auto-generated. + label_position (Literal['before','after']): Whether the label should appear after or before the slide-toggle. Defaults to 'after'. + aria_label (str): Used to set the aria-label attribute on the underlying input element. + aria_labelledby (str): Used to set the aria-labelledby attribute on the underlying input element. + aria_describedby (str): Used to set the aria-describedby attribute on the underlying input element. + required (bool): Whether the slide-toggle is required. + color (str): Palette color of slide toggle. + disabled (bool): Whether the slide toggle is disabled. + disable_ripple (bool): Whether the slide toggle has a ripple. + tab_index (float): Tabindex of slide toggle. + checked (bool): Whether the slide-toggle element is checked or not. + hide_icon (bool): Whether to hide the icon inside of the slide toggle. + on_change (Callable[[SlideToggleChangeEvent], Any]|None): An event will be dispatched each time the slide-toggle changes its value. + """ + insert_component( + key=key, + type_name="slide_toggle", + proto=slide_toggle_pb.SlideToggleType( + name=name, + id=id, + label_position=label_position, + aria_label=aria_label, + aria_labelledby=aria_labelledby, + aria_describedby=aria_describedby, + required=required, + color=color, + disabled=disabled, + disable_ripple=disable_ripple, + tab_index=tab_index, + checked=checked, + hide_icon=hide_icon, + on_slide_toggle_change_event_handler_id=handler_type(on_change) + if on_change + else "", + ), + ) diff --git a/mesop/components/slide_toggle/slide_toggle.ts b/mesop/components/slide_toggle/slide_toggle.ts new file mode 100644 index 000000000..a0d35346b --- /dev/null +++ b/mesop/components/slide_toggle/slide_toggle.ts @@ -0,0 +1,49 @@ +import { + MatSlideToggleModule, + MatSlideToggleChange, +} from '@angular/material/slide-toggle'; +import {Component, Input} from '@angular/core'; +import { + UserEvent, + Key, + Type, +} from 'mesop/mesop/protos/ui_jspb_proto_pb/mesop/protos/ui_pb'; +import {SlideToggleType} from 'mesop/mesop/components/slide_toggle/slide_toggle_jspb_proto_pb/mesop/components/slide_toggle/slide_toggle_pb'; +import {Channel} from '../../web/src/services/channel'; + +@Component({ + templateUrl: 'slide_toggle.ng.html', + standalone: true, + imports: [MatSlideToggleModule], +}) +export class SlideToggleComponent { + @Input({required: true}) type!: Type; + @Input() key!: Key; + private _config!: SlideToggleType; + + constructor(private readonly channel: Channel) {} + + ngOnChanges() { + this._config = SlideToggleType.deserializeBinary( + this.type.getValue() as unknown as Uint8Array, + ); + } + + config(): SlideToggleType { + return this._config; + } + + getLabelPosition(): 'before' | 'after' { + return this.config().getLabelPosition() as 'before' | 'after'; + } + + onSlideToggleChangeEvent(event: MatSlideToggleChange): void { + const userEvent = new UserEvent(); + + userEvent.setHandlerId( + this.config().getOnSlideToggleChangeEventHandlerId(), + ); + userEvent.setKey(this.key); + this.channel.dispatch(userEvent); + } +} diff --git a/mesop/testing/BUILD b/mesop/testing/BUILD index a1aa1b9e2..ae8cd938d 100644 --- a/mesop/testing/BUILD +++ b/mesop/testing/BUILD @@ -9,6 +9,8 @@ py_library( srcs = ["index.py"], deps = [ # REF(//scripts/scaffold_component.py):insert_component_e2e_import + "//mesop/components/slide_toggle/e2e", + "//mesop/components/progress_spinner/e2e", "//mesop/components/progress_bar/e2e", "//mesop/components/icon/e2e", "//mesop/components/divider/e2e", diff --git a/mesop/testing/index.py b/mesop/testing/index.py index f2572f146..fa1c04093 100644 --- a/mesop/testing/index.py +++ b/mesop/testing/index.py @@ -10,4 +10,6 @@ import mesop.components.divider.e2e as divider_e2e import mesop.components.icon.e2e as icon_e2e import mesop.components.progress_bar.e2e as progress_bar_e2e +import mesop.components.progress_spinner.e2e as progress_spinner_e2e +import mesop.components.slide_toggle.e2e as slide_toggle_e2e # REF(//scripts/scaffold_component.py):insert_component_e2e_import_export diff --git a/mesop/web/src/component_renderer/BUILD b/mesop/web/src/component_renderer/BUILD index 07115c7cb..6820650c6 100644 --- a/mesop/web/src/component_renderer/BUILD +++ b/mesop/web/src/component_renderer/BUILD @@ -14,6 +14,8 @@ ng_module( ]), deps = [ # REF(//scripts/scaffold_component.py):insert_component_import + "//mesop/components/slide_toggle:ng", + "//mesop/components/progress_spinner:ng", "//mesop/components/progress_bar:ng", "//mesop/components/icon:ng", "//mesop/components/divider:ng", diff --git a/mesop/web/src/component_renderer/type_to_component.ts b/mesop/web/src/component_renderer/type_to_component.ts index 3d0044633..c94e91092 100644 --- a/mesop/web/src/component_renderer/type_to_component.ts +++ b/mesop/web/src/component_renderer/type_to_component.ts @@ -1,3 +1,5 @@ +import {SlideToggleComponent} from '../../../components/slide_toggle/slide_toggle'; +import {ProgressSpinnerComponent} from '../../../components/progress_spinner/progress_spinner'; import {ProgressBarComponent} from '../../../components/progress_bar/progress_bar'; import {IconComponent} from '../../../components/icon/icon'; import {DividerComponent} from '../../../components/divider/divider'; @@ -27,6 +29,8 @@ export interface TypeToComponent { } export const typeToComponent = { + 'slide_toggle': SlideToggleComponent, + 'progress_spinner': ProgressSpinnerComponent, 'progress_bar': ProgressBarComponent, 'icon': IconComponent, 'divider': DividerComponent, diff --git a/mkdocs.yml b/mkdocs.yml index 9e7bf0e2e..d7191654b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,6 +13,7 @@ nav: - Icon: components/icon.md - Input: components/input.md - Progress bar: components/progress_bar.md + - Progress spinner: components/progress_spinner.md - Text: components/text.md - Tooltip: components/tooltip.md - Features: