import React, {
FC,
ComponentType,
HTMLAttributes,
ReactElement,
ReactNode,
Ref,
createContext,
useId,
useContext,
useMemo,
useRef,
useState,
useEffect,
} from 'react';
import classnames from 'classnames';
import { registerStyle } from './util';
import { DropdownButton, DropdownButtonProps } from './DropdownButton';
import { useControlledValue, useEventCallback } from './hooks';
import { Bivariant } from './typeUtils';
import { TooltipContent } from './TooltipContent';
/**
*
*/
type TabKey = string | number;
export type TabType = 'default' | 'scoped';
/**
*
*/
const TabsHandlersContext = createContext<{
onTabClick?: Bivariant<(tabKey: TabKey) => void>;
onTabKeyDown?: Bivariant<(tabKey: TabKey, e: React.KeyboardEvent) => void>;
}>({});
/**
*
*/
const TabsActiveKeyContext = createContext(undefined);
/**
*
*/
const TabsContext = createContext<{
type: TabType;
activeTabRef?: Ref;
tabIdPrefix?: string;
}>({ type: 'default' });
/**
* Custom hook to generate unique tab IDs
*/
const useTabIds = (eventKey?: TabKey) => {
const { tabIdPrefix } = useContext(TabsContext);
const tabIndex = eventKey ? String(eventKey) : '0';
const tabId = `${tabIdPrefix}-${tabIndex}`;
return { tabId };
};
/**
*
*/
export type TabContentProps = {
active?: boolean;
} & HTMLAttributes;
/**
*
*/
const TabContent: FC = (props) => {
const { className, active, children, ...rprops } = props;
const { type } = useContext(TabsContext);
const tabClassNames = classnames(
className,
`slds-tabs_${type}__content`,
`slds-${active ? 'show' : 'hide'}`
);
return (
{children}
);
};
/**
*
*/
export type TabMenuProps = DropdownButtonProps;
/**
*
*/
const TabMenu: FC = (props) => {
const { icon = 'down', children, ...rprops } = props;
return (
{children}
);
};
/**
*
*/
export type TabItemRendererProps = {
type?: TabType;
title?: string;
alt?: string;
menu?: ReactElement;
menuItems?: ReactElement[];
menuIcon?: string;
eventKey?: TabKey;
activeKey?: TabKey;
activeTabRef?: Ref;
children?: ReactNode;
onTabClick?: Bivariant<(eventKey: TabKey) => void>;
onTabKeyDown?: Bivariant<
(eventKey: TabKey, e: React.KeyboardEvent) => void
>;
tooltip?: ReactNode;
tooltipIcon?: string;
};
const DefaultTabItemRenderer: FC<{ children?: ReactNode }> = (props) => {
const el = React.Children.only(props.children);
return React.isValidElement(el) ? el : <>{el}>;
};
/**
*
*/
export type TabItemProps = {
tabItemRenderer?: ComponentType;
rendererProps?: Omit;
} & Omit<
TabItemRendererProps,
'type' | 'activeKey' | 'activeTabRef' | 'onTabClick' | 'onTabKeyDown'
>;
/**
*
*/
const TabItem = (
props: TabItemProps
) => {
const { title, alt, eventKey, menu, menuIcon, tooltip, tooltipIcon } = props;
const { type, activeTabRef } = useContext(TabsContext);
const activeKey = useContext(TabsActiveKeyContext);
const { onTabClick, onTabKeyDown } = useContext(TabsHandlersContext);
const { tabId } = useTabIds(eventKey);
let { menuItems } = props;
menuItems = menu
? React.Children.toArray(
(menu.props as unknown as { children?: ReactNode }).children || []
).map((el) => (React.isValidElement(el) ? el : <>{el}>))
: menuItems;
const menuProps = (menu?.props as unknown) ?? {};
const isActive = eventKey === activeKey;
const tabItemClassName = classnames(
'react-slds-tab-item',
`slds-tabs_${type}__item`,
{ 'slds-is-active': isActive },
{ 'react-slds-tab-with-menu': menu || menuItems }
);
const tabLinkClassName = `slds-tabs_${type}__link`;
const {
tabItemRenderer: TabItemRenderer = DefaultTabItemRenderer,
rendererProps,
...rprops
} = props;
const itemRendererProps = {
...rendererProps,
...rprops,
type,
activeKey,
activeTabRef,
onTabClick,
onTabKeyDown,
} as RendererProps;
return (
onTabClick?.(eventKey) : undefined
}
onKeyDown={
eventKey != null ? (e) => onTabKeyDown?.(eventKey, e) : undefined
}
>
{title}
{tooltip ? (
{tooltip}
) : null}
{menuItems ? (
{menuItems}
) : undefined}
);
};
/**
*
*/
const TabNav: FC<{ children?: ReactNode }> = (props) => {
const { children } = props;
const { type } = useContext(TabsContext);
const tabNavClassName = `slds-tabs_${type}__nav`;
return (
{React.Children.map(children, (tab) => {
if (!React.isValidElement(tab)) {
return null;
}
return ;
})}
);
};
/**
*
*/
export type TabProps = {
className?: string;
eventKey?: TabKey;
children?: ReactNode;
} & TabItemProps;
export const Tab = <
RendererProps extends TabItemRendererProps = TabItemRendererProps,
>(
props: TabProps
) => {
const { className, eventKey, children } = props;
const activeKey = useContext(TabsActiveKeyContext);
const { tabId } = useTabIds(eventKey);
return (
{children}
);
};
/**
*
*/
export type TabsProps = {
className?: string;
type?: TabType;
defaultActiveKey?: TabKey;
activeKey?: TabKey;
children?: ReactNode;
onSelect?: Bivariant<(tabKey: TabKey) => void>;
};
/**
*
*/
function useInitComponentStyle() {
useEffect(() => {
registerStyle('tab-menu', [
[
'.react-slds-tab-item.react-slds-tab-with-menu',
'{ position: relative !important; overflow: visible !important; }',
],
[
'.react-slds-tab-item.react-slds-tab-with-menu > .react-slds-tab-item-content',
'{ overflow: hidden }',
],
[
'.react-slds-tab-item.react-slds-tab-with-menu > .react-slds-tab-item-content > a',
'{ padding-right: 2rem; }',
],
[
'.react-slds-tab-item.react-slds-tab-with-menu > .react-slds-tab-item-content.react-slds-tooltip-enabled > a',
'{ padding-right: 3.5rem; }',
],
['.react-slds-tab-menu', '{ position: absolute; top: 0; right: 0; }'],
[
'.react-slds-tab-item.react-slds-tab-with-menu .react-slds-tab-item-content .react-slds-tooltip-content',
'{ position: absolute; top: 0.6rem; right: 2.25rem; }',
],
[
'.react-slds-tab-menu button',
'{ height: 2.5rem; line-height: 2rem; width: 2rem; visibility: hidden; justify-content: center }',
],
[
'.react-slds-tab-item.slds-is-active .react-slds-tab-menu button',
'.react-slds-tab-item:hover .react-slds-tab-menu button',
'.react-slds-tab-item .react-slds-tab-menu button:focus',
'{ visibility: visible }',
],
]);
}, []);
}
/**
*
*/
export const Tabs: FC = (props) => {
const {
className,
type = 'default',
activeKey: activeKey_,
defaultActiveKey,
children,
onSelect,
} = props;
const tabsClassNames = classnames(className, `slds-tabs_${type}`);
const activeTabRef = useRef(null);
const [focusTab, setFocusTab] = useState(false);
const [activeKey, setActiveKey] = useControlledValue(
activeKey_,
defaultActiveKey ?? null
);
const tabKeys = React.Children.map(children, (tab: ReactNode) => {
if (React.isValidElement(tab)) {
const { eventKey } = tab.props as { eventKey?: TabKey };
return eventKey;
}
return undefined;
}) as Array;
useInitComponentStyle();
const onTabClick = useEventCallback((tabKey: TabKey) => {
onSelect?.(tabKey);
setActiveKey(tabKey);
setFocusTab(true);
});
const onTabKeyDown = useEventCallback(
(tabKey: TabKey, e: React.KeyboardEvent) => {
if (e.keyCode === 37 || e.keyCode === 39) {
// left/right cursor key
const idx = tabKeys.findIndex((key) => key === tabKey);
if (idx < 0) {
return;
}
const dir = e.keyCode === 37 ? -1 : 1;
const activeIdx = (idx + dir + tabKeys.length) % tabKeys.length;
const activeKey = tabKeys[activeIdx];
if (activeKey) {
onTabClick(activeKey);
}
e.preventDefault();
e.stopPropagation();
}
}
);
useEffect(() => {
if (focusTab) {
activeTabRef.current?.focus();
setFocusTab(false);
}
}, [focusTab]);
const tabIdPrefix = useId();
const tabItemIdPrefix = useId();
const tabCtx = useMemo(
() => ({ type, activeTabRef, tabIdPrefix, tabItemIdPrefix }),
[type, tabIdPrefix, tabItemIdPrefix]
);
const handlers = useMemo(
() => ({ onTabClick, onTabKeyDown }),
[onTabClick, onTabKeyDown]
);
return (
{children}
{children}
);
};