UNPKG

@material-ui/core

Version:

React components that implement Google's Material Design.

495 lines (434 loc) 15.2 kB
import _extends from "@babel/runtime/helpers/extends"; import _objectSpread from "@babel/runtime/helpers/objectSpread"; import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties"; /* eslint-disable no-restricted-globals */ import React from 'react'; import PropTypes from 'prop-types'; import warning from 'warning'; import classNames from 'classnames'; import EventListener from 'react-event-listener'; import debounce from 'debounce'; // < 1kb payload overhead when lodash/debounce is > 3kb. 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'; export const styles = theme => ({ /* Styles applied to the root element. */ root: { overflow: 'hidden', minHeight: 48, WebkitOverflowScrolling: 'touch' // Add iOS momentum scrolling. }, /* Styles applied to the flex container element. */ flexContainer: { display: 'flex' }, /* Styles applied to the flex container element if `centered={true}` & `scrollable={false}`. */ 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 `scrollable={false}`. */ fixed: { overflowX: 'hidden', width: '100%' }, /* Styles applied to the tablist element if `scrollable={true}`. */ scrollable: { overflowX: 'scroll' }, /* Styles applied to the `ScrollButtonComponent` component. */ scrollButtons: {}, /* Styles applied to the `ScrollButtonComponent` component if `scrollButtons="auto"`. */ scrollButtonsAuto: { [theme.breakpoints.down('xs')]: { display: 'none' } }, /* Styles applied to the `TabIndicator` component. */ indicator: {} }); class Tabs extends React.Component { constructor(...args) { super(...args); this.valueToIndex = new Map(); this.handleResize = debounce(() => { this.updateIndicatorState(this.props); this.updateScrollButtonState(); }, 166); this.handleTabsScroll = debounce(() => { this.updateScrollButtonState(); }, 166); this.state = { indicatorStyle: {}, scrollerStyle: { marginBottom: 0 }, showLeftScroll: false, showRightScroll: false, mounted: false }; this.getConditionalElements = () => { const { classes, scrollable, ScrollButtonComponent, scrollButtons, theme } = this.props; const conditionalElements = {}; conditionalElements.scrollbarSizeListener = scrollable ? React.createElement(ScrollbarSize, { onLoad: this.handleScrollbarSizeChange, onChange: this.handleScrollbarSizeChange }) : null; const showScrollButtons = scrollable && (scrollButtons === 'auto' || scrollButtons === 'on'); conditionalElements.scrollButtonLeft = showScrollButtons ? React.createElement(ScrollButtonComponent, { direction: theme && theme.direction === 'rtl' ? 'right' : 'left', onClick: this.handleLeftScrollClick, visible: this.state.showLeftScroll, className: classNames(classes.scrollButtons, { [classes.scrollButtonsAuto]: scrollButtons === 'auto' }) }) : null; conditionalElements.scrollButtonRight = showScrollButtons ? React.createElement(ScrollButtonComponent, { direction: theme && theme.direction === 'rtl' ? 'left' : 'right', onClick: this.handleRightScrollClick, visible: this.state.showRightScroll, className: classNames(classes.scrollButtons, { [classes.scrollButtonsAuto]: scrollButtons === 'auto' }) }) : null; return conditionalElements; }; this.getTabsMeta = (value, direction) => { let tabsMeta; if (this.tabsRef) { const rect = this.tabsRef.getBoundingClientRect(); // create a new object with ClientRect class props + scrollLeft tabsMeta = { clientWidth: this.tabsRef.clientWidth, scrollLeft: this.tabsRef.scrollLeft, scrollLeftNormalized: getNormalizedScrollLeft(this.tabsRef, direction), scrollWidth: this.tabsRef.scrollWidth, left: rect.left, right: rect.right }; } let tabMeta; if (this.tabsRef && value !== false) { const children = this.tabsRef.children[0].children; if (children.length > 0) { const tab = children[this.valueToIndex.get(value)]; process.env.NODE_ENV !== "production" ? warning(tab, `Material-UI: the value provided \`${value}\` is invalid`) : void 0; tabMeta = tab ? tab.getBoundingClientRect() : null; } } return { tabsMeta, tabMeta }; }; this.handleLeftScrollClick = () => { this.moveTabsScroll(-this.tabsRef.clientWidth); }; this.handleRightScrollClick = () => { this.moveTabsScroll(this.tabsRef.clientWidth); }; this.handleScrollbarSizeChange = ({ scrollbarHeight }) => { this.setState({ scrollerStyle: { marginBottom: -scrollbarHeight } }); }; this.moveTabsScroll = delta => { const { theme } = this.props; const multiplier = theme.direction === 'rtl' ? -1 : 1; const nextScrollLeft = this.tabsRef.scrollLeft + delta * multiplier; // Fix for Edge const invert = theme.direction === 'rtl' && detectScrollType() === 'reverse' ? -1 : 1; this.scroll(invert * nextScrollLeft); }; this.scrollSelectedIntoView = () => { const { theme, value } = this.props; const { tabsMeta, tabMeta } = this.getTabsMeta(value, theme.direction); if (!tabMeta || !tabsMeta) { return; } if (tabMeta.left < tabsMeta.left) { // left side of button is out of view const nextScrollLeft = tabsMeta.scrollLeft + (tabMeta.left - tabsMeta.left); this.scroll(nextScrollLeft); } else if (tabMeta.right > tabsMeta.right) { // right side of button is out of view const nextScrollLeft = tabsMeta.scrollLeft + (tabMeta.right - tabsMeta.right); this.scroll(nextScrollLeft); } }; this.scroll = value => { animate('scrollLeft', this.tabsRef, value); }; this.updateScrollButtonState = () => { const { scrollable, scrollButtons, theme } = this.props; if (scrollable && scrollButtons !== 'off') { const { scrollWidth, clientWidth } = this.tabsRef; const scrollLeft = getNormalizedScrollLeft(this.tabsRef, theme.direction); const showLeftScroll = theme.direction === 'rtl' ? scrollWidth > clientWidth + scrollLeft : scrollLeft > 0; const showRightScroll = theme.direction === 'rtl' ? scrollLeft > 0 : scrollWidth > clientWidth + scrollLeft; if (showLeftScroll !== this.state.showLeftScroll || showRightScroll !== this.state.showRightScroll) { this.setState({ showLeftScroll, showRightScroll }); } } }; } componentDidMount() { // eslint-disable-next-line react/no-did-mount-set-state this.setState({ mounted: true }); this.updateIndicatorState(this.props); this.updateScrollButtonState(); if (this.props.action) { this.props.action({ updateIndicator: this.handleResize }); } } componentDidUpdate(prevProps, prevState) { // The index might have changed at the same time. // We need to check again the right indicator position. this.updateIndicatorState(this.props); this.updateScrollButtonState(); if (this.state.indicatorStyle !== prevState.indicatorStyle) { this.scrollSelectedIntoView(); } } componentWillUnmount() { this.handleResize.clear(); this.handleTabsScroll.clear(); } updateIndicatorState(props) { const { theme, value } = props; const { tabsMeta, tabMeta } = this.getTabsMeta(value, theme.direction); let left = 0; if (tabMeta && tabsMeta) { const correction = theme.direction === 'rtl' ? tabsMeta.scrollLeftNormalized + tabsMeta.clientWidth - tabsMeta.scrollWidth : tabsMeta.scrollLeft; left = Math.round(tabMeta.left - tabsMeta.left + correction); } const indicatorStyle = { left, // May be wrong until the font is loaded. width: tabMeta ? Math.round(tabMeta.width) : 0 }; if ((indicatorStyle.left !== this.state.indicatorStyle.left || indicatorStyle.width !== this.state.indicatorStyle.width) && !isNaN(indicatorStyle.left) && !isNaN(indicatorStyle.width)) { this.setState({ indicatorStyle }); } } render() { const _this$props = this.props, { action, centered, children: childrenProp, classes, className: classNameProp, component: Component, fullWidth, indicatorColor, onChange, scrollable, ScrollButtonComponent, scrollButtons, TabIndicatorProps = {}, textColor, theme, value } = _this$props, other = _objectWithoutProperties(_this$props, ["action", "centered", "children", "classes", "className", "component", "fullWidth", "indicatorColor", "onChange", "scrollable", "ScrollButtonComponent", "scrollButtons", "TabIndicatorProps", "textColor", "theme", "value"]); process.env.NODE_ENV !== "production" ? warning(!centered || !scrollable, 'Material-UI: you can not use the `centered={true}` and `scrollable={true}` properties ' + 'at the same time on a `Tabs` component.') : void 0; const className = classNames(classes.root, classNameProp); const flexContainerClassName = classNames(classes.flexContainer, { [classes.centered]: centered && !scrollable }); const scrollerClassName = classNames(classes.scroller, { [classes.fixed]: !scrollable, [classes.scrollable]: scrollable }); const indicator = React.createElement(TabIndicator, _extends({ className: classes.indicator, color: indicatorColor }, TabIndicatorProps, { style: _objectSpread({}, this.state.indicatorStyle, TabIndicatorProps.style) })); this.valueToIndex = new Map(); 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; this.valueToIndex.set(childValue, childIndex); const selected = childValue === value; childIndex += 1; return React.cloneElement(child, { fullWidth, indicator: selected && !this.state.mounted && indicator, selected, onChange, textColor, value: childValue }); }); const conditionalElements = this.getConditionalElements(); return React.createElement(Component, _extends({ className: className }, other), React.createElement(EventListener, { target: "window", onResize: this.handleResize }), conditionalElements.scrollbarSizeListener, React.createElement("div", { className: classes.flexContainer }, conditionalElements.scrollButtonLeft, React.createElement("div", { className: scrollerClassName, style: this.state.scrollerStyle, ref: ref => { this.tabsRef = ref; }, role: "tablist", onScroll: this.handleTabsScroll }, React.createElement("div", { className: flexContainerClassName }, children), this.state.mounted && indicator), conditionalElements.scrollButtonRight)); } } Tabs.propTypes = process.env.NODE_ENV !== "production" ? { /** * 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-api) 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.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]), /** * If `true`, the tabs will grow to use all the available space. * This property is intended for small views, like on mobile. */ fullWidth: PropTypes.bool, /** * 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 {number} value We default to the index of the child */ onChange: PropTypes.func, /** * True invokes scrolling properties and allow for horizontally scrolling * (or swiping) the tab bar. */ scrollable: PropTypes.bool, /** * The component used to render the scroll buttons. */ ScrollButtonComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.object]), /** * Determine behavior of scroll buttons when tabs are set to scroll * `auto` will only present them on medium and larger viewports * `on` will always present them * `off` will never present them */ scrollButtons: PropTypes.oneOf(['auto', 'on', 'off']), /** * Properties applied to the `TabIndicator` 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 } : {}; Tabs.defaultProps = { centered: false, component: 'div', fullWidth: false, indicatorColor: 'secondary', scrollable: false, ScrollButtonComponent: TabScrollButton, scrollButtons: 'auto', textColor: 'inherit' }; export default withStyles(styles, { name: 'MuiTabs', withTheme: true })(Tabs);