@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/dist/Tabs.cjs +396 -175
- package/dist/Tabs.cjs.map +1 -1
- package/dist/Tabs.css +200 -120
- package/dist/Tabs.css.map +1 -1
- package/dist/Tabs.mjs +398 -177
- package/dist/Tabs.mjs.map +1 -1
- package/dist/TabsPicker.cjs +364 -0
- package/dist/TabsPicker.cjs.map +1 -0
- package/dist/TabsPicker.css +416 -0
- package/dist/TabsPicker.css.map +1 -0
- package/dist/TabsPicker.mjs +358 -0
- package/dist/TabsPicker.mjs.map +1 -0
- package/dist/en-US.cjs +1 -0
- package/dist/en-US.cjs.map +1 -1
- package/dist/en-US.mjs +1 -0
- package/dist/en-US.mjs.map +1 -1
- package/dist/he-IL.cjs +1 -0
- package/dist/he-IL.cjs.map +1 -1
- package/dist/he-IL.mjs +1 -0
- package/dist/he-IL.mjs.map +1 -1
- package/dist/types.d.ts +9 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +18 -17
- package/src/Tabs.tsx +402 -155
- package/src/TabsPicker.tsx +321 -0
package/src/Tabs.tsx
CHANGED
|
@@ -11,29 +11,35 @@
|
|
|
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
|
+
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
|
|
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'>, 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
|
|
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
|
-
'
|
|
119
|
-
|
|
120
|
-
|
|
81
|
+
font: 'ui',
|
|
82
|
+
flexDirection: {
|
|
83
|
+
orientation: {
|
|
84
|
+
horizontal: 'column'
|
|
85
|
+
}
|
|
121
86
|
}
|
|
122
|
-
});
|
|
87
|
+
}, getAllowedOverrides({height: true}));
|
|
123
88
|
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
<
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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} =
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
311
|
+
const tab = style({
|
|
312
|
+
...focusRing(),
|
|
325
313
|
display: 'flex',
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
}
|
|
346
|
+
});
|
|
335
347
|
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
<
|
|
352
|
+
<RACTab
|
|
351
353
|
{...props}
|
|
352
|
-
|
|
354
|
+
// @ts-ignore
|
|
355
|
+
originalProps={props}
|
|
353
356
|
style={props.UNSAFE_style}
|
|
354
|
-
className={renderProps => (props.UNSAFE_className || '') +
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
{
|
|
360
|
-
|
|
361
|
-
|
|
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
|
+
};
|