baseui
Version:
A React Component library implementing the Base design language
495 lines (470 loc) • 17.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.Tabs = Tabs;
var React = _interopRequireWildcard(require("react"));
var ReactIs = _interopRequireWildcard(require("react-is"));
var _reactUid = require("react-uid");
var _styles = require("../styles");
var _overrides = require("../helpers/overrides");
var _focusVisible = require("../utils/focusVisible");
var _constants = require("./constants");
var _styledComponents = require("./styled-components");
var _utils = require("./utils");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } /*
Copyright (c) Uber Technologies, Inc.
This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*/ /* global window */
const KEYBOARD_ACTION = {
next: 'next',
previous: 'previous'
};
// @ts-ignore
const getLayoutParams = (el, orientation) => {
if (!el) {
return {
length: 0,
distance: 0
};
}
// Note, we are using clientHeight/Width here, which excludes borders.
// This means borders won't be taken into account if someone adds borders
// through overrides. In that case you would use getBoundingClientRect
// which includes borders, but because it returns a fractional value the
// highlight is slightly misaligned every so often.
if ((0, _utils.isVertical)(orientation)) {
return {
length: el.clientHeight,
distance: el.offsetTop
};
} else {
return {
length: el.clientWidth,
distance: el.offsetLeft
};
}
};
// @ts-ignore
const scrollParentToCentreTarget = targetNode => {
const {
x: parentX,
y: parentY,
width: parentWidth,
height: parentHeight
} = targetNode.parentNode.getBoundingClientRect();
const {
x: childX,
y: childY,
width: childWidth,
height: childHeight
} = targetNode.getBoundingClientRect();
// get the position of the child centre, relative to parent
const childCentre = {
x: childX - parentX + childWidth / 2,
y: childY - parentY + childHeight / 2
};
// aim for the centre of the child to be the centre of the parent
const {
scrollLeft,
scrollTop
} = targetNode.parentNode;
const target = {
x: scrollLeft + childCentre.x - parentWidth / 2,
y: scrollTop + childCentre.y - parentHeight / 2
};
// ignore out of bounds, the browser will manage this for us
targetNode.parentNode.scroll(target.x, target.y);
};
// @ts-ignore
function RenderEnhancer({
Enhancer
}) {
if (typeof Enhancer === 'string') {
return Enhancer;
}
if (ReactIs.isValidElementType(Enhancer)) {
return /*#__PURE__*/React.createElement(Enhancer, null);
}
return Enhancer;
}
function Tabs({
activeKey = '0',
disabled = false,
children,
fill = _constants.FILL.intrinsic,
activateOnFocus = true,
onChange,
orientation = _constants.ORIENTATION.horizontal,
overrides = {},
renderAll = false,
// @ts-ignore
uid: customUid = null,
endEnhancer
}) {
// Create unique id prefix for this tabs component
const generatedUid = (0, _reactUid.useUID)();
const uid = customUid || generatedUid;
// Unpack overrides
const {
Root: RootOverrides,
TabList: TabListOverrides,
TabHighlight: TabHighlightOverrides,
TabBorder: TabBorderOverrides
} = overrides;
const [Root, RootProps] = (0, _overrides.getOverrides)(RootOverrides, _styledComponents.StyledRoot);
const [TabList, TabListProps] = (0, _overrides.getOverrides)(TabListOverrides, _styledComponents.StyledTabList);
const [TabHighlight, TabHighlightProps] = (0, _overrides.getOverrides)(TabHighlightOverrides, _styledComponents.StyledTabHighlight);
const [TabBorder, TabBorderProps] = (0, _overrides.getOverrides)(TabBorderOverrides, _styledComponents.StyledTabBorder);
const [EndEnhancerContainer, endEnhancerContainerProps] = (0, _overrides.getOverrides)(overrides.EndEnhancerContainer, _styledComponents.StyledEndEnhancerContainer);
const [TabBar, tabBarProps] = (0, _overrides.getOverrides)(overrides.TabBar, _styledComponents.StyledTabBar);
// Count key updates
// We disable a few things until after first mount:
// - the highlight animation, avoiding an initial slide-in
// - smooth scrolling active tab into view
const [keyUpdated, setKeyUpdated] = React.useState(0);
React.useEffect(() => {
setKeyUpdated(keyUpdated + 1);
}, [activeKey]);
// Positioning the highlight.
const activeTabRef = React.useRef();
const [highlightLayout, setHighlightLayout] = React.useState({
length: 0,
distance: 0
});
// Create a shared, memoized callback for tabs to call on resize.
const updateHighlight = React.useCallback(() => {
if (activeTabRef.current) {
setHighlightLayout(getLayoutParams(activeTabRef.current, orientation));
}
}, [activeTabRef.current, orientation]);
// Update highlight on key, orientation and children changes.
React.useEffect(updateHighlight, [activeTabRef.current, orientation, children]);
// Scroll active tab into view when the parent has scrollbar on mount and
// on key change (smooth scroll). Note, if the active key changes while
// the tab is not in view, the page will scroll it into view.
// TODO: replace with custom scrolling logic.
React.useEffect(() => {
// Flow needs this condition pulled out.
if (activeTabRef.current) {
if ((0, _utils.isHorizontal)(orientation) ?
// @ts-expect-error todo(flow->ts) maybe parentElement?
activeTabRef.current.parentNode.scrollWidth >
// @ts-expect-error todo(flow->ts) maybe parentElement?
activeTabRef.current.parentNode.clientWidth :
// @ts-expect-error todo(flow->ts) maybe parentElement?
activeTabRef.current.parentNode.scrollHeight >
// @ts-expect-error todo(flow->ts) maybe parentElement?
activeTabRef.current.parentNode.clientHeight) {
if (keyUpdated > 1) {
activeTabRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
});
} else {
scrollParentToCentreTarget(activeTabRef.current);
}
}
}
}, [activeTabRef.current]);
// Collect shared styling props
const sharedStylingProps = {
$orientation: orientation,
$fill: fill
};
// Helper for parsing directional keys
// TODO(WPT-6473): move to universal keycode aliases
const [, theme] = (0, _styles.useStyletron)();
const parseKeyDown = React.useCallback(event => {
if ((0, _utils.isHorizontal)(orientation)) {
if ((0, _utils.isRTL)(theme.direction)) {
switch (event.keyCode) {
case 39:
return KEYBOARD_ACTION.previous;
case 37:
return KEYBOARD_ACTION.next;
default:
return null;
}
} else {
switch (event.keyCode) {
case 37:
return KEYBOARD_ACTION.previous;
case 39:
return KEYBOARD_ACTION.next;
default:
return null;
}
}
} else {
switch (event.keyCode) {
case 38:
return KEYBOARD_ACTION.previous;
case 40:
return KEYBOARD_ACTION.next;
default:
return null;
}
}
}, [orientation, theme.direction]);
return /*#__PURE__*/React.createElement(Root, _extends({}, sharedStylingProps, RootProps), /*#__PURE__*/React.createElement(TabBar, _extends({
$hasEndEnhancer: Boolean(endEnhancer),
$orientation: orientation
}, tabBarProps), /*#__PURE__*/React.createElement(TabList, _extends({
"data-baseweb": "tab-list",
role: "tablist",
"aria-orientation": orientation
}, sharedStylingProps, TabListProps), React.Children.map(children, (child, index) => {
if (!child) return;
return /*#__PURE__*/React.createElement(InternalTab, _extends({
childKey: child.key,
childIndex: index,
activeKey: activeKey,
orientation: orientation,
activeTabRef: activeTabRef,
updateHighlight: updateHighlight,
parseKeyDown: parseKeyDown,
activateOnFocus: activateOnFocus,
uid: uid,
disabled: disabled,
sharedStylingProps: sharedStylingProps,
onChange: onChange
}, child.props));
}), /*#__PURE__*/React.createElement(TabHighlight, _extends({
"data-baseweb": "tab-highlight",
$length: highlightLayout.length,
$distance: highlightLayout.distance
// This avoids the tab sliding in from the side on mount
,
$animate: keyUpdated > 1,
"aria-hidden": "true",
role: "presentation"
}, sharedStylingProps, TabHighlightProps))), orientation === _constants.ORIENTATION.horizontal && endEnhancer !== null && endEnhancer !== undefined && /*#__PURE__*/React.createElement(EndEnhancerContainer, _extends({}, endEnhancerContainerProps, {
$orientation: orientation
}), /*#__PURE__*/React.createElement(RenderEnhancer, {
Enhancer: endEnhancer
}))), /*#__PURE__*/React.createElement(TabBorder, _extends({
"data-baseweb": "tab-border",
"aria-hidden": "true",
role: "presentation"
}, sharedStylingProps, TabBorderProps)), React.Children.map(children, (child, index) => {
if (!child) return;
return /*#__PURE__*/React.createElement(InternalTabPanel, _extends({
childKey: child.key,
childIndex: index,
activeKey: activeKey,
uid: uid,
sharedStylingProps: sharedStylingProps,
renderAll: renderAll
}, child.props));
}));
}
function InternalTab({
// @ts-ignore
childKey,
// @ts-ignore
childIndex,
// @ts-ignore
activeKey,
// @ts-ignore
orientation,
// @ts-ignore
activeTabRef,
// @ts-ignore
updateHighlight,
// @ts-ignore
parseKeyDown,
// @ts-ignore
activateOnFocus,
// @ts-ignore
uid,
// @ts-ignore
disabled,
// @ts-ignore
sharedStylingProps,
// @ts-ignore
onChange,
...props
}) {
const key = childKey || String(childIndex);
const isActive = key == activeKey;
const {
artwork: Artwork,
overrides = {},
tabRef,
onClick,
title,
...restProps
} = props;
// A way to share our internal activeTabRef via the "tabRef" prop.
const ref = React.useRef();
React.useImperativeHandle(tabRef, () => {
return isActive ? activeTabRef.current : ref.current;
});
// Track tab dimensions in a ref after each render
// This is used to compare params when the resize observer fires
const tabLayoutParams = React.useRef({
length: 0,
distance: 0
});
React.useEffect(() => {
tabLayoutParams.current = getLayoutParams(isActive ? activeTabRef.current : ref.current, orientation);
});
// We need to potentially update the active tab highlight when the width or
// placement changes for a tab so we listen for resize updates in each tab.
React.useEffect(() => {
if (window.ResizeObserver) {
const observer = new window.ResizeObserver(entries => {
if (entries[0] && entries[0].target) {
const tabLayoutParamsAfterResize = getLayoutParams(entries[0].target, orientation);
if (tabLayoutParamsAfterResize.length !== tabLayoutParams.current.length || tabLayoutParamsAfterResize.distance !== tabLayoutParams.current.distance) {
updateHighlight();
}
}
});
observer.observe(isActive ? activeTabRef.current : ref.current);
return () => {
observer.disconnect();
};
}
}, [activeKey, orientation]);
React.useEffect(updateHighlight, [title]);
// Collect overrides
const {
Tab: TabOverrides,
ArtworkContainer: ArtworkContainerOverrides
} = overrides;
const [Tab, TabProps] = (0, _overrides.getOverrides)(TabOverrides, _styledComponents.StyledTab);
const [ArtworkContainer, ArtworkContainerProps] = (0, _overrides.getOverrides)(ArtworkContainerOverrides, _styledComponents.StyledArtworkContainer);
// Keyboard focus styling
const [focusVisible, setFocusVisible] = React.useState(false);
const handleFocus = React.useCallback(event => {
if ((0, _focusVisible.isFocusVisible)(event)) {
setFocusVisible(true);
}
}, []);
const handleBlur = React.useCallback(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
event => {
if (focusVisible !== false) {
setFocusVisible(false);
}
}, [focusVisible]);
// Keyboard focus management
// @ts-expect-error todo(flow->ts): deps are required
const handleKeyDown = React.useCallback(event => {
// WAI-ARIA 1.1
// https://www.w3.org/TR/wai-aria-practices-1.1/#tabpanel
// We use directional keys to iterate focus through Tabs.
// Find all tabs eligible for focus
const availableTabs = [...event.target.parentNode.childNodes].filter(node => !node.disabled && node.getAttribute('role') === 'tab');
// Exit early if there are no other tabs available
if (availableTabs.length === 1) return;
// Find tab to focus, looping to start/end of list if necessary
const currentTabIndex = availableTabs.indexOf(event.target);
const action = parseKeyDown(event);
if (action) {
let nextTab;
if (action === KEYBOARD_ACTION.previous) {
if (availableTabs[currentTabIndex - 1]) {
nextTab = availableTabs[currentTabIndex - 1];
} else {
nextTab = availableTabs[availableTabs.length - 1];
}
} else if (action === KEYBOARD_ACTION.next) {
if (availableTabs[currentTabIndex + 1]) {
nextTab = availableTabs[currentTabIndex + 1];
} else {
nextTab = availableTabs[0];
}
}
if (nextTab) {
// Focus the tab
nextTab.focus();
// Optionally activate the tab
if (activateOnFocus) {
nextTab.click();
}
}
// Prevent default page scroll when in vertical orientation
if ((0, _utils.isVertical)(orientation)) {
event.preventDefault();
}
}
});
return /*#__PURE__*/React.createElement(Tab, _extends({
"data-baseweb": "tab",
key: key,
id: (0, _utils.getTabId)(uid, key),
role: "tab",
onKeyDown: handleKeyDown,
"aria-selected": isActive,
"aria-controls": (0, _utils.getTabPanelId)(uid, key),
tabIndex: isActive ? '0' : '-1',
ref: isActive ? activeTabRef : ref,
disabled: !isActive && disabled,
type: "button" // so it doesn't trigger a submit when used inside forms
,
$focusVisible: focusVisible,
$isActive: isActive
}, sharedStylingProps, restProps, TabProps, {
// @ts-ignore
onClick: event => {
if (typeof onChange === 'function') onChange({
activeKey: key
});
if (typeof onClick === 'function') onClick(event);
},
onFocus: (0, _focusVisible.forkFocus)({
...restProps,
...TabProps
}, handleFocus),
onBlur: (0, _focusVisible.forkBlur)({
...restProps,
...TabProps
}, handleBlur)
}), Artwork ? /*#__PURE__*/React.createElement(ArtworkContainer, _extends({
"data-baseweb": "artwork-container"
}, sharedStylingProps, ArtworkContainerProps), /*#__PURE__*/React.createElement(Artwork, {
size: 20,
color: "contentPrimary"
})) : null, title ? title : key);
}
function InternalTabPanel({
// @ts-ignore
childKey,
// @ts-ignore
childIndex,
// @ts-ignore
activeKey,
// @ts-ignore
uid,
// @ts-ignore
sharedStylingProps,
// @ts-ignore
renderAll,
...props
}) {
const key = childKey || String(childIndex);
const isActive = key == activeKey;
const {
overrides = {},
children
} = props;
const {
TabPanel: TabPanelOverrides
} = overrides;
const [TabPanel, TabPanelProps] = (0, _overrides.getOverrides)(TabPanelOverrides, _styledComponents.StyledTabPanel);
return /*#__PURE__*/React.createElement(TabPanel, _extends({
"data-baseweb": "tab-panel",
key: key,
role: "tabpanel",
id: (0, _utils.getTabPanelId)(uid, key),
"aria-labelledby": (0, _utils.getTabId)(uid, key),
hidden: !isActive
}, sharedStylingProps, TabPanelProps), isActive || renderAll ? children : null);
}