diff --git a/@plotly/dash-generator-test-component-typescript/generator.test.ts b/@plotly/dash-generator-test-component-typescript/generator.test.ts index 7ad1546bd3..e84d16aa25 100644 --- a/@plotly/dash-generator-test-component-typescript/generator.test.ts +++ b/@plotly/dash-generator-test-component-typescript/generator.test.ts @@ -95,6 +95,10 @@ describe('Test Typescript component metadata generation', () => { `${componentName} element JSX.Element`, testTypeFactory('element', 'node') ); + test( + `${componentName} dash_component DashComponent`, + testTypeFactory("dash_component", "node"), + ); test( `${componentName} boolean type`, testTypeFactory('a_bool', 'bool') diff --git a/@plotly/dash-generator-test-component-typescript/src/props.ts b/@plotly/dash-generator-test-component-typescript/src/props.ts index e576786a96..46b320f005 100644 --- a/@plotly/dash-generator-test-component-typescript/src/props.ts +++ b/@plotly/dash-generator-test-component-typescript/src/props.ts @@ -1,6 +1,12 @@ // Needs to export types if not in a d.ts file or if any import is present in the d.ts import React from 'react'; +type DashComponent = { + props: string; + namespace: string; + children?: []; +} + type Nested = { nested: Nested; @@ -36,6 +42,7 @@ export type TypescriptComponentProps = { | boolean; element?: JSX.Element; array_elements?: JSX.Element[]; + dash_component?: DashComponent; string_default?: string; number_default?: number; diff --git a/CHANGELOG.md b/CHANGELOG.md index 24ab656228..99bddc4ef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to `dash` will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). +## UNRELEASED + +## Added +- Modernized `dcc.Tabs` + +## Changed +- `dcc.Tab` now accepts a `width` prop which can be a pixel or percentage width for an individual tab. +- `dcc.Tab` can accept other Dash Components for its label, in addition to a simple string. + ## [4.0.0rc2] - 2025-10-10 ## Added diff --git a/components/dash-core-components/src/components/Tab.react.js b/components/dash-core-components/src/components/Tab.react.js deleted file mode 100644 index 273ea16286..0000000000 --- a/components/dash-core-components/src/components/Tab.react.js +++ /dev/null @@ -1,79 +0,0 @@ -import React, {Fragment} from 'react'; -import PropTypes from 'prop-types'; - -/** - * Part of dcc.Tabs - this is the child Tab component used to render a tabbed page. - * Its children will be set as the content of that tab, which if clicked will become visible. - */ - -/* eslint-disable no-unused-vars */ -const Tab = ({ - children, - disabled = false, - disabled_style = {color: '#d6d6d6'}, -}) => {children}; -/* eslint-enable no-unused-vars */ - -// Default props are defined above for proper docstring generation in React 18. -// The actual default values are set in Tabs.react.js. - -Tab.propTypes = { - /** - * The ID of this component, used to identify dash components - * in callbacks. The ID needs to be unique across all of the - * components in an app. - */ - id: PropTypes.string, - - /** - * The tab's label - */ - label: PropTypes.string, - - /** - * The content of the tab - will only be displayed if this tab is selected - */ - children: PropTypes.node, - - /** - * Value for determining which Tab is currently selected - */ - value: PropTypes.string, - - /** - * Determines if tab is disabled or not - defaults to false - */ - disabled: PropTypes.bool, - - /** - * Overrides the default (inline) styles when disabled - */ - disabled_style: PropTypes.object, - - /** - * Appends a class to the Tab component when it is disabled. - */ - disabled_className: PropTypes.string, - - /** - * Appends a class to the Tab component. - */ - className: PropTypes.string, - - /** - * Appends a class to the Tab component when it is selected. - */ - selected_className: PropTypes.string, - - /** - * Overrides the default (inline) styles for the Tab component. - */ - style: PropTypes.object, - - /** - * Overrides the default (inline) styles for the Tab component when it is selected. - */ - selected_style: PropTypes.object, -}; - -export default Tab; diff --git a/components/dash-core-components/src/components/Tab.tsx b/components/dash-core-components/src/components/Tab.tsx new file mode 100644 index 0000000000..935c9d1c4c --- /dev/null +++ b/components/dash-core-components/src/components/Tab.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import './css/tabs.css'; +import {TabProps} from '../types'; + +/** + * Part of dcc.Tabs - this is the child Tab component used to render a tabbed page. + * Its children will be set as the content of that tab, which if clicked will become visible. + */ +function Tab({ + children, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + disabled = false, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + disabled_style = {color: 'var(--Dash-Text-Disabled)'}, +}: TabProps) { + return <>{children}; +} + +export default Tab; diff --git a/components/dash-core-components/src/components/Tabs.react.js b/components/dash-core-components/src/components/Tabs.react.js deleted file mode 100644 index 18acbe08c4..0000000000 --- a/components/dash-core-components/src/components/Tabs.react.js +++ /dev/null @@ -1,435 +0,0 @@ -/* eslint-disable react/prop-types */ -import React, {Component} from 'react'; -import PropTypes from 'prop-types'; -import {has, is, isNil} from 'ramda'; - -// some weird interaction btwn styled-jsx 3.4 and babel -// see https://github.com/vercel/styled-jsx/pull/716 -import _JSXStyle from 'styled-jsx/style'; // eslint-disable-line no-unused-vars -import LoadingElement from '../utils/LoadingElement'; - -// EnhancedTab is defined here instead of in Tab.react.js because if exported there, -// it will mess up the Python imports and metadata.json -const EnhancedTab = ({ - id, - label, - selected, - className, - style, - selectedClassName, - selected_style, - selectHandler, - value, - disabled = false, - disabled_style = {color: '#d6d6d6'}, - disabled_className, - mobile_breakpoint, - amountOfTabs, - colors, - vertical, - componentPath, -}) => { - const ctx = window.dash_component_api.useDashContext(); - // We use the raw path here since it's up one level from - // the tabs child. - const isLoading = ctx.useLoading({rawPath: componentPath}); - - let tabStyle = style; - if (disabled) { - tabStyle = {tabStyle, ...disabled_style}; - } - if (selected) { - tabStyle = {tabStyle, ...selected_style}; - } - let tabClassName = `tab ${className || ''}`; - if (disabled) { - tabClassName += ` tab--disabled ${disabled_className || ''}`; - } - if (selected) { - tabClassName += ` tab--selected ${selectedClassName || ''}`; - } - let labelDisplay; - if (is(Array, label)) { - // label is an array, so it has children that we want to render - labelDisplay = label[0].props.children; - } else { - // else it is a string, so we just want to render that - labelDisplay = label; - } - return ( -
{ - if (!disabled) { - selectHandler(value); - } - }} - > - {labelDisplay} - -
- ); -}; - -/** - * A Dash component that lets you render pages with tabs - the Tabs component's children - * can be dcc.Tab components, which can hold a label that will be displayed as a tab, and can in turn hold - * children components that will be that tab's content. - */ -export default class Tabs extends Component { - constructor(props) { - super(props); - - this.selectHandler = this.selectHandler.bind(this); - - if (!has('value', this.props)) { - this.props.setProps({ - value: this.valueOrDefault(), - }); - } - } - - valueOrDefault() { - if (has('value', this.props)) { - return this.props.value; - } - const children = this.parseChildrenToArray(); - if (children && children.length) { - const firstChildren = window.dash_component_api.getLayout([ - ...children[0].props.componentPath, - 'props', - 'value', - ]); - return firstChildren || 'tab-1'; - } - return 'tab-1'; - } - - parseChildrenToArray() { - if (this.props.children && !is(Array, this.props.children)) { - // if dcc.Tabs.children contains just one single element, it gets passed as an object - // instead of an array - so we put it in an array ourselves! - return [this.props.children]; - } - return this.props.children; - } - - selectHandler(value) { - this.props.setProps({value: value}); - } - - render() { - let EnhancedTabs; - let selectedTab; - - const value = this.valueOrDefault(); - - if (this.props.children) { - const children = this.parseChildrenToArray(); - - const amountOfTabs = children.length; - - EnhancedTabs = children.map((child, index) => { - // TODO: handle components that are not dcc.Tab components (throw error) - // enhance Tab components coming from Dash (as dcc.Tab) with methods needed for handling logic - let childProps; - - if (React.isValidElement(child)) { - childProps = window.dash_component_api.getLayout([ - ...child.props.componentPath, - 'props', - ]); - } else { - // In case the selected tab is a string. - childProps = {}; - } - - if (!childProps.value) { - childProps = {...childProps, value: `tab-${index + 1}`}; - } - - // check if this child/Tab is currently selected - if (childProps.value === value) { - selectedTab = child; - } - - return ( - - ); - }); - } - - const selectedTabContent = !isNil(selectedTab) ? selectedTab : ''; - - const tabContainerClass = this.props.vertical - ? 'tab-container tab-container--vert' - : 'tab-container'; - - const tabContentClass = this.props.vertical - ? 'tab-content tab-content--vert' - : 'tab-content'; - - const tabParentClass = this.props.vertical - ? 'tab-parent tab-parent--vert' - : 'tab-parent'; - - return ( - -
- {EnhancedTabs} -
-
- {selectedTabContent || ''} -
- -
- ); - } -} - -Tabs.defaultProps = { - mobile_breakpoint: 800, - colors: { - border: '#d6d6d6', - primary: '#1975FA', - background: '#f9f9f9', - }, - vertical: false, - persisted_props: ['value'], - persistence_type: 'local', -}; - -Tabs.propTypes = { - /** - * The ID of this component, used to identify dash components - * in callbacks. The ID needs to be unique across all of the - * components in an app. - */ - id: PropTypes.string, - - /** - * The value of the currently selected Tab - */ - value: PropTypes.string, - - /** - * Appends a class to the Tabs container holding the individual Tab components. - */ - className: PropTypes.string, - - /** - * Appends a class to the Tab content container holding the children of the Tab that is selected. - */ - content_className: PropTypes.string, - - /** - * Appends a class to the top-level parent container holding both the Tabs container and the content container. - */ - parent_className: PropTypes.string, - - /** - * Appends (inline) styles to the Tabs container holding the individual Tab components. - */ - style: PropTypes.object, - - /** - * Appends (inline) styles to the top-level parent container holding both the Tabs container and the content container. - */ - parent_style: PropTypes.object, - - /** - * Appends (inline) styles to the tab content container holding the children of the Tab that is selected. - */ - content_style: PropTypes.object, - - /** - * Renders the tabs vertically (on the side) - */ - vertical: PropTypes.bool, - - /** - * Breakpoint at which tabs are rendered full width (can be 0 if you don't want full width tabs on mobile) - */ - mobile_breakpoint: PropTypes.number, - - /** - * Array that holds Tab components - */ - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node, - ]), - - /** - * Holds the colors used by the Tabs and Tab components. If you set these, you should specify colors for all properties, so: - * colors: { - * border: '#d6d6d6', - * primary: '#1975FA', - * background: '#f9f9f9' - * } - */ - colors: PropTypes.exact({ - border: PropTypes.string, - primary: PropTypes.string, - background: PropTypes.string, - }), - - /** - * Used to allow user interactions in this component to be persisted when - * the component - or the page - is refreshed. If `persisted` is truthy and - * hasn't changed from its previous value, a `value` that the user has - * changed while using the app will keep that change, as long as - * the new `value` also matches what was given originally. - * Used in conjunction with `persistence_type`. - */ - persistence: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - PropTypes.number, - ]), - - /** - * Properties whose user interactions will persist after refreshing the - * component or the page. Since only `value` is allowed this prop can - * normally be ignored. - */ - persisted_props: PropTypes.arrayOf(PropTypes.oneOf(['value'])), - - /** - * Where persisted user changes will be stored: - * memory: only kept in memory, reset on page refresh. - * local: window.localStorage, data is kept after the browser quit. - * session: window.sessionStorage, data is cleared once the browser quit. - */ - persistence_type: PropTypes.oneOf(['local', 'session', 'memory']), -}; - -Tabs.dashChildrenUpdate = true; diff --git a/components/dash-core-components/src/components/Tabs.tsx b/components/dash-core-components/src/components/Tabs.tsx new file mode 100644 index 0000000000..b84a8c8dc4 --- /dev/null +++ b/components/dash-core-components/src/components/Tabs.tsx @@ -0,0 +1,311 @@ +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {has, isNil} from 'ramda'; + +import LoadingElement from '../utils/_LoadingElement'; +import {PersistedProps, PersistenceTypes, TabProps, TabsProps} from '../types'; +import './css/tabs.css'; +import {DashComponent} from '@dash-renderer/types'; + +interface EnhancedTabProps extends TabProps { + selected: boolean; + componentPath?: (string | number)[]; +} + +// EnhancedTab is defined here instead of in Tab.tsx because if exported there, +// it will mess up the Python imports and metadata.json +const EnhancedTab = ({ + id, + label, + selected, + className, + style, + selected_className, + selected_style, + setProps: selectHandler, + value, + disabled = false, + disabled_style = {color: 'var(--Dash-Text-Disabled)'}, + disabled_className, + componentPath, +}: EnhancedTabProps) => { + const ExternalWrapper = window.dash_component_api.ExternalWrapper; + const ctx = window.dash_component_api.useDashContext(); + componentPath = componentPath ?? ctx.componentPath; + // We use the raw path here since it's up one level from + // the tabs child. + const isLoading = ctx.useLoading({rawPath: componentPath}); + const tabStyle = useMemo(() => { + return { + ...style, + ...(disabled ? disabled_style : {}), + ...(selected ? selected_style : {}), + }; + }, [style, disabled, disabled_style, selected, selected_style]); + + const tabClassNames = useMemo(() => { + let names = 'tab'; + if (disabled) { + names += ' tab--disabled'; + if (disabled_className) { + names += ` ${disabled_className}`; + } + } + if (selected) { + names += ' tab--selected'; + if (selected_className) { + names += ` ${selected_className}`; + } + } + if (className) { + names += ` ${className}`; + } + return names; + }, [className, disabled, disabled_className, selected, selected_className]); + + let labelDisplay; + if (typeof label === 'object') { + labelDisplay = ( + + ); + } else { + labelDisplay = {label}; + } + + return ( +
{ + if (!disabled) { + selectHandler({value}); + } + }} + > + {labelDisplay} +
+ ); +}; + +/** + * A Dash component that lets you render pages with tabs - the Tabs component's children + * can be dcc.Tab components, which can hold a label that will be displayed as a tab, and can in turn hold + * children components that will be that tab's content. + */ +function Tabs({ + // eslint-disable-next-line no-magic-numbers + mobile_breakpoint = 800, + colors = { + border: 'var(--Dash-Stroke-Weak)', + primary: 'var(--Dash-Fill-Interactive-Strong)', + background: 'var(--Dash-Fill-Weak)', + }, + vertical = false, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + persisted_props = [PersistedProps.value], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + persistence_type = PersistenceTypes.local, + children, + ...props +}: TabsProps) { + const initializedRef = useRef(false); + const [isAboveBreakpoint, setIsAboveBreakpoint] = useState(false); + + const parseChildrenToArray = useCallback((): DashComponent[] => { + if (!children) { + return []; + } + if (children instanceof Array) { + return children; + } + return [children]; + }, [children]); + + const valueOrDefault = (): string | undefined => { + if (has('value', props)) { + return props.value; + } + const children = parseChildrenToArray(); + if (children && children.length && children[0].props.componentPath) { + const firstChildren: TabProps = window.dash_component_api.getLayout( + [...children[0].props.componentPath, 'props'] + ); + return firstChildren.value ?? 'tab-1'; + } + return 'tab-1'; + }; + + // Initialize value on mount if not set + useEffect(() => { + if (!initializedRef.current && !has('value', props)) { + props.setProps({ + value: `${valueOrDefault()}`, + }); + initializedRef.current = true; + } + }, []); + + // Setup matchMedia for responsive breakpoint + useEffect(() => { + const mediaQuery = window.matchMedia( + `(min-width: ${mobile_breakpoint}px)` + ); + + // Set initial value + setIsAboveBreakpoint(mediaQuery.matches); + + // Listen for changes + const handler = (e: MediaQueryListEvent) => + setIsAboveBreakpoint(e.matches); + mediaQuery.addEventListener('change', handler); + + return () => mediaQuery.removeEventListener('change', handler); + }, [mobile_breakpoint]); + + let EnhancedTabs: JSX.Element[]; + let selectedTab; + + const value = valueOrDefault(); + + if (children) { + const children = parseChildrenToArray(); + + EnhancedTabs = children.map((child, index) => { + // TODO: handle components that are not dcc.Tab components (throw error) + // enhance Tab components coming from Dash (as dcc.Tab) with methods needed for handling logic + let childProps: Omit; + + if (React.isValidElement(child) && child.props.componentPath) { + childProps = window.dash_component_api.getLayout([ + ...child.props.componentPath, + 'props', + ]); + } else { + // In case the selected tab is a string. + childProps = {}; + } + + if (!childProps.value) { + childProps = {...childProps, value: `tab-${index + 1}`}; + } + + // check if this child/Tab is currently selected + if (childProps.value === value) { + selectedTab = child; + } + + const style = childProps.style ?? {}; + if (typeof childProps.width === 'number') { + style.width = `${childProps.width}px`; + style.flex = '0 0 auto'; + } else if (typeof childProps.width === 'string') { + style.width = childProps.width; + style.flex = '0 0 auto'; + } + + return ( + + ); + }); + } + + const selectedTabContent = !isNil(selectedTab) ? selectedTab : ''; + + const tabContainerClassNames = useMemo(() => { + let names = 'tab-container'; + if (vertical) { + names += ` tab-container--vert`; + } + if (props.className) { + names += ` ${props.className}`; + } + return names; + }, [vertical, props.className]); + + const tabContentClassNames = useMemo(() => { + let names = 'tab-content'; + if (vertical) { + names += ` tab-content--vert`; + } + if (props.content_className) { + names += ` ${props.content_className}`; + } + return names; + }, [vertical, props.content_className]); + + const tabParentClassNames = useMemo(() => { + let names = 'tab-parent'; + if (vertical) { + names += ` tab-parent--vert`; + } + if (isAboveBreakpoint) { + names += ' tab-parent--above-breakpoint'; + } + if (props.parent_className) { + names += ` ${props.parent_className}`; + } + return names; + }, [vertical, isAboveBreakpoint, props.parent_className]); + + // Set CSS variables for dynamic styling + const cssVars = { + '--tabs-border': colors.border, + '--tabs-primary': colors.primary, + '--tabs-background': colors.background, + } as const; + + return ( + + {loadingProps => ( +
+
+ {EnhancedTabs} +
+
+ {selectedTabContent || ''} +
+
+ )} +
+ ); +} + +Tabs.dashPersistence = { + persisted_props: [PersistedProps.value], + persistence_type: PersistenceTypes.local, +}; + +Tabs.dashChildrenUpdate = true; + +export default Tabs; diff --git a/components/dash-core-components/src/components/css/tabs.css b/components/dash-core-components/src/components/css/tabs.css new file mode 100644 index 0000000000..991e4401c2 --- /dev/null +++ b/components/dash-core-components/src/components/css/tabs.css @@ -0,0 +1,113 @@ +/* Tab parent container */ +.tab-parent { + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* Tab container (holds all tabs) */ +.tab-container { + display: flex; + flex-direction: column; +} + +.tab-container--vert { + display: inline-flex; +} + +/* Individual tab */ +.tab { + flex: 1 1 0; + min-width: 0; + background-color: var(--tabs-background); + border: 1px solid var(--tabs-border); + border-bottom: none; + padding: 20px 25px; + transition: background-color, color 200ms; + text-align: center; + box-sizing: border-box; + cursor: pointer; +} + +/* Tab selected state */ +.tab--selected { + border-top: 2px solid var(--tabs-primary); + color: black; + background-color: white; +} + +.tab--selected:hover { + background-color: white; +} + +/* Tab disabled state */ +.tab--disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Tab content area */ +.tab-content--vert { + display: inline-flex; + flex-direction: column; +} + +/* Desktop/tablet styles (when above breakpoint) */ +.tab-parent--above-breakpoint .tab { + border: 1px solid var(--tabs-border); + border-right: none; +} + +.tab-parent--above-breakpoint .tab:last-child { + border-right: 1px solid var(--tabs-border); + border-bottom: 1px solid var(--tabs-border); +} + +.tab-parent--above-breakpoint .tab--selected, +.tab-parent--above-breakpoint .tab:last-child.tab--selected { + border-bottom: none; +} + +/* Vertical tabs: left border for selected */ +.tab-parent--above-breakpoint .tab-container--vert .tab--selected { + border-left: 2px solid var(--tabs-primary); +} + +/* Horizontal tabs: top border for selected */ +.tab-parent--above-breakpoint + .tab-container:not(.tab-container--vert) + .tab--selected, +.tab-parent--above-breakpoint + .tab-container:not(.tab-container--vert) + .tab:last-child.tab--selected { + border-top: 2px solid var(--tabs-primary); +} + +.tab-parent--above-breakpoint .tab-container--vert .tab { + width: auto; + border-right: none !important; + border-bottom: none !important; +} + +.tab-parent--above-breakpoint .tab-container--vert .tab:last-child { + border-bottom: 1px solid var(--tabs-border) !important; +} + +.tab-parent--above-breakpoint .tab-container--vert .tab--selected { + border-top: 1px solid var(--tabs-border); + border-left: 2px solid var(--tabs-primary); + border-right: none; +} + +.tab-parent--above-breakpoint .tab-container { + flex-direction: row; +} + +.tab-parent--above-breakpoint .tab-container--vert { + flex-direction: column; +} + +.tab-parent--above-breakpoint.tab-parent--vert { + display: inline-flex; + flex-direction: row; +} diff --git a/components/dash-core-components/src/index.ts b/components/dash-core-components/src/index.ts index 4459c0f4c4..e95ae69301 100644 --- a/components/dash-core-components/src/index.ts +++ b/components/dash-core-components/src/index.ts @@ -19,8 +19,8 @@ import RadioItems from './components/RadioItems'; import RangeSlider from './components/RangeSlider'; import Slider from './components/Slider'; import Store from './components/Store.react'; -import Tab from './components/Tab.react'; -import Tabs from './components/Tabs.react'; +import Tab from './components/Tab'; +import Tabs from './components/Tabs'; import Textarea from './components/Textarea'; import Tooltip from './components/Tooltip'; import Upload from './components/Upload.react'; diff --git a/components/dash-core-components/src/types.ts b/components/dash-core-components/src/types.ts index 98f6986444..7efa7cafcf 100644 --- a/components/dash-core-components/src/types.ts +++ b/components/dash-core-components/src/types.ts @@ -1,16 +1,5 @@ import React from 'react'; -import {DashComponent} from '@dash-renderer/types/component'; -import ExternalWrapper from '@dash-renderer/wrapper/ExternalWrapper'; -import {useDashContext} from '@dash-renderer/wrapper/DashContext'; - -declare global { - interface Window { - dash_component_api: { - useDashContext: typeof useDashContext; - ExternalWrapper: typeof ExternalWrapper; - }; - } -} +import {BaseDashProps, DashComponent} from '@dash-renderer/types'; export enum PersistenceTypes { 'local' = 'local', @@ -22,14 +11,7 @@ export enum PersistedProps { 'value' = 'value', } -export interface BaseComponentProps { - /** - * The ID of this component, used to identify dash components - * in callbacks. The ID needs to be unique across all of the - * components in an app. - */ - id?: string; - +export interface BaseDccProps extends BaseDashProps { /** * Additional CSS class for the root DOM node */ @@ -122,7 +104,7 @@ export type SliderTooltip = { transform?: string; }; -export interface SliderProps extends BaseComponentProps { +export interface SliderProps extends BaseDccProps { /** * Minimum allowed value of the slider */ @@ -209,7 +191,7 @@ export interface SliderProps extends BaseComponentProps { verticalHeight?: number; } -export interface RangeSliderProps extends BaseComponentProps { +export interface RangeSliderProps extends BaseDccProps { /** * Minimum allowed value of the slider */ @@ -356,7 +338,7 @@ export type OptionsArray = (OptionValue | DetailedOption)[]; */ export type OptionsDict = Record; -export interface DropdownProps extends BaseComponentProps { +export interface DropdownProps extends BaseDccProps { /** * An array of options {label: [string|number], value: [string|number]}, * an optional disabled field can be used for each option @@ -439,7 +421,7 @@ export interface DropdownProps extends BaseComponentProps { }; } -export interface ChecklistProps extends BaseComponentProps { +export interface ChecklistProps extends BaseDccProps { /** * An array of options */ @@ -484,7 +466,7 @@ export interface ChecklistProps extends BaseComponentProps { labelClassName?: string; } -export interface RadioItemsProps extends BaseComponentProps { +export interface RadioItemsProps extends BaseDccProps { /** * An array of options */ @@ -529,7 +511,7 @@ export interface RadioItemsProps extends BaseComponentProps { labelClassName?: string; } -export interface TextAreaProps extends BaseComponentProps { +export interface TextAreaProps extends BaseDccProps { /** * The value of the textarea */ @@ -753,7 +735,7 @@ export interface TooltipProps { setProps: (props: Partial) => void; } -export interface LoadingProps extends BaseComponentProps { +export interface LoadingProps extends BaseDccProps { /** * Array that holds components to render */ @@ -836,3 +818,130 @@ export interface LoadingProps extends BaseComponentProps { */ custom_spinner?: React.ReactNode; } + +export interface TabsProps extends BaseDccProps { + /** + * The value of the currently selected Tab + */ + value?: string; + + /** + * Appends a class to the Tab content container holding the children of the Tab that is selected. + */ + content_className?: string; + + /** + * Appends a class to the top-level parent container holding both the Tabs container and the content container. + */ + parent_className?: string; + + /** + * Appends (inline) styles to the Tabs container holding the individual Tab components. + */ + style?: React.CSSProperties; + + /** + * Appends (inline) styles to the top-level parent container holding both the Tabs container and the content container. + */ + parent_style?: React.CSSProperties; + + /** + * Appends (inline) styles to the tab content container holding the children of the Tab that is selected. + */ + content_style?: React.CSSProperties; + + /** + * Renders the tabs vertically (on the side) + */ + vertical?: boolean; + + /** + * Breakpoint at which tabs are rendered full width (can be 0 if you don't want full width tabs on mobile) + */ + mobile_breakpoint?: number; + + /** + * Array that holds Tab components + */ + children?: DashComponent; + + /** + * Holds the colors used by the Tabs and Tab components. If you set these, you should specify colors for all properties, so: + * colors: { + * border: '#d6d6d6', + * primary: '#1975FA', + * background: '#f9f9f9' + * } + */ + colors?: { + border: string; + primary: string; + background: string; + }; +} + +// Note a quirk in how this extends the BaseComponentProps: `setProps` is shared +// with `TabsProps` (plural!) due to how tabs are implemented. This is +// intentional. +export interface TabProps extends BaseDccProps { + /** + * The tab's label + */ + label?: string | DashComponent; + + /** + * The content of the tab - will only be displayed if this tab is selected + */ + children?: DashComponent; + + /** + * Value for determining which Tab is currently selected + */ + value?: string; + + /** + * Determines if tab is disabled or not - defaults to false + */ + disabled?: boolean; + + /** + * Overrides the default (inline) styles when disabled + */ + disabled_style?: React.CSSProperties; + + /** + * Appends a class to the Tab component when it is disabled. + */ + disabled_className?: string; + + /** + * Appends a class to the Tab component. + */ + className?: string; + + /** + * Appends a class to the Tab component when it is selected. + */ + selected_className?: string; + + /** + * Overrides the default (inline) styles for the Tab component. + */ + style?: React.CSSProperties; + + /** + * Overrides the default (inline) styles for the Tab component when it is selected. + */ + selected_style?: React.CSSProperties; + + /** + * A custom width for this tab, in the format of `50px` or `50%`; numbers + * are treated as pixel values. By default, there is no width and this Tab + * is evenly spaced along with all the other tabs to occupy the available + * space. Setting this value will "fix" this tab width to the given size. + * while the other "non-fixed" tabs will continue to automatically + * occupying the remaining available space. + * This property has no effect when tabs are displayed vertically. + */ + width?: string | number; +} diff --git a/components/dash-core-components/tests/integration/misc/test_platter.py b/components/dash-core-components/tests/integration/misc/test_platter.py index 4689c7ce04..de83e8577e 100644 --- a/components/dash-core-components/tests/integration/misc/test_platter.py +++ b/components/dash-core-components/tests/integration/misc/test_platter.py @@ -6,6 +6,7 @@ def test_mspl001_dcc_components_platter(platter_app, dash_dcc): + dash_dcc.driver.set_window_size(800, 600) dash_dcc.start_server(platter_app) dash_dcc.wait_for_element("#waitfor") diff --git a/components/dash-core-components/tests/integration/tab/test_tabs.py b/components/dash-core-components/tests/integration/tab/test_tabs.py index 0a9b4410d4..d4fce89f14 100644 --- a/components/dash-core-components/tests/integration/tab/test_tabs.py +++ b/components/dash-core-components/tests/integration/tab/test_tabs.py @@ -34,6 +34,7 @@ def test_tabs001_in_vertical_mode(dash_dcc): ] ) + dash_dcc.driver.set_window_size(800, 600) dash_dcc.start_server(app) dash_dcc.wait_for_text_to_equal("#tab-3", "Tab three") dash_dcc.percy_snapshot("Core Tabs - vertical mode") @@ -67,6 +68,7 @@ def render_content(tab): elif tab == "tab-2": return html.Div([html.H3("Test content 2")], id="test-tab-2") + dash_dcc.driver.set_window_size(800, 600) dash_dcc.start_server(app) dash_dcc.wait_for_text_to_equal("#tabs-content", "Test content 2") dash_dcc.percy_snapshot("Core initial tab - tab 2") @@ -87,6 +89,7 @@ def test_tabs003_without_children_undefined(dash_dcc): id="app", ) + dash_dcc.driver.set_window_size(800, 600) dash_dcc.start_server(app) dash_dcc.wait_for_element("#tabs-content") assert dash_dcc.find_element("#app").text == "Dash Tabs component demo" @@ -122,6 +125,7 @@ def render_content(tab): elif tab == "tab-2": return html.H3("Tab content 2") + dash_dcc.driver.set_window_size(800, 600) dash_dcc.start_server(app) dash_dcc.wait_for_text_to_equal("#tabs-content", "Default selected Tab content 1") assert dash_dcc.get_logs() == [] @@ -155,6 +159,7 @@ def test_tabs005_disabled(dash_dcc): ] ) + dash_dcc.driver.set_window_size(800, 600) dash_dcc.start_server(app) dash_dcc.wait_for_element("#tab-2") diff --git a/components/dash-core-components/tests/integration/tab/test_tabs_with_graphs.py b/components/dash-core-components/tests/integration/tab/test_tabs_with_graphs.py index 48b085850d..7b98de6d39 100644 --- a/components/dash-core-components/tests/integration/tab/test_tabs_with_graphs.py +++ b/components/dash-core-components/tests/integration/tab/test_tabs_with_graphs.py @@ -64,6 +64,7 @@ def render_content(tab): ] ) + dash_dcc.driver.set_window_size(800, 600) dash_dcc.start_server(app) tab_one = dash_dcc.wait_for_element("#tab-1") @@ -156,6 +157,7 @@ def on_click_update_graph(n_clicks): "layout": {"width": 700, "height": 450}, } + dash_dcc.driver.set_window_size(800, 600) dash_dcc.start_server(app) button_one = dash_dcc.wait_for_element("#one") diff --git a/dash/dash-renderer/src/dashApi.ts b/dash/dash-renderer/src/dashApi.ts index f77b2f2e17..29b2e72ce3 100644 --- a/dash/dash-renderer/src/dashApi.ts +++ b/dash/dash-renderer/src/dashApi.ts @@ -4,6 +4,7 @@ import {getPath} from './actions/paths'; import {getStores} from './utils/stores'; import ExternalWrapper from './wrapper/ExternalWrapper'; import {stringifyId} from './actions/dependencies'; +import {DashLayoutPath} from './types/component'; import { DevtoolContext, useDevtool, @@ -17,7 +18,7 @@ import { * @param propPath Additional key to get the property instead of plain props. * @returns */ -function getLayout(componentPathOrId: string[] | string): any { +function getLayout(componentPathOrId: DashLayoutPath | string): any { const ds = getStores(); for (let y = 0; y < ds.length; y++) { const {paths, layout} = ds[y].getState(); @@ -34,7 +35,7 @@ function getLayout(componentPathOrId: string[] | string): any { } } -(window as any).dash_component_api = { +window.dash_component_api = { ExternalWrapper, DashContext, useDashContext, diff --git a/dash/dash-renderer/src/types/component.ts b/dash/dash-renderer/src/types/component.ts index 55c89c7660..2dc139b4c0 100644 --- a/dash/dash-renderer/src/types/component.ts +++ b/dash/dash-renderer/src/types/component.ts @@ -1,12 +1,39 @@ +import { + DashContext, + DashContextProviderProps, + useDashContext +} from '../wrapper/DashContext'; +import ExternalWrapper from '../wrapper/ExternalWrapper'; +import {stringifyId} from '../actions/dependencies'; +import { + DevtoolContext, + useDevtool, + useDevtoolMenuButtonClassName +} from '../components/error/menu/DevtoolContext'; + export type BaseDashProps = { id?: string; + componentPath?: DashLayoutPath; [key: string]: any; }; +export interface DashComponentApi { + ExternalWrapper: typeof ExternalWrapper; + DashContext: typeof DashContext; + useDashContext: typeof useDashContext; + getLayout: (componentPathOrId: DashLayoutPath | string) => any; + stringifyId: typeof stringifyId; + devtool: { + DevtoolContext: typeof DevtoolContext; + useDevtool: typeof useDevtool; + useDevtoolMenuButtonClassName: typeof useDevtoolMenuButtonClassName; + }; +} + export type DashComponent = { type: string; namespace: string; - props: BaseDashProps; + props: DashContextProviderProps & BaseDashProps; }; export type UpdatePropsPayload = { diff --git a/dash/dash-renderer/src/types/index.ts b/dash/dash-renderer/src/types/index.ts new file mode 100644 index 0000000000..786a724e71 --- /dev/null +++ b/dash/dash-renderer/src/types/index.ts @@ -0,0 +1,10 @@ +import {DashComponentApi} from './component'; + +declare global { + interface Window { + dash_component_api: DashComponentApi; + } +} + +export * from './component'; +export * from './callbacks'; diff --git a/dash/dash-renderer/src/wrapper/DashContext.tsx b/dash/dash-renderer/src/wrapper/DashContext.tsx index 1da719f664..6c03f47b2b 100644 --- a/dash/dash-renderer/src/wrapper/DashContext.tsx +++ b/dash/dash-renderer/src/wrapper/DashContext.tsx @@ -18,7 +18,7 @@ type LoadingOptions = { * Useful if you want the loading of a child component * as the path is available in `child.props.componentPath`. */ - rawPath?: boolean; + rawPath?: DashLayoutPath; /** * Function used to filter the properties of the loading component. * Filter argument is an Entry of `{path, property, id}`. @@ -40,7 +40,7 @@ type DashContextType = { export const DashContext = React.createContext({} as any); -type DashContextProviderProps = { +export type DashContextProviderProps = { children: JSX.Element; componentPath: DashLayoutPath; }; diff --git a/dash/extract-meta.js b/dash/extract-meta.js index aaaf73692e..eb017f7f04 100755 --- a/dash/extract-meta.js +++ b/dash/extract-meta.js @@ -67,7 +67,7 @@ const BANNED_TYPES = [ 'ChildNode', 'ParentNode', ]; -const unionSupport = PRIMITIVES.concat('true', 'false', 'Element', 'enum'); +const unionSupport = PRIMITIVES.concat('true', 'false', 'Element', 'enum', 'DashComponent'); /* Regex to capture typescript unions in different formats: * string[] @@ -257,13 +257,18 @@ function gatherComponents(sources, components = {}) { let name = 'union', value; - // Union only do base types + // Union only do base types & DashComponent types value = typeObj.types .filter(t => { let typeName = t.intrinsicName; if (!typeName) { if (t.members) { typeName = 'object'; + } else { + const typeString = checker.typeToString(t); + if (typeString === 'DashComponent') { + typeName = 'node'; + } } } if (t.value) { @@ -307,7 +312,8 @@ function gatherComponents(sources, components = {}) { } else if ( propName === 'Element' || propName === 'ReactNode' || - propName === 'ReactElement' + propName === 'ReactElement' || + propName === 'DashComponent' ) { return 'node'; }