2022-02-18 08:20:47 +00:00
|
|
|
import { cloneElement, useCallback, useEffect, useMemo, useState } from "react";
|
|
|
|
import PropTypes from "prop-types";
|
|
|
|
import styled from "styled-components";
|
2022-02-17 11:53:32 +00:00
|
|
|
|
2022-02-18 08:20:47 +00:00
|
|
|
import { ActiveTabIndicator } from "./ActiveTabIndicator";
|
|
|
|
import { usePrefixedTabIds, useTabsChildren } from "./hooks";
|
2022-02-17 11:53:32 +00:00
|
|
|
|
|
|
|
const Container = styled.div.attrs({
|
2022-03-01 15:49:06 +00:00
|
|
|
className: "tabs-container flex flex-col h-full",
|
2022-02-18 08:20:47 +00:00
|
|
|
})``;
|
2022-02-17 11:53:32 +00:00
|
|
|
|
|
|
|
const Header = styled.div.attrs({
|
2022-03-01 15:49:06 +00:00
|
|
|
className: "relative flex justify-start overflow-hidden grow-0 shrink-0",
|
2022-02-18 08:20:47 +00:00
|
|
|
})``;
|
2022-02-17 11:53:32 +00:00
|
|
|
|
|
|
|
const TabList = styled.div.attrs(({ variant }) => ({
|
2022-02-18 08:20:47 +00:00
|
|
|
role: "tablist",
|
2022-02-17 11:53:32 +00:00
|
|
|
className: `relative inline-grid grid-flow-col auto-cols-fr
|
2022-02-18 08:20:47 +00:00
|
|
|
${variant === "regular" ? "w-full sm:w-auto" : "w-full"}`,
|
|
|
|
}))``;
|
2022-02-17 11:53:32 +00:00
|
|
|
|
|
|
|
const Divider = styled.div.attrs({
|
2022-02-18 08:20:47 +00:00
|
|
|
"aria-hidden": "true",
|
|
|
|
className: "absolute bottom-0 w-screen border-b border-b-palette-200",
|
2022-02-17 11:53:32 +00:00
|
|
|
})`
|
|
|
|
right: calc(-100vw - 2px);
|
2022-02-18 08:20:47 +00:00
|
|
|
`;
|
2022-02-17 11:53:32 +00:00
|
|
|
|
2022-03-01 15:49:06 +00:00
|
|
|
const Body = styled.div.attrs({ className: "grow min-h-0" })``;
|
2022-02-17 11:53:32 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Besides documented props, it accepts all HMTL attributes a `<div>` element does.
|
|
|
|
*/
|
|
|
|
export const Tabs = ({ defaultTab, children, variant }) => {
|
2022-02-18 08:20:47 +00:00
|
|
|
const getTabId = usePrefixedTabIds();
|
|
|
|
const { tabs, panels, tabsRefs } = useTabsChildren(children, getTabId);
|
2022-03-07 11:09:51 +00:00
|
|
|
const defaultTabId = useMemo(() => {
|
|
|
|
const requestedTabIsPresent = tabs.find(({ props }) => props.id === defaultTab);
|
|
|
|
|
|
|
|
return getTabId(requestedTabIsPresent ? defaultTab : tabs[0].props.id);
|
|
|
|
}, [getTabId, defaultTab, tabs]);
|
2022-02-18 08:20:47 +00:00
|
|
|
const [activeTabId, setActiveTabId] = useState(defaultTabId);
|
|
|
|
const [activeTabRef, setActiveTabRef] = useState(tabsRefs[activeTabId]);
|
|
|
|
const isActive = (id) => id === activeTabId;
|
2022-02-17 11:53:32 +00:00
|
|
|
const onTabChange = useCallback(
|
|
|
|
(id) => {
|
2022-02-18 08:20:47 +00:00
|
|
|
setActiveTabId(id);
|
2022-02-17 11:53:32 +00:00
|
|
|
},
|
|
|
|
[setActiveTabId]
|
2022-02-18 08:20:47 +00:00
|
|
|
);
|
2022-02-17 11:53:32 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
// Refresh active tab indicator whenever active tab changes.
|
2022-02-18 08:20:47 +00:00
|
|
|
setActiveTabRef(tabsRefs[activeTabId]);
|
|
|
|
}, [setActiveTabRef, tabsRefs, activeTabId]);
|
2022-02-17 11:53:32 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Container>
|
|
|
|
<Header>
|
|
|
|
<TabList variant={variant}>
|
|
|
|
{tabs.map((tab) => {
|
2022-02-18 08:20:47 +00:00
|
|
|
const tabId = getTabId(tab.props.id);
|
2022-02-17 11:53:32 +00:00
|
|
|
|
|
|
|
return cloneElement(tab, {
|
|
|
|
ref: tabsRefs[tabId],
|
|
|
|
id: tabId,
|
|
|
|
variant,
|
|
|
|
active: isActive(tabId),
|
|
|
|
onClick: () => onTabChange(tabId),
|
2022-02-18 08:20:47 +00:00
|
|
|
});
|
2022-02-17 11:53:32 +00:00
|
|
|
})}
|
|
|
|
<Divider />
|
|
|
|
<ActiveTabIndicator tabRef={activeTabRef} />
|
|
|
|
</TabList>
|
|
|
|
</Header>
|
|
|
|
<Body>
|
|
|
|
{panels.map((panel) => {
|
2022-02-18 08:20:47 +00:00
|
|
|
const tabId = getTabId(panel.props.tabId);
|
2022-02-17 11:53:32 +00:00
|
|
|
|
|
|
|
return cloneElement(panel, {
|
|
|
|
...panel.props,
|
|
|
|
tabId,
|
|
|
|
active: isActive(tabId),
|
2022-02-18 08:20:47 +00:00
|
|
|
});
|
2022-02-17 11:53:32 +00:00
|
|
|
})}
|
|
|
|
</Body>
|
|
|
|
</Container>
|
2022-02-18 08:20:47 +00:00
|
|
|
);
|
|
|
|
};
|
2022-02-17 11:53:32 +00:00
|
|
|
|
|
|
|
Tabs.propTypes = {
|
|
|
|
/**
|
|
|
|
* Should include `<Tab />` and `<TabPanel />` components.
|
|
|
|
*/
|
|
|
|
children: PropTypes.node.isRequired,
|
|
|
|
/**
|
|
|
|
* ID of the `<Tab />` which should be open by default
|
|
|
|
*/
|
|
|
|
defaultTab: PropTypes.string,
|
|
|
|
/**
|
|
|
|
* `regular` (default) will make the tabs only take as much space as they need
|
|
|
|
*
|
|
|
|
* `fill` will make the tabs spread throughout the available width
|
|
|
|
*/
|
2022-02-18 08:20:47 +00:00
|
|
|
variant: PropTypes.oneOf(["regular", "fill"]),
|
|
|
|
};
|
2022-02-17 11:53:32 +00:00
|
|
|
|
|
|
|
Tabs.defaultProps = {
|
2022-02-18 08:20:47 +00:00
|
|
|
variant: "regular",
|
|
|
|
};
|