@itwin/itwinui-react
Version:
A react component library for iTwinUI
474 lines (473 loc) • 13.1 kB
JavaScript
import cx from 'classnames';
import * as React from 'react';
import {
useSafeContext,
Box,
polymorphic,
useIsClient,
useLayoutEffect,
useMergedRefs,
useContainerWidth,
ButtonBase,
mergeEventHandlers,
useControlledState,
useId,
useLatestRef,
} from '../../utils/index.js';
import { Icon } from '../Icon/Icon.js';
let TabsWrapper = React.forwardRef((props, ref) => {
let {
children,
orientation = 'horizontal',
type = 'default',
focusActivationMode = 'auto',
color = 'blue',
defaultValue,
value: activeValueProp,
onValueChange,
...rest
} = props;
let [activeValue, setActiveValue] = useControlledState(
defaultValue,
activeValueProp,
onValueChange,
);
let [stripeProperties, setStripeProperties] = React.useState({});
let [hasSublabel, setHasSublabel] = React.useState(false);
let idPrefix = useId();
return React.createElement(
TabsWrapperPresentation,
{
...rest,
orientation: orientation,
style: {
...stripeProperties,
...props?.style,
},
ref: ref,
},
React.createElement(
TabsContext.Provider,
{
value: {
orientation,
type,
activeValue,
setActiveValue,
setStripeProperties,
idPrefix,
focusActivationMode,
hasSublabel,
setHasSublabel,
color,
},
},
children,
),
);
});
if ('development' === process.env.NODE_ENV)
TabsWrapper.displayName = 'Tabs.Wrapper';
let TabsWrapperPresentation = React.forwardRef((props, forwardedRef) => {
let { orientation = 'horizontal', ...rest } = props;
return React.createElement(Box, {
...rest,
className: cx('iui-tabs-wrapper', `iui-${orientation}`, props.className),
ref: forwardedRef,
});
});
let TabList = React.forwardRef((props, ref) => {
let { className, children, ...rest } = props;
let { type, hasSublabel, color, orientation } = useSafeContext(TabsContext);
let isClient = useIsClient();
let tablistRef = React.useRef(null);
let [tablistSizeRef, tabsWidth] = useContainerWidth('default' !== type);
let refs = useMergedRefs(
ref,
tablistRef,
tablistSizeRef,
useScrollbarGutter(),
);
return React.createElement(
TabListPresentation,
{
className: cx(
{
'iui-animated': 'default' !== type && isClient,
},
className,
),
'data-iui-orientation': orientation,
role: 'tablist',
ref: refs,
...rest,
type: type,
color: color,
size: hasSublabel ? 'large' : void 0,
orientation: orientation,
},
React.createElement(
TabListContext.Provider,
{
value: {
tabsWidth,
tablistRef,
},
},
children,
),
);
});
if ('development' === process.env.NODE_ENV)
TabList.displayName = 'Tabs.TabList';
let TabListPresentation = React.forwardRef((props, forwardedRef) => {
let {
type = 'default',
color,
size,
orientation = 'horizontal',
...rest
} = props;
return React.createElement(Box, {
...rest,
className: cx(
'iui-tabs',
`iui-${type}`,
{
'iui-green': 'green' === color,
'iui-large': 'large' === size,
},
props.className,
),
'data-iui-orientation': orientation,
ref: forwardedRef,
});
});
let Tab = React.forwardRef((props, forwardedRef) => {
let { children, value, label, ...rest } = props;
let {
orientation,
activeValue,
setActiveValue,
type,
setStripeProperties,
idPrefix,
focusActivationMode,
} = useSafeContext(TabsContext);
let { tabsWidth, tablistRef } = useSafeContext(TabListContext);
let tabRef = React.useRef(void 0);
let isActive = activeValue === value;
let isActiveRef = useLatestRef(isActive);
useLayoutEffect(() => {
if (isActiveRef.current)
tabRef.current?.parentElement?.scrollTo({
['horizontal' === orientation ? 'left' : 'top']:
tabRef.current?.[
'horizontal' === orientation ? 'offsetLeft' : 'offsetTop'
] - 4,
behavior: 'instant',
});
}, [isActiveRef, orientation]);
useLayoutEffect(() => {
let updateStripe = () => {
let currentTabRect = tabRef.current?.getBoundingClientRect();
let tabslistRect = tablistRef.current?.getBoundingClientRect();
let currentTabLeftIncludingScroll =
(currentTabRect?.x ?? 0) + (tablistRef.current?.scrollLeft ?? 0);
let tabsStripePosition =
null != currentTabRect && null != tabslistRect
? {
horizontal: currentTabLeftIncludingScroll - tabslistRect.x,
vertical: currentTabRect.y - tabslistRect.y,
}
: {
horizontal: 0,
vertical: 0,
};
setStripeProperties({
'--iui-tabs-stripe-size':
'horizontal' === orientation
? `${currentTabRect?.width}px`
: `${currentTabRect?.height}px`,
'--iui-tabs-stripe-position':
'horizontal' === orientation
? `${tabsStripePosition.horizontal}px`
: `${tabsStripePosition.vertical}px`,
});
};
if ('default' !== type && isActive) updateStripe();
}, [
type,
orientation,
isActive,
tabsWidth,
setStripeProperties,
tablistRef,
value,
]);
let onKeyDown = (event) => {
if (event.altKey) return;
let allTabs = Array.from(event.currentTarget.parentElement?.children ?? []);
let nextTab = tabRef.current?.nextElementSibling ?? allTabs.at(0);
let previousTab = tabRef.current?.previousElementSibling ?? allTabs.at(-1);
switch (event.key) {
case 'ArrowDown':
if ('vertical' === orientation) {
nextTab?.focus();
event.preventDefault();
}
break;
case 'ArrowRight':
if ('horizontal' === orientation) {
nextTab?.focus();
event.preventDefault();
}
break;
case 'ArrowUp':
if ('vertical' === orientation) {
previousTab?.focus();
event.preventDefault();
}
break;
case 'ArrowLeft':
if ('horizontal' === orientation) {
previousTab?.focus();
event.preventDefault();
}
break;
default:
break;
}
};
let setInitialActiveRef = React.useCallback(
(element) => {
if (void 0 !== activeValue) return;
if (element?.matches(':first-of-type')) setActiveValue(value);
},
[activeValue, setActiveValue, value],
);
return React.createElement(
TabPresentation,
{
as: ButtonBase,
role: 'tab',
tabIndex: isActive ? 0 : -1,
'aria-selected': isActive,
'aria-controls': `${idPrefix}-panel-${value.replaceAll(' ', '-')}`,
ref: useMergedRefs(tabRef, forwardedRef, setInitialActiveRef),
...rest,
id: `${idPrefix}-tab-${value.replaceAll(' ', '-')}`,
onClick: mergeEventHandlers(props.onClick, () => setActiveValue(value)),
onKeyDown: mergeEventHandlers(props.onKeyDown, onKeyDown),
onFocus: mergeEventHandlers(props.onFocus, () => {
tabRef.current?.scrollIntoView({
block: 'nearest',
inline: 'nearest',
});
if ('auto' === focusActivationMode && !props.disabled)
setActiveValue(value);
}),
},
label ? React.createElement(Tabs.TabLabel, null, label) : children,
);
});
if ('development' === process.env.NODE_ENV) Tab.displayName = 'Tabs.Tab';
let TabPresentation = React.forwardRef((props, forwardedRef) =>
React.createElement(Box, {
as: 'button',
...props,
className: cx('iui-tab', props.className),
ref: forwardedRef,
}),
);
let TabIcon = React.forwardRef((props, ref) =>
React.createElement(Icon, {
...props,
className: cx('iui-tab-icon', props?.className),
ref: ref,
}),
);
if ('development' === process.env.NODE_ENV)
TabIcon.displayName = 'Tabs.TabIcon';
let TabLabel = polymorphic.span('iui-tab-label');
if ('development' === process.env.NODE_ENV)
TabLabel.displayName = 'Tabs.TabLabel';
let TabDescription = React.forwardRef((props, ref) => {
let { className, children, ...rest } = props;
let { hasSublabel, setHasSublabel } = useSafeContext(TabsContext);
useLayoutEffect(() => {
if (!hasSublabel) setHasSublabel(true);
}, [hasSublabel, setHasSublabel]);
return React.createElement(
Box,
{
as: 'span',
className: cx('iui-tab-description', className),
ref: ref,
...rest,
},
children,
);
});
if ('development' === process.env.NODE_ENV)
TabDescription.displayName = 'Tabs.TabDescription';
let TabsActions = React.forwardRef((props, ref) => {
let { wrapperProps, className, children, ...rest } = props;
return React.createElement(
Box,
{
...wrapperProps,
className: cx('iui-tabs-actions-wrapper', wrapperProps?.className),
},
React.createElement(
Box,
{
className: cx('iui-tabs-actions', className),
ref: ref,
...rest,
},
children,
),
);
});
if ('development' === process.env.NODE_ENV)
TabsActions.displayName = 'Tabs.Actions';
let TabsPanel = React.forwardRef((props, ref) => {
let { value, className, children, ...rest } = props;
let { activeValue, idPrefix } = useSafeContext(TabsContext);
return React.createElement(
Box,
{
className: cx('iui-tabs-content', className),
'aria-labelledby': `${idPrefix}-tab-${value.replaceAll(' ', '-')}`,
role: 'tabpanel',
hidden: activeValue !== value ? true : void 0,
ref: ref,
...rest,
id: `${idPrefix}-panel-${value.replaceAll(' ', '-')}`,
},
children,
);
});
if ('development' === process.env.NODE_ENV)
TabsPanel.displayName = 'Tabs.Panel';
let LegacyTabsComponent = React.forwardRef((props, forwardedRef) => {
let actions;
if ('pill' !== props.type && props.actions) {
actions = props.actions;
props = {
...props,
};
delete props.actions;
}
let {
labels,
onTabSelected,
focusActivationMode,
color,
activeIndex: activeIndexProp,
tabsClassName,
contentClassName,
wrapperClassName,
children,
...rest
} = props;
let [activeIndex, setActiveIndex] = useControlledState(
0,
activeIndexProp,
onTabSelected,
);
return React.createElement(
TabsWrapper,
{
className: wrapperClassName,
focusActivationMode: focusActivationMode,
color: color,
value: `${activeIndex}`,
onValueChange: (value) => setActiveIndex(Number(value)),
...rest,
},
React.createElement(
TabList,
{
className: tabsClassName,
ref: forwardedRef,
},
labels.map((label, index) => {
let tabValue = `${index}`;
return React.isValidElement(label)
? React.cloneElement(label, {
value: tabValue,
})
: React.createElement(LegacyTab, {
key: index,
value: tabValue,
label: label,
});
}),
),
actions && React.createElement(TabsActions, null, actions),
children &&
React.createElement(
TabsPanel,
{
value: `${activeIndex}`,
className: contentClassName,
},
children,
),
);
});
if ('development' === process.env.NODE_ENV)
LegacyTabsComponent.displayName = 'Tabs';
let LegacyTab = React.forwardRef((props, forwardedRef) => {
let { label, sublabel, startIcon, children, value, ...rest } = props;
return React.createElement(
React.Fragment,
null,
React.createElement(
Tab,
{
...rest,
value: value,
ref: forwardedRef,
},
startIcon && React.createElement(TabIcon, null, startIcon),
React.createElement(TabLabel, null, label),
sublabel && React.createElement(TabDescription, null, sublabel),
children,
),
);
});
export const Tabs = Object.assign(LegacyTabsComponent, {
Wrapper: TabsWrapper,
TabList: TabList,
Tab: Tab,
TabIcon: TabIcon,
TabLabel: TabLabel,
TabDescription: TabDescription,
Actions: TabsActions,
Panel: TabsPanel,
});
export const unstable_TabsPresentation = {
Wrapper: TabsWrapperPresentation,
TabList: TabListPresentation,
Tab: TabPresentation,
};
let TabsContext = React.createContext(void 0);
if ('development' === process.env.NODE_ENV)
TabsContext.displayName = 'TabsContext';
let TabListContext = React.createContext(void 0);
if ('development' === process.env.NODE_ENV)
TabListContext.displayName = 'TabListContext';
let useScrollbarGutter = () =>
React.useCallback((element) => {
if (element) {
if (element.scrollHeight > element.clientHeight) {
element.style.scrollbarGutter = 'stable';
if (!CSS.supports('scrollbar-gutter: stable'))
element.style.overflowY = 'scroll';
}
}
}, []);
export { LegacyTab as Tab };