bigbuffet-rw/app/soapbox/components/ui/tabs/tabs.tsx

179 lines
4.4 KiB
TypeScript
Raw Normal View History

2022-03-21 11:09:01 -07:00
import { useRect } from '@reach/rect';
import {
Tabs as ReachTabs,
TabList as ReachTabList,
Tab as ReachTab,
useTabsContext,
} from '@reach/tabs';
import classNames from 'classnames';
import * as React from 'react';
2022-03-22 05:42:26 -07:00
import { useHistory } from 'react-router-dom';
2022-03-21 11:09:01 -07:00
2022-04-28 14:29:15 -07:00
import Counter from '../counter/counter';
2022-03-21 11:09:01 -07:00
import './tabs.css';
const HORIZONTAL_PADDING = 8;
const AnimatedContext = React.createContext(null);
interface IAnimatedInterface {
/** Callback when a tab is chosen. */
2022-03-21 11:09:01 -07:00
onChange(index: number): void,
/** Default tab index. */
2022-03-21 11:09:01 -07:00
defaultIndex: number
}
/** Tabs with a sliding active state. */
2022-03-21 11:09:01 -07:00
const AnimatedTabs: React.FC<IAnimatedInterface> = ({ children, ...rest }) => {
const [activeRect, setActiveRect] = React.useState(null);
const ref = React.useRef();
const rect = useRect(ref);
// @ts-ignore
2022-03-21 11:09:01 -07:00
const top: number = (activeRect && activeRect.bottom) - (rect && rect.top);
// @ts-ignore
2022-03-21 11:09:01 -07:00
const width: number = activeRect && activeRect.width - HORIZONTAL_PADDING * 2;
// @ts-ignore
2022-03-21 11:09:01 -07:00
const left: number = (activeRect && activeRect.left) - (rect && rect.left) + HORIZONTAL_PADDING;
return (
// @ts-ignore
2022-03-21 11:09:01 -07:00
<AnimatedContext.Provider value={setActiveRect}>
<ReachTabs
{...rest}
// @ts-ignore
ref={ref}
>
2022-03-21 11:09:01 -07:00
<div
2022-05-04 07:50:53 -07:00
className='w-full h-[3px] bg-primary-200 dark:bg-slate-700 absolute'
2022-03-21 11:09:01 -07:00
style={{ top }}
/>
<div
className={classNames('absolute h-[3px] bg-primary-600 transition-all duration-200', {
'hidden': top <= 0,
})}
style={{ left, top, width }}
/>
{children}
</ReachTabs>
</AnimatedContext.Provider>
);
};
interface IAnimatedTab {
/** ARIA role. */
2022-03-21 11:09:01 -07:00
role: 'button',
/** Element to represent the tab. */
2022-03-21 11:09:01 -07:00
as: 'a' | 'button',
/** Route to visit when the tab is chosen. */
2022-03-21 11:09:01 -07:00
href?: string,
/** Tab title text. */
2022-03-21 11:09:01 -07:00
title: string,
/** Index value of the tab. */
2022-03-21 11:09:01 -07:00
index: number
}
/** A single animated tab. */
2022-03-21 11:09:01 -07:00
const AnimatedTab: React.FC<IAnimatedTab> = ({ index, ...props }) => {
// get the currently selected index from useTabsContext
const { selectedIndex } = useTabsContext();
const isSelected: boolean = selectedIndex === index;
// measure the size of our element, only listen to rect if active
const ref = React.useRef();
const rect = useRect(ref, { observe: isSelected });
// get the style changing function from context
const setActiveRect = React.useContext(AnimatedContext);
// callup to set styles whenever we're active
React.useLayoutEffect(() => {
if (isSelected) {
// @ts-ignore
2022-03-21 11:09:01 -07:00
setActiveRect(rect);
}
}, [isSelected, rect, setActiveRect]);
return (
// @ts-ignore
2022-03-21 11:09:01 -07:00
<ReachTab ref={ref} {...props} />
);
};
/** Structure to represent a tab. */
export type Item = {
/** Tab text. */
2022-04-29 15:59:30 -07:00
text: React.ReactNode,
/** Tab tooltip text. */
2022-03-21 11:09:01 -07:00
title?: string,
/** URL to visit when the tab is selected. */
2022-03-21 11:09:01 -07:00
href?: string,
/** Route to visit when the tab is selected. */
2022-03-21 11:09:01 -07:00
to?: string,
/** Callback when the tab is selected. */
2022-03-21 11:09:01 -07:00
action?: () => void,
/** Display a counter over the tab. */
2022-04-28 13:28:08 -07:00
count?: number,
/** Unique name for this tab. */
2022-03-21 11:09:01 -07:00
name: string
}
2022-03-22 05:42:26 -07:00
interface ITabs {
/** Array of structured tab items. */
2022-03-21 11:09:01 -07:00
items: Item[],
/** Name of the active tab item. */
2022-03-21 11:09:01 -07:00
activeItem: string,
}
/** Animated tabs component. */
2022-03-22 05:42:26 -07:00
const Tabs = ({ items, activeItem }: ITabs) => {
2022-03-21 11:09:01 -07:00
const defaultIndex = items.findIndex(({ name }) => name === activeItem);
2022-03-22 05:42:26 -07:00
const history = useHistory();
2022-03-21 11:09:01 -07:00
const onChange = (selectedIndex: number) => {
const item = items[selectedIndex];
if (typeof item.action === 'function') {
item.action();
} else if (item.to) {
history.push(item.to);
}
};
const renderItem = (item: Item, idx: number) => {
2022-04-28 13:28:08 -07:00
const { name, text, title, count } = item;
2022-03-21 11:09:01 -07:00
return (
<AnimatedTab
key={name}
as='button'
role='button'
// @ts-ignore
2022-03-21 11:09:01 -07:00
title={title}
index={idx}
>
2022-04-28 13:28:08 -07:00
<div className='relative'>
{count ? (
2022-04-28 14:29:15 -07:00
<span className='absolute -top-2 left-full ml-1'>
<Counter count={count} />
2022-04-28 13:28:08 -07:00
</span>
) : null}
{text}
</div>
2022-03-21 11:09:01 -07:00
</AnimatedTab>
);
};
return (
<AnimatedTabs onChange={onChange} defaultIndex={defaultIndex}>
<ReachTabList>
{items.map((item, i) => renderItem(item, i))}
</ReachTabList>
</AnimatedTabs>
);
};
2022-03-22 05:42:26 -07:00
export default Tabs;