grommet
Version:
focus on the essential experience
324 lines (319 loc) • 14.2 kB
JavaScript
var _excluded = ["alignControls", "children", "flex", "justify", "messages", "responsive"];
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
import React, { forwardRef, useCallback, useContext, useState, useEffect, useRef, useMemo } from 'react';
import { Previous } from 'grommet-icons/icons/Previous';
import { Next } from 'grommet-icons/icons/Next';
import { useLayoutEffect } from '../../utils/use-isomorphic-layout-effect';
import { Box } from '../Box';
import { Button } from '../Button';
import { TabsContext } from './TabsContext';
import { ResponsiveContext } from '../../contexts/ResponsiveContext';
import { StyledTabPanel, StyledTabs, StyledTabsHeader } from './StyledTabs';
import { normalizeColor } from '../../utils';
import { MessageContext } from '../../contexts/MessageContext';
import { TabsPropTypes } from './propTypes';
import { useAnalytics } from '../../contexts/AnalyticsContext/AnalyticsContext';
import { useThemeValue } from '../../utils/useThemeValue';
var Tabs = /*#__PURE__*/forwardRef(function (_ref, ref) {
var _theme$tabs$header, _theme$tabs$header2, _theme$tabs$header3, _theme$tabs$header4;
var alignControls = _ref.alignControls,
children = _ref.children,
flex = _ref.flex,
_ref$justify = _ref.justify,
justify = _ref$justify === void 0 ? 'center' : _ref$justify,
messages = _ref.messages,
_ref$responsive = _ref.responsive,
responsive = _ref$responsive === void 0 ? true : _ref$responsive,
rest = _objectWithoutPropertiesLoose(_ref, _excluded);
var _useThemeValue = useThemeValue(),
theme = _useThemeValue.theme,
passThemeFlag = _useThemeValue.passThemeFlag;
var _useContext = useContext(MessageContext),
format = _useContext.format;
var propsActiveIndex = rest.activeIndex,
onActive = rest.onActive;
var _useState = useState(rest.activeIndex || 0),
activeIndex = _useState[0],
setActiveIndex = _useState[1];
var _useState2 = useState(),
activeContent = _useState2[0],
setActiveContent = _useState2[1];
var _useState3 = useState(),
activeTitle = _useState3[0],
setActiveTitle = _useState3[1];
var _useState4 = useState(),
disableLeftArrow = _useState4[0],
setDisableLeftArrow = _useState4[1];
var _useState5 = useState(),
disableRightArrow = _useState5[0],
setDisableRightArrow = _useState5[1];
var _useState6 = useState(),
overflow = _useState6[0],
setOverflow = _useState6[1];
var _useState7 = useState(-1),
focusIndex = _useState7[0],
setFocusIndex = _useState7[1];
var headerRef = useRef();
var size = useContext(ResponsiveContext);
var PreviousIcon = ((_theme$tabs$header = theme.tabs.header) == null || (_theme$tabs$header = _theme$tabs$header.previousButton) == null ? void 0 : _theme$tabs$header.icon) || Previous;
var NextIcon = ((_theme$tabs$header2 = theme.tabs.header) == null || (_theme$tabs$header2 = _theme$tabs$header2.nextButton) == null ? void 0 : _theme$tabs$header2.icon) || Next;
var sendAnalytics = useAnalytics();
if (activeIndex !== propsActiveIndex && propsActiveIndex !== undefined) {
setActiveIndex(propsActiveIndex);
}
// Safari v15.5 has an issue with scrolling when overflow='hidden'
// and scroll-behavior='smooth'. For now we are detecting if the browser
// is safari to workaround this issue. The issue should be resolved soon
// and we can remove this. https://github.com/WebKit/WebKit/pull/1387
var isSafari = typeof window !== 'undefined' ? /^((?!chrome|android).)*safari/i.test(window.navigator.userAgent) : true;
/* eslint-disable no-param-reassign */
delete rest.activeIndex;
delete rest.onActive;
/* eslint-enable no-param-reassign */
var tabRefs = useMemo(function () {
return React.Children.map(children, function () {
return /*#__PURE__*/React.createRef();
});
}, [children]);
// check if tab is in view
var isVisible = useCallback(function (index) {
if (tabRefs[index].current) {
var _headerRef$current;
var tabRect = tabRefs[index].current.getBoundingClientRect();
var headerRect = (_headerRef$current = headerRef.current) == null ? void 0 : _headerRef$current.getBoundingClientRect();
if (tabRect && headerRect) {
// the -1 and +1 allow a little leniency when calculating if a tab
// is in view. Without the -1 and +1 a tab could be fully in view
// but isVisible will return false.
return tabRect.left >= headerRect.left - 1 && tabRect.right <= headerRect.right + 1;
}
}
return undefined;
}, [headerRef, tabRefs]);
var updateArrowState = useCallback(function () {
setDisableLeftArrow(isVisible(0));
setDisableRightArrow(isVisible(tabRefs.length - 1));
}, [tabRefs, isVisible]);
var scrollTo = useCallback(function (index, keyboard) {
var tabRect = tabRefs[index].current.getBoundingClientRect();
var headerRect = headerRef.current.getBoundingClientRect();
var amountHidden = 0;
if (tabRect.left >= headerRect.left && tabRect.left <= headerRect.right) {
amountHidden = tabRect.width - (headerRect.right - tabRect.left);
} else if (tabRect.right >= headerRect.left && tabRect.right <= headerRect.right) {
amountHidden = tabRect.width - (tabRect.right - headerRect.left);
amountHidden = 0 - amountHidden;
} else if (tabRect.left >= headerRect.right) {
amountHidden = tabRect.right - headerRect.right;
} else if (tabRect.right <= headerRect.left) {
amountHidden = headerRect.left - tabRect.left;
amountHidden = 0 - amountHidden;
}
// We are adding or subtracting 2 from amountHidden to
// ensure the focusIndicator is visible when navigating
// by keyboard
if (keyboard) {
if (amountHidden < 0) amountHidden -= 2;
if (amountHidden > 0) amountHidden += 2;
}
if (isSafari) {
headerRef.current.scrollBy({
left: amountHidden
});
} else {
headerRef.current.scrollBy({
left: amountHidden,
behavior: 'smooth'
});
}
// wait for scroll animation to finish
// checks every 50 milliseconds for 1000 milliseconds
// if the scroll animation has finished. Most scroll
// animations will finish in 1000 milliseconds unless
// the tab name is very long.
if (isSafari) {
updateArrowState();
} else {
var checkVisible = setInterval(function () {
if (tabRefs[index].current && isVisible(index)) {
updateArrowState();
clearInterval(checkVisible);
}
}, 50);
setTimeout(function () {
updateArrowState();
clearInterval(checkVisible);
}, 1000);
}
}, [tabRefs, headerRef, isVisible, updateArrowState, isSafari]);
var moveByArrowKey = function moveByArrowKey(direction) {
var previous = direction === 'previous';
var index = direction === 'previous' ? 0 : tabRefs.length - 1;
var scrolledToIndex;
var moveBy = theme.tabs.step[size] - 1 || 0;
while (scrolledToIndex === undefined && (previous && index < tabRefs.length - 1 || !previous && index > 0)) {
if (!isVisible(index) && (previous && isVisible(index + 1) || !previous && isVisible(index - 1))) {
if (previous) {
if (index - moveBy >= 0) {
scrollTo(index - moveBy, false);
scrolledToIndex = index - moveBy;
} else {
scrollTo(0, false);
scrolledToIndex = 0;
}
} else if (index + moveBy < tabRefs.length) {
scrollTo(index + moveBy, false);
scrolledToIndex = index + moveBy;
} else {
scrollTo(tabRefs.length - 1, false);
scrolledToIndex = tabRefs.length - 1;
}
}
index = previous ? index + 1 : index - 1;
}
};
useEffect(function () {
var _tabRefs$activeIndex;
// if the active tab isn't visible scroll to it
if (overflow && tabRefs && (_tabRefs$activeIndex = tabRefs[activeIndex]) != null && _tabRefs$activeIndex.current && !isVisible(activeIndex)) scrollTo(activeIndex, true);
}, [overflow, activeIndex, tabRefs, isVisible, scrollTo]);
useEffect(function () {
// scroll focus item into view if it is not already visible
if (overflow && focusIndex !== -1 && !isVisible(focusIndex)) scrollTo(focusIndex, true);else if (overflow && focusIndex !== -1) {
// If the browser scrolled the focused item into view and
// the focusedTab is on the edge of the header container
// scroll slightly further to show the focusIndicator
var tabRect = tabRefs[focusIndex].current.getBoundingClientRect();
var headerRect = headerRef.current.getBoundingClientRect();
var amountHidden = 0;
if (tabRect.left >= headerRect.left && tabRect.right <= headerRect.right && tabRect.right + 2 >= headerRect.right) amountHidden = 2;else if (tabRect.right <= headerRect.right && tabRect.left >= headerRect.left && tabRect.left - 2 <= headerRect.left) amountHidden = -2;
headerRef.current.scrollBy({
left: amountHidden
});
}
}, [overflow, tabRefs, focusIndex, isVisible, scrollTo]);
useLayoutEffect(function () {
var onResize = function onResize() {
// check if tabs are overflowing
if (headerRef.current.scrollWidth > headerRef.current.offsetWidth) {
setOverflow(true);
} else setOverflow(false);
updateArrowState();
};
onResize();
window.addEventListener('resize', onResize);
return function () {
return window.removeEventListener('resize', onResize);
};
}, [tabRefs, disableLeftArrow, disableRightArrow, activeIndex, headerRef, overflow, updateArrowState]);
var getTabsContext = useCallback(function (index) {
var activateTab = function activateTab(nextIndex) {
sendAnalytics({
type: 'activateTab',
element: tabRefs[nextIndex].current
});
if (propsActiveIndex === undefined) {
setActiveIndex(nextIndex);
}
if (onActive) {
onActive(nextIndex);
}
};
return {
activeIndex: activeIndex,
active: activeIndex === index,
index: index,
ref: tabRefs[index],
onActivate: function onActivate() {
return activateTab(index);
},
setActiveContent: setActiveContent,
setActiveTitle: setActiveTitle,
setFocusIndex: setFocusIndex
};
}, [activeIndex, onActive, propsActiveIndex, sendAnalytics, tabRefs]);
var tabs = React.Children.map(children, function (child, index) {
return /*#__PURE__*/React.createElement(TabsContext.Provider, {
value: getTabsContext(index)
}, child ?
/*#__PURE__*/
// cloneElement is needed for backward compatibility with custom
// styled components that rely on props.active. We should reassess
// if it is still necessary in our next major release.
React.cloneElement(child, {
active: activeIndex === index
}) : child);
});
var tabsHeaderStyles = {};
if (theme.tabs.header && theme.tabs.header.border) {
var borderColor = theme.tabs.header.border.color || theme.global.control.border.color;
borderColor = normalizeColor(borderColor, theme);
tabsHeaderStyles.border = {
side: theme.tabs.header.border.side,
size: theme.tabs.header.border.size,
style: theme.tabs.header.border.style,
color: borderColor
};
}
var tabContentTitle = (activeTitle || '') + " " + format({
id: 'tabs.tabContents',
messages: messages
});
return /*#__PURE__*/React.createElement(StyledTabs, _extends({
ref: ref,
flex: flex,
responsive: responsive
}, passThemeFlag, rest, {
background: theme.tabs.background
}), /*#__PURE__*/React.createElement(Box, _extends({
alignSelf: alignControls || ((_theme$tabs$header3 = theme.tabs.header) == null ? void 0 : _theme$tabs$header3.alignSelf),
flex: false,
direction: overflow ? 'row' : 'column'
}, tabsHeaderStyles), overflow && /*#__PURE__*/React.createElement(Button, {
a11yTitle: "Previous Tab",
disabled: disableLeftArrow
// removed from tabIndex, button is redundant for keyboard users
,
tabIndex: -1,
onClick: function onClick() {
return moveByArrowKey('previous');
}
}, /*#__PURE__*/React.createElement(Box, {
pad: theme.tabs.header.previousButton.pad
}, /*#__PURE__*/React.createElement(PreviousIcon, {
color: disableLeftArrow ? theme.button.disabled.color : theme.global.colors.text
}))), /*#__PURE__*/React.createElement(StyledTabsHeader, _extends({
role: "tablist",
ref: headerRef,
direction: "row",
justify: overflow ? 'start' : justify,
flex: !!overflow,
wrap: false,
overflow: overflow ? 'hidden' : 'visible',
background: theme.tabs.header.background,
gap: theme.tabs.gap,
pad: overflow ? '2px' : undefined,
margin: overflow ? '-2px' : undefined
}, passThemeFlag), tabs), overflow && /*#__PURE__*/React.createElement(Button, {
a11yTitle: "Next Tab",
disabled: disableRightArrow
// removed from tabIndex, button is redundant for keyboard users
,
tabIndex: -1,
onClick: function onClick() {
return moveByArrowKey('next');
}
}, /*#__PURE__*/React.createElement(Box, {
pad: (_theme$tabs$header4 = theme.tabs.header) == null || (_theme$tabs$header4 = _theme$tabs$header4.nextButton) == null ? void 0 : _theme$tabs$header4.pad
}, /*#__PURE__*/React.createElement(NextIcon, {
color: disableRightArrow ? theme.button.disabled.color : theme.global.colors.text
})))), /*#__PURE__*/React.createElement(StyledTabPanel, _extends({
flex: flex,
"aria-label": tabContentTitle,
role: "tabpanel"
}, passThemeFlag), activeContent));
});
Tabs.displayName = 'Tabs';
Tabs.propTypes = TabsPropTypes;
export { Tabs };