@itwin/itwinui-react
Version:
A react component library for iTwinUI
312 lines (311 loc) • 9.11 kB
JavaScript
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;
}