Skip to content

Commit 49014f4

Browse files
b-yogeshforman
andauthored
Add Dialog component (#90)
* Implement Dialog as mui plugin * Update button.tsx (missing tooltip) * Implement dialog in chartlets.py * Add demo * Add demo in init * Add tests [WIP] * Add tests * update CHANGES.md * Apply suggestions from code review Co-authored-by: Norman Fomferra <[email protected]> * update CHANGES.md --------- Co-authored-by: Norman Fomferra <[email protected]>
1 parent 75f9f37 commit 49014f4

File tree

11 files changed

+379
-5
lines changed

11 files changed

+379
-5
lines changed

chartlets.js/CHANGES.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
* In `chartlets.js` we no longer emit warnings and errors in common
44
situations to avoid too much spam in the browser console.
55

6+
* New (MUI) components
7+
- `DataGrid`
8+
- `Dialog`
9+
610
## Version 0.1.3 (from 2025/01/28)
711

812
* **Chore:** Version bump to align CI process with GitHub release flow.
@@ -55,7 +59,6 @@
5559
- `Switch`
5660
- `Tabs`
5761
- `Slider`
58-
- `DataGrid`
5962

6063
* Supporting `tooltip` property for interactive MUI components.
6164

chartlets.js/packages/lib/src/plugins/mui/Button.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type MouseEvent } from "react";
22
import MuiButton from "@mui/material/Button";
33
import MuiIcon from "@mui/material/Icon";
44

5-
import type { ComponentState, ComponentProps } from "@/index";
5+
import type { ComponentProps, ComponentState } from "@/index";
66
import { Tooltip } from "./Tooltip";
77

88
interface ButtonState extends ComponentState {
@@ -20,7 +20,7 @@ interface ButtonState extends ComponentState {
2020
| "warning";
2121
}
2222

23-
interface ButtonProps extends ComponentProps, ButtonState {}
23+
export interface ButtonProps extends ComponentProps, ButtonState {}
2424

2525
export function Button({
2626
type,
@@ -33,6 +33,7 @@ export function Button({
3333
text,
3434
startIcon,
3535
endIcon,
36+
tooltip,
3637
onChange,
3738
}: ButtonProps) {
3839
const handleClick = (_event: MouseEvent<HTMLButtonElement>) => {
@@ -46,7 +47,7 @@ export function Button({
4647
}
4748
};
4849
return (
49-
<Tooltip>
50+
<Tooltip title={tooltip}>
5051
<MuiButton
5152
id={id}
5253
name={name}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { describe, expect, it } from "vitest";
2+
import { fireEvent, render, screen } from "@testing-library/react";
3+
import { registry } from "@/components/registry";
4+
import { createChangeHandler } from "@/plugins/mui/common.test";
5+
import { Button, type ButtonProps } from "@/plugins/mui/Button";
6+
import { Dialog } from "./Dialog";
7+
8+
describe("Dialog", () => {
9+
it("should render the Dialog component", () => {
10+
render(
11+
<Dialog
12+
id="test-dialog"
13+
type="Dialog"
14+
open={true}
15+
title="Test Title"
16+
content="Test Content"
17+
onChange={() => {}}
18+
/>,
19+
);
20+
21+
expect(screen.getByRole("dialog")).toBeInTheDocument();
22+
expect(screen.getByText("Test Title")).toBeInTheDocument();
23+
expect(screen.getByText("Test Content")).toBeInTheDocument();
24+
});
25+
26+
it("should not render the Dialog if open is false", () => {
27+
render(
28+
<Dialog
29+
id="test-dialog"
30+
type="Dialog"
31+
open={false}
32+
onChange={() => {}}
33+
/>,
34+
);
35+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
36+
});
37+
38+
it("should handle onClose event and call onChange", () => {
39+
const { recordedEvents, onChange } = createChangeHandler();
40+
41+
render(
42+
<Dialog
43+
id="test-dialog"
44+
type="Dialog"
45+
open={true}
46+
title="Test Title"
47+
content="Test Content"
48+
onChange={onChange}
49+
/>,
50+
);
51+
52+
const backdrop = document.querySelector(".MuiBackdrop-root");
53+
expect(backdrop).toBeInTheDocument();
54+
if (backdrop) {
55+
fireEvent.click(backdrop);
56+
}
57+
58+
expect(recordedEvents.length).toBe(1);
59+
expect(recordedEvents[0]).toEqual({
60+
componentType: "Dialog",
61+
id: "test-dialog",
62+
property: "open",
63+
value: false,
64+
});
65+
});
66+
67+
it("should render children within DialogActions", () => {
68+
registry.register("Button", Button);
69+
render(
70+
<Dialog
71+
id="test-dialog"
72+
type="Dialog"
73+
open={true}
74+
title="Test Title"
75+
content="Test Content"
76+
children={[
77+
{
78+
id: "test-button",
79+
type: "Button",
80+
text: "Click me!",
81+
} as ButtonProps,
82+
]}
83+
onChange={() => {}}
84+
></Dialog>,
85+
);
86+
87+
expect(screen.getByRole("button")).toBeInTheDocument();
88+
expect(screen.getByText("Click me!")).toBeInTheDocument();
89+
});
90+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import {
2+
Dialog as MuiDialog,
3+
DialogActions,
4+
DialogContent,
5+
type DialogContentProps,
6+
DialogContentText,
7+
type DialogProps as MuiDialogProps,
8+
DialogTitle,
9+
type DialogTitleProps,
10+
} from "@mui/material";
11+
12+
import type { TypographyProps } from "@mui/material/Typography";
13+
import { Children, type ComponentProps, type ComponentState } from "@/index";
14+
15+
interface DialogState extends ComponentState {
16+
open?: boolean;
17+
title?: string;
18+
titleProps?: DialogTitleProps & TypographyProps;
19+
content?: string;
20+
contentProps?: DialogContentProps & TypographyProps;
21+
disableEscapeKeyDown?: boolean;
22+
fullScreen?: boolean;
23+
fullWidth?: boolean;
24+
maxWidth?: MuiDialogProps["maxWidth"];
25+
scroll?: MuiDialogProps["scroll"];
26+
ariaLabel?: string;
27+
ariaDescribedBy?: string;
28+
}
29+
30+
interface DialogProps extends ComponentProps, DialogState {}
31+
32+
export const Dialog = ({
33+
id,
34+
type,
35+
style,
36+
open,
37+
title,
38+
titleProps,
39+
content,
40+
contentProps,
41+
disableEscapeKeyDown,
42+
fullScreen,
43+
fullWidth,
44+
maxWidth,
45+
scroll,
46+
ariaLabel,
47+
ariaDescribedBy,
48+
children: nodes,
49+
onChange,
50+
}: DialogProps) => {
51+
if (!open) {
52+
return;
53+
}
54+
const handleClose: MuiDialogProps["onClose"] = (_event, _reason) => {
55+
if (id) {
56+
onChange({
57+
componentType: type,
58+
id: id,
59+
property: "open",
60+
value: false,
61+
});
62+
}
63+
};
64+
65+
return (
66+
<MuiDialog
67+
id={id}
68+
style={style}
69+
open={open}
70+
onClose={handleClose}
71+
disableEscapeKeyDown={disableEscapeKeyDown}
72+
fullScreen={fullScreen}
73+
fullWidth={fullWidth}
74+
maxWidth={maxWidth}
75+
scroll={scroll}
76+
aria-label={ariaLabel}
77+
aria-describedby={ariaDescribedBy}
78+
>
79+
{title && (
80+
<DialogTitle {...titleProps}>{title}</DialogTitle>
81+
)}
82+
{content && (
83+
<DialogContent {...contentProps}>
84+
<DialogContentText>{content}</DialogContentText>
85+
</DialogContent>
86+
)}
87+
{nodes && (
88+
<DialogActions>
89+
<Children nodes={nodes} onChange={onChange} />
90+
</DialogActions>
91+
)}
92+
</MuiDialog>
93+
);
94+
};

chartlets.js/packages/lib/src/plugins/mui/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Tabs } from "./Tabs";
1313
import { Typography } from "./Typography";
1414
import { Slider } from "./Slider";
1515
import { DataGrid } from "@/plugins/mui/DataGrid";
16+
import { Dialog } from "@/plugins/mui/Dialog";
1617

1718
export default function mui(): Plugin {
1819
return {
@@ -22,6 +23,7 @@ export default function mui(): Plugin {
2223
["Checkbox", Checkbox],
2324
["CircularProgress", CircularProgress],
2425
["DataGrid", DataGrid],
26+
["Dialog", Dialog],
2527
["Divider", Divider],
2628
["IconButton", IconButton],
2729
["LinearProgress", LinearProgress],

chartlets.py/CHANGES.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1+
## Version 0.1.4 (in development)
2+
3+
* New (MUI) components
4+
- `DataGrid`
5+
- `Dialog`
6+
7+
18
## Version 0.1.3 (from 2025/01/28)
29

310
* **Chore:** Version bump to align CI process with GitHub release flow.
411
No functional changes. This release ensures proper triggering of the CI
512
pipeline for publishing to PyPI.
613

14+
715
## Version 0.1.0 (from 2025/01/14)
816

917
* Reorganised Chartlets project to better separate demo from library code.
@@ -30,7 +38,6 @@
3038
- `Switch`
3139
- `Slider`
3240
- `Tabs` and `Tab`
33-
- `DataGrid`
3441

3542
## Version 0.0.29 (from 2024/11/26)
3643

chartlets.py/chartlets/components/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .button import Button
33
from .button import IconButton
44
from .checkbox import Checkbox
5+
from .dialog import Dialog
56
from .charts.vega import VegaChart
67
from .divider import Divider
78
from .progress import CircularProgress
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from dataclasses import dataclass, field
2+
from typing import Literal, Any
3+
4+
from chartlets import Container
5+
6+
7+
@dataclass(frozen=True)
8+
class Dialog(Container):
9+
"""A modal dialog that presents content and actions in a focused interface."""
10+
11+
open: bool = field(default=False)
12+
"""Controls whether the dialog is open."""
13+
14+
title: str | None = None
15+
"""The title of the dialog."""
16+
17+
titleProps: dict[str, Any] | None = None
18+
"""Additional properties for the dialog title. Can include
19+
typography-related attributes.
20+
https://mui.com/material-ui/api/dialog-title/"""
21+
22+
content: str | None = None
23+
"""The content of the dialog."""
24+
25+
contentProps: dict[str, Any] | None = None
26+
"""Additional properties for the dialog content. Can include
27+
typography-related attributes.
28+
https://mui.com/material-ui/api/dialog-content-text/"""
29+
30+
disableEscapeKeyDown: bool | None = None
31+
"""If true, pressing the Escape key does not close the dialog."""
32+
33+
fullScreen: bool | None = None
34+
"""If true, the dialog will be displayed in full-screen mode."""
35+
36+
fullWidth: bool | None = None
37+
"""If true, the dialog will take up the full width of the screen."""
38+
39+
maxWidth: Literal["xs", "sm", "md", "lg", "xl", False] | str | None = None
40+
"""The maximum width of the dialog."""
41+
42+
scroll: Literal["body", "paper"] | None = None
43+
"""Determines the scroll behavior of the dialog's content."""
44+
45+
ariaLabel: str | None = None
46+
"""Defines a string value that labels the dialog for accessibility."""
47+
48+
ariaDescribedBy: str | None = None
49+
"""Defines the ID of an element that describes the dialog for
50+
accessibility."""

chartlets.py/demo/my_extension/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
from .my_panel_2 import panel as my_panel_2
44
from .my_panel_3 import panel as my_panel_3
55
from .my_panel_4 import panel as my_panel_4
6+
from .my_panel_5 import panel as my_panel_5
67

78
ext = Extension(__name__)
89
ext.add(my_panel_1)
910
ext.add(my_panel_2)
1011
ext.add(my_panel_3)
1112
ext.add(my_panel_4)
13+
ext.add(my_panel_5)

0 commit comments

Comments
 (0)