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 { 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<HTMLDivElement>();
const tagsContainerRef = useRef<HTMLDivElement>();
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) => {
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 (
<div className="flex" style={{ height: tagsContainerHeight }}>
<div className="flex" ref={containerRef} style={{ height: tagsContainerHeight }}>
<div
ref={tagsContainerRef}
className={`absolute flex flex-wrap pl-1 -ml-1 ${
tagsContainerCollapsed ? 'overflow-hidden' : ''
tagsContainerExpanded ? '' : 'overflow-hidden'
}`}
style={{
maxWidth: tagsContainerMaxWidth,
@@ -146,12 +157,15 @@ const NoteTags = observer(({ application, appState }: Props) => {
tabIndex={tagsOverflowed ? -1 : 0}
/>
</div>
{overflowedTagsCount > 1 && tagsContainerCollapsed && (
{tagsOverflowed && (
<button
ref={overflowButtonRef}
type="button"
className={`${tagClass} pl-2 absolute`}
style={{ left: overflowCountPosition }}
onClick={expandTags}
onClick={() => {
setTagsContainerExpanded(true);
}}
>
+{overflowedTagsCount}
</button>

View File

@@ -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 ? (
<div

View File

@@ -1,5 +1,6 @@
import { FunctionComponent, h, render } from 'preact';
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
@@ -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>(
component: FunctionComponent<Props>,
scope: Record<string, '=' | '&' | '@'> = {}

View File

@@ -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 =