123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 |
- import {useMemo, useState} from 'react';
- import {PopperProps, usePopper} from 'react-popper';
- import {detectOverflow, Modifier, preventOverflow} from '@popperjs/core';
- import {useButton} from '@react-aria/button';
- import {
- OverlayProps,
- OverlayTriggerProps,
- useOverlay as useAriaOverlay,
- useOverlayTrigger,
- } from '@react-aria/overlays';
- import {mergeProps} from '@react-aria/utils';
- import {useOverlayTriggerState} from '@react-stately/overlays';
- import {OverlayTriggerProps as OverlayTriggerStateProps} from '@react-types/overlays';
- type PreventOverflowOptions = NonNullable<typeof preventOverflow['options']>;
- const maxSize: Modifier<'maxSize', PreventOverflowOptions> = {
- name: 'maxSize',
- enabled: true,
- phase: 'main',
- requiresIfExists: ['offset', 'preventOverflow', 'flip'],
- fn({state, name, options}) {
- const overflow = detectOverflow(state, options);
- const {x, y} = state.modifiersData.preventOverflow ?? {x: 0, y: 0};
- const {width, height} = state.rects.popper;
- const [basePlacement] = state.placement.split('-');
- const widthSide = basePlacement === 'left' ? 'left' : 'right';
- const heightSide = basePlacement === 'top' ? 'top' : 'bottom';
- const flippedWidthSide = basePlacement === 'left' ? 'right' : 'left';
- const flippedHeightSide = basePlacement === 'top' ? 'bottom' : 'top';
-
-
- const maxHeight = Math.max(
- height - overflow[heightSide] - y,
- -overflow[flippedHeightSide]
- );
-
-
- const maxWidth = Math.max(
- width - overflow[widthSide] - x,
- -overflow[flippedWidthSide]
- );
- state.modifiersData[name] = {
- width: maxWidth,
- height: maxHeight,
- };
- },
- };
- const applyMaxSize: Modifier<'applyMaxSize', {}> = {
- name: 'applyMaxSize',
- enabled: true,
- phase: 'beforeWrite',
- requires: ['maxSize'],
- fn({state}) {
- const {width, height} = state.modifiersData.maxSize;
- state.styles.popper.maxHeight = height;
- state.styles.popper.maxWidth = width;
- },
- };
- export interface UseOverlayProps
- extends Partial<OverlayProps>,
- Partial<OverlayTriggerProps>,
- Partial<OverlayTriggerStateProps> {
-
- offset?: number;
-
- position?: PopperProps<any>['placement'];
- preventOverflowOptions?: PreventOverflowOptions;
- }
- function useOverlay({
- isOpen,
- onClose,
- defaultOpen,
- onOpenChange,
- type = 'dialog',
- offset = 8,
- position = 'top',
- preventOverflowOptions = {},
- isDismissable = true,
- shouldCloseOnBlur = false,
- isKeyboardDismissDisabled,
- shouldCloseOnInteractOutside,
- }: UseOverlayProps = {}) {
-
- const [triggerElement, setTriggerElement] = useState<HTMLButtonElement | null>(null);
- const [overlayElement, setOverlayElement] = useState<HTMLDivElement | null>(null);
- const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null);
-
- const triggerRef = useMemo(() => ({current: triggerElement}), [triggerElement]);
- const overlayRef = useMemo(() => ({current: overlayElement}), [overlayElement]);
- const modifiers = useMemo(
- () => [
- {
- name: 'hide',
- enabled: false,
- },
- {
- name: 'computeStyles',
- options: {
-
-
-
-
-
- gpuAcceleration: false,
- },
- },
- {
- name: 'arrow',
- options: {
- element: arrowElement,
-
-
- padding: 4,
- },
- },
- {
- name: 'offset',
- options: {
- offset: [0, offset],
- },
- },
- {
- name: 'preventOverflow',
- enabled: true,
- options: {
- padding: 16,
- ...preventOverflowOptions,
- },
- },
- {
- ...maxSize,
- options: {
- padding: 16,
- ...preventOverflowOptions,
- },
- },
- applyMaxSize,
- ],
- [arrowElement, offset, preventOverflowOptions]
- );
- const {
- styles: popperStyles,
- state: popperState,
- update: popperUpdate,
- } = usePopper(triggerElement, overlayElement, {modifiers, placement: position});
-
- const openState = useOverlayTriggerState({
- isOpen,
- defaultOpen,
- onOpenChange: open => {
- onOpenChange?.(open);
- open && popperUpdate?.();
- },
- });
- const {buttonProps} = useButton({onPress: openState.open}, triggerRef);
- const {triggerProps, overlayProps: overlayTriggerProps} = useOverlayTrigger(
- {type},
- openState,
- triggerRef
- );
-
- const {overlayProps} = useAriaOverlay(
- {
- onClose: () => {
- onClose?.();
- openState.close();
- },
- isOpen: openState.isOpen,
- isDismissable,
- shouldCloseOnBlur,
- isKeyboardDismissDisabled,
- shouldCloseOnInteractOutside,
- },
- overlayRef
- );
- return {
- isOpen: openState.isOpen,
- state: openState,
- triggerRef,
- triggerProps: {
- ref: setTriggerElement,
- ...mergeProps(buttonProps, triggerProps),
- },
- overlayRef,
- overlayProps: {
- ref: setOverlayElement,
- style: popperStyles.popper,
- ...mergeProps(overlayTriggerProps, overlayProps),
- },
- arrowProps: {
- ref: setArrowElement,
- style: popperStyles.arrow,
- placement: popperState?.placement,
- },
- };
- }
- export default useOverlay;
|