@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/dist/Badge.cjs +2 -1
- package/dist/Badge.cjs.map +1 -1
- package/dist/Badge.css +6 -2
- package/dist/Badge.css.map +1 -1
- package/dist/Badge.mjs +2 -1
- package/dist/Badge.mjs.map +1 -1
- package/dist/Content.cjs.map +1 -1
- package/dist/Content.mjs.map +1 -1
- package/dist/Tabs.cjs +462 -166
- package/dist/Tabs.cjs.map +1 -1
- package/dist/Tabs.css +232 -116
- package/dist/Tabs.css.map +1 -1
- package/dist/Tabs.mjs +463 -167
- package/dist/Tabs.mjs.map +1 -1
- package/dist/TabsPicker.cjs +415 -0
- package/dist/TabsPicker.cjs.map +1 -0
- package/dist/TabsPicker.css +482 -0
- package/dist/TabsPicker.css.map +1 -0
- package/dist/TabsPicker.mjs +409 -0
- package/dist/TabsPicker.mjs.map +1 -0
- package/dist/types.d.ts +12 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +21 -20
- package/src/Badge.tsx +4 -1
- package/src/Content.tsx +2 -1
- package/src/Tabs.tsx +450 -144
- package/src/TabsPicker.tsx +350 -0
package/src/Tabs.tsx
CHANGED
|
@@ -11,28 +11,32 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 {
|
|
30
|
-
import {
|
|
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
|
|
63
|
+
children: ReactNode
|
|
54
64
|
}
|
|
55
65
|
|
|
56
|
-
export interface TabListProps<T> extends Omit<AriaTabListProps<T>, '
|
|
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
|
|
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
|
-
|
|
112
|
-
flexShrink: 0,
|
|
113
|
-
transition: 'default'
|
|
114
|
-
}, getAllowedOverrides());
|
|
115
|
-
|
|
116
|
-
const icon = style({
|
|
85
|
+
display: 'flex',
|
|
117
86
|
flexShrink: 0,
|
|
118
|
-
'
|
|
119
|
-
|
|
120
|
-
|
|
87
|
+
font: 'ui',
|
|
88
|
+
flexDirection: {
|
|
89
|
+
orientation: {
|
|
90
|
+
horizontal: 'column'
|
|
91
|
+
}
|
|
121
92
|
}
|
|
122
|
-
});
|
|
93
|
+
}, getAllowedOverrides({height: true}));
|
|
123
94
|
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
<
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
horizontal: 'column'
|
|
344
|
+
transition: 'default',
|
|
345
|
+
paddingX: {
|
|
346
|
+
labelBehavior: {
|
|
347
|
+
hide: size(6)
|
|
332
348
|
}
|
|
333
349
|
}
|
|
334
|
-
}, getAllowedOverrides(
|
|
350
|
+
}, getAllowedOverrides());
|
|
335
351
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
} =
|
|
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
|
-
<
|
|
367
|
+
<RACTab
|
|
351
368
|
{...props}
|
|
352
|
-
|
|
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 || '') +
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
</
|
|
361
|
-
|
|
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
|
+
};
|