@shopify/polaris
Version:
Shopify’s admin product component library
436 lines (433 loc) • 14.5 kB
JavaScript
import React, { useRef, useReducer, useEffect, useCallback } from 'react';
import { ChevronDownIcon, PlusIcon } from '@shopify/polaris-icons';
import { classNames } from '../../utilities/css.js';
import { useBreakpoints } from '../../utilities/breakpoints.js';
import { usePrevious } from '../../utilities/use-previous.js';
import { getVisibleAndHiddenTabIndices } from './utilities.js';
import styles from './Tabs.css.js';
import { Tab } from './components/Tab/Tab.js';
import { Panel } from './components/Panel/Panel.js';
import { List } from './components/List/List.js';
import { CreateViewModal } from './components/CreateViewModal/CreateViewModal.js';
import { TabMeasurer } from './components/TabMeasurer/TabMeasurer.js';
import { useI18n } from '../../utilities/i18n/hooks.js';
import { Text } from '../Text/Text.js';
import { Icon } from '../Icon/Icon.js';
import { UnstyledButton } from '../UnstyledButton/UnstyledButton.js';
import { Box } from '../Box/Box.js';
import { Popover } from '../Popover/Popover.js';
import { Tooltip } from '../Tooltip/Tooltip.js';
const CREATE_NEW_VIEW_ID = 'create-new-view';
const Tabs = ({
tabs,
children,
selected,
newViewAccessibilityLabel,
canCreateNewView,
disabled,
onCreateNewView,
onSelect,
fitted,
disclosureText,
disclosureZIndexOverride
}) => {
const i18n = useI18n();
const {
mdDown
} = useBreakpoints();
const scrollRef = useRef(null);
const wrapRef = useRef(null);
const selectedTabRef = useRef(null);
const [state, setState] = useReducer((data, partialData) => {
return {
...data,
...partialData
};
}, {
disclosureWidth: 0,
containerWidth: Infinity,
tabWidths: [],
visibleTabs: [],
hiddenTabs: [],
showDisclosure: false,
tabToFocus: -1,
isNewViewModalActive: false,
modalSubmitted: false,
isTabsFocused: false,
isTabPopoverOpen: false,
isTabModalOpen: false
});
const {
tabToFocus,
visibleTabs,
hiddenTabs,
showDisclosure,
isNewViewModalActive,
modalSubmitted,
disclosureWidth,
tabWidths,
containerWidth,
isTabsFocused,
isTabModalOpen,
isTabPopoverOpen
} = state;
const prevModalOpen = usePrevious(isTabModalOpen);
const prevPopoverOpen = usePrevious(isTabPopoverOpen);
useEffect(() => {
const hasModalClosed = prevModalOpen && !isTabModalOpen;
const hasPopoverClosed = prevPopoverOpen && !isTabPopoverOpen;
if (hasModalClosed) {
setState({
isTabsFocused: true,
tabToFocus: selected
});
} else if (hasPopoverClosed && !isTabModalOpen) {
setState({
isTabsFocused: true,
tabToFocus: selected
});
}
}, [prevPopoverOpen, isTabPopoverOpen, prevModalOpen, isTabModalOpen, selected, tabToFocus]);
const handleTogglePopover = useCallback(isOpen => setState({
isTabPopoverOpen: isOpen
}), []);
const handleToggleModal = useCallback(isOpen => setState({
isTabModalOpen: isOpen
}), []);
const handleCloseNewViewModal = () => {
setState({
isNewViewModalActive: false
});
};
const handleSaveNewViewModal = async value => {
if (!onCreateNewView) {
return false;
}
const hasExecuted = await onCreateNewView?.(value);
if (hasExecuted) {
setState({
modalSubmitted: true
});
}
return hasExecuted;
};
const handleClickNewTab = () => {
setState({
isNewViewModalActive: true
});
};
const handleTabClick = useCallback(id => {
const tab = tabs.find(aTab => aTab.id === id);
if (tab == null) {
return null;
}
const selectedIndex = tabs.indexOf(tab);
onSelect?.(selectedIndex);
}, [tabs, onSelect]);
const renderTabMarkup = useCallback((tab, index) => {
const handleClick = () => {
handleTabClick(tab.id);
tab.onAction?.();
};
const viewNames = tabs.map(({
content
}) => content);
const tabPanelID = tab.panelID || `${tab.id}-panel`;
return /*#__PURE__*/React.createElement(Tab, Object.assign({}, tab, {
key: `${index}-${tab.id}`,
id: tab.id,
panelID: children ? tabPanelID : undefined,
disabled: disabled || tab.disabled,
siblingTabHasFocus: tabToFocus > -1,
focused: index === tabToFocus,
selected: index === selected,
onAction: handleClick,
accessibilityLabel: tab.accessibilityLabel,
url: tab.url,
content: tab.content,
onToggleModal: handleToggleModal,
onTogglePopover: handleTogglePopover,
viewNames: viewNames,
disclosureZIndexOverride: disclosureZIndexOverride,
ref: index === selected ? selectedTabRef : null
}));
}, [disabled, tabs, children, selected, tabToFocus, disclosureZIndexOverride, handleTabClick, handleToggleModal, handleTogglePopover]);
const handleFocus = useCallback(event => {
const target = event.target;
const isItem = target.classList.contains(styles.Item);
const isInNaturalDOMOrder = target.closest(`[data-tabs-focus-catchment]`) || isItem;
const isDisclosureActivator = target.classList.contains(styles.DisclosureActivator);
if (isDisclosureActivator || !isInNaturalDOMOrder) {
return;
}
setState({
isTabsFocused: true
});
}, []);
const handleBlur = useCallback(event => {
const target = event.target;
const relatedTarget = event.relatedTarget;
const isInNaturalDOMOrder = relatedTarget?.closest?.(`.${styles.Tabs}`);
const targetIsATab = target?.classList?.contains?.(styles.Tab);
const focusReceiverIsAnItem = relatedTarget?.classList.contains(styles.Item);
if (!relatedTarget && !isTabModalOpen && !targetIsATab && !focusReceiverIsAnItem) {
setState({
tabToFocus: -1
});
return;
}
if (!isInNaturalDOMOrder && !isTabModalOpen && !targetIsATab && !focusReceiverIsAnItem) {
setState({
tabToFocus: -1
});
return;
}
setState({
isTabsFocused: false
});
}, [isTabModalOpen]);
const handleKeyDown = event => {
if (isTabPopoverOpen || isTabModalOpen || isNewViewModalActive) {
return;
}
const {
key
} = event;
if (key === 'ArrowLeft' || key === 'ArrowRight') {
event.preventDefault();
event.stopPropagation();
}
};
useEffect(() => {
const {
visibleTabs,
hiddenTabs
} = getVisibleAndHiddenTabIndices(tabs, selected, disclosureWidth, tabWidths, containerWidth);
setState({
visibleTabs,
hiddenTabs
});
}, [containerWidth, disclosureWidth, tabs, selected, tabWidths, setState]);
const moveToSelectedTab = useCallback(() => {
const activeButton = selectedTabRef.current?.querySelector(`.${styles['Tab-active']}`);
if (activeButton) {
moveToActiveTab(activeButton.offsetLeft);
}
}, []);
useEffect(() => {
if (mdDown) {
moveToSelectedTab();
}
}, [moveToSelectedTab, selected, mdDown]);
useEffect(() => {
if (isTabsFocused && !showDisclosure) {
const tabToFocus = selected;
setState({
tabToFocus
});
}
}, [isTabsFocused, selected, setState, showDisclosure]);
const handleKeyPress = event => {
const {
showDisclosure,
visibleTabs,
hiddenTabs,
tabToFocus,
isNewViewModalActive
} = state;
if (isTabModalOpen || isTabPopoverOpen || isNewViewModalActive) {
return;
}
const key = event.key;
const tabsArrayInOrder = showDisclosure || mdDown ? visibleTabs.concat(hiddenTabs) : [...visibleTabs];
let newFocus = tabsArrayInOrder.indexOf(tabToFocus);
if (key === 'ArrowRight') {
newFocus += 1;
if (newFocus === tabsArrayInOrder.length) {
newFocus = 0;
}
}
if (key === 'ArrowLeft') {
if (newFocus === -1 || newFocus === 0) {
newFocus = tabsArrayInOrder.length - 1;
} else {
newFocus -= 1;
}
}
const buttonToFocus = tabsArrayInOrder[newFocus];
if (buttonToFocus != null) {
setState({
tabToFocus: buttonToFocus
});
}
};
const handleDisclosureActivatorClick = () => {
setState({
showDisclosure: !showDisclosure,
tabToFocus: hiddenTabs[0]
});
};
const handleClose = () => {
setState({
showDisclosure: false
});
};
const handleMeasurement = useCallback(measurements => {
const {
hiddenTabWidths: tabWidths,
containerWidth,
disclosureWidth
} = measurements;
const {
visibleTabs,
hiddenTabs
} = getVisibleAndHiddenTabIndices(tabs, selected, disclosureWidth, tabWidths, containerWidth);
setState({
visibleTabs,
hiddenTabs,
disclosureWidth,
containerWidth,
tabWidths
});
}, [tabs, selected, setState]);
const handleListTabClick = id => {
handleTabClick(id);
handleClose();
setState({
isTabsFocused: true
});
};
const moveToActiveTab = offsetLeft => {
setTimeout(() => {
if (scrollRef.current && typeof scrollRef.current.scroll === 'function') {
const scrollRefOffset = wrapRef?.current?.offsetLeft || 0;
scrollRef?.current?.scroll({
left: offsetLeft - scrollRefOffset
});
}
}, 0);
};
const createViewA11yLabel = newViewAccessibilityLabel || i18n.translate('Polaris.Tabs.newViewAccessibilityLabel');
const tabsToShow = mdDown ? [...visibleTabs, ...hiddenTabs] : visibleTabs;
const tabsMarkup = tabsToShow.sort((tabA, tabB) => tabA - tabB).filter(tabIndex => tabs[tabIndex]).map(tabIndex => renderTabMarkup(tabs[tabIndex], tabIndex));
const disclosureActivatorVisible = visibleTabs.length < tabs.length && !mdDown;
const classname = classNames(styles.Tabs, fitted && styles.fitted, disclosureActivatorVisible && styles.fillSpace);
const wrapperClassNames = classNames(styles.Wrapper, canCreateNewView && styles.WrapperWithNewButton);
const disclosureTabClassName = classNames(styles.DisclosureTab, disclosureActivatorVisible && styles['DisclosureTab-visible']);
const disclosureButtonClassName = classNames(styles.DisclosureActivator);
const disclosureButtonContent = /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Text, {
as: "span",
variant: "bodySm",
fontWeight: "medium"
}, disclosureText ?? i18n.translate('Polaris.Tabs.toggleTabsLabel')), /*#__PURE__*/React.createElement("div", {
className: classNames(styles.IconWrap, disclosureActivatorVisible && showDisclosure && styles['IconWrap-open'])
}, /*#__PURE__*/React.createElement(Icon, {
source: ChevronDownIcon,
tone: "subdued"
})));
const disclosureButton = /*#__PURE__*/React.createElement(UnstyledButton, {
type: "button",
className: disclosureButtonClassName,
onClick: handleDisclosureActivatorClick,
"aria-label": disclosureText ?? i18n.translate('Polaris.Tabs.toggleTabsLabel'),
disabled: disabled
}, disclosureButtonContent);
const activator = disclosureButton;
const disclosureTabs = hiddenTabs.map(tabIndex => tabs[tabIndex]);
const viewNames = tabs.map(({
content
}) => content);
const tabMeasurer = /*#__PURE__*/React.createElement(TabMeasurer, {
tabToFocus: tabToFocus,
activator: activator,
selected: selected,
tabs: tabs,
siblingTabHasFocus: tabToFocus > -1,
handleMeasurement: handleMeasurement
});
const newTab = /*#__PURE__*/React.createElement(Tab, {
id: CREATE_NEW_VIEW_ID,
content: createViewA11yLabel,
actions: [],
onAction: handleClickNewTab,
onFocus: () => {
if (modalSubmitted) {
setState({
tabToFocus: selected,
modalSubmitted: false
});
}
},
icon: /*#__PURE__*/React.createElement(Icon, {
source: PlusIcon,
accessibilityLabel: createViewA11yLabel
}),
disabled: disabled,
onTogglePopover: handleTogglePopover,
onToggleModal: handleToggleModal,
tabIndexOverride: 0
});
const panelMarkup = children ? tabs.map((_tab, index) => {
return selected === index ? /*#__PURE__*/React.createElement(Panel, {
id: tabs[index].panelID || `${tabs[index].id}-panel`,
tabID: tabs[index].id,
key: tabs[index].id
}, children) : /*#__PURE__*/React.createElement(Panel, {
id: tabs[index].panelID || `${tabs[index].id}-panel`,
tabID: tabs[index].id,
key: tabs[index].id,
hidden: true
});
}) : null;
return /*#__PURE__*/React.createElement("div", {
className: styles.Outer
}, /*#__PURE__*/React.createElement(Box, {
padding: {
md: '200'
}
}, tabMeasurer, /*#__PURE__*/React.createElement("div", {
className: wrapperClassNames,
ref: scrollRef
}, /*#__PURE__*/React.createElement("div", {
className: styles.ButtonWrapper,
ref: wrapRef
}, /*#__PURE__*/React.createElement("ul", {
role: tabsMarkup.length > 0 ? 'tablist' : undefined,
className: classname,
onFocus: handleFocus,
onBlur: handleBlur,
onKeyDown: handleKeyDown,
onKeyUp: handleKeyPress,
"data-tabs-focus-catchment": true
}, tabsMarkup, mdDown || tabsToShow.length === 0 ? null : /*#__PURE__*/React.createElement("li", {
className: disclosureTabClassName,
role: "presentation"
}, /*#__PURE__*/React.createElement(Popover, {
preferredPosition: "below",
preferredAlignment: "left",
activator: activator,
active: disclosureActivatorVisible && showDisclosure,
onClose: handleClose,
autofocusTarget: "first-node",
zIndexOverride: disclosureZIndexOverride
}, /*#__PURE__*/React.createElement(List, {
focusIndex: hiddenTabs.indexOf(tabToFocus),
disclosureTabs: disclosureTabs,
onClick: handleListTabClick,
onKeyPress: handleKeyPress
})))), canCreateNewView && tabsToShow.length > 0 ? /*#__PURE__*/React.createElement("div", {
className: styles.NewTab
}, /*#__PURE__*/React.createElement(CreateViewModal, {
open: isNewViewModalActive,
onClose: handleCloseNewViewModal,
onClickPrimaryAction: handleSaveNewViewModal,
viewNames: viewNames,
activator: disabled ? newTab : /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement(Tooltip, {
content: i18n.translate('Polaris.Tabs.newViewTooltip'),
preferredPosition: "above",
hoverDelay: 400,
zIndexOverride: disclosureZIndexOverride
}, newTab))
})) : null))), panelMarkup);
};
export { Tabs };