@react-spectrum/s2 3.0.0-nightly-b0f156972-241202 → 3.0.0-nightly-e94e36431-241203

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,29 +11,35 @@
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
+ CollectionRenderer,
20
+ ContextValue,
21
+ Provider,
22
+ Tab as RACTab,
23
+ TabList as RACTabList,
24
+ Tabs as RACTabs,
25
+ TabListStateContext,
26
+ UNSTABLE_CollectionRendererContext,
27
+ UNSTABLE_DefaultCollectionRenderer
28
+ } from 'react-aria-components';
27
29
  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 {Collection, DOMRef, DOMRefValue, FocusableRef, FocusableRefValue, Key, Node, Orientation, RefObject} from '@react-types/shared';
31
+ import {createContext, forwardRef, Fragment, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
30
32
  import {focusRing, style} from '../style' with {type: 'macro'};
31
33
  import {getAllowedOverrides, StyleProps, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'};
32
34
  import {IconContext} from './Icon';
35
+ // @ts-ignore
36
+ import intlMessages from '../intl/*.json';
37
+ import {Picker, PickerItem} from './TabsPicker';
33
38
  import {Text, TextContext} from './Content';
39
+ import {useControlledState} from '@react-stately/utils';
34
40
  import {useDOMRef} from '@react-spectrum/utils';
35
- import {useLayoutEffect} from '@react-aria/utils';
36
- import {useLocale} from '@react-aria/i18n';
41
+ import {useEffectEvent, useLayoutEffect, useResizeObserver} from '@react-aria/utils';
42
+ import {useLocale, useLocalizedStringFormatter} from '@react-aria/i18n';
37
43
  import {useSpectrumContextProps} from './useSpectrumContextProps';
38
44
 
39
45
  export interface TabsProps extends Omit<AriaTabsProps, 'className' | 'style' | 'children'>, UnsafeStyles {
@@ -45,18 +51,19 @@ export interface TabsProps extends Omit<AriaTabsProps, 'className' | 'style' | '
45
51
  * The amount of space between the tabs.
46
52
  * @default 'regular'
47
53
  */
48
- density?: 'compact' | 'regular'
54
+ density?: 'compact' | 'regular',
55
+ /**
56
+ * If the tabs should only display icons and no text.
57
+ */
58
+ iconOnly?: boolean
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'>, 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,64 @@ 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 & {onFocus:() => void, pickerRef?: FocusableRef<HTMLButtonElement>}>({onFocus: () => {}});
69
77
 
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(),
78
+ const tabs = style({
90
79
  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',
110
- position: 'relative',
111
- cursor: 'default',
112
- flexShrink: 0,
113
- transition: 'default'
114
- }, getAllowedOverrides());
115
-
116
- const icon = style({
117
80
  flexShrink: 0,
118
- '--iconPrimary': {
119
- type: 'fill',
120
- value: 'currentColor'
81
+ font: 'ui',
82
+ flexDirection: {
83
+ orientation: {
84
+ horizontal: 'column'
85
+ }
121
86
  }
122
- });
87
+ }, getAllowedOverrides({height: true}));
123
88
 
124
- export function Tab(props: TabProps) {
125
- let {density} = useSlottedContext(TabsContext) ?? {};
89
+ /**
90
+ * 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.
91
+ */
92
+ export const Tabs = forwardRef(function Tabs(props: TabsProps, ref: DOMRef<HTMLDivElement>) {
93
+ [props, ref] = useSpectrumContextProps(props, ref, TabsContext);
94
+ let {
95
+ density = 'regular',
96
+ isDisabled,
97
+ disabledKeys,
98
+ orientation = 'horizontal',
99
+ iconOnly = false
100
+ } = props;
101
+ let domRef = useDOMRef(ref);
102
+ let [value, setValue] = useControlledState(props.selectedKey, props.defaultSelectedKey ?? null!, props.onSelectionChange);
103
+ let pickerRef = useRef<FocusableRefValue<HTMLButtonElement>>(null);
126
104
 
127
105
  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>
106
+ <Provider
107
+ values={[
108
+ [InternalTabsContext, {
109
+ density,
110
+ isDisabled,
111
+ orientation,
112
+ disabledKeys,
113
+ selectedKey: value,
114
+ onSelectionChange: setValue,
115
+ iconOnly,
116
+ onFocus: () => pickerRef.current?.focus(),
117
+ pickerRef
118
+ }]
119
+ ]}>
120
+ <CollapsingCollection containerRef={domRef}>
121
+ <RACTabs
122
+ {...props}
123
+ ref={domRef}
124
+ selectedKey={value}
125
+ onSelectionChange={setValue}
126
+ style={props.UNSAFE_style}
127
+ className={renderProps => (props.UNSAFE_className || '') + tabs({...renderProps}, props.styles)}>
128
+ {props.children}
129
+ </RACTabs>
130
+ </CollapsingCollection>
131
+ </Provider>
143
132
  );
144
- }
133
+ });
145
134
 
146
135
  const tablist = style({
147
136
  display: 'flex',
@@ -151,6 +140,12 @@ const tablist = style({
151
140
  density: {
152
141
  compact: 24,
153
142
  regular: 32
143
+ },
144
+ isIconOnly: {
145
+ density: {
146
+ compact: 16,
147
+ regular: 24
148
+ }
154
149
  }
155
150
  }
156
151
  }
@@ -175,63 +170,58 @@ const tablist = style({
175
170
  });
176
171
 
177
172
  export function TabList<T extends object>(props: TabListProps<T>) {
178
- let {density, isDisabled, disabledKeys, orientation} = useSlottedContext(TabsContext) ?? {};
173
+ let {density, isDisabled, disabledKeys, orientation, iconOnly, onFocus} = useContext(InternalTabsContext) ?? {};
174
+ let {showItems} = useContext(CollapseContext) ?? {};
179
175
  let state = useContext(TabListStateContext);
180
176
  let [selectedTab, setSelectedTab] = useState<HTMLElement | undefined>(undefined);
181
177
  let tablistRef = useRef<HTMLDivElement>(null);
182
178
 
183
179
  useLayoutEffect(() => {
184
- if (tablistRef?.current) {
180
+ if (tablistRef?.current && showItems) {
185
181
  let tab: HTMLElement | null = tablistRef.current.querySelector('[role=tab][data-selected=true]');
186
182
 
187
183
  if (tab != null) {
188
184
  setSelectedTab(tab);
189
185
  }
186
+ } else if (tablistRef?.current) {
187
+ let picker: HTMLElement | null = tablistRef.current.querySelector('button');
188
+ if (picker != null) {
189
+ setSelectedTab(picker);
190
+ }
190
191
  }
191
- }, [tablistRef, state?.selectedItem?.key]);
192
+ }, [tablistRef, state?.selectedItem?.key, showItems]);
193
+
194
+ let prevFocused = useRef<boolean | undefined>(false);
195
+ useLayoutEffect(() => {
196
+ if (!showItems && !prevFocused.current && state?.selectionManager.isFocused) {
197
+ onFocus();
198
+ }
199
+ prevFocused.current = state?.selectionManager.isFocused;
200
+ }, [state?.selectionManager.isFocused, state?.selectionManager.focusedKey, showItems]);
192
201
 
193
202
  return (
194
203
  <div
195
204
  style={props.UNSAFE_style}
196
205
  className={(props.UNSAFE_className || '') + style({position: 'relative'}, getAllowedOverrides())(null, props.styles)}>
197
- {orientation === 'vertical' &&
206
+ {showItems && orientation === 'vertical' &&
198
207
  <TabLine disabledKeys={disabledKeys} isDisabled={isDisabled} selectedTab={selectedTab} orientation={orientation} density={density} />}
199
208
  <RACTabList
200
209
  {...props}
201
210
  ref={tablistRef}
202
- className={renderProps => tablist({...renderProps, density})} />
211
+ className={renderProps => tablist({...renderProps, isIconOnly: iconOnly, density})} />
203
212
  {orientation === 'horizontal' &&
204
- <TabLine disabledKeys={disabledKeys} isDisabled={isDisabled} selectedTab={selectedTab} orientation={orientation} density={density} />}
213
+ <TabLine showItems={showItems} disabledKeys={disabledKeys} isDisabled={isDisabled} selectedTab={selectedTab} orientation={orientation} density={density} />}
205
214
  </div>
206
215
  );
207
216
  }
208
217
 
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
218
  interface TabLineProps {
230
219
  disabledKeys: Iterable<Key> | undefined,
231
220
  isDisabled: boolean | undefined,
232
221
  selectedTab: HTMLElement | undefined,
233
222
  orientation?: Orientation,
234
- density?: 'compact' | 'regular'
223
+ density?: 'compact' | 'regular',
224
+ showItems?: boolean
235
225
  }
236
226
 
237
227
  const selectedIndicator = style({
@@ -276,12 +266,9 @@ function TabLine(props: TabLineProps) {
276
266
  let {direction} = useLocale();
277
267
  let state = useContext(TabListStateContext);
278
268
 
279
- // We want to add disabled styling to the selection indicator only if all the Tabs are disabled
280
- let [isDisabled, setIsDisabled] = useState<boolean>(false);
281
- useEffect(() => {
282
- let isDisabled = isTabsDisabled || isAllTabsDisabled(state?.collection || null, disabledKeys ? new Set(disabledKeys) : new Set(null));
283
- setIsDisabled(isDisabled);
284
- }, [state?.collection, disabledKeys, isTabsDisabled, setIsDisabled]);
269
+ let isDisabled = useMemo(() => {
270
+ return isTabsDisabled || isAllTabsDisabled(state?.collection, disabledKeys ? new Set(disabledKeys) : new Set());
271
+ }, [state?.collection, disabledKeys, isTabsDisabled]);
285
272
 
286
273
  let [style, setStyle] = useState<{transform: string | undefined, width: string | undefined, height: string | undefined}>({
287
274
  transform: undefined,
@@ -321,43 +308,303 @@ function TabLine(props: TabLineProps) {
321
308
  );
322
309
  }
323
310
 
324
- const tabs = style({
311
+ const tab = style({
312
+ ...focusRing(),
325
313
  display: 'flex',
326
- flexShrink: 0,
327
- fontFamily: 'sans',
328
- fontWeight: 'normal',
329
- flexDirection: {
330
- orientation: {
331
- horizontal: 'column'
314
+ color: {
315
+ default: 'neutral-subdued',
316
+ isSelected: 'neutral',
317
+ isHovered: 'neutral-subdued',
318
+ isDisabled: 'disabled',
319
+ forcedColors: {
320
+ isSelected: 'Highlight',
321
+ isDisabled: 'GrayText'
332
322
  }
323
+ },
324
+ borderRadius: 'sm',
325
+ gap: 'text-to-visual',
326
+ height: {
327
+ density: {
328
+ compact: 32,
329
+ regular: 48
330
+ }
331
+ },
332
+ alignItems: 'center',
333
+ position: 'relative',
334
+ cursor: 'default',
335
+ flexShrink: 0,
336
+ transition: 'default'
337
+ }, getAllowedOverrides());
338
+
339
+ const icon = style({
340
+ display: 'block',
341
+ flexShrink: 0,
342
+ '--iconPrimary': {
343
+ type: 'fill',
344
+ value: 'currentColor'
333
345
  }
334
- }, getAllowedOverrides({height: true}));
346
+ });
335
347
 
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);
348
+ export function Tab(props: TabProps) {
349
+ let {density, iconOnly} = useContext(InternalTabsContext) ?? {};
348
350
 
349
351
  return (
350
- <RACTabs
352
+ <RACTab
351
353
  {...props}
352
- ref={domRef}
354
+ // @ts-ignore
355
+ originalProps={props}
353
356
  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
- ]}>
359
- {props.children}
360
- </Provider>
361
- </RACTabs>
357
+ className={renderProps => (props.UNSAFE_className || '') + tab({...renderProps, density}, props.styles)}>
358
+ {({
359
+ // @ts-ignore
360
+ isMenu
361
+ }) => {
362
+ if (isMenu) {
363
+ return props.children;
364
+ } else {
365
+ return (
366
+ <Provider
367
+ values={[
368
+ [TextContext, {
369
+ styles:
370
+ style({
371
+ order: 1,
372
+ display: {
373
+ isIconOnly: 'none'
374
+ }
375
+ })({isIconOnly: iconOnly})
376
+ }],
377
+ [IconContext, {
378
+ render: centerBaseline({slot: 'icon', styles: style({order: 0})}),
379
+ styles: icon
380
+ }]
381
+ ]}>
382
+ {typeof props.children === 'string' ? <Text>{props.children}</Text> : props.children}
383
+ </Provider>
384
+ );
385
+ }
386
+ }}
387
+ </RACTab>
362
388
  );
363
- });
389
+ }
390
+
391
+ const tabPanel = style({
392
+ marginTop: 4,
393
+ color: 'gray-800',
394
+ flexGrow: 1,
395
+ flexBasis: '[0%]',
396
+ minHeight: 0,
397
+ minWidth: 0
398
+ }, getAllowedOverrides({height: true}));
399
+
400
+ export function TabPanel(props: TabPanelProps) {
401
+ return (
402
+ <AriaTabPanel
403
+ {...props}
404
+ style={props.UNSAFE_style}
405
+ className={(props.UNSAFE_className || '') + tabPanel(null, props.styles)} />
406
+ );
407
+ }
408
+
409
+ function isAllTabsDisabled<T>(collection: Collection<Node<T>> | undefined, disabledKeys: Set<Key>) {
410
+ let testKey: Key | null = null;
411
+ if (collection && collection.size > 0) {
412
+ testKey = collection.getFirstKey();
413
+
414
+ let index = 0;
415
+ while (testKey && index < collection.size) {
416
+ // We have to check if the item in the collection has a key in disabledKeys or has the isDisabled prop set directly on it
417
+ if (!disabledKeys.has(testKey) && !collection.getItem(testKey)?.props?.isDisabled) {
418
+ return false;
419
+ }
420
+
421
+ testKey = collection.getKeyAfter(testKey);
422
+ index++;
423
+ }
424
+ return true;
425
+ }
426
+ return false;
427
+ }
428
+
429
+ let HiddenTabs = function (props: {
430
+ listRef: RefObject<HTMLDivElement | null>,
431
+ items: Array<Node<any>>,
432
+ size?: string,
433
+ density?: 'compact' | 'regular'
434
+ }) {
435
+ let {listRef, items, size, density} = props;
436
+
437
+ return (
438
+ <div
439
+ // @ts-ignore
440
+ inert="true"
441
+ ref={listRef}
442
+ className={style({
443
+ display: '[inherit]',
444
+ flexDirection: '[inherit]',
445
+ gap: '[inherit]',
446
+ flexWrap: '[inherit]',
447
+ position: 'absolute',
448
+ inset: 0,
449
+ visibility: 'hidden',
450
+ overflow: 'hidden',
451
+ opacity: 0
452
+ })}>
453
+ {items.map((item) => {
454
+ // pull off individual props as an allow list, don't want refs or other props getting through
455
+ return (
456
+ <div
457
+ data-hidden-tab
458
+ style={item.props.UNSAFE_style}
459
+ key={item.key}
460
+ className={item.props.className({size, density})}>
461
+ {item.props.children({size, density})}
462
+ </div>
463
+ );
464
+ })}
465
+ </div>
466
+ );
467
+ };
468
+
469
+ let TabsMenu = (props: {items: Array<Node<any>>, onSelectionChange: TabsProps['onSelectionChange']}) => {
470
+ let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2');
471
+ let {items} = props;
472
+ let {density, onSelectionChange, selectedKey, isDisabled, disabledKeys, pickerRef} = useContext(InternalTabsContext);
473
+ let state = useContext(TabListStateContext);
474
+ let allKeysDisabled = useMemo(() => {
475
+ return isAllTabsDisabled(state?.collection, disabledKeys ? new Set(disabledKeys) : new Set());
476
+ }, [state?.collection, disabledKeys]);
477
+
478
+ return (
479
+ <UNSTABLE_CollectionRendererContext.Provider value={UNSTABLE_DefaultCollectionRenderer}>
480
+ <div
481
+ className={style({
482
+ display: 'flex',
483
+ alignItems: 'center',
484
+ height: {
485
+ density: {
486
+ compact: 32,
487
+ regular: 48
488
+ }
489
+ }})({density})}>
490
+ <Picker
491
+ ref={pickerRef ? pickerRef : undefined}
492
+ isDisabled={isDisabled || allKeysDisabled}
493
+ density={density!}
494
+ items={items}
495
+ disabledKeys={disabledKeys}
496
+ selectedKey={selectedKey}
497
+ onSelectionChange={onSelectionChange}
498
+ aria-label={stringFormatter.format('tabs.selectorLabel')}>
499
+ {(item: Node<any>) => {
500
+ // need to determine the best way to handle icon only -> icon and text
501
+ // good enough to aria-label the picker item?
502
+ return (
503
+ <PickerItem
504
+ {...item.props.originalProps}
505
+ isDisabled={isDisabled || allKeysDisabled}
506
+ key={item.key}>
507
+ {item.props.children({density, isMenu: true})}
508
+ </PickerItem>
509
+ );
510
+ }}
511
+ </Picker>
512
+ </div>
513
+ </UNSTABLE_CollectionRendererContext.Provider>
514
+ );
515
+ };
516
+
517
+ // Context for passing the count for the custom renderer
518
+ let CollapseContext = createContext<{
519
+ containerRef: RefObject<HTMLDivElement | null>,
520
+ showItems: boolean,
521
+ setShowItems:(value: boolean) => void
522
+ } | null>(null);
523
+
524
+ function CollapsingCollection({children, containerRef}) {
525
+ let [showItems, _setShowItems] = useState(true);
526
+ let {orientation} = useContext(InternalTabsContext);
527
+ let setShowItems = useCallback((value: boolean) => {
528
+ if (orientation === 'vertical') {
529
+ // if orientation is vertical, we always show the items
530
+ _setShowItems(true);
531
+ } else {
532
+ _setShowItems(value);
533
+ }
534
+ }, [orientation]);
535
+ return (
536
+ <CollapseContext.Provider value={{containerRef, showItems: orientation === 'vertical' ? true : showItems, setShowItems}}>
537
+ <UNSTABLE_CollectionRendererContext.Provider value={CollapsingCollectionRenderer}>
538
+ {children}
539
+ </UNSTABLE_CollectionRendererContext.Provider>
540
+ </CollapseContext.Provider>
541
+ );
542
+ }
543
+
544
+ let CollapsingCollectionRenderer: CollectionRenderer = {
545
+ CollectionRoot({collection}) {
546
+ return useCollectionRender(collection);
547
+ },
548
+ CollectionBranch({collection}) {
549
+ return useCollectionRender(collection);
550
+ }
551
+ };
552
+
553
+
554
+ let useCollectionRender = (collection: Collection<Node<unknown>>) => {
555
+ let {containerRef, showItems, setShowItems} = useContext(CollapseContext) ?? {};
556
+ let {density = 'regular', orientation = 'horizontal', onSelectionChange} = useContext(InternalTabsContext);
557
+ let {direction} = useLocale();
558
+
559
+ let children = useMemo(() => {
560
+ let result: Node<any>[] = [];
561
+ for (let key of collection.getKeys()) {
562
+ result.push(collection.getItem(key)!);
563
+ }
564
+ return result;
565
+ }, [collection]);
566
+
567
+ let listRef = useRef<HTMLDivElement | null>(null);
568
+ let updateOverflow = useEffectEvent(() => {
569
+ if (orientation === 'vertical' || !listRef.current || !containerRef?.current) {
570
+ return;
571
+ }
572
+ let container = listRef.current;
573
+ let containerRect = container.getBoundingClientRect();
574
+ let tabs = container.querySelectorAll('[data-hidden-tab]');
575
+ let lastTab = tabs[tabs.length - 1];
576
+ let lastTabRect = lastTab.getBoundingClientRect();
577
+ if (direction === 'ltr') {
578
+ setShowItems?.(lastTabRect.right <= containerRect.right);
579
+ } else {
580
+ setShowItems?.(lastTabRect.left >= containerRect.left);
581
+ }
582
+ });
583
+
584
+ useResizeObserver({ref: containerRef, onResize: updateOverflow});
585
+
586
+ useLayoutEffect(() => {
587
+ if (collection.size > 0) {
588
+ queueMicrotask(updateOverflow);
589
+ }
590
+ }, [collection.size, updateOverflow]);
591
+
592
+ useEffect(() => {
593
+ // Recalculate visible tags when fonts are loaded.
594
+ document.fonts?.ready.then(() => updateOverflow());
595
+ // eslint-disable-next-line react-hooks/exhaustive-deps
596
+ }, []);
597
+
598
+ return (
599
+ <>
600
+ <HiddenTabs items={children} density={density} listRef={listRef} />
601
+ {showItems ? (
602
+ children.map(node => <Fragment key={node.key}>{node.render?.(node)}</Fragment>)
603
+ ) : (
604
+ <>
605
+ <TabsMenu items={children} onSelectionChange={onSelectionChange} />
606
+ </>
607
+ )}
608
+ </>
609
+ );
610
+ };