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