1 | import _extends from "@babel/runtime/helpers/esm/extends";
|
2 | import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
|
3 | import * as React from 'react';
|
4 | import { isFragment } from 'react-is';
|
5 | import PropTypes from 'prop-types';
|
6 | import clsx from 'clsx';
|
7 | import { refType } from '@material-ui/utils';
|
8 | import debounce from '../utils/debounce';
|
9 | import ownerWindow from '../utils/ownerWindow';
|
10 | import { getNormalizedScrollLeft, detectScrollType } from '../utils/scrollLeft';
|
11 | import animate from '../internal/animate';
|
12 | import ScrollbarSize from './ScrollbarSize';
|
13 | import withStyles from '../styles/withStyles';
|
14 | import TabIndicator from './TabIndicator';
|
15 | import TabScrollButton from '../TabScrollButton';
|
16 | import useEventCallback from '../utils/useEventCallback';
|
17 | import useTheme from '../styles/useTheme';
|
18 | export const styles = theme => ({
|
19 |
|
20 | root: {
|
21 | overflow: 'hidden',
|
22 | minHeight: 48,
|
23 | WebkitOverflowScrolling: 'touch',
|
24 |
|
25 | display: 'flex'
|
26 | },
|
27 |
|
28 |
|
29 | vertical: {
|
30 | flexDirection: 'column'
|
31 | },
|
32 |
|
33 |
|
34 | flexContainer: {
|
35 | display: 'flex'
|
36 | },
|
37 |
|
38 |
|
39 | flexContainerVertical: {
|
40 | flexDirection: 'column'
|
41 | },
|
42 |
|
43 |
|
44 | centered: {
|
45 | justifyContent: 'center'
|
46 | },
|
47 |
|
48 |
|
49 | scroller: {
|
50 | position: 'relative',
|
51 | display: 'inline-block',
|
52 | flex: '1 1 auto',
|
53 | whiteSpace: 'nowrap'
|
54 | },
|
55 |
|
56 |
|
57 | fixed: {
|
58 | overflowX: 'hidden',
|
59 | width: '100%'
|
60 | },
|
61 |
|
62 |
|
63 | scrollable: {
|
64 | overflowX: 'scroll',
|
65 |
|
66 | scrollbarWidth: 'none',
|
67 |
|
68 | '&::-webkit-scrollbar': {
|
69 | display: 'none'
|
70 |
|
71 | }
|
72 | },
|
73 |
|
74 |
|
75 | scrollButtons: {},
|
76 |
|
77 |
|
78 | scrollButtonsDesktop: {
|
79 | [theme.breakpoints.down('xs')]: {
|
80 | display: 'none'
|
81 | }
|
82 | },
|
83 |
|
84 |
|
85 | indicator: {}
|
86 | });
|
87 | const Tabs = React.forwardRef(function Tabs(props, ref) {
|
88 | const {
|
89 | 'aria-label': ariaLabel,
|
90 | 'aria-labelledby': ariaLabelledBy,
|
91 | action,
|
92 | centered = false,
|
93 | children: childrenProp,
|
94 | classes,
|
95 | className,
|
96 | component: Component = 'div',
|
97 | indicatorColor = 'secondary',
|
98 | onChange,
|
99 | orientation = 'horizontal',
|
100 | ScrollButtonComponent = TabScrollButton,
|
101 | scrollButtons = 'auto',
|
102 | selectionFollowsFocus,
|
103 | TabIndicatorProps = {},
|
104 | TabScrollButtonProps,
|
105 | textColor = 'inherit',
|
106 | value,
|
107 | variant = 'standard'
|
108 | } = props,
|
109 | other = _objectWithoutPropertiesLoose(props, ["aria-label", "aria-labelledby", "action", "centered", "children", "classes", "className", "component", "indicatorColor", "onChange", "orientation", "ScrollButtonComponent", "scrollButtons", "selectionFollowsFocus", "TabIndicatorProps", "TabScrollButtonProps", "textColor", "value", "variant"]);
|
110 |
|
111 | const theme = useTheme();
|
112 | const scrollable = variant === 'scrollable';
|
113 | const isRtl = theme.direction === 'rtl';
|
114 | const vertical = orientation === 'vertical';
|
115 | const scrollStart = vertical ? 'scrollTop' : 'scrollLeft';
|
116 | const start = vertical ? 'top' : 'left';
|
117 | const end = vertical ? 'bottom' : 'right';
|
118 | const clientSize = vertical ? 'clientHeight' : 'clientWidth';
|
119 | const size = vertical ? 'height' : 'width';
|
120 |
|
121 | if (process.env.NODE_ENV !== 'production') {
|
122 | if (centered && scrollable) {
|
123 | console.error('Material-UI: You can not use the `centered={true}` and `variant="scrollable"` properties ' + 'at the same time on a `Tabs` component.');
|
124 | }
|
125 | }
|
126 |
|
127 | const [mounted, setMounted] = React.useState(false);
|
128 | const [indicatorStyle, setIndicatorStyle] = React.useState({});
|
129 | const [displayScroll, setDisplayScroll] = React.useState({
|
130 | start: false,
|
131 | end: false
|
132 | });
|
133 | const [scrollerStyle, setScrollerStyle] = React.useState({
|
134 | overflow: 'hidden',
|
135 | marginBottom: null
|
136 | });
|
137 | const valueToIndex = new Map();
|
138 | const tabsRef = React.useRef(null);
|
139 | const tabListRef = React.useRef(null);
|
140 |
|
141 | const getTabsMeta = () => {
|
142 | const tabsNode = tabsRef.current;
|
143 | let tabsMeta;
|
144 |
|
145 | if (tabsNode) {
|
146 | const rect = tabsNode.getBoundingClientRect();
|
147 |
|
148 | tabsMeta = {
|
149 | clientWidth: tabsNode.clientWidth,
|
150 | scrollLeft: tabsNode.scrollLeft,
|
151 | scrollTop: tabsNode.scrollTop,
|
152 | scrollLeftNormalized: getNormalizedScrollLeft(tabsNode, theme.direction),
|
153 | scrollWidth: tabsNode.scrollWidth,
|
154 | top: rect.top,
|
155 | bottom: rect.bottom,
|
156 | left: rect.left,
|
157 | right: rect.right
|
158 | };
|
159 | }
|
160 |
|
161 | let tabMeta;
|
162 |
|
163 | if (tabsNode && value !== false) {
|
164 | const children = tabListRef.current.children;
|
165 |
|
166 | if (children.length > 0) {
|
167 | const tab = children[valueToIndex.get(value)];
|
168 |
|
169 | if (process.env.NODE_ENV !== 'production') {
|
170 | if (!tab) {
|
171 | console.error([`Material-UI: The value provided to the Tabs component is invalid.`, `None of the Tabs' children match with \`${value}\`.`, valueToIndex.keys ? `You can provide one of the following values: ${Array.from(valueToIndex.keys()).join(', ')}.` : null].join('\n'));
|
172 | }
|
173 | }
|
174 |
|
175 | tabMeta = tab ? tab.getBoundingClientRect() : null;
|
176 | }
|
177 | }
|
178 |
|
179 | return {
|
180 | tabsMeta,
|
181 | tabMeta
|
182 | };
|
183 | };
|
184 |
|
185 | const updateIndicatorState = useEventCallback(() => {
|
186 | const {
|
187 | tabsMeta,
|
188 | tabMeta
|
189 | } = getTabsMeta();
|
190 | let startValue = 0;
|
191 |
|
192 | if (tabMeta && tabsMeta) {
|
193 | if (vertical) {
|
194 | startValue = tabMeta.top - tabsMeta.top + tabsMeta.scrollTop;
|
195 | } else {
|
196 | const correction = isRtl ? tabsMeta.scrollLeftNormalized + tabsMeta.clientWidth - tabsMeta.scrollWidth : tabsMeta.scrollLeft;
|
197 | startValue = tabMeta.left - tabsMeta.left + correction;
|
198 | }
|
199 | }
|
200 |
|
201 | const newIndicatorStyle = {
|
202 | [start]: startValue,
|
203 |
|
204 | [size]: tabMeta ? tabMeta[size] : 0
|
205 | };
|
206 |
|
207 | if (isNaN(indicatorStyle[start]) || isNaN(indicatorStyle[size])) {
|
208 | setIndicatorStyle(newIndicatorStyle);
|
209 | } else {
|
210 | const dStart = Math.abs(indicatorStyle[start] - newIndicatorStyle[start]);
|
211 | const dSize = Math.abs(indicatorStyle[size] - newIndicatorStyle[size]);
|
212 |
|
213 | if (dStart >= 1 || dSize >= 1) {
|
214 | setIndicatorStyle(newIndicatorStyle);
|
215 | }
|
216 | }
|
217 | });
|
218 |
|
219 | const scroll = scrollValue => {
|
220 | animate(scrollStart, tabsRef.current, scrollValue);
|
221 | };
|
222 |
|
223 | const moveTabsScroll = delta => {
|
224 | let scrollValue = tabsRef.current[scrollStart];
|
225 |
|
226 | if (vertical) {
|
227 | scrollValue += delta;
|
228 | } else {
|
229 | scrollValue += delta * (isRtl ? -1 : 1);
|
230 |
|
231 | scrollValue *= isRtl && detectScrollType() === 'reverse' ? -1 : 1;
|
232 | }
|
233 |
|
234 | scroll(scrollValue);
|
235 | };
|
236 |
|
237 | const handleStartScrollClick = () => {
|
238 | moveTabsScroll(-tabsRef.current[clientSize]);
|
239 | };
|
240 |
|
241 | const handleEndScrollClick = () => {
|
242 | moveTabsScroll(tabsRef.current[clientSize]);
|
243 | };
|
244 |
|
245 | const handleScrollbarSizeChange = React.useCallback(scrollbarHeight => {
|
246 | setScrollerStyle({
|
247 | overflow: null,
|
248 | marginBottom: -scrollbarHeight
|
249 | });
|
250 | }, []);
|
251 |
|
252 | const getConditionalElements = () => {
|
253 | const conditionalElements = {};
|
254 | conditionalElements.scrollbarSizeListener = scrollable ? React.createElement(ScrollbarSize, {
|
255 | className: classes.scrollable,
|
256 | onChange: handleScrollbarSizeChange
|
257 | }) : null;
|
258 | const scrollButtonsActive = displayScroll.start || displayScroll.end;
|
259 | const showScrollButtons = scrollable && (scrollButtons === 'auto' && scrollButtonsActive || scrollButtons === 'desktop' || scrollButtons === 'on');
|
260 | conditionalElements.scrollButtonStart = showScrollButtons ? React.createElement(ScrollButtonComponent, _extends({
|
261 | orientation: orientation,
|
262 | direction: isRtl ? 'right' : 'left',
|
263 | onClick: handleStartScrollClick,
|
264 | disabled: !displayScroll.start,
|
265 | className: clsx(classes.scrollButtons, scrollButtons !== 'on' && classes.scrollButtonsDesktop)
|
266 | }, TabScrollButtonProps)) : null;
|
267 | conditionalElements.scrollButtonEnd = showScrollButtons ? React.createElement(ScrollButtonComponent, _extends({
|
268 | orientation: orientation,
|
269 | direction: isRtl ? 'left' : 'right',
|
270 | onClick: handleEndScrollClick,
|
271 | disabled: !displayScroll.end,
|
272 | className: clsx(classes.scrollButtons, scrollButtons !== 'on' && classes.scrollButtonsDesktop)
|
273 | }, TabScrollButtonProps)) : null;
|
274 | return conditionalElements;
|
275 | };
|
276 |
|
277 | const scrollSelectedIntoView = useEventCallback(() => {
|
278 | const {
|
279 | tabsMeta,
|
280 | tabMeta
|
281 | } = getTabsMeta();
|
282 |
|
283 | if (!tabMeta || !tabsMeta) {
|
284 | return;
|
285 | }
|
286 |
|
287 | if (tabMeta[start] < tabsMeta[start]) {
|
288 |
|
289 | const nextScrollStart = tabsMeta[scrollStart] + (tabMeta[start] - tabsMeta[start]);
|
290 | scroll(nextScrollStart);
|
291 | } else if (tabMeta[end] > tabsMeta[end]) {
|
292 |
|
293 | const nextScrollStart = tabsMeta[scrollStart] + (tabMeta[end] - tabsMeta[end]);
|
294 | scroll(nextScrollStart);
|
295 | }
|
296 | });
|
297 | const updateScrollButtonState = useEventCallback(() => {
|
298 | if (scrollable && scrollButtons !== 'off') {
|
299 | const {
|
300 | scrollTop,
|
301 | scrollHeight,
|
302 | clientHeight,
|
303 | scrollWidth,
|
304 | clientWidth
|
305 | } = tabsRef.current;
|
306 | let showStartScroll;
|
307 | let showEndScroll;
|
308 |
|
309 | if (vertical) {
|
310 | showStartScroll = scrollTop > 1;
|
311 | showEndScroll = scrollTop < scrollHeight - clientHeight - 1;
|
312 | } else {
|
313 | const scrollLeft = getNormalizedScrollLeft(tabsRef.current, theme.direction);
|
314 |
|
315 | showStartScroll = isRtl ? scrollLeft < scrollWidth - clientWidth - 1 : scrollLeft > 1;
|
316 | showEndScroll = !isRtl ? scrollLeft < scrollWidth - clientWidth - 1 : scrollLeft > 1;
|
317 | }
|
318 |
|
319 | if (showStartScroll !== displayScroll.start || showEndScroll !== displayScroll.end) {
|
320 | setDisplayScroll({
|
321 | start: showStartScroll,
|
322 | end: showEndScroll
|
323 | });
|
324 | }
|
325 | }
|
326 | });
|
327 | React.useEffect(() => {
|
328 | const handleResize = debounce(() => {
|
329 | updateIndicatorState();
|
330 | updateScrollButtonState();
|
331 | });
|
332 | const win = ownerWindow(tabsRef.current);
|
333 | win.addEventListener('resize', handleResize);
|
334 | return () => {
|
335 | handleResize.clear();
|
336 | win.removeEventListener('resize', handleResize);
|
337 | };
|
338 | }, [updateIndicatorState, updateScrollButtonState]);
|
339 | const handleTabsScroll = React.useCallback(debounce(() => {
|
340 | updateScrollButtonState();
|
341 | }));
|
342 | React.useEffect(() => {
|
343 | return () => {
|
344 | handleTabsScroll.clear();
|
345 | };
|
346 | }, [handleTabsScroll]);
|
347 | React.useEffect(() => {
|
348 | setMounted(true);
|
349 | }, []);
|
350 | React.useEffect(() => {
|
351 | updateIndicatorState();
|
352 | updateScrollButtonState();
|
353 | });
|
354 | React.useEffect(() => {
|
355 | scrollSelectedIntoView();
|
356 | }, [scrollSelectedIntoView, indicatorStyle]);
|
357 | React.useImperativeHandle(action, () => ({
|
358 | updateIndicator: updateIndicatorState,
|
359 | updateScrollButtons: updateScrollButtonState
|
360 | }), [updateIndicatorState, updateScrollButtonState]);
|
361 | const indicator = React.createElement(TabIndicator, _extends({
|
362 | className: classes.indicator,
|
363 | orientation: orientation,
|
364 | color: indicatorColor
|
365 | }, TabIndicatorProps, {
|
366 | style: _extends({}, indicatorStyle, TabIndicatorProps.style)
|
367 | }));
|
368 | let childIndex = 0;
|
369 | const children = React.Children.map(childrenProp, child => {
|
370 | if (! React.isValidElement(child)) {
|
371 | return null;
|
372 | }
|
373 |
|
374 | if (process.env.NODE_ENV !== 'production') {
|
375 | if (isFragment(child)) {
|
376 | console.error(["Material-UI: The Tabs component doesn't accept a Fragment as a child.", 'Consider providing an array instead.'].join('\n'));
|
377 | }
|
378 | }
|
379 |
|
380 | const childValue = child.props.value === undefined ? childIndex : child.props.value;
|
381 | valueToIndex.set(childValue, childIndex);
|
382 | const selected = childValue === value;
|
383 | childIndex += 1;
|
384 | return React.cloneElement(child, {
|
385 | fullWidth: variant === 'fullWidth',
|
386 | indicator: selected && !mounted && indicator,
|
387 | selected,
|
388 | selectionFollowsFocus,
|
389 | onChange,
|
390 | textColor,
|
391 | value: childValue
|
392 | });
|
393 | });
|
394 |
|
395 | const handleKeyDown = event => {
|
396 | const {
|
397 | target
|
398 | } = event;
|
399 |
|
400 |
|
401 |
|
402 | const role = target.getAttribute('role');
|
403 |
|
404 | if (role !== 'tab') {
|
405 | return;
|
406 | }
|
407 |
|
408 | let newFocusTarget = null;
|
409 | let previousItemKey = orientation !== "vertical" ? 'ArrowLeft' : 'ArrowUp';
|
410 | let nextItemKey = orientation !== "vertical" ? 'ArrowRight' : 'ArrowDown';
|
411 |
|
412 | if (orientation !== "vertical" && theme.direction === 'rtl') {
|
413 |
|
414 | previousItemKey = 'ArrowRight';
|
415 | nextItemKey = 'ArrowLeft';
|
416 | }
|
417 |
|
418 | switch (event.key) {
|
419 | case previousItemKey:
|
420 | newFocusTarget = target.previousElementSibling || tabListRef.current.lastChild;
|
421 | break;
|
422 |
|
423 | case nextItemKey:
|
424 | newFocusTarget = target.nextElementSibling || tabListRef.current.firstChild;
|
425 | break;
|
426 |
|
427 | case 'Home':
|
428 | newFocusTarget = tabListRef.current.firstChild;
|
429 | break;
|
430 |
|
431 | case 'End':
|
432 | newFocusTarget = tabListRef.current.lastChild;
|
433 | break;
|
434 |
|
435 | default:
|
436 | break;
|
437 | }
|
438 |
|
439 | if (newFocusTarget !== null) {
|
440 | newFocusTarget.focus();
|
441 | event.preventDefault();
|
442 | }
|
443 | };
|
444 |
|
445 | const conditionalElements = getConditionalElements();
|
446 | return React.createElement(Component, _extends({
|
447 | className: clsx(classes.root, className, vertical && classes.vertical),
|
448 | ref: ref
|
449 | }, other), conditionalElements.scrollButtonStart, conditionalElements.scrollbarSizeListener, React.createElement("div", {
|
450 | className: clsx(classes.scroller, scrollable ? classes.scrollable : classes.fixed),
|
451 | style: scrollerStyle,
|
452 | ref: tabsRef,
|
453 | onScroll: handleTabsScroll
|
454 | }, React.createElement("div", {
|
455 | "aria-label": ariaLabel,
|
456 | "aria-labelledby": ariaLabelledBy,
|
457 | className: clsx(classes.flexContainer, vertical && classes.flexContainerVertical, centered && !scrollable && classes.centered),
|
458 | onKeyDown: handleKeyDown,
|
459 | ref: tabListRef,
|
460 | role: "tablist"
|
461 | }, children), mounted && indicator), conditionalElements.scrollButtonEnd);
|
462 | });
|
463 | process.env.NODE_ENV !== "production" ? Tabs.propTypes = {
|
464 |
|
465 |
|
466 |
|
467 |
|
468 |
|
469 | |
470 |
|
471 |
|
472 |
|
473 |
|
474 |
|
475 |
|
476 |
|
477 | action: refType,
|
478 |
|
479 | |
480 |
|
481 |
|
482 | 'aria-label': PropTypes.string,
|
483 |
|
484 | |
485 |
|
486 |
|
487 | 'aria-labelledby': PropTypes.string,
|
488 |
|
489 | |
490 |
|
491 |
|
492 |
|
493 | centered: PropTypes.bool,
|
494 |
|
495 | |
496 |
|
497 |
|
498 | children: PropTypes.node,
|
499 |
|
500 | |
501 |
|
502 |
|
503 |
|
504 | classes: PropTypes.object,
|
505 |
|
506 | |
507 |
|
508 |
|
509 | className: PropTypes.string,
|
510 |
|
511 | |
512 |
|
513 |
|
514 |
|
515 | component: PropTypes
|
516 |
|
517 | .elementType,
|
518 |
|
519 | |
520 |
|
521 |
|
522 | indicatorColor: PropTypes.oneOf(['primary', 'secondary']),
|
523 |
|
524 | |
525 |
|
526 |
|
527 |
|
528 |
|
529 |
|
530 | onChange: PropTypes.func,
|
531 |
|
532 | |
533 |
|
534 |
|
535 | orientation: PropTypes.oneOf(['horizontal', 'vertical']),
|
536 |
|
537 | |
538 |
|
539 |
|
540 | ScrollButtonComponent: PropTypes.elementType,
|
541 |
|
542 | |
543 |
|
544 |
|
545 |
|
546 |
|
547 |
|
548 |
|
549 |
|
550 | scrollButtons: PropTypes.oneOf(['auto', 'desktop', 'off', 'on']),
|
551 |
|
552 | |
553 |
|
554 |
|
555 |
|
556 | selectionFollowsFocus: PropTypes.bool,
|
557 |
|
558 | |
559 |
|
560 |
|
561 | TabIndicatorProps: PropTypes.object,
|
562 |
|
563 | |
564 |
|
565 |
|
566 | TabScrollButtonProps: PropTypes.object,
|
567 |
|
568 | |
569 |
|
570 |
|
571 | textColor: PropTypes.oneOf(['inherit', 'primary', 'secondary']),
|
572 |
|
573 | |
574 |
|
575 |
|
576 |
|
577 | value: PropTypes.any,
|
578 |
|
579 | |
580 |
|
581 |
|
582 |
|
583 |
|
584 |
|
585 |
|
586 |
|
587 |
|
588 | variant: PropTypes.oneOf(['fullWidth', 'scrollable', 'standard'])
|
589 | } : void 0;
|
590 | export default withStyles(styles, {
|
591 | name: 'MuiTabs'
|
592 | })(Tabs); |
\ | No newline at end of file |