239 lines
9.1 KiB
JavaScript
239 lines
9.1 KiB
JavaScript
'use client';
|
|
import * as React from 'react';
|
|
import { useControllableState, useEventCallback, useOnClickOutside, useOnScrollOutside, elementContains, useTimeout } from '@fluentui/react-utilities';
|
|
import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
|
|
import { usePositioning, resolvePositioningShorthand, mergeArrowOffset, usePositioningMouseTarget, usePositioningSlideDirection } from '@fluentui/react-positioning';
|
|
import { useFocusFinders, useActivateModal } from '@fluentui/react-tabster';
|
|
import { arrowHeights } from '../PopoverSurface/index';
|
|
import { popoverSurfaceBorderRadius } from './constants';
|
|
import { presenceMotionSlot } from '@fluentui/react-motion';
|
|
import { PopoverSurfaceMotion } from './PopoverSurfaceMotion';
|
|
/**
|
|
* Create the state required to render Popover.
|
|
*
|
|
* The returned state can be modified with hooks such as usePopoverStyles,
|
|
* before being passed to renderPopover_unstable.
|
|
*
|
|
* @param props - props from this instance of Popover
|
|
*/ export const usePopover_unstable = (props)=>{
|
|
const { appearance, size = 'medium' } = props;
|
|
const positioning = resolvePositioningShorthand(props.positioning);
|
|
const withArrow = props.withArrow && !positioning.coverTarget;
|
|
const { targetDocument } = useFluent();
|
|
const handlePositionEnd = usePositioningSlideDirection({
|
|
targetDocument,
|
|
onPositioningEnd: positioning.onPositioningEnd
|
|
});
|
|
const state = usePopoverBase_unstable({
|
|
...props,
|
|
positioning: {
|
|
...positioning,
|
|
onPositioningEnd: handlePositionEnd,
|
|
// Update the offset with the arrow size only when it's available
|
|
...withArrow ? {
|
|
offset: mergeArrowOffset(positioning.offset, arrowHeights[size])
|
|
} : {}
|
|
}
|
|
});
|
|
return {
|
|
components: {
|
|
surfaceMotion: PopoverSurfaceMotion
|
|
},
|
|
appearance,
|
|
size,
|
|
...state,
|
|
surfaceMotion: presenceMotionSlot(props.surfaceMotion, {
|
|
elementType: PopoverSurfaceMotion,
|
|
defaultProps: {
|
|
visible: state.open,
|
|
appear: true,
|
|
unmountOnExit: true
|
|
}
|
|
})
|
|
};
|
|
};
|
|
/**
|
|
* Base hook that builds Popover state for behavior and structure only.
|
|
* Does not add design-related defaults such as appearance or size.
|
|
* Does not manage focus behavior, it's handled by `usePopoverFocusManagement_unstable`.
|
|
*
|
|
* @internal
|
|
* @param props - props from this instance of Popover
|
|
*/ export const usePopoverBase_unstable = (props)=>{
|
|
const [contextTarget, setContextTarget] = usePositioningMouseTarget();
|
|
const initialState = {
|
|
contextTarget,
|
|
setContextTarget,
|
|
...props
|
|
};
|
|
const children = React.Children.toArray(props.children);
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
if (children.length === 0) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn('Popover must contain at least one child');
|
|
}
|
|
if (children.length > 2) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn('Popover must contain at most two children');
|
|
}
|
|
}
|
|
let popoverTrigger = undefined;
|
|
let popoverSurface = undefined;
|
|
if (children.length === 2) {
|
|
popoverTrigger = children[0];
|
|
popoverSurface = children[1];
|
|
} else if (children.length === 1) {
|
|
popoverSurface = children[0];
|
|
}
|
|
const [open, setOpenState] = useOpenState(initialState);
|
|
const [setOpenTimeout, clearOpenTimeout] = useTimeout();
|
|
const setOpen = useEventCallback((e, shouldOpen)=>{
|
|
clearOpenTimeout();
|
|
if (!(e instanceof Event) && e.persist) {
|
|
// < React 17 still uses pooled synthetic events
|
|
e.persist();
|
|
}
|
|
if (e.type === 'mouseleave') {
|
|
var _props_mouseLeaveDelay;
|
|
// FIXME leaking Node timeout type
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
setOpenTimeout(()=>{
|
|
setOpenState(e, shouldOpen);
|
|
}, (_props_mouseLeaveDelay = props.mouseLeaveDelay) !== null && _props_mouseLeaveDelay !== void 0 ? _props_mouseLeaveDelay : 500);
|
|
} else {
|
|
setOpenState(e, shouldOpen);
|
|
}
|
|
});
|
|
const toggleOpen = React.useCallback((e)=>{
|
|
setOpen(e, !open);
|
|
}, [
|
|
setOpen,
|
|
open
|
|
]);
|
|
const positioningRefs = usePopoverRefs(initialState);
|
|
const { targetDocument } = useFluent();
|
|
var _props_closeOnIframeFocus;
|
|
useOnClickOutside({
|
|
contains: elementContains,
|
|
element: targetDocument,
|
|
callback: (ev)=>setOpen(ev, false),
|
|
refs: [
|
|
positioningRefs.triggerRef,
|
|
positioningRefs.contentRef
|
|
],
|
|
disabled: !open,
|
|
disabledFocusOnIframe: !((_props_closeOnIframeFocus = props.closeOnIframeFocus) !== null && _props_closeOnIframeFocus !== void 0 ? _props_closeOnIframeFocus : true)
|
|
});
|
|
// only close on scroll for context, or when closeOnScroll is specified
|
|
const closeOnScroll = initialState.openOnContext || initialState.closeOnScroll;
|
|
useOnScrollOutside({
|
|
contains: elementContains,
|
|
element: targetDocument,
|
|
callback: (ev)=>setOpen(ev, false),
|
|
refs: [
|
|
positioningRefs.triggerRef,
|
|
positioningRefs.contentRef
|
|
],
|
|
disabled: !open || !closeOnScroll
|
|
});
|
|
const { findFirstFocusable } = useFocusFinders();
|
|
const activateModal = useActivateModal();
|
|
React.useEffect(()=>{
|
|
if (props.unstable_disableAutoFocus) {
|
|
return;
|
|
}
|
|
const contentElement = positioningRefs.contentRef.current;
|
|
if (open && contentElement) {
|
|
var _contentElement_getAttribute;
|
|
const shouldFocusContainer = !isNaN((_contentElement_getAttribute = contentElement.getAttribute('tabIndex')) !== null && _contentElement_getAttribute !== void 0 ? _contentElement_getAttribute : undefined);
|
|
const firstFocusable = shouldFocusContainer ? contentElement : findFirstFocusable(contentElement);
|
|
firstFocusable === null || firstFocusable === void 0 ? void 0 : firstFocusable.focus();
|
|
if (shouldFocusContainer) {
|
|
// Modal activation happens automatically when something inside the modal is focused programmatically.
|
|
// When the container is focused, we need to activate the modal manually.
|
|
activateModal(contentElement);
|
|
}
|
|
}
|
|
}, [
|
|
findFirstFocusable,
|
|
activateModal,
|
|
open,
|
|
positioningRefs.contentRef,
|
|
props.unstable_disableAutoFocus
|
|
]);
|
|
var _props_inertTrapFocus, _props_inline;
|
|
return {
|
|
...initialState,
|
|
...positioningRefs,
|
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
inertTrapFocus: (_props_inertTrapFocus = props.inertTrapFocus) !== null && _props_inertTrapFocus !== void 0 ? _props_inertTrapFocus : props.legacyTrapFocus === undefined ? false : !props.legacyTrapFocus,
|
|
popoverTrigger,
|
|
popoverSurface,
|
|
open,
|
|
setOpen,
|
|
toggleOpen,
|
|
setContextTarget,
|
|
contextTarget,
|
|
inline: (_props_inline = props.inline) !== null && _props_inline !== void 0 ? _props_inline : false
|
|
};
|
|
};
|
|
/**
|
|
* Creates and manages the Popover open state
|
|
*/ function useOpenState(state) {
|
|
'use no memo';
|
|
const onOpenChange = useEventCallback((e, data)=>{
|
|
var _state_onOpenChange;
|
|
return (_state_onOpenChange = state.onOpenChange) === null || _state_onOpenChange === void 0 ? void 0 : _state_onOpenChange.call(state, e, data);
|
|
});
|
|
const [open, setOpenState] = useControllableState({
|
|
state: state.open,
|
|
defaultState: state.defaultOpen,
|
|
initialState: false
|
|
});
|
|
state.open = open !== undefined ? open : state.open;
|
|
const setContextTarget = state.setContextTarget;
|
|
const setOpen = React.useCallback((e, shouldOpen)=>{
|
|
if (shouldOpen && e.type === 'contextmenu') {
|
|
setContextTarget(e);
|
|
}
|
|
if (!shouldOpen) {
|
|
setContextTarget(undefined);
|
|
}
|
|
setOpenState(shouldOpen);
|
|
onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(e, {
|
|
open: shouldOpen
|
|
});
|
|
}, [
|
|
setOpenState,
|
|
onOpenChange,
|
|
setContextTarget
|
|
]);
|
|
return [
|
|
open,
|
|
setOpen
|
|
];
|
|
}
|
|
/**
|
|
* Creates and sets the necessary trigger, target and content refs used by Popover
|
|
*/ function usePopoverRefs(state) {
|
|
'use no memo';
|
|
const positioningOptions = {
|
|
position: 'above',
|
|
align: 'center',
|
|
arrowPadding: 2 * popoverSurfaceBorderRadius,
|
|
target: state.openOnContext ? state.contextTarget : undefined,
|
|
...resolvePositioningShorthand(state.positioning)
|
|
};
|
|
// no reason to render arrow when covering the target
|
|
if (positioningOptions.coverTarget) {
|
|
state.withArrow = false;
|
|
}
|
|
const { targetRef: triggerRef, containerRef: contentRef, arrowRef } = usePositioning(positioningOptions);
|
|
return {
|
|
triggerRef,
|
|
contentRef,
|
|
arrowRef
|
|
};
|
|
}
|