From 08fb913b0e1d1b23b2eb83100c8c08aea44f3fd4 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Sat, 5 Mar 2022 20:20:11 +0530 Subject: [PATCH] feat: close submenu if another submenu is opened (#911) --- .../components/NotesOptions/AddTagOption.tsx | 129 +++++++++++++++ .../NotesOptions/ChangeEditorOption.tsx | 97 +++++------ .../NotesOptions/ListedActionsOption.tsx | 66 ++++---- .../components/NotesOptions/NotesOptions.tsx | 150 +----------------- .../components/NotesOptionsPanel.tsx | 10 +- .../javascripts/components/menu/Menu.tsx | 49 ++---- app/assets/javascripts/components/utils.ts | 37 +++-- 7 files changed, 265 insertions(+), 273 deletions(-) create mode 100644 app/assets/javascripts/components/NotesOptions/AddTagOption.tsx diff --git a/app/assets/javascripts/components/NotesOptions/AddTagOption.tsx b/app/assets/javascripts/components/NotesOptions/AddTagOption.tsx new file mode 100644 index 000000000..c5d046cee --- /dev/null +++ b/app/assets/javascripts/components/NotesOptions/AddTagOption.tsx @@ -0,0 +1,129 @@ +import { AppState } from '@/ui_models/app_state'; +import { + calculateSubmenuStyle, + SubmenuStyle, +} from '@/utils/calculateSubmenuStyle'; +import { + Disclosure, + DisclosureButton, + DisclosurePanel, +} from '@reach/disclosure'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; +import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; +import { Icon } from '../Icon'; +import { useCloseOnBlur } from '../utils'; + +type Props = { + appState: AppState; +}; + +export const AddTagOption: FunctionComponent = observer( + ({ appState }) => { + const menuContainerRef = useRef(null); + const menuRef = useRef(null); + const menuButtonRef = useRef(null); + + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [menuStyle, setMenuStyle] = useState({ + right: 0, + bottom: 0, + maxHeight: 'auto', + }); + + const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen); + + const toggleTagsMenu = () => { + if (!isMenuOpen) { + const menuPosition = calculateSubmenuStyle(menuButtonRef.current); + if (menuPosition) { + setMenuStyle(menuPosition); + console.log(menuPosition); + } + } + + setIsMenuOpen(!isMenuOpen); + }; + + const recalculateMenuStyle = useCallback(() => { + const newMenuPosition = calculateSubmenuStyle( + menuButtonRef.current, + menuRef.current + ); + + if (newMenuPosition) { + setMenuStyle(newMenuPosition); + console.log(newMenuPosition); + } + }, []); + + useEffect(() => { + if (isMenuOpen) { + setTimeout(() => { + recalculateMenuStyle(); + }); + } + }, [isMenuOpen, recalculateMenuStyle]); + + return ( +
+ + { + if (event.key === 'Escape') { + setIsMenuOpen(false); + } + }} + onBlur={closeOnBlur} + ref={menuButtonRef} + className="sn-dropdown-item justify-between" + > +
+ + Add tag +
+ +
+ { + if (event.key === 'Escape') { + setIsMenuOpen(false); + menuButtonRef.current?.focus(); + } + }} + style={{ + ...menuStyle, + position: 'fixed', + }} + className="sn-dropdown min-w-80 flex flex-col py-2 max-h-120 max-w-xs fixed overflow-y-auto" + > + {appState.tags.tags.map((tag) => ( + + ))} + +
+
+ ); + } +); diff --git a/app/assets/javascripts/components/NotesOptions/ChangeEditorOption.tsx b/app/assets/javascripts/components/NotesOptions/ChangeEditorOption.tsx index a8ba28a68..efa4f98c4 100644 --- a/app/assets/javascripts/components/NotesOptions/ChangeEditorOption.tsx +++ b/app/assets/javascripts/components/NotesOptions/ChangeEditorOption.tsx @@ -15,12 +15,12 @@ import { calculateSubmenuStyle, SubmenuStyle, } from '@/utils/calculateSubmenuStyle'; +import { useCloseOnBlur } from '../utils'; type ChangeEditorOptionProps = { appState: AppState; application: WebApplication; note: SNNote; - closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void; }; type AccordionMenuGroup = { @@ -40,7 +40,6 @@ export type EditorMenuGroup = AccordionMenuGroup; export const ChangeEditorOption: FunctionComponent = ({ application, - closeOnBlur, note, }) => { const [isOpen, setIsOpen] = useState(false); @@ -50,9 +49,15 @@ export const ChangeEditorOption: FunctionComponent = ({ bottom: 0, maxHeight: 'auto', }); + const menuContainerRef = useRef(null); const menuRef = useRef(null); const buttonRef = useRef(null); + const [closeOnBlur] = useCloseOnBlur(menuContainerRef, (open: boolean) => { + setIsOpen(open); + setIsVisible(open); + }); + const toggleChangeEditorMenu = () => { if (!isOpen) { const menuStyle = calculateSubmenuStyle(buttonRef.current); @@ -81,49 +86,51 @@ export const ChangeEditorOption: FunctionComponent = ({ }, [isOpen]); return ( - - { - if (event.key === KeyboardKey.Escape) { - setIsOpen(false); - } - }} - onBlur={closeOnBlur} - ref={buttonRef} - className="sn-dropdown-item justify-between" - > -
- - Change editor -
- -
- { - if (event.key === KeyboardKey.Escape) { - setIsOpen(false); - buttonRef.current?.focus(); - } - }} - style={{ - ...menuStyle, - position: 'fixed', - }} - className="sn-dropdown flex flex-col max-h-120 min-w-68 fixed overflow-y-auto" - > - {isOpen && ( - { +
+ + { + if (event.key === KeyboardKey.Escape) { setIsOpen(false); - }} - /> - )} - - + } + }} + onBlur={closeOnBlur} + ref={buttonRef} + className="sn-dropdown-item justify-between" + > +
+ + Change editor +
+ + + { + if (event.key === KeyboardKey.Escape) { + setIsOpen(false); + buttonRef.current?.focus(); + } + }} + style={{ + ...menuStyle, + position: 'fixed', + }} + className="sn-dropdown flex flex-col max-h-120 min-w-68 fixed overflow-y-auto" + > + {isOpen && ( + { + setIsOpen(false); + }} + /> + )} + + +
); }; diff --git a/app/assets/javascripts/components/NotesOptions/ListedActionsOption.tsx b/app/assets/javascripts/components/NotesOptions/ListedActionsOption.tsx index 044939f46..5fc9bef1a 100644 --- a/app/assets/javascripts/components/NotesOptions/ListedActionsOption.tsx +++ b/app/assets/javascripts/components/NotesOptions/ListedActionsOption.tsx @@ -12,11 +12,11 @@ import { Action, ListedAccount, SNNote } from '@standardnotes/snjs'; import { Fragment, FunctionComponent } from 'preact'; import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import { Icon } from '../Icon'; +import { useCloseOnBlur } from '../utils'; type Props = { application: WebApplication; note: SNNote; - closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void; }; type ListedMenuGroup = { @@ -230,8 +230,8 @@ const ListedActionsMenu: FunctionComponent = ({ export const ListedActionsOption: FunctionComponent = ({ application, note, - closeOnBlur, }) => { + const menuContainerRef = useRef(null); const menuRef = useRef(null); const menuButtonRef = useRef(null); @@ -242,6 +242,8 @@ export const ListedActionsOption: FunctionComponent = ({ maxHeight: 'auto', }); + const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen); + const toggleListedMenu = () => { if (!isMenuOpen) { const menuPosition = calculateSubmenuStyle(menuButtonRef.current); @@ -273,34 +275,36 @@ export const ListedActionsOption: FunctionComponent = ({ }, [isMenuOpen, recalculateMenuStyle]); return ( - - -
- - Listed actions -
- -
- - {isMenuOpen && ( - - )} - -
+
+ + +
+ + Listed actions +
+ +
+ + {isMenuOpen && ( + + )} + +
+
); }; diff --git a/app/assets/javascripts/components/NotesOptions/NotesOptions.tsx b/app/assets/javascripts/components/NotesOptions/NotesOptions.tsx index 8a00573b2..7c4b74fb8 100644 --- a/app/assets/javascripts/components/NotesOptions/NotesOptions.tsx +++ b/app/assets/javascripts/components/NotesOptions/NotesOptions.tsx @@ -2,29 +2,20 @@ import { AppState } from '@/ui_models/app_state'; import { Icon } from '../Icon'; import { Switch } from '../Switch'; import { observer } from 'mobx-react-lite'; -import { useRef, useState, useEffect, useMemo } from 'preact/hooks'; -import { - Disclosure, - DisclosureButton, - DisclosurePanel, -} from '@reach/disclosure'; +import { useState, useEffect, useMemo } from 'preact/hooks'; import { SNApplication, SNNote } from '@standardnotes/snjs'; import { WebApplication } from '@/ui_models/application'; import { KeyboardModifier } from '@/services/ioService'; import { FunctionComponent } from 'preact'; import { ChangeEditorOption } from './ChangeEditorOption'; -import { - MENU_MARGIN_FROM_APP_BORDER, - MAX_MENU_SIZE_MULTIPLIER, - BYTES_IN_ONE_MEGABYTE, -} from '@/constants'; +import { BYTES_IN_ONE_MEGABYTE } from '@/constants'; import { ListedActionsOption } from './ListedActionsOption'; +import { AddTagOption } from './AddTagOption'; export type NotesOptionsProps = { application: WebApplication; appState: AppState; closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void; - onSubmenuChange?: (submenuOpen: boolean) => void; }; type DeletePermanentlyButtonProps = { @@ -206,24 +197,7 @@ const NoteSizeWarning: FunctionComponent<{ ) : null; export const NotesOptions = observer( - ({ - application, - appState, - closeOnBlur, - onSubmenuChange, - }: NotesOptionsProps) => { - const [tagsMenuOpen, setTagsMenuOpen] = useState(false); - const [tagsMenuPosition, setTagsMenuPosition] = useState<{ - top: number; - right?: number; - left?: number; - }>({ - top: 0, - right: 0, - }); - const [tagsMenuMaxHeight, setTagsMenuMaxHeight] = useState( - 'auto' - ); + ({ application, appState, closeOnBlur }: NotesOptionsProps) => { const [altKeyDown, setAltKeyDown] = useState(false); const toggleOn = (condition: (note: SNNote) => boolean) => { @@ -246,14 +220,6 @@ export const NotesOptions = observer( const unpinned = notes.some((note) => !note.pinned); const errored = notes.some((note) => note.errorDecrypting); - const tagsButtonRef = useRef(null); - - useEffect(() => { - if (onSubmenuChange) { - onSubmenuChange(tagsMenuOpen); - } - }, [tagsMenuOpen, onSubmenuChange]); - useEffect(() => { const removeAltKeyObserver = application.io.addKeyObserver({ modifiers: [KeyboardModifier.Alt], @@ -270,48 +236,6 @@ export const NotesOptions = observer( }; }, [application]); - const openTagsMenu = () => { - const defaultFontSize = window.getComputedStyle( - document.documentElement - ).fontSize; - const maxTagsMenuSize = - parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER; - const { clientWidth, clientHeight } = document.documentElement; - const buttonRect = tagsButtonRef.current?.getBoundingClientRect(); - const footerElementRect = document - .getElementById('footer-bar') - ?.getBoundingClientRect(); - const footerHeightInPx = footerElementRect?.height; - - if (buttonRect && footerHeightInPx) { - if ( - buttonRect.top + maxTagsMenuSize > - clientHeight - footerHeightInPx - ) { - setTagsMenuMaxHeight( - clientHeight - - buttonRect.top - - footerHeightInPx - - MENU_MARGIN_FROM_APP_BORDER - ); - } - - if (buttonRect.right + maxTagsMenuSize > clientWidth) { - setTagsMenuPosition({ - top: buttonRect.top, - right: clientWidth - buttonRect.left, - }); - } else { - setTagsMenuPosition({ - top: buttonRect.top, - left: buttonRect.right, - }); - } - } - - setTagsMenuOpen(!tagsMenuOpen); - }; - const downloadSelectedItems = () => { notes.forEach((note) => { const editor = application.componentManager.editorForNote(note); @@ -416,70 +340,12 @@ export const NotesOptions = observer( )}
- {appState.tags.tagsCount > 0 && ( - - { - if (event.key === 'Escape') { - setTagsMenuOpen(false); - } - }} - onBlur={closeOnBlur} - ref={tagsButtonRef} - className="sn-dropdown-item justify-between" - > -
- - {'Add tag'} -
- -
- { - if (event.key === 'Escape') { - setTagsMenuOpen(false); - tagsButtonRef.current?.focus(); - } - }} - style={{ - ...tagsMenuPosition, - maxHeight: tagsMenuMaxHeight, - position: 'fixed', - }} - className="sn-dropdown min-w-80 flex flex-col py-2 max-h-120 max-w-xs fixed overflow-y-auto" - > - {appState.tags.tags.map((tag) => ( - - ))} - -
- )} + {appState.tags.tagsCount > 0 && } {unpinned && (