feat: context menu for deleting tags

This commit is contained in:
Antonella Sgarlatta
2021-06-02 18:31:13 -03:00
parent 6fb68d2255
commit de31fe7d61
3 changed files with 77 additions and 57 deletions

View File

@@ -1,26 +1,31 @@
import { Icon } from './Icon'; import { Icon } from './Icon';
import { FunctionalComponent, RefObject } from 'preact'; import { FunctionalComponent } from 'preact';
import { useCallback, useRef, useState } from 'preact/hooks'; import { useCallback, useRef, useState } from 'preact/hooks';
import { AppState } from '@/ui_models/app_state'; import { AppState } from '@/ui_models/app_state';
import { SNTag } from '@standardnotes/snjs/dist/@types'; import { SNTag } from '@standardnotes/snjs/dist/@types';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useCloseOnBlur, useCloseOnClickOutside } from './utils';
type Props = { type Props = {
appState: AppState; appState: AppState;
tag: SNTag; tag: SNTag;
overflowButtonRef: RefObject<HTMLButtonElement>;
}; };
export const NoteTag: FunctionalComponent<Props> = ({ appState, tag, overflowButtonRef }) => { export const NoteTag: FunctionalComponent<Props> = ({ appState, tag }) => {
const { const {
tags, tags,
tagsContainerMaxWidth, tagsContainerMaxWidth,
} = appState.activeNote; } = appState.activeNote;
const [overflowed, setOverflowed] = useState(false); const [overflowed, setOverflowed] = useState(false);
const [showDeleteButton, setShowDeleteButton] = useState(false); const [contextMenuOpen, setContextMenuOpen] = useState(false);
const [contextMenuPosition, setContextMenuPosition] = useState({ top: 0, left: 0 });
const deleteTagRef = useRef<HTMLButtonElement>(); const contextMenuRef = useRef<HTMLDivElement>();
const tagRef = useRef<HTMLButtonElement>();
const [closeOnBlur] = useCloseOnBlur(contextMenuRef, setContextMenuOpen);
useCloseOnClickOutside(contextMenuRef, setContextMenuOpen);
const deleteTag = async () => { const deleteTag = async () => {
await appState.activeNote.removeTagFromActiveNote(tag); await appState.activeNote.removeTagFromActiveNote(tag);
@@ -36,21 +41,6 @@ export const NoteTag: FunctionalComponent<Props> = ({ appState, tag, overflowBut
appState.setSelectedTag(tag); appState.setSelectedTag(tag);
}; };
const onFocus = () => {
appState.activeNote.setTagFocused(true);
setShowDeleteButton(true);
};
const onBlur = (event: FocusEvent) => {
const relatedTarget = event.relatedTarget as Node;
if (relatedTarget === overflowButtonRef.current) {
(event.target as HTMLButtonElement).focus();
} else if (relatedTarget !== deleteTagRef.current) {
appState.activeNote.setTagFocused(false);
setShowDeleteButton(false);
}
};
const reloadOverflowed = useCallback(() => { const reloadOverflowed = useCallback(() => {
const overflowed = appState.activeNote.isTagOverflowed(tag); const overflowed = appState.activeNote.isTagOverflowed(tag);
setOverflowed(overflowed); setOverflowed(overflowed);
@@ -60,44 +50,67 @@ export const NoteTag: FunctionalComponent<Props> = ({ appState, tag, overflowBut
reloadOverflowed(); reloadOverflowed();
}, [reloadOverflowed, tags, tagsContainerMaxWidth]); }, [reloadOverflowed, tags, tagsContainerMaxWidth]);
const contextMenuListener = (event: MouseEvent) => {
event.preventDefault();
setContextMenuPosition({
top: event.clientY,
left: event.clientX,
});
setContextMenuOpen(true);
};
useEffect(() => {
tagRef.current.addEventListener('contextmenu', contextMenuListener);
return () => {
tagRef.current.removeEventListener('contextmenu', contextMenuListener);
};
}, []);
return ( return (
<button <>
ref={(element) => { <button
if (element) { ref={(element) => {
appState.activeNote.setTagElement(tag, element); if (element) {
} appState.activeNote.setTagElement(tag, element);
}} tagRef.current = element;
className="sn-tag pl-1 pr-2 mr-2" }
style={{ maxWidth: tagsContainerMaxWidth }} }}
onClick={onTagClick} className="sn-tag pl-1 pr-2 mr-2"
onKeyUp={(event) => { style={{ maxWidth: tagsContainerMaxWidth }}
if (event.key === 'Backspace') { onClick={onTagClick}
deleteTag(); onKeyUp={(event) => {
} if (event.key === 'Backspace') {
}} deleteTag();
tabIndex={overflowed ? -1 : 0} }
onFocus={onFocus} }}
onBlur={onBlur} tabIndex={overflowed ? -1 : 0}
> onBlur={closeOnBlur}
<Icon type="hashtag" className="sn-icon--small color-neutral mr-1" /> >
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis"> <Icon type="hashtag" className="sn-icon--small color-neutral mr-1" />
{tag.title} <span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
</span> {tag.title}
{showDeleteButton && ( </span>
<button </button>
ref={deleteTagRef} {contextMenuOpen && (
type="button" <div
className="ml-2 -mr-1 border-0 p-0 bg-transparent cursor-pointer flex" ref={contextMenuRef}
onFocus={onFocus} className="sn-dropdown sn-dropdown--small max-h-120 max-w-xs flex flex-col py-2 overflow-y-scroll fixed"
onBlur={onBlur} style={{
onClick={deleteTag} ...contextMenuPosition
}}
> >
<Icon <button
type="close" type="button"
className="sn-icon--small color-neutral hover:color-info" className="sn-dropdown-item"
/> onClick={deleteTag}
</button> >
<div className="flex items-center">
<Icon type="close" className="color-danger mr-2" />
<span className="color-danger">Remove tag</span>
</div>
</button>
</div>
)} )}
</button> </>
); );
}; };

View File

@@ -88,7 +88,6 @@ const NoteTagsContainer = observer(({ application, appState }: Props) => {
key={tag.uuid} key={tag.uuid}
appState={appState} appState={appState}
tag={tag} tag={tag}
overflowButtonRef={overflowButtonRef}
/> />
))} ))}
<AutocompleteTagInput application={application} appState={appState} /> <AutocompleteTagInput application={application} appState={appState} />

View File

@@ -190,6 +190,10 @@
min-width: 1.25rem; min-width: 1.25rem;
} }
.min-w-40 {
min-width: 10rem;
}
.h-1px { .h-1px {
height: 1px; height: 1px;
} }
@@ -366,6 +370,10 @@
@extend .duration-150; @extend .duration-150;
@extend .slide-down-animation; @extend .slide-down-animation;
} }
&.sn-dropdown--small {
@extend .min-w-40;
}
} }
/** Lesser specificity will give priority to reach's styles */ /** Lesser specificity will give priority to reach's styles */