Skip to content

Instantly share code, notes, and snippets.

@isBatak
Created October 15, 2020 12:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save isBatak/2ed2e4ffd57bbea62a6872c46dae0aa7 to your computer and use it in GitHub Desktop.
Save isBatak/2ed2e4ffd57bbea62a6872c46dae0aa7 to your computer and use it in GitHub Desktop.
Tabs
import React, { createContext, useState, useMemo, useContext, cloneElement, isValidElement, FC } from 'react';
import useConstant from '../../../hooks/useConstant';
type TabStateContext = [number, React.Dispatch<React.SetStateAction<number>>];
interface ITabsProps {
state?: TabStateContext;
}
interface IElements {
tabs: number;
panels: number;
}
const TabsState = createContext<TabStateContext | null>(null);
const Elements = createContext<IElements | null>(null);
export const Tabs: FC<ITabsProps> = ({ state: outerState, children }) => {
const innerState = useState(0);
const elements = useConstant(() => ({ tabs: 0, panels: 0 }));
const state = outerState || innerState;
return (
<Elements.Provider value={elements}>
<TabsState.Provider value={state}>{children}</TabsState.Provider>
</Elements.Provider>
);
};
export const useTabState = () => {
const tabState = useContext(TabsState);
const elements = useContext(Elements);
if (!tabState || !elements) {
throw new Error('useTabsState must be used within Tabs');
}
const tabIndex = useConstant(() => {
const currentIndex = elements.tabs;
elements.tabs += 1;
return currentIndex;
});
const [activeIndex, setActive] = tabState;
const onClick = useConstant(() => () => {
setActive(tabIndex);
});
const state = useMemo(
() => ({
isActive: activeIndex === tabIndex,
onClick,
}),
[activeIndex, onClick, tabIndex]
);
return state;
};
export const usePanelState = () => {
const tabsState = useContext(TabsState);
const elements = useContext(Elements);
if (!tabsState || !elements) {
throw new Error('usePanelState must be used within Tabs');
}
const [activeIndex] = tabsState;
const panelIndex = useConstant(() => {
const currentIndex = elements.panels;
elements.panels += 1;
return currentIndex;
});
return panelIndex === activeIndex;
};
export const Tab: FC = ({ children }) => {
const state = useTabState();
if (typeof children === 'function') {
return children(state);
}
return isValidElement(children) ? cloneElement(children, state) : children;
};
interface IPanelProps {
active: boolean;
}
export const Panel: FC<IPanelProps> = ({ active, children }) => {
const isActive = usePanelState();
if (isActive) {
return active ? children : null;
}
return null;
};
import * as React from 'react';
type ResultBox<T> = { v: T };
export default function useConstant<T>(fn: () => T): T {
const ref = React.useRef<ResultBox<T>>();
if (!ref.current) {
ref.current = { v: fn() };
}
return ref.current.v;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment