@itwin/itwinui-react 3.0.0-dev.12 → 3.0.0-dev.14

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