UNPKG

@material-ui/core

Version:

React components that implement Google's Material Design.

486 lines (431 loc) 15.9 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose"; /* eslint-disable no-restricted-globals */ import React from 'react'; import PropTypes from 'prop-types'; import warning from 'warning'; import clsx from 'clsx'; import debounce from '../utils/debounce'; import ownerWindow from '../utils/ownerWindow'; import { getNormalizedScrollLeft, detectScrollType } from 'normalize-scroll-left'; import animate from '../internal/animate'; import ScrollbarSize from './ScrollbarSize'; import withStyles from '../styles/withStyles'; import TabIndicator from './TabIndicator'; import TabScrollButton from './TabScrollButton'; import useEventCallback from '../utils/useEventCallback'; export const styles = theme => ({ /* Styles applied to the root element. */ root: { overflow: 'hidden', minHeight: 48, WebkitOverflowScrolling: 'touch', // Add iOS momentum scrolling. display: 'flex' }, /* Styles applied to the root element if `orientation="vertical"`. */ vertical: { flexDirection: 'column' }, /* Styles applied to the flex container element. */ flexContainer: { display: 'flex' }, /* Styles applied to the flex container element if `orientation="vertical"`. */ flexContainerVertical: { flexDirection: 'column' }, /* Styles applied to the flex container element if `centered={true}` & `!variant="scrollable"`. */ centered: { justifyContent: 'center' }, /* Styles applied to the tablist element. */ scroller: { position: 'relative', display: 'inline-block', flex: '1 1 auto', whiteSpace: 'nowrap' }, /* Styles applied to the tablist element if `!variant="scrollable"`. */ fixed: { overflowX: 'hidden', width: '100%' }, /* Styles applied to the tablist element if `variant="scrollable"`. */ scrollable: { overflowX: 'scroll', // Hide dimensionless scrollbar on MacOS scrollbarWidth: 'none', // Firefox '&::-webkit-scrollbar': { display: 'none' // Safari + Chrome } }, /* Styles applied to the `ScrollButtonComponent` component. */ scrollButtons: {}, /* Styles applied to the `ScrollButtonComponent` component if `scrollButtons="auto"` or scrollButtons="desktop"`. */ scrollButtonsDesktop: { [theme.breakpoints.down('xs')]: { display: 'none' } }, /* Styles applied to the `TabIndicator` component. */ indicator: {} }); const Tabs = React.forwardRef(function Tabs(props, ref) { const { action, centered = false, children: childrenProp, classes, className, component: Component = 'div', indicatorColor = 'secondary', onChange, orientation = 'horizontal', ScrollButtonComponent = TabScrollButton, scrollButtons = 'auto', TabIndicatorProps = {}, textColor = 'inherit', theme, value, variant = 'standard' } = props, other = _objectWithoutPropertiesLoose(props, ["action", "centered", "children", "classes", "className", "component", "indicatorColor", "onChange", "orientation", "ScrollButtonComponent", "scrollButtons", "TabIndicatorProps", "textColor", "theme", "value", "variant"]); const scrollable = variant === 'scrollable'; const isRtl = theme.direction === 'rtl'; const vertical = orientation === 'vertical'; const scrollStart = vertical ? 'scrollTop' : 'scrollLeft'; const start = vertical ? 'top' : 'left'; const end = vertical ? 'bottom' : 'right'; const clientSize = vertical ? 'clientHeight' : 'clientWidth'; const size = vertical ? 'height' : 'width'; process.env.NODE_ENV !== "production" ? warning(!centered || !scrollable, 'Material-UI: you can not use the `centered={true}` and `variant="scrollable"` properties ' + 'at the same time on a `Tabs` component.') : void 0; const [mounted, setMounted] = React.useState(false); const [indicatorStyle, setIndicatorStyle] = React.useState({}); const [displayScroll, setDisplayScroll] = React.useState({ start: false, end: false }); const [scrollerStyle, setScrollerStyle] = React.useState({ overflow: 'hidden', marginBottom: null }); const valueToIndex = new Map(); const tabsRef = React.useRef(null); const childrenWrapperRef = React.useRef(null); const getTabsMeta = () => { const tabsNode = tabsRef.current; let tabsMeta; if (tabsNode) { const rect = tabsNode.getBoundingClientRect(); // create a new object with ClientRect class props + scrollLeft tabsMeta = { clientWidth: tabsNode.clientWidth, scrollLeft: tabsNode.scrollLeft, scrollTop: tabsNode.scrollTop, scrollLeftNormalized: getNormalizedScrollLeft(tabsNode, theme.direction), scrollWidth: tabsNode.scrollWidth, top: rect.top, bottom: rect.bottom, left: rect.left, right: rect.right }; } let tabMeta; if (tabsNode && value !== false) { const children = childrenWrapperRef.current.children; if (children.length > 0) { const tab = children[valueToIndex.get(value)]; process.env.NODE_ENV !== "production" ? warning(tab, [`Material-UI: the value provided \`${value}\` to the Tabs component is invalid.`, 'None of the Tabs children have this value.', valueToIndex.keys ? `You can provide one of the following values: ${Array.from(valueToIndex.keys()).join(', ')}.` : null].join('\n')) : void 0; tabMeta = tab ? tab.getBoundingClientRect() : null; } } return { tabsMeta, tabMeta }; }; const updateIndicatorState = useEventCallback(() => { const { tabsMeta, tabMeta } = getTabsMeta(); let startValue = 0; if (tabMeta && tabsMeta) { if (vertical) { startValue = Math.round(tabMeta.top - tabsMeta.top + tabsMeta.scrollTop); } else { const correction = isRtl ? tabsMeta.scrollLeftNormalized + tabsMeta.clientWidth - tabsMeta.scrollWidth : tabsMeta.scrollLeft; startValue = Math.round(tabMeta.left - tabsMeta.left + correction); } } const newIndicatorStyle = { [start]: startValue, // May be wrong until the font is loaded. [size]: tabMeta ? Math.round(tabMeta[size]) : 0 }; if ((newIndicatorStyle[start] !== indicatorStyle[start] || newIndicatorStyle[size] !== indicatorStyle[size]) && !isNaN(newIndicatorStyle[start]) && !isNaN(newIndicatorStyle[size])) { setIndicatorStyle(newIndicatorStyle); } }); const scroll = scrollValue => { animate(scrollStart, tabsRef.current, scrollValue); }; const moveTabsScroll = delta => { let scrollValue = tabsRef.current[scrollStart]; if (vertical) { scrollValue += delta; } else { scrollValue += delta * (isRtl ? -1 : 1); // Fix for Edge scrollValue *= isRtl && detectScrollType() === 'reverse' ? -1 : 1; } scroll(scrollValue); }; const handleStartScrollClick = () => { moveTabsScroll(-tabsRef.current[clientSize]); }; const handleEndScrollClick = () => { moveTabsScroll(tabsRef.current[clientSize]); }; const handleScrollbarSizeChange = React.useCallback(scrollbarHeight => { setScrollerStyle({ overflow: null, marginBottom: -scrollbarHeight }); }, []); const getConditionalElements = () => { const conditionalElements = {}; conditionalElements.scrollbarSizeListener = scrollable ? React.createElement(ScrollbarSize, { className: classes.scrollable, onChange: handleScrollbarSizeChange }) : null; const scrollButtonsActive = displayScroll.start || displayScroll.end; const showScrollButtons = scrollable && (scrollButtons === 'auto' && scrollButtonsActive || scrollButtons === 'desktop' || scrollButtons === 'on'); conditionalElements.scrollButtonStart = showScrollButtons ? React.createElement(ScrollButtonComponent, { orientation: orientation, direction: isRtl ? 'right' : 'left', onClick: handleStartScrollClick, visible: displayScroll.start, className: clsx(classes.scrollButtons, scrollButtons !== 'on' && classes.scrollButtonsDesktop) }) : null; conditionalElements.scrollButtonEnd = showScrollButtons ? React.createElement(ScrollButtonComponent, { orientation: orientation, direction: isRtl ? 'left' : 'right', onClick: handleEndScrollClick, visible: displayScroll.end, className: clsx(classes.scrollButtons, scrollButtons !== 'on' && classes.scrollButtonsDesktop) }) : null; return conditionalElements; }; const scrollSelectedIntoView = useEventCallback(() => { const { tabsMeta, tabMeta } = getTabsMeta(); if (!tabMeta || !tabsMeta) { return; } if (tabMeta[start] < tabsMeta[start]) { // left side of button is out of view const nextScrollStart = tabsMeta[scrollStart] + (tabMeta[start] - tabsMeta[start]); scroll(nextScrollStart); } else if (tabMeta[end] > tabsMeta[end]) { // right side of button is out of view const nextScrollStart = tabsMeta[scrollStart] + (tabMeta[end] - tabsMeta[end]); scroll(nextScrollStart); } }); const updateScrollButtonState = useEventCallback(() => { if (scrollable && scrollButtons !== 'off') { const { scrollTop, scrollHeight, clientHeight, scrollWidth, clientWidth } = tabsRef.current; let showStartScroll; let showEndScroll; if (vertical) { showStartScroll = scrollTop > 1; showEndScroll = scrollTop < scrollHeight - clientHeight - 1; } else { const scrollLeft = getNormalizedScrollLeft(tabsRef.current, theme.direction); // use 1 for the potential rounding error with browser zooms. showStartScroll = isRtl ? scrollLeft < scrollWidth - clientWidth - 1 : scrollLeft > 1; showEndScroll = !isRtl ? scrollLeft < scrollWidth - clientWidth - 1 : scrollLeft > 1; } if (showStartScroll !== displayScroll.start || showEndScroll !== displayScroll.end) { setDisplayScroll({ start: showStartScroll, end: showEndScroll }); } } }); React.useEffect(() => { const handleResize = debounce(() => { updateIndicatorState(); updateScrollButtonState(); }); const win = ownerWindow(tabsRef.current); win.addEventListener('resize', handleResize); return () => { handleResize.clear(); win.removeEventListener('resize', handleResize); }; }, [updateIndicatorState, updateScrollButtonState]); const handleTabsScroll = React.useCallback(debounce(() => { updateScrollButtonState(); })); React.useEffect(() => { return () => { handleTabsScroll.clear(); }; }, [handleTabsScroll]); React.useEffect(() => { setMounted(true); }, []); React.useEffect(() => { updateIndicatorState(); updateScrollButtonState(); }); React.useEffect(() => { scrollSelectedIntoView(); }, [scrollSelectedIntoView, indicatorStyle]); React.useImperativeHandle(action, () => ({ updateIndicator: updateIndicatorState }), [updateIndicatorState]); const indicator = React.createElement(TabIndicator, _extends({ className: classes.indicator, orientation: orientation, color: indicatorColor }, TabIndicatorProps, { style: _extends({}, indicatorStyle, {}, TabIndicatorProps.style) })); let childIndex = 0; const children = React.Children.map(childrenProp, child => { if (!React.isValidElement(child)) { return null; } process.env.NODE_ENV !== "production" ? warning(child.type !== React.Fragment, ["Material-UI: the Tabs component doesn't accept a Fragment as a child.", 'Consider providing an array instead.'].join('\n')) : void 0; const childValue = child.props.value === undefined ? childIndex : child.props.value; valueToIndex.set(childValue, childIndex); const selected = childValue === value; childIndex += 1; return React.cloneElement(child, { fullWidth: variant === 'fullWidth', indicator: selected && !mounted && indicator, selected, onChange, textColor, value: childValue }); }); const conditionalElements = getConditionalElements(); return React.createElement(Component, _extends({ className: clsx(classes.root, className, vertical && classes.vertical), ref: ref }, other), conditionalElements.scrollButtonStart, conditionalElements.scrollbarSizeListener, React.createElement("div", { className: clsx(classes.scroller, scrollable ? classes.scrollable : classes.fixed), style: scrollerStyle, ref: tabsRef, onScroll: handleTabsScroll }, React.createElement("div", { className: clsx(classes.flexContainer, vertical && classes.flexContainerVertical, centered && !scrollable && classes.centered), ref: childrenWrapperRef, role: "tablist" }, children), mounted && indicator), conditionalElements.scrollButtonEnd); }); process.env.NODE_ENV !== "production" ? Tabs.propTypes = { /** * Callback fired when the component mounts. * This is useful when you want to trigger an action programmatically. * It currently only supports `updateIndicator()` action. * * @param {object} actions This object contains all possible actions * that can be triggered programmatically. */ action: PropTypes.func, /** * If `true`, the tabs will be centered. * This property is intended for large views. */ centered: PropTypes.bool, /** * The content of the component. */ children: PropTypes.node, /** * Override or extend the styles applied to the component. * See [CSS API](#css) below for more details. */ classes: PropTypes.object.isRequired, /** * @ignore */ className: PropTypes.string, /** * The component used for the root node. * Either a string to use a DOM element or a component. */ component: PropTypes.elementType, /** * Determines the color of the indicator. */ indicatorColor: PropTypes.oneOf(['secondary', 'primary']), /** * Callback fired when the value changes. * * @param {object} event The event source of the callback * @param {any} value We default to the index of the child (number) */ onChange: PropTypes.func, /** * The tabs orientation (layout flow direction). */ orientation: PropTypes.oneOf(['horizontal', 'vertical']), /** * The component used to render the scroll buttons. */ ScrollButtonComponent: PropTypes.elementType, /** * Determine behavior of scroll buttons when tabs are set to scroll: * * - `auto` will only present them when not all the items are visible. * - `desktop` will only present them on medium and larger viewports. * - `on` will always present them. * - `off` will never present them. */ scrollButtons: PropTypes.oneOf(['auto', 'desktop', 'on', 'off']), /** * Props applied to the tab indicator element. */ TabIndicatorProps: PropTypes.object, /** * Determines the color of the `Tab`. */ textColor: PropTypes.oneOf(['secondary', 'primary', 'inherit']), /** * @ignore */ theme: PropTypes.object.isRequired, /** * The value of the currently selected `Tab`. * If you don't want any selected `Tab`, you can set this property to `false`. */ value: PropTypes.any, /** * Determines additional display behavior of the tabs: * * - `scrollable` will invoke scrolling properties and allow for horizontally * scrolling (or swiping) of the tab bar. * -`fullWidth` will make the tabs grow to use all the available space, * which should be used for small views, like on mobile. * - `standard` will render the default state. */ variant: PropTypes.oneOf(['standard', 'scrollable', 'fullWidth']) } : void 0; export default withStyles(styles, { name: 'MuiTabs', withTheme: true })(Tabs);