Merge remote-tracking branch 'upstream/develop' into account-menu-react
This commit is contained in:
79
app/assets/javascripts/components/AutocompleteTagHint.tsx
Normal file
79
app/assets/javascripts/components/AutocompleteTagHint.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useRef, useEffect } from 'preact/hooks';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||
};
|
||||
|
||||
export const AutocompleteTagHint = observer(
|
||||
({ appState, closeOnBlur }: Props) => {
|
||||
const { autocompleteTagHintFocused } = appState.noteTags;
|
||||
|
||||
const hintRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const { autocompleteSearchQuery, autocompleteTagResults } =
|
||||
appState.noteTags;
|
||||
|
||||
const onTagHintClick = async () => {
|
||||
await appState.noteTags.createAndAddNewTag();
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
appState.noteTags.setAutocompleteTagHintFocused(true);
|
||||
};
|
||||
|
||||
const onBlur = (event: FocusEvent) => {
|
||||
closeOnBlur(event);
|
||||
appState.noteTags.setAutocompleteTagHintFocused(false);
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
if (autocompleteTagResults.length > 0) {
|
||||
const lastTagResult =
|
||||
autocompleteTagResults[autocompleteTagResults.length - 1];
|
||||
appState.noteTags.setFocusedTagResultUuid(lastTagResult.uuid);
|
||||
} else {
|
||||
appState.noteTags.setAutocompleteInputFocused(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (autocompleteTagHintFocused) {
|
||||
hintRef.current.focus();
|
||||
}
|
||||
}, [appState.noteTags, autocompleteTagHintFocused]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{autocompleteTagResults.length > 0 && (
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
)}
|
||||
<button
|
||||
ref={hintRef}
|
||||
type="button"
|
||||
className="sn-dropdown-item"
|
||||
onClick={onTagHintClick}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<span>Create new tag:</span>
|
||||
<span className="bg-contrast rounded text-xs color-text py-1 pl-1 pr-2 flex items-center ml-2">
|
||||
<Icon
|
||||
type="hashtag"
|
||||
className="sn-icon--small color-neutral mr-1"
|
||||
/>
|
||||
<span className="max-w-40 whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||
{autocompleteSearchQuery}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
140
app/assets/javascripts/components/AutocompleteTagInput.tsx
Normal file
140
app/assets/javascripts/components/AutocompleteTagInput.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { Disclosure, DisclosurePanel } from '@reach/disclosure';
|
||||
import { useCloseOnBlur } from './utils';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { AutocompleteTagResult } from './AutocompleteTagResult';
|
||||
import { AutocompleteTagHint } from './AutocompleteTagHint';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
export const AutocompleteTagInput = observer(({ appState }: Props) => {
|
||||
const {
|
||||
autocompleteInputFocused,
|
||||
autocompleteSearchQuery,
|
||||
autocompleteTagHintVisible,
|
||||
autocompleteTagResults,
|
||||
tags,
|
||||
tagsContainerMaxWidth,
|
||||
} = appState.noteTags;
|
||||
|
||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||
const [dropdownMaxHeight, setDropdownMaxHeight] =
|
||||
useState<number | 'auto'>('auto');
|
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>();
|
||||
const inputRef = useRef<HTMLInputElement>();
|
||||
|
||||
const [closeOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) => {
|
||||
setDropdownVisible(visible);
|
||||
appState.noteTags.clearAutocompleteSearch();
|
||||
});
|
||||
|
||||
const showDropdown = () => {
|
||||
const { clientHeight } = document.documentElement;
|
||||
const inputRect = inputRef.current.getBoundingClientRect();
|
||||
setDropdownMaxHeight(clientHeight - inputRect.bottom - 32 * 2);
|
||||
setDropdownVisible(true);
|
||||
};
|
||||
|
||||
const onSearchQueryChange = (event: Event) => {
|
||||
const query = (event.target as HTMLInputElement).value;
|
||||
appState.noteTags.setAutocompleteSearchQuery(query);
|
||||
appState.noteTags.searchActiveNoteAutocompleteTags();
|
||||
};
|
||||
|
||||
const onFormSubmit = async (event: Event) => {
|
||||
event.preventDefault();
|
||||
await appState.noteTags.createAndAddNewTag();
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
switch (event.key) {
|
||||
case 'Backspace':
|
||||
case 'ArrowLeft':
|
||||
if (autocompleteSearchQuery === '' && tags.length > 0) {
|
||||
appState.noteTags.setFocusedTagUuid(tags[tags.length - 1].uuid);
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
if (autocompleteTagResults.length > 0) {
|
||||
appState.noteTags.setFocusedTagResultUuid(autocompleteTagResults[0].uuid);
|
||||
} else if (autocompleteTagHintVisible) {
|
||||
appState.noteTags.setAutocompleteTagHintFocused(true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
showDropdown();
|
||||
appState.noteTags.setAutocompleteInputFocused(true);
|
||||
};
|
||||
|
||||
const onBlur = (event: FocusEvent) => {
|
||||
closeOnBlur(event);
|
||||
appState.noteTags.setAutocompleteInputFocused(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
appState.noteTags.searchActiveNoteAutocompleteTags();
|
||||
}, [appState.noteTags]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autocompleteInputFocused) {
|
||||
inputRef.current.focus();
|
||||
appState.noteTags.setAutocompleteInputFocused(false);
|
||||
}
|
||||
}, [appState.noteTags, autocompleteInputFocused]);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={onFormSubmit}
|
||||
className={`${tags.length > 0 ? 'mt-2' : ''}`}
|
||||
>
|
||||
<Disclosure open={dropdownVisible} onChange={showDropdown}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={`${tags.length > 0 ? 'w-80' : 'w-70 mr-10'} bg-default text-xs
|
||||
color-text no-border h-7 focus:outline-none focus:shadow-none focus:border-bottom`}
|
||||
value={autocompleteSearchQuery}
|
||||
onChange={onSearchQueryChange}
|
||||
type="text"
|
||||
placeholder="Add tag"
|
||||
onBlur={onBlur}
|
||||
onFocus={onFocus}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
{dropdownVisible && (autocompleteTagResults.length > 0 || autocompleteTagHintVisible) && (
|
||||
<DisclosurePanel
|
||||
ref={dropdownRef}
|
||||
className={`${tags.length > 0 ? 'w-80' : 'w-70 mr-10'} sn-dropdown flex flex-col py-2 absolute`}
|
||||
style={{ maxHeight: dropdownMaxHeight, maxWidth: tagsContainerMaxWidth }}
|
||||
>
|
||||
<div className="overflow-y-scroll">
|
||||
{autocompleteTagResults.map((tagResult) => (
|
||||
<AutocompleteTagResult
|
||||
key={tagResult.uuid}
|
||||
appState={appState}
|
||||
tagResult={tagResult}
|
||||
closeOnBlur={closeOnBlur}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{autocompleteTagHintVisible && (
|
||||
<AutocompleteTagHint
|
||||
appState={appState}
|
||||
closeOnBlur={closeOnBlur}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
)}
|
||||
</Disclosure>
|
||||
</form>
|
||||
);
|
||||
});
|
||||
109
app/assets/javascripts/components/AutocompleteTagResult.tsx
Normal file
109
app/assets/javascripts/components/AutocompleteTagResult.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { SNTag } from '@standardnotes/snjs';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useEffect, useRef } from 'preact/hooks';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
tagResult: SNTag;
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||
};
|
||||
|
||||
export const AutocompleteTagResult = observer(
|
||||
({ appState, tagResult, closeOnBlur }: Props) => {
|
||||
const {
|
||||
autocompleteSearchQuery,
|
||||
autocompleteTagHintVisible,
|
||||
autocompleteTagResults,
|
||||
focusedTagResultUuid,
|
||||
} = appState.noteTags;
|
||||
|
||||
const tagResultRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const onTagOptionClick = async (tag: SNTag) => {
|
||||
await appState.noteTags.addTagToActiveNote(tag);
|
||||
appState.noteTags.clearAutocompleteSearch();
|
||||
appState.noteTags.setAutocompleteInputFocused(true);
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const tagResultIndex = appState.noteTags.getTagIndex(
|
||||
tagResult,
|
||||
autocompleteTagResults
|
||||
);
|
||||
switch (event.key) {
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
if (tagResultIndex === 0) {
|
||||
appState.noteTags.setAutocompleteInputFocused(true);
|
||||
} else {
|
||||
appState.noteTags.focusPreviousTagResult(tagResult);
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
if (
|
||||
tagResultIndex === autocompleteTagResults.length - 1 &&
|
||||
autocompleteTagHintVisible
|
||||
) {
|
||||
appState.noteTags.setAutocompleteTagHintFocused(true);
|
||||
} else {
|
||||
appState.noteTags.focusNextTagResult(tagResult);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
appState.noteTags.setFocusedTagResultUuid(tagResult.uuid);
|
||||
};
|
||||
|
||||
const onBlur = (event: FocusEvent) => {
|
||||
closeOnBlur(event);
|
||||
appState.noteTags.setFocusedTagResultUuid(undefined);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (focusedTagResultUuid === tagResult.uuid) {
|
||||
tagResultRef.current.focus();
|
||||
appState.noteTags.setFocusedTagResultUuid(undefined);
|
||||
}
|
||||
}, [appState.noteTags, focusedTagResultUuid, tagResult]);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={tagResultRef}
|
||||
type="button"
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => onTagOptionClick(tagResult)}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<Icon type="hashtag" className="color-neutral mr-2 min-h-5 min-w-5" />
|
||||
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||
{autocompleteSearchQuery === ''
|
||||
? tagResult.title
|
||||
: tagResult.title
|
||||
.split(new RegExp(`(${autocompleteSearchQuery})`, 'gi'))
|
||||
.map((substring, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className={`${
|
||||
substring.toLowerCase() ===
|
||||
autocompleteSearchQuery.toLowerCase()
|
||||
? 'font-bold whitespace-pre-wrap'
|
||||
: 'whitespace-pre-wrap '
|
||||
}`}
|
||||
>
|
||||
{substring}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -23,9 +23,7 @@ export const ConfirmSignoutContainer = observer((props: Props) => {
|
||||
});
|
||||
|
||||
const ConfirmSignoutModal = observer(({ application, appState }: Props) => {
|
||||
const [deleteLocalBackups, setDeleteLocalBackups] = useState(
|
||||
application.hasAccount()
|
||||
);
|
||||
const [deleteLocalBackups, setDeleteLocalBackups] = useState(false);
|
||||
|
||||
const cancelRef = useRef<HTMLButtonElement>();
|
||||
function close() {
|
||||
|
||||
112
app/assets/javascripts/components/NoteTag.tsx
Normal file
112
app/assets/javascripts/components/NoteTag.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Icon } from './Icon';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { SNTag } from '@standardnotes/snjs/dist/@types';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
tag: SNTag;
|
||||
};
|
||||
|
||||
export const NoteTag = observer(({ appState, tag }: Props) => {
|
||||
const { focusedTagUuid, tags } = appState.noteTags;
|
||||
|
||||
const [showDeleteButton, setShowDeleteButton] = useState(false);
|
||||
const [tagClicked, setTagClicked] = useState(false);
|
||||
const deleteTagRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const tagRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const deleteTag = () => {
|
||||
appState.noteTags.focusPreviousTag(tag);
|
||||
appState.noteTags.removeTagFromActiveNote(tag);
|
||||
};
|
||||
|
||||
const onDeleteTagClick = (event: MouseEvent) => {
|
||||
event.stopImmediatePropagation();
|
||||
event.stopPropagation();
|
||||
deleteTag();
|
||||
};
|
||||
|
||||
const onTagClick = (event: MouseEvent) => {
|
||||
if (tagClicked && event.target !== deleteTagRef.current) {
|
||||
setTagClicked(false);
|
||||
appState.setSelectedTag(tag);
|
||||
} else {
|
||||
setTagClicked(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
appState.noteTags.setFocusedTagUuid(tag.uuid);
|
||||
setShowDeleteButton(true);
|
||||
};
|
||||
|
||||
const onBlur = (event: FocusEvent) => {
|
||||
const relatedTarget = event.relatedTarget as Node;
|
||||
if (relatedTarget !== deleteTagRef.current) {
|
||||
appState.noteTags.setFocusedTagUuid(undefined);
|
||||
setShowDeleteButton(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const tagIndex = appState.noteTags.getTagIndex(tag, tags);
|
||||
switch (event.key) {
|
||||
case 'Backspace':
|
||||
deleteTag();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
appState.noteTags.focusPreviousTag(tag);
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
if (tagIndex === tags.length - 1) {
|
||||
appState.noteTags.setAutocompleteInputFocused(true);
|
||||
} else {
|
||||
appState.noteTags.focusNextTag(tag);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (focusedTagUuid === tag.uuid) {
|
||||
tagRef.current.focus();
|
||||
appState.noteTags.setFocusedTagUuid(undefined);
|
||||
}
|
||||
}, [appState.noteTags, focusedTagUuid, tag]);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={tagRef}
|
||||
className="sn-tag pl-1 pr-2 mr-2"
|
||||
onClick={onTagClick}
|
||||
onKeyDown={onKeyDown}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
<Icon type="hashtag" className="sn-icon--small color-info mr-1" />
|
||||
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis max-w-290px">
|
||||
{tag.title}
|
||||
</span>
|
||||
{showDeleteButton && (
|
||||
<button
|
||||
ref={deleteTagRef}
|
||||
type="button"
|
||||
className="ml-2 -mr-1 border-0 p-0 bg-transparent cursor-pointer flex"
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onClick={onDeleteTagClick}
|
||||
>
|
||||
<Icon
|
||||
type="close"
|
||||
className="sn-icon--small color-neutral hover:color-info"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
41
app/assets/javascripts/components/NoteTagsContainer.tsx
Normal file
41
app/assets/javascripts/components/NoteTagsContainer.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { toDirective } from './utils';
|
||||
import { AutocompleteTagInput } from './AutocompleteTagInput';
|
||||
import { NoteTag } from './NoteTag';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
const NoteTagsContainer = observer(({ appState }: Props) => {
|
||||
const {
|
||||
tags,
|
||||
tagsContainerMaxWidth,
|
||||
} = appState.noteTags;
|
||||
|
||||
useEffect(() => {
|
||||
appState.noteTags.reloadTagsContainerMaxWidth();
|
||||
}, [appState.noteTags]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-default flex flex-wrap min-w-80 -mt-1 -mr-2"
|
||||
style={{
|
||||
maxWidth: tagsContainerMaxWidth,
|
||||
}}
|
||||
>
|
||||
{tags.map((tag) => (
|
||||
<NoteTag
|
||||
key={tag.uuid}
|
||||
appState={appState}
|
||||
tag={tag}
|
||||
/>
|
||||
))}
|
||||
<AutocompleteTagInput appState={appState} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const NoteTagsContainerDirective = toDirective<Props>(NoteTagsContainer);
|
||||
@@ -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,23 +15,15 @@ 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
|
||||
ref={contextMenuRef}
|
||||
className="sn-dropdown max-h-120 max-w-80 flex flex-col py-2 overflow-y-scroll fixed"
|
||||
className="sn-dropdown max-h-120 max-w-xs flex flex-col py-2 overflow-y-scroll fixed"
|
||||
style={{
|
||||
...appState.notes.contextMenuPosition,
|
||||
maxHeight: appState.notes.contextMenuMaxHeight,
|
||||
|
||||
@@ -52,10 +52,6 @@ export const NotesOptions = observer(
|
||||
const tagsButtonRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const iconClass = 'color-neutral mr-2';
|
||||
const buttonClass =
|
||||
'flex items-center border-0 focus:inner-ring-info ' +
|
||||
'cursor-pointer hover:bg-contrast color-text bg-transparent px-3 ' +
|
||||
'text-left';
|
||||
|
||||
useEffect(() => {
|
||||
if (onSubmenuChange) {
|
||||
@@ -136,14 +132,14 @@ export const NotesOptions = observer(
|
||||
{appState.tags.tagsCount > 0 && (
|
||||
<Disclosure open={tagsMenuOpen} onChange={openTagsMenu}>
|
||||
<DisclosureButton
|
||||
onKeyUp={(event) => {
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setTagsMenuOpen(false);
|
||||
}
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={tagsButtonRef}
|
||||
className={`${buttonClass} py-1.5 justify-between`}
|
||||
className="sn-dropdown-item justify-between"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="hashtag" className={iconClass} />
|
||||
@@ -152,7 +148,7 @@ export const NotesOptions = observer(
|
||||
<Icon type="chevron-right" className="color-neutral" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
onKeyUp={(event) => {
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setTagsMenuOpen(false);
|
||||
tagsButtonRef.current.focus();
|
||||
@@ -163,12 +159,12 @@ export const NotesOptions = observer(
|
||||
maxHeight: tagsMenuMaxHeight,
|
||||
position: 'fixed',
|
||||
}}
|
||||
className="sn-dropdown flex flex-col py-2 max-h-120 max-w-80 fixed overflow-y-scroll"
|
||||
className="sn-dropdown flex flex-col py-2 max-h-120 max-w-xs fixed overflow-y-scroll"
|
||||
>
|
||||
{appState.tags.tags.map((tag) => (
|
||||
<button
|
||||
key={tag.title}
|
||||
className={`${buttonClass} py-2 max-w-80`}
|
||||
className="sn-dropdown-item sn-dropdown-item--no-icon max-w-80"
|
||||
onBlur={closeOnBlur}
|
||||
onClick={() => {
|
||||
appState.notes.isTagInSelectedNotes(tag)
|
||||
@@ -194,7 +190,7 @@ export const NotesOptions = observer(
|
||||
{unpinned && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
appState.notes.setPinSelectedNotes(true);
|
||||
}}
|
||||
@@ -206,7 +202,7 @@ export const NotesOptions = observer(
|
||||
{pinned && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
appState.notes.setPinSelectedNotes(false);
|
||||
}}
|
||||
@@ -218,7 +214,7 @@ export const NotesOptions = observer(
|
||||
{unarchived && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
appState.notes.setArchiveSelectedNotes(true);
|
||||
}}
|
||||
@@ -230,7 +226,7 @@ export const NotesOptions = observer(
|
||||
{archived && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
className="sn-dropdown-item"
|
||||
onClick={() => {
|
||||
appState.notes.setArchiveSelectedNotes(false);
|
||||
}}
|
||||
@@ -242,7 +238,7 @@ export const NotesOptions = observer(
|
||||
{notTrashed && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
className="sn-dropdown-item"
|
||||
onClick={async () => {
|
||||
await appState.notes.setTrashSelectedNotes(true);
|
||||
}}
|
||||
@@ -255,7 +251,7 @@ export const NotesOptions = observer(
|
||||
<>
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
className="sn-dropdown-item"
|
||||
onClick={async () => {
|
||||
await appState.notes.setTrashSelectedNotes(false);
|
||||
}}
|
||||
@@ -265,7 +261,7 @@ export const NotesOptions = observer(
|
||||
</button>
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
className="sn-dropdown-item"
|
||||
onClick={async () => {
|
||||
await appState.notes.deleteNotesPermanently();
|
||||
}}
|
||||
@@ -275,7 +271,7 @@ export const NotesOptions = observer(
|
||||
</button>
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className={`${buttonClass} py-1.5`}
|
||||
className="sn-dropdown-item"
|
||||
onClick={async () => {
|
||||
await appState.notes.emptyTrash();
|
||||
}}
|
||||
|
||||
@@ -47,7 +47,7 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
|
||||
}}
|
||||
>
|
||||
<DisclosureButton
|
||||
onKeyUp={(event) => {
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape' && !submenuOpen) {
|
||||
setOpen(false);
|
||||
}
|
||||
@@ -59,28 +59,28 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
|
||||
<VisuallyHidden>Actions</VisuallyHidden>
|
||||
<Icon type="more" className="block" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Escape' && !submenuOpen) {
|
||||
setOpen(false);
|
||||
buttonRef.current.focus();
|
||||
}
|
||||
}}
|
||||
ref={panelRef}
|
||||
style={{
|
||||
...position,
|
||||
maxHeight
|
||||
}}
|
||||
className="sn-dropdown sn-dropdown--animated max-h-120 max-w-80 flex flex-col py-2 overflow-y-scroll fixed"
|
||||
>
|
||||
{open && (
|
||||
<NotesOptions
|
||||
appState={appState}
|
||||
closeOnBlur={closeOnBlur}
|
||||
onSubmenuChange={onSubmenuChange}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
<DisclosurePanel
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape' && !submenuOpen) {
|
||||
setOpen(false);
|
||||
buttonRef.current.focus();
|
||||
}
|
||||
}}
|
||||
ref={panelRef}
|
||||
style={{
|
||||
...position,
|
||||
maxHeight,
|
||||
}}
|
||||
className="sn-dropdown sn-dropdown--animated max-h-120 max-w-xs flex flex-col py-2 overflow-y-scroll fixed"
|
||||
>
|
||||
{open && (
|
||||
<NotesOptions
|
||||
appState={appState}
|
||||
closeOnBlur={closeOnBlur}
|
||||
onSubmenuChange={onSubmenuChange}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -126,7 +126,7 @@ const SessionsModal: FunctionComponent<{
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog onDismiss={close} className="sessions-modal">
|
||||
<Dialog onDismiss={close} className="sessions-modal h-screen py-8">
|
||||
<div className="sk-modal-content">
|
||||
<div class="sn-component">
|
||||
<div class="sk-panel">
|
||||
@@ -145,7 +145,7 @@ const SessionsModal: FunctionComponent<{
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sk-panel-content">
|
||||
<div class="sk-panel-content overflow-y-scroll">
|
||||
{refreshing ? (
|
||||
<>
|
||||
<div class="sk-spinner small info"></div>
|
||||
|
||||
@@ -42,7 +42,7 @@ export const Switch: FunctionalComponent<SwitchProps> = (
|
||||
<span
|
||||
aria-hidden
|
||||
className={`sn-switch-handle ${
|
||||
checked ? 'sn-switch-handle-right' : ''
|
||||
checked ? 'sn-switch-handle--right' : ''
|
||||
}`}
|
||||
/>
|
||||
</CustomCheckboxContainer>
|
||||
|
||||
@@ -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, '=' | '&' | '@'> = {}
|
||||
|
||||
Reference in New Issue
Block a user