From fd194938a632186511e358f5af3924a9354c2a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 16 Aug 2024 15:17:27 +0200 Subject: [PATCH 1/9] Dropdown menu mobile variant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../dropdown-menu/dropdown-menu.tsx | 124 +++++++++--------- src/components/sidebar-menu.tsx | 2 +- .../ui/components/modals/actions-modal.tsx | 11 +- 3 files changed, 63 insertions(+), 74 deletions(-) diff --git a/src/components/dropdown-menu/dropdown-menu.tsx b/src/components/dropdown-menu/dropdown-menu.tsx index 528044066..821a3bbf7 100644 --- a/src/components/dropdown-menu/dropdown-menu.tsx +++ b/src/components/dropdown-menu/dropdown-menu.tsx @@ -2,10 +2,9 @@ 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 { useHistory } from 'react-router-dom'; +import { FormattedMessage } from 'react-intl'; import { closeDropdownMenu as closeDropdownMenuRedux, openDropdownMenu } from 'soapbox/actions/dropdown-menu'; -import { closeModal, openModal } from 'soapbox/actions/modals'; import { useAppDispatch } from 'soapbox/hooks'; import { userTouching } from 'soapbox/is-mobile'; @@ -43,15 +42,15 @@ const DropdownMenu = (props: IDropdownMenu) => { placement: initialPlacement = 'top', src = require('@tabler/icons/outline/dots.svg'), title = 'Menu', - ...filteredProps } = props; const dispatch = useAppDispatch(); - const history = useHistory(); const [isOpen, setIsOpen] = useState(false); const [isDisplayed, setIsDisplayed] = useState(false); + const touching = userTouching.matches; + const arrowRef = useRef(null); const { x, y, strategy, refs, middlewareData, placement } = useFloating({ @@ -91,18 +90,8 @@ const DropdownMenu = (props: IDropdownMenu) => { * On mobile screens, let's replace the Popper dropdown with a Modal. */ const handleOpen = () => { - if (userTouching.matches) { - dispatch( - openModal('ACTIONS', { - status: filteredProps.status, - actions: items, - onClick: handleItemClick, - }), - ); - } else { - dispatch(openDropdownMenu()); - setIsOpen(true); - } + dispatch(openDropdownMenu()); + setIsOpen(true); if (onOpen) { onOpen(); @@ -112,12 +101,8 @@ const DropdownMenu = (props: IDropdownMenu) => { const handleClose = () => { (refs.reference.current as HTMLButtonElement)?.focus(); - if (userTouching.matches) { - dispatch(closeModal('ACTIONS')); - } else { - closeDropdownMenu(); - setIsOpen(false); - } + closeDropdownMenu(); + setIsOpen(false); if (onClose) { onClose(); @@ -145,26 +130,6 @@ const DropdownMenu = (props: IDropdownMenu) => { } }; - const handleItemClick: React.EventHandler = (event) => { - event.preventDefault(); - event.stopPropagation(); - - const i = Number(event.currentTarget.getAttribute('data-index')); - const item = items[i]; - if (!item) return; - - const { action, to } = item; - - handleClose(); - - if (typeof action === 'function') { - action(event); - } else if (to) { - dispatch(closeModal('MEDIA')); - history.push(to); - } - }; - const handleDocumentClick = (event: Event) => { if (refs.floating.current && !refs.floating.current.contains(event.target as Node)) { handleClose(); @@ -174,7 +139,7 @@ const DropdownMenu = (props: IDropdownMenu) => { const handleKeyDown = (e: KeyboardEvent) => { if (!refs.floating.current) return; - const items = Array.from(refs.floating.current.getElementsByTagName('a')); + const items = Array.from(refs.floating.current.querySelectorAll('a, button')); const index = items.indexOf(document.activeElement as any); let element = null; @@ -205,7 +170,7 @@ const DropdownMenu = (props: IDropdownMenu) => { } if (element) { - element.focus(); + (element as HTMLAnchorElement).focus(); e.preventDefault(); e.stopPropagation(); } @@ -265,6 +230,28 @@ const DropdownMenu = (props: IDropdownMenu) => { const autoFocus = !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-hidden fixed left-0 right-0 mx-auto w-[calc(100vw-2rem)] max-w-lg rounded-t-xl duration-200': true, + 'bottom-0 opacity-100': isDisplayed && isOpen, + '-bottom-32 opacity-0': !(isDisplayed && isOpen), + }) : clsx({ + 'rounded-md w-56 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; + }; + return ( <> {children ? ( @@ -291,24 +278,21 @@ const DropdownMenu = (props: IDropdownMenu) => { {isOpen || isDisplayed ? ( + {touching && ( +
+ )}
{ autoFocus={autoFocus} /> ))} + {touching && ( +
  • + +
  • + )} {/* Arrow */} -
    + {!touching && ( +
    + )}
    ) : null} diff --git a/src/components/sidebar-menu.tsx b/src/components/sidebar-menu.tsx index 0fd14418f..25b401bc1 100644 --- a/src/components/sidebar-menu.tsx +++ b/src/components/sidebar-menu.tsx @@ -183,7 +183,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => { >
    void; onClose: () => void; } -const ActionsModal: React.FC = ({ status, actions, onClick, onClose }) => { +const ActionsModal: React.FC = ({ actions, onClick, onClose }) => { const renderAction = (action: MenuItem | null, i: number) => { if (action === null) { return
  • ; @@ -60,11 +57,7 @@ const ActionsModal: React.FC = ({ status, actions, onClick, onClo className='pointer-events-auto relative z-[9999] m-auto flex max-h-[calc(100vh-3rem)] w-full max-w-lg flex-col overflow-hidden rounded-2xl bg-white text-gray-400 shadow-xl black:bg-black dark:bg-gray-900' style={{ top: `${top}%` }} > - {status && ( - - )} - -
      +
        {actions && actions.map(renderAction)}
      • From bc1853f4d8403abb0819d2e3e94deaf63aa5910d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 16 Aug 2024 15:22:50 +0200 Subject: [PATCH 2/9] Use DropdownMenu for events page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../dropdown-menu/dropdown-menu.tsx | 3 -- .../event/components/event-header.tsx | 35 ++++--------------- 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/src/components/dropdown-menu/dropdown-menu.tsx b/src/components/dropdown-menu/dropdown-menu.tsx index 821a3bbf7..fc61a4e8a 100644 --- a/src/components/dropdown-menu/dropdown-menu.tsx +++ b/src/components/dropdown-menu/dropdown-menu.tsx @@ -86,9 +86,6 @@ const DropdownMenu = (props: IDropdownMenu) => { } }; - /** - * On mobile screens, let's replace the Popper dropdown with a Modal. - */ const handleOpen = () => { dispatch(openDropdownMenu()); setIsOpen(true); diff --git a/src/features/event/components/event-header.tsx b/src/features/event/components/event-header.tsx index 6214fcdf2..1ce402867 100644 --- a/src/features/event/components/event-header.tsx +++ b/src/features/event/components/event-header.tsx @@ -12,10 +12,10 @@ import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions import { initMuteModal } from 'soapbox/actions/mutes'; import { initReport, ReportableEntities } from 'soapbox/actions/reports'; import { deleteStatus } from 'soapbox/actions/statuses'; +import DropdownMenu, { type Menu as MenuType } from 'soapbox/components/dropdown-menu'; import Icon from 'soapbox/components/icon'; import StillImage from 'soapbox/components/still-image'; -import { Button, HStack, IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuLink, MenuList, Stack, Text } from 'soapbox/components/ui'; -import SvgIcon from 'soapbox/components/ui/icon/svg-icon'; +import { Button, HStack, IconButton, Stack, Text } from 'soapbox/components/ui'; import VerificationBadge from 'soapbox/components/verification-badge'; import { useAppDispatch, useFeatures, useOwnAccount, useSettings } from 'soapbox/hooks'; import { useChats } from 'soapbox/queries/chats'; @@ -27,7 +27,6 @@ import PlaceholderEventHeader from '../../placeholder/components/placeholder-eve import EventActionButton from '../components/event-action-button'; import EventDate from '../components/event-date'; -import type { Menu as MenuType } from 'soapbox/components/dropdown-menu'; import type { Status as StatusEntity } from 'soapbox/types/entities'; const messages = defineMessages({ @@ -379,39 +378,17 @@ const EventHeader: React.FC = ({ status }) => { {event.name} - - + + - - {makeMenu().map((menuItem, idx) => { - if (typeof menuItem?.text === 'undefined') { - return ; - } else { - const Comp = (menuItem.href ? MenuLink : MenuItem) as any; - const itemProps = menuItem.href ? { href: menuItem.href, target: menuItem.target || '_self' } : { onSelect: menuItem.action }; - - return ( - -
        - {menuItem.icon && ( - - )} - -
        {menuItem.text}
        -
        -
        - ); - } - })} -
        -
        {account.id === ownAccount?.id ? ( + ) : ( + + ) + )} + + ); + })} +
  • + + ); +}; + +interface ILanguageDropdownButton { + composeId: string; +} + +const LanguageDropdownButton: React.FC = ({ composeId }) => { + const intl = useIntl(); + + const { + language, + modified_language: modifiedLanguage, + suggested_language: suggestedLanguage, + textMap, + } = useCompose(composeId); + let buttonLabel = intl.formatMessage(messages.languagePrompt); if (language) { const list: string[] = [languagesObject[modifiedLanguage || language]]; @@ -311,8 +272,12 @@ const LanguageDropdown: React.FC = ({ composeId }) => { language: languagesObject[suggestedLanguage as Language] || suggestedLanguage, }); + const LanguageDropdown = useMemo(() => getLanguageDropdown(composeId), [composeId]); + return ( - <> + - ) : ( - - ) - )} -
    - ); - })} -
    -
    -
    - - ) : null} - + ); }; -export { LanguageDropdown as default }; +export { LanguageDropdownButton as default }; diff --git a/src/features/compose/components/privacy-dropdown.tsx b/src/features/compose/components/privacy-dropdown.tsx index a194aac21..6f98f8f56 100644 --- a/src/features/compose/components/privacy-dropdown.tsx +++ b/src/features/compose/components/privacy-dropdown.tsx @@ -1,21 +1,15 @@ import clsx from 'clsx'; import { supportsPassiveEvents } from 'detect-passive-events'; -import React, { useState, useRef, useEffect } from 'react'; +import React, { useRef, useEffect } from 'react'; import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; -import { spring } from 'react-motion'; -// @ts-ignore -import Overlay from 'react-overlays/lib/Overlay'; import { changeComposeFederated, changeComposeVisibility } from 'soapbox/actions/compose'; -import { closeModal, openModal } from 'soapbox/actions/modals'; +import DropdownMenu from 'soapbox/components/dropdown-menu'; import Icon from 'soapbox/components/icon'; import { Button, Toggle } from 'soapbox/components/ui'; import { useAppDispatch, useCompose, useFeatures, useInstance } from 'soapbox/hooks'; -import { userTouching } from 'soapbox/is-mobile'; import { GOTOSOCIAL, parseVersion, PLEROMA } from 'soapbox/utils/features'; -import Motion from '../../ui/util/optional-motion'; - const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' }, @@ -44,10 +38,8 @@ interface Option { } interface IPrivacyDropdownMenu { - style?: React.CSSProperties; items: any[]; value: string; - placement: string; onClose: () => void; onChange: (value: string | null) => void; unavailable?: boolean; @@ -57,12 +49,10 @@ interface IPrivacyDropdownMenu { } const PrivacyDropdownMenu: React.FC = ({ - style, items, placement, value, onClose, onChange, showFederated, federated, onChangeFederated, + items, value, onClose, onChange, showFederated, federated, onChangeFederated, }) => { - const node = useRef(null); - const focusedItem = useRef(null); - - const [mounted, setMounted] = useState(false); + const node = useRef(null); + const focusedItem = useRef(null); const handleDocumentClick = (e: MouseEvent | TouchEvent) => { if (node.current && !node.current.contains(e.target as HTMLElement)) { @@ -71,7 +61,8 @@ const PrivacyDropdownMenu: React.FC = ({ }; const handleKeyDown: React.KeyboardEventHandler = e => { - const index = [...e.currentTarget.parentElement!.children].indexOf(e.currentTarget); let element: ChildNode | null | undefined = null; + const index = [...e.currentTarget.parentElement!.children].indexOf(e.currentTarget); + let element: ChildNode | null | undefined = null; switch (e.key) { case 'Escape': @@ -127,7 +118,6 @@ const PrivacyDropdownMenu: React.FC = ({ document.addEventListener('touchend', handleDocumentClick, listenerOptions); focusedItem.current?.focus({ preventScroll: true }); - setMounted(true); return () => { document.removeEventListener('click', handleDocumentClick, false); @@ -136,82 +126,66 @@ const PrivacyDropdownMenu: React.FC = ({ }, []); return ( - - {({ opacity, scaleX, scaleY }) => ( - // It should not be transformed when mounting because the resulting - // size will be used to determine the coordinate of the menu by - // react-overlays -
    - {items.map(item => { - const active = item.value === value; - return ( -
    -
    - -
    - -
    - {item.text} - {item.meta} -
    -
    - ); - })} - {showFederated && ( -
    -
    - -
    - -
    - - - - -
    - - +
      + {items.map(item => { + const active = item.value === value; + return ( +
    • +
      +
      - )} -
    + +
    + {item.text} + {item.meta} +
    + + ); + })} + {showFederated && ( +
  • +
    + +
    + +
    + + + + +
    + + +
  • )} - + + ); }; @@ -224,8 +198,6 @@ const PrivacyDropdown: React.FC = ({ }) => { const dispatch = useAppDispatch(); const intl = useIntl(); - const node = useRef(null); - const activeElement = useRef(null); const instance = useInstance(); const features = useFeatures(); @@ -236,9 +208,6 @@ const PrivacyDropdown: React.FC = ({ const value = compose.privacy; const unavailable = compose.id; - const [open, setOpen] = useState(false); - const [placement, setPlacement] = useState('bottom'); - const options = [ { icon: require('@tabler/icons/outline/world.svg'), value: 'public', text: intl.formatMessage(messages.public_short), meta: intl.formatMessage(messages.public_long) }, { icon: require('@tabler/icons/outline/lock-open.svg'), value: 'unlisted', text: intl.formatMessage(messages.unlisted_short), meta: intl.formatMessage(messages.unlisted_long) }, @@ -252,70 +221,6 @@ const PrivacyDropdown: React.FC = ({ const onChangeFederated = () => dispatch(changeComposeFederated(composeId)); - const onModalOpen = (props: Record) => dispatch(openModal('ACTIONS', props)); - - const onModalClose = () => dispatch(closeModal('ACTIONS')); - - const handleToggle: React.MouseEventHandler = (e) => { - if (userTouching.matches) { - if (open) { - onModalClose(); - } else { - onModalOpen({ - actions: options.map(option => ({ ...option, active: option.value === value })), - onClick: handleModalActionClick, - }); - } - } else { - const { top } = e.currentTarget.getBoundingClientRect(); - if (open) { - activeElement.current?.focus(); - } - setPlacement(top * 2 < innerHeight ? 'bottom' : 'top'); - setOpen(!open); - } - e.stopPropagation(); - }; - - const handleModalActionClick: React.MouseEventHandler = (e) => { - e.preventDefault(); - - const { value } = options[e.currentTarget.getAttribute('data-index') as any]; - - onModalClose(); - onChange(value); - }; - - const handleKeyDown: React.KeyboardEventHandler = e => { - switch (e.key) { - case 'Escape': - handleClose(); - break; - } - }; - - const handleMouseDown = () => { - if (!open) { - activeElement.current = document.activeElement as HTMLElement | null; - } - }; - - const handleButtonKeyDown: React.KeyboardEventHandler = (e) => { - switch (e.key) { - case ' ': - case 'Enter': - handleMouseDown(); - break; - } - }; - - const handleClose = () => { - if (open) { - activeElement.current?.focus(); - } - setOpen(false); - }; - if (unavailable) { return null; } @@ -323,41 +228,30 @@ const PrivacyDropdown: React.FC = ({ const valueOption = options.find(item => item.value === value); return ( -
    -
    -
    - - + ( - -
    + )} + > + diff --git a/src/features/compose/components/language-dropdown.tsx b/src/features/compose/components/language-dropdown.tsx index 68c96a2b0..5192f30a4 100644 --- a/src/features/compose/components/language-dropdown.tsx +++ b/src/features/compose/components/language-dropdown.tsx @@ -52,39 +52,6 @@ const getLanguageDropdown = (composeId: string): React.FC => textMap, } = useCompose(composeId); - const handleOptionKeyDown: React.KeyboardEventHandler = e => { - const value = e.currentTarget.getAttribute('data-index'); - const index = results.findIndex(([key]) => key === value); - let element: ChildNode | null | undefined = null; - - switch (e.key) { - case 'Escape': - handleClose(); - break; - case 'Enter': - handleOptionClick(e); - break; - case 'ArrowDown': - element = node.current?.childNodes[index + 1] || node.current?.firstChild; - break; - case 'ArrowUp': - element = node.current?.childNodes[index - 1] || node.current?.lastChild; - break; - case 'Home': - element = node.current?.firstChild; - break; - case 'End': - element = node.current?.lastChild; - break; - } - - if (element) { - (element as HTMLElement).focus(); - e.preventDefault(); - e.stopPropagation(); - } - }; - const handleOptionClick: React.EventHandler = (e: MouseEvent | KeyboardEvent) => { const value = (e.currentTarget as HTMLElement)?.getAttribute('data-index') as Language; @@ -207,7 +174,6 @@ const getLanguageDropdown = (composeId: string): React.FC => tabIndex={0} key={code} data-index={code} - onKeyDown={handleOptionKeyDown} onClick={handleOptionClick} className={clsx( 'flex w-full gap-2 p-2.5 text-left text-sm text-gray-700 dark:text-gray-400', From d6fba3ad13d18e9402ab1cc5111b7e439531dc68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 16 Aug 2024 17:41:51 +0200 Subject: [PATCH 6/9] DropdownMenu: close on navigation back, i don't understand this code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../dropdown-menu/dropdown-menu.tsx | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/components/dropdown-menu/dropdown-menu.tsx b/src/components/dropdown-menu/dropdown-menu.tsx index dec0a5960..8a2d0da4e 100644 --- a/src/components/dropdown-menu/dropdown-menu.tsx +++ b/src/components/dropdown-menu/dropdown-menu.tsx @@ -3,6 +3,7 @@ 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 { closeDropdownMenu as closeDropdownMenuRedux, openDropdownMenu } from 'soapbox/actions/dropdown-menu'; import { useAppDispatch } from 'soapbox/hooks'; @@ -47,6 +48,7 @@ const DropdownMenu = (props: IDropdownMenu) => { } = props; const dispatch = useAppDispatch(); + const history = useHistory(); const [isOpen, setIsOpen] = useState(false); const [isDisplayed, setIsDisplayed] = useState(false); @@ -54,6 +56,8 @@ const DropdownMenu = (props: IDropdownMenu) => { const touching = userTouching.matches; const arrowRef = useRef(null); + const dropdownHistoryKey = useRef(); + const unlistenHistory = useRef>(); const { x, y, strategy, refs, middlewareData, placement } = useFloating({ placement: initialPlacement, @@ -97,9 +101,18 @@ const DropdownMenu = (props: IDropdownMenu) => { } }; - const handleClose = () => { + 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).soapboxDropdownKey === dropdownHistoryKey.current) { + history.goBack(); + } + closeDropdownMenu(); setIsOpen(false); @@ -221,6 +234,22 @@ const DropdownMenu = (props: IDropdownMenu) => { 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), soapboxDropdownKey: dropdownHistoryKey.current }); + + unlistenHistory.current = history.listen(({ state }, action) => { + if (!(state as any)?.soapboxModalKey) { + handleClose(); + } else if (action === 'POP') { + handleClose(false); + } + }); + } }, [isOpen]); if (items?.length === 0 && !Component) { @@ -284,7 +313,6 @@ const DropdownMenu = (props: IDropdownMenu) => { 'opacity-60': (isOpen && isDisplayed), })} role='button' - onClick={handleClose} /> )}
    Date: Fri, 16 Aug 2024 17:53:00 +0200 Subject: [PATCH 7/9] fix scroll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- src/components/dropdown-menu/dropdown-menu.tsx | 2 +- src/init/soapbox-mount.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/dropdown-menu/dropdown-menu.tsx b/src/components/dropdown-menu/dropdown-menu.tsx index 8a2d0da4e..cc28011fe 100644 --- a/src/components/dropdown-menu/dropdown-menu.tsx +++ b/src/components/dropdown-menu/dropdown-menu.tsx @@ -243,7 +243,7 @@ const DropdownMenu = (props: IDropdownMenu) => { history.push(pathname, { ...(state as any), soapboxDropdownKey: dropdownHistoryKey.current }); unlistenHistory.current = history.listen(({ state }, action) => { - if (!(state as any)?.soapboxModalKey) { + if (!(state as any)?.soapboxDropdownKey) { handleClose(); } else if (action === 'POP') { handleClose(false); diff --git a/src/init/soapbox-mount.tsx b/src/init/soapbox-mount.tsx index ab7b1b613..61dc3a4b8 100644 --- a/src/init/soapbox-mount.tsx +++ b/src/init/soapbox-mount.tsx @@ -38,7 +38,8 @@ const SoapboxMount = () => { // @ts-ignore: I don't actually know what these should be, lol const shouldUpdateScroll = (prevRouterProps, { location }) => - !(location.state?.soapboxModalKey && location.state?.soapboxModalKey !== prevRouterProps?.location?.state?.soapboxModalKey); + !(location.state?.soapboxModalKey && location.state?.soapboxModalKey !== prevRouterProps?.location?.state?.soapboxModalKey) + && !(location.state?.soapboxDropdownKey && location.state?.soapboxDropdownKey !== prevRouterProps?.location?.state?.soapboxDropdownKey); return ( From f48dc1f0b874758ea5740868fb167461fa4e630f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 17 Aug 2024 00:26:29 +0200 Subject: [PATCH 8/9] Fix navigation in modal + dropdown combination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- src/actions/modals.ts | 5 ++++ .../dropdown-menu/dropdown-menu.tsx | 4 +++- src/components/modal-root.tsx | 18 +++++++++++---- .../compose/components/privacy-dropdown.tsx | 23 +------------------ 4 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/actions/modals.ts b/src/actions/modals.ts index ef3f55a2c..82f220ada 100644 --- a/src/actions/modals.ts +++ b/src/actions/modals.ts @@ -24,9 +24,14 @@ const closeModal = (type?: ModalType) => ({ modalType: type, }); +type ModalsAction = + ReturnType + | ReturnType; + export { MODAL_OPEN, MODAL_CLOSE, openModal, closeModal, + type ModalsAction, }; diff --git a/src/components/dropdown-menu/dropdown-menu.tsx b/src/components/dropdown-menu/dropdown-menu.tsx index cc28011fe..6cee89a01 100644 --- a/src/components/dropdown-menu/dropdown-menu.tsx +++ b/src/components/dropdown-menu/dropdown-menu.tsx @@ -111,6 +111,7 @@ const DropdownMenu = (props: IDropdownMenu) => { const { state } = history.location; if (goBack && state && (state as any).soapboxDropdownKey === dropdownHistoryKey.current) { history.goBack(); + (history.location.state as any).soapboxDropdownKey = true; } closeDropdownMenu(); @@ -145,6 +146,7 @@ const DropdownMenu = (props: IDropdownMenu) => { const handleDocumentClick = (event: Event) => { if (refs.floating.current && !refs.floating.current.contains(event.target as Node)) { handleClose(); + event.stopPropagation(); } }; @@ -244,7 +246,7 @@ const DropdownMenu = (props: IDropdownMenu) => { unlistenHistory.current = history.listen(({ state }, action) => { if (!(state as any)?.soapboxDropdownKey) { - handleClose(); + handleClose(false); } else if (action === 'POP') { handleClose(false); } diff --git a/src/components/modal-root.tsx b/src/components/modal-root.tsx index 34b2699d4..70a9ccf30 100644 --- a/src/components/modal-root.tsx +++ b/src/components/modal-root.tsx @@ -8,7 +8,7 @@ import { cancelReplyCompose } from 'soapbox/actions/compose'; import { saveDraftStatus } from 'soapbox/actions/draft-statuses'; import { cancelEventCompose } from 'soapbox/actions/events'; import { openModal, closeModal } from 'soapbox/actions/modals'; -import { useAppDispatch, usePrevious } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, usePrevious } from 'soapbox/hooks'; import type { ModalType } from 'soapbox/features/ui/components/modal-root'; import type { ReducerCompose } from 'soapbox/reducers/compose'; @@ -49,6 +49,8 @@ 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); @@ -56,7 +58,6 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) const unlistenHistory = useRef>(); const prevChildren = usePrevious(children); - const prevType = usePrevious(type); const visible = !!children; @@ -158,7 +159,7 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) }); }; - const handleModalClose = (type: string) => { + const handleModalClose = () => { if (unlistenHistory.current) { unlistenHistory.current(); } @@ -206,7 +207,7 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) activeElement.current = null; getSiblings().forEach(sibling => (sibling as HTMLDivElement).removeAttribute('inert')); - handleModalClose(prevType!); + handleModalClose(); } if (children) { @@ -218,6 +219,15 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) } }, [children]); + useEffect(() => { + if (isDropdownOpen && unlistenHistory.current) { + unlistenHistory.current(); + } else if (!isDropdownOpen && wasDropdownOpen) { + // TODO find a better solution + setTimeout(() => handleModalOpen(), 100); + } + }, [isDropdownOpen]); + if (!visible) { return (
    diff --git a/src/features/compose/components/privacy-dropdown.tsx b/src/features/compose/components/privacy-dropdown.tsx index 6f98f8f56..8bab2beac 100644 --- a/src/features/compose/components/privacy-dropdown.tsx +++ b/src/features/compose/components/privacy-dropdown.tsx @@ -1,6 +1,5 @@ import clsx from 'clsx'; -import { supportsPassiveEvents } from 'detect-passive-events'; -import React, { useRef, useEffect } from 'react'; +import React, { useRef } from 'react'; import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; import { changeComposeFederated, changeComposeVisibility } from 'soapbox/actions/compose'; @@ -28,8 +27,6 @@ const messages = defineMessages({ local: { id: 'privacy.local', defaultMessage: '{privacy} (local-only)' }, }); -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; - interface Option { icon: string; value: string; @@ -54,12 +51,6 @@ const PrivacyDropdownMenu: React.FC = ({ const node = useRef(null); const focusedItem = useRef(null); - const handleDocumentClick = (e: MouseEvent | TouchEvent) => { - if (node.current && !node.current.contains(e.target as HTMLElement)) { - onClose(); - } - }; - const handleKeyDown: React.KeyboardEventHandler = e => { const index = [...e.currentTarget.parentElement!.children].indexOf(e.currentTarget); let element: ChildNode | null | undefined = null; @@ -113,18 +104,6 @@ const PrivacyDropdownMenu: React.FC = ({ } }; - useEffect(() => { - document.addEventListener('click', handleDocumentClick, false); - document.addEventListener('touchend', handleDocumentClick, listenerOptions); - - focusedItem.current?.focus({ preventScroll: true }); - - return () => { - document.removeEventListener('click', handleDocumentClick, false); - document.removeEventListener('touchend', handleDocumentClick); - }; - }, []); - return (
      {items.map(item => { From 9a0324b0554bd9eda1c3ccb700b7cee1ebfa6665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 17 Aug 2024 00:37:48 +0200 Subject: [PATCH 9/9] Only apply the modal+dropdown workaround to touchscreen devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- src/components/modal-root.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/modal-root.tsx b/src/components/modal-root.tsx index 70a9ccf30..1a4ab2572 100644 --- a/src/components/modal-root.tsx +++ b/src/components/modal-root.tsx @@ -9,6 +9,7 @@ import { saveDraftStatus } from 'soapbox/actions/draft-statuses'; import { cancelEventCompose } from 'soapbox/actions/events'; import { openModal, closeModal } from 'soapbox/actions/modals'; import { useAppDispatch, useAppSelector, usePrevious } from 'soapbox/hooks'; +import { userTouching } from 'soapbox/is-mobile'; import type { ModalType } from 'soapbox/features/ui/components/modal-root'; import type { ReducerCompose } from 'soapbox/reducers/compose'; @@ -220,6 +221,8 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) }, [children]); useEffect(() => { + if (!userTouching.matches) return; + if (isDropdownOpen && unlistenHistory.current) { unlistenHistory.current(); } else if (!isDropdownOpen && wasDropdownOpen) {