@react-spectrum/s2 3.0.0-nightly-e3ed3c7f6-250130 → 3.0.0-nightly-016590a4a-250131

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/src/Tabs.tsx CHANGED
@@ -11,28 +11,32 @@
11
11
  */
12
12
 
13
13
  import {
14
- TabListProps as AriaTabListProps,
15
- TabPanel as AriaTabPanel,
16
- TabPanelProps as AriaTabPanelProps,
17
- TabProps as AriaTabProps,
18
- TabsProps as AriaTabsProps,
19
- ContextValue,
20
- Provider,
21
- Tab as RACTab,
22
- TabList as RACTabList,
23
- Tabs as RACTabs,
24
- TabListStateContext,
25
- useSlottedContext
26
- } from 'react-aria-components';
14
+ TabListProps as AriaTabListProps,
15
+ TabPanel as AriaTabPanel,
16
+ TabPanelProps as AriaTabPanelProps,
17
+ TabProps as AriaTabProps,
18
+ TabsProps as AriaTabsProps,
19
+ ContextValue,
20
+ Group,
21
+ Provider,
22
+ Tab as RACTab,
23
+ TabList as RACTabList,
24
+ Tabs as RACTabs,
25
+ TabListStateContext
26
+ } from 'react-aria-components';
27
27
  import {centerBaseline} from './CenterBaseline';
28
- import {Collection, DOMRef, DOMRefValue, Key, Node, Orientation} from '@react-types/shared';
29
- import {createContext, forwardRef, ReactNode, useCallback, useContext, useEffect, useRef, useState} from 'react';
30
- import {focusRing, style} from '../style' with {type: 'macro'};
28
+ import {Collection, DOMRef, DOMRefValue, Key, Node, Orientation, RefObject} from '@react-types/shared';
29
+ import {CollectionBuilder} from '@react-aria/collections';
30
+ import {createContext, forwardRef, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
31
+ import {focusRing, size, style} from '../style' with {type: 'macro'};
31
32
  import {getAllowedOverrides, StyleProps, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'};
32
33
  import {IconContext} from './Icon';
34
+ import {Picker, PickerItem} from './TabsPicker';
33
35
  import {Text, TextContext} from './Content';
36
+ import {useControlledState} from '@react-stately/utils';
34
37
  import {useDOMRef} from '@react-spectrum/utils';
35
- import {useLayoutEffect} from '@react-aria/utils';
38
+ import {useEffectEvent, useId, useLabels, useLayoutEffect, useResizeObserver} from '@react-aria/utils';
39
+ import {useHasTabbableChild} from '@react-aria/focus';
36
40
  import {useLocale} from '@react-aria/i18n';
37
41
  import {useSpectrumContextProps} from './useSpectrumContextProps';
38
42
 
@@ -45,18 +49,21 @@ export interface TabsProps extends Omit<AriaTabsProps, 'className' | 'style' | '
45
49
  * The amount of space between the tabs.
46
50
  * @default 'regular'
47
51
  */
48
- density?: 'compact' | 'regular'
52
+ density?: 'compact' | 'regular',
53
+ /**
54
+ * Defines if the text within the tabs should be hidden and only the icon should be shown.
55
+ * The text is always visible when the item is collapsed into a picker.
56
+ * @default 'show'
57
+ */
58
+ labelBehavior?: 'show' | 'hide'
49
59
  }
50
60
 
51
61
  export interface TabProps extends Omit<AriaTabProps, 'children' | 'style' | 'className'>, StyleProps {
52
62
  /** The content to display in the tab. */
53
- children?: ReactNode
63
+ children: ReactNode
54
64
  }
55
65
 
56
- export interface TabListProps<T> extends Omit<AriaTabListProps<T>, 'children' | 'style' | 'className'>, StyleProps {
57
- /** The content to display in the tablist. */
58
- children?: ReactNode
59
- }
66
+ export interface TabListProps<T> extends Omit<AriaTabListProps<T>, 'style' | 'className' | 'aria-label' | 'aria-labelledby'>, StyleProps {}
60
67
 
61
68
  export interface TabPanelProps extends Omit<AriaTabPanelProps, 'children' | 'style' | 'className'>, UnsafeStyles {
62
69
  /** Spectrum-defined styles, returned by the `style()` macro. */
@@ -66,82 +73,72 @@ export interface TabPanelProps extends Omit<AriaTabPanelProps, 'children' | 'sty
66
73
  }
67
74
 
68
75
  export const TabsContext = createContext<ContextValue<TabsProps, DOMRefValue<HTMLDivElement>>>(null);
76
+ const InternalTabsContext = createContext<TabsProps>({});
77
+ const CollapseContext = createContext({
78
+ showTabs: true,
79
+ menuId: '',
80
+ valueId: ''
81
+ });
69
82
 
70
- const tabPanel = style({
71
- marginTop: 4,
72
- color: 'gray-800',
73
- flexGrow: 1,
74
- flexBasis: '[0%]',
75
- minHeight: 0,
76
- minWidth: 0
77
- }, getAllowedOverrides({height: true}));
78
-
79
- export function TabPanel(props: TabPanelProps) {
80
- return (
81
- <AriaTabPanel
82
- {...props}
83
- style={props.UNSAFE_style}
84
- className={(props.UNSAFE_className || '') + tabPanel(null, props.styles)} />
85
- );
86
- }
87
-
88
- const tab = style({
89
- ...focusRing(),
90
- display: 'flex',
91
- color: {
92
- default: 'neutral-subdued',
93
- isSelected: 'neutral',
94
- isHovered: 'neutral-subdued',
95
- isDisabled: 'disabled',
96
- forcedColors: {
97
- isSelected: 'Highlight',
98
- isDisabled: 'GrayText'
99
- }
100
- },
101
- borderRadius: 'sm',
102
- gap: 'text-to-visual',
103
- height: {
104
- density: {
105
- compact: 32,
106
- regular: 48
107
- }
108
- },
109
- alignItems: 'center',
83
+ const tabs = style({
110
84
  position: 'relative',
111
- cursor: 'default',
112
- flexShrink: 0,
113
- transition: 'default'
114
- }, getAllowedOverrides());
115
-
116
- const icon = style({
85
+ display: 'flex',
117
86
  flexShrink: 0,
118
- '--iconPrimary': {
119
- type: 'fill',
120
- value: 'currentColor'
87
+ font: 'ui',
88
+ flexDirection: {
89
+ orientation: {
90
+ horizontal: 'column'
91
+ }
121
92
  }
122
- });
93
+ }, getAllowedOverrides({height: true}));
123
94
 
124
- export function Tab(props: TabProps) {
125
- let {density} = useSlottedContext(TabsContext) ?? {};
95
+ /**
96
+ * Tabs organize content into multiple sections and allow users to navigate between them. The content under the set of tabs should be related and form a coherent unit.
97
+ */
98
+ export const Tabs = forwardRef(function Tabs(props: TabsProps, ref: DOMRef<HTMLDivElement>) {
99
+ [props, ref] = useSpectrumContextProps(props, ref, TabsContext);
100
+ let {
101
+ density = 'regular',
102
+ isDisabled,
103
+ disabledKeys,
104
+ orientation = 'horizontal',
105
+ labelBehavior = 'show'
106
+ } = props;
107
+ let domRef = useDOMRef(ref);
108
+ let [value, setValue] = useControlledState(props.selectedKey, props.defaultSelectedKey ?? null!, props.onSelectionChange);
109
+
110
+ if (!props['aria-label'] && !props['aria-labelledby']) {
111
+ throw new Error('An aria-label or aria-labelledby prop is required on Tabs for accessibility.');
112
+ }
126
113
 
127
114
  return (
128
- <RACTab
129
- {...props}
130
- style={props.UNSAFE_style}
131
- className={renderProps => (props.UNSAFE_className || '') + tab({...renderProps, density}, props.styles)}>
132
- <Provider
133
- values={[
134
- [TextContext, {styles: style({order: 1})}],
135
- [IconContext, {
136
- render: centerBaseline({slot: 'icon', styles: style({order: 0})}),
137
- styles: icon
138
- }]
139
- ]}>
140
- {typeof props.children === 'string' ? <Text>{props.children}</Text> : props.children}
141
- </Provider>
142
- </RACTab>
115
+ <Provider
116
+ values={[
117
+ [InternalTabsContext, {
118
+ density,
119
+ isDisabled,
120
+ orientation,
121
+ disabledKeys,
122
+ selectedKey: value,
123
+ onSelectionChange: setValue,
124
+ labelBehavior,
125
+ 'aria-label': props['aria-label'],
126
+ 'aria-labelledby': props['aria-labelledby']
127
+ }]
128
+ ]}>
129
+ <CollectionBuilder content={props.children}>
130
+ {collection => (
131
+ <CollapsingTabs
132
+ {...props}
133
+ selectedKey={value}
134
+ onSelectionChange={setValue}
135
+ collection={collection}
136
+ containerRef={domRef} />
137
+ )}
138
+ </CollectionBuilder>
139
+ </Provider>
143
140
  );
144
- }
141
+ });
145
142
 
146
143
  const tablist = style({
147
144
  display: 'flex',
@@ -151,6 +148,14 @@ const tablist = style({
151
148
  density: {
152
149
  compact: 24,
153
150
  regular: 32
151
+ },
152
+ labelBehavior: {
153
+ hide: {
154
+ density: {
155
+ compact: 16,
156
+ regular: 24
157
+ }
158
+ }
154
159
  }
155
160
  }
156
161
  }
@@ -175,7 +180,15 @@ const tablist = style({
175
180
  });
176
181
 
177
182
  export function TabList<T extends object>(props: TabListProps<T>) {
178
- let {density, isDisabled, disabledKeys, orientation} = useSlottedContext(TabsContext) ?? {};
183
+ let {showTabs} = useContext(CollapseContext) ?? {};
184
+
185
+ if (showTabs) {
186
+ return <TabListInner {...props} />;
187
+ }
188
+ }
189
+
190
+ function TabListInner<T extends object>(props: TabListProps<T>) {
191
+ let {density, isDisabled, disabledKeys, orientation, labelBehavior, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy} = useContext(InternalTabsContext) ?? {};
179
192
  let state = useContext(TabListStateContext);
180
193
  let [selectedTab, setSelectedTab] = useState<HTMLElement | undefined>(undefined);
181
194
  let tablistRef = useRef<HTMLDivElement>(null);
@@ -198,34 +211,16 @@ export function TabList<T extends object>(props: TabListProps<T>) {
198
211
  <TabLine disabledKeys={disabledKeys} isDisabled={isDisabled} selectedTab={selectedTab} orientation={orientation} density={density} />}
199
212
  <RACTabList
200
213
  {...props}
214
+ aria-label={ariaLabel}
215
+ aria-labelledby={ariaLabelledBy}
201
216
  ref={tablistRef}
202
- className={renderProps => tablist({...renderProps, density})} />
217
+ className={renderProps => tablist({...renderProps, labelBehavior, density})} />
203
218
  {orientation === 'horizontal' &&
204
219
  <TabLine disabledKeys={disabledKeys} isDisabled={isDisabled} selectedTab={selectedTab} orientation={orientation} density={density} />}
205
220
  </div>
206
221
  );
207
222
  }
208
223
 
209
- function isAllTabsDisabled<T>(collection: Collection<Node<T>> | null, disabledKeys: Set<Key>) {
210
- let testKey: Key | null = null;
211
- if (collection && collection.size > 0) {
212
- testKey = collection.getFirstKey();
213
-
214
- let index = 0;
215
- while (testKey && index < collection.size) {
216
- // We have to check if the item in the collection has a key in disabledKeys or has the isDisabled prop set directly on it
217
- if (!disabledKeys.has(testKey) && !collection.getItem(testKey)?.props?.isDisabled) {
218
- return false;
219
- }
220
-
221
- testKey = collection.getKeyAfter(testKey);
222
- index++;
223
- }
224
- return true;
225
- }
226
- return false;
227
- }
228
-
229
224
  interface TabLineProps {
230
225
  disabledKeys: Iterable<Key> | undefined,
231
226
  isDisabled: boolean | undefined,
@@ -279,7 +274,7 @@ function TabLine(props: TabLineProps) {
279
274
  // We want to add disabled styling to the selection indicator only if all the Tabs are disabled
280
275
  let [isDisabled, setIsDisabled] = useState<boolean>(false);
281
276
  useEffect(() => {
282
- let isDisabled = isTabsDisabled || isAllTabsDisabled(state?.collection || null, disabledKeys ? new Set(disabledKeys) : new Set(null));
277
+ let isDisabled = isTabsDisabled || isAllTabsDisabled(state?.collection, disabledKeys ? new Set(disabledKeys) : new Set(null));
283
278
  setIsDisabled(isDisabled);
284
279
  }, [state?.collection, disabledKeys, isTabsDisabled, setIsDisabled]);
285
280
 
@@ -314,50 +309,361 @@ function TabLine(props: TabLineProps) {
314
309
 
315
310
  useLayoutEffect(() => {
316
311
  onResize();
317
- }, [onResize, state?.selectedItem?.key, direction, orientation, density]);
312
+ }, [onResize, state?.selectedItem?.key, density, direction, orientation]);
318
313
 
319
314
  return (
320
315
  <div style={{...style}} className={selectedIndicator({isDisabled, orientation})} />
321
316
  );
322
317
  }
323
318
 
324
- const tabs = style({
319
+ const tab = style({
320
+ ...focusRing(),
325
321
  display: 'flex',
322
+ color: {
323
+ default: 'neutral-subdued',
324
+ isSelected: 'neutral',
325
+ isHovered: 'neutral-subdued',
326
+ isDisabled: 'disabled',
327
+ forcedColors: {
328
+ isSelected: 'Highlight',
329
+ isDisabled: 'GrayText'
330
+ }
331
+ },
332
+ borderRadius: 'sm',
333
+ gap: 'text-to-visual',
334
+ height: {
335
+ density: {
336
+ compact: 32,
337
+ regular: 48
338
+ }
339
+ },
340
+ alignItems: 'center',
341
+ position: 'relative',
342
+ cursor: 'default',
326
343
  flexShrink: 0,
327
- fontFamily: 'sans',
328
- fontWeight: 'normal',
329
- flexDirection: {
330
- orientation: {
331
- horizontal: 'column'
344
+ transition: 'default',
345
+ paddingX: {
346
+ labelBehavior: {
347
+ hide: size(6)
332
348
  }
333
349
  }
334
- }, getAllowedOverrides({height: true}));
350
+ }, getAllowedOverrides());
335
351
 
336
- /**
337
- * Tabs organize content into multiple sections and allow users to navigate between them. The content under the set of tabs should be related and form a coherent unit.
338
- */
339
- export const Tabs = forwardRef(function Tabs(props: TabsProps, ref: DOMRef<HTMLDivElement>) {
340
- [props, ref] = useSpectrumContextProps(props, ref, TabsContext);
341
- let {
342
- density = 'regular',
343
- isDisabled,
344
- disabledKeys,
345
- orientation = 'horizontal'
346
- } = props;
347
- let domRef = useDOMRef(ref);
352
+ const icon = style({
353
+ display: 'block',
354
+ flexShrink: 0,
355
+ '--iconPrimary': {
356
+ type: 'fill',
357
+ value: 'currentColor'
358
+ }
359
+ });
360
+
361
+ export function Tab(props: TabProps) {
362
+ let {density, labelBehavior} = useContext(InternalTabsContext) ?? {};
348
363
 
364
+ let contentId = useId();
365
+ let ariaLabelledBy = props['aria-labelledby'] || '';
349
366
  return (
350
- <RACTabs
367
+ <RACTab
351
368
  {...props}
352
- ref={domRef}
369
+ // @ts-ignore
370
+ originalProps={props}
371
+ aria-labelledby={`${labelBehavior === 'hide' ? contentId : ''} ${ariaLabelledBy}`}
353
372
  style={props.UNSAFE_style}
354
- className={renderProps => (props.UNSAFE_className || '') + tabs({...renderProps}, props.styles)}>
355
- <Provider
356
- values={[
357
- [TabsContext, {density, isDisabled, disabledKeys, orientation}]
358
- ]}>
373
+ className={renderProps => (props.UNSAFE_className || '') + tab({...renderProps, density, labelBehavior}, props.styles)}>
374
+ {({
375
+ // @ts-ignore
376
+ isMenu
377
+ }) => {
378
+ if (isMenu) {
379
+ return props.children;
380
+ } else {
381
+ return (
382
+ <Provider
383
+ values={[
384
+ [TextContext, {
385
+ id: contentId,
386
+ styles:
387
+ style({
388
+ order: 1,
389
+ display: {
390
+ labelBehavior: {
391
+ hide: 'none'
392
+ }
393
+ }
394
+ })({labelBehavior})
395
+ }],
396
+ [IconContext, {
397
+ render: centerBaseline({slot: 'icon', styles: style({order: 0})}),
398
+ styles: icon
399
+ }]
400
+ ]}>
401
+ {typeof props.children === 'string' ? <Text>{props.children}</Text> : props.children}
402
+ </Provider>
403
+ );
404
+ }
405
+ }}
406
+ </RACTab>
407
+ );
408
+ }
409
+
410
+ const tabPanel = style({
411
+ ...focusRing(),
412
+ display: 'flex',
413
+ marginTop: 4,
414
+ marginX: -4,
415
+ paddingX: 4,
416
+ color: 'gray-800',
417
+ flexGrow: 1,
418
+ flexShrink: 1,
419
+ flexBasis: '[0%]',
420
+ minHeight: 0,
421
+ minWidth: 0,
422
+ overflow: 'auto'
423
+ }, getAllowedOverrides({height: true}));
424
+
425
+ export function TabPanel(props: TabPanelProps) {
426
+ let {showTabs} = useContext(CollapseContext);
427
+ let {selectedKey} = useContext(InternalTabsContext);
428
+ if (showTabs) {
429
+ return (
430
+ <AriaTabPanel
431
+ {...props}
432
+ style={props.UNSAFE_style}
433
+ className={renderProps => (props.UNSAFE_className ?? '') + tabPanel(renderProps, props.styles)} />
434
+ );
435
+ }
436
+
437
+ if (props.id !== selectedKey) {
438
+ return null;
439
+ }
440
+
441
+ return <CollapsedTabPanel {...props} />;
442
+ }
443
+
444
+ function CollapsedTabPanel(props: TabPanelProps) {
445
+ let {UNSAFE_style, UNSAFE_className = '', ...otherProps} = props;
446
+ let {menuId, valueId} = useContext(CollapseContext);
447
+ let ref = useRef(null);
448
+ let tabIndex = useHasTabbableChild(ref) ? undefined : 0;
449
+
450
+ return (
451
+ <Group
452
+ {...otherProps}
453
+ ref={ref}
454
+ aria-labelledby={menuId + ' ' + valueId}
455
+ tabIndex={tabIndex}
456
+ style={UNSAFE_style}
457
+ className={renderProps => UNSAFE_className + tabPanel(renderProps, props.styles)} />
458
+ );
459
+ }
460
+
461
+ function isAllTabsDisabled<T>(collection: Collection<Node<T>> | undefined, disabledKeys: Set<Key>) {
462
+ let testKey: Key | null = null;
463
+ if (collection && collection.size > 0) {
464
+ testKey = collection.getFirstKey();
465
+
466
+ let index = 0;
467
+ while (testKey && index < collection.size) {
468
+ // We have to check if the item in the collection has a key in disabledKeys or has the isDisabled prop set directly on it
469
+ if (!disabledKeys.has(testKey) && !collection.getItem(testKey)?.props?.isDisabled) {
470
+ return false;
471
+ }
472
+
473
+ testKey = collection.getKeyAfter(testKey);
474
+ index++;
475
+ }
476
+ return true;
477
+ }
478
+ return false;
479
+ }
480
+
481
+ let HiddenTabs = function (props: {
482
+ listRef: RefObject<HTMLDivElement | null>,
483
+ items: Array<Node<any>>,
484
+ size?: string,
485
+ density?: 'compact' | 'regular'
486
+ }) {
487
+ let {listRef, items, size, density} = props;
488
+
489
+ return (
490
+ <div
491
+ // @ts-ignore
492
+ inert="true"
493
+ ref={listRef}
494
+ className={style({
495
+ display: '[inherit]',
496
+ flexDirection: '[inherit]',
497
+ gap: '[inherit]',
498
+ flexWrap: '[inherit]',
499
+ position: 'absolute',
500
+ inset: 0,
501
+ visibility: 'hidden',
502
+ overflow: 'hidden',
503
+ opacity: 0
504
+ })}>
505
+ {items.map((item) => {
506
+ // pull off individual props as an allow list, don't want refs or other props getting through
507
+ return (
508
+ <div
509
+ data-hidden-tab
510
+ style={item.props.UNSAFE_style}
511
+ key={item.key}
512
+ className={item.props.className({size, density})}>
513
+ {item.props.children({size, density})}
514
+ </div>
515
+ );
516
+ })}
517
+ </div>
518
+ );
519
+ };
520
+
521
+ let TabsMenu = (props: {valueId: string, items: Array<Node<any>>, onSelectionChange: TabsProps['onSelectionChange']} & TabsProps) => {
522
+ let {id, items, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, valueId} = props;
523
+ let {density, onSelectionChange, selectedKey, isDisabled, disabledKeys, labelBehavior} = useContext(InternalTabsContext);
524
+ let state = useContext(TabListStateContext);
525
+ let allKeysDisabled = useMemo(() => {
526
+ return isAllTabsDisabled(state?.collection, disabledKeys ? new Set(disabledKeys) : new Set());
527
+ }, [state?.collection, disabledKeys]);
528
+ let labelProps = useLabels({
529
+ id,
530
+ 'aria-label': ariaLabel,
531
+ 'aria-labelledby': ariaLabelledBy
532
+ });
533
+
534
+ return (
535
+ <div
536
+ className={style({
537
+ display: 'flex',
538
+ flexShrink: 0,
539
+ alignItems: 'center',
540
+ height: {
541
+ density: {
542
+ compact: 32,
543
+ regular: 48
544
+ }
545
+ }})({density})}>
546
+ <Picker
547
+ id={id}
548
+ valueId={valueId}
549
+ {...labelProps}
550
+ aria-describedby={props['aria-describedby']}
551
+ aria-details={props['aria-details']}
552
+ isDisabled={isDisabled || allKeysDisabled}
553
+ density={density!}
554
+ labelBehavior={labelBehavior}
555
+ items={items}
556
+ disabledKeys={disabledKeys}
557
+ selectedKey={selectedKey}
558
+ onSelectionChange={onSelectionChange}>
559
+ {(item: Node<any>) => {
560
+ return (
561
+ <PickerItem
562
+ {...item.props.originalProps}
563
+ isDisabled={isDisabled || allKeysDisabled}
564
+ key={item.key}>
565
+ {item.props.children({density, isMenu: true})}
566
+ </PickerItem>
567
+ );
568
+ }}
569
+ </Picker>
570
+ </div>
571
+ );
572
+ };
573
+
574
+ let CollapsingTabs = ({collection, containerRef, ...props}: {collection: Collection<Node<unknown>>, containerRef: any} & TabsProps) => {
575
+ let {density = 'regular', orientation = 'horizontal', labelBehavior = 'show', onSelectionChange} = props;
576
+ let [showItems, _setShowItems] = useState(true);
577
+ showItems = orientation === 'vertical' ? true : showItems;
578
+ let setShowItems = useCallback((value: boolean) => {
579
+ if (orientation === 'vertical') {
580
+ // if orientation is vertical, we always show the items
581
+ _setShowItems(true);
582
+ } else {
583
+ _setShowItems(value);
584
+ }
585
+ }, [orientation]);
586
+
587
+ let {direction} = useLocale();
588
+
589
+ let children = useMemo(() => [...collection], [collection]);
590
+
591
+ let listRef = useRef<HTMLDivElement | null>(null);
592
+ let updateOverflow = useEffectEvent(() => {
593
+ if (orientation === 'vertical' || !listRef.current || !containerRef?.current) {
594
+ return;
595
+ }
596
+ let container = listRef.current;
597
+ let containerRect = container.getBoundingClientRect();
598
+ let tabs = container.querySelectorAll('[data-hidden-tab]');
599
+ let lastTab = tabs[tabs.length - 1];
600
+ let lastTabRect = lastTab.getBoundingClientRect();
601
+ if (direction === 'ltr') {
602
+ setShowItems?.(lastTabRect.right <= containerRect.right);
603
+ } else {
604
+ setShowItems?.(lastTabRect.left >= containerRect.left);
605
+ }
606
+ });
607
+
608
+ useResizeObserver({ref: containerRef, onResize: updateOverflow});
609
+
610
+ useLayoutEffect(() => {
611
+ if (collection.size > 0) {
612
+ queueMicrotask(updateOverflow);
613
+ }
614
+ }, [collection.size, updateOverflow]);
615
+
616
+ let prevOrientation = useRef(orientation);
617
+ useLayoutEffect(() => {
618
+ if (collection.size > 0 && prevOrientation.current !== orientation) {
619
+ updateOverflow();
620
+ }
621
+ prevOrientation.current = orientation;
622
+ }, [collection.size, updateOverflow, orientation]);
623
+
624
+ useEffect(() => {
625
+ // Recalculate visible tags when fonts are loaded.
626
+ document.fonts?.ready.then(() => updateOverflow());
627
+ // eslint-disable-next-line react-hooks/exhaustive-deps
628
+ }, []);
629
+
630
+ let menuId = useId();
631
+ let valueId = useId();
632
+
633
+ let contents: ReactNode;
634
+ if (showItems) {
635
+ contents = (
636
+ <RACTabs
637
+ {...props}
638
+ style={{display: 'contents'}}>
359
639
  {props.children}
360
- </Provider>
361
- </RACTabs>
640
+ </RACTabs>
641
+ );
642
+ } else {
643
+ contents = (
644
+ <>
645
+ <TabsMenu
646
+ id={menuId}
647
+ valueId={valueId}
648
+ items={children}
649
+ onSelectionChange={onSelectionChange}
650
+ aria-label={props['aria-label']}
651
+ aria-describedby={props['aria-labelledby']} />
652
+ <CollapseContext.Provider value={{showTabs: false, menuId, valueId}}>
653
+ {props.children}
654
+ </CollapseContext.Provider>
655
+ </>
656
+ );
657
+ }
658
+
659
+ return (
660
+ <div style={props.UNSAFE_style} className={(props.UNSAFE_className || '') + tabs({orientation}, props.styles)} ref={containerRef}>
661
+ <div className={tablist({orientation, labelBehavior, density})}>
662
+ <HiddenTabs items={children} density={density} listRef={listRef} />
663
+ </div>
664
+ <CollapseContext.Provider value={{showTabs: true, menuId, valueId}}>
665
+ {contents}
666
+ </CollapseContext.Provider>
667
+ </div>
362
668
  );
363
- });
669
+ };