From d95e66f1d92d57a03e2e8d24a11d14e4446d8b79 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 12 Dec 2024 10:07:57 +0100 Subject: [PATCH 1/2] Added Python `Tabs` class --- chartlets.py/CHANGES.md | 3 +++ chartlets.py/chartlets/components/__init__.py | 1 + chartlets.py/chartlets/components/tabs.py | 15 +++++++++++++++ chartlets.py/tests/components/tabs_test.py | 16 ++++++++++++++++ 4 files changed, 35 insertions(+) create mode 100644 chartlets.py/chartlets/components/tabs.py create mode 100644 chartlets.py/tests/components/tabs_test.py diff --git a/chartlets.py/CHANGES.md b/chartlets.py/CHANGES.md index 776fb51c..03ecb7ba 100644 --- a/chartlets.py/CHANGES.md +++ b/chartlets.py/CHANGES.md @@ -16,6 +16,9 @@ - using `schema` instead of `type` property for callback arguments - using `return` object with `schema` property for callback return values +* New (MUI) components: + - `Tabs` + ## 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 21908027..d85291f4 100644 --- a/chartlets.py/chartlets/components/__init__.py +++ b/chartlets.py/chartlets/components/__init__.py @@ -8,4 +8,5 @@ from .progress import LinearProgressWithLabel from .charts.vega import VegaChart from .select import Select +from .tabs import Tabs from .typography import Typography diff --git a/chartlets.py/chartlets/components/tabs.py b/chartlets.py/chartlets/components/tabs.py new file mode 100644 index 00000000..a0bca541 --- /dev/null +++ b/chartlets.py/chartlets/components/tabs.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass, field + +from chartlets import Component + + +@dataclass(frozen=True) +class Tabs(Component): + """Select components are used for collecting user provided + information from a list of options.""" + + value: int | None = None + """The currently selected tab index.""" + + titles: list[str] = field(default_factory=list) + """The list of tab titles.""" diff --git a/chartlets.py/tests/components/tabs_test.py b/chartlets.py/tests/components/tabs_test.py new file mode 100644 index 00000000..031d94ad --- /dev/null +++ b/chartlets.py/tests/components/tabs_test.py @@ -0,0 +1,16 @@ +from chartlets.components import Tabs +from tests.component_test import make_base + + +class TabsTest(make_base(Tabs)): + + def test_is_json_serializable(self): + self.assert_is_json_serializable( + self.cls(titles=["A", "B", "C"]), + {"type": "Tabs", "titles": ["A", "B", "C"]}, + ) + + self.assert_is_json_serializable( + self.cls(value=1, titles=["A", "B", "C"]), + {"type": "Tabs", "value": 1, "titles": ["A", "B", "C"]}, + ) From b8a0fa0c1418381a8047635b6d59afa8e52833a7 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 12 Dec 2024 16:21:24 +0100 Subject: [PATCH 2/2] Working now, demo needed still --- chartlets.js/CHANGES.md | 1 + .../lib/src/plugins/mui/Tabs.test.tsx | 49 +++++++++++++++ .../packages/lib/src/plugins/mui/Tabs.tsx | 59 +++++++++++++++++++ .../packages/lib/src/plugins/mui/index.ts | 2 + chartlets.py/chartlets/components/__init__.py | 1 + chartlets.py/chartlets/components/tabs.py | 26 ++++++-- chartlets.py/tests/components/tabs_test.py | 25 ++++++-- 7 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 chartlets.js/packages/lib/src/plugins/mui/Tabs.test.tsx create mode 100644 chartlets.js/packages/lib/src/plugins/mui/Tabs.tsx diff --git a/chartlets.js/CHANGES.md b/chartlets.js/CHANGES.md index 181bfa29..5e44a3d9 100644 --- a/chartlets.js/CHANGES.md +++ b/chartlets.js/CHANGES.md @@ -40,6 +40,7 @@ * New (MUI) components - `LinearProgress` - `Switch` + - `Tabs` ## Version 0.0.29 (from 2024/11/26) diff --git a/chartlets.js/packages/lib/src/plugins/mui/Tabs.test.tsx b/chartlets.js/packages/lib/src/plugins/mui/Tabs.test.tsx new file mode 100644 index 00000000..44e38bbd --- /dev/null +++ b/chartlets.js/packages/lib/src/plugins/mui/Tabs.test.tsx @@ -0,0 +1,49 @@ +import { describe, it, expect } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { createChangeHandler } from "./common.test"; +import { Tabs } from "./Tabs"; + +describe("Tabs", () => { + it("should render the Tabs component", () => { + render( + {}} + children={["Datasets", "Variables"]} + />, + ); + // to inspect rendered component, do: + // expect(document.querySelector("#tbs")).toEqual({}); + expect(screen.getByText("Datasets")).not.toBeUndefined(); + }); + + it("should fire 'value' property", () => { + const { recordedEvents, onChange } = createChangeHandler(); + render( + , + ); + fireEvent.click(screen.getByText("Variables")); + expect(recordedEvents.length).toEqual(1); + expect(recordedEvents[0]).toEqual({ + componentType: "Tabs", + id: "tbs", + property: "value", + value: 1, + }); + fireEvent.click(screen.getByText("Stats")); + expect(recordedEvents.length).toEqual(2); + expect(recordedEvents[1]).toEqual({ + componentType: "Tabs", + id: "tbs", + property: "value", + value: 2, + }); + }); +}); diff --git a/chartlets.js/packages/lib/src/plugins/mui/Tabs.tsx b/chartlets.js/packages/lib/src/plugins/mui/Tabs.tsx new file mode 100644 index 00000000..355c3042 --- /dev/null +++ b/chartlets.js/packages/lib/src/plugins/mui/Tabs.tsx @@ -0,0 +1,59 @@ +import MuiIcon from "@mui/material/Icon"; +import MuiTabs from "@mui/material/Tabs"; +import MuiTab from "@mui/material/Tab"; + +import type { ComponentProps, ComponentState } from "@/index"; +import type { SyntheticEvent } from "react"; +import { isString } from "@/utils/isString"; +import { isComponentState } from "@/types/state/component"; + +interface TabState { + type: "Tab"; + label?: string; + icon?: string; + disabled?: boolean; +} + +interface TabsState extends ComponentState { + value?: number; + children?: (string | TabState)[]; +} + +interface TabsProps extends ComponentProps, TabsState {} + +export function Tabs({ + type, + id, + value, + children: tabItems, + disabled, + style, + onChange, +}: TabsProps) { + const handleChange = (_event: SyntheticEvent, value: number) => { + if (id) { + onChange({ + componentType: type, + id: id, + property: "value", + value: value, + }); + } + }; + return ( + + {tabItems?.map((tab) => { + const tabState = isComponentState(tab) ? (tab as TabState) : undefined; + return ( + {tabState.icon} + } + disabled={disabled || (tabState && tabState.disabled)} + /> + ); + })} + + ); +} diff --git a/chartlets.js/packages/lib/src/plugins/mui/index.ts b/chartlets.js/packages/lib/src/plugins/mui/index.ts index 06b0219d..4878289b 100644 --- a/chartlets.js/packages/lib/src/plugins/mui/index.ts +++ b/chartlets.js/packages/lib/src/plugins/mui/index.ts @@ -6,6 +6,7 @@ import { CircularProgress } from "./CircularProgress"; import { IconButton } from "./IconButton"; import { Select } from "./Select"; import { Switch } from "./Switch"; +import { Tabs } from "./Tabs"; import { Typography } from "./Typography"; export default function mui(): Plugin { @@ -18,6 +19,7 @@ export default function mui(): Plugin { ["IconButton", IconButton], ["Select", Select], ["Switch", Switch], + ["Tabs", Tabs], ["Typography", Typography], ], }; diff --git a/chartlets.py/chartlets/components/__init__.py b/chartlets.py/chartlets/components/__init__.py index 9585041f..1c9b27dd 100644 --- a/chartlets.py/chartlets/components/__init__.py +++ b/chartlets.py/chartlets/components/__init__.py @@ -9,5 +9,6 @@ from .charts.vega import VegaChart from .select import Select from .switch import Switch +from .tabs import Tab from .tabs import Tabs from .typography import Typography diff --git a/chartlets.py/chartlets/components/tabs.py b/chartlets.py/chartlets/components/tabs.py index a0bca541..49057a02 100644 --- a/chartlets.py/chartlets/components/tabs.py +++ b/chartlets.py/chartlets/components/tabs.py @@ -3,13 +3,31 @@ from chartlets import Component +@dataclass(frozen=True) +class Tab(Component): + """The tab element itself. + Clicking on a tab displays its corresponding panel. + """ + + icon: str | None = None + """The tab icon's name.""" + + label: str | None = None + """The tab label.""" + + disabled: bool | None = None + """Whether the tab is disabled.""" + + @dataclass(frozen=True) class Tabs(Component): - """Select components are used for collecting user provided - information from a list of options.""" + """Tabs make it easy to explore and switch between different views. + Tabs organize and allow navigation between groups of content that + are related and at the same level of hierarchy. + """ value: int | None = None """The currently selected tab index.""" - titles: list[str] = field(default_factory=list) - """The list of tab titles.""" + children: list[str | Tab] = field(default_factory=list) + """The list of tab labels or `Tab` components.""" diff --git a/chartlets.py/tests/components/tabs_test.py b/chartlets.py/tests/components/tabs_test.py index 031d94ad..fb7feeef 100644 --- a/chartlets.py/tests/components/tabs_test.py +++ b/chartlets.py/tests/components/tabs_test.py @@ -1,4 +1,4 @@ -from chartlets.components import Tabs +from chartlets.components import Tabs, Tab from tests.component_test import make_base @@ -6,11 +6,26 @@ class TabsTest(make_base(Tabs)): def test_is_json_serializable(self): self.assert_is_json_serializable( - self.cls(titles=["A", "B", "C"]), - {"type": "Tabs", "titles": ["A", "B", "C"]}, + self.cls(children=["A", "B", "C"]), + {"type": "Tabs", "children": ["A", "B", "C"]}, ) self.assert_is_json_serializable( - self.cls(value=1, titles=["A", "B", "C"]), - {"type": "Tabs", "value": 1, "titles": ["A", "B", "C"]}, + self.cls( + value=1, + children=[ + Tab(label="A"), + Tab(icon="favorite"), + Tab(label="C", disabled=True), + ], + ), + { + "type": "Tabs", + "value": 1, + "children": [ + {"type": "Tab", "label": "A"}, + {"type": "Tab", "icon": "favorite"}, + {"type": "Tab", "label": "C", "disabled": True}, + ], + }, )