From 427723d01013e8746bbd909b4c39d004baa22db9 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Tue, 7 Oct 2025 09:23:46 -0600 Subject: [PATCH 01/14] Refactor Tabs component --- .../src/components/Tabs.react.js | 435 ------------------ .../src/components/Tabs.tsx | 278 +++++++++++ .../src/components/css/tabs.css | 119 +++++ components/dash-core-components/src/index.ts | 2 +- components/dash-core-components/src/types.ts | 69 ++- dash/dash-renderer/src/dashApi.ts | 8 + 6 files changed, 469 insertions(+), 442 deletions(-) delete mode 100644 components/dash-core-components/src/components/Tabs.react.js create mode 100644 components/dash-core-components/src/components/Tabs.tsx create mode 100644 components/dash-core-components/src/components/css/tabs.css 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..95a8332fa9 --- /dev/null +++ b/components/dash-core-components/src/components/Tabs.tsx @@ -0,0 +1,278 @@ +import React, {useEffect, useRef, useState} from 'react'; +import {has, is, isNil} from 'ramda'; + +import LoadingElement from '../utils/_LoadingElement'; +import {DashComponent} from '@dash-renderer/types/component'; +import './css/tabs.css'; + +interface EnhancedTabProps { + id?: string; + label?: string | DashComponent[]; + selected: boolean; + className?: string; + style?: React.CSSProperties; + selectedClassName?: string; + selected_style?: React.CSSProperties; + selectHandler: (value: string) => void; + value: string; + disabled?: boolean; + disabled_style?: React.CSSProperties; + disabled_className?: string; + componentPath: string[]; +} +import {PersistedProps, PersistenceTypes, TabsProps} from '../types'; + +// 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, + componentPath, +}: EnhancedTabProps) => { + 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. + */ +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, + ...props +}: TabsProps) { + const initializedRef = useRef(false); + const [isAboveBreakpoint, setIsAboveBreakpoint] = useState(false); + + const parseChildrenToArray = () => { + if (props.children && !is(Array, 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 [props.children]; + } + return props.children ?? []; + }; + + const valueOrDefault = () => { + if (has('value', props)) { + return props.value; + } + const children = 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'; + }; + + const selectHandler = (value: string) => { + props.setProps({value: value}); + }; + + // 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 (props.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; + + 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 tabContainerClassNames = [ + 'tab-container', + vertical ? 'tab-container--vert' : null, + props.className, + ].filter(Boolean); + + const tabContentClassNames = [ + 'tab-content', + vertical ? 'tab-content--vert' : null, + props.content_className, + ].filter(Boolean); + + const tabParentClassNames = [ + 'tab-parent', + vertical ? ' tab-parent--vert' : null, + isAboveBreakpoint ? ' tab-parent--above-breakpoint' : null, + props.parent_className, + ].filter(Boolean); + + // Set CSS variables for dynamic styling + const cssVars = { + '--tabs-border': colors.border, + '--tabs-primary': colors.primary, + '--tabs-background': colors.background, + '--tabs-width': `calc(100% / ${parseChildrenToArray().length})`, + } as const; + + return ( + + {loadingProps => ( +
+
+ {EnhancedTabs} +
+
+ {selectedTabContent || ''} +
+
+ )} +
+ ); +} + +Tabs.dashPersistence = true; +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..8dff25a29c --- /dev/null +++ b/components/dash-core-components/src/components/css/tabs.css @@ -0,0 +1,119 @@ +/* Tab parent container */ +.tab-parent { + display: flex; + flex-direction: column; +} + +/* Tab container (holds all tabs) */ +.tab-container { + display: flex; + flex-direction: column; +} + +.tab-container--vert { + display: inline-flex; +} + +/* Individual tab */ +.tab { + display: inline-block; + background-color: var(--tabs-background); + border: 1px solid var(--tabs-border); + border-bottom: none; + padding: 20px 25px; + transition: background-color, color 200ms; + width: 100%; + text-align: center; + box-sizing: border-box; +} + +.tab:last-of-type { + border-right: 1px solid var(--tabs-border); + border-bottom: 1px solid var(--tabs-border); +} + +.tab:hover { + 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 { + color: #d6d6d6; +} + +/* 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; +} + +/* Horizontal tabs: equal width distribution (only when not vertical) */ +.tab-parent--above-breakpoint .tab-container:not(.tab-container--vert) .tab { + width: var(--tabs-width); +} + +.tab-parent--above-breakpoint .tab--selected, +.tab-parent--above-breakpoint .tab:last-of-type.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-of-type.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-of-type { + 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 6e73047885..3523aa913b 100644 --- a/components/dash-core-components/src/index.ts +++ b/components/dash-core-components/src/index.ts @@ -20,7 +20,7 @@ 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 Tabs from './components/Tabs'; import Textarea from './components/Textarea.react'; import Tooltip from './components/Tooltip.react'; 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 7bf9da8476..7aae14e043 100644 --- a/components/dash-core-components/src/types.ts +++ b/components/dash-core-components/src/types.ts @@ -1,14 +1,10 @@ import React from 'react'; +import {DashComponentApi} from '@dash-renderer/dashApi'; 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; - }; + dash_component_api: DashComponentApi; } } @@ -518,3 +514,64 @@ export interface RadioItemsProps extends BaseComponentProps { */ labelClassName?: string; } + +export interface TabsProps extends BaseComponentProps { + /** + * 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; + }; +} diff --git a/dash/dash-renderer/src/dashApi.ts b/dash/dash-renderer/src/dashApi.ts index 75365f731c..f7600eb191 100644 --- a/dash/dash-renderer/src/dashApi.ts +++ b/dash/dash-renderer/src/dashApi.ts @@ -36,3 +36,11 @@ function getLayout(componentPathOrId: string[] | string): any { getLayout, stringifyId }; + +export interface DashComponentApi { + ExternalWrapper: typeof ExternalWrapper; + DashContext: typeof DashContext; + useDashContext: typeof useDashContext; + getLayout: typeof getLayout; + stringifyId: typeof stringifyId; +} From 0fbd86aaf628736967fffdcfd8a623602c005e03 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Thu, 9 Oct 2025 18:08:15 -0600 Subject: [PATCH 02/14] Refactor Tab component --- .../generator.test.ts | 4 + .../src/props.ts | 2 + .../src/components/Tab.react.js | 79 ----------- .../src/components/Tab.tsx | 19 +++ .../src/components/Tabs.tsx | 129 +++++++++--------- .../src/components/css/tabs.css | 7 +- components/dash-core-components/src/index.ts | 3 +- components/dash-core-components/src/types.ts | 68 ++++++++- dash/dash-renderer/src/dashApi.ts | 3 +- dash/dash-renderer/src/types/component.ts | 1 + .../dash-renderer/src/wrapper/DashContext.tsx | 2 +- dash/extract-meta.js | 12 +- 12 files changed, 172 insertions(+), 157 deletions(-) delete mode 100644 components/dash-core-components/src/components/Tab.react.js create mode 100644 components/dash-core-components/src/components/Tab.tsx 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..0a305b6839 100644 --- a/@plotly/dash-generator-test-component-typescript/src/props.ts +++ b/@plotly/dash-generator-test-component-typescript/src/props.ts @@ -1,5 +1,6 @@ // 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'; +import {DashComponent} from '@dash-renderer/types/component'; type Nested = { @@ -36,6 +37,7 @@ export type TypescriptComponentProps = { | boolean; element?: JSX.Element; array_elements?: JSX.Element[]; + dash_component?: DashComponent; string_default?: string; number_default?: number; 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.tsx b/components/dash-core-components/src/components/Tabs.tsx index 95a8332fa9..c22686d21c 100644 --- a/components/dash-core-components/src/components/Tabs.tsx +++ b/components/dash-core-components/src/components/Tabs.tsx @@ -1,28 +1,17 @@ -import React, {useEffect, useRef, useState} from 'react'; -import {has, is, isNil} from 'ramda'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import {has, isNil} from 'ramda'; import LoadingElement from '../utils/_LoadingElement'; -import {DashComponent} from '@dash-renderer/types/component'; +import {PersistedProps, PersistenceTypes, TabProps, TabsProps} from '../types'; import './css/tabs.css'; +import {DashComponent} from '@dash-renderer/types/component'; -interface EnhancedTabProps { - id?: string; - label?: string | DashComponent[]; +interface EnhancedTabProps extends TabProps { selected: boolean; - className?: string; - style?: React.CSSProperties; - selectedClassName?: string; - selected_style?: React.CSSProperties; - selectHandler: (value: string) => void; - value: string; - disabled?: boolean; - disabled_style?: React.CSSProperties; - disabled_className?: string; - componentPath: string[]; + componentPath?: (string | number)[]; } -import {PersistedProps, PersistenceTypes, TabsProps} from '../types'; -// EnhancedTab is defined here instead of in Tab.react.js because if exported there, +// 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, @@ -30,55 +19,61 @@ const EnhancedTab = ({ selected, className, style, - selectedClassName, + selected_className, selected_style, - selectHandler, + setProps: selectHandler, value, disabled = false, - disabled_style = {color: '#d6d6d6'}, + 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 isLoading = ctx.useLoading({rawPath: componentPath}); + const tabStyle = { + ...style, + ...(disabled ? disabled_style : {}), + ...(selected ? selected_style : {}), + }; + + const tabClassNames = [ + 'tab', + className, + disabled ? 'tab--disabled' : null, + disabled ? disabled_className : null, + selected ? 'tab--selected' : null, + selected ? selected_className : null, + ].filter(Boolean); - 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; + if (typeof label === 'object') { + labelDisplay = ( + + ); } else { - // else it is a string, so we just want to render that - labelDisplay = label; + labelDisplay = {label}; } + return (
{ if (!disabled) { - selectHandler(value); + selectHandler({value}); } }} > - {labelDisplay} + {labelDisplay}
); }; @@ -101,26 +96,28 @@ function Tabs({ 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 = () => { - if (props.children && !is(Array, 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 [props.children]; + const parseChildrenToArray = useCallback((): DashComponent[] => { + if (!children) { + return []; } - return props.children ?? []; - }; + if (children instanceof Array) { + return children; + } + return [children]; + }, [children]); const valueOrDefault = () => { if (has('value', props)) { return props.value; } const children = parseChildrenToArray(); - if (children && children.length) { + if (children && children.length && children[0].props.componentPath) { const firstChildren = window.dash_component_api.getLayout([ ...children[0].props.componentPath, 'props', @@ -131,10 +128,6 @@ function Tabs({ return 'tab-1'; }; - const selectHandler = (value: string) => { - props.setProps({value: value}); - }; - // Initialize value on mount if not set useEffect(() => { if (!initializedRef.current && !has('value', props)) { @@ -167,15 +160,15 @@ function Tabs({ const value = valueOrDefault(); - if (props.children) { + 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; + let childProps: Omit; - if (React.isValidElement(child)) { + if (React.isValidElement(child) && child.props.componentPath) { childProps = window.dash_component_api.getLayout([ ...child.props.componentPath, 'props', @@ -194,22 +187,31 @@ function Tabs({ selectedTab = child; } + const style = childProps.style ?? {}; + if (typeof childProps.width === 'number') { + style.width = `${childProps.width}px`; + style.flex = 'none'; + } else if (typeof childProps.width === 'string') { + style.width = childProps.width; + style.flex = 'none'; + } + return ( ); }); @@ -241,7 +243,6 @@ function Tabs({ '--tabs-border': colors.border, '--tabs-primary': colors.primary, '--tabs-background': colors.background, - '--tabs-width': `calc(100% / ${parseChildrenToArray().length})`, } as const; return ( diff --git a/components/dash-core-components/src/components/css/tabs.css b/components/dash-core-components/src/components/css/tabs.css index 8dff25a29c..8bc952f0b2 100644 --- a/components/dash-core-components/src/components/css/tabs.css +++ b/components/dash-core-components/src/components/css/tabs.css @@ -16,7 +16,7 @@ /* Individual tab */ .tab { - display: inline-block; + flex: 1; background-color: var(--tabs-background); border: 1px solid var(--tabs-border); border-bottom: none; @@ -64,11 +64,6 @@ border-right: none; } -/* Horizontal tabs: equal width distribution (only when not vertical) */ -.tab-parent--above-breakpoint .tab-container:not(.tab-container--vert) .tab { - width: var(--tabs-width); -} - .tab-parent--above-breakpoint .tab--selected, .tab-parent--above-breakpoint .tab:last-of-type.tab--selected { border-bottom: none; diff --git a/components/dash-core-components/src/index.ts b/components/dash-core-components/src/index.ts index 3523aa913b..2af446da9c 100644 --- a/components/dash-core-components/src/index.ts +++ b/components/dash-core-components/src/index.ts @@ -1,4 +1,3 @@ -/* eslint-disable import/prefer-default-export */ import Checklist from './components/Checklist'; import Clipboard from './components/Clipboard.react'; import ConfirmDialog from './components/ConfirmDialog.react'; @@ -19,7 +18,7 @@ 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 Tab from './components/Tab'; import Tabs from './components/Tabs'; import Textarea from './components/Textarea.react'; import Tooltip from './components/Tooltip.react'; diff --git a/components/dash-core-components/src/types.ts b/components/dash-core-components/src/types.ts index 7aae14e043..c694d8ae24 100644 --- a/components/dash-core-components/src/types.ts +++ b/components/dash-core-components/src/types.ts @@ -559,7 +559,7 @@ export interface TabsProps extends BaseComponentProps { /** * Array that holds Tab components */ - children?: DashComponent[]; + children?: DashComponent; /** * Holds the colors used by the Tabs and Tab components. If you set these, you should specify colors for all properties, so: @@ -575,3 +575,69 @@ export interface TabsProps extends BaseComponentProps { 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 BaseComponentProps { + /** + * 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/dash/dash-renderer/src/dashApi.ts b/dash/dash-renderer/src/dashApi.ts index f7600eb191..6dae9a5734 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'; /** * Get the dash props from a component path or id. @@ -12,7 +13,7 @@ import {stringifyId} from './actions/dependencies'; * @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(); diff --git a/dash/dash-renderer/src/types/component.ts b/dash/dash-renderer/src/types/component.ts index 55c89c7660..176d9c3ac7 100644 --- a/dash/dash-renderer/src/types/component.ts +++ b/dash/dash-renderer/src/types/component.ts @@ -1,5 +1,6 @@ export type BaseDashProps = { id?: string; + componentPath?: DashLayoutPath; [key: string]: any; }; diff --git a/dash/dash-renderer/src/wrapper/DashContext.tsx b/dash/dash-renderer/src/wrapper/DashContext.tsx index 1da719f664..ff9cf44589 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}`. diff --git a/dash/extract-meta.js b/dash/extract-meta.js index aaaf73692e..a5a35905de 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).replace(/^React\./, ''); + 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'; } From 850cb36bb8d3c89c8265eea2b57a161b194f9880 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Fri, 10 Oct 2025 11:07:54 -0600 Subject: [PATCH 03/14] improve typing --- .../src/components/Tabs.tsx | 2 +- components/dash-core-components/src/types.ts | 40 +++++-------------- dash/dash-renderer/src/dashApi.ts | 10 +---- dash/dash-renderer/src/types/component.ts | 18 ++++++++- dash/dash-renderer/src/types/index.ts | 10 +++++ .../dash-renderer/src/wrapper/DashContext.tsx | 2 +- 6 files changed, 41 insertions(+), 41 deletions(-) create mode 100644 dash/dash-renderer/src/types/index.ts diff --git a/components/dash-core-components/src/components/Tabs.tsx b/components/dash-core-components/src/components/Tabs.tsx index c22686d21c..8d0ddfc058 100644 --- a/components/dash-core-components/src/components/Tabs.tsx +++ b/components/dash-core-components/src/components/Tabs.tsx @@ -4,7 +4,7 @@ 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/component'; +import {DashComponent} from '@dash-renderer/types'; interface EnhancedTabProps extends TabProps { selected: boolean; diff --git a/components/dash-core-components/src/types.ts b/components/dash-core-components/src/types.ts index e21fafa92b..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 */ @@ -837,7 +819,7 @@ export interface LoadingProps extends BaseComponentProps { custom_spinner?: React.ReactNode; } -export interface TabsProps extends BaseComponentProps { +export interface TabsProps extends BaseDccProps { /** * The value of the currently selected Tab */ @@ -901,7 +883,7 @@ export interface TabsProps extends BaseComponentProps { // 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 BaseComponentProps { +export interface TabProps extends BaseDccProps { /** * The tab's label */ diff --git a/dash/dash-renderer/src/dashApi.ts b/dash/dash-renderer/src/dashApi.ts index 6dae9a5734..f76c093f5d 100644 --- a/dash/dash-renderer/src/dashApi.ts +++ b/dash/dash-renderer/src/dashApi.ts @@ -30,18 +30,10 @@ function getLayout(componentPathOrId: DashLayoutPath | string): any { } } -(window as any).dash_component_api = { +window.dash_component_api = { ExternalWrapper, DashContext, useDashContext, getLayout, stringifyId }; - -export interface DashComponentApi { - ExternalWrapper: typeof ExternalWrapper; - DashContext: typeof DashContext; - useDashContext: typeof useDashContext; - getLayout: typeof getLayout; - stringifyId: typeof stringifyId; -} diff --git a/dash/dash-renderer/src/types/component.ts b/dash/dash-renderer/src/types/component.ts index 176d9c3ac7..82b0020d4a 100644 --- a/dash/dash-renderer/src/types/component.ts +++ b/dash/dash-renderer/src/types/component.ts @@ -1,13 +1,29 @@ +import { + DashContext, + DashContextProviderProps, + useDashContext +} from '../wrapper/DashContext'; +import ExternalWrapper from '../wrapper/ExternalWrapper'; +import {stringifyId} from '../actions/dependencies'; + 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) => DashComponent; + stringifyId: typeof stringifyId; +} + 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 ff9cf44589..6c03f47b2b 100644 --- a/dash/dash-renderer/src/wrapper/DashContext.tsx +++ b/dash/dash-renderer/src/wrapper/DashContext.tsx @@ -40,7 +40,7 @@ type DashContextType = { export const DashContext = React.createContext({} as any); -type DashContextProviderProps = { +export type DashContextProviderProps = { children: JSX.Element; componentPath: DashLayoutPath; }; From e8800d608cda82fc2a07e159e8094919163b67c1 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Thu, 16 Oct 2025 10:47:03 -0600 Subject: [PATCH 04/14] Fix issue with auto-expanding flex tabs --- CHANGELOG.md | 8 ++++++++ components/dash-core-components/src/components/Tabs.tsx | 6 +++--- .../dash-core-components/src/components/css/tabs.css | 5 +++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd52156b97..981e8dac6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ 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. + ## [4.0.0rc2] - 2025-10-10 ## Added diff --git a/components/dash-core-components/src/components/Tabs.tsx b/components/dash-core-components/src/components/Tabs.tsx index 8d0ddfc058..5de7c3945a 100644 --- a/components/dash-core-components/src/components/Tabs.tsx +++ b/components/dash-core-components/src/components/Tabs.tsx @@ -54,7 +54,7 @@ const EnhancedTab = ({ labelDisplay = ( ); } else { @@ -190,10 +190,10 @@ function Tabs({ const style = childProps.style ?? {}; if (typeof childProps.width === 'number') { style.width = `${childProps.width}px`; - style.flex = 'none'; + style.flex = '0 0 auto'; } else if (typeof childProps.width === 'string') { style.width = childProps.width; - style.flex = 'none'; + style.flex = '0 0 auto'; } return ( diff --git a/components/dash-core-components/src/components/css/tabs.css b/components/dash-core-components/src/components/css/tabs.css index 8bc952f0b2..a448875c9d 100644 --- a/components/dash-core-components/src/components/css/tabs.css +++ b/components/dash-core-components/src/components/css/tabs.css @@ -2,6 +2,7 @@ .tab-parent { display: flex; flex-direction: column; + overflow: hidden; } /* Tab container (holds all tabs) */ @@ -16,13 +17,13 @@ /* Individual tab */ .tab { - flex: 1; + 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; - width: 100%; text-align: center; box-sizing: border-box; } From 5ff94d2d7900e4b532bee69b83c8d287c3d1f068 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Fri, 17 Oct 2025 10:04:36 -0600 Subject: [PATCH 05/14] fix typescript error & css --- .../dash-core-components/src/components/Tabs.tsx | 16 +++++++--------- .../src/components/css/tabs.css | 16 ++++++++-------- dash/dash-renderer/src/types/component.ts | 2 +- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/components/dash-core-components/src/components/Tabs.tsx b/components/dash-core-components/src/components/Tabs.tsx index 5de7c3945a..11dea16768 100644 --- a/components/dash-core-components/src/components/Tabs.tsx +++ b/components/dash-core-components/src/components/Tabs.tsx @@ -112,27 +112,25 @@ function Tabs({ return [children]; }, [children]); - const valueOrDefault = () => { + const valueOrDefault = (): string | undefined => { if (has('value', props)) { return props.value; } const children = parseChildrenToArray(); if (children && children.length && children[0].props.componentPath) { - const firstChildren = window.dash_component_api.getLayout([ - ...children[0].props.componentPath, - 'props', - 'value', - ]); - return firstChildren || 'tab-1'; + const firstChildren: TabProps = window.dash_component_api.getLayout( + [...children[0].props.componentPath, 'props'] + ); + return firstChildren.value; } - return 'tab-1'; + return undefined; }; // Initialize value on mount if not set useEffect(() => { if (!initializedRef.current && !has('value', props)) { props.setProps({ - value: valueOrDefault(), + value: `${valueOrDefault()}`, }); initializedRef.current = true; } diff --git a/components/dash-core-components/src/components/css/tabs.css b/components/dash-core-components/src/components/css/tabs.css index a448875c9d..84160d09aa 100644 --- a/components/dash-core-components/src/components/css/tabs.css +++ b/components/dash-core-components/src/components/css/tabs.css @@ -28,11 +28,6 @@ box-sizing: border-box; } -.tab:last-of-type { - border-right: 1px solid var(--tabs-border); - border-bottom: 1px solid var(--tabs-border); -} - .tab:hover { cursor: pointer; } @@ -65,8 +60,13 @@ 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-of-type.tab--selected { +.tab-parent--above-breakpoint .tab:last-child.tab--selected { border-bottom: none; } @@ -81,7 +81,7 @@ .tab--selected, .tab-parent--above-breakpoint .tab-container:not(.tab-container--vert) - .tab:last-of-type.tab--selected { + .tab:last-child.tab--selected { border-top: 2px solid var(--tabs-primary); } @@ -91,7 +91,7 @@ border-bottom: none !important; } -.tab-parent--above-breakpoint .tab-container--vert .tab:last-of-type { +.tab-parent--above-breakpoint .tab-container--vert .tab:last-child { border-bottom: 1px solid var(--tabs-border) !important; } diff --git a/dash/dash-renderer/src/types/component.ts b/dash/dash-renderer/src/types/component.ts index 82b0020d4a..7847b3b1e8 100644 --- a/dash/dash-renderer/src/types/component.ts +++ b/dash/dash-renderer/src/types/component.ts @@ -16,7 +16,7 @@ export interface DashComponentApi { ExternalWrapper: typeof ExternalWrapper; DashContext: typeof DashContext; useDashContext: typeof useDashContext; - getLayout: (componentPathOrId: DashLayoutPath | string) => DashComponent; + getLayout: (componentPathOrId: DashLayoutPath | string) => any; stringifyId: typeof stringifyId; } From 67cc824ee1ae21060ffe620743fc2cd878038185 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Fri, 17 Oct 2025 10:31:53 -0600 Subject: [PATCH 06/14] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 981e8dac6f..fad730dc70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## 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 From 082a4c12f3816a246c96a7784bdb8072308cb959 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Fri, 17 Oct 2025 10:38:14 -0600 Subject: [PATCH 07/14] Remove useless string replace --- dash/extract-meta.js | 383 ++++++++++++++++++++++--------------------- 1 file changed, 196 insertions(+), 187 deletions(-) diff --git a/dash/extract-meta.js b/dash/extract-meta.js index a5a35905de..0d225be5c5 100755 --- a/dash/extract-meta.js +++ b/dash/extract-meta.js @@ -5,27 +5,27 @@ if (process.env.MODULES_PATH) { let ts, tsEnabled = true; try { - ts = require('typescript'); + ts = require("typescript"); } catch (e) { ts = {}; tsEnabled = false; } -const fs = require('fs'); -const path = require('path'); -const reactDocs = require('react-docgen'); +const fs = require("fs"); +const path = require("path"); +const reactDocs = require("react-docgen"); const args = process.argv.slice(2); const src = args.slice(2); const ignorePattern = args[0] ? new RegExp(args[0]) : null; const reservedPatterns = args[1] - ? args[1].split('|').map(part => new RegExp(part)) + ? args[1].split("|").map((part) => new RegExp(part)) : []; function help() { - console.error('usage: '); + console.error("usage: "); console.error( - 'extract-meta ^fileIgnorePattern ^forbidden$|^props$|^patterns$' + - ' path/to/component(s) [path/to/more/component(s) ...] > metadata.json' + "extract-meta ^fileIgnorePattern ^forbidden$|^props$|^patterns$" + + " path/to/component(s) [path/to/more/component(s) ...] > metadata.json", ); } @@ -38,7 +38,11 @@ function getTsConfigCompilerOptions() { // Since extract-meta can be run on JavaScript sources, if trying to get the // config doesn't work, we can fall back gracefully. try { - const tsconfig = ts.getParsedCommandLineOfConfigFile('tsconfig.json', { esModuleInterop: true }, ts.sys); + const tsconfig = ts.getParsedCommandLineOfConfigFile( + "tsconfig.json", + { esModuleInterop: true }, + ts.sys, + ); return tsconfig?.options ?? {}; } catch { return {}; @@ -46,64 +50,67 @@ function getTsConfigCompilerOptions() { } let failedBuild = false; -const excludedDocProps = ['setProps', 'id', 'className', 'style']; +const excludedDocProps = ["setProps", "id", "className", "style"]; -const isOptional = prop => (prop.getFlags() & ts.SymbolFlags.Optional) !== 0; +const isOptional = (prop) => (prop.getFlags() & ts.SymbolFlags.Optional) !== 0; const PRIMITIVES = [ - 'string', - 'number', - 'bool', - 'any', - 'array', - 'object', - 'node' + "string", + "number", + "bool", + "any", + "array", + "object", + "node", ]; // These types take too long to parse because of heavy nesting. -const BANNED_TYPES = [ - 'Document', - 'ShadowRoot', - 'ChildNode', - 'ParentNode', -]; -const unionSupport = PRIMITIVES.concat('true', 'false', 'Element', 'enum', 'DashComponent'); +const BANNED_TYPES = ["Document", "ShadowRoot", "ChildNode", "ParentNode"]; +const unionSupport = PRIMITIVES.concat( + "true", + "false", + "Element", + "enum", + "DashComponent", +); /* Regex to capture typescript unions in different formats: * string[] * (string | number)[] * SomeCustomType[] */ -const reArray = new RegExp(`(${unionSupport.join('|')}|\\(.+\\)|[A-Z][a-zA-Z]*Value)\\[\\]`); +const reArray = new RegExp( + `(${unionSupport.join("|")}|\\(.+\\)|[A-Z][a-zA-Z]*Value)\\[\\]`, +); -const isArray = rawType => reArray.test(rawType); +const isArray = (rawType) => reArray.test(rawType); -const isUnionLiteral = typeObj => +const isUnionLiteral = (typeObj) => typeObj.types.every( - t => + (t) => t.getFlags() & (ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral | ts.TypeFlags.EnumLiteral | - ts.TypeFlags.Undefined) + ts.TypeFlags.Undefined), ); function logError(error, filePath) { if (filePath) { process.stderr.write(`Error with path ${filePath}\n`); } - process.stderr.write(error + '\n'); + process.stderr.write(error + "\n"); if (error instanceof Error) { - process.stderr.write(error.stack + '\n'); + process.stderr.write(error.stack + "\n"); } } function isReservedPropName(propName) { - reservedPatterns.forEach(reservedPattern => { + reservedPatterns.forEach((reservedPattern) => { if (reservedPattern.test(propName)) { process.stderr.write( `\nERROR: "${propName}" matches reserved word ` + - `pattern: ${reservedPattern.toString()}\n` + `pattern: ${reservedPattern.toString()}\n`, ); failedBuild = true; } @@ -114,7 +121,7 @@ function isReservedPropName(propName) { function checkDocstring(name, value) { if ( !value || - (value.length < 1 && !excludedDocProps.includes(name.split('.').pop())) + (value.length < 1 && !excludedDocProps.includes(name.split(".").pop())) ) { logError(`\nDescription for ${name} is missing!`); } @@ -124,28 +131,28 @@ function docstringWarning(doc) { checkDocstring(doc.displayName, doc.description); Object.entries(doc.props || {}).forEach(([name, p]) => - checkDocstring(`${doc.displayName}.${name}`, p.description) + checkDocstring(`${doc.displayName}.${name}`, p.description), ); } function zipArrays(...arrays) { const arr = []; for (let i = 0; i <= arrays[0].length - 1; i++) { - arr.push(arrays.map(a => a[i])); + arr.push(arrays.map((a) => a[i])); } return arr; } function cleanPath(filepath) { - return filepath.split(path.sep).join('/'); + return filepath.split(path.sep).join("/"); } function parseJSX(filepath) { try { const src = fs.readFileSync(filepath); const doc = reactDocs.parse(src); - Object.keys(doc.props).forEach(propName => - isReservedPropName(propName) + Object.keys(doc.props).forEach((propName) => + isReservedPropName(propName), ); docstringWarning(doc); return doc; @@ -158,29 +165,29 @@ function gatherComponents(sources, components = {}) { const names = []; const filepaths = []; - const gather = filepath => { + const gather = (filepath) => { if (ignorePattern && ignorePattern.test(filepath)) { return; } const extension = path.extname(filepath); - if (['.jsx', '.js'].includes(extension)) { + if ([".jsx", ".js"].includes(extension)) { components[cleanPath(filepath)] = parseJSX(filepath); - } else if (filepath.endsWith('.tsx')) { + } else if (filepath.endsWith(".tsx")) { try { const name = /(.*)\.tsx/.exec(path.basename(filepath))[1]; filepaths.push(filepath); names.push(name); } catch (err) { process.stderr.write( - `ERROR: Invalid component file ${filepath}: ${err}` + `ERROR: Invalid component file ${filepath}: ${err}`, ); } } }; - sources.forEach(sourcePath => { + sources.forEach((sourcePath) => { if (fs.lstatSync(sourcePath).isDirectory()) { - fs.readdirSync(sourcePath).forEach(f => { + fs.readdirSync(sourcePath).forEach((f) => { const filepath = path.join(sourcePath, f); if (fs.lstatSync(filepath).isDirectory()) { gatherComponents([filepath], components); @@ -200,13 +207,13 @@ function gatherComponents(sources, components = {}) { const program = ts.createProgram(filepaths, getTsConfigCompilerOptions()); const checker = program.getTypeChecker(); - const coerceValue = t => { + const coerceValue = (t) => { // May need to improve for shaped/list literals. if (t.isStringLiteral()) return `'${t.value}'`; return t.value; }; - const getComponentFromExport = exp => { + const getComponentFromExport = (exp) => { const decl = exp.valueDeclaration || exp.declarations[0]; const type = checker.getTypeOfSymbolAtLocation(exp, decl); const typeSymbol = type.symbol || type.aliasSymbol; @@ -218,14 +225,14 @@ function gatherComponents(sources, components = {}) { const symbolName = typeSymbol.getName(); if ( - (symbolName === 'MemoExoticComponent' || - symbolName === 'ForwardRefExoticComponent') && + (symbolName === "MemoExoticComponent" || + symbolName === "ForwardRefExoticComponent") && exp.valueDeclaration && ts.isExportAssignment(exp.valueDeclaration) && ts.isCallExpression(exp.valueDeclaration.expression) ) { const component = checker.getSymbolAtLocation( - exp.valueDeclaration.expression.arguments[0] + exp.valueDeclaration.expression.arguments[0], ); if (component) return component; @@ -233,7 +240,7 @@ function gatherComponents(sources, components = {}) { return exp; }; - const getParent = node => { + const getParent = (node) => { let parent = node; while (parent.parent) { if (parent.parent.kind === ts.SyntaxKind.SourceFile) { @@ -245,155 +252,159 @@ function gatherComponents(sources, components = {}) { return parent; }; - const getEnum = typeObj => ({ - name: 'enum', - value: typeObj.types.map(t => ({ + const getEnum = (typeObj) => ({ + name: "enum", + value: typeObj.types.map((t) => ({ value: coerceValue(t), - computed: false - })) + computed: false, + })), }); const getUnion = (typeObj, propObj, parentType) => { - let name = 'union', + let name = "union", value; // 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).replace(/^React\./, ''); - if (typeString === 'DashComponent') { - typeName = 'node'; - } + 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) { - // A literal value - return true; - } - return ( - unionSupport.includes(typeName) || - isArray(checker.typeToString(t)) - ); - }); - value = value.map(t => t.value ? {name: 'literal', value: t.value} : getPropType(t, propObj, parentType)); + } + if (t.value) { + // A literal value + return true; + } + return ( + unionSupport.includes(typeName) || + isArray(checker.typeToString(t)) + ); + }); + value = value.map((t) => + t.value + ? { name: "literal", value: t.value } + : getPropType(t, propObj, parentType), + ); // de-dupe any types in this union - value = value.reduce((acc, t) => { - const key = `${t.name}:${t.value}`; - if (!acc.seen.has(key)) { - acc.seen.add(key); - acc.result.push(t); - } - return acc; - }, { seen: new Set(), result: [] }).result; + value = value.reduce( + (acc, t) => { + const key = `${t.name}:${t.value}`; + if (!acc.seen.has(key)) { + acc.seen.add(key); + acc.result.push(t); + } + return acc; + }, + { seen: new Set(), result: [] }, + ).result; if (!value.length) { - name = 'any'; + name = "any"; value = undefined; } return { name, - value + value, }; }; - const getPropTypeName = propName => { - if (propName.includes('=>') || propName === 'Function') { - return 'func'; - } else if (['boolean', 'false', 'true'].includes(propName)) { - return 'bool'; - } else if (propName === '[]') { - return 'array'; + const getPropTypeName = (propName) => { + if (propName.includes("=>") || propName === "Function") { + return "func"; + } else if (["boolean", "false", "true"].includes(propName)) { + return "bool"; + } else if (propName === "[]") { + return "array"; } else if ( - propName === 'Element' || - propName === 'ReactNode' || - propName === 'ReactElement' || - propName === 'DashComponent' + propName === "Element" || + propName === "ReactNode" || + propName === "ReactElement" || + propName === "DashComponent" ) { - return 'node'; + return "node"; } return propName; }; const getPropType = (propType, propObj, parentType = null) => { // Types can get namespace prefixes or not. - let name = checker.typeToString(propType).replace(/^React\./, ''); + let name = checker.typeToString(propType).replace(/^React\./, ""); let value, elements; const raw = name; - const newParentType = (parentType || []).concat(raw) + const newParentType = (parentType || []).concat(raw); if (propType.isUnion()) { if (isUnionLiteral(propType)) { - return {...getEnum(propType), raw}; - } else if (raw.includes('|')) { - return {...getUnion(propType, propObj, newParentType), raw}; + return { ...getEnum(propType), raw }; + } else if (raw.includes("|")) { + return { ...getUnion(propType, propObj, newParentType), raw }; } } name = getPropTypeName(name); // Shapes & array support. - if (!PRIMITIVES.concat('enum', 'func', 'union').includes(name)) { + if (!PRIMITIVES.concat("enum", "func", "union").includes(name)) { if ( // Excluding object with arrays in the raw. - (name.includes('[]') && name.endsWith("]")) || - name.includes('Array') + (name.includes("[]") && name.endsWith("]")) || + name.includes("Array") ) { - name = 'arrayOf'; - const replaced = raw.replace('[]', ''); + name = "arrayOf"; + const replaced = raw.replace("[]", ""); if (unionSupport.includes(replaced)) { // Simple types are easier. value = { name: getPropTypeName(replaced), - raw: replaced + raw: replaced, }; } else { // Complex types get the type parameter (Array) const [nodeType] = checker.getTypeArguments(propType); if (nodeType) { - value = getPropType( - nodeType, propObj, newParentType, - ); + value = getPropType(nodeType, propObj, newParentType); } else { // Not sure, might be unsupported here. - name = 'array'; + name = "array"; } } } else if ( - name === 'tuple' || - (name.startsWith('[') && name.endsWith(']')) + name === "tuple" || + (name.startsWith("[") && name.endsWith("]")) ) { - name = 'tuple'; - elements = propType.resolvedTypeArguments.map( - t => getPropType(t, propObj, newParentType) + name = "tuple"; + elements = propType.resolvedTypeArguments.map((t) => + getPropType(t, propObj, newParentType), ); } else if ( BANNED_TYPES.includes(name) || (parentType && parentType.includes(name)) ) { console.error(`Warning nested type: ${name}`); - name = 'any'; + name = "any"; } else { - name = 'shape'; + name = "shape"; // If the type is declared as union it will have a types attribute. if (propType.types && propType.types.length) { if (isUnionLiteral(propType)) { - return {...getEnum(propType), raw}; + return { ...getEnum(propType), raw }; } return { ...getUnion(propType, propObj, newParentType), - raw + raw, }; } else if (propType.indexInfos && propType.indexInfos.length) { - const {type} = propType.indexInfos[0]; - name = 'objectOf'; + const { type } = propType.indexInfos[0]; + name = "objectOf"; value = getPropType(type, propObj, newParentType); } else { value = getProps( @@ -412,23 +423,23 @@ function gatherComponents(sources, components = {}) { name, value, elements, - raw + raw, }; }; const getDefaultProps = (symbol, source) => { const statements = source.statements.filter( - stmt => + (stmt) => (!!stmt.name && checker.getSymbolAtLocation(stmt.name) === symbol) || ts.isExpressionStatement(stmt) || - ts.isVariableStatement(stmt) + ts.isVariableStatement(stmt), ); return statements.reduce((acc, statement) => { let propMap = {}; - statement.getChildren().forEach(child => { - let {right} = child; + statement.getChildren().forEach((child) => { + let { right } = child; if (right && ts.isIdentifier(right)) { const value = source.locals.get(right.escapedText); if ( @@ -441,7 +452,7 @@ function gatherComponents(sources, components = {}) { } } if (right) { - const {properties} = right; + const { properties } = right; if (properties) { propMap = getDefaultPropsValues(properties); } @@ -450,30 +461,30 @@ function gatherComponents(sources, components = {}) { return { ...acc, - ...propMap + ...propMap, }; }, {}); }; - const getPropComment = symbol => { + const getPropComment = (symbol) => { // Doesn't work too good with the JsDocTags losing indentation. // But used only in props should be fine. const comment = symbol.getDocumentationComment(); const tags = symbol.getJsDocTags(); if (comment && comment.length) { return comment - .map(c => c.text) + .map((c) => c.text) .concat( - tags.map(t => - ['@', t.name].concat((t.text || []).map(e => e.text)) - ) + tags.map((t) => + ["@", t.name].concat((t.text || []).map((e) => e.text)), + ), ) - .join('\n'); + .join("\n"); } - return ''; + return ""; }; - const getPropsForFunctionalComponent = type => { + const getPropsForFunctionalComponent = (type) => { const callSignatures = type.getCallSignatures(); for (const sig of callSignatures) { @@ -484,7 +495,7 @@ function gatherComponents(sources, components = {}) { // There is only one parameter for functional components: props const p = params[0]; - if (p.name === 'props' || params.length === 1) { + if (p.name === "props" || params.length === 1) { return p; } } @@ -512,13 +523,13 @@ function gatherComponents(sources, components = {}) { type.getProperties(), typeSymbol, [], - defaultProps + defaultProps, ); } } }; - const getDefaultPropsValues = properties => + const getDefaultPropsValues = (properties) => properties.reduce((acc, p) => { if (!p.name || !p.initializer) { return acc; @@ -536,7 +547,7 @@ function gatherComponents(sources, components = {}) { break; } - const {initializer} = p; + const { initializer } = p; switch (initializer.kind) { case ts.SyntaxKind.StringLiteral: @@ -546,13 +557,13 @@ function gatherComponents(sources, components = {}) { value = initializer.text; break; case ts.SyntaxKind.NullKeyword: - value = 'null'; + value = "null"; break; case ts.SyntaxKind.FalseKeyword: - value = 'false'; + value = "false"; break; case ts.SyntaxKind.TrueKeyword: - value = 'true'; + value = "true"; break; default: try { @@ -562,7 +573,7 @@ function gatherComponents(sources, components = {}) { } } - acc[propName] = {value, computed: false}; + acc[propName] = { value, computed: false }; return acc; }, {}); @@ -572,14 +583,14 @@ function gatherComponents(sources, components = {}) { // first declaration and one of them will be either // an ObjectLiteralExpression or an Identifier which get in the // newChild with the proper props. - const defaultProps = type.getProperty('defaultProps'); + const defaultProps = type.getProperty("defaultProps"); if (!defaultProps) { return {}; } const decl = defaultProps.getDeclarations()[0]; let propValues = {}; - decl.getChildren().forEach(child => { + decl.getChildren().forEach((child) => { let newChild = child; if (ts.isIdentifier(child)) { @@ -595,7 +606,7 @@ function gatherComponents(sources, components = {}) { } } - const {properties} = newChild; + const { properties } = newChild; if (properties) { propValues = getDefaultPropsValues(properties); } @@ -613,16 +624,16 @@ function gatherComponents(sources, components = {}) { ) => { const results = {}; - properties.forEach(prop => { + properties.forEach((prop) => { const name = prop.getName(); if (isReservedPropName(name)) { return; } const propType = checker.getTypeOfSymbolAtLocation( prop, - propsObj.valueDeclaration + propsObj.valueDeclaration, ); - const baseProp = baseProps.find(p => p.getName() === name); + const baseProp = baseProps.find((p) => p.getName() === name); const defaultValue = defaultProps[name]; const required = @@ -635,7 +646,7 @@ function gatherComponents(sources, components = {}) { let result = { description, required, - defaultValue + defaultValue, }; const type = getPropType(propType, propsObj, parentType); // root object is inserted as type, @@ -643,7 +654,7 @@ function gatherComponents(sources, components = {}) { if (!flat) { result.type = type; } else { - result = {...result, ...type}; + result = { ...result, ...type }; } results[name] = result; @@ -655,7 +666,7 @@ function gatherComponents(sources, components = {}) { const getPropInfo = (propsObj, defaultProps) => { const propsType = checker.getTypeOfSymbolAtLocation( propsObj, - propsObj.valueDeclaration + propsObj.valueDeclaration, ); const baseProps = propsType.getApparentProperties(); let propertiesOfProps = baseProps; @@ -663,15 +674,15 @@ function gatherComponents(sources, components = {}) { if (propsType.isUnionOrIntersection()) { propertiesOfProps = [ ...checker.getAllPossiblePropertiesOfTypes(propsType.types), - ...baseProps + ...baseProps, ]; if (!propertiesOfProps.length) { const subTypes = checker.getAllPossiblePropertiesOfTypes( propsType.types.reduce( (all, t) => [...all, ...(t.types || [])], - [] - ) + [], + ), ); propertiesOfProps = [...subTypes, ...baseProps]; } @@ -685,13 +696,13 @@ function gatherComponents(sources, components = {}) { const moduleSymbol = checker.getSymbolAtLocation(source); const exports = checker.getExportsOfModule(moduleSymbol); - exports.forEach(exp => { + exports.forEach((exp) => { let rootExp = getComponentFromExport(exp); const declaration = rootExp.valueDeclaration || rootExp.declarations[0]; const type = checker.getTypeOfSymbolAtLocation( rootExp, - declaration + declaration, ); let commentSource = rootExp; @@ -700,14 +711,14 @@ function gatherComponents(sources, components = {}) { if (!rootExp.valueDeclaration) { if ( - originalName === 'default' && + originalName === "default" && !typeSymbol && (rootExp.flags & ts.SymbolFlags.Alias) !== 0 ) { // Some type of Exotic? commentSource = checker.getAliasedSymbol( - commentSource + commentSource, ).valueDeclaration; } else if (!typeSymbol) { // Invalid component @@ -715,15 +726,11 @@ function gatherComponents(sources, components = {}) { } else { // Function components. rootExp = typeSymbol; - commentSource = rootExp.valueDeclaration || rootExp.declarations[0]; - if ( - commentSource && - commentSource.parent - ) { + commentSource = + rootExp.valueDeclaration || rootExp.declarations[0]; + if (commentSource && commentSource.parent) { // Function with export later like `const MyComponent = (props) => <>;` - commentSource = getParent( - commentSource.parent - ); + commentSource = getParent(commentSource.parent); } } } else if ( @@ -747,7 +754,7 @@ function gatherComponents(sources, components = {}) { let defaultProps = getDefaultProps(typeSymbol, source); const propsType = getPropsForFunctionalComponent(type); - const isContext = !!type.getProperty('isContext'); + const isContext = !!type.getProperty("isContext"); let props; @@ -758,7 +765,9 @@ function gatherComponents(sources, components = {}) { propsType.valueDeclaration.name.elements && propsType.valueDeclaration.name.elements.length ) { - defaultProps = getDefaultPropsValues(propsType.valueDeclaration.name.elements); + defaultProps = getDefaultPropsValues( + propsType.valueDeclaration.name.elements, + ); } props = getPropInfo(propsType, defaultProps); } else { @@ -766,7 +775,7 @@ function gatherComponents(sources, components = {}) { props = getPropsForClassComponent( typeSymbol, source, - defaultProps + defaultProps, ); } @@ -776,28 +785,28 @@ function gatherComponents(sources, components = {}) { } const fullText = source.getFullText(); - let description = ''; + let description = ""; const commentRanges = ts.getLeadingCommentRanges( fullText, - commentSource.getFullStart() + commentSource.getFullStart(), ); if (commentRanges && commentRanges.length) { description = commentRanges - .map(r => + .map((r) => fullText .slice(r.pos + 4, r.end - 3) - .split('\n') - .map(s => s.replace(/^(\s*\*?\s)/, '')) - .filter(e => e) - .join('\n') + .split("\n") + .map((s) => s.replace(/^(\s*\*?\s)/, "")) + .filter((e) => e) + .join("\n"), ) - .join(''); + .join(""); } const doc = { displayName: name, description, props, - isContext + isContext, }; docstringWarning(doc); components[cleanPath(filepath)] = doc; @@ -811,6 +820,6 @@ const metadata = gatherComponents(Array.isArray(src) ? src : [src]); if (!failedBuild) { process.stdout.write(JSON.stringify(metadata, null, 2)); } else { - logError('extract-meta failed'); + logError("extract-meta failed"); process.exit(1); } From 58833999d9b1995a00a10ea67ad003660b35fbf7 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Fri, 17 Oct 2025 10:39:10 -0600 Subject: [PATCH 08/14] Revert "Remove useless string replace" This reverts commit 082a4c12f3816a246c96a7784bdb8072308cb959. --- dash/extract-meta.js | 383 +++++++++++++++++++++---------------------- 1 file changed, 187 insertions(+), 196 deletions(-) diff --git a/dash/extract-meta.js b/dash/extract-meta.js index 0d225be5c5..a5a35905de 100755 --- a/dash/extract-meta.js +++ b/dash/extract-meta.js @@ -5,27 +5,27 @@ if (process.env.MODULES_PATH) { let ts, tsEnabled = true; try { - ts = require("typescript"); + ts = require('typescript'); } catch (e) { ts = {}; tsEnabled = false; } -const fs = require("fs"); -const path = require("path"); -const reactDocs = require("react-docgen"); +const fs = require('fs'); +const path = require('path'); +const reactDocs = require('react-docgen'); const args = process.argv.slice(2); const src = args.slice(2); const ignorePattern = args[0] ? new RegExp(args[0]) : null; const reservedPatterns = args[1] - ? args[1].split("|").map((part) => new RegExp(part)) + ? args[1].split('|').map(part => new RegExp(part)) : []; function help() { - console.error("usage: "); + console.error('usage: '); console.error( - "extract-meta ^fileIgnorePattern ^forbidden$|^props$|^patterns$" + - " path/to/component(s) [path/to/more/component(s) ...] > metadata.json", + 'extract-meta ^fileIgnorePattern ^forbidden$|^props$|^patterns$' + + ' path/to/component(s) [path/to/more/component(s) ...] > metadata.json' ); } @@ -38,11 +38,7 @@ function getTsConfigCompilerOptions() { // Since extract-meta can be run on JavaScript sources, if trying to get the // config doesn't work, we can fall back gracefully. try { - const tsconfig = ts.getParsedCommandLineOfConfigFile( - "tsconfig.json", - { esModuleInterop: true }, - ts.sys, - ); + const tsconfig = ts.getParsedCommandLineOfConfigFile('tsconfig.json', { esModuleInterop: true }, ts.sys); return tsconfig?.options ?? {}; } catch { return {}; @@ -50,67 +46,64 @@ function getTsConfigCompilerOptions() { } let failedBuild = false; -const excludedDocProps = ["setProps", "id", "className", "style"]; +const excludedDocProps = ['setProps', 'id', 'className', 'style']; -const isOptional = (prop) => (prop.getFlags() & ts.SymbolFlags.Optional) !== 0; +const isOptional = prop => (prop.getFlags() & ts.SymbolFlags.Optional) !== 0; const PRIMITIVES = [ - "string", - "number", - "bool", - "any", - "array", - "object", - "node", + 'string', + 'number', + 'bool', + 'any', + 'array', + 'object', + 'node' ]; // These types take too long to parse because of heavy nesting. -const BANNED_TYPES = ["Document", "ShadowRoot", "ChildNode", "ParentNode"]; -const unionSupport = PRIMITIVES.concat( - "true", - "false", - "Element", - "enum", - "DashComponent", -); +const BANNED_TYPES = [ + 'Document', + 'ShadowRoot', + 'ChildNode', + 'ParentNode', +]; +const unionSupport = PRIMITIVES.concat('true', 'false', 'Element', 'enum', 'DashComponent'); /* Regex to capture typescript unions in different formats: * string[] * (string | number)[] * SomeCustomType[] */ -const reArray = new RegExp( - `(${unionSupport.join("|")}|\\(.+\\)|[A-Z][a-zA-Z]*Value)\\[\\]`, -); +const reArray = new RegExp(`(${unionSupport.join('|')}|\\(.+\\)|[A-Z][a-zA-Z]*Value)\\[\\]`); -const isArray = (rawType) => reArray.test(rawType); +const isArray = rawType => reArray.test(rawType); -const isUnionLiteral = (typeObj) => +const isUnionLiteral = typeObj => typeObj.types.every( - (t) => + t => t.getFlags() & (ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral | ts.TypeFlags.EnumLiteral | - ts.TypeFlags.Undefined), + ts.TypeFlags.Undefined) ); function logError(error, filePath) { if (filePath) { process.stderr.write(`Error with path ${filePath}\n`); } - process.stderr.write(error + "\n"); + process.stderr.write(error + '\n'); if (error instanceof Error) { - process.stderr.write(error.stack + "\n"); + process.stderr.write(error.stack + '\n'); } } function isReservedPropName(propName) { - reservedPatterns.forEach((reservedPattern) => { + reservedPatterns.forEach(reservedPattern => { if (reservedPattern.test(propName)) { process.stderr.write( `\nERROR: "${propName}" matches reserved word ` + - `pattern: ${reservedPattern.toString()}\n`, + `pattern: ${reservedPattern.toString()}\n` ); failedBuild = true; } @@ -121,7 +114,7 @@ function isReservedPropName(propName) { function checkDocstring(name, value) { if ( !value || - (value.length < 1 && !excludedDocProps.includes(name.split(".").pop())) + (value.length < 1 && !excludedDocProps.includes(name.split('.').pop())) ) { logError(`\nDescription for ${name} is missing!`); } @@ -131,28 +124,28 @@ function docstringWarning(doc) { checkDocstring(doc.displayName, doc.description); Object.entries(doc.props || {}).forEach(([name, p]) => - checkDocstring(`${doc.displayName}.${name}`, p.description), + checkDocstring(`${doc.displayName}.${name}`, p.description) ); } function zipArrays(...arrays) { const arr = []; for (let i = 0; i <= arrays[0].length - 1; i++) { - arr.push(arrays.map((a) => a[i])); + arr.push(arrays.map(a => a[i])); } return arr; } function cleanPath(filepath) { - return filepath.split(path.sep).join("/"); + return filepath.split(path.sep).join('/'); } function parseJSX(filepath) { try { const src = fs.readFileSync(filepath); const doc = reactDocs.parse(src); - Object.keys(doc.props).forEach((propName) => - isReservedPropName(propName), + Object.keys(doc.props).forEach(propName => + isReservedPropName(propName) ); docstringWarning(doc); return doc; @@ -165,29 +158,29 @@ function gatherComponents(sources, components = {}) { const names = []; const filepaths = []; - const gather = (filepath) => { + const gather = filepath => { if (ignorePattern && ignorePattern.test(filepath)) { return; } const extension = path.extname(filepath); - if ([".jsx", ".js"].includes(extension)) { + if (['.jsx', '.js'].includes(extension)) { components[cleanPath(filepath)] = parseJSX(filepath); - } else if (filepath.endsWith(".tsx")) { + } else if (filepath.endsWith('.tsx')) { try { const name = /(.*)\.tsx/.exec(path.basename(filepath))[1]; filepaths.push(filepath); names.push(name); } catch (err) { process.stderr.write( - `ERROR: Invalid component file ${filepath}: ${err}`, + `ERROR: Invalid component file ${filepath}: ${err}` ); } } }; - sources.forEach((sourcePath) => { + sources.forEach(sourcePath => { if (fs.lstatSync(sourcePath).isDirectory()) { - fs.readdirSync(sourcePath).forEach((f) => { + fs.readdirSync(sourcePath).forEach(f => { const filepath = path.join(sourcePath, f); if (fs.lstatSync(filepath).isDirectory()) { gatherComponents([filepath], components); @@ -207,13 +200,13 @@ function gatherComponents(sources, components = {}) { const program = ts.createProgram(filepaths, getTsConfigCompilerOptions()); const checker = program.getTypeChecker(); - const coerceValue = (t) => { + const coerceValue = t => { // May need to improve for shaped/list literals. if (t.isStringLiteral()) return `'${t.value}'`; return t.value; }; - const getComponentFromExport = (exp) => { + const getComponentFromExport = exp => { const decl = exp.valueDeclaration || exp.declarations[0]; const type = checker.getTypeOfSymbolAtLocation(exp, decl); const typeSymbol = type.symbol || type.aliasSymbol; @@ -225,14 +218,14 @@ function gatherComponents(sources, components = {}) { const symbolName = typeSymbol.getName(); if ( - (symbolName === "MemoExoticComponent" || - symbolName === "ForwardRefExoticComponent") && + (symbolName === 'MemoExoticComponent' || + symbolName === 'ForwardRefExoticComponent') && exp.valueDeclaration && ts.isExportAssignment(exp.valueDeclaration) && ts.isCallExpression(exp.valueDeclaration.expression) ) { const component = checker.getSymbolAtLocation( - exp.valueDeclaration.expression.arguments[0], + exp.valueDeclaration.expression.arguments[0] ); if (component) return component; @@ -240,7 +233,7 @@ function gatherComponents(sources, components = {}) { return exp; }; - const getParent = (node) => { + const getParent = node => { let parent = node; while (parent.parent) { if (parent.parent.kind === ts.SyntaxKind.SourceFile) { @@ -252,159 +245,155 @@ function gatherComponents(sources, components = {}) { return parent; }; - const getEnum = (typeObj) => ({ - name: "enum", - value: typeObj.types.map((t) => ({ + const getEnum = typeObj => ({ + name: 'enum', + value: typeObj.types.map(t => ({ value: coerceValue(t), - computed: false, - })), + computed: false + })) }); const getUnion = (typeObj, propObj, parentType) => { - let name = "union", + let name = 'union', value; // 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"; + value = typeObj.types + .filter(t => { + let typeName = t.intrinsicName; + if (!typeName) { + if (t.members) { + typeName = 'object'; + } else { + const typeString = checker.typeToString(t).replace(/^React\./, ''); + if (typeString === 'DashComponent') { + typeName = 'node'; + } } } - } - if (t.value) { - // A literal value - return true; - } - return ( - unionSupport.includes(typeName) || - isArray(checker.typeToString(t)) - ); - }); - value = value.map((t) => - t.value - ? { name: "literal", value: t.value } - : getPropType(t, propObj, parentType), - ); + if (t.value) { + // A literal value + return true; + } + return ( + unionSupport.includes(typeName) || + isArray(checker.typeToString(t)) + ); + }); + value = value.map(t => t.value ? {name: 'literal', value: t.value} : getPropType(t, propObj, parentType)); // de-dupe any types in this union - value = value.reduce( - (acc, t) => { - const key = `${t.name}:${t.value}`; - if (!acc.seen.has(key)) { - acc.seen.add(key); - acc.result.push(t); - } - return acc; - }, - { seen: new Set(), result: [] }, - ).result; + value = value.reduce((acc, t) => { + const key = `${t.name}:${t.value}`; + if (!acc.seen.has(key)) { + acc.seen.add(key); + acc.result.push(t); + } + return acc; + }, { seen: new Set(), result: [] }).result; if (!value.length) { - name = "any"; + name = 'any'; value = undefined; } return { name, - value, + value }; }; - const getPropTypeName = (propName) => { - if (propName.includes("=>") || propName === "Function") { - return "func"; - } else if (["boolean", "false", "true"].includes(propName)) { - return "bool"; - } else if (propName === "[]") { - return "array"; + const getPropTypeName = propName => { + if (propName.includes('=>') || propName === 'Function') { + return 'func'; + } else if (['boolean', 'false', 'true'].includes(propName)) { + return 'bool'; + } else if (propName === '[]') { + return 'array'; } else if ( - propName === "Element" || - propName === "ReactNode" || - propName === "ReactElement" || - propName === "DashComponent" + propName === 'Element' || + propName === 'ReactNode' || + propName === 'ReactElement' || + propName === 'DashComponent' ) { - return "node"; + return 'node'; } return propName; }; const getPropType = (propType, propObj, parentType = null) => { // Types can get namespace prefixes or not. - let name = checker.typeToString(propType).replace(/^React\./, ""); + let name = checker.typeToString(propType).replace(/^React\./, ''); let value, elements; const raw = name; - const newParentType = (parentType || []).concat(raw); + const newParentType = (parentType || []).concat(raw) if (propType.isUnion()) { if (isUnionLiteral(propType)) { - return { ...getEnum(propType), raw }; - } else if (raw.includes("|")) { - return { ...getUnion(propType, propObj, newParentType), raw }; + return {...getEnum(propType), raw}; + } else if (raw.includes('|')) { + return {...getUnion(propType, propObj, newParentType), raw}; } } name = getPropTypeName(name); // Shapes & array support. - if (!PRIMITIVES.concat("enum", "func", "union").includes(name)) { + if (!PRIMITIVES.concat('enum', 'func', 'union').includes(name)) { if ( // Excluding object with arrays in the raw. - (name.includes("[]") && name.endsWith("]")) || - name.includes("Array") + (name.includes('[]') && name.endsWith("]")) || + name.includes('Array') ) { - name = "arrayOf"; - const replaced = raw.replace("[]", ""); + name = 'arrayOf'; + const replaced = raw.replace('[]', ''); if (unionSupport.includes(replaced)) { // Simple types are easier. value = { name: getPropTypeName(replaced), - raw: replaced, + raw: replaced }; } else { // Complex types get the type parameter (Array) const [nodeType] = checker.getTypeArguments(propType); if (nodeType) { - value = getPropType(nodeType, propObj, newParentType); + value = getPropType( + nodeType, propObj, newParentType, + ); } else { // Not sure, might be unsupported here. - name = "array"; + name = 'array'; } } } else if ( - name === "tuple" || - (name.startsWith("[") && name.endsWith("]")) + name === 'tuple' || + (name.startsWith('[') && name.endsWith(']')) ) { - name = "tuple"; - elements = propType.resolvedTypeArguments.map((t) => - getPropType(t, propObj, newParentType), + name = 'tuple'; + elements = propType.resolvedTypeArguments.map( + t => getPropType(t, propObj, newParentType) ); } else if ( BANNED_TYPES.includes(name) || (parentType && parentType.includes(name)) ) { console.error(`Warning nested type: ${name}`); - name = "any"; + name = 'any'; } else { - name = "shape"; + name = 'shape'; // If the type is declared as union it will have a types attribute. if (propType.types && propType.types.length) { if (isUnionLiteral(propType)) { - return { ...getEnum(propType), raw }; + return {...getEnum(propType), raw}; } return { ...getUnion(propType, propObj, newParentType), - raw, + raw }; } else if (propType.indexInfos && propType.indexInfos.length) { - const { type } = propType.indexInfos[0]; - name = "objectOf"; + const {type} = propType.indexInfos[0]; + name = 'objectOf'; value = getPropType(type, propObj, newParentType); } else { value = getProps( @@ -423,23 +412,23 @@ function gatherComponents(sources, components = {}) { name, value, elements, - raw, + raw }; }; const getDefaultProps = (symbol, source) => { const statements = source.statements.filter( - (stmt) => + stmt => (!!stmt.name && checker.getSymbolAtLocation(stmt.name) === symbol) || ts.isExpressionStatement(stmt) || - ts.isVariableStatement(stmt), + ts.isVariableStatement(stmt) ); return statements.reduce((acc, statement) => { let propMap = {}; - statement.getChildren().forEach((child) => { - let { right } = child; + statement.getChildren().forEach(child => { + let {right} = child; if (right && ts.isIdentifier(right)) { const value = source.locals.get(right.escapedText); if ( @@ -452,7 +441,7 @@ function gatherComponents(sources, components = {}) { } } if (right) { - const { properties } = right; + const {properties} = right; if (properties) { propMap = getDefaultPropsValues(properties); } @@ -461,30 +450,30 @@ function gatherComponents(sources, components = {}) { return { ...acc, - ...propMap, + ...propMap }; }, {}); }; - const getPropComment = (symbol) => { + const getPropComment = symbol => { // Doesn't work too good with the JsDocTags losing indentation. // But used only in props should be fine. const comment = symbol.getDocumentationComment(); const tags = symbol.getJsDocTags(); if (comment && comment.length) { return comment - .map((c) => c.text) + .map(c => c.text) .concat( - tags.map((t) => - ["@", t.name].concat((t.text || []).map((e) => e.text)), - ), + tags.map(t => + ['@', t.name].concat((t.text || []).map(e => e.text)) + ) ) - .join("\n"); + .join('\n'); } - return ""; + return ''; }; - const getPropsForFunctionalComponent = (type) => { + const getPropsForFunctionalComponent = type => { const callSignatures = type.getCallSignatures(); for (const sig of callSignatures) { @@ -495,7 +484,7 @@ function gatherComponents(sources, components = {}) { // There is only one parameter for functional components: props const p = params[0]; - if (p.name === "props" || params.length === 1) { + if (p.name === 'props' || params.length === 1) { return p; } } @@ -523,13 +512,13 @@ function gatherComponents(sources, components = {}) { type.getProperties(), typeSymbol, [], - defaultProps, + defaultProps ); } } }; - const getDefaultPropsValues = (properties) => + const getDefaultPropsValues = properties => properties.reduce((acc, p) => { if (!p.name || !p.initializer) { return acc; @@ -547,7 +536,7 @@ function gatherComponents(sources, components = {}) { break; } - const { initializer } = p; + const {initializer} = p; switch (initializer.kind) { case ts.SyntaxKind.StringLiteral: @@ -557,13 +546,13 @@ function gatherComponents(sources, components = {}) { value = initializer.text; break; case ts.SyntaxKind.NullKeyword: - value = "null"; + value = 'null'; break; case ts.SyntaxKind.FalseKeyword: - value = "false"; + value = 'false'; break; case ts.SyntaxKind.TrueKeyword: - value = "true"; + value = 'true'; break; default: try { @@ -573,7 +562,7 @@ function gatherComponents(sources, components = {}) { } } - acc[propName] = { value, computed: false }; + acc[propName] = {value, computed: false}; return acc; }, {}); @@ -583,14 +572,14 @@ function gatherComponents(sources, components = {}) { // first declaration and one of them will be either // an ObjectLiteralExpression or an Identifier which get in the // newChild with the proper props. - const defaultProps = type.getProperty("defaultProps"); + const defaultProps = type.getProperty('defaultProps'); if (!defaultProps) { return {}; } const decl = defaultProps.getDeclarations()[0]; let propValues = {}; - decl.getChildren().forEach((child) => { + decl.getChildren().forEach(child => { let newChild = child; if (ts.isIdentifier(child)) { @@ -606,7 +595,7 @@ function gatherComponents(sources, components = {}) { } } - const { properties } = newChild; + const {properties} = newChild; if (properties) { propValues = getDefaultPropsValues(properties); } @@ -624,16 +613,16 @@ function gatherComponents(sources, components = {}) { ) => { const results = {}; - properties.forEach((prop) => { + properties.forEach(prop => { const name = prop.getName(); if (isReservedPropName(name)) { return; } const propType = checker.getTypeOfSymbolAtLocation( prop, - propsObj.valueDeclaration, + propsObj.valueDeclaration ); - const baseProp = baseProps.find((p) => p.getName() === name); + const baseProp = baseProps.find(p => p.getName() === name); const defaultValue = defaultProps[name]; const required = @@ -646,7 +635,7 @@ function gatherComponents(sources, components = {}) { let result = { description, required, - defaultValue, + defaultValue }; const type = getPropType(propType, propsObj, parentType); // root object is inserted as type, @@ -654,7 +643,7 @@ function gatherComponents(sources, components = {}) { if (!flat) { result.type = type; } else { - result = { ...result, ...type }; + result = {...result, ...type}; } results[name] = result; @@ -666,7 +655,7 @@ function gatherComponents(sources, components = {}) { const getPropInfo = (propsObj, defaultProps) => { const propsType = checker.getTypeOfSymbolAtLocation( propsObj, - propsObj.valueDeclaration, + propsObj.valueDeclaration ); const baseProps = propsType.getApparentProperties(); let propertiesOfProps = baseProps; @@ -674,15 +663,15 @@ function gatherComponents(sources, components = {}) { if (propsType.isUnionOrIntersection()) { propertiesOfProps = [ ...checker.getAllPossiblePropertiesOfTypes(propsType.types), - ...baseProps, + ...baseProps ]; if (!propertiesOfProps.length) { const subTypes = checker.getAllPossiblePropertiesOfTypes( propsType.types.reduce( (all, t) => [...all, ...(t.types || [])], - [], - ), + [] + ) ); propertiesOfProps = [...subTypes, ...baseProps]; } @@ -696,13 +685,13 @@ function gatherComponents(sources, components = {}) { const moduleSymbol = checker.getSymbolAtLocation(source); const exports = checker.getExportsOfModule(moduleSymbol); - exports.forEach((exp) => { + exports.forEach(exp => { let rootExp = getComponentFromExport(exp); const declaration = rootExp.valueDeclaration || rootExp.declarations[0]; const type = checker.getTypeOfSymbolAtLocation( rootExp, - declaration, + declaration ); let commentSource = rootExp; @@ -711,14 +700,14 @@ function gatherComponents(sources, components = {}) { if (!rootExp.valueDeclaration) { if ( - originalName === "default" && + originalName === 'default' && !typeSymbol && (rootExp.flags & ts.SymbolFlags.Alias) !== 0 ) { // Some type of Exotic? commentSource = checker.getAliasedSymbol( - commentSource, + commentSource ).valueDeclaration; } else if (!typeSymbol) { // Invalid component @@ -726,11 +715,15 @@ function gatherComponents(sources, components = {}) { } else { // Function components. rootExp = typeSymbol; - commentSource = - rootExp.valueDeclaration || rootExp.declarations[0]; - if (commentSource && commentSource.parent) { + commentSource = rootExp.valueDeclaration || rootExp.declarations[0]; + if ( + commentSource && + commentSource.parent + ) { // Function with export later like `const MyComponent = (props) => <>;` - commentSource = getParent(commentSource.parent); + commentSource = getParent( + commentSource.parent + ); } } } else if ( @@ -754,7 +747,7 @@ function gatherComponents(sources, components = {}) { let defaultProps = getDefaultProps(typeSymbol, source); const propsType = getPropsForFunctionalComponent(type); - const isContext = !!type.getProperty("isContext"); + const isContext = !!type.getProperty('isContext'); let props; @@ -765,9 +758,7 @@ function gatherComponents(sources, components = {}) { propsType.valueDeclaration.name.elements && propsType.valueDeclaration.name.elements.length ) { - defaultProps = getDefaultPropsValues( - propsType.valueDeclaration.name.elements, - ); + defaultProps = getDefaultPropsValues(propsType.valueDeclaration.name.elements); } props = getPropInfo(propsType, defaultProps); } else { @@ -775,7 +766,7 @@ function gatherComponents(sources, components = {}) { props = getPropsForClassComponent( typeSymbol, source, - defaultProps, + defaultProps ); } @@ -785,28 +776,28 @@ function gatherComponents(sources, components = {}) { } const fullText = source.getFullText(); - let description = ""; + let description = ''; const commentRanges = ts.getLeadingCommentRanges( fullText, - commentSource.getFullStart(), + commentSource.getFullStart() ); if (commentRanges && commentRanges.length) { description = commentRanges - .map((r) => + .map(r => fullText .slice(r.pos + 4, r.end - 3) - .split("\n") - .map((s) => s.replace(/^(\s*\*?\s)/, "")) - .filter((e) => e) - .join("\n"), + .split('\n') + .map(s => s.replace(/^(\s*\*?\s)/, '')) + .filter(e => e) + .join('\n') ) - .join(""); + .join(''); } const doc = { displayName: name, description, props, - isContext, + isContext }; docstringWarning(doc); components[cleanPath(filepath)] = doc; @@ -820,6 +811,6 @@ const metadata = gatherComponents(Array.isArray(src) ? src : [src]); if (!failedBuild) { process.stdout.write(JSON.stringify(metadata, null, 2)); } else { - logError("extract-meta failed"); + logError('extract-meta failed'); process.exit(1); } From bb74ebce0a56f6899c0f7ba61720142c340bcae6 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Fri, 17 Oct 2025 10:40:14 -0600 Subject: [PATCH 09/14] Remove useless string replace --- dash/extract-meta.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dash/extract-meta.js b/dash/extract-meta.js index a5a35905de..eb017f7f04 100755 --- a/dash/extract-meta.js +++ b/dash/extract-meta.js @@ -265,7 +265,7 @@ function gatherComponents(sources, components = {}) { if (t.members) { typeName = 'object'; } else { - const typeString = checker.typeToString(t).replace(/^React\./, ''); + const typeString = checker.typeToString(t); if (typeString === 'DashComponent') { typeName = 'node'; } From 0fab9a1126f70b015bc6dc328181b35f5a383fb2 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Fri, 17 Oct 2025 11:09:08 -0600 Subject: [PATCH 10/14] fix typescript testing issue --- @plotly/dash-generator-test-component-typescript/src/props.ts | 2 +- @plotly/dash-generator-test-component-typescript/tsconfig.json | 1 + dash/dash-renderer/src/wrapper/DashWrapper.tsx | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/@plotly/dash-generator-test-component-typescript/src/props.ts b/@plotly/dash-generator-test-component-typescript/src/props.ts index 0a305b6839..4b4f63f86d 100644 --- a/@plotly/dash-generator-test-component-typescript/src/props.ts +++ b/@plotly/dash-generator-test-component-typescript/src/props.ts @@ -1,6 +1,6 @@ // 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'; -import {DashComponent} from '@dash-renderer/types/component'; +import {DashComponent} from '@dash-renderer/types'; type Nested = { diff --git a/@plotly/dash-generator-test-component-typescript/tsconfig.json b/@plotly/dash-generator-test-component-typescript/tsconfig.json index 02dccc9a77..0d97d727df 100644 --- a/@plotly/dash-generator-test-component-typescript/tsconfig.json +++ b/@plotly/dash-generator-test-component-typescript/tsconfig.json @@ -3,6 +3,7 @@ "jsx": "react", "baseUrl": "src/ts", "paths": { + "@dash-renderer/*": ["../../../../dash/dash-renderer/src/*"] }, "inlineSources": true, "sourceMap": true, diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 57e152ebcc..514857d650 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -391,7 +391,7 @@ function DashWrapper({ if (node !== undefined) { if (isArray) { - for (let j = 0; j < node.length; j++) { + for (let j = 0; j < (node as []).length; j++) { const aPath = concat([opath], [j]); props = assocPath( aPath, From 6cefd884cddb7edc4e344e19eb8bd010c5a72577 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Fri, 17 Oct 2025 11:59:53 -0600 Subject: [PATCH 11/14] a different approach for TS compile issues --- .../src/props.ts | 7 ++++++- .../tsconfig.json | 1 - .../dash-core-components/src/components/Tabs.tsx | 10 +++++++--- dash/dash-renderer/src/wrapper/DashWrapper.tsx | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/@plotly/dash-generator-test-component-typescript/src/props.ts b/@plotly/dash-generator-test-component-typescript/src/props.ts index 4b4f63f86d..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,11 @@ // 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'; -import {DashComponent} from '@dash-renderer/types'; + +type DashComponent = { + props: string; + namespace: string; + children?: []; +} type Nested = { diff --git a/@plotly/dash-generator-test-component-typescript/tsconfig.json b/@plotly/dash-generator-test-component-typescript/tsconfig.json index 0d97d727df..02dccc9a77 100644 --- a/@plotly/dash-generator-test-component-typescript/tsconfig.json +++ b/@plotly/dash-generator-test-component-typescript/tsconfig.json @@ -3,7 +3,6 @@ "jsx": "react", "baseUrl": "src/ts", "paths": { - "@dash-renderer/*": ["../../../../dash/dash-renderer/src/*"] }, "inlineSources": true, "sourceMap": true, diff --git a/components/dash-core-components/src/components/Tabs.tsx b/components/dash-core-components/src/components/Tabs.tsx index 11dea16768..b12b90d1c7 100644 --- a/components/dash-core-components/src/components/Tabs.tsx +++ b/components/dash-core-components/src/components/Tabs.tsx @@ -121,9 +121,9 @@ function Tabs({ const firstChildren: TabProps = window.dash_component_api.getLayout( [...children[0].props.componentPath, 'props'] ); - return firstChildren.value; + return firstChildren.value ?? 'tab-1'; } - return undefined; + return 'tab-1'; }; // Initialize value on mount if not set @@ -271,7 +271,11 @@ function Tabs({ ); } -Tabs.dashPersistence = true; +Tabs.dashPersistence = { + persisted_props: [PersistedProps.value], + persistence_type: PersistenceTypes.local, +}; + Tabs.dashChildrenUpdate = true; export default Tabs; diff --git a/dash/dash-renderer/src/wrapper/DashWrapper.tsx b/dash/dash-renderer/src/wrapper/DashWrapper.tsx index 514857d650..57e152ebcc 100644 --- a/dash/dash-renderer/src/wrapper/DashWrapper.tsx +++ b/dash/dash-renderer/src/wrapper/DashWrapper.tsx @@ -391,7 +391,7 @@ function DashWrapper({ if (node !== undefined) { if (isArray) { - for (let j = 0; j < (node as []).length; j++) { + for (let j = 0; j < node.length; j++) { const aPath = concat([opath], [j]); props = assocPath( aPath, From 577158a130b5f002cc7912efba59748a8368cbe5 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Fri, 17 Oct 2025 13:58:27 -0600 Subject: [PATCH 12/14] fix browser size in tests --- components/dash-core-components/src/components/css/tabs.css | 6 ++---- .../tests/integration/misc/test_platter.py | 1 + .../dash-core-components/tests/integration/tab/test_tabs.py | 5 +++++ .../tests/integration/tab/test_tabs_with_graphs.py | 2 ++ 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/components/dash-core-components/src/components/css/tabs.css b/components/dash-core-components/src/components/css/tabs.css index 84160d09aa..991e4401c2 100644 --- a/components/dash-core-components/src/components/css/tabs.css +++ b/components/dash-core-components/src/components/css/tabs.css @@ -26,9 +26,6 @@ transition: background-color, color 200ms; text-align: center; box-sizing: border-box; -} - -.tab:hover { cursor: pointer; } @@ -45,7 +42,8 @@ /* Tab disabled state */ .tab--disabled { - color: #d6d6d6; + opacity: 0.6; + cursor: not-allowed; } /* Tab content area */ 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") From dd6c4af969aba706327805c23de1b9d3c9c37558 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Mon, 20 Oct 2025 13:39:43 -0600 Subject: [PATCH 13/14] memoize classnames --- .../src/components/Tabs.tsx | 76 +++++++++++-------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/components/dash-core-components/src/components/Tabs.tsx b/components/dash-core-components/src/components/Tabs.tsx index b12b90d1c7..7e97002ad3 100644 --- a/components/dash-core-components/src/components/Tabs.tsx +++ b/components/dash-core-components/src/components/Tabs.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {has, isNil} from 'ramda'; import LoadingElement from '../utils/_LoadingElement'; @@ -34,20 +34,24 @@ const EnhancedTab = ({ // We use the raw path here since it's up one level from // the tabs child. const isLoading = ctx.useLoading({rawPath: componentPath}); - const tabStyle = { - ...style, - ...(disabled ? disabled_style : {}), - ...(selected ? selected_style : {}), - }; - - const tabClassNames = [ - 'tab', - className, - disabled ? 'tab--disabled' : null, - disabled ? disabled_className : null, - selected ? 'tab--selected' : null, - selected ? selected_className : null, - ].filter(Boolean); + const tabStyle = useMemo(() => { + return { + ...style, + ...(disabled ? disabled_style : {}), + ...(selected ? selected_style : {}), + }; + }, [style, disabled, disabled_style, selected, selected_style]); + + const tabClassNames = useMemo(() => { + return [ + 'tab', + className, + disabled ? 'tab--disabled' : null, + disabled ? disabled_className : null, + selected ? 'tab--selected' : null, + selected ? selected_className : null, + ].filter((el): el is string => Boolean(el)); + }, [className, disabled, disabled_className, selected, selected_className]); let labelDisplay; if (typeof label === 'object') { @@ -217,24 +221,30 @@ function Tabs({ const selectedTabContent = !isNil(selectedTab) ? selectedTab : ''; - const tabContainerClassNames = [ - 'tab-container', - vertical ? 'tab-container--vert' : null, - props.className, - ].filter(Boolean); - - const tabContentClassNames = [ - 'tab-content', - vertical ? 'tab-content--vert' : null, - props.content_className, - ].filter(Boolean); - - const tabParentClassNames = [ - 'tab-parent', - vertical ? ' tab-parent--vert' : null, - isAboveBreakpoint ? ' tab-parent--above-breakpoint' : null, - props.parent_className, - ].filter(Boolean); + const tabContainerClassNames = useMemo(() => { + return [ + 'tab-container', + vertical ? 'tab-container--vert' : null, + props.className, + ].filter((el): el is string => Boolean(el)); + }, [vertical, props.className]); + + const tabContentClassNames = useMemo(() => { + return [ + 'tab-content', + vertical ? 'tab-content--vert' : null, + props.content_className, + ].filter((el): el is string => Boolean(el)); + }, [vertical, props.content_className]); + + const tabParentClassNames = useMemo(() => { + return [ + 'tab-parent', + vertical ? ' tab-parent--vert' : null, + isAboveBreakpoint ? ' tab-parent--above-breakpoint' : null, + props.parent_className, + ].filter((el): el is string => Boolean(el)); + }, [vertical, isAboveBreakpoint, props.parent_className]); // Set CSS variables for dynamic styling const cssVars = { From 2075e5b3aa746edc9e7adbb8bbbd069a7fe67da9 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Thu, 23 Oct 2025 09:32:00 -0600 Subject: [PATCH 14/14] Refactor how classNameis composed in tabs --- .../src/components/Tabs.tsx | 76 ++++++++++++------- 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/components/dash-core-components/src/components/Tabs.tsx b/components/dash-core-components/src/components/Tabs.tsx index 7e97002ad3..b84a8c8dc4 100644 --- a/components/dash-core-components/src/components/Tabs.tsx +++ b/components/dash-core-components/src/components/Tabs.tsx @@ -43,14 +43,23 @@ const EnhancedTab = ({ }, [style, disabled, disabled_style, selected, selected_style]); const tabClassNames = useMemo(() => { - return [ - 'tab', - className, - disabled ? 'tab--disabled' : null, - disabled ? disabled_className : null, - selected ? 'tab--selected' : null, - selected ? selected_className : null, - ].filter((el): el is string => Boolean(el)); + 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; @@ -68,7 +77,7 @@ const EnhancedTab = ({ return (
{ @@ -222,28 +231,39 @@ function Tabs({ const selectedTabContent = !isNil(selectedTab) ? selectedTab : ''; const tabContainerClassNames = useMemo(() => { - return [ - 'tab-container', - vertical ? 'tab-container--vert' : null, - props.className, - ].filter((el): el is string => Boolean(el)); + let names = 'tab-container'; + if (vertical) { + names += ` tab-container--vert`; + } + if (props.className) { + names += ` ${props.className}`; + } + return names; }, [vertical, props.className]); const tabContentClassNames = useMemo(() => { - return [ - 'tab-content', - vertical ? 'tab-content--vert' : null, - props.content_className, - ].filter((el): el is string => Boolean(el)); + 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(() => { - return [ - 'tab-parent', - vertical ? ' tab-parent--vert' : null, - isAboveBreakpoint ? ' tab-parent--above-breakpoint' : null, - props.parent_className, - ].filter((el): el is string => Boolean(el)); + 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 @@ -257,20 +277,20 @@ function Tabs({ {loadingProps => (
{EnhancedTabs}
{selectedTabContent || ''}