diff --git a/packages/pl-fe/src/actions/modals.ts b/packages/pl-fe/src/actions/modals.ts index 83ce07b74..999d240e3 100644 --- a/packages/pl-fe/src/actions/modals.ts +++ b/packages/pl-fe/src/actions/modals.ts @@ -1,5 +1,3 @@ -import { AppDispatch } from 'pl-fe/store'; - import type { ICryptoAddress } from 'pl-fe/features/crypto-donate/components/crypto-address'; import type { ModalType } from 'pl-fe/features/ui/components/modal-root'; import type { AccountModerationModalProps } from 'pl-fe/features/ui/components/modals/account-moderation-modal'; @@ -9,6 +7,7 @@ import type { ComponentModalProps } from 'pl-fe/features/ui/components/modals/co import type { ComposeModalProps } from 'pl-fe/features/ui/components/modals/compose-modal'; import type { ConfirmationModalProps } from 'pl-fe/features/ui/components/modals/confirmation-modal'; import type { DislikesModalProps } from 'pl-fe/features/ui/components/modals/dislikes-modal'; +import type { DropdownMenuModalProps } from 'pl-fe/features/ui/components/modals/dropdown-menu-modal'; import type { EditAnnouncementModalProps } from 'pl-fe/features/ui/components/modals/edit-announcement-modal'; import type { EditBookmarkFolderModalProps } from 'pl-fe/features/ui/components/modals/edit-bookmark-folder-modal'; import type { EditDomainModalProps } from 'pl-fe/features/ui/components/modals/edit-domain-modal'; @@ -46,6 +45,7 @@ type OpenModalProps = | [type: 'CONFIRM', props: ConfirmationModalProps] | [type: 'CRYPTO_DONATE', props: ICryptoAddress] | [type: 'DISLIKES', props: DislikesModalProps] + | [type: 'DROPDOWN_MENU', props: DropdownMenuModalProps] | [type: 'EDIT_ANNOUNCEMENT', props?: EditAnnouncementModalProps] | [type: 'EDIT_BOOKMARK_FOLDER', props: EditBookmarkFolderModalProps] | [type: 'EDIT_DOMAIN', props?: EditDomainModalProps] @@ -72,15 +72,7 @@ type OpenModalProps = | [type: 'VIDEO', props: VideoModalProps]; /** Open a modal of the given type */ -const openModal = (...[type, props]: OpenModalProps) => { - // const [type, props] = args; - return (dispatch: AppDispatch) => { - dispatch(closeModal(type)); - dispatch(openModalSuccess(type, props)); - }; -}; - -const openModalSuccess = (type: ModalType, props?: any) => ({ +const openModal = (...[type, props]: OpenModalProps) => ({ type: MODAL_OPEN, modalType: type, modalProps: props, @@ -93,7 +85,7 @@ const closeModal = (type?: ModalType) => ({ }); type ModalsAction = - ReturnType + ReturnType | ReturnType; export { diff --git a/packages/pl-fe/src/components/dropdown-menu/dropdown-menu-item.tsx b/packages/pl-fe/src/components/dropdown-menu/dropdown-menu-item.tsx index 8503b5c01..b7dee6099 100644 --- a/packages/pl-fe/src/components/dropdown-menu/dropdown-menu-item.tsx +++ b/packages/pl-fe/src/components/dropdown-menu/dropdown-menu-item.tsx @@ -58,8 +58,7 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo } else if (typeof item.action === 'function') { const action = item.action; event.preventDefault(); - // TODO - setTimeout(() => action(event), userTouching.matches ? 10 : 0); + action(event); } }; diff --git a/packages/pl-fe/src/components/dropdown-menu/dropdown-menu.tsx b/packages/pl-fe/src/components/dropdown-menu/dropdown-menu.tsx index d9e805ed4..bba78814c 100644 --- a/packages/pl-fe/src/components/dropdown-menu/dropdown-menu.tsx +++ b/packages/pl-fe/src/components/dropdown-menu/dropdown-menu.tsx @@ -2,11 +2,10 @@ import { offset, Placement, useFloating, flip, arrow, shift } from '@floating-ui import clsx from 'clsx'; import { supportsPassiveEvents } from 'detect-passive-events'; import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { useHistory } from 'react-router-dom'; import ReactSwipeableViews from 'react-swipeable-views'; import { closeDropdownMenu as closeDropdownMenuRedux, openDropdownMenu } from 'pl-fe/actions/dropdown-menu'; +import { closeModal, openModal } from 'pl-fe/actions/modals'; import { useAppDispatch } from 'pl-fe/hooks'; import { userTouching } from 'pl-fe/is-mobile'; @@ -16,6 +15,13 @@ import DropdownMenuItem, { MenuItem } from './dropdown-menu-item'; type Menu = Array; +interface IDropdownMenuContent { + handleClose: () => any; + items?: Menu; + component?: React.FC<{ handleClose: () => any }>; + touchscreen?: boolean; +} + interface IDropdownMenu { children?: React.ReactElement; disabled?: boolean; @@ -31,134 +37,16 @@ interface IDropdownMenu { const listenerOptions = supportsPassiveEvents ? { passive: true } : false; -const DropdownMenu = (props: IDropdownMenu) => { - const { - children, - disabled, - items, - component: Component, - onClose, - onOpen, - onShiftClick, - placement: initialPlacement = 'top', - src = require('@tabler/icons/outline/dots.svg'), - title = 'Menu', - } = props; - - const dispatch = useAppDispatch(); - const history = useHistory(); - - const [isOpen, setIsOpen] = useState(false); - const [isDisplayed, setIsDisplayed] = useState(false); +const DropdownMenuContent: React.FC = ({ handleClose, items, component: Component, touchscreen }) => { const [tab, setTab] = useState(); + const ref = useRef(null); - const touching = userTouching.matches; - - const arrowRef = useRef(null); - const dropdownHistoryKey = useRef(); - const unlistenHistory = useRef>(); - - const { x, y, strategy, refs, middlewareData, placement } = useFloating({ - placement: initialPlacement, - middleware: [ - offset(12), - flip(), - shift({ - padding: 8, - }), - arrow({ - element: arrowRef, - }), - ], - }); - - const handleClick: React.EventHandler< - React.MouseEvent | React.KeyboardEvent - > = (event) => { - event.stopPropagation(); - - if (onShiftClick && event.shiftKey) { - event.preventDefault(); - - onShiftClick(event); - return; - } - - if (isOpen) { - handleClose(); - } else { - handleOpen(); - } - }; - - const handleOpen = () => { - dispatch(openDropdownMenu()); - setIsOpen(true); - setTab(undefined); - - if (onOpen) { - onOpen(); - } - }; - - const handleClose = (goBack: any = true) => { - (refs.reference.current as HTMLButtonElement)?.focus(); - - if (unlistenHistory.current) { - unlistenHistory.current(); - unlistenHistory.current = undefined; - } - const { state } = history.location; - if (goBack && state && (state as any).plFeDropdownKey && (state as any).plFeDropdownKey === dropdownHistoryKey.current) { - history.goBack(); - (history.location.state as any).plFeDropdownKey = undefined; - } - - closeDropdownMenu(); - setIsOpen(false); - - if (onClose) { - onClose(); - } - }; - - const closeDropdownMenu = () => { - dispatch((dispatch, getState) => { - const isOpenRedux = getState().dropdown_menu.isOpen; - - if (isOpenRedux) { - dispatch(closeDropdownMenuRedux()); - } - }); - }; - - const handleKeyPress: React.EventHandler> = (event) => { - switch (event.key) { - case ' ': - case 'Enter': - event.stopPropagation(); - event.preventDefault(); - handleClick(event); - break; - } - }; - - const handleDocumentClick = useMemo(() => (event: Event) => { - if (refs.floating.current && !refs.floating.current.contains(event.target as Node)) { - handleClose(); - event.stopPropagation(); - } - }, [refs.floating.current]); - - const handleExitSubmenu: React.EventHandler = (event) => { - event.stopPropagation(); - setTab(undefined); - }; + const autoFocus = items && !items.some((item) => item?.active); const handleKeyDown = useMemo(() => (e: KeyboardEvent) => { - if (!refs.floating.current) return; + if (!ref.current) return; - const items = Array.from(refs.floating.current.querySelectorAll('a, button')); + const items = Array.from(ref.current.querySelectorAll('a, button')); const index = items.indexOf(document.activeElement as any); let element = null; @@ -196,7 +84,187 @@ const DropdownMenu = (props: IDropdownMenu) => { e.preventDefault(); e.stopPropagation(); } - }, [refs.floating.current]); + }, [ref.current]); + + const handleDocumentClick = useMemo(() => (event: Event) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + handleClose(); + event.stopPropagation(); + } + }, [ref.current]); + + useEffect(() => { + if (!touchscreen) { + document.addEventListener('click', handleDocumentClick, false); + document.addEventListener('touchend', handleDocumentClick, listenerOptions); + } + document.addEventListener('keydown', handleKeyDown, false); + + return () => { + document.removeEventListener('click', handleDocumentClick); + document.removeEventListener('touchend', handleDocumentClick); + document.removeEventListener('keydown', handleKeyDown); + }; + }, [ref.current]); + + const handleExitSubmenu: React.EventHandler = (event) => { + event.stopPropagation(); + setTab(undefined); + }; + + const renderItems = (items: Menu | undefined) => ( +
    + {items?.map((item, idx) => ( + + ))} +
+ ); + + return ( +
+ {items?.some(item => item?.items?.length) ? ( + +
+ {Component && } + {(items?.length || touchscreen) && renderItems(items)} +
+
+ {tab !== undefined && ( + <> + + + {items![tab]?.text} + + {renderItems(items![tab]?.items)} + + )} +
+
+ ) : ( + <> + {Component && } + {(items?.length || touchscreen) && renderItems(items)} + + )} +
+ ); +}; + +const DropdownMenu = (props: IDropdownMenu) => { + const { + children, + disabled, + items, + component, + onClose, + onOpen, + onShiftClick, + placement: initialPlacement = 'top', + src = require('@tabler/icons/outline/dots.svg'), + title = 'Menu', + } = props; + + const dispatch = useAppDispatch(); + + const [isOpen, setIsOpen] = useState(false); + const [isDisplayed, setIsDisplayed] = useState(false); + + const arrowRef = useRef(null); + + const { x, y, strategy, refs, middlewareData, placement } = useFloating({ + placement: initialPlacement, + middleware: [ + offset(12), + flip(), + shift({ + padding: 8, + }), + arrow({ + element: arrowRef, + }), + ], + }); + + const handleClick: React.EventHandler< + React.MouseEvent | React.KeyboardEvent + > = (event) => { + event.stopPropagation(); + + if (onShiftClick && event.shiftKey) { + event.preventDefault(); + + onShiftClick(event); + return; + } + + if (isOpen) { + handleClose(); + } else { + handleOpen(); + } + }; + + const handleOpen = () => { + if (userTouching.matches) { + const handleClose = () => { + dispatch(closeModal('DROPDOWN_MENU')); + }; + dispatch(openModal('DROPDOWN_MENU', { + content: , + })); + } else { + dispatch(openDropdownMenu()); + setIsOpen(true); + } + + if (onOpen) { + onOpen(); + } + }; + + const handleClose = (goBack: any = true) => { + (refs.reference.current as HTMLButtonElement)?.focus(); + + closeDropdownMenu(); + setIsOpen(false); + + if (onClose) { + onClose(); + } + }; + + const closeDropdownMenu = () => { + dispatch((dispatch, getState) => { + const isOpenRedux = getState().dropdown_menu.isOpen; + + if (isOpenRedux) { + dispatch(closeDropdownMenuRedux()); + } + }); + }; + + const handleKeyPress: React.EventHandler> = (event) => { + switch (event.key) { + case ' ': + case 'Enter': + event.stopPropagation(); + event.preventDefault(); + handleClick(event); + break; + } + }; const arrowProps: React.CSSProperties = useMemo(() => { if (middlewareData.arrow) { @@ -228,84 +296,33 @@ const DropdownMenu = (props: IDropdownMenu) => { closeDropdownMenu(); }, []); - useEffect(() => { - if (isOpen) { - document.addEventListener('click', handleDocumentClick, false); - document.addEventListener('keydown', handleKeyDown, false); - document.addEventListener('touchend', handleDocumentClick, listenerOptions); - - return () => { - document.removeEventListener('click', handleDocumentClick); - document.removeEventListener('keydown', handleKeyDown); - document.removeEventListener('touchend', handleDocumentClick); - }; - } - }, [isOpen, refs.floating.current]); - useEffect(() => { setTimeout(() => setIsDisplayed(isOpen), isOpen ? 0 : 150); - if (isOpen && touching) { - const { pathname, state } = history.location; - - dropdownHistoryKey.current = Date.now(); - - history.push(pathname, { ...(state as any), plFeDropdownKey: dropdownHistoryKey.current }); - - unlistenHistory.current = history.listen(({ state }, action) => { - if (!(state as any)?.plFeDropdownKey) { - handleClose(false); - } else if (action === 'POP') { - handleClose(false); - } - }); - } }, [isOpen]); - if (items?.length === 0 && !Component) { + if (items?.length === 0 && !component) { return null; } - const autoFocus = items && !items.some((item) => item?.active); - const getClassName = () => { - const className = clsx('z-[1001] bg-white py-1 shadow-lg ease-in-out focus:outline-none black:bg-black no-reduce-motion:transition-all dark:bg-gray-900 dark:ring-2 dark:ring-primary-700', - touching ? clsx({ - 'overflow-auto fixed left-0 right-0 mx-auto w-[calc(100vw-2rem)] max-w-lg max-h-[calc(100dvh-1rem)] rounded-t-xl duration-200': true, - 'bottom-0 opacity-100': isDisplayed && isOpen, - '-bottom-32 opacity-0': !(isDisplayed && isOpen), - }) : clsx({ - 'rounded-md min-w-56 max-w-sm duration-100': true, - 'scale-0': !(isDisplayed && isOpen), - 'scale-100': isDisplayed && isOpen, - 'origin-bottom': placement === 'top', - 'origin-left': placement === 'right', - 'origin-top': placement === 'bottom', - 'origin-right': placement === 'left', - 'origin-bottom-left': placement === 'top-start', - 'origin-bottom-right': placement === 'top-end', - 'origin-top-left': placement === 'bottom-start', - 'origin-top-right': placement === 'bottom-end', - })); + const className = clsx('z-[1001] bg-white py-1 shadow-lg ease-in-out focus:outline-none black:bg-black no-reduce-motion:transition-all dark:bg-gray-900 dark:ring-2 dark:ring-primary-700', { + 'rounded-md min-w-56 max-w-sm duration-100': true, + 'scale-0': !(isDisplayed && isOpen), + 'scale-100': isDisplayed && isOpen, + 'origin-bottom': placement === 'top', + 'origin-left': placement === 'right', + 'origin-top': placement === 'bottom', + 'origin-right': placement === 'left', + 'origin-bottom-left': placement === 'top-start', + 'origin-bottom-right': placement === 'top-end', + 'origin-top-left': placement === 'bottom-start', + 'origin-top-right': placement === 'bottom-end', + }); return className; }; - const renderItems = (items: Menu | undefined) => ( -
    - {items?.map((item, idx) => ( - - ))} -
- ); - return ( <> {children ? ( @@ -332,74 +349,24 @@ const DropdownMenu = (props: IDropdownMenu) => { {isOpen || isDisplayed ? ( - {touching && ( -
- )}
- {items?.some(item => item?.items?.length) ? ( - -
- {Component && } - {(items?.length || touching) && renderItems(items)} -
-
- {tab !== undefined && ( - <> - - - {items![tab]?.text} - - {renderItems(items![tab]?.items)} - - )} -
-
- ) : ( - <> - {Component && } - {(items?.length || touching) && renderItems(items)} - - )} - - {touching && ( -
- -
- )} + {/* Arrow */} - {!touching && ( -
- )} +
) : null} diff --git a/packages/pl-fe/src/components/modal-root.tsx b/packages/pl-fe/src/components/modal-root.tsx index 862d1f04c..e3d34e84e 100644 --- a/packages/pl-fe/src/components/modal-root.tsx +++ b/packages/pl-fe/src/components/modal-root.tsx @@ -8,8 +8,7 @@ import { cancelReplyCompose } from 'pl-fe/actions/compose'; import { saveDraftStatus } from 'pl-fe/actions/draft-statuses'; import { cancelEventCompose } from 'pl-fe/actions/events'; import { openModal, closeModal } from 'pl-fe/actions/modals'; -import { useAppDispatch, useAppSelector, usePrevious } from 'pl-fe/hooks'; -import { userTouching } from 'pl-fe/is-mobile'; +import { useAppDispatch, usePrevious } from 'pl-fe/hooks'; import type { ModalType } from 'pl-fe/features/ui/components/modal-root'; import type { ReducerCompose } from 'pl-fe/reducers/compose'; @@ -50,8 +49,6 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) const dispatch = useAppDispatch(); const [revealed, setRevealed] = useState(!!children); - const isDropdownOpen = useAppSelector(state => state.dropdown_menu.isOpen); - const wasDropdownOpen = usePrevious(isDropdownOpen); const ref = useRef(null); const activeElement = useRef(revealed ? document.activeElement as HTMLDivElement | null : null); @@ -148,9 +145,7 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) const handleModalOpen = () => { modalHistoryKey.current = Date.now(); unlistenHistory.current = history.listen(({ state }, action) => { - if ((state as any)?.plFeDropdownKey) { - return; - } else if (!(state as any)?.plFeModalKey) { + if (!(state as any)?.plFeModalKey) { onClose(); } else if (action === 'POP') { handleOnClose(); @@ -220,17 +215,6 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) } }, [children]); - useEffect(() => { - if (!userTouching.matches) return; - - if (isDropdownOpen && unlistenHistory.current) { - unlistenHistory.current(); - } else if (!isDropdownOpen && wasDropdownOpen) { - // TODO find a better solution - setTimeout(() => handleModalOpen(), 50); - } - }, [isDropdownOpen]); - if (!visible) { return (
@@ -248,7 +232,9 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type })