Skip to content

Commit 0fbd86a

Browse files
committed
Refactor Tab component
1 parent 427723d commit 0fbd86a

File tree

12 files changed

+172
-157
lines changed

12 files changed

+172
-157
lines changed

@plotly/dash-generator-test-component-typescript/generator.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ describe('Test Typescript component metadata generation', () => {
9595
`${componentName} element JSX.Element`,
9696
testTypeFactory('element', 'node')
9797
);
98+
test(
99+
`${componentName} dash_component DashComponent`,
100+
testTypeFactory("dash_component", "node"),
101+
);
98102
test(
99103
`${componentName} boolean type`,
100104
testTypeFactory('a_bool', 'bool')

@plotly/dash-generator-test-component-typescript/src/props.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Needs to export types if not in a d.ts file or if any import is present in the d.ts
22
import React from 'react';
3+
import {DashComponent} from '@dash-renderer/types/component';
34

45

56
type Nested = {
@@ -36,6 +37,7 @@ export type TypescriptComponentProps = {
3637
| boolean;
3738
element?: JSX.Element;
3839
array_elements?: JSX.Element[];
40+
dash_component?: DashComponent;
3941

4042
string_default?: string;
4143
number_default?: number;

components/dash-core-components/src/components/Tab.react.js

Lines changed: 0 additions & 79 deletions
This file was deleted.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react';
2+
import './css/tabs.css';
3+
import {TabProps} from '../types';
4+
5+
/**
6+
* Part of dcc.Tabs - this is the child Tab component used to render a tabbed page.
7+
* Its children will be set as the content of that tab, which if clicked will become visible.
8+
*/
9+
function Tab({
10+
children,
11+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
12+
disabled = false,
13+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
14+
disabled_style = {color: 'var(--Dash-Text-Disabled)'},
15+
}: TabProps) {
16+
return <>{children}</>;
17+
}
18+
19+
export default Tab;

components/dash-core-components/src/components/Tabs.tsx

Lines changed: 65 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,79 @@
1-
import React, {useEffect, useRef, useState} from 'react';
2-
import {has, is, isNil} from 'ramda';
1+
import React, {useCallback, useEffect, useRef, useState} from 'react';
2+
import {has, isNil} from 'ramda';
33

44
import LoadingElement from '../utils/_LoadingElement';
5-
import {DashComponent} from '@dash-renderer/types/component';
5+
import {PersistedProps, PersistenceTypes, TabProps, TabsProps} from '../types';
66
import './css/tabs.css';
7+
import {DashComponent} from '@dash-renderer/types/component';
78

8-
interface EnhancedTabProps {
9-
id?: string;
10-
label?: string | DashComponent[];
9+
interface EnhancedTabProps extends TabProps {
1110
selected: boolean;
12-
className?: string;
13-
style?: React.CSSProperties;
14-
selectedClassName?: string;
15-
selected_style?: React.CSSProperties;
16-
selectHandler: (value: string) => void;
17-
value: string;
18-
disabled?: boolean;
19-
disabled_style?: React.CSSProperties;
20-
disabled_className?: string;
21-
componentPath: string[];
11+
componentPath?: (string | number)[];
2212
}
23-
import {PersistedProps, PersistenceTypes, TabsProps} from '../types';
2413

25-
// EnhancedTab is defined here instead of in Tab.react.js because if exported there,
14+
// EnhancedTab is defined here instead of in Tab.tsx because if exported there,
2615
// it will mess up the Python imports and metadata.json
2716
const EnhancedTab = ({
2817
id,
2918
label,
3019
selected,
3120
className,
3221
style,
33-
selectedClassName,
22+
selected_className,
3423
selected_style,
35-
selectHandler,
24+
setProps: selectHandler,
3625
value,
3726
disabled = false,
38-
disabled_style = {color: '#d6d6d6'},
27+
disabled_style = {color: 'var(--Dash-Text-Disabled)'},
3928
disabled_className,
4029
componentPath,
4130
}: EnhancedTabProps) => {
31+
const ExternalWrapper = window.dash_component_api.ExternalWrapper;
4232
const ctx = window.dash_component_api.useDashContext();
33+
componentPath = componentPath ?? ctx.componentPath;
4334
// We use the raw path here since it's up one level from
4435
// the tabs child.
45-
const isLoading = ctx.useLoading({rawPath: !!componentPath});
36+
const isLoading = ctx.useLoading({rawPath: componentPath});
37+
const tabStyle = {
38+
...style,
39+
...(disabled ? disabled_style : {}),
40+
...(selected ? selected_style : {}),
41+
};
42+
43+
const tabClassNames = [
44+
'tab',
45+
className,
46+
disabled ? 'tab--disabled' : null,
47+
disabled ? disabled_className : null,
48+
selected ? 'tab--selected' : null,
49+
selected ? selected_className : null,
50+
].filter(Boolean);
4651

47-
let tabStyle = style;
48-
if (disabled) {
49-
tabStyle = {...tabStyle, ...disabled_style};
50-
}
51-
if (selected) {
52-
tabStyle = {...tabStyle, ...selected_style};
53-
}
54-
let tabClassName = `tab ${className || ''}`;
55-
if (disabled) {
56-
tabClassName += ` tab--disabled ${disabled_className || ''}`;
57-
}
58-
if (selected) {
59-
tabClassName += ` tab--selected ${selectedClassName || ''}`;
60-
}
6152
let labelDisplay;
62-
if (is(Array, label)) {
63-
// label is an array, so it has children that we want to render
64-
labelDisplay = label[0].props.children;
53+
if (typeof label === 'object') {
54+
labelDisplay = (
55+
<ExternalWrapper
56+
component={label}
57+
componentPath={[...componentPath, 0]}
58+
/>
59+
);
6560
} else {
66-
// else it is a string, so we just want to render that
67-
labelDisplay = label;
61+
labelDisplay = <span>{label}</span>;
6862
}
63+
6964
return (
7065
<div
7166
data-dash-is-loading={isLoading}
72-
className={tabClassName}
67+
className={tabClassNames.join(' ')}
7368
id={id}
7469
style={tabStyle}
7570
onClick={() => {
7671
if (!disabled) {
77-
selectHandler(value);
72+
selectHandler({value});
7873
}
7974
}}
8075
>
81-
<span>{labelDisplay}</span>
76+
{labelDisplay}
8277
</div>
8378
);
8479
};
@@ -101,26 +96,28 @@ function Tabs({
10196
persisted_props = [PersistedProps.value],
10297
// eslint-disable-next-line @typescript-eslint/no-unused-vars
10398
persistence_type = PersistenceTypes.local,
99+
children,
104100
...props
105101
}: TabsProps) {
106102
const initializedRef = useRef(false);
107103
const [isAboveBreakpoint, setIsAboveBreakpoint] = useState(false);
108104

109-
const parseChildrenToArray = () => {
110-
if (props.children && !is(Array, props.children)) {
111-
// if dcc.Tabs.children contains just one single element, it gets passed as an object
112-
// instead of an array - so we put it in an array ourselves!
113-
return [props.children];
105+
const parseChildrenToArray = useCallback((): DashComponent[] => {
106+
if (!children) {
107+
return [];
114108
}
115-
return props.children ?? [];
116-
};
109+
if (children instanceof Array) {
110+
return children;
111+
}
112+
return [children];
113+
}, [children]);
117114

118115
const valueOrDefault = () => {
119116
if (has('value', props)) {
120117
return props.value;
121118
}
122119
const children = parseChildrenToArray();
123-
if (children && children.length) {
120+
if (children && children.length && children[0].props.componentPath) {
124121
const firstChildren = window.dash_component_api.getLayout([
125122
...children[0].props.componentPath,
126123
'props',
@@ -131,10 +128,6 @@ function Tabs({
131128
return 'tab-1';
132129
};
133130

134-
const selectHandler = (value: string) => {
135-
props.setProps({value: value});
136-
};
137-
138131
// Initialize value on mount if not set
139132
useEffect(() => {
140133
if (!initializedRef.current && !has('value', props)) {
@@ -167,15 +160,15 @@ function Tabs({
167160

168161
const value = valueOrDefault();
169162

170-
if (props.children) {
163+
if (children) {
171164
const children = parseChildrenToArray();
172165

173166
EnhancedTabs = children.map((child, index) => {
174167
// TODO: handle components that are not dcc.Tab components (throw error)
175168
// enhance Tab components coming from Dash (as dcc.Tab) with methods needed for handling logic
176-
let childProps;
169+
let childProps: Omit<TabProps, 'setProps'>;
177170

178-
if (React.isValidElement(child)) {
171+
if (React.isValidElement(child) && child.props.componentPath) {
179172
childProps = window.dash_component_api.getLayout([
180173
...child.props.componentPath,
181174
'props',
@@ -194,22 +187,31 @@ function Tabs({
194187
selectedTab = child;
195188
}
196189

190+
const style = childProps.style ?? {};
191+
if (typeof childProps.width === 'number') {
192+
style.width = `${childProps.width}px`;
193+
style.flex = 'none';
194+
} else if (typeof childProps.width === 'string') {
195+
style.width = childProps.width;
196+
style.flex = 'none';
197+
}
198+
197199
return (
198200
<EnhancedTab
199201
key={index}
200202
id={childProps.id}
201203
label={childProps.label}
202204
selected={value === childProps.value}
203-
selectHandler={selectHandler}
205+
setProps={props.setProps}
204206
className={childProps.className}
205-
style={childProps.style}
206-
selectedClassName={childProps.selected_className}
207+
style={style}
208+
selected_className={childProps.selected_className}
207209
selected_style={childProps.selected_style}
208210
value={childProps.value}
209211
disabled={childProps.disabled}
210212
disabled_style={childProps.disabled_style}
211213
disabled_className={childProps.disabled_className}
212-
componentPath={child.componentPath}
214+
componentPath={child.props.componentPath}
213215
/>
214216
);
215217
});
@@ -241,7 +243,6 @@ function Tabs({
241243
'--tabs-border': colors.border,
242244
'--tabs-primary': colors.primary,
243245
'--tabs-background': colors.background,
244-
'--tabs-width': `calc(100% / ${parseChildrenToArray().length})`,
245246
} as const;
246247

247248
return (

components/dash-core-components/src/components/css/tabs.css

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
/* Individual tab */
1818
.tab {
19-
display: inline-block;
19+
flex: 1;
2020
background-color: var(--tabs-background);
2121
border: 1px solid var(--tabs-border);
2222
border-bottom: none;
@@ -64,11 +64,6 @@
6464
border-right: none;
6565
}
6666

67-
/* Horizontal tabs: equal width distribution (only when not vertical) */
68-
.tab-parent--above-breakpoint .tab-container:not(.tab-container--vert) .tab {
69-
width: var(--tabs-width);
70-
}
71-
7267
.tab-parent--above-breakpoint .tab--selected,
7368
.tab-parent--above-breakpoint .tab:last-of-type.tab--selected {
7469
border-bottom: none;

components/dash-core-components/src/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable import/prefer-default-export */
21
import Checklist from './components/Checklist';
32
import Clipboard from './components/Clipboard.react';
43
import ConfirmDialog from './components/ConfirmDialog.react';
@@ -19,7 +18,7 @@ import RadioItems from './components/RadioItems';
1918
import RangeSlider from './components/RangeSlider';
2019
import Slider from './components/Slider';
2120
import Store from './components/Store.react';
22-
import Tab from './components/Tab.react';
21+
import Tab from './components/Tab';
2322
import Tabs from './components/Tabs';
2423
import Textarea from './components/Textarea.react';
2524
import Tooltip from './components/Tooltip.react';

0 commit comments

Comments
 (0)