'use client'; import * as React from 'react'; import { getIntrinsicElementProps, isResolvedShorthand, useMergedRefs, slot, useEventCallback, elementContains, useControllableState } from '@fluentui/react-utilities'; import { useTreeItemContext_unstable, useTreeContext_unstable } from '../../contexts'; import { Checkbox } from '@fluentui/react-checkbox'; import { Radio } from '@fluentui/react-radio'; import { TreeItemChevron } from '../TreeItemChevron'; import { useArrowNavigationGroup, useIsNavigatingWithKeyboard } from '@fluentui/react-tabster'; import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; /** * Create the state required to render TreeItemLayout. * * The returned state can be modified with hooks such as useTreeItemLayoutStyles_unstable, * before being passed to renderTreeItemLayout_unstable. * * @param props - props from this instance of TreeItemLayout * @param ref - reference to root HTMLElement of TreeItemLayout */ export const useTreeItemLayout_unstable = (props, ref)=>{ 'use no memo'; const { main, iconAfter, iconBefore } = props; const layoutRef = useTreeItemContext_unstable((ctx)=>ctx.layoutRef); const selectionMode = useTreeContext_unstable((ctx)=>ctx.selectionMode); const navigationMode = useTreeContext_unstable((ctx)=>{ var _ctx_navigationMode; return (_ctx_navigationMode = ctx.navigationMode) !== null && _ctx_navigationMode !== void 0 ? _ctx_navigationMode : 'tree'; }); const [isActionsVisibleFromProps, onActionVisibilityChange] = isResolvedShorthand(props.actions) ? [ props.actions.visible, props.actions.onVisibilityChange ] : [ undefined, undefined ]; const [isActionsVisible, setIsActionsVisible] = useControllableState({ state: isActionsVisibleFromProps, initialState: false }); const selectionRef = useTreeItemContext_unstable((ctx)=>ctx.selectionRef); const expandIconRef = useTreeItemContext_unstable((ctx)=>ctx.expandIconRef); const actionsRef = useTreeItemContext_unstable((ctx)=>ctx.actionsRef); const actionsRefInternal = React.useRef(null); const treeItemRef = useTreeItemContext_unstable((ctx)=>ctx.treeItemRef); const subtreeRef = useTreeItemContext_unstable((ctx)=>ctx.subtreeRef); const checked = useTreeItemContext_unstable((ctx)=>ctx.checked); const isBranch = useTreeItemContext_unstable((ctx)=>ctx.itemType === 'branch'); // FIXME: Asserting is required here, as converting this to RefObject on context type would be a breaking change assertIsRefObject(treeItemRef); // FIXME: Asserting is required here, as converting this to RefObject on context type would be a breaking change assertIsRefObject(subtreeRef); const setActionsVisibleIfNotFromSubtree = React.useCallback((event)=>{ const isTargetFromSubtree = Boolean(subtreeRef.current && elementContains(subtreeRef.current, event.target)); if (!isTargetFromSubtree) { onActionVisibilityChange === null || onActionVisibilityChange === void 0 ? void 0 : onActionVisibilityChange(event, { visible: true, event, type: event.type }); if (event.defaultPrevented) { return; } setIsActionsVisible(true); } }, [ subtreeRef, setIsActionsVisible, onActionVisibilityChange ]); const { targetDocument } = useFluent(); const isNavigatingWithKeyboard = useIsNavigatingWithKeyboard(); const setActionsInvisibleIfNotFromSubtree = React.useCallback((event)=>{ const isRelatedTargetFromActions = ()=>Boolean(actionsRefInternal.current && elementContains(actionsRefInternal.current, event.relatedTarget)); const isRelatedTargetFromTreeItem = ()=>Boolean(treeItemRef.current && elementContains(treeItemRef.current, event.relatedTarget)); const isTargetFromActions = ()=>{ var _actionsRefInternal_current; return Boolean((_actionsRefInternal_current = actionsRefInternal.current) === null || _actionsRefInternal_current === void 0 ? void 0 : _actionsRefInternal_current.contains(event.target)); }; if (isRelatedTargetFromActions()) { onActionVisibilityChange === null || onActionVisibilityChange === void 0 ? void 0 : onActionVisibilityChange(event, { visible: true, event, type: event.type }); if (event.defaultPrevented) { return; } setIsActionsVisible(true); return; } if (isTargetFromActions() && isRelatedTargetFromTreeItem()) { return; } // when a mouseout event happens during keyboard interaction // we should not hide the actions if the activeElement is the treeitem or an action // as the focus on the treeitem takes precedence over the mouseout event if (event.type === 'mouseout' && isNavigatingWithKeyboard() && ((targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.activeElement) === treeItemRef.current || elementContains(actionsRefInternal.current, targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.activeElement))) { return; } onActionVisibilityChange === null || onActionVisibilityChange === void 0 ? void 0 : onActionVisibilityChange(event, { visible: false, event, type: event.type }); if (event.defaultPrevented) { return; } setIsActionsVisible(false); }, [ setIsActionsVisible, onActionVisibilityChange, treeItemRef, isNavigatingWithKeyboard, targetDocument ]); const expandIcon = slot.optional(props.expandIcon, { renderByDefault: isBranch, defaultProps: { children: /*#__PURE__*/ React.createElement(TreeItemChevron, null), 'aria-hidden': true }, elementType: 'div' }); const expandIconRefs = useMergedRefs(expandIcon === null || expandIcon === void 0 ? void 0 : expandIcon.ref, expandIconRef); if (expandIcon) { expandIcon.ref = expandIconRefs; } const arrowNavigationProps = useArrowNavigationGroup({ circular: navigationMode === 'tree', axis: 'horizontal' }); const actions = isActionsVisible ? slot.optional(props.actions, { defaultProps: { ...arrowNavigationProps, role: 'toolbar' }, elementType: 'div' }) : undefined; actions === null || actions === void 0 ? true : delete actions.visible; actions === null || actions === void 0 ? true : delete actions.onVisibilityChange; const actionsRefs = useMergedRefs(actions === null || actions === void 0 ? void 0 : actions.ref, actionsRef, actionsRefInternal); const handleActionsBlur = useEventCallback((event)=>{ if (isResolvedShorthand(props.actions)) { var _props_actions_onBlur, _props_actions; (_props_actions_onBlur = (_props_actions = props.actions).onBlur) === null || _props_actions_onBlur === void 0 ? void 0 : _props_actions_onBlur.call(_props_actions, event); } const isRelatedTargetFromActions = Boolean(elementContains(event.currentTarget, event.relatedTarget)); onActionVisibilityChange === null || onActionVisibilityChange === void 0 ? void 0 : onActionVisibilityChange(event, { visible: isRelatedTargetFromActions, event, type: event.type }); setIsActionsVisible(isRelatedTargetFromActions); }); if (actions) { actions.ref = actionsRefs; actions.onBlur = handleActionsBlur; } const hasActions = Boolean(props.actions); React.useEffect(()=>{ if (treeItemRef.current && hasActions) { const treeItemElement = treeItemRef.current; const handleMouseOver = setActionsVisibleIfNotFromSubtree; const handleMouseOut = setActionsInvisibleIfNotFromSubtree; const handleFocus = setActionsVisibleIfNotFromSubtree; const handleBlur = setActionsInvisibleIfNotFromSubtree; treeItemElement.addEventListener('mouseover', handleMouseOver); treeItemElement.addEventListener('mouseout', handleMouseOut); treeItemElement.addEventListener('focus', handleFocus); treeItemElement.addEventListener('blur', handleBlur); return ()=>{ treeItemElement.removeEventListener('mouseover', handleMouseOver); treeItemElement.removeEventListener('mouseout', handleMouseOut); treeItemElement.removeEventListener('focus', handleFocus); treeItemElement.removeEventListener('blur', handleBlur); }; } }, [ hasActions, treeItemRef, setActionsVisibleIfNotFromSubtree, setActionsInvisibleIfNotFromSubtree ]); return { components: { root: 'div', expandIcon: 'div', iconBefore: 'div', main: 'div', iconAfter: 'div', actions: 'div', aside: 'div', // Casting here to a union between checkbox and radio selector: selectionMode === 'multiselect' ? Checkbox : Radio }, buttonContextValue: { size: 'small' }, root: slot.always(getIntrinsicElementProps('div', { ...props, // FIXME: // `ref` is wrongly assigned to be `HTMLElement` instead of `HTMLDivElement` // but since it would be a breaking change to fix it, we are casting ref to it's proper type ref: useMergedRefs(ref, layoutRef) }), { elementType: 'div' }), iconBefore: slot.optional(iconBefore, { elementType: 'div' }), main: slot.always(main, { elementType: 'div' }), iconAfter: slot.optional(iconAfter, { elementType: 'div' }), aside: !isActionsVisible ? slot.optional(props.aside, { elementType: 'div' }) : undefined, actions, expandIcon, selector: slot.optional(props.selector, { renderByDefault: selectionMode !== 'none', defaultProps: { checked, tabIndex: -1, 'aria-hidden': true, ref: selectionRef }, elementType: selectionMode === 'multiselect' ? Checkbox : Radio }) }; }; function assertIsRefObject(ref) { if (process.env.NODE_ENV !== 'production') { if (typeof ref !== 'object' || ref === null || !('current' in ref)) { throw new Error(` @fluentui/react-tree [${useTreeItemLayout_unstable.name}]: Internal Error: contextual ref is not a RefObject! Please report this bug immediately, as contextual refs should be RefObjects. `); } } }