/* * Copyright 2020 Adobe. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ import {DialogContext} from './context'; import {Modal, Popover, Tray} from '@react-spectrum/overlays'; import {OverlayTriggerState, useOverlayTriggerState} from '@react-stately/overlays'; import {PressResponder} from '@react-aria/interactions'; import React, {Fragment, JSX, ReactElement, useEffect, useRef} from 'react'; import {SpectrumDialogClose, SpectrumDialogProps, SpectrumDialogTriggerProps} from '@react-types/dialog'; import {useIsMobileDevice} from '@react-spectrum/utils'; import {useOverlayTrigger} from '@react-aria/overlays'; function DialogTrigger(props: SpectrumDialogTriggerProps) { let { children, type = 'modal', mobileType = type === 'popover' ? 'modal' : type, hideArrow, targetRef, isDismissable, isKeyboardDismissDisabled, ...positionProps } = props; if (!Array.isArray(children) || children.length > 2) { throw new Error('DialogTrigger must have exactly 2 children'); } // if a function is passed as the second child, it won't appear in toArray let [trigger, content] = children as [ReactElement, SpectrumDialogClose]; // On small devices, show a modal or tray instead of a popover. let isMobile = useIsMobileDevice(); if (isMobile) { // handle cases where desktop popovers need a close button for the mobile modal view if (type !== 'modal' && mobileType === 'modal') { isDismissable = true; } type = mobileType; } let state = useOverlayTriggerState(props); let wasOpen = useRef(false); useEffect(() => { wasOpen.current = state.isOpen; }, [state.isOpen]); let isExiting = useRef(false); let onExiting = () => isExiting.current = true; let onExited = () => isExiting.current = false; useEffect(() => { return () => { if ((wasOpen.current || isExiting.current) && type !== 'popover' && type !== 'tray' && process.env.NODE_ENV !== 'production') { console.warn('A DialogTrigger unmounted while open. This is likely due to being placed within a trigger that unmounts or inside a conditional. Consider using a DialogContainer instead.'); } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); if (type === 'popover') { return ( ); } let renderOverlay = () => { switch (type) { case 'fullscreen': case 'fullscreenTakeover': case 'modal': return ( {typeof content === 'function' ? content(state.close) : content} ); case 'tray': return ( {typeof content === 'function' ? content(state.close) : content} ); } }; return ( ); } // Support DialogTrigger inside components using CollectionBuilder. DialogTrigger.getCollectionNode = function* (props: SpectrumDialogTriggerProps) { // @ts-ignore - seems like types are wrong. Function children work fine. let [trigger] = React.Children.toArray(props.children); let [, content] = props.children as [ReactElement, SpectrumDialogClose]; yield { element: trigger, wrapper: (element) => ( {element} {content} ) }; }; /** * DialogTrigger serves as a wrapper around a Dialog and its associated trigger, linking the Dialog's * open state with the trigger's press state. Additionally, it allows you to customize the type and * positioning of the Dialog. */ // We don't want getCollectionNode to show up in the type definition let _DialogTrigger = DialogTrigger as (props: SpectrumDialogTriggerProps) => JSX.Element; export {_DialogTrigger as DialogTrigger}; function PopoverTrigger({state, targetRef, trigger, content, hideArrow, ...props}) { let triggerRef = useRef(null); let {triggerProps, overlayProps} = useOverlayTrigger({type: 'dialog'}, state, triggerRef); let triggerPropsWithRef = { ...triggerProps, ref: targetRef ? undefined : triggerRef }; let overlay = ( {typeof content === 'function' ? content(state.close) : content} ); return ( ); } interface SpectrumDialogTriggerBase { type: 'modal' | 'popover' | 'tray' | 'fullscreen' | 'fullscreenTakeover', state: OverlayTriggerState, isDismissable?: boolean, dialogProps?: SpectrumDialogProps | {}, triggerProps?: any, overlay?: ReactElement, trigger: ReactElement } function DialogTriggerBase({type, state, isDismissable, dialogProps = {}, triggerProps = {}, overlay, trigger}: SpectrumDialogTriggerBase) { let context = { type, onClose: state.close, isDismissable, ...dialogProps }; return ( {trigger} {overlay} ); }