├── .nvmrc
├── dash_antd
├── py.typed
├── ext
│ ├── __init__.py
│ ├── _sidebar.py
│ └── _theming.py
├── __init__.py
└── _imports_.py
├── poetry.toml
├── .sonarcloud.properties
├── .vscode
├── settings.json
└── cspell.json
├── .eslintignore
├── setup.cfg
├── example
├── assets
│ └── app.css
├── __main__.py
├── pages
│ ├── __init__.py
│ ├── graph.py
│ └── 02_cards.py
├── __init__.py
└── metadata.py
├── codecov.yml
├── demo
├── index.tsx
├── index.html
└── Demo.tsx
├── webpack
├── directories.js
├── moduleDefinition.js
├── config.demo.js
└── config.dist.js
├── tsconfig.json
├── src
└── ts
│ ├── components
│ ├── tag
│ │ ├── __tests__
│ │ │ ├── Tag.test.tsx
│ │ │ └── CheckableTag.test.tsx
│ │ ├── CheckableTag.tsx
│ │ └── Tag.tsx
│ ├── alert
│ │ ├── __tests__
│ │ │ └── Alert.test.tsx
│ │ └── Alert.tsx
│ ├── divider
│ │ ├── __tests__
│ │ │ └── Divider.test.tsx
│ │ └── Divider.tsx
│ ├── select
│ │ ├── __tests__
│ │ │ └── Select.test.tsx
│ │ └── Select.tsx
│ ├── icon
│ │ └── Icon.tsx
│ ├── layout
│ │ ├── Footer.tsx
│ │ ├── Header.tsx
│ │ ├── Content.tsx
│ │ ├── Layout.tsx
│ │ ├── Row.tsx
│ │ ├── Sidebar.tsx
│ │ └── Col.tsx
│ ├── timeline
│ │ ├── TimelineItem.tsx
│ │ └── Timeline.tsx
│ ├── radio
│ │ ├── __tests__
│ │ │ ├── Radio.test.tsx
│ │ │ ├── RadioButton.test.tsx
│ │ │ └── RadioGroup.test.tsx
│ │ ├── Radio.tsx
│ │ ├── RadioButton.tsx
│ │ └── RadioGroup.tsx
│ ├── tabs
│ │ ├── TabPane.tsx
│ │ └── Tabs.tsx
│ ├── checkbox
│ │ ├── __tests__
│ │ │ ├── Checkbox.test.tsx
│ │ │ └── CheckboxGroup.test.tsx
│ │ ├── CheckboxGroup.tsx
│ │ └── Checkbox.tsx
│ ├── Space.tsx
│ ├── menu
│ │ ├── MenuItem.tsx
│ │ └── Menu.tsx
│ ├── templates
│ │ ├── Page.tsx
│ │ └── PagesWithSidebar.tsx
│ ├── button
│ │ ├── __tests__
│ │ │ └── Button.test.tsx
│ │ └── Button.tsx
│ ├── Switch.tsx
│ ├── Segmented.tsx
│ ├── dropdown
│ │ ├── DropdownMenu.tsx
│ │ └── DropdownButton.tsx
│ ├── Slider.tsx
│ ├── card
│ │ └── Card.tsx
│ ├── ConfigProvider.tsx
│ ├── steps
│ │ └── Steps.tsx
│ ├── datepicker
│ │ ├── TimeRangePicker.tsx
│ │ ├── DatePicker.tsx
│ │ ├── DateRangePicker.tsx
│ │ └── TimePicker.tsx
│ ├── autocomplete
│ │ ├── __tests__
│ │ │ └── AutoComplete.test.tsx
│ │ └── AutoComplete.tsx
│ └── input
│ │ ├── TextArea.tsx
│ │ ├── __tests__
│ │ ├── TextArea.test.tsx
│ │ └── Input.test.tsx
│ │ ├── InputNumber.tsx
│ │ └── Input.tsx
│ ├── utilities.ts
│ ├── index.ts
│ └── types.ts
├── .npmignore
├── .eslintrc.js
├── .editorconfig
├── LICENSE
├── .pre-commit-config.yaml
├── justfile
├── README.md
├── .github
└── workflows
│ ├── test.yaml
│ └── release.yaml
├── pyproject.toml
├── package.json
├── .secrets.baseline
├── .gitignore
└── jest.config.ts
/.nvmrc:
--------------------------------------------------------------------------------
1 | 16.14.0
2 |
--------------------------------------------------------------------------------
/dash_antd/py.typed:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/poetry.toml:
--------------------------------------------------------------------------------
1 | [virtualenvs]
2 | in-project = true
3 |
--------------------------------------------------------------------------------
/.sonarcloud.properties:
--------------------------------------------------------------------------------
1 | sonar.python.version=3.7, 3.8, 3.9, 3.10
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/*
2 | dash_antd/*
3 | webpack/*
4 | inst/*
5 | deps/*
6 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | # set to a value larger then configured in black
3 | max-line-length = 140
4 |
--------------------------------------------------------------------------------
/example/assets/app.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | scrollbar-width: thin;
5 | }
6 |
--------------------------------------------------------------------------------
/example/__main__.py:
--------------------------------------------------------------------------------
1 | from example import app
2 |
3 | if __name__ == "__main__":
4 | app.run_server(debug=True)
5 |
--------------------------------------------------------------------------------
/example/pages/__init__.py:
--------------------------------------------------------------------------------
1 | from .controls import controls_page as controls_page
2 | from .graph import graph_page as graph_page
3 |
--------------------------------------------------------------------------------
/dash_antd/ext/__init__.py:
--------------------------------------------------------------------------------
1 | from ._sidebar import generate_sidebar_layout as generate_sidebar_layout
2 | from ._theming import parse_tokens as parse_tokens
3 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project:
4 | default:
5 | informational: true
6 | patch:
7 | default:
8 | informational: true
9 |
--------------------------------------------------------------------------------
/demo/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | import Demo from "./Demo";
5 |
6 | ReactDOM.render(, document.getElementById("react-demo-entry-point"));
7 |
--------------------------------------------------------------------------------
/webpack/directories.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var path = require('path');
4 |
5 | var ROOT = process.cwd();
6 |
7 | module.exports = {
8 | ROOT: ROOT,
9 | SRC: path.join(ROOT, 'src'),
10 | DEMO: path.join(ROOT, 'demo'),
11 | };
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "baseUrl": "src/ts",
5 | "inlineSources": true,
6 | "sourceMap": true,
7 | "esModuleInterop": true
8 | },
9 | "exclude": [
10 | "node_modules"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/src/ts/components/tag/__tests__/Tag.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 | import Tag from "../Tag";
4 |
5 | describe("Tag", () => {
6 | test("renders its content", () => {
7 | const { container } = render(tag-content);
8 |
9 | expect(container).toHaveTextContent("tag-content");
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 |
4 | # testing
5 | /coverage
6 |
7 | # misc
8 | .DS_Store
9 | .env.local
10 | .env.development.local
11 | .env.test.local
12 | .env.production.local
13 |
14 | npm-debug.log*
15 | yarn-debug.log*
16 | yarn-error.log*
17 |
18 | # Development folders and files
19 | public
20 | src
21 | scripts
22 | config
23 | .travis.yml
24 | CHANGELOG.md
25 | README.md
26 |
--------------------------------------------------------------------------------
/src/ts/components/alert/__tests__/Alert.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 | import Alert from "../Alert";
4 |
5 | describe("Alert", () => {
6 | test("renders its message", () => {
7 | const { container } = render();
8 |
9 | expect(container).toHaveTextContent("alert-message");
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/ts/components/divider/__tests__/Divider.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 | import Divider from "../Divider";
4 |
5 | describe("Divider", () => {
6 | test("renders its content", () => {
7 | const { container } = render(divider-content);
8 |
9 | expect(container).toHaveTextContent("divider-content");
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: "@typescript-eslint/parser",
4 | plugins: ["@typescript-eslint", "jest"],
5 | ignorePatterns: ["webpack/*", ".eslintrc.js"],
6 | extends: [
7 | "eslint:recommended",
8 | "plugin:@typescript-eslint/recommended",
9 | "plugin:react/recommended",
10 | "plugin:react-hooks/recommended",
11 | "prettier",
12 | ],
13 | };
14 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dash Ant Design Components Reference
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # Unix-style newlines with a newline ending every file
4 | [*]
5 | end_of_line = lf
6 | insert_final_newline = true
7 |
8 | # Matches multiple files with brace expansion notation
9 | # Set default charset
10 | [*.{js,py,ts,tsx}]
11 | charset = utf-8
12 | indent_style = space
13 | indent_size = 4
14 |
15 | # Matches the exact files either package.json or .travis.yml
16 | [{package.json,.circleci/config.yml}]
17 | indent_style = space
18 | indent_size = 2
19 |
--------------------------------------------------------------------------------
/.vscode/cspell.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2",
3 | "ignorePaths": [],
4 | "dictionaryDefinitions": [],
5 | "dictionaries": [],
6 | "words": [],
7 | "ignoreWords": [
8 | "Plotly",
9 | "Sider",
10 | "antd",
11 | "basepath",
12 | "codecov",
13 | "dadc",
14 | "dashprivate",
15 | "docgen",
16 | "hoverable",
17 | "isnumeric",
18 | "isort",
19 | "justfile",
20 | "packagejson",
21 | "pageheader",
22 | "venv",
23 | "virtualenvs"
24 | ],
25 | "import": []
26 | }
27 |
--------------------------------------------------------------------------------
/example/__init__.py:
--------------------------------------------------------------------------------
1 | from dash import Dash
2 |
3 | import dash_antd as ant
4 |
5 | from .pages import controls_page, graph_page
6 |
7 | app = Dash(__name__, suppress_callback_exceptions=True)
8 |
9 | app.layout = ant.ConfigProvider(
10 | id="app-config",
11 | use_dark_theme=True,
12 | # use_compact=True,
13 | token={"colorPrimary": "green"},
14 | children=ant.PagesWithSidebar(
15 | sidebar_width=200,
16 | menu_theme="light",
17 | children=[controls_page, graph_page],
18 | selected_key="controls",
19 | ),
20 | )
21 |
--------------------------------------------------------------------------------
/src/ts/components/select/__tests__/Select.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 | import Select from "../Select";
4 |
5 | describe("Select", () => {
6 | test("renders its selected option", () => {
7 | const { container } = render(
8 |
15 | );
16 |
17 | expect(container).toHaveTextContent("opt1");
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/ts/components/icon/Icon.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import * as icons from "@ant-design/icons";
3 | import { DashComponentProps, StyledComponentProps } from "../../types";
4 | import { omit } from "ramda";
5 |
6 | type Props = {
7 | /**
8 | * Name for the icon https://ant.design/components/icon/
9 | */
10 | icon_name: string;
11 | } & StyledComponentProps &
12 | DashComponentProps;
13 | /**
14 | * Icon
15 | */
16 | const Icon = (props: Props) => {
17 | const { class_name, icon_name, ...otherProps } = props;
18 | const SelectedIcon = icons[icon_name];
19 | return (
20 |
24 | );
25 | };
26 |
27 | Icon.defaultProps = {};
28 |
29 | export default Icon;
30 |
--------------------------------------------------------------------------------
/src/ts/components/layout/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Layout } from "antd";
4 | import { omit } from "ramda";
5 |
6 | const { Footer: AntFooter } = Layout;
7 |
8 | type Props = {
9 | /**
10 | * The children of this component.
11 | */
12 | children?: ReactNode;
13 | } & DashComponentProps &
14 | StyledComponentProps;
15 |
16 | /**
17 | * Handling the overall layout of a page.
18 | */
19 | const Footer = (props: Props) => {
20 | const { children, class_name, ...otherProps } = props;
21 | return (
22 |
23 | {children}
24 |
25 | );
26 | };
27 |
28 | Footer.defaultProps = {};
29 |
30 | export default Footer;
31 |
--------------------------------------------------------------------------------
/src/ts/components/layout/Header.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Layout } from "antd";
4 | import { omit } from "ramda";
5 |
6 | const { Header: AntHeader } = Layout;
7 |
8 | type Props = {
9 | /**
10 | * The children of this component.
11 | */
12 | children?: ReactNode;
13 | } & DashComponentProps &
14 | StyledComponentProps;
15 |
16 | /**
17 | * Handling the overall layout of a page.
18 | */
19 | const Header = (props: Props) => {
20 | const { children, class_name, ...otherProps } = props;
21 | return (
22 |
23 | {children}
24 |
25 | );
26 | };
27 |
28 | Header.defaultProps = {};
29 |
30 | export default Header;
31 |
--------------------------------------------------------------------------------
/example/metadata.py:
--------------------------------------------------------------------------------
1 | import collections
2 | import importlib.util
3 | import json
4 | from functools import lru_cache
5 | from pathlib import Path
6 |
7 |
8 | def get_component_metadata(component_path):
9 | metadata = _load_metadata()
10 | return metadata.get(component_path)
11 |
12 |
13 | @lru_cache(maxsize=1)
14 | def _load_metadata():
15 | module_path = importlib.util.find_spec("dash_antd")
16 | if module_path is None:
17 | raise ModuleNotFoundError("Module `dah_antd` not found.")
18 | return _get_metadata(Path(module_path.origin).parent / "metadata.json") # type: ignore
19 |
20 |
21 | def _get_metadata(metadata_path: Path):
22 | with metadata_path.open() as data_file:
23 | json_string = data_file.read()
24 | data = json.JSONDecoder(object_pairs_hook=collections.OrderedDict).decode(json_string)
25 | return data
26 |
--------------------------------------------------------------------------------
/src/ts/components/layout/Content.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Layout } from "antd";
4 | import { omit } from "ramda";
5 |
6 | const { Content: AntContent } = Layout;
7 |
8 | type Props = {
9 | /**
10 | * The children of this component.
11 | */
12 | children?: ReactNode;
13 | } & DashComponentProps &
14 | StyledComponentProps;
15 |
16 | /**
17 | * Handling the overall layout of a page.
18 | */
19 | const Content = (props: Props) => {
20 | const { children, class_name, ...otherProps } = props;
21 | return (
22 |
23 | {children}
24 |
25 | );
26 | };
27 |
28 | Content.defaultProps = {};
29 |
30 | export default Content;
31 |
--------------------------------------------------------------------------------
/example/pages/graph.py:
--------------------------------------------------------------------------------
1 | import plotly.express as px
2 | from dash import Input, Output, callback, dcc
3 |
4 | import dash_antd as ant
5 | from dash_antd.ext import parse_tokens
6 |
7 | df = px.data.iris()
8 |
9 |
10 | graph_page = ant.Page(
11 | page_key="graph",
12 | children=[ts_plot := dcc.Graph(responsive=True, style={"height": "100%"})],
13 | )
14 |
15 |
16 | @callback(Output(ts_plot, "figure"), Input("app-config", "use_dark_theme"), Input("app-config", "active_tokens"))
17 | def plot(use_dark_theme: bool, active_tokens):
18 | colors = parse_tokens(active_tokens)
19 | fig = px.scatter(
20 | df,
21 | x="sepal_width",
22 | y="sepal_length",
23 | color_discrete_sequence=[colors.colorPrimary],
24 | template="plotly_dark" if use_dark_theme else "plotly_white",
25 | )
26 | fig.update_layout({"autosize": True})
27 | return fig
28 |
--------------------------------------------------------------------------------
/webpack/moduleDefinition.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var directories = require("./directories");
4 |
5 | module.exports = {
6 | noParse: /node_modules\/json-schema\/lib\/validate\.js/, // used to get `request` to work: https://github.com/request/request/issues/1920#issuecomment-171246043
7 | rules: [
8 | {
9 | test: /\.json$/,
10 | loader: "json-loader",
11 | },
12 | {
13 | test: /\.jsx?$/,
14 | include: [directories.SRC, directories.DEMO],
15 | loader: "babel-loader",
16 | },
17 | {
18 | test: /\.tsx?$/,
19 | use: "ts-loader",
20 | include: [directories.SRC, directories.DEMO],
21 | exclude: /node_modules/,
22 | },
23 | {
24 | test: /\.css$/i,
25 | use: ["style-loader", "css-loader"],
26 | },
27 | ],
28 | };
29 |
--------------------------------------------------------------------------------
/src/ts/components/timeline/TimelineItem.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Timeline } from "antd";
4 | import { omit } from "ramda";
5 |
6 | const { Item } = Timeline;
7 |
8 | type Props = {
9 | /**
10 | * Set the circle's color to blue, red, green, gray or other custom colors string
11 | */
12 | color?: string;
13 | /**
14 | * Set the label
15 | */
16 | label?: string;
17 | /**
18 | * Customize node position
19 | */
20 | position?: "left" | "right";
21 | } & DashComponentProps &
22 | StyledComponentProps;
23 |
24 | /**
25 | * An item in the timeline
26 | */
27 | const TimelineItem = (props: Props) => {
28 | const { class_name, ...otherProps } = props;
29 |
30 | return ;
31 | };
32 |
33 | export default TimelineItem;
34 |
--------------------------------------------------------------------------------
/src/ts/components/radio/__tests__/Radio.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 | import userEvent from "@testing-library/user-event";
4 | import Radio from "../Radio";
5 |
6 | describe("Radio", () => {
7 | test("renders its content", () => {
8 | const { container } = render(Some Radio content);
9 |
10 | expect(container).toHaveTextContent("Some Radio content");
11 | });
12 |
13 | test("updates checked prop when clicked", async () => {
14 | const user = userEvent.setup();
15 | const mockSetProps = jest.fn();
16 |
17 | const checkbox = render(
18 | Clickable
19 | );
20 |
21 | expect(mockSetProps.mock.calls).toHaveLength(0);
22 |
23 | await user.click(checkbox.queryByText("Clickable"));
24 |
25 | expect(mockSetProps.mock.calls).toHaveLength(1);
26 | expect(mockSetProps.mock.calls[0][0].checked).toBe(true);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/ts/components/tabs/TabPane.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, Fragment } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 |
4 | export type Props = {
5 | /**
6 | * The children of this component.
7 | */
8 | children?: ReactNode;
9 | /**
10 | * Customize close icon in TabPane's head. Only works while type="editable-card"
11 | */
12 | close_icon?: string;
13 | /**
14 | * Forced render of content in tabs, not lazy render after clicking on tabs
15 | */
16 | force_render?: boolean;
17 | /**
18 | * TabPane's key
19 | */
20 | tab_key: string;
21 | /**
22 | * Show text in TabPane's head
23 | */
24 | label: string;
25 | } & DashComponentProps &
26 | StyledComponentProps;
27 |
28 | /**
29 | * TabPane
30 | */
31 | const TabPane = (props: Props) => {
32 | const { children } = props;
33 | return {children};
34 | };
35 |
36 | TabPane.defaultProps = {};
37 |
38 | export default TabPane;
39 |
--------------------------------------------------------------------------------
/src/ts/components/checkbox/__tests__/Checkbox.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 | import userEvent from "@testing-library/user-event";
4 | import Checkbox from "../Checkbox";
5 |
6 | describe("Checkbox", () => {
7 | test("renders its content", () => {
8 | const { container } = render(
9 | Some Checkbox content
10 | );
11 |
12 | expect(container).toHaveTextContent("Some Checkbox content");
13 | });
14 |
15 | test("updates checked prop when clicked", async () => {
16 | const user = userEvent.setup();
17 | const mockSetProps = jest.fn();
18 |
19 | const checkbox = render(
20 | Clickable
21 | );
22 |
23 | expect(mockSetProps.mock.calls).toHaveLength(0);
24 |
25 | await user.click(checkbox.queryByText("Clickable"));
26 |
27 | expect(mockSetProps.mock.calls).toHaveLength(1);
28 | expect(mockSetProps.mock.calls[0][0].checked).toBe(true);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/src/ts/components/tag/__tests__/CheckableTag.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 | import userEvent from "@testing-library/user-event";
4 | import CheckableTag from "../CheckableTag";
5 |
6 | describe("CheckableTag", () => {
7 | test("renders its content", () => {
8 | const { container } = render(
9 | Some Tag content
10 | );
11 |
12 | expect(container).toHaveTextContent("Some Tag content");
13 | });
14 |
15 | test("updates checked prop when clicked", async () => {
16 | const user = userEvent.setup();
17 | const mockSetProps = jest.fn();
18 |
19 | const checkableTag = render(
20 | Clickable
21 | );
22 |
23 | expect(mockSetProps.mock.calls).toHaveLength(0);
24 |
25 | await user.click(checkableTag.queryByText("Clickable"));
26 |
27 | expect(mockSetProps.mock.calls).toHaveLength(1);
28 | expect(mockSetProps.mock.calls[0][0].checked).toBe(true);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/src/ts/components/radio/__tests__/RadioButton.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 | import userEvent from "@testing-library/user-event";
4 | import RadioButton from "../RadioButton";
5 |
6 | describe("RadioButton", () => {
7 | test("renders its content", () => {
8 | const { container } = render(
9 | Some RadioButton content
10 | );
11 |
12 | expect(container).toHaveTextContent("Some RadioButton content");
13 | });
14 |
15 | test("updates checked prop when clicked", async () => {
16 | const user = userEvent.setup();
17 | const mockSetProps = jest.fn();
18 |
19 | const checkbox = render(
20 | Clickable
21 | );
22 |
23 | expect(mockSetProps.mock.calls).toHaveLength(0);
24 |
25 | await user.click(checkbox.queryByText("Clickable"));
26 |
27 | expect(mockSetProps.mock.calls).toHaveLength(1);
28 | expect(mockSetProps.mock.calls[0][0].checked).toBe(true);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/src/ts/components/layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Layout as AntLayout } from "antd";
4 | import { omit } from "ramda";
5 |
6 | type Props = {
7 | /**
8 | * The children of this component.
9 | */
10 | children?: ReactNode;
11 | /**
12 | * Whether a Sidebar is contained in children. Does not necessarily have to be specified,
13 | * but useful in ssr avoid style flickering.
14 | */
15 | has_sidebar?: boolean;
16 | } & DashComponentProps &
17 | StyledComponentProps;
18 |
19 | /**
20 | * Handling the overall layout of a page.
21 | */
22 | const Layout = (props: Props) => {
23 | const { children, class_name, has_sidebar, ...otherProps } = props;
24 | return (
25 |
30 | {children}
31 |
32 | );
33 | };
34 |
35 | Layout.defaultProps = {};
36 |
37 | export default Layout;
38 |
--------------------------------------------------------------------------------
/src/ts/components/checkbox/__tests__/CheckboxGroup.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 | import userEvent from "@testing-library/user-event";
4 | import CheckboxGroup from "../CheckboxGroup";
5 |
6 | describe("CheckboxGroup", () => {
7 | test("renders its content", () => {
8 | const { container } = render(
9 | Some CheckboxGroup content
10 | );
11 |
12 | expect(container).toHaveTextContent("Some CheckboxGroup content");
13 | });
14 |
15 | test("updates checked prop when clicked", async () => {
16 | const user = userEvent.setup();
17 | const mockSetProps = jest.fn();
18 |
19 | const checkbox = render(
20 |
21 | );
22 |
23 | expect(mockSetProps.mock.calls).toHaveLength(0);
24 |
25 | await user.click(checkbox.queryByText("foo"));
26 |
27 | expect(mockSetProps.mock.calls).toHaveLength(1);
28 | expect(mockSetProps.mock.calls[0][0].value).toStrictEqual(["foo"]);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022 Robert Pack
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/ts/components/timeline/Timeline.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Timeline as AntTimeline } from "antd";
4 | import { omit } from "ramda";
5 |
6 | type Props = {
7 | /**
8 | * The children of this component.
9 | */
10 | children?: ReactNode;
11 | /**
12 | * By sending alternate the timeline will distribute the nodes to the left and right
13 | */
14 | mode?: "left" | "alternate" | "right";
15 | /**
16 | * Set the last ghost node's existence or its content
17 | */
18 | pending?: boolean;
19 | /**
20 | * Whether reverse nodes or not
21 | */
22 | reverse?: boolean;
23 | } & DashComponentProps &
24 | StyledComponentProps;
25 |
26 | /**
27 | * Timeline component
28 | */
29 | const TimelineItem = (props: Props) => {
30 | const { class_name, children, ...otherProps } = props;
31 |
32 | return (
33 |
34 | {children}
35 |
36 | );
37 | };
38 |
39 | export default TimelineItem;
40 |
--------------------------------------------------------------------------------
/src/ts/components/tag/CheckableTag.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Tag } from "antd";
4 | import { CheckableTagProps } from "antd/lib/tag";
5 |
6 | const { CheckableTag: AntCheckableTag } = Tag;
7 |
8 | type Props = {
9 | /**
10 | * The children of this component.
11 | */
12 | children?: ReactNode;
13 | /**
14 | * Checked status of Tag
15 | */
16 | checked: boolean;
17 | } & DashComponentProps &
18 | StyledComponentProps;
19 |
20 | /**
21 | * CheckableTag works like a Checkbox, click it to toggle checked state.
22 | */
23 | const CheckableTag = (props: Props) => {
24 | const { children, class_name, checked, setProps, ...otherProps } = props;
25 |
26 | const onClick: CheckableTagProps["onClick"] = () =>
27 | setProps({ checked: !checked });
28 |
29 | return (
30 |
36 | {children}
37 |
38 | );
39 | };
40 |
41 | CheckableTag.defaultProps = {
42 | checked: false,
43 | };
44 |
45 | export default CheckableTag;
46 |
--------------------------------------------------------------------------------
/src/ts/components/checkbox/CheckboxGroup.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Checkbox } from "antd";
4 |
5 | const { Group } = Checkbox;
6 |
7 | type Option = {
8 | label: string;
9 | value: string;
10 | disabled?: boolean;
11 | style?: object;
12 | };
13 |
14 | type Props = {
15 | /**
16 | * Currently selected values
17 | */
18 | value?: string[];
19 | /**
20 | * All options within the CheckboxGroup
21 | */
22 | options: string[] | number[] | Option[];
23 | /**
24 | * Disables all checkboxes within the group
25 | */
26 | disabled?: boolean;
27 | } & DashComponentProps &
28 | StyledComponentProps;
29 |
30 | /**
31 | * A collection of Checkboxes.
32 | */
33 | const CheckboxGroup = (props: Props) => {
34 | const { class_name, setProps, ...otherProps } = props;
35 |
36 | const onChange = useCallback(
37 | (checkedValues) => setProps({ value: checkedValues }),
38 | [setProps]
39 | );
40 | return ;
41 | };
42 |
43 | CheckboxGroup.defaultProps = {
44 | options: [],
45 | value: [],
46 | };
47 |
48 | export default CheckboxGroup;
49 |
--------------------------------------------------------------------------------
/src/ts/components/tag/Tag.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Tag as AntTag } from "antd";
4 | import { omit } from "ramda";
5 |
6 | type Props = {
7 | /**
8 | * The children of this component.
9 | */
10 | children?: ReactNode;
11 | /**
12 | * Whether the Tag can be closed
13 | */
14 | closable?: boolean;
15 | /**
16 | * Custom close icon
17 | */
18 | close_icon?: ReactNode;
19 | /**
20 | * Color of the Tag
21 | */
22 | color?: string;
23 | /**
24 | * Set the icon of tag
25 | */
26 | icon?: ReactNode;
27 | /**
28 | * Whether the Tag is closed or not
29 | */
30 | visible?: boolean;
31 | } & DashComponentProps &
32 | StyledComponentProps;
33 |
34 | /**
35 | * Tag for categorizing or markup.
36 | */
37 | const Tag = (props: Props) => {
38 | const { children, class_name, close_icon, ...otherProps } = props;
39 | return (
40 |
45 | {children}
46 |
47 | );
48 | };
49 |
50 | Tag.defaultProps = {};
51 |
52 | export default Tag;
53 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | fail_fast: false
2 | default_stages: [commit, push]
3 | repos:
4 | - repo: https://github.com/pre-commit/pre-commit-hooks
5 | rev: v4.4.0
6 | hooks:
7 | - id: check-case-conflict
8 | - id: check-merge-conflict
9 | - id: end-of-file-fixer
10 | - id: mixed-line-ending
11 | - id: trailing-whitespace
12 | args: [--markdown-linebreak-ext=md]
13 |
14 | - repo: https://github.com/commitizen-tools/commitizen
15 | rev: v2.37.1
16 | hooks:
17 | - id: commitizen
18 | stages: [commit-msg]
19 |
20 | - repo: https://github.com/psf/black
21 | rev: 22.10.0
22 | hooks:
23 | - id: black
24 | args: [--config, pyproject.toml]
25 |
26 | - repo: https://github.com/asottile/pyupgrade
27 | rev: v3.2.3
28 | hooks:
29 | - id: pyupgrade
30 | args: [--py39-plus]
31 |
32 | - repo: https://github.com/charliermarsh/ruff-pre-commit
33 | rev: v0.0.150
34 | hooks:
35 | - id: ruff
36 | args: [--config, pyproject.toml, --fix]
37 |
38 | - repo: https://github.com/Yelp/detect-secrets
39 | rev: v1.4.0
40 | hooks:
41 | - id: detect-secrets
42 | args: ["--baseline", ".secrets.baseline", "force-use-all-plugins"]
43 | exclude: .*\.lock*|.*\.md|.*\.ipynb|.secrets.baseline|.*\.svg|.*\.pkl|.*\.drawio
44 |
--------------------------------------------------------------------------------
/src/ts/components/Space.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../types";
3 | import { Space as AntSpace } from "antd";
4 | import { omit } from "ramda";
5 |
6 | type Size = "small" | "middle" | "large" | number;
7 |
8 | type Props = {
9 | /**
10 | * The children of this component.
11 | */
12 | children?: ReactNode;
13 | /**
14 | * Align items
15 | */
16 | align?: "start" | "end" | "center" | "baseline";
17 | /**
18 | * The space direction
19 | */
20 | direction?: "vertical" | "horizontal";
21 | /**
22 | * The space size
23 | */
24 | size?: Size | [Size, Size];
25 | /**
26 | * Set split
27 | */
28 | split?: ReactNode;
29 | /**
30 | * Auto wrap line, when horizontal effective
31 | */
32 | wrap?: boolean;
33 | } & DashComponentProps &
34 | StyledComponentProps;
35 |
36 | /**
37 | * Set components spacing.
38 | */
39 | const Space = (props: Props) => {
40 | const { children, class_name, ...otherProps } = props;
41 |
42 | return (
43 |
44 | {children}
45 |
46 | );
47 | };
48 |
49 | Space.defaultProps = {
50 | checked: false,
51 | };
52 |
53 | export default Space;
54 |
--------------------------------------------------------------------------------
/src/ts/components/radio/Radio.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useCallback } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Radio as AntRadio, RadioProps } from "antd";
4 |
5 | type Props = {
6 | /**
7 | * The children of this component.
8 | */
9 | children?: ReactNode;
10 | /**
11 | * Specifies whether the radio is selected
12 | */
13 | checked?: boolean;
14 | /**
15 | * Disable radio
16 | */
17 | disabled?: boolean;
18 | } & DashComponentProps &
19 | StyledComponentProps;
20 |
21 | /**
22 | * Radio
23 | */
24 | const Radio = (props: Props) => {
25 | const { children, checked, disabled, class_name, setProps, ...otherProps } =
26 | props;
27 |
28 | const onClick: RadioProps["onClick"] = useCallback(() => {
29 | if (!disabled && setProps) {
30 | setProps({ checked: !checked });
31 | }
32 | }, [setProps, checked, disabled]);
33 |
34 | return (
35 |
42 | {children}
43 |
44 | );
45 | };
46 |
47 | Radio.defaultProps = { checked: false };
48 |
49 | export default Radio;
50 |
--------------------------------------------------------------------------------
/src/ts/components/menu/MenuItem.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Menu } from "antd";
4 | import { omit } from "ramda";
5 |
6 | const { Item } = Menu;
7 |
8 | type Props = {
9 | /**
10 | * The children of this component.
11 | */
12 | children?: ReactNode;
13 | /**
14 | * Display the danger style
15 | */
16 | danger?: boolean;
17 | /**
18 | * Whether menu item is disabled
19 | */
20 | disabled?: boolean;
21 | /**
22 | * The icon of the menu item
23 | */
24 | icon?: ReactNode;
25 | /**
26 | * Unique ID of the menu item
27 | */
28 | key: string;
29 | /**
30 | * Menu label
31 | */
32 | label: ReactNode;
33 | /**
34 | * Set display title for collapsed item
35 | */
36 | title?: string;
37 | } & DashComponentProps &
38 | StyledComponentProps;
39 |
40 | /**
41 | * MenuItem to be used as child elements to the "Menu" component.
42 | * If used, "items" property on menu must be left empty.
43 | */
44 | const MenuItem = (props: Props) => {
45 | const { children, class_name, ...otherProps } = props;
46 | return (
47 | -
48 | {children}
49 |
50 | );
51 | };
52 |
53 | export default MenuItem;
54 |
--------------------------------------------------------------------------------
/src/ts/components/radio/RadioButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useCallback } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Radio, RadioProps } from "antd";
4 |
5 | const { Button } = Radio;
6 |
7 | type Props = {
8 | /**
9 | * The children of this component.
10 | */
11 | children?: ReactNode;
12 | /**
13 | * Specifies whether the radio is selected
14 | */
15 | checked?: boolean;
16 | /**
17 | * Disable radio
18 | */
19 | disabled?: boolean;
20 | } & DashComponentProps &
21 | StyledComponentProps;
22 | /**
23 | * RadioButton
24 | */
25 | const RadioButton = (props: Props) => {
26 | const { children, checked, disabled, class_name, setProps, ...otherProps } =
27 | props;
28 |
29 | const onClick: RadioProps["onClick"] = useCallback(() => {
30 | if (!disabled && setProps) {
31 | setProps({ checked: !checked });
32 | }
33 | }, [setProps, checked, disabled]);
34 |
35 | return (
36 |
45 | );
46 | };
47 |
48 | RadioButton.defaultProps = { checked: false };
49 |
50 | export default RadioButton;
51 |
--------------------------------------------------------------------------------
/justfile:
--------------------------------------------------------------------------------
1 | set dotenv-load := false
2 |
3 | _default:
4 | @just --list
5 |
6 | # Generate components and build the bundle
7 | build:
8 | poetry run yarn build
9 | poetry run black .
10 | poetry run ruff --fix .
11 |
12 | # Build the webpack bundle
13 | build-js:
14 | yarn build:js
15 |
16 | # Generate the components
17 | generate:
18 | yarn build:backends
19 |
20 | # Rebuild the bundle on change
21 | watch:
22 | yarn watch
23 |
24 | # Install pip requirements & node modules.
25 | install:
26 | poetry install
27 | yarn install
28 | poetry run pre-commit install --hook-type commit-msg --hook-type pre-commit
29 |
30 | # run linters on codebase
31 | lint:
32 | yarn lint
33 | poetry run black --check .
34 | poetry run ruff .
35 |
36 | fix:
37 | poetry run ruff --fix .
38 |
39 | # Run demo server
40 | demo:
41 | yarn demo
42 |
43 | # run the example dash app
44 | run:
45 | python -m example
46 |
47 | # forat all source files
48 | format:
49 | yarn format
50 | poetry run black .
51 | poetry run ruff --fix .
52 |
53 | # Run component tests
54 | test:
55 | yarn test
56 |
57 | # Remove dist & build directories
58 | clean:
59 | rm -rf dist
60 | rm -rf build
61 |
62 | # Package the application for distribution using python wheel.
63 | package: clean build
64 | poetry build
65 |
66 | # Publish the package to pypi using twine.
67 | publish: package
68 | yarn publish
69 | twine upload dist/*
70 |
--------------------------------------------------------------------------------
/src/ts/components/templates/Page.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useContext, useEffect } from "react";
2 | import {
3 | DashComponentProps,
4 | DashLoadingState,
5 | StyledComponentProps,
6 | } from "../../types";
7 | import { PagesContext } from "./PagesWithSidebar";
8 |
9 | export type Props = {
10 | /**
11 | * child components
12 | */
13 | children?: ReactNode;
14 | /**
15 | * child components
16 | */
17 | controls?: ReactNode;
18 | /**
19 | * Show clear button
20 | */
21 | label?: string;
22 | /**
23 | * Show clear button
24 | */
25 | page_key?: string;
26 | /**
27 | * Icon for display in page navigation
28 | */
29 | icon?: ReactNode;
30 | /**
31 | * Object that holds the loading state object coming from dash-renderer
32 | */
33 | loading_state?: DashLoadingState;
34 | } & DashComponentProps &
35 | StyledComponentProps;
36 |
37 | /**
38 | * A page within a multi page layout
39 | */
40 | const Page = (props: Props) => {
41 | const { children, controls, page_key } = props;
42 | const { selectedKey, cbControls } = useContext(PagesContext);
43 |
44 | useEffect(() => {
45 | if (selectedKey === page_key) cbControls(controls);
46 | }, [selectedKey, cbControls, controls, page_key]);
47 |
48 | if (selectedKey !== page_key) return null;
49 | return <>{children}>;
50 | };
51 |
52 | Page.defaultProps = {};
53 |
54 | export default Page;
55 |
--------------------------------------------------------------------------------
/src/ts/components/layout/Row.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Row as AntRow } from "antd";
4 | import { omit } from "ramda";
5 |
6 | export type Props = {
7 | /**
8 | * The children of this component.
9 | */
10 | children?: ReactNode;
11 | /**
12 | * Vertical alignment
13 | */
14 | align: "top" | "middle" | "bottom";
15 | /**
16 | * Spacing between grids, could be a number or a object like { xs: 8, sm: 16, md: 24}
17 | * Or you can use array to make horizontal and vertical spacing work at the same time [horizontal, vertical]
18 | */
19 | gutter: number | object | [number, number] | [object, object];
20 | /**
21 | * Horizontal arrangement
22 | */
23 | justify:
24 | | "start"
25 | | "end"
26 | | "center"
27 | | "space-around"
28 | | "space-between"
29 | | "space-evenly";
30 | /**
31 | * Auto wrap line
32 | */
33 | wrap: boolean;
34 | } & DashComponentProps &
35 | StyledComponentProps;
36 |
37 | /**
38 | * Row
39 | */
40 | const Row = (props: Props) => {
41 | const { children, class_name, ...otherProps } = props;
42 | return (
43 |
44 | {children}
45 |
46 | );
47 | };
48 |
49 | Row.defaultProps = {
50 | align: "top",
51 | gutter: 0,
52 | justify: "start",
53 | wrap: true,
54 | };
55 |
56 | export default Row;
57 |
--------------------------------------------------------------------------------
/src/ts/components/button/__tests__/Button.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 | import userEvent from "@testing-library/user-event";
4 | import Button from "../Button";
5 |
6 | describe("Button", () => {
7 | test("renders its content", () => {
8 | const { container } = render();
9 |
10 | expect(container).toHaveTextContent("Some button content");
11 | });
12 |
13 | test("tracks clicks with n_clicks", async () => {
14 | const user = userEvent.setup();
15 | const mockSetProps = jest.fn();
16 |
17 | const { container } = render(
18 |
19 | );
20 |
21 | expect(mockSetProps.mock.calls).toHaveLength(0);
22 |
23 | await user.click(container.querySelector(".ant-btn"));
24 |
25 | expect(mockSetProps.mock.calls).toHaveLength(1);
26 | expect(mockSetProps.mock.calls[0][0].n_clicks).toBe(1);
27 | });
28 |
29 | test("doesn't track clicks if disabled", async () => {
30 | const user = userEvent.setup();
31 | const mockSetProps = jest.fn();
32 | const { container } = render(
33 |
36 | );
37 |
38 | expect(mockSetProps.mock.calls).toHaveLength(0);
39 |
40 | await user.click(container.querySelector(".ant-btn"));
41 |
42 | expect(mockSetProps.mock.calls).toHaveLength(0);
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/src/ts/components/divider/Divider.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Divider as AntDivider } from "antd";
4 | import { omit } from "ramda";
5 |
6 | type Props = {
7 | /**
8 | * The children of this component.
9 | */
10 | children?: ReactNode;
11 | /**
12 | * Whether line is dashed
13 | */
14 | dashed?: boolean;
15 | /**
16 | * The position of title inside divider
17 | */
18 | orientation?: "left" | "right" | "center";
19 | /**
20 | * The margin-left/right between the title and its closest border,
21 | * while the orientation must be left or right
22 | */
23 | orientation_margin?: string | number;
24 | /**
25 | * Divider text show as plain style
26 | */
27 | plain?: boolean;
28 | /**
29 | * The direction type of divider
30 | */
31 | type?: "horizontal" | "vertical";
32 | } & DashComponentProps &
33 | StyledComponentProps;
34 |
35 | /**
36 | * Divides content with a simple line and optional text included.
37 | */
38 | const Divider = (props: Props) => {
39 | const { class_name, orientation_margin, children, ...otherProps } = props;
40 | return (
41 |
46 | {children}
47 |
48 | );
49 | };
50 |
51 | Divider.defaultProps = {};
52 |
53 | export default Divider;
54 |
--------------------------------------------------------------------------------
/webpack/config.demo.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var path = require("path");
4 | var webpack = require("webpack");
5 | var moduleDefinition = require("./moduleDefinition");
6 | var directories = require("./directories");
7 | const packagejson = require("../package.json");
8 |
9 | const dashLibraryName = packagejson.name.replace(/-/g, "_");
10 | var publicHost = process.env.DEMO_PUBLIC_HOST || undefined;
11 | var buildPath = path.join(directories.ROOT, "demo-lib");
12 |
13 | module.exports = function (_env, argv) {
14 | const mode = (argv && argv.mode) || "development";
15 | const entry = { bundle: [path.join(directories.ROOT, "demo/index.tsx")] };
16 |
17 | /* eslint-disable no-console */
18 | console.log("Current environment: " + mode);
19 | console.log("Using public host: " + publicHost || "");
20 | /* eslint-enable no-console */
21 |
22 | return {
23 | cache: false,
24 | // context: directories.SRC,
25 | mode,
26 | entry,
27 | module: moduleDefinition,
28 | devServer: {
29 | static: "demo",
30 | compress: true,
31 | port: 9000,
32 | },
33 | resolve: {
34 | extensions: [".ts", ".tsx", ".js", ".json"],
35 | },
36 | output: {
37 | library: {
38 | name: dashLibraryName,
39 | type: "this",
40 | },
41 | path: buildPath,
42 | pathinfo: true,
43 | publicPath: "/demo-lib/", // For loading from webpack dev server
44 | filename: "[name].js",
45 | },
46 | };
47 | };
48 |
--------------------------------------------------------------------------------
/src/ts/components/Switch.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from "react";
2 | import {
3 | DashComponentProps,
4 | StyledComponentProps,
5 | DashLoadingState,
6 | } from "../types";
7 | import { Switch as AntSwitch, SwitchProps } from "antd";
8 |
9 | type Props = {
10 | /**
11 | * Specifies whether the radio is selected
12 | */
13 | checked?: boolean;
14 | /**
15 | * Disable radio
16 | */
17 | disabled?: boolean;
18 | /**
19 | * The size of the Switch
20 | */
21 | size?: "default" | "small";
22 | /**
23 | * Object that holds the loading state object coming from dash-renderer
24 | */
25 | loading_state?: DashLoadingState;
26 | } & DashComponentProps &
27 | StyledComponentProps;
28 |
29 | /**
30 | * Switching Selector.
31 | */
32 | const Switch = (props: Props) => {
33 | const { class_name, loading_state, disabled, setProps, ...otherProps } =
34 | props;
35 |
36 | const onChange: SwitchProps["onChange"] = useCallback(
37 | (checked: boolean) => {
38 | if (!disabled && setProps) {
39 | setProps({ checked });
40 | }
41 | },
42 | [setProps, disabled]
43 | );
44 |
45 | return (
46 |
56 | );
57 | };
58 |
59 | export default Switch;
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Dash Ant Design Components
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Ant Design components for Plotly Dash.
14 |
15 | ## Install
16 |
17 | to install in your python environment.
18 |
19 | ```shell
20 | poetry add dash-antd
21 | ```
22 |
23 | or via pip.
24 |
25 | ```shell
26 | pip install dash-antd
27 | ```
28 |
29 | ## Development
30 |
31 | We use [just](https://github.com/casey/just) as command runner, if you prefer not to install
32 | just, have a look at the `justfile` for detailed commands.
33 |
34 | To manage python dependencies, we utilize [poetry](python-poetry.org/).
35 |
36 | ### Getting Started
37 |
38 | Install python and node dependencies.
39 |
40 | ```sh
41 | just install
42 | ```
43 |
44 | Build the Dash packages.
45 |
46 | ```sh
47 | just build
48 | ```
49 |
50 | See all commands with `just -l`
51 |
52 | ### Example app
53 |
54 | An example app is contained in the `example` folder. To run it execute:
55 |
56 | ```sh
57 | just run
58 | ```
59 |
--------------------------------------------------------------------------------
/src/ts/components/Segmented.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../types";
3 | import { Segmented as AntSegmented, SegmentedProps } from "antd";
4 |
5 | type Props = {
6 | /**
7 | * Option to fit width to its parent\'s width
8 | */
9 | block?: boolean;
10 | /**
11 | * Disable all segments
12 | */
13 | disabled?: boolean;
14 | /**
15 | * Set children optional
16 | */
17 | options:
18 | | string[]
19 | | number[]
20 | | Array<{
21 | label: string;
22 | value: string;
23 | icon?: string;
24 | disabled?: boolean;
25 | className?: string;
26 | }>;
27 | /**
28 | * The size of the Segmented.
29 | */
30 | size?: "large" | "middle" | "small";
31 | /**
32 | * The input content value
33 | */
34 | value?: string | number;
35 | } & DashComponentProps &
36 | StyledComponentProps;
37 |
38 | /**
39 | * Segmented component
40 | */
41 | const Segmented = (props: Props) => {
42 | const { class_name, disabled, setProps, ...otherProps } = props;
43 |
44 | const onChange: SegmentedProps["onChange"] = useCallback(
45 | (value) => {
46 | if (!disabled && setProps) {
47 | setProps({ value });
48 | }
49 | },
50 | [setProps, disabled]
51 | );
52 |
53 | return (
54 | // @ts-expect-error TODO why is this asking for all props?
55 |
61 | );
62 | };
63 |
64 | export default Segmented;
65 |
--------------------------------------------------------------------------------
/dash_antd/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function as _
2 |
3 | import json
4 | import os as _os
5 | import sys as _sys
6 |
7 | import dash as _dash
8 |
9 | # noinspection PyUnresolvedReferences
10 | from ._imports_ import * # noqa
11 | from ._imports_ import __all__
12 |
13 | if not hasattr(_dash, "__plotly_dash") and not hasattr(_dash, "development"):
14 | print(
15 | "Dash was not successfully imported. "
16 | "Make sure you don't have a file "
17 | 'named \n"dash.py" in your current directory.',
18 | file=_sys.stderr,
19 | )
20 | _sys.exit(1)
21 |
22 | _basepath = _os.path.dirname(__file__)
23 | _filepath = _os.path.abspath(_os.path.join(_basepath, "package.json"))
24 | with open(_filepath) as f:
25 | package = json.load(f)
26 |
27 | package_name = package["name"].replace(" ", "_").replace("-", "_")
28 | __version__ = package["version"]
29 |
30 | _current_path = _os.path.dirname(_os.path.abspath(__file__))
31 |
32 | _this_module = _sys.modules[__name__]
33 |
34 | _js_dist = []
35 |
36 | _js_dist.extend(
37 | [
38 | {
39 | "relative_package_path": "dash_antd.min.js",
40 | "external_url": "https://unpkg.com/{0}@{2}/{1}/{1}.js".format(package_name, __name__, __version__),
41 | "namespace": package_name,
42 | },
43 | {
44 | "relative_package_path": "dash_antd.js.map",
45 | "external_url": "https://unpkg.com/{0}@{2}/{1}/{1}.js.map".format(package_name, __name__, __version__),
46 | "namespace": package_name,
47 | "dynamic": True,
48 | },
49 | ]
50 | )
51 |
52 | _css_dist = []
53 |
54 |
55 | for _component in __all__:
56 | setattr(locals()[_component], "_js_dist", _js_dist)
57 | setattr(locals()[_component], "_css_dist", _css_dist)
58 |
--------------------------------------------------------------------------------
/src/ts/components/checkbox/Checkbox.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useCallback } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Checkbox as AntCheckbox, CheckboxProps } from "antd";
4 |
5 | type Props = {
6 | /**
7 | * The children of this component.
8 | */
9 | children?: ReactNode;
10 | /**
11 | * Whether the checkbox is selected
12 | */
13 | checked?: boolean;
14 | /**
15 | * Whether the checkbox is disabled
16 | */
17 | disabled: boolean;
18 | /**
19 | * Whether the checkbox is indeterminate
20 | */
21 | indeterminate: boolean;
22 | } & DashComponentProps &
23 | StyledComponentProps;
24 |
25 | /**
26 | * Checkbox component.
27 | */
28 | const Checkbox = (props: Props) => {
29 | const {
30 | id,
31 | children,
32 | class_name,
33 | checked,
34 | disabled,
35 | indeterminate,
36 | setProps,
37 | } = props;
38 |
39 | const onChange: CheckboxProps["onChange"] = useCallback(
40 | (e) => {
41 | setProps({
42 | checked: e.target.checked,
43 | indeterminate: typeof e.target.checked === "undefined",
44 | });
45 | },
46 | [setProps]
47 | );
48 |
49 | return (
50 |
58 | {children}
59 |
60 | );
61 | };
62 |
63 | Checkbox.defaultProps = {
64 | checked: false,
65 | disabled: false,
66 | indeterminate: false,
67 | };
68 |
69 | export default Checkbox;
70 |
--------------------------------------------------------------------------------
/src/ts/components/dropdown/DropdownMenu.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, ItemType } from "../../types";
3 | import { Dropdown as AntDropdown, Menu as AntMenu } from "antd";
4 | import { omit } from "ramda";
5 |
6 | type Props = {
7 | /**
8 | * The children of this component.
9 | */
10 | children?: ReactNode;
11 | /**
12 | * Items displayed in the dropdown menu
13 | */
14 | items: ItemType[];
15 | /**
16 | * Whether the dropdown arrow should be visible
17 | */
18 | arrow?: boolean;
19 | /**
20 | * Whether the dropdown menu is disabled
21 | */
22 | disabled?: boolean;
23 | /**
24 | * Placement of popup menu
25 | */
26 | placement?:
27 | | "bottom"
28 | | "bottomLeft"
29 | | "bottomRight"
30 | | "top"
31 | | "topLeft"
32 | | "topRight";
33 | /**
34 | * The trigger mode which executes the dropdown action.
35 | * Note that hover can't be used on touchscreens
36 | */
37 | trigger?: Array<"click" | "hover" | "contextMenu">;
38 | /**
39 | * Whether the dropdown menu is currently visible
40 | */
41 | visible?: boolean;
42 | } & DashComponentProps;
43 |
44 | /**
45 | *
46 | * A Dropdown component
47 | */
48 | const DropdownMenu = (props: Props) => {
49 | const { children, items, ...otherProps } = props;
50 |
51 | const overlay = ;
52 |
53 | return (
54 |
55 | {children}
56 |
57 | );
58 | };
59 |
60 | DropdownMenu.defaultProps = {
61 | shape: "default",
62 | size: "middle",
63 | type: "default",
64 | n_clicks: 0,
65 | };
66 |
67 | export default DropdownMenu;
68 |
--------------------------------------------------------------------------------
/src/ts/components/alert/Alert.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Alert as AntAlert } from "antd";
4 | import { omit } from "ramda";
5 |
6 | type Props = {
7 | /**
8 | * The children of this component.
9 | */
10 | children?: ReactNode;
11 | /**
12 | * Whether to show as banner
13 | */
14 | banner?: boolean;
15 | /**
16 | * Whether Alert can be closed
17 | */
18 | closable?: boolean;
19 | /**
20 | * Close text to show
21 | */
22 | close_text?: string;
23 | /**
24 | * Custom close icon
25 | */
26 | close_icon?: ReactNode;
27 | /**
28 | * Additional content of Alert
29 | */
30 | description?: string;
31 | /**
32 | * Custom icon, effective when showIcon is true
33 | */
34 | icon?: ReactNode;
35 | /**
36 | * Content of Alert
37 | */
38 | message?: string;
39 | /**
40 | * Whether to show icon
41 | */
42 | show_icon?: boolean;
43 | /**
44 | * Type of Alert
45 | */
46 | type?: "success" | "info" | "warning" | "error";
47 | } & DashComponentProps &
48 | StyledComponentProps;
49 |
50 | /**
51 | * Alert component for feedback.
52 | */
53 | const Alert = (props: Props) => {
54 | const {
55 | close_text,
56 | children,
57 | close_icon,
58 | class_name,
59 | show_icon,
60 | ...otherProps
61 | } = props;
62 |
63 | return (
64 |
72 | );
73 | };
74 |
75 | export default Alert;
76 |
--------------------------------------------------------------------------------
/example/pages/02_cards.py:
--------------------------------------------------------------------------------
1 | from dataclasses import fields
2 |
3 | from dash import Input, Output, callback
4 |
5 | import dash_antd as ant
6 | from dash_antd.ext import parse_tokens
7 |
8 | actions = [
9 | ant.Icon("StepBackwardOutlined", key="action-1"),
10 | ant.Icon("StepBackwardOutlined", key="action-2"),
11 | ant.Icon("StepBackwardOutlined", key="action-3"),
12 | ]
13 |
14 | extra = ant.Button("Hello Extra")
15 |
16 |
17 | layout = [
18 | ant.Row(
19 | id="card-tokens",
20 | gutter=[24, 24],
21 | style={"height": "100%", "padding": 24},
22 | children=[
23 | ant.Col(ant.Card(ant.Button("Hello World"), hoverable=True, title="Hoverable Card"), span=12),
24 | ant.Col(
25 | ant.Card(
26 | [ant.Button("Hello World")],
27 | actions=actions,
28 | title="Card With Actions",
29 | ),
30 | span=12,
31 | ),
32 | ant.Col(
33 | ant.Card(
34 | [ant.Button("Hello World")],
35 | actions=actions,
36 | extra=extra,
37 | title="Card With Actions and extras",
38 | ),
39 | span=12,
40 | ),
41 | ant.Col(ant.Card(ant.Button("Hello World")), span=12),
42 | ant.Col(ant.Card(ant.Button("Hello World"), hoverable=True), span=12),
43 | ],
44 | ),
45 | ]
46 |
47 |
48 | @callback(Output("card-tokens", "children"), Input("app-config", "active_tokens"))
49 | def tokens(active_tokens: dict[str, str]):
50 | parsed = parse_tokens(active_tokens)
51 |
52 | cols = []
53 | for field in fields(parsed):
54 | cols.append(ant.Col(ant.Card(title=field.name, body_style={"background": getattr(parsed, field.name)}), span=3))
55 |
56 | return cols
57 |
--------------------------------------------------------------------------------
/src/ts/components/layout/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Layout } from "antd";
4 | import { omit } from "ramda";
5 |
6 | const { Sider } = Layout;
7 |
8 | type Props = {
9 | /**
10 | * The children of this component.
11 | */
12 | children?: ReactNode;
13 | /**
14 | * Whether the sidebar is collapsed
15 | */
16 | collapsed?: boolean;
17 | /**
18 | * Width of the collapsed sidebar, by setting to 0 a special trigger will appear
19 | */
20 | collapsed_width: number;
21 | /**
22 | * Whether the sidebar can be collapsed
23 | */
24 | collapsible: boolean;
25 | /**
26 | * Reverse direction of arrow, for a sidebar that expands from the right
27 | */
28 | reverse_arrow: boolean;
29 | /**
30 | * Color theme of the sidebar
31 | */
32 | theme?: "light" | "dark";
33 | /**
34 | * Width of the sidebar
35 | */
36 | width: number | string;
37 | } & DashComponentProps &
38 | StyledComponentProps;
39 |
40 | /**
41 | * Handling the overall layout of a page.
42 | */
43 | const Sidebar = (props: Props) => {
44 | const {
45 | children,
46 | class_name,
47 | reverse_arrow,
48 | collapsed_width,
49 | ...otherProps
50 | } = props;
51 | return (
52 |
58 | {children}
59 |
60 | );
61 | };
62 |
63 | Sidebar.defaultProps = {
64 | collapsed_width: 80,
65 | collapsible: false,
66 | collapsed: false,
67 | reverse_arrow: false,
68 | width: 200,
69 | };
70 |
71 | export default Sidebar;
72 |
--------------------------------------------------------------------------------
/dash_antd/ext/_sidebar.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | import dash
4 |
5 | import dash_antd as ant
6 |
7 | _ROOT_STYLE = {
8 | "overflow": "auto",
9 | "height": "100vh",
10 | "position": "fixed",
11 | "left": 0,
12 | "top": 0,
13 | "bottom": 0,
14 | "borderInlineEnd": "1px solid rgba(253, 253, 253, 0.12)",
15 | "overflowX": "hidden",
16 | }
17 |
18 |
19 | def get_nav_item(page):
20 | if "icon" in page:
21 | return {"label": page["title"], "key": page["path"], "path": page["path"], "icon": page["icon"]}
22 | return {"label": page["title"], "key": page["path"], "path": page["path"]}
23 |
24 |
25 | def generate_sidebar_layout(primary_color: Optional[str] = None, use_dark_theme: bool = False) -> ant.ConfigProvider:
26 | nav_items = [get_nav_item(page) for page in dash.page_registry.values()]
27 |
28 | token = {}
29 | if primary_color is not None:
30 | token["colorPrimary"] = primary_color
31 |
32 | return ant.ConfigProvider(
33 | id="app-config",
34 | use_dark_theme=use_dark_theme,
35 | token=token,
36 | children=ant.Layout(
37 | has_sidebar=True,
38 | children=[
39 | ant.Sidebar(
40 | style=_ROOT_STYLE,
41 | theme="light",
42 | children=[
43 | ant.Menu(id="page-nav", items=nav_items, selected_keys=["page-1"], style={"marginRight": -2}),
44 | ant.Divider("Controls"),
45 | ],
46 | ),
47 | ant.Layout(
48 | style={"marginLeft": 200, "minHeight": "100vh"},
49 | children=[
50 | ant.Content(
51 | dash.page_container,
52 | id="page-content",
53 | style={"margin": "0", "padding": 0},
54 | ),
55 | ],
56 | ),
57 | ],
58 | ),
59 | )
60 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | test:
7 | name: Lint and test
8 | runs-on: "ubuntu-latest"
9 | strategy:
10 | fail-fast: false
11 | matrix:
12 | python-version:
13 | - "3.10"
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - uses: extractions/setup-just@v1
18 |
19 | - name: Use Node.js 16
20 | uses: actions/setup-node@v1
21 | id: setup-node
22 | with:
23 | node-version: 16.x
24 |
25 | - name: Setup Python
26 | uses: actions/setup-python@v2
27 | id: setup-python
28 | with:
29 | python-version: ${{ matrix.python-version }}
30 |
31 | - name: Load cached venv
32 | id: cached-poetry-dependencies
33 | uses: actions/cache@v2
34 | with:
35 | path: .venv
36 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('poetry.lock') }}
37 |
38 | - name: Load cached venv
39 | id: cached-node-modules
40 | uses: actions/cache@v2
41 | with:
42 | path: node_modules
43 | key: venv-${{ runner.os }}-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('yarn.lock') }}
44 |
45 | - name: Install Poetry
46 | uses: snok/install-poetry@v1
47 | with:
48 | virtualenvs-create: true
49 | virtualenvs-in-project: true
50 | installer-parallel: true
51 |
52 | - name: Install Python dependencies
53 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
54 | run: poetry install --no-interaction --no-root
55 |
56 | - name: Install Node dependencies
57 | if: steps.cached-node-modules.outputs.cache-hit != 'true'
58 | run: yarn install --frozen-lockfile
59 |
60 | - name: Lint source
61 | run: just lint
62 |
63 | - name: Run tests
64 | run: just test
65 |
66 | - name: Upload to Codecov
67 | uses: codecov/codecov-action@v2
68 |
--------------------------------------------------------------------------------
/src/ts/components/radio/__tests__/RadioGroup.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 | import userEvent from "@testing-library/user-event";
4 | import RadioGroup from "../RadioGroup";
5 |
6 | describe("RadioButton", () => {
7 | test("renders its content", () => {
8 | const { container } = render(
9 | Some RadioButton content
10 | );
11 |
12 | expect(container).toHaveTextContent("Some RadioButton content");
13 | });
14 |
15 | test("updates checked prop when clicked", async () => {
16 | const user = userEvent.setup();
17 | const mockSetProps = jest.fn();
18 |
19 | const checkbox = render(
20 |
24 | );
25 |
26 | expect(mockSetProps.mock.calls).toHaveLength(0);
27 |
28 | await user.click(checkbox.queryByText("foo"));
29 |
30 | expect(mockSetProps.mock.calls).toHaveLength(1);
31 | expect(mockSetProps.mock.calls[0][0].value).toBe("foo");
32 | });
33 |
34 | test("does not update checked prop when disabled item is clicked", async () => {
35 | const user = userEvent.setup();
36 | const mockSetProps = jest.fn();
37 |
38 | const checkbox = render(
39 |
47 | );
48 |
49 | expect(mockSetProps.mock.calls).toHaveLength(0);
50 |
51 | await user.click(checkbox.queryByText("foo"));
52 |
53 | expect(mockSetProps.mock.calls).toHaveLength(1);
54 |
55 | await user.click(checkbox.queryByText("baz"));
56 |
57 | expect(mockSetProps.mock.calls).toHaveLength(1);
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/webpack/config.dist.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var path = require("path");
4 | var webpack = require("webpack");
5 | var moduleDefinition = require("./moduleDefinition");
6 | var directories = require("./directories");
7 | const packagejson = require("../package.json");
8 |
9 | var OccurrenceOrderPlugin = require("webpack").optimize.OccurrenceOrderPlugin;
10 |
11 | var NODE_ENV = process.env.NODE_ENV || "development";
12 | var environment = JSON.stringify(NODE_ENV);
13 |
14 | const dashLibraryName = packagejson.name.replace(/-/g, "_");
15 | var buildPath = path.join(directories.ROOT, dashLibraryName);
16 |
17 | module.exports = function (_env, argv) {
18 | const mode = (argv && argv.mode) || "production";
19 | const entry = { main: [path.join(directories.SRC, "ts/index.ts")] };
20 | const output = {
21 | path: buildPath,
22 | filename: `${dashLibraryName}.min.js`,
23 | library: {
24 | name: dashLibraryName,
25 | type: "umd",
26 | },
27 | };
28 |
29 | const externals = {
30 | react: {
31 | commonjs: "react",
32 | commonjs2: "react",
33 | amd: "react",
34 | umd: "react",
35 | root: "React",
36 | },
37 | "react-dom": {
38 | commonjs: "react-dom",
39 | commonjs2: "react-dom",
40 | amd: "react-dom",
41 | umd: "react-dom",
42 | root: "ReactDOM",
43 | },
44 | };
45 |
46 | /* eslint-disable no-console */
47 | console.log("Current environment: " + mode);
48 | /* eslint-enable no-console */
49 |
50 | return {
51 | cache: false,
52 | // context: `${directories.SRC}/ts`,
53 | mode,
54 | externals,
55 | resolve: {
56 | extensions: [".ts", ".tsx", ".js", ".json"],
57 | },
58 | module: moduleDefinition,
59 | optimization: {
60 | minimize: true,
61 | chunkIds: "total-size",
62 | moduleIds: "size",
63 | },
64 | entry,
65 | output,
66 | };
67 | };
68 |
--------------------------------------------------------------------------------
/src/ts/components/radio/RadioGroup.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useCallback } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Radio, RadioChangeEvent } from "antd";
4 | import { omit } from "ramda";
5 |
6 | const { Group } = Radio;
7 |
8 | type Props = {
9 | /**
10 | * The children of this component.
11 | */
12 | children?: ReactNode;
13 | /**
14 | * The style type of radio button
15 | */
16 | button_style?: "outline" | "solid";
17 | /**
18 | * Disable all radio buttons
19 | */
20 | disabled?: boolean;
21 | /**
22 | * Set children optional
23 | */
24 | options?:
25 | | string[]
26 | | number[]
27 | | Array<{ label: string; value: string; disabled?: boolean }>;
28 | /**
29 | * Set Radio optionType
30 | */
31 | option_type?: "default" | "button";
32 | /**
33 | * The size of radio button
34 | */
35 | size?: "large" | "middle" | "small";
36 | /**
37 | * Used for setting the currently selected value
38 | */
39 | value?: string | number;
40 | } & DashComponentProps &
41 | StyledComponentProps;
42 |
43 | /**
44 | * RadioGroup
45 | */
46 | const RadioGroup = (props: Props) => {
47 | const {
48 | children,
49 | button_style,
50 | disabled,
51 | option_type,
52 | setProps,
53 | ...otherProps
54 | } = props;
55 |
56 | const onChange = useCallback(
57 | (e: RadioChangeEvent) => {
58 | if (!disabled && setProps) {
59 | setProps({ value: e.target.value });
60 | }
61 | },
62 | [setProps, disabled]
63 | );
64 |
65 | return (
66 |
73 | {children}
74 |
75 | );
76 | };
77 |
78 | RadioGroup.defaultProps = {};
79 |
80 | export default RadioGroup;
81 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["poetry-core>=1.0.0"]
3 | build-backend = "poetry.core.masonry.api"
4 |
5 | [tool.poetry]
6 | name = "dash-antd"
7 | version = "0.1.6"
8 | description = "Ant Design components for Plotly Dash"
9 | repository = "https://github.com/roeap/dash-ant-design-components"
10 | authors = ["Robert Pack "]
11 | license = "MIT"
12 | readme = "README.md"
13 | include = [
14 | "dash_antd/*.py",
15 | "dash_antd/metadata.json",
16 | "dash_antd/package-info.json",
17 | "dash_antd/package.json",
18 | "dash_antd/dash_antd.min.js",
19 | "dash_antd/LICENSE.txt",
20 | ]
21 |
22 | [tool.poetry.dependencies]
23 | python = "^3.8"
24 | dash = "^2.7"
25 |
26 | [tool.poetry.group.dev.dependencies]
27 | black = "^22.10.0"
28 | ruff = "^0.0.150"
29 | wheel = ">=0.37.1"
30 | PyYAML = "^6.0"
31 | pre-commit = "^2.20.0"
32 | detect-secrets = "^1.4.0"
33 | pandas = "^1.5.2"
34 |
35 | [tool.black]
36 | line-length = 120
37 | target-version = ['py38']
38 | include = '\.pyi?$'
39 |
40 | [tool.ruff]
41 | line-length = 120
42 | select = [
43 | "B", # flake8-bugbear
44 | "C", # flake8-comprehensions
45 | "E", # pycodestyle errors
46 | "F", # pyflakes
47 | "I", # isort
48 | "S", # flake8-bandit
49 | "W", # pycodestyle warnings
50 | ]
51 | exclude = [
52 | ".bzr",
53 | ".direnv",
54 | ".eggs",
55 | ".git",
56 | ".hg",
57 | ".mypy_cache",
58 | ".nox",
59 | ".pants.d",
60 | ".ruff_cache",
61 | ".svn",
62 | ".tox",
63 | ".venv",
64 | "__pypackages__",
65 | "_build",
66 | "buck-out",
67 | "build",
68 | "dist",
69 | "node_modules",
70 | "venv",
71 | "dash_antd/__init__.py",
72 | ]
73 | # Allow unused variables when underscore-prefixed.
74 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
75 | target-version = "py39"
76 |
77 | [tool.ruff.isort]
78 | known-first-party = ["argus"]
79 |
80 | [tool.ruff.per-file-ignores]
81 | "test_*" = [
82 | "S101", # assert staments allowed in tests
83 | "B008", # do not perform function calls in argument defaults
84 | ]
85 | "dash_antd/*" = [
86 | "E501", # line too long
87 | "W605",
88 | ]
89 |
--------------------------------------------------------------------------------
/src/ts/components/dropdown/DropdownButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import {
3 | DashComponentProps,
4 | ItemType,
5 | DashLoadingState,
6 | StyledComponentProps,
7 | } from "../../types";
8 | import { Dropdown as AntDropdown, Menu as AntMenu } from "antd";
9 |
10 | const { Button: AntDropdownButton } = AntDropdown;
11 |
12 | type Props = {
13 | /**
14 | * The children of this component.
15 | */
16 | children?: ReactNode;
17 | /**
18 | * Items displayed in the dropdown menu
19 | */
20 | items: ItemType[];
21 | /**
22 | * Disables all checkboxes within the group
23 | */
24 | disabled?: boolean;
25 | /**
26 | * Object that holds the loading state object coming from dash-renderer
27 | */
28 | loading_state?: DashLoadingState;
29 | /**
30 | * An integer that represents the number of times
31 | * that this element has been clicked on.
32 | */
33 | n_clicks: number;
34 | } & DashComponentProps &
35 | StyledComponentProps;
36 |
37 | /**
38 | * A button with an integrated dropdown menu
39 | */
40 | const DropdownButton = (props: Props) => {
41 | const {
42 | children,
43 | items,
44 | class_name,
45 | loading_state,
46 | disabled,
47 | setProps,
48 | n_clicks,
49 | ...otherProps
50 | } = props;
51 |
52 | const overlay = ;
53 |
54 | const onClick = () => {
55 | if (!disabled && setProps) {
56 | setProps({
57 | n_clicks: n_clicks + 1,
58 | });
59 | }
60 | };
61 |
62 | return (
63 |
74 | {children}
75 |
76 | );
77 | };
78 |
79 | DropdownButton.defaultProps = {
80 | n_clicks: 0,
81 | };
82 |
83 | export default DropdownButton;
84 |
--------------------------------------------------------------------------------
/src/ts/components/layout/Col.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Col as AntCol } from "antd";
4 | import { omit } from "ramda";
5 |
6 | export type Props = {
7 | /**
8 | * The children of this component.
9 | */
10 | children?: ReactNode;
11 | /**
12 | * Flex layout style
13 | */
14 | flex?: string | number;
15 | /**
16 | * The number of cells to offset Col from the left
17 | */
18 | offset?: number;
19 | /**
20 | * Raster order
21 | */
22 | order?: number;
23 | /**
24 | * The number of cells that raster is moved to the left
25 | */
26 | pull?: number;
27 | /**
28 | * The number of cells that raster is moved to the right
29 | */
30 | push?: number;
31 | /**
32 | * Raster number of cells to occupy, 0 corresponds to display: none
33 | */
34 | span?: number;
35 | /**
36 | * screen < 576px and also default setting, could be a span value or an object containing above props
37 | */
38 | xs?: number | object;
39 | /**
40 | * screen ≥ 576px, could be a span value or an object containing above props
41 | */
42 | sm?: number | object;
43 | /**
44 | * screen ≥ 768px, could be a span value or an object containing above props
45 | */
46 | md?: number | object;
47 | /**
48 | * screen ≥ 992px, could be a span value or an object containing above props
49 | */
50 | lg?: number | object;
51 | /**
52 | * screen ≥ 1200px, could be a span value or an object containing above props
53 | */
54 | xl?: number | object;
55 | /**
56 | * screen ≥ 1600px, could be a span value or an object containing above props
57 | */
58 | xxl?: number | object;
59 | } & DashComponentProps &
60 | StyledComponentProps;
61 |
62 | /**
63 | * Col
64 | */
65 | const Col = (props: Props) => {
66 | const { children, class_name, ...otherProps } = props;
67 | return (
68 |
69 | {children}
70 |
71 | );
72 | };
73 |
74 | Col.defaultProps = {
75 | align: "top",
76 | gutter: 0,
77 | justify: "start",
78 | wrap: true,
79 | };
80 |
81 | export default Col;
82 |
--------------------------------------------------------------------------------
/src/ts/components/Slider.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../types";
3 | import { Slider as AntSlider, SliderSingleProps } from "antd";
4 |
5 | type Props = {
6 | /**
7 | * Disable radio
8 | */
9 | disabled?: boolean;
10 | /**
11 | * Tick mark of Slider, type of key must be number, and must in closed interval
12 | * [min, max], each mark can declare its own style
13 | */
14 | marks?:
15 | | { [key: number]: string }
16 | | { [key: number]: { label: string; style: object } };
17 | /**
18 | * The maximum value the slider can slide to
19 | */
20 | max: number;
21 | /**
22 | * The minimum value the slider can slide to
23 | */
24 | min: number;
25 | /**
26 | * Dual thumb mode
27 | */
28 | range?: boolean;
29 | /**
30 | * Reverse the component
31 | */
32 | reverse?: boolean;
33 | /**
34 | * The granularity the slider can step through values.
35 | * Must greater than 0, and be divided by (max - min). When marks no null, step can be null
36 | */
37 | step?: number;
38 | /**
39 | * The value of slider. When range is false, use number, otherwise, use [number, number]
40 | */
41 | value?: number | [number, number];
42 | /**
43 | * If true, the slider will be vertical
44 | */
45 | vertical?: boolean;
46 | } & DashComponentProps &
47 | StyledComponentProps;
48 |
49 | /**
50 | * A Slider component for displaying current value and intervals in range.
51 | */
52 | const Slider = (props: Props) => {
53 | const { class_name, disabled, setProps, ...otherProps } = props;
54 |
55 | const onChange: SliderSingleProps["onChange"] = useCallback(
56 | (value: number | [number, number]) => {
57 | if (!disabled && setProps) {
58 | setProps({ value });
59 | }
60 | },
61 | [setProps, disabled]
62 | );
63 |
64 | return (
65 | // @ts-expect-error we use only boolean range prop.
66 |
72 | );
73 | };
74 |
75 | Slider.defaultProps = {
76 | min: 0,
77 | max: 100,
78 | };
79 |
80 | export default Slider;
81 |
--------------------------------------------------------------------------------
/dash_antd/_imports_.py:
--------------------------------------------------------------------------------
1 | from .Alert import Alert
2 | from .AutoComplete import AutoComplete
3 | from .Button import Button
4 | from .Card import Card
5 | from .CheckableTag import CheckableTag
6 | from .Checkbox import Checkbox
7 | from .CheckboxGroup import CheckboxGroup
8 | from .Col import Col
9 | from .ConfigProvider import ConfigProvider
10 | from .Content import Content
11 | from .DatePicker import DatePicker
12 | from .DateRangePicker import DateRangePicker
13 | from .Divider import Divider
14 | from .DropdownButton import DropdownButton
15 | from .DropdownMenu import DropdownMenu
16 | from .Footer import Footer
17 | from .Header import Header
18 | from .Icon import Icon
19 | from .Input import Input
20 | from .InputNumber import InputNumber
21 | from .Layout import Layout
22 | from .Menu import Menu
23 | from .MenuItem import MenuItem
24 | from .Page import Page
25 | from .PagesWithSidebar import PagesWithSidebar
26 | from .Radio import Radio
27 | from .RadioButton import RadioButton
28 | from .RadioGroup import RadioGroup
29 | from .Row import Row
30 | from .Segmented import Segmented
31 | from .Select import Select
32 | from .Sidebar import Sidebar
33 | from .Slider import Slider
34 | from .Space import Space
35 | from .Steps import Steps
36 | from .Switch import Switch
37 | from .TabPane import TabPane
38 | from .Tabs import Tabs
39 | from .Tag import Tag
40 | from .TextArea import TextArea
41 | from .Timeline import Timeline
42 | from .TimelineItem import TimelineItem
43 | from .TimePicker import TimePicker
44 | from .TimeRangePicker import TimeRangePicker
45 |
46 | __all__ = [
47 | "Alert",
48 | "AutoComplete",
49 | "Button",
50 | "Card",
51 | "Checkbox",
52 | "CheckboxGroup",
53 | "DatePicker",
54 | "DateRangePicker",
55 | "TimePicker",
56 | "TimeRangePicker",
57 | "Divider",
58 | "DropdownButton",
59 | "DropdownMenu",
60 | "Icon",
61 | "Input",
62 | "InputNumber",
63 | "TextArea",
64 | "Col",
65 | "Content",
66 | "Footer",
67 | "Header",
68 | "Layout",
69 | "Row",
70 | "Sidebar",
71 | "Menu",
72 | "MenuItem",
73 | "Radio",
74 | "RadioButton",
75 | "RadioGroup",
76 | "Select",
77 | "Steps",
78 | "TabPane",
79 | "Tabs",
80 | "CheckableTag",
81 | "Tag",
82 | "Page",
83 | "PagesWithSidebar",
84 | "Timeline",
85 | "TimelineItem",
86 | "ConfigProvider",
87 | "Segmented",
88 | "Slider",
89 | "Space",
90 | "Switch",
91 | ]
92 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dash_antd",
3 | "version": "0.1.6",
4 | "description": "A Dash wrapper around React Ant Design Components",
5 | "main": "index.ts",
6 | "scripts": {
7 | "build:js::dev": "webpack --mode development --config=webpack/config.dist.js",
8 | "build:js": "webpack --mode production --config=webpack/config.dist.js",
9 | "build:backends": "dash-generate-components ./src/ts/components dash_antd --ignore \\.test\\.",
10 | "build": "yarn build:js && yarn build:backends",
11 | "watch": "yarn build:js::dev -- --watch",
12 | "test": "jest",
13 | "demo": "webpack serve --mode development --hot --port=8888 --config=webpack/config.demo.js",
14 | "format": "prettier 'src/' --write",
15 | "lint": "prettier 'src/' --list-different && eslint . --ext .js,.jsx,.ts,.tsx"
16 | },
17 | "devDependencies": {
18 | "@testing-library/dom": "^8.13.0",
19 | "@testing-library/jest-dom": "^5.16.4",
20 | "@testing-library/react": "^12.1.5",
21 | "@testing-library/user-event": "^14.1.1",
22 | "@types/fast-isnumeric": "^1.1.0",
23 | "@types/jest": "^27.4.1",
24 | "@types/node": "^17.0.31",
25 | "@types/ramda": "^0.28.12",
26 | "@types/react": "^17.0.39",
27 | "@typescript-eslint/eslint-plugin": "^5.22.0",
28 | "@typescript-eslint/parser": "^5.22.0",
29 | "css-loader": "^6.7.1",
30 | "eslint": "^8.14.0",
31 | "eslint-config-prettier": "^8.5.0",
32 | "eslint-plugin-jest": "^26.1.5",
33 | "eslint-plugin-react": "^7.29.4",
34 | "eslint-plugin-react-hooks": "^4.5.0",
35 | "jest": "^27.5.1",
36 | "jsdom": "^19.0.0",
37 | "npm-run-all": "^4.1.5",
38 | "prettier": "^2.6.2",
39 | "react": "16.13.0",
40 | "react-docgen": "^5.4.0",
41 | "react-dom": "16.13.0",
42 | "style-loader": "^3.3.1",
43 | "ts-jest": "^27.1.4",
44 | "ts-loader": "^8.2.3",
45 | "ts-node": "^10.7.0",
46 | "typescript": "^4.6.2",
47 | "webpack": "^5.50.0",
48 | "webpack-cli": "^4.9.1",
49 | "webpack-dev-server": "^4.8.1"
50 | },
51 | "peerDependencies": {
52 | "react": "^16.13.0",
53 | "react-dom": "^16.13.0"
54 | },
55 | "author": "Robert Pack ",
56 | "license": "MIT",
57 | "dependencies": {
58 | "@ant-design/icons": "^4.8.0",
59 | "antd": "^5",
60 | "dayjs": "^1.11.6",
61 | "fast-isnumeric": "^1.1.4",
62 | "fuse.js": "^6.6.2",
63 | "less": "^4.1.2",
64 | "less-loader": "^10.2.0",
65 | "ramda": "^0.28.0"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/ts/components/card/Card.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import {
3 | DashComponentProps,
4 | StyledComponentProps,
5 | DashLoadingState,
6 | } from "../../types";
7 | import { Card as AntCard, CardProps } from "antd";
8 |
9 | type Props = {
10 | /**
11 | * The children of this component.
12 | */
13 | children?: ReactNode;
14 | /**
15 | * The action list, shows at the bottom of the Card
16 | */
17 | actions?: ReactNode[];
18 | /**
19 | * Content to render in the top-right corner of the card
20 | */
21 | extra?: ReactNode;
22 | /**
23 | * Current TabPane's key
24 | */
25 | active_tab_key?: string;
26 | /**
27 | * Inline style to apply to the card content
28 | */
29 | body_style?: object;
30 | /**
31 | * Toggles rendering of the border around the card
32 | */
33 | bordered?: boolean;
34 | /**
35 | * Inline style to apply to the card head
36 | */
37 | head_style?: object;
38 | /**
39 | * Lift up when hovering card
40 | */
41 | hoverable?: boolean;
42 | /**
43 | * Card title
44 | */
45 | title?: string;
46 | /**
47 | * Shows a loading indicator while the contents of the card are being fetched
48 | */
49 | loading?: boolean;
50 | /**
51 | * Object that holds the loading state object coming from dash-renderer
52 | */
53 | loading_state?: DashLoadingState;
54 | } & DashComponentProps &
55 | StyledComponentProps;
56 |
57 | /**
58 | * Simple rectangular container.
59 | */
60 | const Card = (props: Props) => {
61 | const {
62 | active_tab_key,
63 | body_style,
64 | children,
65 | class_name,
66 | head_style,
67 | loading_state,
68 | setProps,
69 | ...otherProps
70 | } = props;
71 |
72 | const onTabChange: CardProps["onTabChange"] = (key) => {
73 | setProps({ active_tab_key: key });
74 | };
75 |
76 | return (
77 |
89 | {children}
90 |
91 | );
92 | };
93 |
94 | export default Card;
95 |
--------------------------------------------------------------------------------
/src/ts/utilities.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { isNil } from "ramda";
3 |
4 | function isNodeArray(node: T | T[]): node is T[] {
5 | return Array.isArray(node);
6 | }
7 |
8 | export function parseChildrenToArray(children?: T | T[]): T[] {
9 | if (children) {
10 | if (!isNodeArray(children)) {
11 | return [children];
12 | }
13 | return children;
14 | }
15 | return [];
16 | }
17 |
18 | function isUndefined(obj?: unknown): obj is null | undefined {
19 | return obj === undefined || obj === null;
20 | }
21 |
22 | export function getComponentType(component: ReactNode): string {
23 | if (!component || isUndefined(component)) return "";
24 | if (typeof component === "string") return "string";
25 | if (typeof component === "number") return "number";
26 | if (typeof component === "boolean") return "boolean";
27 | if (!("props" in component)) return "object";
28 | if (
29 | // disabled is a defaultProp (so it's always set)
30 | // meaning that if it's not set on component.props, the actual
31 | // props we want are lying a bit deeper - which means they
32 | // are coming from Dash
33 | isNil(component.props.disabled) &&
34 | component.props._dashprivate_layout &&
35 | component.props._dashprivate_layout.props
36 | ) {
37 | // props are coming from Dash
38 | return component.props._dashprivate_layout.type;
39 | }
40 | // else props are coming from React
41 | // @ts-expect-error ts does not know what we know
42 | return component.type.name;
43 | }
44 |
45 | export function getComponentProps(
46 | component: ReactNode
47 | ): Record {
48 | if (!component || isUndefined(component)) return {};
49 | if (typeof component === "string") return {};
50 | if (typeof component === "number") return {};
51 | if (typeof component === "boolean") return {};
52 | if (!("props" in component)) return {};
53 | if (
54 | // disabled is a defaultProp (so it's always set)
55 | // meaning that if it's not set on component.props, the actual
56 | // props we want are lying a bit deeper - which means they
57 | // are coming from Dash
58 | isNil(component.props.disabled) &&
59 | component.props._dashprivate_layout &&
60 | component.props._dashprivate_layout.props
61 | ) {
62 | // props are coming from Dash
63 | return component.props._dashprivate_layout.props;
64 | }
65 | // else props are coming from React
66 | return component.props;
67 | }
68 |
--------------------------------------------------------------------------------
/demo/Demo.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Layout, Menu, MenuProps } from "antd";
3 | import {
4 | AppstoreOutlined,
5 | BarChartOutlined,
6 | CloudOutlined,
7 | ShopOutlined,
8 | TeamOutlined,
9 | UserOutlined,
10 | UploadOutlined,
11 | VideoCameraOutlined,
12 | } from "@ant-design/icons";
13 | import { Button } from "../src/ts";
14 | import "../styles/app.less";
15 |
16 | const { Header, Content, Footer, Sider } = Layout;
17 |
18 | // eslint-disable-next-line
19 | const StateWrapper = ({ tag: Tag, ...otherProps }) => {
20 | // helper to mimic setProps functionality
21 | const [state, setState] = useState(otherProps);
22 | return (
23 | setState({ ...state, ...props })}
25 | {...state}
26 | />
27 | );
28 | };
29 |
30 | const items: MenuProps["items"] = [
31 | UserOutlined,
32 | VideoCameraOutlined,
33 | UploadOutlined,
34 | BarChartOutlined,
35 | CloudOutlined,
36 | AppstoreOutlined,
37 | TeamOutlined,
38 | ShopOutlined,
39 | ].map((icon, index) => ({
40 | key: String(index + 1),
41 | icon: React.createElement(icon),
42 | label: `nav ${index + 1}`,
43 | }));
44 |
45 | const Demo = () => {
46 | return (
47 |
48 |
59 |
60 |
66 |
67 |
68 |
69 |
70 |
73 |
76 |
77 |
80 |
81 |
82 | );
83 | };
84 |
85 | export default Demo;
86 |
--------------------------------------------------------------------------------
/src/ts/components/ConfigProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useMemo, useEffect } from "react";
2 | import { DashComponentProps } from "../types";
3 | import { ConfigProvider as AntConfigProvider, theme } from "antd";
4 | import { omit } from "ramda";
5 | import { AliasToken } from "antd/lib/theme/interface";
6 | const { useToken } = theme;
7 |
8 | type Size = "small" | "middle" | "large" | number;
9 |
10 | type Props = {
11 | /**
12 | * The children of this component.
13 | */
14 | children?: ReactNode;
15 | /**
16 | * Set common properties for Input component
17 | */
18 | input?: { autoComplete?: string };
19 | /**
20 | * Set sizing in Space component
21 | */
22 | space?: { size?: Size };
23 | /**
24 | * Set component specific design tokens
25 | */
26 | components?: { [token: string]: { [token: string]: string } };
27 | /**
28 | * Set global design tokens
29 | */
30 | token?: { [token: string]: string };
31 | /**
32 | * Create a dark theming for all child components
33 | */
34 | use_dark_theme?: boolean;
35 | /**
36 | * Create a dark theming for all child components
37 | */
38 | use_compact?: boolean;
39 | /**
40 | * (ReadOnly) fully resolved global design tokens
41 | */
42 | active_tokens?: Partial;
43 | } & DashComponentProps;
44 |
45 | const ThemeUpdater = (props: DashComponentProps) => {
46 | const { setProps } = props;
47 | const { token: active_tokens } = useToken();
48 | useEffect(() => {
49 | setProps({ active_tokens });
50 | }, [setProps, active_tokens]);
51 | return null;
52 | };
53 |
54 | /**
55 | * Set components spacing.
56 | */
57 | const ConfigProvider = (props: Props) => {
58 | const {
59 | children,
60 | token,
61 | components,
62 | use_dark_theme: useDarkTheme,
63 | use_compact: useCompact,
64 | setProps,
65 | ...otherProps
66 | } = props;
67 |
68 | const themeConfig = useMemo(() => {
69 | const algorithm = useDarkTheme
70 | ? [theme.darkAlgorithm]
71 | : [theme.defaultAlgorithm];
72 | if (useCompact) algorithm.push(theme.compactAlgorithm);
73 | return { algorithm, token, components };
74 | }, [token, components, useDarkTheme, useCompact]);
75 |
76 | return (
77 |
81 |
82 | {children}
83 |
84 | );
85 | };
86 |
87 | ConfigProvider.defaultProps = {};
88 |
89 | export default ConfigProvider;
90 |
--------------------------------------------------------------------------------
/.secrets.baseline:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.4.0",
3 | "plugins_used": [
4 | {
5 | "name": "ArtifactoryDetector"
6 | },
7 | {
8 | "name": "AWSKeyDetector"
9 | },
10 | {
11 | "name": "AzureStorageKeyDetector"
12 | },
13 | {
14 | "name": "Base64HighEntropyString",
15 | "limit": 4.5
16 | },
17 | {
18 | "name": "BasicAuthDetector"
19 | },
20 | {
21 | "name": "CloudantDetector"
22 | },
23 | {
24 | "name": "DiscordBotTokenDetector"
25 | },
26 | {
27 | "name": "GitHubTokenDetector"
28 | },
29 | {
30 | "name": "HexHighEntropyString",
31 | "limit": 3.0
32 | },
33 | {
34 | "name": "IbmCloudIamDetector"
35 | },
36 | {
37 | "name": "IbmCosHmacDetector"
38 | },
39 | {
40 | "name": "JwtTokenDetector"
41 | },
42 | {
43 | "name": "KeywordDetector",
44 | "keyword_exclude": ""
45 | },
46 | {
47 | "name": "MailchimpDetector"
48 | },
49 | {
50 | "name": "NpmDetector"
51 | },
52 | {
53 | "name": "PrivateKeyDetector"
54 | },
55 | {
56 | "name": "SendGridDetector"
57 | },
58 | {
59 | "name": "SlackDetector"
60 | },
61 | {
62 | "name": "SoftlayerDetector"
63 | },
64 | {
65 | "name": "SquareOAuthDetector"
66 | },
67 | {
68 | "name": "StripeDetector"
69 | },
70 | {
71 | "name": "TwilioKeyDetector"
72 | }
73 | ],
74 | "filters_used": [
75 | {
76 | "path": "detect_secrets.filters.allowlist.is_line_allowlisted"
77 | },
78 | {
79 | "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
80 | "min_level": 2
81 | },
82 | {
83 | "path": "detect_secrets.filters.heuristic.is_indirect_reference"
84 | },
85 | {
86 | "path": "detect_secrets.filters.heuristic.is_likely_id_string"
87 | },
88 | {
89 | "path": "detect_secrets.filters.heuristic.is_lock_file"
90 | },
91 | {
92 | "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string"
93 | },
94 | {
95 | "path": "detect_secrets.filters.heuristic.is_potential_uuid"
96 | },
97 | {
98 | "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign"
99 | },
100 | {
101 | "path": "detect_secrets.filters.heuristic.is_sequential_string"
102 | },
103 | {
104 | "path": "detect_secrets.filters.heuristic.is_swagger_file"
105 | },
106 | {
107 | "path": "detect_secrets.filters.heuristic.is_templated_secret"
108 | }
109 | ],
110 | "results": {},
111 | "generated_at": "2022-11-28T08:46:46Z"
112 | }
113 |
--------------------------------------------------------------------------------
/src/ts/components/button/Button.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import {
3 | DashComponentProps,
4 | DashLoadingState,
5 | StyledComponentProps,
6 | } from "../../types";
7 | import { Button as AntButton } from "antd";
8 |
9 | type Props = {
10 | /**
11 | * The children of this component.
12 | */
13 | children?: ReactNode;
14 | /**
15 | * Set the danger status of button
16 | */
17 | danger: boolean;
18 | /**
19 | * Disabled state of button
20 | */
21 | disabled: boolean;
22 | /**
23 | * The shape of the button
24 | */
25 | shape: "default" | "circle" | "round";
26 | /**
27 | * The size of the button
28 | */
29 | size: "large" | "middle" | "small";
30 | /**
31 | * Same as target attribute of a, works when href is specified
32 | */
33 | target?: string;
34 | /**
35 | * The type of the button
36 | */
37 | type: "primary" | "ghost" | "dashed" | "link" | "text" | "default";
38 | /**
39 | * An integer that represents the number of times
40 | * that this element has been clicked on.
41 | */
42 | n_clicks: number;
43 | /**
44 | * Object that holds the loading state object coming from dash-renderer
45 | */
46 | loading_state?: DashLoadingState;
47 | /**
48 | * Pass a URL (relative or absolute) to make the menu entry a link.
49 | */
50 | href?: string;
51 | } & DashComponentProps &
52 | StyledComponentProps;
53 |
54 | /**
55 | * A basic Button component
56 | */
57 | const Button = (props: Props) => {
58 | const {
59 | disabled,
60 | children,
61 | setProps,
62 | n_clicks,
63 | href,
64 | class_name,
65 | loading_state,
66 | ...otherProps
67 | } = props;
68 |
69 | const onClick = () => {
70 | if (!disabled && setProps) {
71 | setProps({
72 | n_clicks: n_clicks + 1,
73 | });
74 | }
75 | };
76 |
77 | return (
78 |
89 | {children || "Button"}
90 |
91 | );
92 | };
93 |
94 | Button.defaultProps = {
95 | danger: false,
96 | disabled: false,
97 | shape: "default",
98 | size: "middle",
99 | type: "default",
100 | n_clicks: 0,
101 | };
102 |
103 | export default Button;
104 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Publish Python 🐍 distributions 📦 to PyPI
2 |
3 | on:
4 | push:
5 | tags: ["v*"]
6 |
7 | jobs:
8 | validate-release-tag:
9 | name: Validate git tag
10 | runs-on: ubuntu-20.04
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: compare git tag with cargo metadata
14 | run: |
15 | PUSHED_TAG=${GITHUB_REF##*/}
16 | CURR_VER=$( grep version pyproject.toml | head -n 1 | awk '{print $3}' | tr -d '"' )
17 | if [[ "${PUSHED_TAG}" != "v${CURR_VER}" ]]; then
18 | echo "pyproject metadata has version set to ${CURR_VER}, but got pushed tag ${PUSHED_TAG}."
19 | exit 1
20 | fi
21 |
22 | build-n-publish:
23 | name: Build and publish Python 🐍 distributions 📦 to PyPI
24 | needs: validate-release-tag
25 | runs-on: ubuntu-latest
26 |
27 | steps:
28 | - uses: actions/checkout@v2
29 | - uses: extractions/setup-just@v1
30 |
31 | - name: Use Node.js 16
32 | uses: actions/setup-node@v1
33 | id: setup-node
34 | with:
35 | node-version: 16.x
36 |
37 | - name: Setup Python
38 | uses: actions/setup-python@v2
39 | id: setup-python
40 | with:
41 | python-version: ${{ matrix.python-version }}
42 |
43 | - name: Load cached venv
44 | id: cached-poetry-dependencies
45 | uses: actions/cache@v2
46 | with:
47 | path: .venv
48 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('poetry.lock') }}
49 |
50 | - name: Load cached venv
51 | id: cached-node-modules
52 | uses: actions/cache@v2
53 | with:
54 | path: node_modules
55 | key: venv-${{ runner.os }}-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('yarn.lock') }}
56 |
57 | - name: Install Poetry
58 | uses: snok/install-poetry@v1
59 | with:
60 | virtualenvs-create: true
61 | virtualenvs-in-project: true
62 | installer-parallel: true
63 |
64 | - name: Install Python dependencies
65 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
66 | run: poetry install --no-interaction --no-root
67 |
68 | - name: Install Node dependencies
69 | if: steps.cached-node-modules.outputs.cache-hit != 'true'
70 | run: yarn install --frozen-lockfile
71 |
72 | - name: Lint source
73 | run: just lint
74 |
75 | - name: Run tests
76 | run: just test
77 |
78 | - name: Build package
79 | run: just package
80 |
81 | - name: Publish distribution 📦 to PyPI
82 | run: |
83 | pip install twine
84 | twine upload dist/*
85 | env:
86 | TWINE_USERNAME: __token__
87 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
88 |
--------------------------------------------------------------------------------
/src/ts/components/steps/Steps.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Steps as AntSteps } from "antd";
4 |
5 | type StepItem = {
6 | /**
7 | * Description of the step
8 | */
9 | description?: string;
10 | /**
11 | * Disable click
12 | */
13 | disabled?: boolean;
14 | /**
15 | * Icon of the step
16 | */
17 | icon: ReactNode;
18 | /**
19 | * To specify the status. It will be automatically set by current of Steps if not configured.
20 | */
21 | status?: "wait" | "process" | "finish" | "error";
22 | /**
23 | * Subtitle of the step
24 | */
25 | subTitle?: ReactNode;
26 | /**
27 | * Title of the step
28 | */
29 | title?: ReactNode;
30 | };
31 |
32 | type Props = {
33 | children?: ReactNode;
34 | /**
35 | * The children of this component.
36 | */
37 | items: StepItem[];
38 | /**
39 | * To set the current step, counting from 0. You can overwrite this state by using status of Step
40 | */
41 | current?: number;
42 | /**
43 | * To specify the direction of the step bar, horizontal or vertical
44 | */
45 | direction?: "horizontal" | "vertical";
46 | /**
47 | * Set the initial step, counting from 0
48 | */
49 | initial?: number;
50 | /**
51 | * Place title and description with horizontal or vertical direction
52 | */
53 | label_placement?: "horizontal" | "vertical";
54 | /**
55 | * Progress circle percentage of current step in process status (only works on basic Steps)
56 | */
57 | percent?: number;
58 | /**
59 | * Steps with progress dot style
60 | */
61 | progress_dot?: boolean;
62 | /**
63 | * Change to vertical direction when screen width smaller than 532px
64 | */
65 | responsive?: boolean;
66 | /**
67 | * To specify the size of the step bar
68 | */
69 | size?: "default" | "small";
70 | /**
71 | * Specify the status of current step
72 | */
73 | status?: "wait" | "process" | "finish" | "error";
74 | /**
75 | * Type of steps
76 | */
77 | type?: "default" | "navigation";
78 | } & DashComponentProps &
79 | StyledComponentProps;
80 |
81 | /**
82 | * Steps
83 | */
84 | const Steps = (props: Props) => {
85 | const { children, label_placement, progress_dot, setProps, ...otherProps } =
86 | props;
87 |
88 | const onChange = (current: number) => {
89 | if (setProps) {
90 | setProps({ current });
91 | }
92 | };
93 |
94 | return (
95 |
101 | {children}
102 |
103 | );
104 | };
105 |
106 | Steps.defaultProps = {};
107 |
108 | export default Steps;
109 |
--------------------------------------------------------------------------------
/src/ts/components/datepicker/TimeRangePicker.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { TimePicker, TimeRangePickerProps } from "antd";
4 | import dayjs from "dayjs";
5 |
6 | const { RangePicker: AntRangePicker } = TimePicker;
7 |
8 | type Props = {
9 | /**
10 | * If allow to remove input content with clear icon
11 | */
12 | allow_clear?: boolean;
13 | /**
14 | * Whether has border style
15 | */
16 | bordered?: boolean;
17 | /**
18 | * Disables all checkboxes within the group
19 | */
20 | disabled?: boolean;
21 | /**
22 | * The open state of picker
23 | */
24 | open?: boolean;
25 | /**
26 | * The position where the selection box pops up
27 | */
28 | placement?: "bottomLeft" | "bottomRight" | "topLeft" | "topRight";
29 | /**
30 | * Whether to show 'Now' button on panel when show_time is set
31 | */
32 | show_now?: boolean;
33 | /**
34 | * To determine the size of the input box, the height of large and small,
35 | * are 40px and 24px respectively, while default size is 32px
36 | */
37 | size?: "large" | "middle" | "small";
38 | /**
39 | * Set validation status
40 | */
41 | status?: "error" | "warning";
42 | /**
43 | * The selected start date / datetime
44 | */
45 | start?: string;
46 | /**
47 | * The selected start date / datetime
48 | */
49 | end?: string;
50 | } & DashComponentProps &
51 | StyledComponentProps;
52 |
53 | /**
54 | * Select Date or DateTime
55 | */
56 | const TimeRangePicker = (props: Props) => {
57 | const {
58 | allow_clear,
59 | disabled,
60 | start,
61 | end,
62 | show_now,
63 | setProps,
64 | ...otherProps
65 | } = props;
66 |
67 | const onOpenChange: TimeRangePickerProps["onOpenChange"] = useCallback(
68 | (open) => {
69 | if (!disabled && setProps) {
70 | setProps({ open });
71 | }
72 | },
73 | [setProps, disabled]
74 | );
75 |
76 | const onChange: TimeRangePickerProps["onChange"] = useCallback(
77 | (dates) => {
78 | if (!disabled && setProps) {
79 | const [startDate, endDate] = dates;
80 | setProps({
81 | start: startDate.toISOString(),
82 | end: endDate.toISOString(),
83 | });
84 | }
85 | },
86 | [setProps, disabled]
87 | );
88 |
89 | return (
90 |
98 | );
99 | };
100 |
101 | TimeRangePicker.defaultProps = {};
102 |
103 | export default TimeRangePicker;
104 |
--------------------------------------------------------------------------------
/src/ts/components/datepicker/DatePicker.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { DatePicker as AntDatePicker, DatePickerProps } from "antd";
4 | import dayjs from "dayjs";
5 |
6 | type Props = {
7 | /**
8 | * If allow to remove input content with clear icon
9 | */
10 | allow_clear?: boolean;
11 | /**
12 | * Whether has border style
13 | */
14 | bordered?: boolean;
15 | /**
16 | * Disables all checkboxes within the group
17 | */
18 | disabled?: boolean;
19 | /**
20 | * The open state of picker
21 | */
22 | open?: boolean;
23 | /**
24 | * Set picker type
25 | */
26 | picker?: "date" | "week" | "month" | "quarter" | "year";
27 | /**
28 | * The placeholder of date input
29 | */
30 | placeholder?: string;
31 | /**
32 | * The position where the selection box pops up
33 | */
34 | placement?: "bottomLeft" | "bottomRight" | "topLeft" | "topRight";
35 | /**
36 | * Whether to provide an additional time selection
37 | */
38 | show_time?: boolean;
39 | /**
40 | * Whether to show 'Now' button on panel when show_time is set
41 | */
42 | show_now?: boolean;
43 | /**
44 | * To determine the size of the input box, the height of large and small,
45 | * are 40px and 24px respectively, while default size is 32px
46 | */
47 | size?: "large" | "middle" | "small";
48 | /**
49 | * Set validation status
50 | */
51 | status?: "error" | "warning";
52 | /**
53 | * The selected date / datetime
54 | */
55 | value?: string;
56 | } & DashComponentProps &
57 | StyledComponentProps;
58 |
59 | /**
60 | * Select Date or DateTime
61 | */
62 | const DatePicker = (props: Props) => {
63 | const {
64 | allow_clear,
65 | disabled,
66 | picker,
67 | value,
68 | show_time,
69 | show_now,
70 | setProps,
71 | ...otherProps
72 | } = props;
73 |
74 | const onOpenChange: DatePickerProps["onOpenChange"] = useCallback(
75 | (open) => {
76 | if (!disabled && setProps) {
77 | setProps({ open });
78 | }
79 | },
80 | [setProps, disabled]
81 | );
82 |
83 | const onChange: DatePickerProps["onChange"] = useCallback(
84 | (date) => {
85 | if (!disabled && setProps) {
86 | setProps({ value: date.toISOString() });
87 | }
88 | },
89 | [setProps, disabled]
90 | );
91 |
92 | return (
93 |
103 | );
104 | };
105 |
106 | DatePicker.defaultProps = {};
107 |
108 | export default DatePicker;
109 |
--------------------------------------------------------------------------------
/src/ts/components/datepicker/DateRangePicker.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { DatePicker as AntDatePicker, TimeRangePickerProps } from "antd";
4 | import dayjs from "dayjs";
5 |
6 | const { RangePicker: AntRangePicker } = AntDatePicker;
7 |
8 | type Props = {
9 | /**
10 | * If allow to remove input content with clear icon
11 | */
12 | allow_clear?: boolean;
13 | /**
14 | * Whether has border style
15 | */
16 | bordered?: boolean;
17 | /**
18 | * Disables all checkboxes within the group
19 | */
20 | disabled?: boolean;
21 | /**
22 | * The open state of picker
23 | */
24 | open?: boolean;
25 | /**
26 | * The position where the selection box pops up
27 | */
28 | placement?: "bottomLeft" | "bottomRight" | "topLeft" | "topRight";
29 | /**
30 | * Whether to provide an additional time selection
31 | */
32 | show_time?: boolean;
33 | /**
34 | * Whether to show 'Now' button on panel when show_time is set
35 | */
36 | show_now?: boolean;
37 | /**
38 | * To determine the size of the input box, the height of large and small,
39 | * are 40px and 24px respectively, while default size is 32px
40 | */
41 | size?: "large" | "middle" | "small";
42 | /**
43 | * Set validation status
44 | */
45 | status?: "error" | "warning";
46 | /**
47 | * The selected start date / datetime
48 | */
49 | start?: string;
50 | /**
51 | * The selected end date / datetime
52 | */
53 | end?: string;
54 | } & DashComponentProps &
55 | StyledComponentProps;
56 |
57 | /**
58 | * Select Date or DateTime
59 | */
60 | const DateRangePicker = (props: Props) => {
61 | const {
62 | allow_clear,
63 | disabled,
64 | start,
65 | end,
66 | show_time,
67 | show_now,
68 | setProps,
69 | ...otherProps
70 | } = props;
71 |
72 | const onOpenChange: TimeRangePickerProps["onOpenChange"] = useCallback(
73 | (open) => {
74 | if (!disabled && setProps) {
75 | setProps({ open });
76 | }
77 | },
78 | [setProps, disabled]
79 | );
80 |
81 | const onChange: TimeRangePickerProps["onChange"] = useCallback(
82 | (dates) => {
83 | if (!disabled && setProps) {
84 | const [startDate, endDate] = dates;
85 | setProps({
86 | start: startDate.toISOString(),
87 | end: endDate.toISOString(),
88 | });
89 | }
90 | },
91 | [setProps, disabled]
92 | );
93 |
94 | return (
95 |
104 | );
105 | };
106 |
107 | DateRangePicker.defaultProps = {};
108 |
109 | export default DateRangePicker;
110 |
--------------------------------------------------------------------------------
/src/ts/index.ts:
--------------------------------------------------------------------------------
1 | import Alert from "./components/alert/Alert";
2 | import AutoComplete from "./components/autocomplete/AutoComplete";
3 | import Button from "./components/button/Button";
4 | import Card from "./components/card/Card";
5 | import Checkbox from "./components/checkbox/Checkbox";
6 | import CheckboxGroup from "./components/checkbox/CheckboxGroup";
7 | import Col from "./components/layout/Col";
8 | import ConfigProvider from "./components/ConfigProvider";
9 | import Layout from "./components/layout/Layout";
10 | import Header from "./components/layout/Header";
11 | import Sidebar from "./components/layout/Sidebar";
12 | import Content from "./components/layout/Content";
13 | import Footer from "./components/layout/Footer";
14 | import Row from "./components/layout/Row";
15 | import Menu from "./components/menu/Menu";
16 | import MenuItem from "./components/menu/MenuItem";
17 | import Input from "./components/input/Input";
18 | import InputNumber from "./components/input/InputNumber";
19 | import TextArea from "./components/input/TextArea";
20 | import DropdownMenu from "./components/dropdown/DropdownMenu";
21 | import DropdownButton from "./components/dropdown/DropdownButton";
22 | import DatePicker from "./components/datepicker/DatePicker";
23 | import TimePicker from "./components/datepicker/TimePicker";
24 | import DateRangePicker from "./components/datepicker/DateRangePicker";
25 | import TimeRangePicker from "./components/datepicker/TimeRangePicker";
26 | import Divider from "./components/divider/Divider";
27 | import Tag from "./components/tag/Tag";
28 | import CheckableTag from "./components/tag/CheckableTag";
29 | import Icon from "./components/icon/Icon";
30 | import Tabs from "./components/tabs/Tabs";
31 | import TabPane from "./components/tabs/TabPane";
32 | import Steps from "./components/steps/Steps";
33 | import Radio from "./components/radio/Radio";
34 | import RadioGroup from "./components/radio/RadioGroup";
35 | import RadioButton from "./components/radio/RadioButton";
36 | import Segmented from "./components/Segmented";
37 | import Select from "./components/select/Select";
38 | import Space from "./components/Space";
39 | import Slider from "./components/Slider";
40 | import Switch from "./components/Switch";
41 | import Timeline from "./components/timeline/Timeline";
42 | import TimelineItem from "./components/timeline/TimelineItem";
43 | import PagesWithSidebar from "./components/templates/PagesWithSidebar";
44 | import Page from "./components/templates/Page";
45 |
46 | export {
47 | Alert,
48 | AutoComplete,
49 | Button,
50 | Card,
51 | Checkbox,
52 | CheckboxGroup,
53 | Col,
54 | ConfigProvider,
55 | DatePicker,
56 | DateRangePicker,
57 | TimePicker,
58 | TimeRangePicker,
59 | Divider,
60 | DropdownButton,
61 | DropdownMenu,
62 | Icon,
63 | Input,
64 | InputNumber,
65 | TextArea,
66 | Content,
67 | Footer,
68 | Header,
69 | Layout,
70 | Sidebar,
71 | Menu,
72 | MenuItem,
73 | Radio,
74 | RadioButton,
75 | RadioGroup,
76 | Select,
77 | Space,
78 | Slider,
79 | Steps,
80 | Switch,
81 | TabPane,
82 | Tabs,
83 | CheckableTag,
84 | Tag,
85 | Row,
86 | Segmented,
87 | Timeline,
88 | TimelineItem,
89 | PagesWithSidebar,
90 | Page,
91 | };
92 |
--------------------------------------------------------------------------------
/src/ts/components/autocomplete/__tests__/AutoComplete.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "@testing-library/react";
3 | import userEvent from "@testing-library/user-event";
4 | import AutoComplete from "../AutoComplete";
5 |
6 | describe("AutoComplete", () => {
7 | test("has default value", () => {
8 | const autocomplete = render(
9 |
14 | );
15 |
16 | expect(
17 | autocomplete.container.querySelector("#autocomplete_test")
18 | ).toHaveValue("default");
19 | });
20 |
21 | test("on events", async () => {
22 | const user = userEvent.setup();
23 | const mockSetProps = jest.fn();
24 |
25 | const autocomplete = render(
26 |
35 | );
36 | const autocompleteElement =
37 | autocomplete.container.querySelector("#autocomplete_test");
38 | expect(autocompleteElement).not.toHaveValue();
39 |
40 | const input = render();
41 | const inputElement = input.container.querySelector("#other_input");
42 |
43 | // test onFocus, onChange, onSelect
44 | await user.click(autocompleteElement);
45 | await user.type(autocompleteElement, "opt_");
46 |
47 | const firstOptionElement = document.querySelector(
48 | ".ant-select-item-option-content"
49 | );
50 | await user.click(firstOptionElement);
51 |
52 | expect(autocompleteElement).toHaveValue("opt_1");
53 |
54 | expect(
55 | mockSetProps.mock.calls.filter((x) =>
56 | Object.keys(x[0]).includes("n_onFocus")
57 | )[0][0].n_onFocus
58 | ).toEqual(1);
59 | expect(
60 | mockSetProps.mock.calls.filter((x) =>
61 | Object.keys(x[0]).includes("n_onSelect")
62 | )
63 | ).toHaveLength(1);
64 | expect(
65 | mockSetProps.mock.calls.filter((x) =>
66 | Object.keys(x[0]).includes("n_onSelect")
67 | )[0][0].value
68 | ).toEqual("opt_1");
69 | expect(
70 | mockSetProps.mock.calls.filter((x) =>
71 | Object.keys(x[0]).includes("n_onChange")
72 | )
73 | ).toHaveLength(5); // typed 4 chars "opt_" + selected once
74 |
75 | // test clear
76 | const clearElement =
77 | autocomplete.container.querySelector(".ant-select-clear");
78 | await user.click(clearElement);
79 | expect(autocompleteElement).not.toHaveValue();
80 |
81 | // test onBlur
82 | await user.click(inputElement);
83 | expect(
84 | mockSetProps.mock.calls.filter((x) =>
85 | Object.keys(x[0]).includes("n_onBlur")
86 | )
87 | ).toHaveLength(1);
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/src/ts/components/datepicker/TimePicker.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { TimePicker as AntTimePicker, TimePickerProps } from "antd";
4 | import dayjs from "dayjs";
5 |
6 | type Props = {
7 | /**
8 | * If allow to remove input content with clear icon
9 | */
10 | allow_clear?: boolean;
11 | /**
12 | * Whether has border style
13 | */
14 | bordered?: boolean;
15 | /**
16 | * Disables all checkboxes within the group
17 | */
18 | disabled?: boolean;
19 | /**
20 | * Time format - e.g. HH:mm:ss
21 | */
22 | format?: string;
23 | /**
24 | * Interval between hours in picker
25 | */
26 | hour_step?: number;
27 | /**
28 | * Interval between minutes in picker
29 | */
30 | minute_step?: number;
31 | /**
32 | * The open state of picker
33 | */
34 | open?: boolean;
35 | /**
36 | * Set picker type
37 | */
38 | picker?: "date" | "week" | "month" | "quarter" | "year";
39 | /**
40 | * The placeholder of date input
41 | */
42 | placeholder?: string;
43 | /**
44 | * The position where the selection box pops up
45 | */
46 | placement?: "bottomLeft" | "bottomRight" | "topLeft" | "topRight";
47 | /**
48 | * Interval between seconds in picker
49 | */
50 | second_step?: number;
51 | /**
52 | * Whether to show 'Now' button on panel when show_time is set
53 | */
54 | show_now?: boolean;
55 | /**
56 | * To determine the size of the input box, the height of large and small,
57 | * are 40px and 24px respectively, while default size is 32px
58 | */
59 | size?: "large" | "middle" | "small";
60 | /**
61 | * Set validation status
62 | */
63 | status?: "error" | "warning";
64 | /**
65 | * The selected time as ISO string (YYYY-MM-DDTHH:MM:SSZ)
66 | */
67 | value?: string;
68 | } & DashComponentProps &
69 | StyledComponentProps;
70 |
71 | /**
72 | * Select Date or DateTime
73 | */
74 | const TimePicker = (props: Props) => {
75 | const {
76 | allow_clear,
77 | disabled,
78 | value,
79 | hour_step,
80 | minute_step,
81 | second_step,
82 | show_now,
83 | setProps,
84 | ...otherProps
85 | } = props;
86 |
87 | const onOpenChange: TimePickerProps["onOpenChange"] = useCallback(
88 | (open) => {
89 | if (!disabled && setProps) {
90 | setProps({ open });
91 | }
92 | },
93 | [setProps, disabled]
94 | );
95 |
96 | const onChange: TimePickerProps["onChange"] = useCallback(
97 | (value) => {
98 | if (!disabled && setProps) {
99 | setProps({ value: value.toISOString() });
100 | }
101 | },
102 | [setProps, disabled]
103 | );
104 |
105 | return (
106 |
117 | );
118 | };
119 |
120 | TimePicker.defaultProps = {};
121 |
122 | export default TimePicker;
123 |
--------------------------------------------------------------------------------
/src/ts/types.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | /**
4 | * Every Dash components are given these props.
5 | * Use with your own props:
6 | * ```
7 | * type Props = {
8 | * my_prop: string;
9 | * } & DashComponentProps;
10 | * ```
11 | * Recommended to use `type` instead of `interface` so you can define the
12 | * order of props with types concatenation.
13 | */
14 | export type DashComponentProps = {
15 | /**
16 | * Unique ID to identify this component in Dash callbacks.
17 | */
18 | id?: string;
19 | /**
20 | * A unique identifier for the component, used to improve
21 | * performance by React.js while rendering components
22 | * See https://reactjs.org/docs/lists-and-keys.html for more info
23 | */
24 | key?: string;
25 | /**
26 | * Update props to trigger callbacks.
27 | */
28 | // eslint-disable-next-line
29 | setProps?: (props: Record) => void;
30 | };
31 |
32 | /**
33 | * Components that can be styles from Dash via `styles` or `class_name` props.
34 | */
35 | export type StyledComponentProps = {
36 | /**
37 | * Defines CSS styles which will override styles previously set.
38 | */
39 | style?: object;
40 | /**
41 | * Often used with CSS to style elements with common properties.
42 | */
43 | class_name?: string;
44 | };
45 |
46 | /**
47 | * Object that holds the loading state object coming from dash-renderer
48 | */
49 | export type DashLoadingState = {
50 | /**
51 | * Determines if the component is loading or not
52 | */
53 | is_loading: boolean;
54 | /**
55 | * Holds which property is loading
56 | */
57 | prop_name: string;
58 | /**
59 | * Holds the name of the component that is loading
60 | */
61 | component_name: string;
62 | };
63 |
64 | export type MenuItem = {
65 | /**
66 | * Display the danger style
67 | */
68 | danger?: boolean;
69 | /**
70 | * Whether menu item is disabled
71 | */
72 | disabled?: boolean;
73 | /**
74 | * The icon of the menu item
75 | */
76 | icon?: string;
77 | /**
78 | * Unique ID of the menu item
79 | */
80 | key: string;
81 | /**
82 | * Menu label
83 | */
84 | label: string;
85 | /**
86 | * Set display title for collapsed item
87 | */
88 | title?: string;
89 | };
90 |
91 | export type MenuItemGroup = {
92 | type: "group";
93 | /**
94 | * Sub-menu items
95 | */
96 | children: MenuItem[];
97 | /**
98 | * The title of the group
99 | */
100 | label: ReactNode;
101 | };
102 |
103 | export type SubMenu = {
104 | /**
105 | * Sub-menus or sub-menu items
106 | */
107 | children: ItemType[];
108 | /**
109 | * Whether menu item is disabled
110 | */
111 | disabled?: boolean;
112 | /**
113 | * The icon of the menu item
114 | */
115 | icon?: string;
116 | /**
117 | * Unique ID of the menu item
118 | */
119 | key: string;
120 | /**
121 | * Menu label
122 | */
123 | label: string;
124 | /**
125 | * Set display title for collapsed item
126 | */
127 | title?: string;
128 | /**
129 | * Color theme of the SubMenu (inherits from Menu by default)
130 | */
131 | theme?: "light" | "dark";
132 | /**
133 | * Sub-menu class name, not working when mode="inline"
134 | */
135 | popupClassName?: string;
136 | /**
137 | * Sub-menu offset, not working when mode="inline"
138 | */
139 | popupOffset?: [number, number];
140 | };
141 |
142 | export type MenuDivider = {
143 | type: "divider";
144 | };
145 |
146 | export type ItemType = MenuItem | MenuItemGroup | SubMenu | MenuDivider;
147 |
148 | export type BreadcrumbRoute = {
149 | path: string;
150 | breadcrumbName: string;
151 | children: Array<{
152 | path: string;
153 | breadcrumbName: string;
154 | }>;
155 | };
156 |
--------------------------------------------------------------------------------
/src/ts/components/tabs/Tabs.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useMemo } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { Tabs as AntTabs } from "antd";
4 | import Icon from "../icon/Icon";
5 | import {
6 | parseChildrenToArray,
7 | getComponentType,
8 | getComponentProps,
9 | } from "../../utilities";
10 | import { Props as TabPaneProps } from "./TabPane";
11 |
12 | const { TabPane: AntTabPane } = AntTabs;
13 |
14 | type Props = {
15 | /**
16 | * The children of this component.
17 | */
18 | children?: ReactNode;
19 | /**
20 | * Current TabPane's key
21 | */
22 | value?: string;
23 | /**
24 | * Customize add icon
25 | */
26 | add_icon?: string;
27 | /**
28 | * Whether to change tabs with animation. Only works while tabPosition="top"
29 | */
30 | animated?: boolean;
31 | /**
32 | * Centers tabs
33 | */
34 | centered?: boolean;
35 | /**
36 | * Hide plus icon or not. Only works while type="editable-card"
37 | */
38 | hide_add?: boolean;
39 | /**
40 | * The custom icon of ellipsis
41 | */
42 | more_icon?: string;
43 | /**
44 | * Preset tab bar size
45 | */
46 | size?: "large" | "middle" | "small";
47 | /**
48 | * The gap between tabs
49 | */
50 | tab_bar_gutter?: number;
51 | /**
52 | * Tab bar style object
53 | */
54 | tab_bar_style?: object;
55 | /**
56 | * Position of tabs
57 | */
58 | tab_position?: "top" | "right" | "bottom" | "left";
59 | /**
60 | * Whether destroy inactive TabPane when tab is changed
61 | */
62 | destroy_inactive_tab_pane?: boolean;
63 | /**
64 | * Basic style of tabs
65 | */
66 | type?: "line" | "card" | "editable-card";
67 | } & DashComponentProps &
68 | StyledComponentProps;
69 |
70 | /**
71 | * Tabs
72 | */
73 | const Tabs = (props: Props) => {
74 | const {
75 | children,
76 | value,
77 | class_name,
78 | add_icon,
79 | hide_add,
80 | more_icon,
81 | tab_bar_gutter,
82 | tab_bar_style,
83 | tab_position,
84 | destroy_inactive_tab_pane,
85 | setProps,
86 | ...otherProps
87 | } = props;
88 |
89 | const onChange = (activeKey: string) => {
90 | if (setProps) {
91 | setProps({ value: activeKey });
92 | }
93 | };
94 |
95 | const tabItems = useMemo(
96 | () =>
97 | parseChildrenToArray(children)
98 | .filter((c) => getComponentType(c) === "TabPane")
99 | .map((c) => {
100 | const tabProps = getComponentProps(c) as TabPaneProps;
101 | return (
102 |
113 | {c}
114 |
115 | );
116 | }),
117 | [children]
118 | );
119 |
120 | return (
121 |
134 | {tabItems}
135 |
136 | );
137 | };
138 |
139 | Tabs.defaultProps = {};
140 |
141 | export default Tabs;
142 |
--------------------------------------------------------------------------------
/src/ts/components/templates/PagesWithSidebar.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | ReactNode,
3 | useMemo,
4 | useState,
5 | useCallback,
6 | useEffect,
7 | } from "react";
8 | import { Layout, Menu, Divider, MenuProps } from "antd";
9 | import {
10 | DashComponentProps,
11 | DashLoadingState,
12 | StyledComponentProps,
13 | } from "../../types";
14 | import {
15 | parseChildrenToArray,
16 | getComponentType,
17 | getComponentProps,
18 | } from "../../utilities";
19 |
20 | const { Sider, Content } = Layout;
21 |
22 | type Context = {
23 | selectedKey: string;
24 | cbControls: (node: ReactNode) => void;
25 | };
26 |
27 | export const PagesContext = React.createContext({
28 | selectedKey: "",
29 | cbControls: undefined,
30 | });
31 |
32 | const default_sidebar_style = {
33 | overflow: "auto",
34 | height: "100vh",
35 | position: "fixed",
36 | left: 0,
37 | top: 0,
38 | bottom: 0,
39 | borderInlineEnd: "1px solid rgba(253, 253, 253, 0.12)",
40 | overflowX: "hidden",
41 | };
42 | const default_content_style = { margin: 0, padding: 0, minHeight: "100vh" };
43 |
44 | type Props = {
45 | /**
46 | * Children should exclusively be Page components
47 | */
48 | children?: ReactNode;
49 | /**
50 | * Currently active page key
51 | */
52 | selected_key?: string;
53 | content_style?: React.CSSProperties;
54 | sidebar_style?: React.CSSProperties;
55 | sidebar_width?: string | number;
56 | menu_theme: "dark" | "light";
57 | /**
58 | * Object that holds the loading state object coming from dash-render+er
59 | */
60 | loading_state?: DashLoadingState;
61 | } & DashComponentProps &
62 | StyledComponentProps;
63 |
64 | /**
65 | * A templated layout for a sidebar with multiple pages
66 | */
67 | const PagesWithSidebar = (props: Props) => {
68 | const {
69 | children,
70 | selected_key,
71 | sidebar_width,
72 | menu_theme,
73 | sidebar_style,
74 | content_style,
75 | setProps,
76 | } = props;
77 | const [controls, setControls] = useState(undefined);
78 |
79 | const handleSelect: MenuProps["onSelect"] = useCallback(
80 | ({ key: selected_key }) => {
81 | setProps({ selected_key });
82 | },
83 | [setProps]
84 | );
85 |
86 | const options = useMemo(
87 | () =>
88 | parseChildrenToArray(children)
89 | .filter((c) => getComponentType(c) === "Page")
90 | .map((c) => {
91 | const componentProps = getComponentProps(c);
92 | return {
93 | label: componentProps.page_key as string,
94 | key: componentProps.page_key as string,
95 | };
96 | }),
97 | [children]
98 | );
99 |
100 | const eff_sidebar_style = useMemo(
101 | () => ({ ...default_sidebar_style, ...sidebar_style }),
102 | [sidebar_style]
103 | );
104 |
105 | const eff_content_style = useMemo(
106 | () => ({
107 | ...default_content_style,
108 | ...content_style,
109 | }),
110 | [content_style]
111 | );
112 |
113 | useEffect(() => {
114 | if (!selected_key) {
115 | setProps({ selected_key: options[0].key });
116 | }
117 | }, [selected_key, setProps, options]);
118 |
119 | return (
120 |
123 |
124 |
130 | {options.length > 1 && (
131 |
139 | )}
140 | {options.length > 1 && }
141 | {controls}
142 |
143 |
151 | {children}
152 |
153 |
154 |
155 | );
156 | };
157 |
158 | PagesWithSidebar.defaultProps = { menu_theme: "dark" };
159 |
160 | export default PagesWithSidebar;
161 |
--------------------------------------------------------------------------------
/src/ts/components/input/TextArea.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import {
3 | DashComponentProps,
4 | DashLoadingState,
5 | StyledComponentProps,
6 | } from "../../types";
7 | import { omit } from "ramda";
8 | import { Input } from "antd";
9 |
10 | const { TextArea: AntTextArea } = Input;
11 |
12 | type Props = {
13 | /**
14 | * The input content value
15 | */
16 | value?: string;
17 | /**
18 | * If allow to remove input content with clear icon
19 | */
20 | allow_clear?: boolean;
21 | /**
22 | * Whether has border style
23 | */
24 | bordered?: boolean;
25 | /**
26 | * Defines the number of columns in a textarea.
27 | */
28 | cols?: number;
29 | /**
30 | * The max length
31 | */
32 | max_length?: number;
33 | /**
34 | * Whether show text count
35 | */
36 | show_count?: boolean;
37 | /**
38 | * Indicates whether the element can be edited.
39 | */
40 | readonly?: boolean;
41 | /**
42 | * Defines the number of rows in a text area.
43 | */
44 | rows?: number;
45 | /**
46 | * A hint to the user of what can be entered in the control.
47 | */
48 | placeholder?: string;
49 | /**
50 | * Object that holds the loading state object coming from dash-renderer
51 | */
52 | loading_state?: DashLoadingState;
53 | /**
54 | * Number of times the input lost focus.
55 | */
56 | n_blur: number;
57 | /**
58 | * Last time the input lost focus.
59 | */
60 | n_blur_timestamp: number;
61 | /**
62 | * Number of times the `Enter` key was pressed while the textarea had focus.
63 | */
64 | n_submit: number;
65 | /**
66 | * Last time that `Enter` was pressed.
67 | */
68 | n_submit_timestamp: number;
69 | /**
70 | * An integer that represents the number of times
71 | * that this element has been clicked on.
72 | */
73 | n_clicks: number;
74 | /**
75 | * If true, changes to input will be sent back to the Dash server only on enter or when losing focus.
76 | * If it's false, it will sent the value back on every change.
77 | */
78 | debounce: boolean;
79 | } & DashComponentProps &
80 | StyledComponentProps;
81 |
82 | /**
83 | * TextArea component.
84 | */
85 | const TextArea = (props: Props) => {
86 | const {
87 | value,
88 | debounce,
89 | loading_state,
90 | n_blur,
91 | n_clicks,
92 | n_submit,
93 | class_name,
94 | setProps,
95 | ...otherProps
96 | } = props;
97 | const [valueState, setValueState] = useState(value || "");
98 |
99 | useEffect(() => {
100 | if (value !== valueState) {
101 | setValueState(value || "");
102 | }
103 | // ignore b/c we may not add valueState to have debounce work
104 | // eslint-disable-next-line
105 | }, [value]);
106 |
107 | const onChange = (e) => {
108 | const newValue = e.target.value;
109 | setValueState(newValue);
110 | if (!debounce && setProps) {
111 | setProps({ value: newValue });
112 | }
113 | };
114 |
115 | const onBlur = () => {
116 | if (setProps) {
117 | const payload = {
118 | n_blur: n_blur + 1,
119 | n_blur_timestamp: Date.now(),
120 | };
121 | if (debounce) {
122 | // @ts-expect-error the value fields does in fact make sense
123 | payload.value = value;
124 | }
125 | setProps(payload);
126 | }
127 | };
128 |
129 | const onKeyPress = (e) => {
130 | if (setProps && e.key === "Enter") {
131 | const payload = {
132 | n_submit: n_submit + 1,
133 | n_submit_timestamp: Date.now(),
134 | };
135 | if (debounce) {
136 | // @ts-expect-error the value fields does in fact make sense
137 | payload.value = value;
138 | }
139 | setProps(payload);
140 | }
141 | };
142 |
143 | const onClick = () => {
144 | if (setProps) {
145 | setProps({
146 | n_clicks: n_clicks + 1,
147 | n_clicks_timestamp: Date.now(),
148 | });
149 | }
150 | };
151 | return (
152 |
164 | );
165 | };
166 |
167 | TextArea.defaultProps = {
168 | n_blur: 0,
169 | n_blur_timestamp: -1,
170 | n_submit: 0,
171 | n_submit_timestamp: -1,
172 | n_clicks: 0,
173 | n_clicks_timestamp: -1,
174 | debounce: false,
175 | value: "",
176 | };
177 |
178 | export default TextArea;
179 |
--------------------------------------------------------------------------------
/src/ts/components/input/__tests__/TextArea.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render, fireEvent } from "@testing-library/react";
3 | import userEvent from "@testing-library/user-event";
4 | import Textarea from "../TextArea";
5 |
6 | describe("Textarea", () => {
7 | describe("setProps", () => {
8 | let textarea, mockSetProps;
9 |
10 | beforeEach(() => {
11 | mockSetProps = jest.fn();
12 | const { container } = render();
13 | textarea = container.firstChild;
14 | });
15 |
16 | test('tracks changes with "value" prop', () => {
17 | fireEvent.change(textarea, {
18 | target: { value: "some-textarea-value" },
19 | });
20 | expect(mockSetProps.mock.calls).toHaveLength(1);
21 | expect(mockSetProps.mock.calls[0][0]).toEqual({
22 | value: "some-textarea-value",
23 | });
24 | expect(textarea).toHaveValue("some-textarea-value");
25 | });
26 |
27 | test("dispatches update for each typed character", async () => {
28 | await userEvent.type(textarea, "abc");
29 |
30 | expect(textarea).toHaveValue("abc");
31 | expect(mockSetProps.mock.calls).toHaveLength(4);
32 | // eslint-disable-next-line
33 | const [call0, call1, call2, call3] = mockSetProps.mock.calls;
34 | expect(call1).toEqual([{ value: "a" }]);
35 | expect(call2).toEqual([{ value: "ab" }]);
36 | expect(call3).toEqual([{ value: "abc" }]);
37 | });
38 |
39 | test('track number of blurs with "n_blur" and "n_blur_timestamp"', () => {
40 | const before = Date.now();
41 | fireEvent.blur(textarea);
42 | const after = Date.now();
43 |
44 | expect(mockSetProps.mock.calls).toHaveLength(1);
45 |
46 | const [[{ n_blur, n_blur_timestamp }]] = mockSetProps.mock.calls;
47 | expect(n_blur).toEqual(1);
48 | expect(n_blur_timestamp).toBeGreaterThanOrEqual(before);
49 | expect(n_blur_timestamp).toBeLessThanOrEqual(after);
50 | });
51 |
52 | test('tracks submit with "n_submit" and "n_submit_timestamp"', () => {
53 | const before = Date.now();
54 | fireEvent.keyPress(textarea, {
55 | key: "Enter",
56 | code: 13,
57 | charCode: 13,
58 | });
59 | const after = Date.now();
60 |
61 | expect(mockSetProps.mock.calls).toHaveLength(1);
62 |
63 | const [[{ n_submit, n_submit_timestamp }]] =
64 | mockSetProps.mock.calls;
65 | expect(n_submit).toEqual(1);
66 | expect(n_submit_timestamp).toBeGreaterThanOrEqual(before);
67 | expect(n_submit_timestamp).toBeLessThanOrEqual(after);
68 | });
69 |
70 | test("don't increment n_submit if key is not Enter", () => {
71 | fireEvent.keyPress(textarea, { key: "a", code: 65, charCode: 65 });
72 | expect(mockSetProps.mock.calls).toHaveLength(0);
73 | });
74 |
75 | describe("debounce", () => {
76 | let textarea, mockSetProps;
77 | beforeEach(() => {
78 | mockSetProps = jest.fn();
79 | const { container } = render(
80 |
85 | );
86 | textarea = container.firstChild;
87 | });
88 |
89 | test("don't call setProps on change if debounce is true", () => {
90 | fireEvent.change(textarea, {
91 | target: { value: "some-textarea-value" },
92 | });
93 | expect(mockSetProps.mock.calls).toHaveLength(0);
94 | expect(textarea).toHaveValue("some-textarea-value");
95 | });
96 |
97 | test("dispatch value on blur if debounce is true", () => {
98 | const before = Date.now();
99 | fireEvent.blur(textarea);
100 | const after = Date.now();
101 |
102 | expect(mockSetProps.mock.calls).toHaveLength(1);
103 |
104 | const [[{ n_blur, n_blur_timestamp, value }]] =
105 | mockSetProps.mock.calls;
106 | expect(n_blur).toEqual(1);
107 | expect(n_blur_timestamp).toBeGreaterThanOrEqual(before);
108 | expect(n_blur_timestamp).toBeLessThanOrEqual(after);
109 | expect(value).toEqual("some-textarea-value");
110 | });
111 |
112 | test("dispatch value on submit if debounce is true", () => {
113 | const before = Date.now();
114 | fireEvent.keyPress(textarea, {
115 | key: "Enter",
116 | code: 13,
117 | charCode: 13,
118 | });
119 | const after = Date.now();
120 |
121 | expect(mockSetProps.mock.calls).toHaveLength(1);
122 |
123 | const [[{ n_submit, n_submit_timestamp, value }]] =
124 | mockSetProps.mock.calls;
125 | expect(n_submit).toEqual(1);
126 | expect(n_submit_timestamp).toBeGreaterThanOrEqual(before);
127 | expect(n_submit_timestamp).toBeLessThanOrEqual(after);
128 | expect(value).toEqual("some-textarea-value");
129 | });
130 | });
131 | });
132 | });
133 |
--------------------------------------------------------------------------------
/src/ts/components/menu/Menu.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useCallback } from "react";
2 | import {
3 | DashComponentProps,
4 | ItemType,
5 | StyledComponentProps,
6 | } from "../../types";
7 | import { Menu as AntMenu, MenuProps } from "antd";
8 | import Icon from "../icon/Icon";
9 |
10 | /*
11 | * event polyfill for IE
12 | * https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent
13 | */
14 | function CustomEvent(event, params) {
15 | // eslint-disable-next-line no-param-reassign
16 | params = params || {
17 | bubbles: false,
18 | cancelable: false,
19 | // eslint-disable-next-line no-undefined
20 | detail: undefined,
21 | };
22 | const evt = document.createEvent("CustomEvent");
23 | evt.initCustomEvent(
24 | event,
25 | params.bubbles,
26 | params.cancelable,
27 | params.detail
28 | );
29 | return evt;
30 | }
31 | CustomEvent.prototype = window.Event.prototype;
32 |
33 | type Props = {
34 | /**
35 | * The children of this component.
36 | */
37 | children?: ReactNode;
38 | /**
39 | * custom expand icon of submenu
40 | */
41 | expand_icon?: ReactNode;
42 | /**
43 | * Render submenu into DOM before it becomes visible
44 | */
45 | force_sub_menu_render?: boolean;
46 | /**
47 | * Specifies the collapsed status when menu is inline mode
48 | */
49 | inline_collapsed?: boolean;
50 | /**
51 | * Indent (in pixels) of inline menu items on each level
52 | */
53 | inline_indent?: number;
54 | /**
55 | * Menu item content
56 | */
57 | items?: ItemType[];
58 | /**
59 | * Type of menu
60 | */
61 | mode?: "vertical" | "horizontal" | "inline";
62 | /**
63 | * Allows selection of multiple items
64 | */
65 | multiple?: boolean;
66 | /**
67 | * Array with the keys of currently opened sub-menus
68 | */
69 | open_keys?: string[];
70 | /**
71 | * Customized the ellipsis icon when menu is collapsed horizontally
72 | */
73 | overflowed_indicator?: ReactNode;
74 | /**
75 | * Allows selecting menu items
76 | */
77 | selectable?: boolean;
78 | /**
79 | * Array with the keys of currently selected menu items
80 | */
81 | selected_keys: string[];
82 | /**
83 | * Delay time to hide submenu when mouse leaves (in seconds)
84 | */
85 | sub_menu_close_delay?: number;
86 | /**
87 | * Delay time to show submenu when mouse enters, (in seconds)
88 | */
89 | sub_menu_open_delay?: number;
90 | /**
91 | * Color theme of the menu
92 | */
93 | theme?: "light" | "dark";
94 | } & DashComponentProps &
95 | StyledComponentProps;
96 |
97 | /**
98 | * A versatile menu for navigation.
99 | */
100 | const Menu = (props: Props) => {
101 | const {
102 | class_name,
103 | children,
104 | expand_icon,
105 | force_sub_menu_render,
106 | inline_collapsed,
107 | inline_indent,
108 | open_keys,
109 | overflowed_indicator,
110 | selected_keys,
111 | sub_menu_close_delay,
112 | sub_menu_open_delay,
113 | multiple,
114 | items,
115 | setProps,
116 | ...otherProps
117 | } = props;
118 |
119 | const mappedItems = items.map((it) => {
120 | if ("icon" in it) {
121 | return {
122 | ...it,
123 | icon: it.icon && Icon({ icon_name: it.icon }),
124 | };
125 | }
126 | return it;
127 | });
128 |
129 | const onSelect: MenuProps["onSelect"] = useCallback(
130 | (e) => {
131 | if (multiple) {
132 | setProps({ selected_keys: [...e.selectedKeys, e.key] });
133 | } else {
134 | // @ts-expect-error if key does not exist, we get undefined, also fine
135 | const curr = items.filter((it) => it.key === e.key);
136 | if (curr.length > 0) {
137 | // @ts-expect-error maybe it does
138 | const path = curr[0].path;
139 | if (typeof path !== "undefined") {
140 | window.history.pushState({}, "", path);
141 | window.dispatchEvent(
142 | CustomEvent("_dashprivate_pushstate", {})
143 | );
144 | window.scrollTo(0, 0);
145 | }
146 | }
147 | setProps({ selected_keys: [e.key] });
148 | }
149 | },
150 | [multiple, items, setProps]
151 | );
152 |
153 | const onDeselect: MenuProps["onDeselect"] = useCallback(
154 | ({ key, selectedKeys }) => {
155 | // no need to check for multiple, since this function only gets call when multiple true.
156 | setProps({
157 | selected_keys: selectedKeys.filter((val) => val !== key),
158 | });
159 | },
160 | [setProps]
161 | );
162 |
163 | const onOpenChange: MenuProps["onOpenChange"] = useCallback(
164 | (openKeys) => {
165 | setProps({ open_keys: openKeys });
166 | },
167 | [setProps]
168 | );
169 |
170 | return (
171 |
188 | {children}
189 |
190 | );
191 | };
192 |
193 | Menu.defaultProps = {
194 | selectable: true,
195 | selected_keys: [],
196 | };
197 |
198 | export default Menu;
199 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Webpack
3 | .build_cache
4 | ### VisualStudioCode template
5 | .vscode/*
6 | !.vscode/settings.json
7 | !.vscode/tasks.json
8 | !.vscode/launch.json
9 | !.vscode/extensions.json
10 | ### JetBrains template
11 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
12 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
13 |
14 | # User-specific stuff
15 | .idea/**/workspace.xml
16 | .idea/**/tasks.xml
17 | .idea/**/usage.statistics.xml
18 | .idea/**/dictionaries
19 | .idea/**/shelf
20 |
21 | # Sensitive or high-churn files
22 | .idea/**/dataSources/
23 | .idea/**/dataSources.ids
24 | .idea/**/dataSources.local.xml
25 | .idea/**/sqlDataSources.xml
26 | .idea/**/dynamic.xml
27 | .idea/**/uiDesigner.xml
28 | .idea/**/dbnavigator.xml
29 |
30 | # Gradle
31 | .idea/**/gradle.xml
32 | .idea/**/libraries
33 |
34 | # Gradle and Maven with auto-import
35 | # When using Gradle or Maven with auto-import, you should exclude module files,
36 | # since they will be recreated, and may cause churn. Uncomment if using
37 | # auto-import.
38 | # .idea/modules.xml
39 | # .idea/*.iml
40 | # .idea/modules
41 |
42 | # CMake
43 | cmake-build-*/
44 |
45 | # Mongo Explorer plugin
46 | .idea/**/mongoSettings.xml
47 |
48 | # File-based project format
49 | *.iws
50 |
51 | # IntelliJ
52 | out/
53 |
54 | # mpeltonen/sbt-idea plugin
55 | .idea_modules/
56 |
57 | # JIRA plugin
58 | atlassian-ide-plugin.xml
59 |
60 | # Cursive Clojure plugin
61 | .idea/replstate.xml
62 |
63 | # Crashlytics plugin (for Android Studio and IntelliJ)
64 | com_crashlytics_export_strings.xml
65 | crashlytics.properties
66 | crashlytics-build.properties
67 | fabric.properties
68 |
69 | # Editor-based Rest Client
70 | .idea/httpRequests
71 | ### Node template
72 | # Logs
73 | logs
74 | *.log
75 | npm-debug.log*
76 | yarn-debug.log*
77 | yarn-error.log*
78 |
79 | # Runtime data
80 | pids
81 | *.pid
82 | *.seed
83 | *.pid.lock
84 |
85 | # Directory for instrumented libs generated by jscoverage/JSCover
86 | lib-cov
87 |
88 | # Coverage directory used by tools like istanbul
89 | coverage
90 |
91 | # nyc test coverage
92 | .nyc_output
93 |
94 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
95 | .grunt
96 |
97 | # Bower dependency directory (https://bower.io/)
98 | bower_components
99 |
100 | # node-waf configuration
101 | .lock-wscript
102 |
103 | # Compiled binary addons (https://nodejs.org/api/addons.html)
104 | build/Release
105 |
106 | # Dependency directories
107 | node_modules/
108 | jspm_packages/
109 |
110 | # TypeScript v1 declaration files
111 | typings/
112 |
113 | # Optional npm cache directory
114 | .npm
115 |
116 | # Optional eslint cache
117 | .eslintcache
118 |
119 | # Optional REPL history
120 | .node_repl_history
121 |
122 | # Output of 'npm pack'
123 | *.tgz
124 |
125 | # Yarn Integrity file
126 | .yarn-integrity
127 |
128 | # dotenv environment variables file
129 | .env
130 |
131 | # parcel-bundler cache (https://parceljs.org/)
132 | .cache
133 |
134 | # next.js build output
135 | .next
136 |
137 | # nuxt.js build output
138 | .nuxt
139 |
140 | # vuepress build output
141 | .vuepress/dist
142 |
143 | # Serverless directories
144 | .serverless
145 | ### Python template
146 | # Byte-compiled / optimized / DLL files
147 | __pycache__/
148 | *.py[cod]
149 | *$py.class
150 |
151 | # C extensions
152 | *.so
153 |
154 | # Distribution / packaging
155 | .Python
156 | build/
157 | develop-eggs/
158 | dist/
159 | downloads/
160 | eggs/
161 | .eggs/
162 | lib64/
163 | parts/
164 | sdist/
165 | var/
166 | wheels/
167 | *.egg-info/
168 | .installed.cfg
169 | *.egg
170 | MANIFEST
171 |
172 | # PyInstaller
173 | # Usually these files are written by a python script from a template
174 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
175 | *.manifest
176 | *.spec
177 |
178 | # Installer logs
179 | pip-log.txt
180 | pip-delete-this-directory.txt
181 |
182 | # Unit test / coverage reports
183 | htmlcov/
184 | .tox/
185 | .coverage
186 | .coverage.*
187 | nosetests.xml
188 | coverage.xml
189 | *.cover
190 | .hypothesis/
191 | .pytest_cache/
192 |
193 | # Translations
194 | *.mo
195 | *.pot
196 |
197 | # Django stuff:
198 | local_settings.py
199 | db.sqlite3
200 |
201 | # Flask stuff:
202 | instance/
203 | .webassets-cache
204 |
205 | # Scrapy stuff:
206 | .scrapy
207 |
208 | # Sphinx documentation
209 | docs/_build/
210 |
211 | # PyBuilder
212 | target/
213 |
214 | # Jupyter Notebook
215 | .ipynb_checkpoints
216 |
217 | # pyenv
218 | .python-version
219 |
220 | # celery beat schedule file
221 | celerybeat-schedule
222 |
223 | # SageMath parsed files
224 | *.sage.py
225 |
226 | # Environments
227 | .venv
228 | env/
229 | venv/
230 | ENV/
231 | env.bak/
232 | venv.bak/
233 |
234 | # Spyder project settings
235 | .spyderproject
236 | .spyproject
237 |
238 | # Rope project settings
239 | .ropeproject
240 |
241 | # mkdocs documentation
242 | /site
243 |
244 | # mypy
245 | .mypy_cache/
246 | ### SublimeText template
247 | # Cache files for Sublime Text
248 | *.tmlanguage.cache
249 | *.tmPreferences.cache
250 | *.stTheme.cache
251 |
252 | # Workspace files are user-specific
253 | *.sublime-workspace
254 |
255 | # Project files should be checked into the repository, unless a significant
256 | # proportion of contributors will probably not be using Sublime Text
257 | # *.sublime-project
258 |
259 | # SFTP configuration file
260 | sftp-config.json
261 |
262 | # Package control specific files
263 | Package Control.last-run
264 | Package Control.ca-list
265 | Package Control.ca-bundle
266 | Package Control.system-ca-bundle
267 | Package Control.cache/
268 | Package Control.ca-certs/
269 | Package Control.merged-ca-bundle
270 | Package Control.user-ca-bundle
271 | oscrypto-ca-bundle.crt
272 | bh_unicode_properties.cache
273 |
274 | # Sublime-github package stores a github token in this file
275 | # https://packagecontrol.io/packages/sublime-github
276 | GitHub.sublime-settings
277 |
278 | # Julia manifest file names
279 | Manifest.toml
280 | JuliaManifest.toml
281 |
282 | # R build artifacts
283 | inst
284 | man
285 | R/*
286 | DESCRIPTION
287 | NAMESPACE
288 | !R/icons.R
289 | !R/themes.R
290 |
291 | # Julia build artifacts
292 | deps/
293 | src/*.jl
294 | src/jl
295 | Project.toml
296 |
297 | # Python build artifacts
298 | dash_antd/*
299 | !dash_antd/__init__.py
300 | !dash_antd/_imports_.py
301 | !.vscode/cspell.json
302 | !dash_antd/ext
303 | !dash_antd/py.typed
304 |
--------------------------------------------------------------------------------
/src/ts/components/autocomplete/AutoComplete.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from "react";
2 | import { DashComponentProps, StyledComponentProps } from "../../types";
3 | import { AutoComplete as AntAutoComplete } from "antd";
4 |
5 | type Props = {
6 | /**
7 | * The children of this component.
8 | */
9 | children?: ReactNode;
10 | /**
11 | * Show clear button
12 | */
13 | allow_clear: boolean;
14 | /**
15 | * If get focus when component mounted
16 | */
17 | auto_focus: boolean;
18 | /**
19 | * If backfill selected item the input when using keyboard
20 | */
21 | backfill: boolean;
22 | /**
23 | * Whether active first option by default
24 | */
25 | default_active_first_option?: boolean;
26 | /**
27 | * Initial open state of dropdown
28 | */
29 | default_open?: boolean;
30 | /**
31 | * Initial selected option
32 | */
33 | default_value?: string;
34 | /**
35 | * Whether disabled select
36 | */
37 | disabled: boolean;
38 | /**
39 | * The className of dropdown menu
40 | */
41 | popup_class_name?: string;
42 | /**
43 | * Determine whether the dropdown menu and the select input are the same width.
44 | * Default set min-width same as input. Will ignore when value less than select width. false will disable virtual scroll
45 | */
46 | dropdown_match_select_width: boolean | number;
47 | /**
48 | * If true, filter options by input, if function, filter options against it. The function will receive two arguments,
49 | * inputValue and option, if the function returns true, the option will be included in the filtered set; Otherwise, it will be excluded
50 | */
51 | filter_option: boolean;
52 | /**
53 | * Specify content to show when no result matches
54 | */
55 | not_found_content: string;
56 | /**
57 | * Controlled open state of dropdown
58 | */
59 | open?: boolean;
60 | /**
61 | * Select options. Will get better perf than jsx definition
62 | */
63 | options?: Array<{
64 | label: string;
65 | value: string;
66 | }>;
67 | /**
68 | * The placeholder of input
69 | */
70 | placeholder?: string;
71 | /**
72 | * Set validation status
73 | */
74 | status?: "error" | "warning";
75 | /**
76 | * Selected option
77 | */
78 | value?: string;
79 | /**
80 | * Called when leaving the component
81 | */
82 | n_blur: number;
83 | /**
84 | * Called when selecting an option or changing an input value
85 | */
86 | n_change: number;
87 | /**
88 | * Call when dropdown open
89 | */
90 | n_dropdown_visible_change: number;
91 | /**
92 | * Called when entering the component
93 | */
94 | n_focus: number;
95 | /**
96 | * Called when searching items
97 | */
98 | n_search: number;
99 | /**
100 | * Called when a option is selected. param is option's value and option instance
101 | */
102 | n_select: number;
103 | /**
104 | * Called when a option is selected. param is option's value and option instance
105 | */
106 | n_clear: number;
107 | } & DashComponentProps &
108 | StyledComponentProps;
109 |
110 | /**
111 | * Alert component for feedback.
112 | */
113 | const AutoComplete = (props: Props) => {
114 | const {
115 | children,
116 | allow_clear,
117 | auto_focus,
118 | default_active_first_option,
119 | default_open,
120 | default_value,
121 | popup_class_name,
122 | dropdown_match_select_width,
123 | filter_option,
124 | not_found_content,
125 | setProps,
126 | n_blur,
127 | n_change,
128 | n_dropdown_visible_change,
129 | n_focus,
130 | n_search,
131 | n_select,
132 | n_clear,
133 | ...otherProps
134 | } = props;
135 |
136 | const onBlur = () => {
137 | if (setProps) {
138 | setProps({
139 | n_onBlur: n_blur + 1,
140 | });
141 | }
142 | };
143 |
144 | const onChange = (value) => {
145 | if (setProps) {
146 | setProps({
147 | value: value,
148 | n_onChange: n_change + 1,
149 | });
150 | }
151 | };
152 |
153 | const onDropdownVisibleChange = () => {
154 | if (setProps) {
155 | setProps({
156 | n_onDropdownVisibleChange: n_dropdown_visible_change + 1,
157 | });
158 | }
159 | };
160 |
161 | const onFocus = () => {
162 | if (setProps) {
163 | setProps({
164 | n_onFocus: n_focus + 1,
165 | });
166 | }
167 | };
168 |
169 | const onSearch = (value) => {
170 | if (setProps) {
171 | setProps({
172 | value: value,
173 | n_onSearch: n_search + 1,
174 | });
175 | }
176 | };
177 |
178 | const onSelect = (value, option) => {
179 | if (setProps) {
180 | setProps({
181 | value: value,
182 | n_onSelect: n_select + 1,
183 | });
184 | }
185 | };
186 |
187 | const onClear = () => {
188 | if (setProps) {
189 | setProps({
190 | value: "",
191 | n_onClear: n_clear + 1,
192 | });
193 | }
194 | };
195 |
196 | return (
197 |
216 | {children}
217 |
218 | );
219 | };
220 |
221 | AutoComplete.defaultProps = {
222 | allow_clear: false,
223 | auto_focus: false,
224 | backfill: false,
225 | default_active_first_option: true,
226 | disabled: false,
227 | dropdown_match_select_width: true,
228 | filter_option: true,
229 | not_found_content: "Not Found",
230 | n_blur: 0,
231 | n_change: 0,
232 | n_dropdown_visible_change: 0,
233 | n_focus: 0,
234 | n_search: 0,
235 | n_select: 0,
236 | n_clear: 0,
237 | };
238 |
239 | export default AutoComplete;
240 |
--------------------------------------------------------------------------------
/src/ts/components/input/InputNumber.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useCallback } from "react";
2 | import { InputNumber as AntInputNumber, InputNumberProps } from "antd";
3 | import {
4 | DashComponentProps,
5 | DashLoadingState,
6 | StyledComponentProps,
7 | } from "../../types";
8 | import { isNil, omit } from "ramda";
9 | import isNumeric from "fast-isnumeric";
10 |
11 | const convert = (val) => (isNumeric(val) ? +val : NaN);
12 | const isEquivalent = (v1, v2) => v1 === v2 || (isNaN(v1) && isNaN(v2));
13 |
14 | type Props = {
15 | /**
16 | * The label text displayed after (on the right side of) the input field
17 | */
18 | addon_after?: string;
19 | /**
20 | * The label text displayed before (on the left side of) the input field
21 | */
22 | addon_before?: string;
23 | /**
24 | * Whether has border style
25 | */
26 | bordered?: boolean;
27 | /**
28 | * Whether to show +- controls
29 | */
30 | controls?: boolean;
31 | /**
32 | * If true, changes to input will be sent back to the Dash server
33 | * only when the enter key is pressed or when the component loses
34 | * focus. If it's false, it will sent the value back on every
35 | * change.
36 | */
37 | debounce: boolean;
38 | /**
39 | * Whether the input is disabled
40 | */
41 | disabled?: boolean;
42 | /**
43 | * The max value
44 | */
45 | max?: number;
46 | /**
47 | * The min value
48 | */
49 | min?: number;
50 | /**
51 | * The precision of input value
52 | */
53 | precision?: number;
54 | /**
55 | * If readonly the input
56 | */
57 | read_only?: boolean;
58 | /**
59 | * Set validation status
60 | */
61 | status?: "error" | "warning";
62 | /**
63 | * The prefix icon for the Input
64 | */
65 | prefix?: string;
66 | /**
67 | * The height of input box
68 | */
69 | size?: "large" | "middle" | "small";
70 | /**
71 | * The number to which the current value is increased or decreased. It can be an integer or decimal
72 | */
73 | step?: number | string;
74 | /**
75 | * Set value as string to support high precision decimals
76 | */
77 | string_mode?: boolean;
78 | /**
79 | * The current value
80 | */
81 | value?: number;
82 | /**
83 | * Number of times the input lost focus.
84 | */
85 | n_blur: number;
86 | /**
87 | * Last time the input lost focus.
88 | */
89 | n_blur_timestamp: number;
90 | /**
91 | * Number of times the `Enter` key was pressed while the input had focus.
92 | */
93 | n_submit: number;
94 | /**
95 | * Last time that `Enter` was pressed.
96 | */
97 | n_submit_timestamp: number;
98 | /**
99 | * Object that holds the loading state object coming from dash-renderer
100 | */
101 | loading_state?: DashLoadingState;
102 | } & DashComponentProps &
103 | StyledComponentProps;
104 |
105 | type PayloadType = {
106 | value?: number | string;
107 | n_blur?: number;
108 | n_blur_timestamp?: number;
109 | n_submit?: number;
110 | n_submit_timestamp?: number;
111 | };
112 |
113 | /**
114 | * InputNumber
115 | */
116 | const InputNumber = (props: Props) => {
117 | const {
118 | addon_after,
119 | addon_before,
120 | class_name,
121 | debounce,
122 | n_blur,
123 | n_submit,
124 | value,
125 | loading_state,
126 | setProps,
127 | ...otherProps
128 | } = props;
129 | const inputRef = useRef(null);
130 |
131 | useEffect(() => {
132 | const inputValue = inputRef.current.value;
133 | const inputValueAsNumber = inputRef.current.checkValidity()
134 | ? convert(inputValue)
135 | : NaN;
136 | const valueAsNumber = convert(value);
137 | if (!isEquivalent(valueAsNumber, inputValueAsNumber)) {
138 | inputRef.current.value = isNil(valueAsNumber)
139 | ? valueAsNumber
140 | : value;
141 | }
142 | }, [value]);
143 |
144 | const onEvent = useCallback(
145 | (payload: PayloadType = {}) => {
146 | const inputValue = inputRef.current.value;
147 | const inputValueAsNumber = inputRef.current.checkValidity()
148 | ? convert(inputValue)
149 | : NaN;
150 | const valueAsNumber = convert(value);
151 |
152 | if (!isEquivalent(valueAsNumber, inputValueAsNumber)) {
153 | setProps({ ...payload, value: inputValueAsNumber });
154 | } else if (Object.keys(payload).length) {
155 | setProps(payload);
156 | }
157 | },
158 | [setProps, inputRef, value]
159 | );
160 |
161 | const onStep: InputNumberProps["onStep"] = useCallback(
162 | (newValue) => {
163 | const valueAsNumber = convert(newValue);
164 | inputRef.current.value = isNil(valueAsNumber)
165 | ? valueAsNumber
166 | : newValue;
167 | if (!debounce) {
168 | onEvent();
169 | }
170 | },
171 | [onEvent, debounce, inputRef]
172 | );
173 |
174 | const onBlur: InputNumberProps["onBlur"] = useCallback(() => {
175 | if (setProps) {
176 | const payload = {
177 | n_blur: n_blur + 1,
178 | n_blur_timestamp: Date.now(),
179 | };
180 | if (debounce) {
181 | onEvent(payload);
182 | } else {
183 | setProps(payload);
184 | }
185 | }
186 | }, [setProps, onEvent, n_blur, debounce]);
187 |
188 | const onKeyPress: InputNumberProps["onPressEnter"] = useCallback(() => {
189 | const payload = {
190 | n_submit: n_submit + 1,
191 | n_submit_timestamp: Date.now(),
192 | };
193 | if (debounce) {
194 | onEvent(payload);
195 | } else {
196 | setProps(payload);
197 | }
198 | }, [setProps, onEvent, n_submit, debounce]);
199 |
200 | return (
201 |
218 | );
219 | };
220 |
221 | InputNumber.defaultProps = {
222 | n_blur: 0,
223 | n_blur_timestamp: -1,
224 | n_submit: 0,
225 | n_submit_timestamp: -1,
226 | debounce: false,
227 | value: 0,
228 | step: 1,
229 | };
230 |
231 | export default InputNumber;
232 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * For a detailed explanation regarding each configuration property and type check, visit:
3 | * https://jestjs.io/docs/configuration
4 | */
5 |
6 | export default {
7 | // All imported modules in your tests should be mocked automatically
8 | // automock: false,
9 |
10 | // Stop running tests after `n` failures
11 | // bail: 0,
12 |
13 | // The directory where Jest should store its cached dependency information
14 | // cacheDirectory: "/tmp/jest_rs",
15 |
16 | // Automatically clear mock calls, instances and results before every test
17 | clearMocks: true,
18 |
19 | // Indicates whether the coverage information should be collected while executing the test
20 | collectCoverage: true,
21 |
22 | // An array of glob patterns indicating a set of files for which coverage information should be collected
23 | // collectCoverageFrom: undefined,
24 |
25 | // The directory where Jest should output its coverage files
26 | coverageDirectory: "coverage",
27 |
28 | // An array of regexp pattern strings used to skip coverage collection
29 | coveragePathIgnorePatterns: ["/node_modules/", ".venv/"],
30 |
31 | // Indicates which provider should be used to instrument code for coverage
32 | // coverageProvider: "babel",
33 |
34 | // A list of reporter names that Jest uses when writing coverage reports
35 | // coverageReporters: [
36 | // "json",
37 | // "text",
38 | // "lcov",
39 | // "clover"
40 | // ],
41 |
42 | // An object that configures minimum threshold enforcement for coverage results
43 | // coverageThreshold: undefined,
44 |
45 | // A path to a custom dependency extractor
46 | // dependencyExtractor: undefined,
47 |
48 | // Make calling deprecated APIs throw helpful error messages
49 | // errorOnDeprecated: false,
50 |
51 | // Force coverage collection from ignored files using an array of glob patterns
52 | // forceCoverageMatch: [],
53 |
54 | // A path to a module which exports an async function that is triggered once before all test suites
55 | // globalSetup: undefined,
56 |
57 | // A path to a module which exports an async function that is triggered once after all test suites
58 | // globalTeardown: undefined,
59 |
60 | // A set of global variables that need to be available in all test environments
61 | // globals: {},
62 |
63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
64 | // maxWorkers: "50%",
65 |
66 | // An array of directory names to be searched recursively up from the requiring module's location
67 | // moduleDirectories: [
68 | // "node_modules"
69 | // ],
70 |
71 | // An array of file extensions your modules use
72 | moduleFileExtensions: ["js", "jsx", "ts", "tsx", "json", "node"],
73 |
74 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
75 | // moduleNameMapper: {},
76 |
77 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
78 | // modulePathIgnorePatterns: [],
79 |
80 | // Activates notifications for test results
81 | // notify: false,
82 |
83 | // An enum that specifies notification mode. Requires { notify: true }
84 | // notifyMode: "failure-change",
85 |
86 | // A preset that is used as a base for Jest's configuration
87 | preset: "ts-jest",
88 |
89 | // Run tests from one or more projects
90 | // projects: undefined,
91 |
92 | // Use this configuration option to add custom reporters to Jest
93 | // reporters: undefined,
94 |
95 | // Automatically reset mock state before every test
96 | // resetMocks: false,
97 |
98 | // Reset the module registry before running each individual test
99 | // resetModules: false,
100 |
101 | // A path to a custom resolver
102 | // resolver: undefined,
103 |
104 | // Automatically restore mock state and implementation before every test
105 | // restoreMocks: false,
106 |
107 | // The root directory that Jest should scan for tests and modules within
108 | // rootDir: undefined,
109 |
110 | // A list of paths to directories that Jest should use to search for files in
111 | // roots: [
112 | // ""
113 | // ],
114 |
115 | // Allows you to use a custom runner instead of Jest's default test runner
116 | // runner: "jest-runner",
117 |
118 | // The paths to modules that run some code to configure or set up the testing environment before each test
119 | // setupFiles: [],
120 |
121 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
122 | setupFilesAfterEnv: ["@testing-library/jest-dom/extend-expect"],
123 |
124 | // The number of seconds after which a test is considered as slow and reported as such in the results.
125 | // slowTestThreshold: 5,
126 |
127 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
128 | // snapshotSerializers: [],
129 |
130 | // The test environment that will be used for testing
131 | testEnvironment: "jsdom",
132 |
133 | // Options that will be passed to the testEnvironment
134 | // testEnvironmentOptions: {},
135 |
136 | // Adds a location field to test results
137 | // testLocationInResults: false,
138 |
139 | // The glob patterns Jest uses to detect test files
140 | // testMatch: [
141 | // "**/__tests__/**/*.[jt]s?(x)",
142 | // "**/?(*.)+(spec|test).[tj]s?(x)"
143 | // ],
144 |
145 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
146 | testPathIgnorePatterns: ["/node_modules/", ".venv/"],
147 |
148 | // The regexp pattern or array of patterns that Jest uses to detect test files
149 | // testRegex: [],
150 |
151 | // This option allows the use of a custom results processor
152 | // testResultsProcessor: undefined,
153 |
154 | // This option allows use of a custom test runner
155 | // testRunner: "jest-circus/runner",
156 |
157 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
158 | // testURL: "http://localhost",
159 |
160 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
161 | // timers: "real",
162 |
163 | // A map from regular expressions to paths to transformers
164 | transformIgnorePatterns: ["/node_modules/"],
165 |
166 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
167 | transform: {
168 | "^.+\\.ts?$": "ts-jest",
169 | },
170 |
171 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
172 | // unmockedModulePathPatterns: undefined,
173 |
174 | // Indicates whether each individual test should be reported during the run
175 | // verbose: undefined,
176 |
177 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
178 | // watchPathIgnorePatterns: [],
179 |
180 | // Whether to use watchman for file crawling
181 | // watchman: true,
182 | };
183 |
--------------------------------------------------------------------------------
/src/ts/components/input/Input.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useEffect, useRef } from "react";
2 | import {
3 | DashComponentProps,
4 | DashLoadingState,
5 | StyledComponentProps,
6 | } from "../../types";
7 | import { Input as AntInput, InputProps } from "antd";
8 | import { isNil, omit } from "ramda";
9 | import isNumeric from "fast-isnumeric";
10 |
11 | const convert = (val) => (isNumeric(val) ? +val : NaN);
12 | const isEquivalent = (v1, v2) => v1 === v2 || (isNaN(v1) && isNaN(v2));
13 |
14 | type Props = {
15 | /**
16 | * The input content value
17 | */
18 | value?: string | number;
19 | /**
20 | * The label text displayed after (on the right side of) the input field
21 | */
22 | addon_after?: ReactNode;
23 | /**
24 | * The label text displayed before (on the left side of) the input field
25 | */
26 | addon_before?: ReactNode;
27 | /**
28 | * If allow to remove input content with clear icon
29 | */
30 | allow_clear?: boolean;
31 | /**
32 | * This attribute indicates whether the value of the control can be
33 | * automatically completed by the browser.
34 | */
35 | autocomplete?: "on" | "off";
36 | /**
37 | * Whether has border style
38 | */
39 | bordered?: boolean;
40 | /**
41 | * Whether the input is disabled
42 | */
43 | disabled?: boolean;
44 | /**
45 | * The maximum (numeric or date-time) value for this item, which must not be
46 | * less than its minimum (min attribute) value.
47 | */
48 | max?: string | number;
49 | /**
50 | * The minimum (numeric or date-time) value for this item, which must not be
51 | * greater than its maximum (max attribute) value.
52 | */
53 | min?: string | number;
54 | /**
55 | * The max length
56 | */
57 | max_length?: number;
58 | /**
59 | * Indicates whether the element can be edited.
60 | */
61 | readonly?: boolean;
62 | /**
63 | * Whether show character count
64 | */
65 | show_count?: boolean;
66 | /**
67 | * The size of the input box
68 | */
69 | size?: "large" | "middle" | "small";
70 | /**
71 | * Number of times the input lost focus.
72 | */
73 | n_blur: number;
74 | /**
75 | * Last time the input lost focus.
76 | */
77 | n_blur_timestamp: number;
78 | /**
79 | * Number of times the `Enter` key was pressed while the input had focus.
80 | */
81 | n_submit: number;
82 | /**
83 | * Last time that `Enter` was pressed.
84 | */
85 | n_submit_timestamp: number;
86 | /**
87 | * If true, changes to input will be sent back to the Dash server
88 | * only when the enter key is pressed or when the component loses
89 | * focus. If it's false, it will sent the value back on every
90 | * change.
91 | */
92 | debounce: boolean;
93 | /**
94 | * The type of control to render
95 | */
96 | type?:
97 | | "text"
98 | | "number"
99 | | "password"
100 | | "email"
101 | | "range"
102 | | "search"
103 | | "tel"
104 | | "url"
105 | | "hidden";
106 | /**
107 | * Set validation status
108 | */
109 | status?: "error" | "warning";
110 | /**
111 | * A hint to the user of what can be entered in the control.
112 | */
113 | placeholder?: string;
114 | /**
115 | * Object that holds the loading state object coming from dash-renderer
116 | */
117 | loading_state?: DashLoadingState;
118 | } & DashComponentProps &
119 | StyledComponentProps;
120 |
121 | type PayloadType = {
122 | value?: number | string;
123 | n_blur?: number;
124 | n_blur_timestamp?: number;
125 | n_submit?: number;
126 | n_submit_timestamp?: number;
127 | };
128 |
129 | /**
130 | * A basic widget for getting the user input is a text field.
131 | * Keyboard and mouse can be used for providing or changing data.
132 | */
133 | const Input = (props: Props) => {
134 | const {
135 | addon_after,
136 | addon_before,
137 | allow_clear,
138 | max_length,
139 | show_count,
140 | class_name,
141 | debounce,
142 | n_blur,
143 | n_submit,
144 | value,
145 | type,
146 | loading_state,
147 | setProps,
148 | ...otherProps
149 | } = props;
150 | const inputRef = useRef(null);
151 |
152 | const onChange: InputProps["onChange"] = () => {
153 | if (!debounce) {
154 | onEvent();
155 | }
156 | };
157 |
158 | useEffect(() => {
159 | if (type === "number") {
160 | const inputValue = inputRef.current.input.value;
161 | const inputValueAsNumber = inputRef.current.input.checkValidity()
162 | ? convert(inputValue)
163 | : NaN;
164 | const valueAsNumber = convert(value);
165 |
166 | if (!isEquivalent(valueAsNumber, inputValueAsNumber)) {
167 | inputRef.current.input.value = isNil(valueAsNumber)
168 | ? valueAsNumber
169 | : value;
170 | }
171 | } else {
172 | const inputValue = inputRef.current.input.value;
173 |
174 | if (value !== inputValue) {
175 | inputRef.current.input.value =
176 | value !== null && value !== undefined ? value : "";
177 | }
178 | }
179 | }, [value, type]);
180 |
181 | const onEvent = (payload: PayloadType = {}) => {
182 | if (type === "number") {
183 | const inputValue = inputRef.current.input.value;
184 | const inputValueAsNumber = inputRef.current.input.checkValidity()
185 | ? convert(inputValue)
186 | : NaN;
187 | const valueAsNumber = convert(value);
188 |
189 | if (!isEquivalent(valueAsNumber, inputValueAsNumber)) {
190 | setProps({ ...payload, value: inputValueAsNumber });
191 | } else if (Object.keys(payload).length) {
192 | setProps(payload);
193 | }
194 | } else {
195 | payload.value = inputRef.current.input.value;
196 | setProps(payload);
197 | }
198 | };
199 |
200 | const handleBlur: InputProps["onBlur"] = () => {
201 | if (setProps) {
202 | const payload = {
203 | n_blur: n_blur + 1,
204 | n_blur_timestamp: Date.now(),
205 | };
206 | if (debounce) {
207 | onEvent(payload);
208 | } else {
209 | setProps(payload);
210 | }
211 | }
212 | };
213 |
214 | const handleKeyPress: InputProps["onKeyPress"] = (e) => {
215 | if (setProps && e.key === "Enter") {
216 | const payload = {
217 | n_submit: n_submit + 1,
218 | n_submit_timestamp: Date.now(),
219 | };
220 | if (debounce) {
221 | onEvent(payload);
222 | } else {
223 | setProps(payload);
224 | }
225 | }
226 | };
227 |
228 | return (
229 |
246 | );
247 | };
248 |
249 | Input.defaultProps = {
250 | n_blur: 0,
251 | n_blur_timestamp: -1,
252 | n_submit: 0,
253 | n_submit_timestamp: -1,
254 | debounce: false,
255 | };
256 |
257 | export default Input;
258 |
--------------------------------------------------------------------------------
/src/ts/components/input/__tests__/Input.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render, fireEvent } from "@testing-library/react";
3 | import userEvent from "@testing-library/user-event";
4 | import Input from "../Input";
5 |
6 | describe("Input", () => {
7 | test("has no value by default", () => {
8 | const input = render();
9 |
10 | expect(input.container.firstChild).not.toHaveValue();
11 | });
12 |
13 | test("passes value on to the underlying HTML input", () => {
14 | const {
15 | container: { firstChild: input },
16 | rerender,
17 | } = render();
18 |
19 | expect(input).toHaveValue("some-input-value");
20 |
21 | rerender();
22 | expect(input).toHaveValue("another-input-value");
23 | });
24 |
25 | describe("setProps", () => {
26 | let inputElement, mockSetProps;
27 | const user = userEvent.setup();
28 |
29 | beforeEach(() => {
30 | mockSetProps = jest.fn();
31 | const { container } = render();
32 | inputElement = container.firstChild;
33 | });
34 |
35 | test('tracks changes with "value" prop', () => {
36 | fireEvent.change(inputElement, {
37 | target: { value: "some-input-value" },
38 | });
39 | expect(mockSetProps.mock.calls).toHaveLength(1);
40 | expect(mockSetProps.mock.calls[0][0]).toEqual({
41 | value: "some-input-value",
42 | });
43 | });
44 |
45 | test("dispatches update for each typed character", async () => {
46 | await user.type(inputElement, "abc");
47 |
48 | expect(mockSetProps.mock.calls).toHaveLength(3);
49 |
50 | const [call1, call2, call3] = mockSetProps.mock.calls;
51 | expect(call1).toEqual([{ value: "a" }]);
52 | expect(call2).toEqual([{ value: "ab" }]);
53 | expect(call3).toEqual([{ value: "abc" }]);
54 | });
55 |
56 | test('track number of blurs with "n_blur" and "n_blur_timestamp"', () => {
57 | const before = Date.now();
58 | fireEvent.blur(inputElement);
59 | const after = Date.now();
60 |
61 | expect(mockSetProps.mock.calls).toHaveLength(1);
62 |
63 | const [[{ n_blur, n_blur_timestamp }]] = mockSetProps.mock.calls;
64 | expect(n_blur).toEqual(1);
65 | expect(n_blur_timestamp).toBeGreaterThanOrEqual(before);
66 | expect(n_blur_timestamp).toBeLessThanOrEqual(after);
67 | });
68 |
69 | test('tracks submit with "n_submit" and "n_submit_timestamp"', () => {
70 | const before = Date.now();
71 | fireEvent.keyPress(inputElement, {
72 | key: "Enter",
73 | code: 13,
74 | charCode: 13,
75 | });
76 | const after = Date.now();
77 |
78 | expect(mockSetProps.mock.calls).toHaveLength(1);
79 |
80 | const [[{ n_submit, n_submit_timestamp }]] =
81 | mockSetProps.mock.calls;
82 | expect(n_submit).toEqual(1);
83 | expect(n_submit_timestamp).toBeGreaterThanOrEqual(before);
84 | expect(n_submit_timestamp).toBeLessThanOrEqual(after);
85 | });
86 |
87 | test("don't increment n_submit if key is not Enter", () => {
88 | fireEvent.keyPress(inputElement, {
89 | key: "a",
90 | code: 65,
91 | charCode: 65,
92 | });
93 | expect(mockSetProps.mock.calls).toHaveLength(0);
94 | });
95 | });
96 |
97 | describe("debounce", () => {
98 | let inputElement, mockSetProps;
99 | beforeEach(() => {
100 | mockSetProps = jest.fn();
101 | const { container } = render(
102 |
107 | );
108 | inputElement = container.firstChild;
109 | });
110 |
111 | test("don't call setProps on change if debounce is true", () => {
112 | fireEvent.change(inputElement, {
113 | target: { value: "some-input-value" },
114 | });
115 | expect(mockSetProps.mock.calls).toHaveLength(0);
116 | expect(inputElement).toHaveValue("some-input-value");
117 | });
118 |
119 | test("dispatch value on blur if debounce is true", () => {
120 | const before = Date.now();
121 | fireEvent.blur(inputElement);
122 | const after = Date.now();
123 |
124 | expect(mockSetProps.mock.calls).toHaveLength(1);
125 |
126 | const [[{ n_blur, n_blur_timestamp, value }]] =
127 | mockSetProps.mock.calls;
128 | expect(n_blur).toEqual(1);
129 | expect(n_blur_timestamp).toBeGreaterThanOrEqual(before);
130 | expect(n_blur_timestamp).toBeLessThanOrEqual(after);
131 | expect(value).toEqual("some-input-value");
132 | });
133 |
134 | test("dispatch value on submit if debounce is true", () => {
135 | const before = Date.now();
136 | fireEvent.keyPress(inputElement, {
137 | key: "Enter",
138 | code: 13,
139 | charCode: 13,
140 | });
141 | const after = Date.now();
142 |
143 | expect(mockSetProps.mock.calls).toHaveLength(1);
144 |
145 | const [[{ n_submit, n_submit_timestamp, value }]] =
146 | mockSetProps.mock.calls;
147 | expect(n_submit).toEqual(1);
148 | expect(n_submit_timestamp).toBeGreaterThanOrEqual(before);
149 | expect(n_submit_timestamp).toBeLessThanOrEqual(after);
150 | expect(value).toEqual("some-input-value");
151 | });
152 | });
153 |
154 | describe("number input", () => {
155 | let inputElement, mockSetProps;
156 |
157 | beforeEach(() => {
158 | mockSetProps = jest.fn();
159 | const { container } = render(
160 |
161 | );
162 | inputElement = container.firstChild;
163 | });
164 |
165 | test('tracks changes with "value" prop', () => {
166 | fireEvent.change(inputElement, {
167 | target: { value: 12 },
168 | });
169 | expect(inputElement).toHaveValue(12);
170 |
171 | fireEvent.change(inputElement, {
172 | target: { value: -42 },
173 | });
174 | expect(inputElement).toHaveValue(-42);
175 |
176 | // fireEvent.change(inputElement, {
177 | // target: { value: 1.01 },
178 | // });
179 | // expect(inputElement).toHaveValue(1.01);
180 |
181 | fireEvent.change(inputElement, {
182 | target: { value: 0 },
183 | });
184 | expect(inputElement).toHaveValue(0);
185 |
186 | // expect(mockSetProps.mock.calls).toHaveLength(4);
187 |
188 | const [call1, call2, call3] = mockSetProps.mock.calls;
189 | expect(call1).toEqual([{ value: 12 }]);
190 | expect(call2).toEqual([{ value: -42 }]);
191 | // expect(call3).toEqual([{ value: 1.01 }]);
192 | expect(call3).toEqual([{ value: 0 }]);
193 | });
194 |
195 | // test("dispatches update for each typed character", () => {
196 | // userEvent.type(inputElement, "-1e4");
197 |
198 | // expect(inputElement).toHaveValue(-10000);
199 | // expect(mockSetProps.mock.calls).toHaveLength(2);
200 |
201 | // const [call1, call3] = mockSetProps.mock.calls;
202 | // expect(call1).toEqual([{ value: -1 }]);
203 | // expect(call3).toEqual([{ value: -10000 }]);
204 | // });
205 |
206 | test("only accepts numeric input", () => {
207 | userEvent.type(inputElement, "asdf?");
208 |
209 | expect(inputElement).not.toHaveValue();
210 | expect(mockSetProps.mock.calls).toHaveLength(0);
211 | });
212 | });
213 | });
214 |
--------------------------------------------------------------------------------
/dash_antd/ext/_theming.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from dataclasses import dataclass, fields
4 | from typing import Any
5 |
6 |
7 | def parse_tokens(raw: dict[str, Any]) -> Colors:
8 | field_names = {field.name for field in fields(Colors)}
9 | converted = {key.replace("-", "_"): value for key, value in raw.items() if not key.startswith("_")}
10 | return Colors(**{k: v for k, v in converted.items() if k in field_names})
11 |
12 |
13 | @dataclass(frozen=True)
14 | class Model:
15 | fontFamily: str
16 | fontSize: int
17 | lineWidth: int
18 | lineType: str
19 | motionUnit: float
20 | motionBase: int
21 | motionEaseOutCirc: str
22 | motionEaseInOutCirc: str
23 | motionEaseOut: str
24 | motionEaseInOut: str
25 | motionEaseOutBack: str
26 | motionEaseInBack: str
27 | motionEaseInQuint: str
28 | motionEaseOutQuint: str
29 | borderRadius: int
30 | sizeUnit: int
31 | sizeStep: int
32 | sizePopupArrow: int
33 | controlHeight: int
34 | zIndexBase: int
35 | zIndexPopupBase: int
36 | opacityImage: int
37 | wireframe: bool
38 |
39 | sizeXXL: int
40 | sizeXL: int
41 | sizeLG: int
42 | sizeMD: int
43 | sizeMS: int
44 | size: int
45 | sizeSM: int
46 | sizeXS: int
47 | sizeXXS: int
48 | controlHeightSM: int
49 | controlHeightXS: int
50 | controlHeightLG: int
51 | motionDurationFast: str
52 | motionDurationMid: str
53 | motionDurationSlow: str
54 | fontSizes: list[int]
55 | lineHeights: list[float]
56 | lineWidthBold: int
57 | borderRadiusXS: int
58 | borderRadiusSM: int
59 | borderRadiusLG: int
60 | borderRadiusOuter: int
61 |
62 | fontSizeSM: int
63 | fontSizeLG: int
64 | fontSizeXL: int
65 | fontSizeHeading1: int
66 | fontSizeHeading2: int
67 | fontSizeHeading3: int
68 | fontSizeHeading4: int
69 | fontSizeHeading5: int
70 | fontSizeIcon: int
71 | lineHeight: float
72 | lineHeightLG: float
73 | lineHeightSM: float
74 | lineHeightHeading1: float
75 | lineHeightHeading2: float
76 | lineHeightHeading3: float
77 | lineHeightHeading4: float
78 | lineHeightHeading5: float
79 | controlOutlineWidth: int
80 | controlInteractiveSize: int
81 | controlItemBgHover: str
82 | controlItemBgActive: str
83 | controlItemBgActiveHover: str
84 | controlItemBgActiveDisabled: str
85 | controlTmpOutline: str
86 | controlOutline: str
87 | fontWeightStrong: int
88 | opacityLoading: float
89 | linkDecoration: str
90 | linkHoverDecoration: str
91 | linkFocusDecoration: str
92 | controlPaddingHorizontal: int
93 | controlPaddingHorizontalSM: int
94 | paddingXXS: int
95 | paddingXS: int
96 | paddingSM: int
97 | padding: int
98 | paddingMD: int
99 | paddingLG: int
100 | paddingXL: int
101 | paddingContentHorizontalLG: int
102 | paddingContentVerticalLG: int
103 | paddingContentHorizontal: int
104 | paddingContentVertical: int
105 | paddingContentHorizontalSM: int
106 | paddingContentVerticalSM: int
107 | marginXXS: int
108 | marginXS: int
109 | marginSM: int
110 | margin: int
111 | marginMD: int
112 | marginLG: int
113 | marginXL: int
114 | marginXXL: int
115 | boxShadow: str
116 | boxShadowSecondary: str
117 | screenXS: int
118 | screenXSMin: int
119 | screenXSMax: int
120 | screenSM: int
121 | screenSMMin: int
122 | screenSMMax: int
123 | screenMD: int
124 | screenMDMin: int
125 | screenMDMax: int
126 | screenLG: int
127 | screenLGMin: int
128 | screenLGMax: int
129 | screenXL: int
130 | screenXLMin: int
131 | screenXLMax: int
132 | screenXXL: int
133 | screenXXLMin: int
134 | screenXXLMax: int
135 | boxShadowPopoverArrow: str
136 | boxShadowCard: str
137 | boxShadowDrawerRight: str
138 | boxShadowDrawerLeft: str
139 | boxShadowDrawerUp: str
140 | boxShadowDrawerDown: str
141 | boxShadowTabsOverflowLeft: str
142 | boxShadowTabsOverflowRight: str
143 | boxShadowTabsOverflowTop: str
144 | boxShadowTabsOverflowBottom: str
145 |
146 |
147 | @dataclass(frozen=True)
148 | class Colors:
149 | blue: str
150 | purple: str
151 | cyan: str
152 | green: str
153 | magenta: str
154 | pink: str
155 | red: str
156 | orange: str
157 | yellow: str
158 | volcano: str
159 | geekblue: str
160 | gold: str
161 | lime: str
162 | colorPrimary: str
163 | colorSuccess: str
164 | colorWarning: str
165 | colorError: str
166 | colorInfo: str
167 | colorTextBase: str
168 | colorBgBase: str
169 | blue_1: str
170 | blue_2: str
171 | blue_3: str
172 | blue_4: str
173 | blue_5: str
174 | blue_6: str
175 | blue_7: str
176 | blue_8: str
177 | blue_9: str
178 | blue_10: str
179 | purple_1: str
180 | purple_2: str
181 | purple_3: str
182 | purple_4: str
183 | purple_5: str
184 | purple_6: str
185 | purple_7: str
186 | purple_8: str
187 | purple_9: str
188 | purple_10: str
189 | cyan_1: str
190 | cyan_2: str
191 | cyan_3: str
192 | cyan_4: str
193 | cyan_5: str
194 | cyan_6: str
195 | cyan_7: str
196 | cyan_8: str
197 | cyan_9: str
198 | cyan_10: str
199 | green_1: str
200 | green_2: str
201 | green_3: str
202 | green_4: str
203 | green_5: str
204 | green_6: str
205 | green_7: str
206 | green_8: str
207 | green_9: str
208 | green_10: str
209 | magenta_1: str
210 | magenta_2: str
211 | magenta_3: str
212 | magenta_4: str
213 | magenta_5: str
214 | magenta_6: str
215 | magenta_7: str
216 | magenta_8: str
217 | magenta_9: str
218 | magenta_10: str
219 | pink_1: str
220 | pink_2: str
221 | pink_3: str
222 | pink_4: str
223 | pink_5: str
224 | pink_6: str
225 | pink_7: str
226 | pink_8: str
227 | pink_9: str
228 | pink_10: str
229 | red_1: str
230 | red_2: str
231 | red_3: str
232 | red_4: str
233 | red_5: str
234 | red_6: str
235 | red_7: str
236 | red_8: str
237 | red_9: str
238 | red_10: str
239 | orange_1: str
240 | orange_2: str
241 | orange_3: str
242 | orange_4: str
243 | orange_5: str
244 | orange_6: str
245 | orange_7: str
246 | orange_8: str
247 | orange_9: str
248 | orange_10: str
249 | yellow_1: str
250 | yellow_2: str
251 | yellow_3: str
252 | yellow_4: str
253 | yellow_5: str
254 | yellow_6: str
255 | yellow_7: str
256 | yellow_8: str
257 | yellow_9: str
258 | yellow_10: str
259 | volcano_1: str
260 | volcano_2: str
261 | volcano_3: str
262 | volcano_4: str
263 | volcano_5: str
264 | volcano_6: str
265 | volcano_7: str
266 | volcano_8: str
267 | volcano_9: str
268 | volcano_10: str
269 | geekblue_1: str
270 | geekblue_2: str
271 | geekblue_3: str
272 | geekblue_4: str
273 | geekblue_5: str
274 | geekblue_6: str
275 | geekblue_7: str
276 | geekblue_8: str
277 | geekblue_9: str
278 | geekblue_10: str
279 | gold_1: str
280 | gold_2: str
281 | gold_3: str
282 | gold_4: str
283 | gold_5: str
284 | gold_6: str
285 | gold_7: str
286 | gold_8: str
287 | gold_9: str
288 | gold_10: str
289 | lime_1: str
290 | lime_2: str
291 | lime_3: str
292 | lime_4: str
293 | lime_5: str
294 | lime_6: str
295 | lime_7: str
296 | lime_8: str
297 | lime_9: str
298 | lime_10: str
299 | colorText: str
300 | colorTextSecondary: str
301 | colorTextTertiary: str
302 | colorTextQuaternary: str
303 | colorFill: str
304 | colorFillSecondary: str
305 | colorFillTertiary: str
306 | colorFillQuaternary: str
307 | colorBgLayout: str
308 | colorBgContainer: str
309 | colorBgElevated: str
310 | colorBgSpotlight: str
311 | colorBorder: str
312 | colorBorderSecondary: str
313 | colorPrimaryBg: str
314 | colorPrimaryBgHover: str
315 | colorPrimaryBorder: str
316 | colorPrimaryBorderHover: str
317 | colorPrimaryHover: str
318 | colorPrimaryActive: str
319 | colorPrimaryTextHover: str
320 | colorPrimaryText: str
321 | colorPrimaryTextActive: str
322 | colorSuccessBg: str
323 | colorSuccessBgHover: str
324 | colorSuccessBorder: str
325 | colorSuccessBorderHover: str
326 | colorSuccessHover: str
327 | colorSuccessActive: str
328 | colorSuccessTextHover: str
329 | colorSuccessText: str
330 | colorSuccessTextActive: str
331 | colorErrorBg: str
332 | colorErrorBgHover: str
333 | colorErrorBorder: str
334 | colorErrorBorderHover: str
335 | colorErrorHover: str
336 | colorErrorActive: str
337 | colorErrorTextHover: str
338 | colorErrorText: str
339 | colorErrorTextActive: str
340 | colorWarningBg: str
341 | colorWarningBgHover: str
342 | colorWarningBorder: str
343 | colorWarningBorderHover: str
344 | colorWarningHover: str
345 | colorWarningActive: str
346 | colorWarningTextHover: str
347 | colorWarningText: str
348 | colorWarningTextActive: str
349 | colorInfoBg: str
350 | colorInfoBgHover: str
351 | colorInfoBorder: str
352 | colorInfoBorderHover: str
353 | colorInfoHover: str
354 | colorInfoActive: str
355 | colorInfoTextHover: str
356 | colorInfoText: str
357 | colorInfoTextActive: str
358 | colorBgMask: str
359 | colorWhite: str
360 | colorLink: str
361 | colorLinkHover: str
362 | colorLinkActive: str
363 | colorFillContent: str
364 | colorFillContentHover: str
365 | colorFillAlter: str
366 | colorBgContainerDisabled: str
367 | colorBorderBg: str
368 | colorSplit: str
369 | colorTextPlaceholder: str
370 | colorTextDisabled: str
371 | colorTextHeading: str
372 | colorTextLabel: str
373 | colorTextDescription: str
374 | colorTextLightSolid: str
375 | colorHighlight: str
376 | colorBgTextHover: str
377 | colorBgTextActive: str
378 | colorIcon: str
379 | colorIconHover: str
380 | colorErrorOutline: str
381 | colorWarningOutline: str
382 |
--------------------------------------------------------------------------------
/src/ts/components/select/Select.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | useCallback,
3 | useState,
4 | useEffect,
5 | useMemo,
6 | ReactNode,
7 | } from "react";
8 | import { Select as AntSelect, SelectProps } from "antd";
9 | import { LabeledValue } from "antd/lib/select";
10 | import Fuse from "fuse.js";
11 | import { omit } from "ramda";
12 | import {
13 | DashComponentProps,
14 | DashLoadingState,
15 | StyledComponentProps,
16 | } from "../../types";
17 |
18 | type Props = {
19 | /**
20 | * Show clear button
21 | */
22 | allow_clear?: boolean;
23 | /**
24 | * Whether the current search will be cleared on selecting an item.
25 | * Only applies when mode is set to multiple or tags
26 | */
27 | auto_clear_search_value?: boolean;
28 | /**
29 | * Whether has border style
30 | */
31 | bordered?: boolean;
32 | /**
33 | * The custom clear icon
34 | */
35 | clear_icon?: ReactNode;
36 | /**
37 | * Whether disabled select
38 | */
39 | disabled?: boolean;
40 | /**
41 | * The className of dropdown menu
42 | */
43 | dropdown_class_name?: string;
44 | /**
45 | * Determine whether the dropdown menu and the select input are the same width.
46 | * Default set min-width same as input. Will ignore when value less than
47 | * select width false will disable virtual scroll
48 | */
49 | dropdown_match_select_width?: boolean | number;
50 | /**
51 | * The style of dropdown menu
52 | */
53 | dropdown_style?: object;
54 | /**
55 | * Customize node label, value, options field name
56 | */
57 | fieldNames?: object; // { label: label, value: value, options: options } 4.17.0
58 | /**
59 | * If true, filter options by input
60 | */
61 | filter_option?: boolean;
62 | /**
63 | * Whether to embed label in value, turn the format of value from string to { value: string, label: ReactNode }
64 | */
65 | label_in_value?: boolean;
66 | /**
67 | * Config popup height
68 | */
69 | list_height?: number;
70 | /**
71 | * Indicate loading state
72 | */
73 | loading?: boolean;
74 | /**
75 | * Max tag count to show. responsive will cost render performance
76 | */
77 | max_tag_count?: number | "responsive";
78 | /**
79 | * Max tag text length to show
80 | */
81 | max_tag_text_length?: number;
82 | /**
83 | * The custom menuItemSelected icon with multiple options
84 | */
85 | menu_item_selected_icon?: ReactNode;
86 | /**
87 | * Set mode of Select
88 | */
89 | mode?: "multiple" | "tags";
90 | /**
91 | * Controlled open state of dropdown
92 | */
93 | open?: boolean;
94 | /**
95 | * Which prop value of option will be used for filter if filterOption is true.
96 | * If options is set, it should be set to 'label'
97 | */
98 | option_filter_prop?: string;
99 | /**
100 | * Which prop value of option will render as content of select.
101 | */
102 | option_label_prop?: string;
103 | /**
104 | * Select options. Will get better perf than jsx definition { label, value }[]
105 | */
106 | options?: object[];
107 | /**
108 | * Placeholder of select
109 | */
110 | placeholder?: string;
111 | /**
112 | * The position where the selection box pops up
113 | */
114 | placement?: "bottomLeft" | "bottomRight" | "topLeft" | "topRight";
115 | /**
116 | * The custom remove icon
117 | */
118 | remove_icon?: ReactNode;
119 | /**
120 | * Whether to show the drop-down arrow
121 | */
122 | show_arrow?: boolean;
123 | /**
124 | * Whether show search input and filter options by text entered in field
125 | */
126 | use_search?: boolean;
127 | /**
128 | * Fields to search in documents (options). Search is based on fuse.js, with
129 | * corresponding field notations https://fusejs.io/examples.html
130 | */
131 | search_fields?: string[];
132 | /**
133 | * The maximum number of results to show in search
134 | */
135 | search_max_results?: number;
136 | /**
137 | * Size of Select input
138 | */
139 | size?: "large" | "middle" | "small";
140 | /**
141 | * Set validation status
142 | */
143 | status?: "error" | "warning";
144 | /**
145 | * The custom suffix icon
146 | */
147 | suffix_icon?: ReactNode;
148 | /**
149 | * Separator used to tokenize, only applies when mode="tags"
150 | */
151 | token_separators?: string[];
152 | /**
153 | * Current selected option (considered as a immutable array)
154 | */
155 | value?:
156 | | string
157 | | string[]
158 | | number
159 | | number[]
160 | | LabeledValue
161 | | LabeledValue[];
162 | /**
163 | * Disable virtual scroll when set to false
164 | */
165 | virtual?: boolean;
166 | /**
167 | * Object that holds the loading state object coming from dash-renderer
168 | */
169 | loading_state?: DashLoadingState;
170 | /**
171 | * Number of times the input lost focus.
172 | */
173 | n_blur: number;
174 | /**
175 | * Last time the input lost focus.
176 | */
177 | n_blur_timestamp: number;
178 | /**
179 | * Number of times the `Enter` key was pressed while the input had focus.
180 | */
181 | n_submit: number;
182 | /**
183 | * Last time that `Enter` was pressed.
184 | */
185 | n_submit_timestamp: number;
186 | } & DashComponentProps &
187 | StyledComponentProps;
188 |
189 | /**
190 | * A dropdown component
191 | */
192 | const Select = (props: Props) => {
193 | const {
194 | allow_clear,
195 | auto_clear_search_value,
196 | class_name,
197 | clear_icon,
198 | dropdown_class_name,
199 | dropdown_match_select_width,
200 | dropdown_style,
201 | filter_option,
202 | label_in_value,
203 | list_height,
204 | loading,
205 | loading_state,
206 | max_tag_count,
207 | max_tag_text_length,
208 | menu_item_selected_icon,
209 | option_filter_prop,
210 | option_label_prop,
211 | remove_icon,
212 | show_arrow,
213 | use_search,
214 | suffix_icon,
215 | token_separators,
216 | n_blur,
217 | n_submit,
218 | open,
219 | disabled,
220 | options,
221 | search_fields,
222 | search_max_results,
223 | setProps,
224 | ...otherProps
225 | } = props;
226 | const [currentOptions, setCurrentOptions] = useState([]);
227 |
228 | const onSearch = useMemo(() => {
229 | if (!use_search) return undefined;
230 |
231 | const fuseOptions = {
232 | includeScore: true,
233 | keys: search_fields || ["value", "label"],
234 | };
235 | const fuse = new Fuse(options, fuseOptions);
236 |
237 | return (value: string) => {
238 | if (!value || value.length === 0) {
239 | setCurrentOptions(options);
240 | } else {
241 | setCurrentOptions(
242 | fuse
243 | .search(value, { limit: search_max_results || 10 })
244 | .map((res) => res.item)
245 | );
246 | }
247 | };
248 | }, [options, use_search, search_fields, search_max_results]);
249 |
250 | useEffect(() => {
251 | setCurrentOptions(options);
252 | }, [options]);
253 |
254 | const onBlur: SelectProps["onBlur"] = useCallback(() => {
255 | if (!disabled && setProps) {
256 | setProps({
257 | n_blur: n_blur + 1,
258 | n_blur_timestamp: Date.now(),
259 | });
260 | }
261 | }, [setProps, n_blur, disabled]);
262 |
263 | const onDropdownVisibleChange: SelectProps["onDropdownVisibleChange"] =
264 | useCallback(
265 | (open) => {
266 | if (!disabled && setProps) {
267 | setProps({ open });
268 | }
269 | },
270 | [setProps, disabled]
271 | );
272 |
273 | const onChange: SelectProps["onChange"] = useCallback(
274 | (value) => {
275 | if (!disabled && setProps) {
276 | setProps({ value });
277 | setCurrentOptions(options);
278 | }
279 | },
280 | [setProps, disabled, options]
281 | );
282 |
283 | const onKeyPress: SelectProps["onInputKeyDown"] = useCallback(
284 | (e) => {
285 | if (!disabled && setProps && e.key === "Enter") {
286 | setProps({
287 | n_submit: n_submit + 1,
288 | n_submit_timestamp: Date.now(),
289 | });
290 | }
291 | },
292 | [setProps, n_submit, disabled]
293 | );
294 |
295 | return (
296 |
329 | );
330 | };
331 |
332 | Select.defaultProps = {
333 | n_blur: 0,
334 | n_blur_timestamp: -1,
335 | n_submit: 0,
336 | n_submit_timestamp: -1,
337 | };
338 |
339 | export default Select;
340 |
--------------------------------------------------------------------------------