From b5906ecf788fdc1832ea6b205aefb1cb4bef0038 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 31 May 2021 17:58:46 -0300 Subject: [PATCH] feat: collapse tags on click outside --- .../javascripts/components/NoteTags.tsx | 50 ++++++++++++------- .../components/NotesContextMenu.tsx | 20 +++----- app/assets/javascripts/components/utils.ts | 21 ++++++++ .../ui_models/app_state/active_note_state.ts | 14 +++--- 4 files changed, 66 insertions(+), 39 deletions(-) diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index 217b10620..ab7a84620 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -1,6 +1,6 @@ import { AppState } from '@/ui_models/app_state'; import { observer } from 'mobx-react-lite'; -import { toDirective } from './utils'; +import { toDirective, useCloseOnClickOutside } from './utils'; import { Icon } from './Icon'; import { AutocompleteTagInput } from './AutocompleteTagInput'; import { WebApplication } from '@/ui_models/application'; @@ -22,7 +22,7 @@ const NoteTags = observer(({ application, appState }: Props) => { tags, tagsContainerPosition, tagsContainerMaxWidth, - tagsContainerCollapsed, + tagsContainerExpanded, tagsOverflowed, } = appState.activeNote; @@ -30,8 +30,19 @@ const NoteTags = observer(({ application, appState }: Props) => { useState(TAGS_ROW_HEIGHT); const [overflowCountPosition, setOverflowCountPosition] = useState(0); + const containerRef = useRef(); const tagsContainerRef = useRef(); const tagsRef = useRef([]); + const overflowButtonRef = useRef(); + + useCloseOnClickOutside( + tagsContainerRef, + (expanded: boolean) => { + if (overflowButtonRef.current || tagsContainerExpanded) { + appState.activeNote.setTagsContainerExpanded(expanded); + } + } + ); const onTagBackspacePress = async (tag: SNTag) => { await appState.activeNote.removeTagFromActiveNote(tag); @@ -41,28 +52,24 @@ const NoteTags = observer(({ application, appState }: Props) => { } }; - const expandTags = () => { - appState.activeNote.setTagsContainerCollapsed(false); - }; - const isTagOverflowed = useCallback( (tagElement?: HTMLButtonElement): boolean | undefined => { if (!tagElement) { return; } - if (!tagsContainerCollapsed) { + if (tagsContainerExpanded) { return false; } return tagElement.getBoundingClientRect().top >= MIN_OVERFLOW_TOP; }, - [tagsContainerCollapsed] + [tagsContainerExpanded] ); const reloadOverflowCountPosition = useCallback(() => { const firstOverflowedTagIndex = tagsRef.current.findIndex((tagElement) => isTagOverflowed(tagElement) ); - if (!tagsContainerCollapsed || firstOverflowedTagIndex < 1) { + if (tagsContainerExpanded || firstOverflowedTagIndex < 1) { return; } const previousTagRect = @@ -70,14 +77,14 @@ const NoteTags = observer(({ application, appState }: Props) => { const position = previousTagRect.right - (tagsContainerPosition ?? 0) + TAG_RIGHT_MARGIN; setOverflowCountPosition(position); - }, [isTagOverflowed, tagsContainerCollapsed, tagsContainerPosition]); + }, [isTagOverflowed, tagsContainerExpanded, tagsContainerPosition]); const reloadTagsContainerHeight = useCallback(() => { - const height = tagsContainerCollapsed - ? TAGS_ROW_HEIGHT - : tagsContainerRef.current.scrollHeight; + const height = tagsContainerExpanded + ? tagsContainerRef.current.scrollHeight + : TAGS_ROW_HEIGHT; setTagsContainerHeight(height); - }, [tagsContainerCollapsed]); + }, [tagsContainerExpanded]); const reloadOverflowCount = useCallback(() => { const count = tagsRef.current.filter((tagElement) => @@ -86,6 +93,10 @@ const NoteTags = observer(({ application, appState }: Props) => { appState.activeNote.setOverflowedTagsCount(count); }, [appState.activeNote, isTagOverflowed]); + const setTagsContainerExpanded = (expanded: boolean) => { + appState.activeNote.setTagsContainerExpanded(expanded); + }; + useEffect(() => { appState.activeNote.reloadTagsContainerLayout(); reloadOverflowCountPosition(); @@ -103,11 +114,11 @@ const NoteTags = observer(({ application, appState }: Props) => { mt-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`; return ( -
+
{ tabIndex={tagsOverflowed ? -1 : 0} />
- {overflowedTagsCount > 1 && tagsContainerCollapsed && ( + {tagsOverflowed && ( diff --git a/app/assets/javascripts/components/NotesContextMenu.tsx b/app/assets/javascripts/components/NotesContextMenu.tsx index 4940207eb..fa21ac7eb 100644 --- a/app/assets/javascripts/components/NotesContextMenu.tsx +++ b/app/assets/javascripts/components/NotesContextMenu.tsx @@ -1,8 +1,8 @@ import { AppState } from '@/ui_models/app_state'; -import { toDirective, useCloseOnBlur } from './utils'; +import { toDirective, useCloseOnBlur, useCloseOnClickOutside } from './utils'; import { observer } from 'mobx-react-lite'; import { NotesOptions } from './NotesOptions'; -import { useCallback, useEffect, useRef } from 'preact/hooks'; +import { useRef } from 'preact/hooks'; type Props = { appState: AppState; @@ -15,18 +15,10 @@ const NotesContextMenu = observer(({ appState }: Props) => { (open: boolean) => appState.notes.setContextMenuOpen(open) ); - const closeOnClickOutside = useCallback((event: MouseEvent) => { - if (!contextMenuRef.current?.contains(event.target as Node)) { - appState.notes.setContextMenuOpen(false); - } - }, [appState]); - - useEffect(() => { - document.addEventListener('click', closeOnClickOutside); - return () => { - document.removeEventListener('click', closeOnClickOutside); - }; - }, [closeOnClickOutside]); + useCloseOnClickOutside( + contextMenuRef, + (open: boolean) => appState.notes.setContextMenuOpen(open) + ); return appState.notes.contextMenuOpen ? (
void +): void { + const closeOnClickOutside = useCallback((event: { target: EventTarget | null }) => { + if ( + !container.current?.contains(event.target as Node) + ) { + setOpen(false); + } + }, [container, setOpen]); + + useEffect(() => { + document.addEventListener('click', closeOnClickOutside); + return () => { + document.removeEventListener('click', closeOnClickOutside); + }; + }, [closeOnClickOutside]); +} + export function toDirective( component: FunctionComponent, scope: Record = {} diff --git a/app/assets/javascripts/ui_models/app_state/active_note_state.ts b/app/assets/javascripts/ui_models/app_state/active_note_state.ts index 3e91cd318..521090f70 100644 --- a/app/assets/javascripts/ui_models/app_state/active_note_state.ts +++ b/app/assets/javascripts/ui_models/app_state/active_note_state.ts @@ -16,7 +16,7 @@ export class ActiveNoteState { tags: SNTag[] = []; tagsContainerPosition? = 0; tagsContainerMaxWidth: number | 'auto' = 'auto'; - tagsContainerCollapsed = true; + tagsContainerExpanded = false; overflowedTagsCount = 0; constructor( @@ -28,14 +28,14 @@ export class ActiveNoteState { tags: observable, tagsContainerPosition: observable, tagsContainerMaxWidth: observable, - tagsContainerCollapsed: observable, + tagsContainerExpanded: observable, overflowedTagsCount: observable, tagsOverflowed: computed, setTagsContainerPosition: action, setTagsContainerMaxWidth: action, - setTagsContainerCollapsed: action, + setTagsContainerExpanded: action, setOverflowedTagsCount: action, reloadTags: action, }); @@ -59,7 +59,7 @@ export class ActiveNoteState { } get tagsOverflowed(): boolean { - return this.overflowedTagsCount > 0 && this.tagsContainerCollapsed; + return this.overflowedTagsCount > 0 && !this.tagsContainerExpanded; } setTagsContainerPosition(position: number): void { @@ -70,8 +70,8 @@ export class ActiveNoteState { this.tagsContainerMaxWidth = width; } - setTagsContainerCollapsed(collapsed: boolean): void { - this.tagsContainerCollapsed = collapsed; + setTagsContainerExpanded(expanded: boolean): void { + this.tagsContainerExpanded = expanded; } setOverflowedTagsCount(count: number): void { @@ -86,7 +86,7 @@ export class ActiveNoteState { } reloadTagsContainerLayout(): void { - const MARGIN = this.tagsContainerCollapsed ? 68 : 24; + const MARGIN = this.tagsContainerExpanded ? 68 : 24; const EDITOR_ELEMENT_ID = 'editor-column'; const { clientWidth } = document.documentElement; const editorPosition =