diff --git a/chartlets.js/CHANGES.md b/chartlets.js/CHANGES.md index c93634e..a89a622 100644 --- a/chartlets.js/CHANGES.md +++ b/chartlets.js/CHANGES.md @@ -3,6 +3,10 @@ * In `chartlets.js` we no longer emit warnings and errors in common situations to avoid too much spam in the browser console. +* New (MUI) components + - `DataGrid` + - `Dialog` + ## Version 0.1.3 (from 2025/01/28) * **Chore:** Version bump to align CI process with GitHub release flow. @@ -55,7 +59,6 @@ - `Switch` - `Tabs` - `Slider` - - `DataGrid` * Supporting `tooltip` property for interactive MUI components. diff --git a/chartlets.js/packages/lib/src/plugins/mui/Button.tsx b/chartlets.js/packages/lib/src/plugins/mui/Button.tsx index 5e63927..fb2f3aa 100644 --- a/chartlets.js/packages/lib/src/plugins/mui/Button.tsx +++ b/chartlets.js/packages/lib/src/plugins/mui/Button.tsx @@ -2,7 +2,7 @@ import { type MouseEvent } from "react"; import MuiButton from "@mui/material/Button"; import MuiIcon from "@mui/material/Icon"; -import type { ComponentState, ComponentProps } from "@/index"; +import type { ComponentProps, ComponentState } from "@/index"; import { Tooltip } from "./Tooltip"; interface ButtonState extends ComponentState { @@ -20,7 +20,7 @@ interface ButtonState extends ComponentState { | "warning"; } -interface ButtonProps extends ComponentProps, ButtonState {} +export interface ButtonProps extends ComponentProps, ButtonState {} export function Button({ type, @@ -33,6 +33,7 @@ export function Button({ text, startIcon, endIcon, + tooltip, onChange, }: ButtonProps) { const handleClick = (_event: MouseEvent) => { @@ -46,7 +47,7 @@ export function Button({ } }; return ( - + { + it("should render the Dialog component", () => { + render( + {}} + />, + ); + + expect(screen.getByRole("dialog")).toBeInTheDocument(); + expect(screen.getByText("Test Title")).toBeInTheDocument(); + expect(screen.getByText("Test Content")).toBeInTheDocument(); + }); + + it("should not render the Dialog if open is false", () => { + render( + {}} + />, + ); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + }); + + it("should handle onClose event and call onChange", () => { + const { recordedEvents, onChange } = createChangeHandler(); + + render( + , + ); + + const backdrop = document.querySelector(".MuiBackdrop-root"); + expect(backdrop).toBeInTheDocument(); + if (backdrop) { + fireEvent.click(backdrop); + } + + expect(recordedEvents.length).toBe(1); + expect(recordedEvents[0]).toEqual({ + componentType: "Dialog", + id: "test-dialog", + property: "open", + value: false, + }); + }); + + it("should render children within DialogActions", () => { + registry.register("Button", Button); + render( + {}} + >, + ); + + expect(screen.getByRole("button")).toBeInTheDocument(); + expect(screen.getByText("Click me!")).toBeInTheDocument(); + }); +}); diff --git a/chartlets.js/packages/lib/src/plugins/mui/Dialog.tsx b/chartlets.js/packages/lib/src/plugins/mui/Dialog.tsx new file mode 100644 index 0000000..e672097 --- /dev/null +++ b/chartlets.js/packages/lib/src/plugins/mui/Dialog.tsx @@ -0,0 +1,94 @@ +import { + Dialog as MuiDialog, + DialogActions, + DialogContent, + type DialogContentProps, + DialogContentText, + type DialogProps as MuiDialogProps, + DialogTitle, + type DialogTitleProps, +} from "@mui/material"; + +import type { TypographyProps } from "@mui/material/Typography"; +import { Children, type ComponentProps, type ComponentState } from "@/index"; + +interface DialogState extends ComponentState { + open?: boolean; + title?: string; + titleProps?: DialogTitleProps & TypographyProps; + content?: string; + contentProps?: DialogContentProps & TypographyProps; + disableEscapeKeyDown?: boolean; + fullScreen?: boolean; + fullWidth?: boolean; + maxWidth?: MuiDialogProps["maxWidth"]; + scroll?: MuiDialogProps["scroll"]; + ariaLabel?: string; + ariaDescribedBy?: string; +} + +interface DialogProps extends ComponentProps, DialogState {} + +export const Dialog = ({ + id, + type, + style, + open, + title, + titleProps, + content, + contentProps, + disableEscapeKeyDown, + fullScreen, + fullWidth, + maxWidth, + scroll, + ariaLabel, + ariaDescribedBy, + children: nodes, + onChange, +}: DialogProps) => { + if (!open) { + return; + } + const handleClose: MuiDialogProps["onClose"] = (_event, _reason) => { + if (id) { + onChange({ + componentType: type, + id: id, + property: "open", + value: false, + }); + } + }; + + return ( + + {title && ( + {title} + )} + {content && ( + + {content} + + )} + {nodes && ( + + + + )} + + ); +}; diff --git a/chartlets.js/packages/lib/src/plugins/mui/index.ts b/chartlets.js/packages/lib/src/plugins/mui/index.ts index 0f08b34..9b10ac9 100644 --- a/chartlets.js/packages/lib/src/plugins/mui/index.ts +++ b/chartlets.js/packages/lib/src/plugins/mui/index.ts @@ -13,6 +13,7 @@ import { Tabs } from "./Tabs"; import { Typography } from "./Typography"; import { Slider } from "./Slider"; import { DataGrid } from "@/plugins/mui/DataGrid"; +import { Dialog } from "@/plugins/mui/Dialog"; export default function mui(): Plugin { return { @@ -22,6 +23,7 @@ export default function mui(): Plugin { ["Checkbox", Checkbox], ["CircularProgress", CircularProgress], ["DataGrid", DataGrid], + ["Dialog", Dialog], ["Divider", Divider], ["IconButton", IconButton], ["LinearProgress", LinearProgress], diff --git a/chartlets.py/CHANGES.md b/chartlets.py/CHANGES.md index 8b9d67a..683b9a4 100644 --- a/chartlets.py/CHANGES.md +++ b/chartlets.py/CHANGES.md @@ -1,9 +1,17 @@ +## Version 0.1.4 (in development) + +* New (MUI) components + - `DataGrid` + - `Dialog` + + ## Version 0.1.3 (from 2025/01/28) * **Chore:** Version bump to align CI process with GitHub release flow. No functional changes. This release ensures proper triggering of the CI pipeline for publishing to PyPI. + ## Version 0.1.0 (from 2025/01/14) * Reorganised Chartlets project to better separate demo from library code. @@ -30,7 +38,6 @@ - `Switch` - `Slider` - `Tabs` and `Tab` - - `DataGrid` ## Version 0.0.29 (from 2024/11/26) diff --git a/chartlets.py/chartlets/components/__init__.py b/chartlets.py/chartlets/components/__init__.py index b42c2ee..6fdb1e3 100644 --- a/chartlets.py/chartlets/components/__init__.py +++ b/chartlets.py/chartlets/components/__init__.py @@ -2,6 +2,7 @@ from .button import Button from .button import IconButton from .checkbox import Checkbox +from .dialog import Dialog from .charts.vega import VegaChart from .divider import Divider from .progress import CircularProgress diff --git a/chartlets.py/chartlets/components/dialog.py b/chartlets.py/chartlets/components/dialog.py new file mode 100644 index 0000000..91a5ef4 --- /dev/null +++ b/chartlets.py/chartlets/components/dialog.py @@ -0,0 +1,50 @@ +from dataclasses import dataclass, field +from typing import Literal, Any + +from chartlets import Container + + +@dataclass(frozen=True) +class Dialog(Container): + """A modal dialog that presents content and actions in a focused interface.""" + + open: bool = field(default=False) + """Controls whether the dialog is open.""" + + title: str | None = None + """The title of the dialog.""" + + titleProps: dict[str, Any] | None = None + """Additional properties for the dialog title. Can include + typography-related attributes. + https://mui.com/material-ui/api/dialog-title/""" + + content: str | None = None + """The content of the dialog.""" + + contentProps: dict[str, Any] | None = None + """Additional properties for the dialog content. Can include + typography-related attributes. + https://mui.com/material-ui/api/dialog-content-text/""" + + disableEscapeKeyDown: bool | None = None + """If true, pressing the Escape key does not close the dialog.""" + + fullScreen: bool | None = None + """If true, the dialog will be displayed in full-screen mode.""" + + fullWidth: bool | None = None + """If true, the dialog will take up the full width of the screen.""" + + maxWidth: Literal["xs", "sm", "md", "lg", "xl", False] | str | None = None + """The maximum width of the dialog.""" + + scroll: Literal["body", "paper"] | None = None + """Determines the scroll behavior of the dialog's content.""" + + ariaLabel: str | None = None + """Defines a string value that labels the dialog for accessibility.""" + + ariaDescribedBy: str | None = None + """Defines the ID of an element that describes the dialog for + accessibility.""" diff --git a/chartlets.py/demo/my_extension/__init__.py b/chartlets.py/demo/my_extension/__init__.py index 0f969ee..e1d4538 100644 --- a/chartlets.py/demo/my_extension/__init__.py +++ b/chartlets.py/demo/my_extension/__init__.py @@ -3,9 +3,11 @@ from .my_panel_2 import panel as my_panel_2 from .my_panel_3 import panel as my_panel_3 from .my_panel_4 import panel as my_panel_4 +from .my_panel_5 import panel as my_panel_5 ext = Extension(__name__) ext.add(my_panel_1) ext.add(my_panel_2) ext.add(my_panel_3) ext.add(my_panel_4) +ext.add(my_panel_5) diff --git a/chartlets.py/demo/my_extension/my_panel_5.py b/chartlets.py/demo/my_extension/my_panel_5.py new file mode 100644 index 0000000..1b0b4f5 --- /dev/null +++ b/chartlets.py/demo/my_extension/my_panel_5.py @@ -0,0 +1,79 @@ +from chartlets import Component, Input, Output +from chartlets.components import Box, Button, Dialog, Typography + +from server.context import Context +from server.panel import Panel + +panel = Panel(__name__, title="Panel E") + + +# noinspection PyUnusedLocal +@panel.layout() +def render_panel( + ctx: Context, +) -> Component: + open_button = Button(id="open_button", text="Open Dialog") + okay_button = Button(id="okay_button", text="Okay") + not_okay_button = Button(id="not_okay_button", text="Not okay") + dialog = Dialog( + id="dialog", + title="This is a modal dialog", + titleProps={ + "id": "dialog-title", + "variant": "h6", + "style": {"fontWeight": "bold", "color": "darkblue"}, + }, + content="You can add your content here in the dialog.", + contentProps={ + "id": "dialog-description", + "variant": "body1", + "style": {"padding": "16px", "color": "gray"}, + }, + children=[okay_button, not_okay_button], + disableEscapeKeyDown=True, + fullScreen=False, + fullWidth=True, + maxWidth="sm", + scroll="paper", + ariaLabel="dialog-title", + ariaDescribedBy="dialog-description", + ) + + info_text = Typography(id="info_text") + + return Box( + style={ + "display": "flex", + "flexDirection": "column", + "width": "100%", + "height": "100%", + "gap": "6px", + }, + children=[open_button, dialog, info_text], + ) + + +# noinspection PyUnusedLocal +@panel.callback(Input("open_button", "clicked"), Output("dialog", "open")) +def dialog_on_open(ctx: Context, button) -> bool: + return True + + +# noinspection PyUnusedLocal +@panel.callback( + Input("okay_button", "clicked"), + Output("dialog", "open"), + Output("info_text", "children"), +) +def dialog_on_close(ctx: Context, button) -> tuple[bool, list[str]]: + return False, ["Okay button was clicked!"] + + +# noinspection PyUnusedLocal +@panel.callback( + Input("not_okay_button", "clicked"), + Output("dialog", "open"), + Output("info_text", "children"), +) +def dialog_on_close(ctx: Context, button) -> tuple[bool, list[str]]: + return False, ["Not okay button was clicked!"] diff --git a/chartlets.py/tests/dialog_test.py b/chartlets.py/tests/dialog_test.py new file mode 100644 index 0000000..e03b0d9 --- /dev/null +++ b/chartlets.py/tests/dialog_test.py @@ -0,0 +1,45 @@ +from chartlets.components import Dialog, Button +from tests.component_test import make_base + + +class DialogTest(make_base(Dialog)): + + def test_is_json_serializable(self): + self.assert_is_json_serializable( + self.cls( + open=True, + title="My Dialog", + content="This is the content", + maxWidth="md", + scroll="body", + ), + { + "type": "Dialog", + "open": True, + "title": "My Dialog", + "content": "This is the content", + "maxWidth": "md", + "scroll": "body", + "children": [], + }, + ) + + self.assert_is_json_serializable( + self.cls( + open=True, + title="My Dialog", + content="This is the content", + maxWidth="md", + scroll="body", + children=[Button(id="button")], + ), + { + "type": "Dialog", + "open": True, + "title": "My Dialog", + "content": "This is the content", + "maxWidth": "md", + "scroll": "body", + "children": [{"id": "button", "type": "Button"}], + }, + )