UNPKG

@itwin/itwinui-react

Version:

A react component library for iTwinUI

312 lines (311 loc) 9.11 kB
import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { Box, cloneElementWithRef, mergeEventHandlers, SvgChevronLeft, useInstance, useMergedRefs, useSafeContext, useMediaQuery, useWarningLogger, useLayoutEffect, useLatestRef, useId, } from '../../utils/index.js'; import { IconButton } from '../Buttons/IconButton.js'; import { Flex } from '../Flex/Flex.js'; import { Text } from '../Typography/Text.js'; import cx from 'classnames'; import { PanelsInstanceContext, PanelsInstanceProvider } from './helpers.js'; export const PanelsWrapper = React.forwardRef((props, forwardedRef) => { let { children, className, onActiveIdChange: onActiveIdChangeProp, instance, ...rest } = props; let onActiveIdChange = useLatestRef(onActiveIdChangeProp); let ref = React.useRef(null); let [activePanelId, setActivePanelId] = React.useState(void 0); let [triggers, setTriggers] = React.useState({}); let panels = React.useRef(new Set()); let [shouldFocus, setShouldFocus] = React.useState(void 0); let motionOk = useMediaQuery('(prefers-reduced-motion: no-preference)'); let changeActivePanel = React.useCallback( (newActiveId) => { if (!panels.current.has(newActiveId) || newActiveId === activePanelId) return; ReactDOM.flushSync(() => setActivePanelId(newActiveId)); onActiveIdChange.current?.(newActiveId); ref.current ?.getRootNode() .getElementById(newActiveId) ?.scrollIntoView({ block: 'nearest', inline: 'center', behavior: motionOk ? 'smooth' : 'instant', }); }, [activePanelId, motionOk, onActiveIdChange], ); return React.createElement( PanelsWrapperContext.Provider, { value: React.useMemo( () => ({ activePanelId, setActivePanelId, changeActivePanel, triggers, setTriggers, shouldFocus, setShouldFocus, panels, }), [ activePanelId, changeActivePanel, setActivePanelId, setTriggers, shouldFocus, triggers, ], ), }, React.createElement( PanelsInstanceProvider, { instance: instance, }, React.createElement( Box, { ref: useMergedRefs(ref, forwardedRef), ...rest, className: cx('iui-panel-wrapper', className), }, children, ), ), ); }); if ('development' === process.env.NODE_ENV) PanelsWrapper.displayName = 'Panels.Wrapper'; export const PanelsWrapperContext = React.createContext(void 0); if ('development' === process.env.NODE_ENV) PanelsWrapperContext.displayName = 'PanelsWrapperContext'; let Panel = React.forwardRef((props, forwardedRef) => { let { id, children, className, ...rest } = props; let { activePanelId, triggers, panels, setActivePanelId } = useSafeContext(PanelsWrapperContext); let associatedTrigger = React.useMemo(() => triggers[id], [id, triggers]); let previousActivePanelId = useDelayed(activePanelId) || activePanelId; let isMounted = [activePanelId, previousActivePanelId].includes(id); let isTransitioning = activePanelId === id && activePanelId !== previousActivePanelId; let isInert = previousActivePanelId === id && activePanelId !== id; useLayoutEffect(() => { let isFirstPanel = null == activePanelId && 0 === panels.current.size; if (isFirstPanel) setActivePanelId(id); let panelsCurrent = panels.current; if (!panelsCurrent.has(id)) panelsCurrent.add(id); return () => { panelsCurrent.delete(id); }; }, [activePanelId, id, panels, setActivePanelId]); return React.createElement( PanelContext.Provider, { value: React.useMemo( () => ({ id, associatedTrigger, }), [associatedTrigger, id], ), }, isMounted && React.createElement( Box, { ref: forwardedRef, id: id, className: cx('iui-panel', className), 'aria-labelledby': `${id}-header-title`, role: 'group', inert: isInert ? 'true' : void 0, 'data-iui-transitioning': isTransitioning ? 'true' : void 0, ...rest, }, children, ), ); }); if ('development' === process.env.NODE_ENV) Panel.displayName = 'Panels.Panel'; let PanelContext = React.createContext(void 0); if ('development' === process.env.NODE_ENV) PanelContext.displayName = 'PanelContext'; let PanelTrigger = (props) => { let { children, for: forProp } = props; let { changeActivePanel, triggers, setTriggers, activePanelId: activePanel, shouldFocus, setShouldFocus, panels, } = useSafeContext(PanelsWrapperContext); let { id: panelId } = useSafeContext(PanelContext); let fallbackId = useId(); let triggerId = children.props.id || fallbackId; let onClick = React.useCallback(() => { if (null == activePanel) return; setShouldFocus({ fromPanelId: activePanel, toPanelId: forProp, direction: 'forward', }); changeActivePanel?.(forProp); }, [activePanel, changeActivePanel, forProp, setShouldFocus]); let focusRef = React.useCallback( (el) => { if ( shouldFocus?.direction === 'backward' && shouldFocus?.toPanelId === panelId && shouldFocus?.fromPanelId === forProp ) { el?.focus({ preventScroll: true, }); setShouldFocus(void 0); } }, [forProp, panelId, setShouldFocus, shouldFocus], ); let logWarning = useWarningLogger(); React.useEffect(() => { if (!panels.current.has(forProp)) logWarning( `Panels.Trigger's \`for\` prop ("${forProp}") corresponds to no Panel.`, ); }, [forProp, logWarning, panels, triggers]); React.useEffect(() => { setTriggers((oldTriggers) => { let triggersMatch = oldTriggers[forProp]; if ( null == triggersMatch || panelId !== triggersMatch.panelId || triggerId !== triggersMatch.triggerId ) return { ...oldTriggers, [forProp]: { panelId, triggerId, }, }; return oldTriggers; }); }, [forProp, panelId, setTriggers, triggerId]); return cloneElementWithRef(children, (children) => ({ ...children.props, id: triggerId, ref: focusRef, onClick: mergeEventHandlers(children.props.onClick, onClick), 'aria-expanded': activePanel === forProp, 'aria-controls': forProp, })); }; if ('development' === process.env.NODE_ENV) PanelTrigger.displayName = 'Panels.Trigger'; let PanelHeader = React.forwardRef((props, forwardedRef) => { let { titleProps, children, ...rest } = props; let { shouldFocus, setShouldFocus } = useSafeContext(PanelsWrapperContext); let { id: panelId, associatedTrigger: panelAssociatedTrigger } = useSafeContext(PanelContext); let focusRef = React.useCallback( (el) => { if ( shouldFocus?.direction === 'forward' && shouldFocus.toPanelId === panelId ) { el?.focus({ preventScroll: true, }); setShouldFocus(void 0); } }, [panelId, setShouldFocus, shouldFocus?.direction, shouldFocus?.toPanelId], ); return React.createElement( Flex, { ref: forwardedRef, ...rest, }, panelAssociatedTrigger && React.createElement(PanelBackButton, null), React.createElement( Text, { id: `${panelId}-header-title`, as: 'h2', tabIndex: -1, ref: focusRef, ...titleProps, }, children, ), ); }); if ('development' === process.env.NODE_ENV) PanelHeader.displayName = 'Panels.Header'; let PanelBackButton = React.forwardRef((props, forwardedRef) => { let { children, onClick, ...rest } = props; let { instance: panelInstance } = useSafeContext(PanelsInstanceContext); return React.createElement( IconButton, { ref: forwardedRef, 'aria-label': 'Previous panel', styleType: 'borderless', size: 'small', 'data-iui-shift': 'left', ...rest, onClick: mergeEventHandlers( React.useCallback(() => panelInstance?.goBack(), [panelInstance]), onClick, ), }, children || React.createElement(SvgChevronLeft, null), ); }); if ('development' === process.env.NODE_ENV) PanelBackButton.displayName = 'Panels.BackButton'; export const Panels = { Wrapper: PanelsWrapper, Panel, Trigger: PanelTrigger, Header: PanelHeader, useInstance: useInstance, }; function useDelayed( value, { delay } = { delay: 500, }, ) { let [delayed, setDelayed] = React.useState(void 0); let timeout = React.useRef(void 0); React.useEffect(() => { if (0 === delay) setDelayed(value); else timeout.current = setTimeout(() => setDelayed(value), delay); return () => { clearTimeout(timeout.current); }; }, [value, delay]); return delayed; }