UNPKG

@itwin/itwinui-react

Version:

A react component library for iTwinUI

474 lines (473 loc) 13.1 kB
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 };