130 lines
3.6 KiB
JavaScript
130 lines
3.6 KiB
JavaScript
// @flow
|
|
import React, { Fragment, useState, useRef, useContext, useLayoutEffect, createContext } from 'react';
|
|
import {
|
|
Tabs as ReachTabs,
|
|
Tab as ReachTab,
|
|
TabList as ReachTabList,
|
|
TabPanels as ReachTabPanels,
|
|
TabPanel as ReachTabPanel,
|
|
} from '@reach/tabs';
|
|
import classnames from 'classnames';
|
|
import { useRect } from '@reach/rect';
|
|
|
|
// Tabs are a compound component
|
|
// The components are used individually, but they will still interact and share state
|
|
// When using, at a minimum you must arrange the components in this pattern
|
|
// When the <Tab> at index 0 is active, the TabPanel at index 0 will be displayed
|
|
//
|
|
// <Tabs onChange={...} index={...}>
|
|
// <TabList>
|
|
// <Tab>Tab label 1</Tab>
|
|
// <Tab>Tab label 2</Tab>
|
|
// ...
|
|
// </TabList>
|
|
// <TabPanels>
|
|
// <TabPanel>Content for Tab 1</TabPanel>
|
|
// <TabPanel>Content for Tab 2</TabPanel>
|
|
// ...
|
|
// </TabPanels>
|
|
// </Tabs>
|
|
//
|
|
// the base @reach/tabs components handle all the focus/accessibilty labels
|
|
// We're just adding some styling
|
|
|
|
type TabsProps = {
|
|
index?: number,
|
|
onChange?: number => void,
|
|
children: Array<React$Node>,
|
|
};
|
|
|
|
// Use context so child TabPanels can set the active tab, which is kept in Tabs' state
|
|
const AnimatedContext = createContext<any>();
|
|
|
|
function Tabs(props: TabsProps) {
|
|
// Store the position of the selected Tab so we can animate the "active" bar to its position
|
|
const [selectedRect, setSelectedRect] = useState(null);
|
|
|
|
// Create a ref of the parent element so we can measure the relative "left" for the bar for the child Tab's
|
|
const tabsRef = useRef();
|
|
const tabsRect = useRect(tabsRef);
|
|
|
|
const tabLabels = props.children[0];
|
|
const tabContent = props.children[1];
|
|
|
|
return (
|
|
<AnimatedContext.Provider value={setSelectedRect}>
|
|
<ReachTabs className="tabs" {...props} ref={tabsRef}>
|
|
{tabLabels}
|
|
|
|
<div
|
|
className="tab__divider"
|
|
style={{
|
|
left: selectedRect && tabsRect && selectedRect.left - tabsRect.left,
|
|
width: selectedRect && selectedRect.width,
|
|
}}
|
|
/>
|
|
|
|
{tabContent}
|
|
</ReachTabs>
|
|
</AnimatedContext.Provider>
|
|
);
|
|
}
|
|
|
|
//
|
|
// The wrapper for the list of tab labels that users can click
|
|
type TabListProps = {
|
|
className?: string,
|
|
};
|
|
function TabList(props: TabListProps) {
|
|
const { className, ...rest } = props;
|
|
return <ReachTabList className={classnames('tabs__list', className)} {...rest} />;
|
|
}
|
|
|
|
//
|
|
// The links that users click
|
|
// Accesses `setSelectedRect` from context to set itself as active if needed
|
|
// Flow doesn't understand we don't have to pass it in ourselves
|
|
type TabProps = {
|
|
isSelected?: Boolean,
|
|
};
|
|
function Tab(props: TabProps) {
|
|
// @reach/tabs provides an `isSelected` prop
|
|
// We could also useContext to read it manually
|
|
const { isSelected } = props;
|
|
|
|
// Each tab measures itself
|
|
const ref = useRef();
|
|
const rect = useRect(ref, isSelected);
|
|
|
|
// and calls up to the parent when it becomes selected
|
|
// we useLayoutEffect to avoid flicker
|
|
const setSelectedRect = useContext(AnimatedContext);
|
|
useLayoutEffect(() => {
|
|
if (isSelected) setSelectedRect(rect);
|
|
}, [isSelected, rect, setSelectedRect]);
|
|
|
|
return <ReachTab ref={ref} {...props} className="tab" />;
|
|
}
|
|
|
|
//
|
|
// The wrapper for TabPanel
|
|
type TabPanelsProps = {
|
|
header?: React$Node,
|
|
};
|
|
function TabPanels(props: TabPanelsProps) {
|
|
const { header, ...rest } = props;
|
|
return (
|
|
<Fragment>
|
|
{header}
|
|
<ReachTabPanels {...rest} />
|
|
</Fragment>
|
|
);
|
|
}
|
|
|
|
//
|
|
// The wrapper for content when it's associated Tab is selected
|
|
function TabPanel(props: any) {
|
|
return <ReachTabPanel className="tab__panel" {...props} />;
|
|
}
|
|
|
|
export { Tabs, TabList, Tab, TabPanels, TabPanel };
|