Move mobile dropdown menu back to modals
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
@ -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<typeof openModalSuccess>
|
||||
ReturnType<typeof openModal>
|
||||
| ReturnType<typeof closeModal>;
|
||||
|
||||
export {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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<MenuItem | null>;
|
||||
|
||||
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<boolean>(false);
|
||||
const [isDisplayed, setIsDisplayed] = useState<boolean>(false);
|
||||
const DropdownMenuContent: React.FC<IDropdownMenuContent> = ({ handleClose, items, component: Component, touchscreen }) => {
|
||||
const [tab, setTab] = useState<number>();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const touching = userTouching.matches;
|
||||
|
||||
const arrowRef = useRef<HTMLDivElement>(null);
|
||||
const dropdownHistoryKey = useRef<number>();
|
||||
const unlistenHistory = useRef<ReturnType<typeof history.listen>>();
|
||||
|
||||
const { x, y, strategy, refs, middlewareData, placement } = useFloating<HTMLButtonElement>({
|
||||
placement: initialPlacement,
|
||||
middleware: [
|
||||
offset(12),
|
||||
flip(),
|
||||
shift({
|
||||
padding: 8,
|
||||
}),
|
||||
arrow({
|
||||
element: arrowRef,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const handleClick: React.EventHandler<
|
||||
React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
|
||||
> = (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<React.KeyboardEvent<HTMLButtonElement>> = (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<any> = (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<any> = (event) => {
|
||||
event.stopPropagation();
|
||||
setTab(undefined);
|
||||
};
|
||||
|
||||
const renderItems = (items: Menu | undefined) => (
|
||||
<ul>
|
||||
{items?.map((item, idx) => (
|
||||
<DropdownMenuItem
|
||||
key={idx}
|
||||
item={item}
|
||||
index={idx}
|
||||
onClick={handleClose}
|
||||
autoFocus={autoFocus}
|
||||
onSetTab={setTab}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{items?.some(item => item?.items?.length) ? (
|
||||
<ReactSwipeableViews animateHeight index={tab === undefined ? 0 : 1}>
|
||||
<div className={clsx({ 'w-full': touchscreen })}>
|
||||
{Component && <Component handleClose={handleClose} />}
|
||||
{(items?.length || touchscreen) && renderItems(items)}
|
||||
</div>
|
||||
<div className={clsx({ 'w-full': touchscreen, 'fit-content': !touchscreen })}>
|
||||
{tab !== undefined && (
|
||||
<>
|
||||
<HStack className='mx-2 my-1 text-gray-700 dark:text-gray-300' space={3} alignItems='center'>
|
||||
<IconButton
|
||||
theme='transparent'
|
||||
src={require('@tabler/icons/outline/arrow-left.svg')}
|
||||
iconClassName='h-5 w-5'
|
||||
onClick={handleExitSubmenu}
|
||||
/>
|
||||
{items![tab]?.text}
|
||||
</HStack>
|
||||
{renderItems(items![tab]?.items)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ReactSwipeableViews>
|
||||
) : (
|
||||
<>
|
||||
{Component && <Component handleClose={handleClose} />}
|
||||
{(items?.length || touchscreen) && renderItems(items)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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<boolean>(false);
|
||||
const [isDisplayed, setIsDisplayed] = useState<boolean>(false);
|
||||
|
||||
const arrowRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { x, y, strategy, refs, middlewareData, placement } = useFloating<HTMLButtonElement>({
|
||||
placement: initialPlacement,
|
||||
middleware: [
|
||||
offset(12),
|
||||
flip(),
|
||||
shift({
|
||||
padding: 8,
|
||||
}),
|
||||
arrow({
|
||||
element: arrowRef,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const handleClick: React.EventHandler<
|
||||
React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
|
||||
> = (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: <DropdownMenuContent handleClose={handleClose} items={items} component={component} touchscreen />,
|
||||
}));
|
||||
} 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<React.KeyboardEvent<HTMLButtonElement>> = (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) => (
|
||||
<ul>
|
||||
{items?.map((item, idx) => (
|
||||
<DropdownMenuItem
|
||||
key={idx}
|
||||
item={item}
|
||||
index={idx}
|
||||
onClick={handleClose}
|
||||
autoFocus={autoFocus}
|
||||
onSetTab={setTab}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{children ? (
|
||||
@ -332,74 +349,24 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
||||
|
||||
{isOpen || isDisplayed ? (
|
||||
<Portal>
|
||||
{touching && (
|
||||
<div
|
||||
className={clsx('fixed inset-0 z-[1000] bg-gray-500 black:bg-gray-900 no-reduce-motion:transition-opacity dark:bg-gray-700', {
|
||||
'opacity-0': !(isOpen && isDisplayed),
|
||||
'opacity-60': (isOpen && isDisplayed),
|
||||
})}
|
||||
role='button'
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
data-testid='dropdown-menu'
|
||||
className={getClassName()}
|
||||
ref={refs.setFloating}
|
||||
style={touching ? undefined : {
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
}}
|
||||
>
|
||||
{items?.some(item => item?.items?.length) ? (
|
||||
<ReactSwipeableViews animateHeight index={tab === undefined ? 0 : 1}>
|
||||
<div className={clsx({ 'w-full': touching })}>
|
||||
{Component && <Component handleClose={handleClose} />}
|
||||
{(items?.length || touching) && renderItems(items)}
|
||||
</div>
|
||||
<div className={clsx({ 'w-full': touching, 'fit-content': !touching })}>
|
||||
{tab !== undefined && (
|
||||
<>
|
||||
<HStack className='mx-2 my-1 text-gray-700 dark:text-gray-300' space={3} alignItems='center'>
|
||||
<IconButton
|
||||
theme='transparent'
|
||||
src={require('@tabler/icons/outline/arrow-left.svg')}
|
||||
iconClassName='h-5 w-5'
|
||||
onClick={handleExitSubmenu}
|
||||
/>
|
||||
{items![tab]?.text}
|
||||
</HStack>
|
||||
{renderItems(items![tab]?.items)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ReactSwipeableViews>
|
||||
) : (
|
||||
<>
|
||||
{Component && <Component handleClose={handleClose} />}
|
||||
{(items?.length || touching) && renderItems(items)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{touching && (
|
||||
<div className='p-2 px-3'>
|
||||
<button
|
||||
className='flex w-full appearance-none place-content-center items-center justify-center rounded-full border border-gray-700 bg-transparent p-2 text-sm font-medium text-gray-700 transition-all hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:border-gray-500 dark:text-gray-500'
|
||||
onClick={handleClose}
|
||||
>
|
||||
<FormattedMessage id='lightbox.close' defaultMessage='Close' />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<DropdownMenuContent handleClose={handleClose} items={items} component={component} />
|
||||
|
||||
{/* Arrow */}
|
||||
{!touching && (
|
||||
<div
|
||||
ref={arrowRef}
|
||||
style={arrowProps}
|
||||
className='pointer-events-none absolute z-[-1] h-3 w-3 bg-white black:bg-black dark:bg-gray-900'
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
ref={arrowRef}
|
||||
style={arrowProps}
|
||||
className='pointer-events-none absolute z-[-1] h-3 w-3 bg-white black:bg-black dark:bg-gray-900'
|
||||
/>
|
||||
</div>
|
||||
</Portal>
|
||||
) : null}
|
||||
|
||||
@ -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<IModalRoot> = ({ 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<HTMLDivElement>(null);
|
||||
const activeElement = useRef<HTMLDivElement | null>(revealed ? document.activeElement as HTMLDivElement | null : null);
|
||||
@ -148,9 +145,7 @@ const ModalRoot: React.FC<IModalRoot> = ({ 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<IModalRoot> = ({ 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 (
|
||||
<div className='z-50 transition-all' ref={ref} style={{ opacity: 0 }} />
|
||||
@ -248,7 +232,9 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
||||
<div
|
||||
role='presentation'
|
||||
id='modal-overlay'
|
||||
className='fixed inset-0 bg-gray-500/90 backdrop-blur black:bg-gray-900/90 dark:bg-gray-700/90'
|
||||
className={clsx('fixed inset-0 bg-gray-500/90 black:bg-gray-900/90 dark:bg-gray-700/90', {
|
||||
'opacity-60': type === 'DROPDOWN_MENU',
|
||||
})}
|
||||
onClick={handleOnClose}
|
||||
/>
|
||||
|
||||
@ -257,7 +243,7 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
||||
className={clsx({
|
||||
'my-2 mx-auto relative pointer-events-none flex items-center min-h-[calc(100%-3.5rem)]': true,
|
||||
'p-4 md:p-0': type !== 'MEDIA',
|
||||
'!my-0': type === 'MEDIA',
|
||||
'!my-0': type === 'MEDIA' || type === 'DROPDOWN_MENU',
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -22,6 +22,7 @@ const MODAL_COMPONENTS = {
|
||||
CREATE_GROUP: lazy(() => import('pl-fe/features/ui/components/modals/manage-group-modal')),
|
||||
CRYPTO_DONATE: lazy(() => import('pl-fe/features/ui/components/modals/crypto-donate-modal')),
|
||||
DISLIKES: lazy(() => import('pl-fe/features/ui/components/modals/dislikes-modal')),
|
||||
DROPDOWN_MENU: lazy(() => import('pl-fe/features/ui/components/modals/dropdown-menu-modal')),
|
||||
EDIT_ANNOUNCEMENT: lazy(() => import('pl-fe/features/ui/components/modals/edit-announcement-modal')),
|
||||
EDIT_BOOKMARK_FOLDER: lazy(() => import('pl-fe/features/ui/components/modals/edit-bookmark-folder-modal')),
|
||||
EDIT_DOMAIN: lazy(() => import('pl-fe/features/ui/components/modals/edit-domain-modal')),
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { BaseModalProps } from '../modal-root';
|
||||
|
||||
interface DropdownMenuModalProps {
|
||||
content?: JSX.Element;
|
||||
}
|
||||
|
||||
const DropdownMenuModal: React.FC<BaseModalProps & DropdownMenuModalProps> = ({ content, onClose }) => {
|
||||
const handleClick: React.MouseEventHandler<HTMLElement> = (e) => {
|
||||
onClose('DROPDOWN_MENU');
|
||||
e.stopPropagation();
|
||||
};
|
||||
const [firstRender, setFirstRender] = React.useState(true);
|
||||
|
||||
const handleClickOutside: React.MouseEventHandler<HTMLElement> = (e) => {
|
||||
if ((e.target as HTMLElement).id === 'dropdown-menu-modal') {
|
||||
handleClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
setFirstRender(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
id='dropdown-menu-modal'
|
||||
className='absolute inset-0'
|
||||
role='presentation'
|
||||
onClick={handleClickOutside}
|
||||
>
|
||||
<div
|
||||
className={clsx('pointer-events-auto fixed inset-x-0 z-[1001] mx-auto max-h-[calc(100dvh-1rem)] w-[calc(100vw-2rem)] max-w-lg overflow-auto rounded-t-xl bg-white py-1 shadow-lg duration-200 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', {
|
||||
'bottom-0 opacity-100': !firstRender,
|
||||
'-bottom-32 opacity-0': firstRender,
|
||||
})}
|
||||
>
|
||||
{content}
|
||||
<div className='p-2 px-3'>
|
||||
<button
|
||||
className='flex w-full appearance-none place-content-center items-center justify-center rounded-full border border-gray-700 bg-transparent p-2 text-sm font-medium text-gray-700 transition-all hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:border-gray-500 dark:text-gray-500'
|
||||
onClick={handleClick}
|
||||
>
|
||||
<FormattedMessage id='lightbox.close' defaultMessage='Close' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { DropdownMenuModal as default, type DropdownMenuModalProps };
|
||||
@ -36,7 +36,7 @@ const toSimplePolicy = (configs: ImmutableList<Config>): MRFSimple => {
|
||||
}
|
||||
};
|
||||
|
||||
const fromSimplePolicy = (simplePolicy: Policy)=> {
|
||||
const fromSimplePolicy = (simplePolicy: Policy) => {
|
||||
const mapper = ([key, hosts]: [key: string, hosts: ImmutableList<string>]) => ({ tuple: [`:${key}`, hosts] });
|
||||
|
||||
const value = Object.entries(simplePolicy).map(mapper);
|
||||
|
||||
Reference in New Issue
Block a user