feat: collapse tags on click outside

This commit is contained in:
Antonella Sgarlatta
2021-05-31 17:58:46 -03:00
parent b54de00b40
commit b5906ecf78
4 changed files with 66 additions and 39 deletions

View File

@@ -1,6 +1,6 @@
import { AppState } from '@/ui_models/app_state'; import { AppState } from '@/ui_models/app_state';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { toDirective } from './utils'; import { toDirective, useCloseOnClickOutside } from './utils';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { AutocompleteTagInput } from './AutocompleteTagInput'; import { AutocompleteTagInput } from './AutocompleteTagInput';
import { WebApplication } from '@/ui_models/application'; import { WebApplication } from '@/ui_models/application';
@@ -22,7 +22,7 @@ const NoteTags = observer(({ application, appState }: Props) => {
tags, tags,
tagsContainerPosition, tagsContainerPosition,
tagsContainerMaxWidth, tagsContainerMaxWidth,
tagsContainerCollapsed, tagsContainerExpanded,
tagsOverflowed, tagsOverflowed,
} = appState.activeNote; } = appState.activeNote;
@@ -30,8 +30,19 @@ const NoteTags = observer(({ application, appState }: Props) => {
useState(TAGS_ROW_HEIGHT); useState(TAGS_ROW_HEIGHT);
const [overflowCountPosition, setOverflowCountPosition] = useState(0); const [overflowCountPosition, setOverflowCountPosition] = useState(0);
const containerRef = useRef<HTMLDivElement>();
const tagsContainerRef = useRef<HTMLDivElement>(); const tagsContainerRef = useRef<HTMLDivElement>();
const tagsRef = useRef<HTMLButtonElement[]>([]); const tagsRef = useRef<HTMLButtonElement[]>([]);
const overflowButtonRef = useRef<HTMLButtonElement>();
useCloseOnClickOutside(
tagsContainerRef,
(expanded: boolean) => {
if (overflowButtonRef.current || tagsContainerExpanded) {
appState.activeNote.setTagsContainerExpanded(expanded);
}
}
);
const onTagBackspacePress = async (tag: SNTag) => { const onTagBackspacePress = async (tag: SNTag) => {
await appState.activeNote.removeTagFromActiveNote(tag); await appState.activeNote.removeTagFromActiveNote(tag);
@@ -41,28 +52,24 @@ const NoteTags = observer(({ application, appState }: Props) => {
} }
}; };
const expandTags = () => {
appState.activeNote.setTagsContainerCollapsed(false);
};
const isTagOverflowed = useCallback( const isTagOverflowed = useCallback(
(tagElement?: HTMLButtonElement): boolean | undefined => { (tagElement?: HTMLButtonElement): boolean | undefined => {
if (!tagElement) { if (!tagElement) {
return; return;
} }
if (!tagsContainerCollapsed) { if (tagsContainerExpanded) {
return false; return false;
} }
return tagElement.getBoundingClientRect().top >= MIN_OVERFLOW_TOP; return tagElement.getBoundingClientRect().top >= MIN_OVERFLOW_TOP;
}, },
[tagsContainerCollapsed] [tagsContainerExpanded]
); );
const reloadOverflowCountPosition = useCallback(() => { const reloadOverflowCountPosition = useCallback(() => {
const firstOverflowedTagIndex = tagsRef.current.findIndex((tagElement) => const firstOverflowedTagIndex = tagsRef.current.findIndex((tagElement) =>
isTagOverflowed(tagElement) isTagOverflowed(tagElement)
); );
if (!tagsContainerCollapsed || firstOverflowedTagIndex < 1) { if (tagsContainerExpanded || firstOverflowedTagIndex < 1) {
return; return;
} }
const previousTagRect = const previousTagRect =
@@ -70,14 +77,14 @@ const NoteTags = observer(({ application, appState }: Props) => {
const position = const position =
previousTagRect.right - (tagsContainerPosition ?? 0) + TAG_RIGHT_MARGIN; previousTagRect.right - (tagsContainerPosition ?? 0) + TAG_RIGHT_MARGIN;
setOverflowCountPosition(position); setOverflowCountPosition(position);
}, [isTagOverflowed, tagsContainerCollapsed, tagsContainerPosition]); }, [isTagOverflowed, tagsContainerExpanded, tagsContainerPosition]);
const reloadTagsContainerHeight = useCallback(() => { const reloadTagsContainerHeight = useCallback(() => {
const height = tagsContainerCollapsed const height = tagsContainerExpanded
? TAGS_ROW_HEIGHT ? tagsContainerRef.current.scrollHeight
: tagsContainerRef.current.scrollHeight; : TAGS_ROW_HEIGHT;
setTagsContainerHeight(height); setTagsContainerHeight(height);
}, [tagsContainerCollapsed]); }, [tagsContainerExpanded]);
const reloadOverflowCount = useCallback(() => { const reloadOverflowCount = useCallback(() => {
const count = tagsRef.current.filter((tagElement) => const count = tagsRef.current.filter((tagElement) =>
@@ -86,6 +93,10 @@ const NoteTags = observer(({ application, appState }: Props) => {
appState.activeNote.setOverflowedTagsCount(count); appState.activeNote.setOverflowedTagsCount(count);
}, [appState.activeNote, isTagOverflowed]); }, [appState.activeNote, isTagOverflowed]);
const setTagsContainerExpanded = (expanded: boolean) => {
appState.activeNote.setTagsContainerExpanded(expanded);
};
useEffect(() => { useEffect(() => {
appState.activeNote.reloadTagsContainerLayout(); appState.activeNote.reloadTagsContainerLayout();
reloadOverflowCountPosition(); reloadOverflowCountPosition();
@@ -103,11 +114,11 @@ const NoteTags = observer(({ application, appState }: Props) => {
mt-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`; mt-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`;
return ( return (
<div className="flex" style={{ height: tagsContainerHeight }}> <div className="flex" ref={containerRef} style={{ height: tagsContainerHeight }}>
<div <div
ref={tagsContainerRef} ref={tagsContainerRef}
className={`absolute flex flex-wrap pl-1 -ml-1 ${ className={`absolute flex flex-wrap pl-1 -ml-1 ${
tagsContainerCollapsed ? 'overflow-hidden' : '' tagsContainerExpanded ? '' : 'overflow-hidden'
}`} }`}
style={{ style={{
maxWidth: tagsContainerMaxWidth, maxWidth: tagsContainerMaxWidth,
@@ -146,12 +157,15 @@ const NoteTags = observer(({ application, appState }: Props) => {
tabIndex={tagsOverflowed ? -1 : 0} tabIndex={tagsOverflowed ? -1 : 0}
/> />
</div> </div>
{overflowedTagsCount > 1 && tagsContainerCollapsed && ( {tagsOverflowed && (
<button <button
ref={overflowButtonRef}
type="button" type="button"
className={`${tagClass} pl-2 absolute`} className={`${tagClass} pl-2 absolute`}
style={{ left: overflowCountPosition }} style={{ left: overflowCountPosition }}
onClick={expandTags} onClick={() => {
setTagsContainerExpanded(true);
}}
> >
+{overflowedTagsCount} +{overflowedTagsCount}
</button> </button>

View File

@@ -1,8 +1,8 @@
import { AppState } from '@/ui_models/app_state'; 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 { observer } from 'mobx-react-lite';
import { NotesOptions } from './NotesOptions'; import { NotesOptions } from './NotesOptions';
import { useCallback, useEffect, useRef } from 'preact/hooks'; import { useRef } from 'preact/hooks';
type Props = { type Props = {
appState: AppState; appState: AppState;
@@ -15,18 +15,10 @@ const NotesContextMenu = observer(({ appState }: Props) => {
(open: boolean) => appState.notes.setContextMenuOpen(open) (open: boolean) => appState.notes.setContextMenuOpen(open)
); );
const closeOnClickOutside = useCallback((event: MouseEvent) => { useCloseOnClickOutside(
if (!contextMenuRef.current?.contains(event.target as Node)) { contextMenuRef,
appState.notes.setContextMenuOpen(false); (open: boolean) => appState.notes.setContextMenuOpen(open)
} );
}, [appState]);
useEffect(() => {
document.addEventListener('click', closeOnClickOutside);
return () => {
document.removeEventListener('click', closeOnClickOutside);
};
}, [closeOnClickOutside]);
return appState.notes.contextMenuOpen ? ( return appState.notes.contextMenuOpen ? (
<div <div

View File

@@ -1,5 +1,6 @@
import { FunctionComponent, h, render } from 'preact'; import { FunctionComponent, h, render } from 'preact';
import { StateUpdater, useCallback, useState } from 'preact/hooks'; import { StateUpdater, useCallback, useState } from 'preact/hooks';
import { useEffect } from 'react';
/** /**
* @returns a callback that will close a dropdown if none of its children has * @returns a callback that will close a dropdown if none of its children has
@@ -30,6 +31,26 @@ export function useCloseOnBlur(
]; ];
} }
export function useCloseOnClickOutside(
container: { current: HTMLDivElement },
setOpen: (open: boolean) => 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<Props>( export function toDirective<Props>(
component: FunctionComponent<Props>, component: FunctionComponent<Props>,
scope: Record<string, '=' | '&' | '@'> = {} scope: Record<string, '=' | '&' | '@'> = {}

View File

@@ -16,7 +16,7 @@ export class ActiveNoteState {
tags: SNTag[] = []; tags: SNTag[] = [];
tagsContainerPosition? = 0; tagsContainerPosition? = 0;
tagsContainerMaxWidth: number | 'auto' = 'auto'; tagsContainerMaxWidth: number | 'auto' = 'auto';
tagsContainerCollapsed = true; tagsContainerExpanded = false;
overflowedTagsCount = 0; overflowedTagsCount = 0;
constructor( constructor(
@@ -28,14 +28,14 @@ export class ActiveNoteState {
tags: observable, tags: observable,
tagsContainerPosition: observable, tagsContainerPosition: observable,
tagsContainerMaxWidth: observable, tagsContainerMaxWidth: observable,
tagsContainerCollapsed: observable, tagsContainerExpanded: observable,
overflowedTagsCount: observable, overflowedTagsCount: observable,
tagsOverflowed: computed, tagsOverflowed: computed,
setTagsContainerPosition: action, setTagsContainerPosition: action,
setTagsContainerMaxWidth: action, setTagsContainerMaxWidth: action,
setTagsContainerCollapsed: action, setTagsContainerExpanded: action,
setOverflowedTagsCount: action, setOverflowedTagsCount: action,
reloadTags: action, reloadTags: action,
}); });
@@ -59,7 +59,7 @@ export class ActiveNoteState {
} }
get tagsOverflowed(): boolean { get tagsOverflowed(): boolean {
return this.overflowedTagsCount > 0 && this.tagsContainerCollapsed; return this.overflowedTagsCount > 0 && !this.tagsContainerExpanded;
} }
setTagsContainerPosition(position: number): void { setTagsContainerPosition(position: number): void {
@@ -70,8 +70,8 @@ export class ActiveNoteState {
this.tagsContainerMaxWidth = width; this.tagsContainerMaxWidth = width;
} }
setTagsContainerCollapsed(collapsed: boolean): void { setTagsContainerExpanded(expanded: boolean): void {
this.tagsContainerCollapsed = collapsed; this.tagsContainerExpanded = expanded;
} }
setOverflowedTagsCount(count: number): void { setOverflowedTagsCount(count: number): void {
@@ -86,7 +86,7 @@ export class ActiveNoteState {
} }
reloadTagsContainerLayout(): void { reloadTagsContainerLayout(): void {
const MARGIN = this.tagsContainerCollapsed ? 68 : 24; const MARGIN = this.tagsContainerExpanded ? 68 : 24;
const EDITOR_ELEMENT_ID = 'editor-column'; const EDITOR_ELEMENT_ID = 'editor-column';
const { clientWidth } = document.documentElement; const { clientWidth } = document.documentElement;
const editorPosition = const editorPosition =