├── .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 | ); 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(