@react-spectrum/tabs 3.8.29 → 3.9.0

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 DELETED
@@ -1,466 +0,0 @@
1
- /*
2
- * Copyright 2020 Adobe. All rights reserved.
3
- * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
- * you may not use this file except in compliance with the License. You may obtain a copy
5
- * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
- *
7
- * Unless required by applicable law or agreed to in writing, software distributed under
8
- * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
- * OF ANY KIND, either express or implied. See the License for the specific language
10
- * governing permissions and limitations under the License.
11
- */
12
-
13
- import {AriaTabPanelProps, SpectrumTabListProps, SpectrumTabPanelsProps, SpectrumTabsProps} from '@react-types/tabs';
14
- import {classNames, SlotProvider, unwrapDOMRef, useDOMRef, useStyleProps} from '@react-spectrum/utils';
15
- import {DOMProps, DOMRef, DOMRefValue, Key, Node, Orientation, RefObject, StyleProps} from '@react-types/shared';
16
- import {filterDOMProps, mergeProps, useId, useLayoutEffect, useResizeObserver} from '@react-aria/utils';
17
- import {FocusRing} from '@react-aria/focus';
18
- import {Item, Picker} from '@react-spectrum/picker';
19
- import {ListCollection} from '@react-stately/list';
20
- import React, {
21
- CSSProperties,
22
- HTMLAttributes,
23
- ReactElement,
24
- ReactNode,
25
- useCallback,
26
- useContext,
27
- useEffect,
28
- useRef,
29
- useState
30
- } from 'react';
31
- import {SpectrumPickerProps} from '@react-types/select';
32
- import styles from '@adobe/spectrum-css-temp/components/tabs/vars.css';
33
- import {TabListState, useTabListState} from '@react-stately/tabs';
34
- import {Text} from '@react-spectrum/text';
35
- import {useCollection} from '@react-stately/collections';
36
- import {useHover} from '@react-aria/interactions';
37
- import {useLocale} from '@react-aria/i18n';
38
- import {useProvider, useProviderProps} from '@react-spectrum/provider';
39
- import {useTab, useTabList, useTabPanel} from '@react-aria/tabs';
40
-
41
- interface TabsContext<T> {
42
- tabProps: SpectrumTabsProps<T>,
43
- tabState: {
44
- tabListState: TabListState<T> | null,
45
- setTabListState: (state: TabListState<T>) => void,
46
- selectedTab: HTMLElement | null,
47
- collapsed: boolean
48
- },
49
- refs: {
50
- wrapperRef: RefObject<HTMLDivElement | null>,
51
- tablistRef: RefObject<HTMLDivElement | null>
52
- },
53
- tabPanelProps: HTMLAttributes<HTMLElement>,
54
- tabLineState: Array<DOMRect>
55
- }
56
-
57
- const TabContext = React.createContext<TabsContext<any> | null>(null);
58
-
59
- /**
60
- * 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.
61
- */
62
- // forwardRef doesn't support generic parameters, so cast the result to the correct type
63
- // https://stackoverflow.com/questions/58469229/react-with-typescript-generics-while-using-react-forwardref
64
- export const Tabs = React.forwardRef(function Tabs<T extends object>(props: SpectrumTabsProps<T>, ref: DOMRef<HTMLDivElement>) {
65
- props = useProviderProps(props);
66
- let {
67
- orientation = 'horizontal' as Orientation,
68
- density = 'regular',
69
- children,
70
- ...otherProps
71
- } = props;
72
-
73
- let domRef = useDOMRef(ref);
74
- let tablistRef = useRef<HTMLDivElement>(null);
75
- let wrapperRef = useRef<HTMLDivElement>(null);
76
-
77
- let {direction} = useLocale();
78
- let {styleProps} = useStyleProps(otherProps);
79
- let [collapsed, setCollapsed] = useState(false);
80
- let [selectedTab, setSelectedTab] = useState<HTMLElement | null>(null);
81
- const [tabListState, setTabListState] = useState<TabListState<T> | null>(null);
82
- let [tabPositions, setTabPositions] = useState<DOMRect[]>([]);
83
- let prevTabPositions = useRef<DOMRect[]>(tabPositions);
84
-
85
- useEffect(() => {
86
- if (tablistRef.current) {
87
- let selectedTab: HTMLElement | null = tablistRef.current.querySelector(`[data-key="${CSS.escape(tabListState?.selectedKey?.toString() ?? '')}"]`);
88
-
89
- if (selectedTab != null) {
90
- setSelectedTab(selectedTab);
91
- }
92
- }
93
- // collapse is in the dep array so selectedTab can be updated for TabLine positioning
94
- }, [children, tabListState?.selectedKey, collapsed, tablistRef]);
95
-
96
- let checkShouldCollapse = useCallback(() => {
97
- if (wrapperRef.current && orientation !== 'vertical') {
98
- let tabsComponent = wrapperRef.current;
99
- let tabs: NodeListOf<Element> = tablistRef.current?.querySelectorAll('[role="tab"]') ?? new NodeList() as NodeListOf<Element>;
100
- let tabDimensions = [...tabs].map((tab: Element) => tab.getBoundingClientRect());
101
-
102
- let end = direction === 'rtl' ? 'left' : 'right';
103
- let farEdgeTabList = tabsComponent.getBoundingClientRect()[end];
104
- let farEdgeLastTab = tabDimensions[tabDimensions.length - 1][end];
105
- let shouldCollapse = direction === 'rtl' ? farEdgeLastTab < farEdgeTabList : farEdgeTabList < farEdgeLastTab;
106
- setCollapsed(shouldCollapse);
107
- if (tabDimensions.length !== prevTabPositions.current.length
108
- || tabDimensions.some((box, index) => box?.left !== prevTabPositions.current[index]?.left || box?.right !== prevTabPositions.current[index]?.right)) {
109
- setTabPositions(tabDimensions);
110
- prevTabPositions.current = tabDimensions;
111
- }
112
- }
113
- }, [tablistRef, wrapperRef, direction, orientation, setCollapsed, prevTabPositions, setTabPositions]);
114
-
115
- useLayoutEffect(() => {
116
- checkShouldCollapse();
117
- }, [children, checkShouldCollapse]);
118
-
119
- useResizeObserver({ref: wrapperRef, onResize: checkShouldCollapse});
120
-
121
- let tabPanelProps: HTMLAttributes<HTMLElement> = {
122
- 'aria-labelledby': undefined
123
- };
124
-
125
- // When the tabs are collapsed, the tabPanel should be labelled by the Picker button element.
126
- let collapsibleTabListId = useId();
127
- if (collapsed && orientation !== 'vertical') {
128
- tabPanelProps['aria-labelledby'] = collapsibleTabListId;
129
- }
130
- return (
131
- <TabContext.Provider
132
- value={{
133
- tabProps: {...props, orientation, density},
134
- tabState: {tabListState, setTabListState, selectedTab, collapsed},
135
- refs: {tablistRef, wrapperRef},
136
- tabPanelProps,
137
- tabLineState: tabPositions
138
- }}>
139
- <div
140
- {...filterDOMProps(otherProps)}
141
- {...styleProps}
142
- ref={domRef}
143
- className={classNames(
144
- styles,
145
- 'spectrum-TabsPanel',
146
- `spectrum-TabsPanel--${orientation}`,
147
- styleProps.className
148
- )}>
149
- {props.children}
150
- </div>
151
- </TabContext.Provider>
152
- );
153
- }) as <T>(props: SpectrumTabsProps<T> & {ref?: DOMRef<HTMLDivElement>}) => ReactElement;
154
-
155
- interface TabProps<T> extends DOMProps {
156
- item: Node<T>,
157
- state: TabListState<T>,
158
- isDisabled?: boolean,
159
- orientation?: Orientation
160
- }
161
-
162
- // @private
163
- function Tab<T>(props: TabProps<T>) {
164
- let {item, state} = props;
165
- let {key, rendered} = item;
166
-
167
- let ref = useRef<any>(undefined);
168
- let {tabProps, isSelected, isDisabled} = useTab({key}, state, ref);
169
-
170
- let {hoverProps, isHovered} = useHover({
171
- ...props
172
- });
173
- let ElementType: React.ElementType = item.props.href ? 'a' : 'div';
174
-
175
- return (
176
- <FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
177
- <ElementType
178
- {...mergeProps(tabProps, hoverProps)}
179
- ref={ref}
180
- className={classNames(
181
- styles,
182
- 'spectrum-Tabs-item',
183
- {
184
- 'is-selected': isSelected,
185
- 'is-disabled': isDisabled,
186
- 'is-hovered': isHovered
187
- }
188
- )}>
189
- <SlotProvider
190
- slots={{
191
- icon: {
192
- size: 'S',
193
- UNSAFE_className: classNames(styles, 'spectrum-Icon')
194
- },
195
- text: {
196
- UNSAFE_className: classNames(styles, 'spectrum-Tabs-itemLabel')
197
- }
198
- }}>
199
- {typeof rendered === 'string'
200
- ? <Text>{rendered}</Text>
201
- : rendered}
202
- </SlotProvider>
203
- </ElementType>
204
- </FocusRing>
205
- );
206
- }
207
-
208
- interface TabLineProps {
209
- orientation?: Orientation,
210
- selectedTab?: HTMLElement | null,
211
- selectedKey?: Key | null
212
- }
213
-
214
- // @private
215
- function TabLine(props: TabLineProps) {
216
- let {
217
- orientation,
218
- // Is either the tab node (non-collapsed) or the picker node (collapsed)
219
- selectedTab,
220
- // selectedKey is provided so that the TabLine styles are updated when the TabPicker's width updates from a selection change
221
- selectedKey
222
- } = props;
223
-
224
- let {direction} = useLocale();
225
- let {scale} = useProvider();
226
- let {tabLineState} = useContext(TabContext)!;
227
-
228
- let [style, setStyle] = useState<CSSProperties>({
229
- width: undefined,
230
- height: undefined
231
- });
232
-
233
- let onResize = useCallback(() => {
234
- if (selectedTab) {
235
- let styleObj: CSSProperties = {transform: undefined, width: undefined, height: undefined};
236
- // In RTL, calculate the transform from the right edge of the tablist so that resizing the window doesn't break the Tabline position due to offsetLeft changes
237
- let offset = direction === 'rtl' ?
238
- -1 * ((selectedTab.offsetParent as HTMLElement)?.offsetWidth - selectedTab.offsetWidth - selectedTab.offsetLeft) :
239
- selectedTab.offsetLeft;
240
- styleObj.transform = orientation === 'vertical'
241
- ? `translateY(${selectedTab.offsetTop}px)`
242
- : `translateX(${offset}px)`;
243
-
244
- if (orientation === 'horizontal') {
245
- styleObj.width = `${selectedTab.offsetWidth}px`;
246
- } else {
247
- styleObj.height = `${selectedTab.offsetHeight}px`;
248
- }
249
- setStyle(styleObj);
250
- }
251
- }, [direction, setStyle, selectedTab, orientation]);
252
-
253
- useLayoutEffect(() => {
254
- onResize();
255
- }, [onResize, scale, selectedKey, tabLineState]);
256
-
257
- return <div className={classNames(styles, 'spectrum-Tabs-selectionIndicator')} role="presentation" style={style} />;
258
- }
259
-
260
- /**
261
- * A TabList is used within Tabs to group tabs that a user can switch between.
262
- * The keys of the items within the <TabList> must match up with a corresponding item inside the <TabPanels>.
263
- */
264
- export function TabList<T>(props: SpectrumTabListProps<T>): ReactElement {
265
- const tabContext = useContext(TabContext)!;
266
- const {refs, tabState, tabProps, tabPanelProps} = tabContext;
267
- const {isQuiet, density, isEmphasized, orientation} = tabProps;
268
- const {selectedTab, collapsed, setTabListState} = tabState;
269
- const {tablistRef, wrapperRef} = refs;
270
- // Pass original Tab props but override children to create the collection.
271
- const state = useTabListState({...tabProps, children: props.children});
272
-
273
- let {styleProps} = useStyleProps(props);
274
- const {tabListProps} = useTabList({...tabProps, ...props}, state, tablistRef);
275
-
276
- useEffect(() => {
277
- // Passing back to root as useTabPanel needs the TabListState
278
- setTabListState(state);
279
- // eslint-disable-next-line react-hooks/exhaustive-deps
280
- }, [state.disabledKeys, state.selectedItem, state.selectedKey, props.children]);
281
-
282
- let collapseStyle : React.CSSProperties = collapsed && orientation !== 'vertical' ? {maxWidth: 'calc(100% + 1px)', overflow: 'hidden', visibility: 'hidden', position: 'absolute'} : {maxWidth: 'calc(100% + 1px)'};
283
- let stylePropsFinal = orientation === 'vertical' ? styleProps : {style: collapseStyle};
284
-
285
- if (collapsed && orientation !== 'vertical') {
286
- tabListProps['aria-hidden'] = true;
287
- }
288
-
289
- let tabListclassName = classNames(styles, 'spectrum-TabsPanel-tabs');
290
-
291
- const tabContent = (
292
- <div
293
- {...stylePropsFinal}
294
- {...tabListProps}
295
- ref={tablistRef}
296
- className={classNames(
297
- styles,
298
- 'spectrum-Tabs',
299
- `spectrum-Tabs--${orientation}`,
300
- tabListclassName,
301
- {
302
- 'spectrum-Tabs--quiet': isQuiet,
303
- 'spectrum-Tabs--emphasized': isEmphasized,
304
- ['spectrum-Tabs--compact']: density === 'compact'
305
- },
306
- orientation === 'vertical' && styleProps.className
307
- )
308
- }>
309
- {[...state.collection].map((item) => (
310
- <Tab key={item.key} item={item} state={state} orientation={orientation} />
311
- ))}
312
- <TabLine orientation={orientation} selectedTab={selectedTab} />
313
- </div>
314
- );
315
-
316
-
317
- if (orientation === 'vertical') {
318
- return tabContent;
319
- } else {
320
- return (
321
- <div
322
- {...styleProps}
323
- ref={wrapperRef}
324
- className={classNames(
325
- styles,
326
- 'spectrum-TabsPanel-collapseWrapper',
327
- styleProps.className
328
- )}>
329
- <TabPicker {...props} {...tabProps} visible={collapsed} id={tabPanelProps['aria-labelledby']} state={state} className={tabListclassName} />
330
- {tabContent}
331
- </div>
332
- );
333
- }
334
- }
335
-
336
- /**
337
- * TabPanels is used within Tabs as a container for the content of each tab.
338
- * The keys of the items within the <TabPanels> must match up with a corresponding item inside the <TabList>.
339
- */
340
- export function TabPanels<T extends object>(props: SpectrumTabPanelsProps<T>): ReactElement {
341
- const {tabState, tabProps} = useContext(TabContext)!;
342
- const {tabListState} = tabState;
343
-
344
- const factory = useCallback((nodes: Iterable<Node<T>>) => new ListCollection(nodes), []);
345
- const collection = useCollection({items: tabProps.items, ...props}, factory, {suppressTextValueWarning: true});
346
- const selectedItem = tabListState && tabListState.selectedKey != null ? collection.getItem(tabListState.selectedKey) : null;
347
-
348
- return (
349
- <TabPanel {...props} key={tabListState?.selectedKey}>
350
- {selectedItem && selectedItem.props.children}
351
- </TabPanel>
352
- );
353
- }
354
-
355
- interface TabPanelProps extends AriaTabPanelProps, StyleProps {
356
- children?: ReactNode
357
- }
358
-
359
- // @private
360
- function TabPanel(props: TabPanelProps) {
361
- const {tabState, tabPanelProps: ctxTabPanelProps} = useContext(TabContext)!;
362
- const {tabListState} = tabState;
363
- let ref = useRef<HTMLDivElement | null>(null);
364
- const {tabPanelProps} = useTabPanel(props, tabListState, ref);
365
- let {styleProps} = useStyleProps(props);
366
-
367
- if (ctxTabPanelProps['aria-labelledby']) {
368
- tabPanelProps['aria-labelledby'] = ctxTabPanelProps['aria-labelledby'];
369
- }
370
-
371
- return (
372
- <FocusRing focusRingClass={classNames(styles, 'focus-ring')}>
373
- <div {...styleProps} {...tabPanelProps} ref={ref} className={classNames(styles, 'spectrum-TabsPanel-tabpanel', styleProps.className)}>
374
- {props.children}
375
- </div>
376
- </FocusRing>
377
- );
378
- }
379
-
380
- interface TabPickerProps<T> extends Omit<SpectrumPickerProps<T>, 'children' | 'onSelectionChange'> {
381
- density?: 'compact' | 'regular',
382
- isEmphasized?: boolean,
383
- state: TabListState<T>,
384
- className?: string,
385
- visible: boolean,
386
- onSelectionChange?: (key: Key) => void
387
- }
388
-
389
- function TabPicker<T>(props: TabPickerProps<T>) {
390
- let {
391
- isDisabled,
392
- isEmphasized,
393
- isQuiet,
394
- state,
395
- 'aria-labelledby': ariaLabeledBy,
396
- 'aria-label': ariaLabel,
397
- density,
398
- className,
399
- id,
400
- visible
401
- } = props;
402
-
403
- let ref = useRef<DOMRefValue<HTMLDivElement>>(null);
404
- let [pickerNode, setPickerNode] = useState<HTMLElement | null>(null);
405
-
406
- useEffect(() => {
407
- let node = unwrapDOMRef(ref);
408
- setPickerNode(node.current);
409
- }, [ref]);
410
-
411
- let items = [...state.collection];
412
- let pickerProps = {
413
- 'aria-labelledby': ariaLabeledBy,
414
- 'aria-label': ariaLabel
415
- };
416
-
417
- const style : React.CSSProperties = visible ? {} : {visibility: 'hidden', position: 'absolute'};
418
-
419
- return (
420
- <div
421
- className={classNames(
422
- styles,
423
- 'spectrum-Tabs',
424
- 'spectrum-Tabs--horizontal',
425
- 'spectrum-Tabs--isCollapsed',
426
- {
427
- 'spectrum-Tabs--quiet': isQuiet,
428
- ['spectrum-Tabs--compact']: density === 'compact',
429
- 'spectrum-Tabs--emphasized': isEmphasized
430
- },
431
- className
432
- )}
433
- style={style}
434
- aria-hidden={visible ? undefined : true}>
435
- <SlotProvider
436
- slots={{
437
- icon: {
438
- size: 'S',
439
- UNSAFE_className: classNames(styles, 'spectrum-Icon')
440
- },
441
- button: {
442
- focusRingClass: classNames(styles, 'focus-ring')
443
- }
444
- }}>
445
- <Picker
446
- {...pickerProps}
447
- id={id}
448
- items={items}
449
- ref={ref}
450
- isQuiet
451
- isDisabled={!visible || isDisabled}
452
- selectedKey={state.selectedKey}
453
- disabledKeys={state.disabledKeys}
454
- onSelectionChange={key => {
455
- if (key != null) {
456
- state.setSelectedKey(key);
457
- }
458
- }}
459
- UNSAFE_className={classNames(styles, 'spectrum-Tabs-picker')}>
460
- {item => <Item {...item.props}>{item.rendered}</Item>}
461
- </Picker>
462
- {pickerNode && <TabLine orientation="horizontal" selectedTab={pickerNode} selectedKey={state.selectedKey} />}
463
- </SlotProvider>
464
- </div>
465
- );
466
- }