@itwin/itwinui-react 3.0.0-dev.11 → 3.0.0-dev.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/cjs/core/ColorPicker/ColorBuilder.js +2 -0
- package/cjs/core/ColorPicker/ColorInputPanel.js +24 -4
- package/cjs/core/ColorPicker/ColorPalette.js +2 -80
- package/cjs/core/ColorPicker/ColorSwatch.d.ts +1 -1
- package/cjs/core/ColorPicker/ColorSwatch.js +25 -15
- package/cjs/core/DropdownMenu/DropdownMenu.d.ts +2 -2
- package/cjs/core/LabeledSelect/LabeledSelect.d.ts +1 -1
- package/cjs/core/LabeledSelect/LabeledSelect.js +3 -3
- package/cjs/core/Popover/Popover.d.ts +26 -6
- package/cjs/core/Popover/Popover.js +38 -24
- package/cjs/core/Select/Select.d.ts +1 -1
- package/cjs/core/Select/Select.js +6 -4
- package/cjs/core/Surface/Surface.d.ts +8 -0
- package/cjs/core/Surface/Surface.js +19 -1
- package/cjs/core/Table/columns/expanderColumn.js +1 -0
- package/cjs/core/Tabs/Tabs.d.ts +222 -52
- package/cjs/core/Tabs/Tabs.js +436 -376
- package/cjs/core/ThemeProvider/ThemeProvider.js +3 -1
- package/cjs/index.d.ts +1 -2
- package/cjs/index.js +1 -2
- package/cjs/styles.js +4 -2
- package/esm/core/ColorPicker/ColorBuilder.js +2 -0
- package/esm/core/ColorPicker/ColorInputPanel.js +25 -5
- package/esm/core/ColorPicker/ColorPalette.js +3 -83
- package/esm/core/ColorPicker/ColorSwatch.d.ts +1 -1
- package/esm/core/ColorPicker/ColorSwatch.js +18 -12
- package/esm/core/DropdownMenu/DropdownMenu.d.ts +2 -2
- package/esm/core/DropdownMenu/DropdownMenu.js +1 -1
- package/esm/core/LabeledSelect/LabeledSelect.d.ts +1 -1
- package/esm/core/LabeledSelect/LabeledSelect.js +3 -2
- package/esm/core/Popover/Popover.d.ts +26 -6
- package/esm/core/Popover/Popover.js +40 -24
- package/esm/core/Select/Select.d.ts +1 -1
- package/esm/core/Select/Select.js +3 -3
- package/esm/core/Surface/Surface.d.ts +8 -0
- package/esm/core/Surface/Surface.js +19 -1
- package/esm/core/Table/columns/expanderColumn.js +1 -0
- package/esm/core/Tabs/Tabs.d.ts +222 -52
- package/esm/core/Tabs/Tabs.js +429 -369
- package/esm/core/ThemeProvider/ThemeProvider.js +3 -1
- package/esm/index.d.ts +1 -2
- package/esm/index.js +1 -2
- package/esm/styles.js +4 -2
- package/package.json +2 -2
- package/styles.css +13 -13
- package/cjs/core/Tabs/Tab.d.ts +0 -40
- package/cjs/core/Tabs/Tab.js +0 -65
- package/esm/core/Tabs/Tab.d.ts +0 -40
- package/esm/core/Tabs/Tab.js +0 -57
package/esm/core/Tabs/Tabs.js
CHANGED
|
@@ -5,440 +5,500 @@
|
|
|
5
5
|
import cx from 'classnames';
|
|
6
6
|
import * as React from 'react';
|
|
7
7
|
import {
|
|
8
|
+
useSafeContext,
|
|
9
|
+
Box,
|
|
10
|
+
polymorphic,
|
|
11
|
+
useIsClient,
|
|
12
|
+
useIsomorphicLayoutEffect,
|
|
8
13
|
useMergedRefs,
|
|
9
|
-
getBoundedValue,
|
|
10
14
|
useContainerWidth,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
ButtonBase,
|
|
16
|
+
mergeEventHandlers,
|
|
17
|
+
useControlledState,
|
|
18
|
+
useId,
|
|
19
|
+
getWindow,
|
|
15
20
|
} from '../utils/index.js';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* const tabs = [
|
|
22
|
-
* <Tab label='Label 1' />,
|
|
23
|
-
* <Tab label='Label 2' />,
|
|
24
|
-
* <Tab label='Label 3' />,
|
|
25
|
-
* ];
|
|
26
|
-
* <Tabs labels={tabs} />
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* <Tabs orientation='vertical' labels={tabs} />
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* const tabsWithSublabels = [
|
|
33
|
-
* <Tab label='Label 1' sublabel='First tab' />,
|
|
34
|
-
* <Tab label='Label 2' sublabel='Active tab' />,
|
|
35
|
-
* ];
|
|
36
|
-
* <Tabs labels={tabsWithSublabels} activeIndex={1} />
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* const tabsWithIcons = [
|
|
40
|
-
* <Tab label='Label 1' icon={<SvgPlaceholder />} />,
|
|
41
|
-
* <Tab label='Label 2' icon={<SvgPlaceholder />} />,
|
|
42
|
-
* ];
|
|
43
|
-
* <Tabs labels={tabsWithIcons} type='pill' />
|
|
44
|
-
*/
|
|
45
|
-
export const Tabs = (props) => {
|
|
46
|
-
// Separate actions from props to avoid adding it to the DOM (using {...rest})
|
|
47
|
-
let actions;
|
|
48
|
-
if (props.type !== 'pill' && props.actions) {
|
|
49
|
-
actions = props.actions;
|
|
50
|
-
props = { ...props };
|
|
51
|
-
delete props.actions;
|
|
52
|
-
}
|
|
53
|
-
// Separate overflowOptions from props to avoid adding it to the DOM (using {...rest})
|
|
54
|
-
let overflowOptions;
|
|
55
|
-
if (
|
|
56
|
-
props.type !== 'borderless' &&
|
|
57
|
-
props.type !== 'pill' &&
|
|
58
|
-
props.overflowOptions
|
|
59
|
-
) {
|
|
60
|
-
overflowOptions = props.overflowOptions;
|
|
61
|
-
props = { ...props };
|
|
62
|
-
delete props.overflowOptions;
|
|
63
|
-
}
|
|
21
|
+
import { Icon } from '../Icon/Icon.js';
|
|
22
|
+
// Checking user motion preference for scroll into view animation
|
|
23
|
+
const isMotionOk = () =>
|
|
24
|
+
getWindow()?.matchMedia?.('(prefers-reduced-motion: no-preference)')?.matches;
|
|
25
|
+
const TabsWrapper = React.forwardRef((props, ref) => {
|
|
64
26
|
const {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
focusActivationMode = 'auto',
|
|
27
|
+
className,
|
|
28
|
+
children,
|
|
29
|
+
orientation = 'horizontal',
|
|
69
30
|
type = 'default',
|
|
31
|
+
focusActivationMode = 'auto',
|
|
70
32
|
color = 'blue',
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
wrapperClassName,
|
|
75
|
-
children,
|
|
33
|
+
defaultValue,
|
|
34
|
+
value: activeValueProp,
|
|
35
|
+
onValueChange,
|
|
76
36
|
...rest
|
|
77
37
|
} = props;
|
|
38
|
+
const [activeValue, setActiveValue] = useControlledState(
|
|
39
|
+
defaultValue,
|
|
40
|
+
activeValueProp,
|
|
41
|
+
onValueChange,
|
|
42
|
+
);
|
|
43
|
+
const [stripeProperties, setStripeProperties] = React.useState({});
|
|
44
|
+
const [hasSublabel, setHasSublabel] = React.useState(false); // used for setting size
|
|
45
|
+
const idPrefix = useId();
|
|
46
|
+
return React.createElement(
|
|
47
|
+
Box,
|
|
48
|
+
{
|
|
49
|
+
className: cx('iui-tabs-wrapper', `iui-${orientation}`, className),
|
|
50
|
+
...rest,
|
|
51
|
+
style: { ...stripeProperties, ...props?.style },
|
|
52
|
+
ref: ref,
|
|
53
|
+
},
|
|
54
|
+
React.createElement(
|
|
55
|
+
TabsContext.Provider,
|
|
56
|
+
{
|
|
57
|
+
value: {
|
|
58
|
+
orientation,
|
|
59
|
+
type,
|
|
60
|
+
activeValue,
|
|
61
|
+
setActiveValue,
|
|
62
|
+
setStripeProperties,
|
|
63
|
+
idPrefix,
|
|
64
|
+
focusActivationMode,
|
|
65
|
+
hasSublabel,
|
|
66
|
+
setHasSublabel,
|
|
67
|
+
color,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
children,
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
TabsWrapper.displayName = 'Tabs.Wrapper';
|
|
75
|
+
const TabList = React.forwardRef((props, ref) => {
|
|
76
|
+
const { className, children, ...rest } = props;
|
|
77
|
+
const { type, hasSublabel, color } = useSafeContext(TabsContext);
|
|
78
78
|
const isClient = useIsClient();
|
|
79
79
|
const tablistRef = React.useRef(null);
|
|
80
80
|
const [tablistSizeRef, tabsWidth] = useContainerWidth(type !== 'default');
|
|
81
|
-
const refs = useMergedRefs(tablistRef, tablistSizeRef);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
:
|
|
81
|
+
const refs = useMergedRefs(ref, tablistRef, tablistSizeRef);
|
|
82
|
+
return React.createElement(
|
|
83
|
+
Box,
|
|
84
|
+
{
|
|
85
|
+
className: cx(
|
|
86
|
+
'iui-tabs',
|
|
87
|
+
`iui-${type}`,
|
|
88
|
+
{
|
|
89
|
+
'iui-green': color === 'green',
|
|
90
|
+
'iui-animated': type !== 'default' && isClient,
|
|
91
|
+
'iui-not-animated': type !== 'default' && !isClient,
|
|
92
|
+
'iui-large': hasSublabel,
|
|
93
|
+
},
|
|
94
|
+
className,
|
|
95
|
+
),
|
|
96
|
+
role: 'tablist',
|
|
97
|
+
ref: refs,
|
|
98
|
+
...rest,
|
|
99
|
+
},
|
|
100
|
+
React.createElement(
|
|
101
|
+
TabListContext.Provider,
|
|
102
|
+
{
|
|
103
|
+
value: {
|
|
104
|
+
tabsWidth,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
children,
|
|
108
|
+
),
|
|
86
109
|
);
|
|
110
|
+
});
|
|
111
|
+
TabList.displayName = 'Tabs.TabList';
|
|
112
|
+
const Tab = React.forwardRef((props, forwardedRef) => {
|
|
113
|
+
const { className, children, value, label, ...rest } = props;
|
|
114
|
+
const {
|
|
115
|
+
orientation,
|
|
116
|
+
activeValue,
|
|
117
|
+
setActiveValue,
|
|
118
|
+
type,
|
|
119
|
+
setStripeProperties,
|
|
120
|
+
idPrefix,
|
|
121
|
+
focusActivationMode,
|
|
122
|
+
} = useSafeContext(TabsContext);
|
|
123
|
+
const { tabsWidth } = useSafeContext(TabListContext);
|
|
124
|
+
const tabRef = React.useRef();
|
|
125
|
+
const isActive = activeValue === value;
|
|
87
126
|
useIsomorphicLayoutEffect(() => {
|
|
88
|
-
if (
|
|
89
|
-
|
|
127
|
+
if (isActive) {
|
|
128
|
+
if (orientation === 'horizontal') {
|
|
129
|
+
tabRef.current?.scrollIntoView({
|
|
130
|
+
inline: 'center',
|
|
131
|
+
behavior: isMotionOk() ? 'smooth' : 'auto',
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
tabRef.current?.scrollIntoView({
|
|
135
|
+
block: 'center',
|
|
136
|
+
behavior: isMotionOk() ? 'smooth' : 'auto',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
90
139
|
}
|
|
91
|
-
}, [
|
|
140
|
+
}, [isActive]);
|
|
141
|
+
const updateStripe = () => {
|
|
142
|
+
const currentTabRect = tabRef.current?.getBoundingClientRect();
|
|
143
|
+
setStripeProperties({
|
|
144
|
+
'--iui-tabs-stripe-size':
|
|
145
|
+
orientation === 'horizontal'
|
|
146
|
+
? `${currentTabRect?.width}px`
|
|
147
|
+
: `${currentTabRect?.height}px`,
|
|
148
|
+
'--iui-tabs-stripe-position':
|
|
149
|
+
orientation === 'horizontal'
|
|
150
|
+
? `${tabRef.current?.offsetLeft}px`
|
|
151
|
+
: `${tabRef.current?.offsetTop}px`,
|
|
152
|
+
});
|
|
153
|
+
};
|
|
92
154
|
// CSS custom properties to place the active stripe
|
|
93
|
-
const [stripeProperties, setStripeProperties] = React.useState({});
|
|
94
|
-
useIsomorphicLayoutEffect(() => {
|
|
95
|
-
if (type !== 'default' && tablistRef.current != undefined) {
|
|
96
|
-
const activeTab = tablistRef.current.children[currentActiveIndex];
|
|
97
|
-
const activeTabRect = activeTab.getBoundingClientRect();
|
|
98
|
-
setStripeProperties({
|
|
99
|
-
...(orientation === 'horizontal' && {
|
|
100
|
-
'--stripe-width': `${activeTabRect.width}px`,
|
|
101
|
-
'--stripe-left': `${activeTab.offsetLeft}px`,
|
|
102
|
-
}),
|
|
103
|
-
...(orientation === 'vertical' && {
|
|
104
|
-
'--stripe-height': `${activeTabRect.height}px`,
|
|
105
|
-
'--stripe-top': `${activeTab.offsetTop}px`,
|
|
106
|
-
}),
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}, [currentActiveIndex, type, orientation, tabsWidth]);
|
|
110
|
-
const [focusedIndex, setFocusedIndex] = React.useState();
|
|
111
|
-
React.useEffect(() => {
|
|
112
|
-
if (tablistRef.current && focusedIndex !== undefined) {
|
|
113
|
-
const tab = tablistRef.current.querySelectorAll(`.${styles['iui-tab']}`)[
|
|
114
|
-
focusedIndex
|
|
115
|
-
];
|
|
116
|
-
tab?.focus();
|
|
117
|
-
}
|
|
118
|
-
}, [focusedIndex]);
|
|
119
|
-
const [hasSublabel, setHasSublabel] = React.useState(false); // used for setting size
|
|
120
155
|
useIsomorphicLayoutEffect(() => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
!!tablistRef.current?.querySelector(
|
|
124
|
-
`.${styles['iui-tab-description']}`,
|
|
125
|
-
),
|
|
126
|
-
);
|
|
127
|
-
}, [type]);
|
|
128
|
-
const enableHorizontalScroll = React.useCallback((e) => {
|
|
129
|
-
const ownerDoc = tablistRef.current;
|
|
130
|
-
if (ownerDoc === null) {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
let scrollLeft = ownerDoc?.scrollLeft ?? 0;
|
|
134
|
-
if (e.deltaY > 0 || e.deltaX > 0) {
|
|
135
|
-
scrollLeft += 25;
|
|
136
|
-
} else if (e.deltaY < 0 || e.deltaX < 0) {
|
|
137
|
-
scrollLeft -= 25;
|
|
138
|
-
}
|
|
139
|
-
ownerDoc.scrollLeft = scrollLeft;
|
|
140
|
-
}, []);
|
|
141
|
-
// allow normal mouse wheels to scroll horizontally for horizontal overflow
|
|
142
|
-
React.useEffect(() => {
|
|
143
|
-
const ownerDoc = tablistRef.current;
|
|
144
|
-
if (ownerDoc === null) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (!overflowOptions?.useOverflow || orientation === 'vertical') {
|
|
148
|
-
ownerDoc.removeEventListener('wheel', enableHorizontalScroll);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
ownerDoc.addEventListener('wheel', enableHorizontalScroll);
|
|
152
|
-
}, [overflowOptions?.useOverflow, orientation, enableHorizontalScroll]);
|
|
153
|
-
const isTabHidden = (activeTab, isVertical) => {
|
|
154
|
-
const ownerDoc = tablistRef.current;
|
|
155
|
-
if (ownerDoc === null) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
const fadeBuffer = isVertical
|
|
159
|
-
? ownerDoc.offsetHeight * 0.05
|
|
160
|
-
: ownerDoc.offsetWidth * 0.05;
|
|
161
|
-
const visibleStart = isVertical ? ownerDoc.scrollTop : ownerDoc.scrollLeft;
|
|
162
|
-
const visibleEnd = isVertical
|
|
163
|
-
? ownerDoc.scrollTop + ownerDoc.offsetHeight
|
|
164
|
-
: ownerDoc.scrollLeft + ownerDoc.offsetWidth;
|
|
165
|
-
const tabStart = isVertical ? activeTab.offsetTop : activeTab.offsetLeft;
|
|
166
|
-
const tabEnd = isVertical
|
|
167
|
-
? activeTab.offsetTop + activeTab.offsetHeight
|
|
168
|
-
: activeTab.offsetLeft + activeTab.offsetWidth;
|
|
169
|
-
if (
|
|
170
|
-
tabStart > visibleStart + fadeBuffer &&
|
|
171
|
-
tabEnd < visibleEnd - fadeBuffer
|
|
172
|
-
) {
|
|
173
|
-
return 0; // tab is visible
|
|
174
|
-
} else if (tabStart < visibleStart + fadeBuffer) {
|
|
175
|
-
return -1; // tab is before visible section
|
|
176
|
-
} else {
|
|
177
|
-
return 1; // tab is after visible section
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
const easeInOutQuad = (time, beginning, change, duration) => {
|
|
181
|
-
if ((time /= duration / 2) < 1) {
|
|
182
|
-
return (change / 2) * time * time + beginning;
|
|
156
|
+
if (type !== 'default' && isActive) {
|
|
157
|
+
updateStripe();
|
|
183
158
|
}
|
|
184
|
-
return (-change / 2) * (--time * (time - 2) - 1) + beginning;
|
|
185
|
-
};
|
|
186
|
-
const scrollToTab = React.useCallback(
|
|
187
|
-
(list, activeTab, duration, isVertical, tabPlacement) => {
|
|
188
|
-
const start = isVertical ? list.scrollTop : list.scrollLeft;
|
|
189
|
-
let change = 0;
|
|
190
|
-
let currentTime = 0;
|
|
191
|
-
const increment = 20;
|
|
192
|
-
const fadeBuffer = isVertical
|
|
193
|
-
? list.offsetHeight * 0.05
|
|
194
|
-
: list.offsetWidth * 0.05;
|
|
195
|
-
if (tabPlacement < 0) {
|
|
196
|
-
// if tab is before visible section
|
|
197
|
-
change = isVertical
|
|
198
|
-
? activeTab.offsetTop - list.scrollTop
|
|
199
|
-
: activeTab.offsetLeft - list.scrollLeft;
|
|
200
|
-
change -= fadeBuffer; // give some space so the active tab isn't covered by the fade
|
|
201
|
-
} else {
|
|
202
|
-
// tab is after visible section
|
|
203
|
-
change = isVertical
|
|
204
|
-
? activeTab.offsetTop -
|
|
205
|
-
(list.scrollTop + list.offsetHeight) +
|
|
206
|
-
activeTab.offsetHeight
|
|
207
|
-
: activeTab.offsetLeft -
|
|
208
|
-
(list.scrollLeft + list.offsetWidth) +
|
|
209
|
-
activeTab.offsetWidth;
|
|
210
|
-
change += fadeBuffer; // give some space so the active tab isn't covered by the fade
|
|
211
|
-
}
|
|
212
|
-
const animateScroll = () => {
|
|
213
|
-
currentTime += increment;
|
|
214
|
-
const val = easeInOutQuad(currentTime, start, change, duration);
|
|
215
|
-
if (isVertical) {
|
|
216
|
-
list.scrollTop = val;
|
|
217
|
-
} else {
|
|
218
|
-
list.scrollLeft = val;
|
|
219
|
-
}
|
|
220
|
-
if (currentTime < duration) {
|
|
221
|
-
setTimeout(animateScroll, increment);
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
animateScroll();
|
|
225
|
-
},
|
|
226
|
-
[],
|
|
227
|
-
);
|
|
228
|
-
// scroll to active tab if it is not visible with overflow
|
|
229
|
-
useIsomorphicLayoutEffect(() => {
|
|
230
|
-
setTimeout(() => {
|
|
231
|
-
const ownerDoc = tablistRef.current;
|
|
232
|
-
if (
|
|
233
|
-
ownerDoc !== null &&
|
|
234
|
-
overflowOptions?.useOverflow &&
|
|
235
|
-
currentActiveIndex !== undefined
|
|
236
|
-
) {
|
|
237
|
-
const activeTab = ownerDoc.querySelectorAll(`.${styles['iui-tab']}`)[
|
|
238
|
-
currentActiveIndex
|
|
239
|
-
];
|
|
240
|
-
const isVertical = orientation === 'vertical';
|
|
241
|
-
const tabPlacement = isTabHidden(activeTab, isVertical);
|
|
242
|
-
if (tabPlacement) {
|
|
243
|
-
scrollToTab(ownerDoc, activeTab, 100, isVertical, tabPlacement);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}, 50);
|
|
247
159
|
}, [
|
|
248
|
-
|
|
249
|
-
currentActiveIndex,
|
|
250
|
-
focusedIndex,
|
|
160
|
+
type,
|
|
251
161
|
orientation,
|
|
252
|
-
|
|
162
|
+
isActive,
|
|
163
|
+
tabsWidth, // to fix visual artifact on initial render
|
|
253
164
|
]);
|
|
254
|
-
const [scrollingPlacement, setScrollingPlacement] = React.useState(undefined);
|
|
255
|
-
const determineScrollingPlacement = React.useCallback(() => {
|
|
256
|
-
const ownerDoc = tablistRef.current;
|
|
257
|
-
if (ownerDoc === null) {
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const isVertical = orientation === 'vertical';
|
|
261
|
-
const visibleStart = isVertical ? ownerDoc.scrollTop : ownerDoc.scrollLeft;
|
|
262
|
-
const visibleEnd = isVertical
|
|
263
|
-
? ownerDoc.scrollTop + ownerDoc.offsetHeight
|
|
264
|
-
: ownerDoc.scrollLeft + ownerDoc.offsetWidth;
|
|
265
|
-
const totalTabsSpace = isVertical
|
|
266
|
-
? ownerDoc.scrollHeight
|
|
267
|
-
: ownerDoc.scrollWidth;
|
|
268
|
-
if (
|
|
269
|
-
Math.abs(visibleStart - 0) < 1 &&
|
|
270
|
-
Math.abs(visibleEnd - totalTabsSpace) < 1
|
|
271
|
-
) {
|
|
272
|
-
setScrollingPlacement(undefined);
|
|
273
|
-
} else if (Math.abs(visibleStart - 0) < 1) {
|
|
274
|
-
setScrollingPlacement('start');
|
|
275
|
-
} else if (Math.abs(visibleEnd - totalTabsSpace) < 1) {
|
|
276
|
-
setScrollingPlacement('end');
|
|
277
|
-
} else {
|
|
278
|
-
setScrollingPlacement('center');
|
|
279
|
-
}
|
|
280
|
-
}, [orientation, setScrollingPlacement]);
|
|
281
|
-
// apply correct mask when tabs list is resized
|
|
282
|
-
const [resizeRef] = useResizeObserver(determineScrollingPlacement);
|
|
283
|
-
resizeRef(tablistRef?.current);
|
|
284
|
-
// check if overflow tabs are scrolled to far edges
|
|
285
|
-
React.useEffect(() => {
|
|
286
|
-
const ownerDoc = tablistRef.current;
|
|
287
|
-
if (ownerDoc === null) {
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
if (!overflowOptions?.useOverflow) {
|
|
291
|
-
ownerDoc.removeEventListener('scroll', determineScrollingPlacement);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
ownerDoc.addEventListener('scroll', determineScrollingPlacement);
|
|
295
|
-
}, [overflowOptions?.useOverflow, determineScrollingPlacement]);
|
|
296
|
-
const onTabClick = React.useCallback(
|
|
297
|
-
(index) => {
|
|
298
|
-
if (onTabSelected) {
|
|
299
|
-
onTabSelected(index);
|
|
300
|
-
}
|
|
301
|
-
setCurrentActiveIndex(index);
|
|
302
|
-
},
|
|
303
|
-
[onTabSelected],
|
|
304
|
-
);
|
|
305
165
|
const onKeyDown = (event) => {
|
|
306
|
-
// alt + arrow keys are used by browser / assistive technologies
|
|
307
166
|
if (event.altKey) {
|
|
308
167
|
return;
|
|
309
168
|
}
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const focusTab = (delta = +1) => {
|
|
317
|
-
do {
|
|
318
|
-
newIndex = (newIndex + delta + labels.length) % labels.length;
|
|
319
|
-
} while (isTabDisabled(newIndex) && newIndex !== focusedIndex);
|
|
320
|
-
setFocusedIndex(newIndex);
|
|
321
|
-
focusActivationMode === 'auto' && onTabClick(newIndex);
|
|
322
|
-
};
|
|
169
|
+
const allTabs = Array.from(
|
|
170
|
+
event.currentTarget.parentElement?.children ?? [],
|
|
171
|
+
);
|
|
172
|
+
const nextTab = tabRef.current?.nextElementSibling ?? allTabs.at(0);
|
|
173
|
+
const previousTab =
|
|
174
|
+
tabRef.current?.previousElementSibling ?? allTabs.at(-1);
|
|
323
175
|
switch (event.key) {
|
|
324
176
|
case 'ArrowDown': {
|
|
325
177
|
if (orientation === 'vertical') {
|
|
326
|
-
|
|
178
|
+
nextTab?.focus();
|
|
327
179
|
event.preventDefault();
|
|
328
180
|
}
|
|
329
181
|
break;
|
|
330
182
|
}
|
|
331
183
|
case 'ArrowRight': {
|
|
332
184
|
if (orientation === 'horizontal') {
|
|
333
|
-
|
|
185
|
+
nextTab?.focus();
|
|
334
186
|
event.preventDefault();
|
|
335
187
|
}
|
|
336
188
|
break;
|
|
337
189
|
}
|
|
338
190
|
case 'ArrowUp': {
|
|
339
191
|
if (orientation === 'vertical') {
|
|
340
|
-
|
|
192
|
+
previousTab?.focus();
|
|
341
193
|
event.preventDefault();
|
|
342
194
|
}
|
|
343
195
|
break;
|
|
344
196
|
}
|
|
345
197
|
case 'ArrowLeft': {
|
|
346
198
|
if (orientation === 'horizontal') {
|
|
347
|
-
|
|
199
|
+
previousTab?.focus();
|
|
348
200
|
event.preventDefault();
|
|
349
201
|
}
|
|
350
202
|
break;
|
|
351
203
|
}
|
|
352
|
-
case 'Enter':
|
|
353
|
-
case ' ':
|
|
354
|
-
case 'Spacebar': {
|
|
355
|
-
event.preventDefault();
|
|
356
|
-
if (focusActivationMode === 'manual' && focusedIndex !== undefined) {
|
|
357
|
-
onTabClick(focusedIndex);
|
|
358
|
-
}
|
|
359
|
-
break;
|
|
360
|
-
}
|
|
361
204
|
default:
|
|
362
205
|
break;
|
|
363
206
|
}
|
|
364
207
|
};
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
!React.isValidElement(label)
|
|
375
|
-
? React.createElement(Tab, {
|
|
376
|
-
label: label,
|
|
377
|
-
className: cx({
|
|
378
|
-
'iui-active': index === currentActiveIndex,
|
|
379
|
-
}),
|
|
380
|
-
tabIndex: index === currentActiveIndex ? 0 : -1,
|
|
381
|
-
onClick: onClick,
|
|
382
|
-
'aria-selected': index === currentActiveIndex,
|
|
383
|
-
})
|
|
384
|
-
: React.cloneElement(label, {
|
|
385
|
-
active: index === currentActiveIndex,
|
|
386
|
-
'aria-selected': index === currentActiveIndex,
|
|
387
|
-
tabIndex: index === currentActiveIndex ? 0 : -1,
|
|
388
|
-
onClick: (args) => {
|
|
389
|
-
onClick();
|
|
390
|
-
label.props.onClick?.(args);
|
|
391
|
-
},
|
|
392
|
-
}),
|
|
393
|
-
);
|
|
208
|
+
// use first tab as active if no `value` passed.
|
|
209
|
+
const setInitialActiveRef = React.useCallback(
|
|
210
|
+
(element) => {
|
|
211
|
+
if (activeValue !== undefined) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (element?.matches(':first-of-type')) {
|
|
215
|
+
setActiveValue(value);
|
|
216
|
+
}
|
|
394
217
|
},
|
|
395
|
-
[
|
|
218
|
+
[activeValue, setActiveValue, value],
|
|
396
219
|
);
|
|
220
|
+
return React.createElement(
|
|
221
|
+
ButtonBase,
|
|
222
|
+
{
|
|
223
|
+
className: cx('iui-tab', className),
|
|
224
|
+
role: 'tab',
|
|
225
|
+
tabIndex: isActive ? 0 : -1,
|
|
226
|
+
'aria-selected': isActive,
|
|
227
|
+
'aria-controls': `${idPrefix}-panel-${value}`,
|
|
228
|
+
ref: useMergedRefs(tabRef, forwardedRef, setInitialActiveRef),
|
|
229
|
+
...rest,
|
|
230
|
+
id: `${idPrefix}-tab-${value}`,
|
|
231
|
+
onClick: mergeEventHandlers(props.onClick, () => setActiveValue(value)),
|
|
232
|
+
onKeyDown: mergeEventHandlers(props.onKeyDown, onKeyDown),
|
|
233
|
+
onFocus: mergeEventHandlers(props.onFocus, () => {
|
|
234
|
+
if (focusActivationMode === 'auto' && !props.disabled) {
|
|
235
|
+
setActiveValue(value);
|
|
236
|
+
}
|
|
237
|
+
}),
|
|
238
|
+
},
|
|
239
|
+
label ? React.createElement(Tabs.TabLabel, null, label) : children,
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
Tab.displayName = 'Tabs.Tab';
|
|
243
|
+
// ----------------------------------------------------------------------------
|
|
244
|
+
// Tabs.TabIcon component
|
|
245
|
+
const TabIcon = React.forwardRef((props, ref) => {
|
|
246
|
+
return React.createElement(Icon, {
|
|
247
|
+
...props,
|
|
248
|
+
className: cx('iui-tab-icon', props?.className),
|
|
249
|
+
ref: ref,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
TabIcon.displayName = 'Tabs.TabIcon';
|
|
253
|
+
// ----------------------------------------------------------------------------
|
|
254
|
+
// Tabs.TabLabel component
|
|
255
|
+
const TabLabel = polymorphic.span('iui-tab-label');
|
|
256
|
+
TabLabel.displayName = 'Tabs.TabLabel';
|
|
257
|
+
// ----------------------------------------------------------------------------
|
|
258
|
+
// Tabs.TabDescription component
|
|
259
|
+
const TabDescription = React.forwardRef((props, ref) => {
|
|
260
|
+
const { className, children, ...rest } = props;
|
|
261
|
+
const { hasSublabel, setHasSublabel } = useSafeContext(TabsContext);
|
|
262
|
+
useIsomorphicLayoutEffect(() => {
|
|
263
|
+
if (!hasSublabel) {
|
|
264
|
+
setHasSublabel(true);
|
|
265
|
+
}
|
|
266
|
+
}, [hasSublabel, setHasSublabel]);
|
|
397
267
|
return React.createElement(
|
|
398
268
|
Box,
|
|
399
269
|
{
|
|
400
|
-
|
|
401
|
-
|
|
270
|
+
as: 'span',
|
|
271
|
+
className: cx('iui-tab-description', className),
|
|
272
|
+
ref: ref,
|
|
273
|
+
...rest,
|
|
274
|
+
},
|
|
275
|
+
children,
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
TabDescription.displayName = 'Tabs.TabDescription';
|
|
279
|
+
const TabsActions = React.forwardRef((props, ref) => {
|
|
280
|
+
const { wrapperProps, className, children, ...rest } = props;
|
|
281
|
+
return React.createElement(
|
|
282
|
+
Box,
|
|
283
|
+
{
|
|
284
|
+
...wrapperProps,
|
|
285
|
+
className: cx('iui-tabs-actions-wrapper', wrapperProps?.className),
|
|
402
286
|
},
|
|
403
287
|
React.createElement(
|
|
404
288
|
Box,
|
|
405
|
-
{
|
|
406
|
-
|
|
407
|
-
className: cx(
|
|
408
|
-
'iui-tabs',
|
|
409
|
-
`iui-${type}`,
|
|
410
|
-
{
|
|
411
|
-
'iui-green': color === 'green',
|
|
412
|
-
'iui-animated': type !== 'default' && isClient,
|
|
413
|
-
'iui-not-animated': type !== 'default' && !isClient,
|
|
414
|
-
'iui-large': hasSublabel,
|
|
415
|
-
},
|
|
416
|
-
tabsClassName,
|
|
417
|
-
),
|
|
418
|
-
'data-iui-overflow': overflowOptions?.useOverflow,
|
|
419
|
-
'data-iui-scroll-placement': scrollingPlacement,
|
|
420
|
-
role: 'tablist',
|
|
421
|
-
ref: refs,
|
|
422
|
-
onKeyDown: onKeyDown,
|
|
423
|
-
...rest,
|
|
424
|
-
},
|
|
425
|
-
labels.map((label, index) => createTab(label, index)),
|
|
289
|
+
{ className: cx('iui-tabs-actions', className), ref: ref, ...rest },
|
|
290
|
+
children,
|
|
426
291
|
),
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
TabsActions.displayName = 'Tabs.Actions';
|
|
295
|
+
const TabsPanel = React.forwardRef((props, ref) => {
|
|
296
|
+
const { value, className, children, ...rest } = props;
|
|
297
|
+
const { activeValue, idPrefix } = useSafeContext(TabsContext);
|
|
298
|
+
return React.createElement(
|
|
299
|
+
Box,
|
|
300
|
+
{
|
|
301
|
+
className: cx('iui-tabs-content', className),
|
|
302
|
+
'aria-labelledby': `${idPrefix}-tab-${value}`,
|
|
303
|
+
role: 'tabpanel',
|
|
304
|
+
hidden: activeValue !== value ? true : undefined,
|
|
305
|
+
ref: ref,
|
|
306
|
+
...rest,
|
|
307
|
+
id: `${idPrefix}-panel-${value}`,
|
|
308
|
+
},
|
|
309
|
+
children,
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
TabsPanel.displayName = 'Tabs.Panel';
|
|
313
|
+
const LegacyTabsComponent = React.forwardRef((props, forwardedRef) => {
|
|
314
|
+
let actions;
|
|
315
|
+
if (props.type !== 'pill' && props.actions) {
|
|
316
|
+
actions = props.actions;
|
|
317
|
+
props = { ...props };
|
|
318
|
+
delete props.actions;
|
|
319
|
+
}
|
|
320
|
+
const {
|
|
321
|
+
labels,
|
|
322
|
+
onTabSelected,
|
|
323
|
+
focusActivationMode,
|
|
324
|
+
color,
|
|
325
|
+
activeIndex: activeIndexProp,
|
|
326
|
+
tabsClassName,
|
|
327
|
+
contentClassName,
|
|
328
|
+
wrapperClassName,
|
|
329
|
+
children,
|
|
330
|
+
...rest
|
|
331
|
+
} = props;
|
|
332
|
+
const [activeIndex, setActiveIndex] = useControlledState(
|
|
333
|
+
0,
|
|
334
|
+
activeIndexProp,
|
|
335
|
+
onTabSelected,
|
|
336
|
+
);
|
|
337
|
+
return React.createElement(
|
|
338
|
+
TabsWrapper,
|
|
339
|
+
{
|
|
340
|
+
className: wrapperClassName,
|
|
341
|
+
focusActivationMode: focusActivationMode,
|
|
342
|
+
color: color,
|
|
343
|
+
value: `${activeIndex}`,
|
|
344
|
+
onValueChange: (value) => setActiveIndex(Number(value)),
|
|
345
|
+
...rest,
|
|
346
|
+
},
|
|
347
|
+
React.createElement(
|
|
348
|
+
TabList,
|
|
349
|
+
{ className: tabsClassName, ref: forwardedRef },
|
|
350
|
+
labels.map((label, index) => {
|
|
351
|
+
const tabValue = `${index}`;
|
|
352
|
+
return React.isValidElement(label)
|
|
353
|
+
? React.cloneElement(label, {
|
|
354
|
+
value: tabValue,
|
|
355
|
+
})
|
|
356
|
+
: React.createElement(LegacyTab, {
|
|
357
|
+
key: index,
|
|
358
|
+
value: tabValue,
|
|
359
|
+
label: label,
|
|
360
|
+
});
|
|
361
|
+
}),
|
|
362
|
+
),
|
|
363
|
+
actions && React.createElement(TabsActions, null, actions),
|
|
433
364
|
children &&
|
|
434
365
|
React.createElement(
|
|
435
|
-
|
|
436
|
-
{
|
|
437
|
-
className: cx('iui-tabs-content', contentClassName),
|
|
438
|
-
role: 'tabpanel',
|
|
439
|
-
},
|
|
366
|
+
TabsPanel,
|
|
367
|
+
{ value: `${activeIndex}`, className: contentClassName },
|
|
440
368
|
children,
|
|
441
369
|
),
|
|
442
370
|
);
|
|
443
|
-
};
|
|
371
|
+
});
|
|
372
|
+
LegacyTabsComponent.displayName = 'Tabs';
|
|
373
|
+
/**
|
|
374
|
+
* Legacy Tab component.
|
|
375
|
+
* For full functionality use composition API.
|
|
376
|
+
*
|
|
377
|
+
* Individual tab component to be used in the `labels` prop of `Tabs`.
|
|
378
|
+
* @example
|
|
379
|
+
* const tabs = [
|
|
380
|
+
* <Tab label='Label 1' sublabel='Description 1' />,
|
|
381
|
+
* <Tab label='Label 2' startIcon={<SvgPlaceholder />} />,
|
|
382
|
+
* ];
|
|
383
|
+
*/
|
|
384
|
+
const LegacyTab = React.forwardRef((props, forwardedRef) => {
|
|
385
|
+
const { label, sublabel, startIcon, children, value, ...rest } = props;
|
|
386
|
+
return React.createElement(
|
|
387
|
+
React.Fragment,
|
|
388
|
+
null,
|
|
389
|
+
React.createElement(
|
|
390
|
+
Tab,
|
|
391
|
+
{ ...rest, value: value, ref: forwardedRef },
|
|
392
|
+
startIcon && React.createElement(TabIcon, null, startIcon),
|
|
393
|
+
React.createElement(TabLabel, null, label),
|
|
394
|
+
sublabel && React.createElement(TabDescription, null, sublabel),
|
|
395
|
+
children,
|
|
396
|
+
),
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
// ----------------------------------------------------------------------------
|
|
400
|
+
// exports
|
|
401
|
+
export { LegacyTab as Tab };
|
|
402
|
+
/**
|
|
403
|
+
* Tabs organize and allow navigation between groups of content that are related and at the same level of hierarchy.
|
|
404
|
+
* `Tabs.Tab` and `Tabs.Panel` can be associated with each other by passing them the same `value`.
|
|
405
|
+
* @example
|
|
406
|
+
* <Tabs.Wrapper>
|
|
407
|
+
* <Tabs.TabList>
|
|
408
|
+
* <Tabs.Tab value='tab1' label='Label 1' />
|
|
409
|
+
* <Tabs.Tab value='tab2' label='Label 2' />
|
|
410
|
+
* <Tabs.Tab value='tab3' label='Label 3' />
|
|
411
|
+
* </Tabs.TabList>
|
|
412
|
+
* <Tabs.ActionsWrapper>
|
|
413
|
+
* <Tabs.Actions>
|
|
414
|
+
* <Button>Sample Button</Button>
|
|
415
|
+
* </Tabs.Actions>
|
|
416
|
+
* </Tabs.ActionsWrapper>
|
|
417
|
+
* <Tabs.Panel value='tab1'>Content 1</Tabs.Panel>
|
|
418
|
+
* <Tabs.Panel value='tab2'>Content 2</Tabs.Panel>
|
|
419
|
+
* <Tabs.Panel value='tab3'>Content 3</Tabs.Panel>
|
|
420
|
+
* </Tabs.Wrapper>
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* <Tabs orientation='vertical'/>
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* <Tabs.Wrapper focusActivationMode='manual'>
|
|
427
|
+
* <Tabs.Tab value='sample'>
|
|
428
|
+
* <Tabs.TabIcon>
|
|
429
|
+
* <SvgPlaceholder />
|
|
430
|
+
* </Tabs.TabIcon>
|
|
431
|
+
* <Tabs.TabLabel>Sample Label</Tabs.TabLabel>
|
|
432
|
+
* <Tabs.TabDescription>Sample Description</Tabs.TabDescription>
|
|
433
|
+
* </Tabs.Tab>
|
|
434
|
+
* </Tabs.Wrapper>
|
|
435
|
+
*/
|
|
436
|
+
export const Tabs = Object.assign(LegacyTabsComponent, {
|
|
437
|
+
/**
|
|
438
|
+
* A wrapper component for Tabs
|
|
439
|
+
*/
|
|
440
|
+
Wrapper: TabsWrapper,
|
|
441
|
+
/**
|
|
442
|
+
* Tablist subcomponent which contains all of the tab subcomponents.
|
|
443
|
+
* @example
|
|
444
|
+
* <Tabs.TabList>
|
|
445
|
+
* <Tabs.Tab value='tab1' label='Label 1' />
|
|
446
|
+
* <Tabs.Tab value='tab2' label='Label 2' />
|
|
447
|
+
* <Tabs.Tab value='tab3' label='Label 3' />
|
|
448
|
+
* </Tabs.TabList>
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* <Tabs.TabList>
|
|
452
|
+
* <Tabs.Tab value='tab1' label='Green Tab' />
|
|
453
|
+
* </Tabs.TabList>
|
|
454
|
+
*
|
|
455
|
+
* @example
|
|
456
|
+
* <Tabs.TabList focusActivationMode='manual'>
|
|
457
|
+
* <Tabs.Tab value='tab1' label='Manual Focus Tab' />
|
|
458
|
+
* </Tabs.TabList>
|
|
459
|
+
*/
|
|
460
|
+
TabList: TabList,
|
|
461
|
+
/**
|
|
462
|
+
* Tab subcomponent which is used for each of the tabs.
|
|
463
|
+
* @example
|
|
464
|
+
* <Tabs.Tab value='tab1' label='Label 1' />
|
|
465
|
+
*
|
|
466
|
+
* @example
|
|
467
|
+
* <Tabs.Tab value='sample'>
|
|
468
|
+
* <Tabs.TabIcon>
|
|
469
|
+
* <SvgPlaceholder />
|
|
470
|
+
* </Tabs.TabIcon>
|
|
471
|
+
* <Tabs.TabLabel>Sample Label</Tabs.TabLabel>
|
|
472
|
+
* <Tabs.TabDescription>Sample Description</Tabs.TabDescription>
|
|
473
|
+
* </Tabs.Tab>
|
|
474
|
+
*
|
|
475
|
+
*/
|
|
476
|
+
Tab: Tab,
|
|
477
|
+
/**
|
|
478
|
+
* Tab icon subcomponent which places an icon on the left side of the tab.
|
|
479
|
+
*/
|
|
480
|
+
TabIcon: TabIcon,
|
|
481
|
+
/**
|
|
482
|
+
* Tab label subcomponent which holds the tab's label.
|
|
483
|
+
*/
|
|
484
|
+
TabLabel: TabLabel,
|
|
485
|
+
/**
|
|
486
|
+
* Tab description subcomponent which places a description under the tab label.
|
|
487
|
+
*/
|
|
488
|
+
TabDescription: TabDescription,
|
|
489
|
+
/**
|
|
490
|
+
* Tab actions subcomponent which contains action buttons that are placed at the end of the tabs.
|
|
491
|
+
*/
|
|
492
|
+
Actions: TabsActions,
|
|
493
|
+
/**
|
|
494
|
+
* Tab panel subcomponent which contains the tab's content.
|
|
495
|
+
* @example
|
|
496
|
+
* <Tabs.Panel value='tab1'>
|
|
497
|
+
* Sample Panel
|
|
498
|
+
* </Tabs.Panel>
|
|
499
|
+
*/
|
|
500
|
+
Panel: TabsPanel,
|
|
501
|
+
});
|
|
502
|
+
const TabsContext = React.createContext(undefined);
|
|
503
|
+
const TabListContext = React.createContext(undefined);
|
|
444
504
|
export default Tabs;
|