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';
}