feat: close submenu if another submenu is opened (#911)
This commit is contained in:
129
app/assets/javascripts/components/NotesOptions/AddTagOption.tsx
Normal file
129
app/assets/javascripts/components/NotesOptions/AddTagOption.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { AppState } from '@/ui_models/app_state';
|
||||||
|
import {
|
||||||
|
calculateSubmenuStyle,
|
||||||
|
SubmenuStyle,
|
||||||
|
} from '@/utils/calculateSubmenuStyle';
|
||||||
|
import {
|
||||||
|
Disclosure,
|
||||||
|
DisclosureButton,
|
||||||
|
DisclosurePanel,
|
||||||
|
} from '@reach/disclosure';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { FunctionComponent } from 'preact';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||||
|
import { Icon } from '../Icon';
|
||||||
|
import { useCloseOnBlur } from '../utils';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
appState: AppState;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddTagOption: FunctionComponent<Props> = observer(
|
||||||
|
({ appState }) => {
|
||||||
|
const menuContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
maxHeight: 'auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen);
|
||||||
|
|
||||||
|
const toggleTagsMenu = () => {
|
||||||
|
if (!isMenuOpen) {
|
||||||
|
const menuPosition = calculateSubmenuStyle(menuButtonRef.current);
|
||||||
|
if (menuPosition) {
|
||||||
|
setMenuStyle(menuPosition);
|
||||||
|
console.log(menuPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsMenuOpen(!isMenuOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const recalculateMenuStyle = useCallback(() => {
|
||||||
|
const newMenuPosition = calculateSubmenuStyle(
|
||||||
|
menuButtonRef.current,
|
||||||
|
menuRef.current
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newMenuPosition) {
|
||||||
|
setMenuStyle(newMenuPosition);
|
||||||
|
console.log(newMenuPosition);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMenuOpen) {
|
||||||
|
setTimeout(() => {
|
||||||
|
recalculateMenuStyle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isMenuOpen, recalculateMenuStyle]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={menuContainerRef}>
|
||||||
|
<Disclosure open={isMenuOpen} onChange={toggleTagsMenu}>
|
||||||
|
<DisclosureButton
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setIsMenuOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={closeOnBlur}
|
||||||
|
ref={menuButtonRef}
|
||||||
|
className="sn-dropdown-item justify-between"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Icon type="hashtag" className="mr-2 color-neutral" />
|
||||||
|
Add tag
|
||||||
|
</div>
|
||||||
|
<Icon type="chevron-right" className="color-neutral" />
|
||||||
|
</DisclosureButton>
|
||||||
|
<DisclosurePanel
|
||||||
|
ref={menuRef}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setIsMenuOpen(false);
|
||||||
|
menuButtonRef.current?.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
...menuStyle,
|
||||||
|
position: 'fixed',
|
||||||
|
}}
|
||||||
|
className="sn-dropdown min-w-80 flex flex-col py-2 max-h-120 max-w-xs fixed overflow-y-auto"
|
||||||
|
>
|
||||||
|
{appState.tags.tags.map((tag) => (
|
||||||
|
<button
|
||||||
|
key={tag.title}
|
||||||
|
className="sn-dropdown-item sn-dropdown-item--no-icon max-w-80"
|
||||||
|
onBlur={closeOnBlur}
|
||||||
|
onClick={() => {
|
||||||
|
appState.notes.isTagInSelectedNotes(tag)
|
||||||
|
? appState.notes.removeTagFromSelectedNotes(tag)
|
||||||
|
: appState.notes.addTagToSelectedNotes(tag);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`whitespace-nowrap overflow-hidden overflow-ellipsis
|
||||||
|
${
|
||||||
|
appState.notes.isTagInSelectedNotes(tag)
|
||||||
|
? 'font-bold'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{appState.noteTags.getLongTitle(tag)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</DisclosurePanel>
|
||||||
|
</Disclosure>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -15,12 +15,12 @@ import {
|
|||||||
calculateSubmenuStyle,
|
calculateSubmenuStyle,
|
||||||
SubmenuStyle,
|
SubmenuStyle,
|
||||||
} from '@/utils/calculateSubmenuStyle';
|
} from '@/utils/calculateSubmenuStyle';
|
||||||
|
import { useCloseOnBlur } from '../utils';
|
||||||
|
|
||||||
type ChangeEditorOptionProps = {
|
type ChangeEditorOptionProps = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
note: SNNote;
|
note: SNNote;
|
||||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type AccordionMenuGroup<T> = {
|
type AccordionMenuGroup<T> = {
|
||||||
@@ -40,7 +40,6 @@ export type EditorMenuGroup = AccordionMenuGroup<EditorMenuItem>;
|
|||||||
|
|
||||||
export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
|
export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
|
||||||
application,
|
application,
|
||||||
closeOnBlur,
|
|
||||||
note,
|
note,
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@@ -50,9 +49,15 @@ export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
maxHeight: 'auto',
|
maxHeight: 'auto',
|
||||||
});
|
});
|
||||||
|
const menuContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, (open: boolean) => {
|
||||||
|
setIsOpen(open);
|
||||||
|
setIsVisible(open);
|
||||||
|
});
|
||||||
|
|
||||||
const toggleChangeEditorMenu = () => {
|
const toggleChangeEditorMenu = () => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
const menuStyle = calculateSubmenuStyle(buttonRef.current);
|
const menuStyle = calculateSubmenuStyle(buttonRef.current);
|
||||||
@@ -81,49 +86,51 @@ export const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
|
|||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure open={isOpen} onChange={toggleChangeEditorMenu}>
|
<div ref={menuContainerRef}>
|
||||||
<DisclosureButton
|
<Disclosure open={isOpen} onChange={toggleChangeEditorMenu}>
|
||||||
onKeyDown={(event) => {
|
<DisclosureButton
|
||||||
if (event.key === KeyboardKey.Escape) {
|
onKeyDown={(event) => {
|
||||||
setIsOpen(false);
|
if (event.key === KeyboardKey.Escape) {
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={closeOnBlur}
|
|
||||||
ref={buttonRef}
|
|
||||||
className="sn-dropdown-item justify-between"
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Icon type="dashboard" className="color-neutral mr-2" />
|
|
||||||
Change editor
|
|
||||||
</div>
|
|
||||||
<Icon type="chevron-right" className="color-neutral" />
|
|
||||||
</DisclosureButton>
|
|
||||||
<DisclosurePanel
|
|
||||||
ref={menuRef}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === KeyboardKey.Escape) {
|
|
||||||
setIsOpen(false);
|
|
||||||
buttonRef.current?.focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
...menuStyle,
|
|
||||||
position: 'fixed',
|
|
||||||
}}
|
|
||||||
className="sn-dropdown flex flex-col max-h-120 min-w-68 fixed overflow-y-auto"
|
|
||||||
>
|
|
||||||
{isOpen && (
|
|
||||||
<ChangeEditorMenu
|
|
||||||
application={application}
|
|
||||||
closeOnBlur={closeOnBlur}
|
|
||||||
note={note}
|
|
||||||
isVisible={isVisible}
|
|
||||||
closeMenu={() => {
|
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
)}
|
onBlur={closeOnBlur}
|
||||||
</DisclosurePanel>
|
ref={buttonRef}
|
||||||
</Disclosure>
|
className="sn-dropdown-item justify-between"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Icon type="dashboard" className="color-neutral mr-2" />
|
||||||
|
Change editor
|
||||||
|
</div>
|
||||||
|
<Icon type="chevron-right" className="color-neutral" />
|
||||||
|
</DisclosureButton>
|
||||||
|
<DisclosurePanel
|
||||||
|
ref={menuRef}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === KeyboardKey.Escape) {
|
||||||
|
setIsOpen(false);
|
||||||
|
buttonRef.current?.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
...menuStyle,
|
||||||
|
position: 'fixed',
|
||||||
|
}}
|
||||||
|
className="sn-dropdown flex flex-col max-h-120 min-w-68 fixed overflow-y-auto"
|
||||||
|
>
|
||||||
|
{isOpen && (
|
||||||
|
<ChangeEditorMenu
|
||||||
|
application={application}
|
||||||
|
closeOnBlur={closeOnBlur}
|
||||||
|
note={note}
|
||||||
|
isVisible={isVisible}
|
||||||
|
closeMenu={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DisclosurePanel>
|
||||||
|
</Disclosure>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import { Action, ListedAccount, SNNote } from '@standardnotes/snjs';
|
|||||||
import { Fragment, FunctionComponent } from 'preact';
|
import { Fragment, FunctionComponent } from 'preact';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { Icon } from '../Icon';
|
import { Icon } from '../Icon';
|
||||||
|
import { useCloseOnBlur } from '../utils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
note: SNNote;
|
note: SNNote;
|
||||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ListedMenuGroup = {
|
type ListedMenuGroup = {
|
||||||
@@ -230,8 +230,8 @@ const ListedActionsMenu: FunctionComponent<ListedActionsMenuProps> = ({
|
|||||||
export const ListedActionsOption: FunctionComponent<Props> = ({
|
export const ListedActionsOption: FunctionComponent<Props> = ({
|
||||||
application,
|
application,
|
||||||
note,
|
note,
|
||||||
closeOnBlur,
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const menuContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
@@ -242,6 +242,8 @@ export const ListedActionsOption: FunctionComponent<Props> = ({
|
|||||||
maxHeight: 'auto',
|
maxHeight: 'auto',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen);
|
||||||
|
|
||||||
const toggleListedMenu = () => {
|
const toggleListedMenu = () => {
|
||||||
if (!isMenuOpen) {
|
if (!isMenuOpen) {
|
||||||
const menuPosition = calculateSubmenuStyle(menuButtonRef.current);
|
const menuPosition = calculateSubmenuStyle(menuButtonRef.current);
|
||||||
@@ -273,34 +275,36 @@ export const ListedActionsOption: FunctionComponent<Props> = ({
|
|||||||
}, [isMenuOpen, recalculateMenuStyle]);
|
}, [isMenuOpen, recalculateMenuStyle]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure open={isMenuOpen} onChange={toggleListedMenu}>
|
<div ref={menuContainerRef}>
|
||||||
<DisclosureButton
|
<Disclosure open={isMenuOpen} onChange={toggleListedMenu}>
|
||||||
ref={menuButtonRef}
|
<DisclosureButton
|
||||||
onBlur={closeOnBlur}
|
ref={menuButtonRef}
|
||||||
className="sn-dropdown-item justify-between"
|
onBlur={closeOnBlur}
|
||||||
>
|
className="sn-dropdown-item justify-between"
|
||||||
<div className="flex items-center">
|
>
|
||||||
<Icon type="listed" className="color-neutral mr-2" />
|
<div className="flex items-center">
|
||||||
Listed actions
|
<Icon type="listed" className="color-neutral mr-2" />
|
||||||
</div>
|
Listed actions
|
||||||
<Icon type="chevron-right" className="color-neutral" />
|
</div>
|
||||||
</DisclosureButton>
|
<Icon type="chevron-right" className="color-neutral" />
|
||||||
<DisclosurePanel
|
</DisclosureButton>
|
||||||
ref={menuRef}
|
<DisclosurePanel
|
||||||
style={{
|
ref={menuRef}
|
||||||
...menuStyle,
|
style={{
|
||||||
position: 'fixed',
|
...menuStyle,
|
||||||
}}
|
position: 'fixed',
|
||||||
className="sn-dropdown flex flex-col max-h-120 min-w-68 pb-1 fixed overflow-y-auto"
|
}}
|
||||||
>
|
className="sn-dropdown flex flex-col max-h-120 min-w-68 pb-1 fixed overflow-y-auto"
|
||||||
{isMenuOpen && (
|
>
|
||||||
<ListedActionsMenu
|
{isMenuOpen && (
|
||||||
application={application}
|
<ListedActionsMenu
|
||||||
note={note}
|
application={application}
|
||||||
recalculateMenuStyle={recalculateMenuStyle}
|
note={note}
|
||||||
/>
|
recalculateMenuStyle={recalculateMenuStyle}
|
||||||
)}
|
/>
|
||||||
</DisclosurePanel>
|
)}
|
||||||
</Disclosure>
|
</DisclosurePanel>
|
||||||
|
</Disclosure>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,29 +2,20 @@ import { AppState } from '@/ui_models/app_state';
|
|||||||
import { Icon } from '../Icon';
|
import { Icon } from '../Icon';
|
||||||
import { Switch } from '../Switch';
|
import { Switch } from '../Switch';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { useRef, useState, useEffect, useMemo } from 'preact/hooks';
|
import { useState, useEffect, useMemo } from 'preact/hooks';
|
||||||
import {
|
|
||||||
Disclosure,
|
|
||||||
DisclosureButton,
|
|
||||||
DisclosurePanel,
|
|
||||||
} from '@reach/disclosure';
|
|
||||||
import { SNApplication, SNNote } from '@standardnotes/snjs';
|
import { SNApplication, SNNote } from '@standardnotes/snjs';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { KeyboardModifier } from '@/services/ioService';
|
import { KeyboardModifier } from '@/services/ioService';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { ChangeEditorOption } from './ChangeEditorOption';
|
import { ChangeEditorOption } from './ChangeEditorOption';
|
||||||
import {
|
import { BYTES_IN_ONE_MEGABYTE } from '@/constants';
|
||||||
MENU_MARGIN_FROM_APP_BORDER,
|
|
||||||
MAX_MENU_SIZE_MULTIPLIER,
|
|
||||||
BYTES_IN_ONE_MEGABYTE,
|
|
||||||
} from '@/constants';
|
|
||||||
import { ListedActionsOption } from './ListedActionsOption';
|
import { ListedActionsOption } from './ListedActionsOption';
|
||||||
|
import { AddTagOption } from './AddTagOption';
|
||||||
|
|
||||||
export type NotesOptionsProps = {
|
export type NotesOptionsProps = {
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
|
||||||
onSubmenuChange?: (submenuOpen: boolean) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type DeletePermanentlyButtonProps = {
|
type DeletePermanentlyButtonProps = {
|
||||||
@@ -206,24 +197,7 @@ const NoteSizeWarning: FunctionComponent<{
|
|||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
export const NotesOptions = observer(
|
export const NotesOptions = observer(
|
||||||
({
|
({ application, appState, closeOnBlur }: NotesOptionsProps) => {
|
||||||
application,
|
|
||||||
appState,
|
|
||||||
closeOnBlur,
|
|
||||||
onSubmenuChange,
|
|
||||||
}: NotesOptionsProps) => {
|
|
||||||
const [tagsMenuOpen, setTagsMenuOpen] = useState(false);
|
|
||||||
const [tagsMenuPosition, setTagsMenuPosition] = useState<{
|
|
||||||
top: number;
|
|
||||||
right?: number;
|
|
||||||
left?: number;
|
|
||||||
}>({
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
});
|
|
||||||
const [tagsMenuMaxHeight, setTagsMenuMaxHeight] = useState<number | 'auto'>(
|
|
||||||
'auto'
|
|
||||||
);
|
|
||||||
const [altKeyDown, setAltKeyDown] = useState(false);
|
const [altKeyDown, setAltKeyDown] = useState(false);
|
||||||
|
|
||||||
const toggleOn = (condition: (note: SNNote) => boolean) => {
|
const toggleOn = (condition: (note: SNNote) => boolean) => {
|
||||||
@@ -246,14 +220,6 @@ export const NotesOptions = observer(
|
|||||||
const unpinned = notes.some((note) => !note.pinned);
|
const unpinned = notes.some((note) => !note.pinned);
|
||||||
const errored = notes.some((note) => note.errorDecrypting);
|
const errored = notes.some((note) => note.errorDecrypting);
|
||||||
|
|
||||||
const tagsButtonRef = useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (onSubmenuChange) {
|
|
||||||
onSubmenuChange(tagsMenuOpen);
|
|
||||||
}
|
|
||||||
}, [tagsMenuOpen, onSubmenuChange]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const removeAltKeyObserver = application.io.addKeyObserver({
|
const removeAltKeyObserver = application.io.addKeyObserver({
|
||||||
modifiers: [KeyboardModifier.Alt],
|
modifiers: [KeyboardModifier.Alt],
|
||||||
@@ -270,48 +236,6 @@ export const NotesOptions = observer(
|
|||||||
};
|
};
|
||||||
}, [application]);
|
}, [application]);
|
||||||
|
|
||||||
const openTagsMenu = () => {
|
|
||||||
const defaultFontSize = window.getComputedStyle(
|
|
||||||
document.documentElement
|
|
||||||
).fontSize;
|
|
||||||
const maxTagsMenuSize =
|
|
||||||
parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER;
|
|
||||||
const { clientWidth, clientHeight } = document.documentElement;
|
|
||||||
const buttonRect = tagsButtonRef.current?.getBoundingClientRect();
|
|
||||||
const footerElementRect = document
|
|
||||||
.getElementById('footer-bar')
|
|
||||||
?.getBoundingClientRect();
|
|
||||||
const footerHeightInPx = footerElementRect?.height;
|
|
||||||
|
|
||||||
if (buttonRect && footerHeightInPx) {
|
|
||||||
if (
|
|
||||||
buttonRect.top + maxTagsMenuSize >
|
|
||||||
clientHeight - footerHeightInPx
|
|
||||||
) {
|
|
||||||
setTagsMenuMaxHeight(
|
|
||||||
clientHeight -
|
|
||||||
buttonRect.top -
|
|
||||||
footerHeightInPx -
|
|
||||||
MENU_MARGIN_FROM_APP_BORDER
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buttonRect.right + maxTagsMenuSize > clientWidth) {
|
|
||||||
setTagsMenuPosition({
|
|
||||||
top: buttonRect.top,
|
|
||||||
right: clientWidth - buttonRect.left,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setTagsMenuPosition({
|
|
||||||
top: buttonRect.top,
|
|
||||||
left: buttonRect.right,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTagsMenuOpen(!tagsMenuOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadSelectedItems = () => {
|
const downloadSelectedItems = () => {
|
||||||
notes.forEach((note) => {
|
notes.forEach((note) => {
|
||||||
const editor = application.componentManager.editorForNote(note);
|
const editor = application.componentManager.editorForNote(note);
|
||||||
@@ -416,70 +340,12 @@ export const NotesOptions = observer(
|
|||||||
<ChangeEditorOption
|
<ChangeEditorOption
|
||||||
appState={appState}
|
appState={appState}
|
||||||
application={application}
|
application={application}
|
||||||
closeOnBlur={closeOnBlur}
|
|
||||||
note={notes[0]}
|
note={notes[0]}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="min-h-1px my-2 bg-border"></div>
|
<div className="min-h-1px my-2 bg-border"></div>
|
||||||
{appState.tags.tagsCount > 0 && (
|
{appState.tags.tagsCount > 0 && <AddTagOption appState={appState} />}
|
||||||
<Disclosure open={tagsMenuOpen} onChange={openTagsMenu}>
|
|
||||||
<DisclosureButton
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
setTagsMenuOpen(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={closeOnBlur}
|
|
||||||
ref={tagsButtonRef}
|
|
||||||
className="sn-dropdown-item justify-between"
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Icon type="hashtag" className={iconClass} />
|
|
||||||
{'Add tag'}
|
|
||||||
</div>
|
|
||||||
<Icon type="chevron-right" className="color-neutral" />
|
|
||||||
</DisclosureButton>
|
|
||||||
<DisclosurePanel
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
setTagsMenuOpen(false);
|
|
||||||
tagsButtonRef.current?.focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
...tagsMenuPosition,
|
|
||||||
maxHeight: tagsMenuMaxHeight,
|
|
||||||
position: 'fixed',
|
|
||||||
}}
|
|
||||||
className="sn-dropdown min-w-80 flex flex-col py-2 max-h-120 max-w-xs fixed overflow-y-auto"
|
|
||||||
>
|
|
||||||
{appState.tags.tags.map((tag) => (
|
|
||||||
<button
|
|
||||||
key={tag.title}
|
|
||||||
className="sn-dropdown-item sn-dropdown-item--no-icon max-w-80"
|
|
||||||
onBlur={closeOnBlur}
|
|
||||||
onClick={() => {
|
|
||||||
appState.notes.isTagInSelectedNotes(tag)
|
|
||||||
? appState.notes.removeTagFromSelectedNotes(tag)
|
|
||||||
: appState.notes.addTagToSelectedNotes(tag);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`whitespace-nowrap overflow-hidden overflow-ellipsis
|
|
||||||
${
|
|
||||||
appState.notes.isTagInSelectedNotes(tag)
|
|
||||||
? 'font-bold'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{appState.noteTags.getLongTitle(tag)}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</DisclosurePanel>
|
|
||||||
</Disclosure>
|
|
||||||
)}
|
|
||||||
{unpinned && (
|
{unpinned && (
|
||||||
<button
|
<button
|
||||||
onBlur={closeOnBlur}
|
onBlur={closeOnBlur}
|
||||||
@@ -604,11 +470,7 @@ export const NotesOptions = observer(
|
|||||||
{notes.length === 1 ? (
|
{notes.length === 1 ? (
|
||||||
<>
|
<>
|
||||||
<div className="min-h-1px my-2 bg-border"></div>
|
<div className="min-h-1px my-2 bg-border"></div>
|
||||||
<ListedActionsOption
|
<ListedActionsOption application={application} note={notes[0]} />
|
||||||
application={application}
|
|
||||||
closeOnBlur={closeOnBlur}
|
|
||||||
note={notes[0]}
|
|
||||||
/>
|
|
||||||
<div className="min-h-1px my-2 bg-border"></div>
|
<div className="min-h-1px my-2 bg-border"></div>
|
||||||
<SpellcheckOptions appState={appState} note={notes[0]} />
|
<SpellcheckOptions appState={appState} note={notes[0]} />
|
||||||
<div className="min-h-1px my-2 bg-border"></div>
|
<div className="min-h-1px my-2 bg-border"></div>
|
||||||
|
|||||||
@@ -30,11 +30,6 @@ export const NotesOptionsPanel = observer(
|
|||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
const panelRef = useRef<HTMLDivElement>(null);
|
const panelRef = useRef<HTMLDivElement>(null);
|
||||||
const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen);
|
const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen);
|
||||||
const [submenuOpen, setSubmenuOpen] = useState(false);
|
|
||||||
|
|
||||||
const onSubmenuChange = (open: boolean) => {
|
|
||||||
setSubmenuOpen(open);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure
|
<Disclosure
|
||||||
@@ -64,7 +59,7 @@ export const NotesOptionsPanel = observer(
|
|||||||
>
|
>
|
||||||
<DisclosureButton
|
<DisclosureButton
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Escape' && !submenuOpen) {
|
if (event.key === 'Escape') {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -77,7 +72,7 @@ export const NotesOptionsPanel = observer(
|
|||||||
</DisclosureButton>
|
</DisclosureButton>
|
||||||
<DisclosurePanel
|
<DisclosurePanel
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Escape' && !submenuOpen) {
|
if (event.key === 'Escape') {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
buttonRef.current?.focus();
|
buttonRef.current?.focus();
|
||||||
}
|
}
|
||||||
@@ -96,7 +91,6 @@ export const NotesOptionsPanel = observer(
|
|||||||
application={application}
|
application={application}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
closeOnBlur={closeOnBlur}
|
closeOnBlur={closeOnBlur}
|
||||||
onSubmenuChange={onSubmenuChange}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DisclosurePanel>
|
</DisclosurePanel>
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import {
|
|||||||
RefCallback,
|
RefCallback,
|
||||||
ComponentChild,
|
ComponentChild,
|
||||||
} from 'preact';
|
} from 'preact';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef } from 'preact/hooks';
|
||||||
import { JSXInternal } from 'preact/src/jsx';
|
import { JSXInternal } from 'preact/src/jsx';
|
||||||
import { MenuItem, MenuItemListElement } from './MenuItem';
|
import { MenuItem, MenuItemListElement } from './MenuItem';
|
||||||
import { KeyboardKey } from '@/services/ioService';
|
import { KeyboardKey } from '@/services/ioService';
|
||||||
|
import { useListKeyboardNavigation } from '../utils';
|
||||||
|
|
||||||
type MenuProps = {
|
type MenuProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -28,7 +29,6 @@ export const Menu: FunctionComponent<MenuProps> = ({
|
|||||||
closeMenu,
|
closeMenu,
|
||||||
isOpen,
|
isOpen,
|
||||||
}: MenuProps) => {
|
}: MenuProps) => {
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
|
||||||
const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
const menuItemRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||||
|
|
||||||
const menuElementRef = useRef<HTMLMenuElement>(null);
|
const menuElementRef = useRef<HTMLMenuElement>(null);
|
||||||
@@ -40,44 +40,21 @@ export const Menu: FunctionComponent<MenuProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (event.key) {
|
if (event.key === KeyboardKey.Escape) {
|
||||||
case KeyboardKey.Home:
|
closeMenu?.();
|
||||||
setCurrentIndex(0);
|
return;
|
||||||
break;
|
|
||||||
case KeyboardKey.End:
|
|
||||||
setCurrentIndex(
|
|
||||||
menuItemRefs.current.length ? menuItemRefs.current.length - 1 : 0
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case KeyboardKey.Down:
|
|
||||||
setCurrentIndex((index) => {
|
|
||||||
if (index + 1 < menuItemRefs.current.length) {
|
|
||||||
return index + 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case KeyboardKey.Up:
|
|
||||||
setCurrentIndex((index) => {
|
|
||||||
if (index - 1 > -1) {
|
|
||||||
return index - 1;
|
|
||||||
} else {
|
|
||||||
return menuItemRefs.current.length - 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case KeyboardKey.Escape:
|
|
||||||
closeMenu?.();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useListKeyboardNavigation(menuElementRef);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && menuItemRefs.current[currentIndex]) {
|
if (isOpen && menuItemRefs.current.length > 0) {
|
||||||
menuItemRefs.current[currentIndex]?.focus();
|
setTimeout(() => {
|
||||||
|
menuElementRef.current?.focus();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [currentIndex, isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const pushRefToArray: RefCallback<HTMLLIElement> = (instance) => {
|
const pushRefToArray: RefCallback<HTMLLIElement> = (instance) => {
|
||||||
if (instance && instance.children) {
|
if (instance && instance.children) {
|
||||||
@@ -128,7 +105,7 @@ export const Menu: FunctionComponent<MenuProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<menu
|
<menu
|
||||||
className={`m-0 p-0 list-style-none ${className}`}
|
className={`m-0 p-0 list-style-none focus:shadow-none ${className}`}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
ref={menuElementRef}
|
ref={menuElementRef}
|
||||||
style={style}
|
style={style}
|
||||||
|
|||||||
@@ -89,13 +89,17 @@ export const calculateDifferenceBetweenDatesInDays = (
|
|||||||
export const useListKeyboardNavigation = (
|
export const useListKeyboardNavigation = (
|
||||||
container: Ref<HTMLElement | null>
|
container: Ref<HTMLElement | null>
|
||||||
) => {
|
) => {
|
||||||
const [listItems, setListItems] = useState<NodeListOf<HTMLButtonElement>>();
|
const [listItems, setListItems] = useState<HTMLButtonElement[]>();
|
||||||
const [focusedItemIndex, setFocusedItemIndex] = useState<number>(0);
|
const [focusedItemIndex, setFocusedItemIndex] = useState<number>(0);
|
||||||
|
|
||||||
const focusItemWithIndex = useCallback(
|
const focusItemWithIndex = useCallback(
|
||||||
(index: number) => {
|
(index: number, items?: HTMLButtonElement[]) => {
|
||||||
setFocusedItemIndex(index);
|
setFocusedItemIndex(index);
|
||||||
listItems?.[index]?.focus();
|
if (items && items.length > 0) {
|
||||||
|
items[index]?.focus();
|
||||||
|
} else {
|
||||||
|
listItems?.[index]?.focus();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[listItems]
|
[listItems]
|
||||||
);
|
);
|
||||||
@@ -103,7 +107,7 @@ export const useListKeyboardNavigation = (
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (container.current) {
|
if (container.current) {
|
||||||
container.current.tabIndex = FOCUSABLE_BUT_NOT_TABBABLE;
|
container.current.tabIndex = FOCUSABLE_BUT_NOT_TABBABLE;
|
||||||
setListItems(container.current.querySelectorAll('button'));
|
setListItems(Array.from(container.current.querySelectorAll('button')));
|
||||||
}
|
}
|
||||||
}, [container]);
|
}, [container]);
|
||||||
|
|
||||||
@@ -116,7 +120,13 @@ export const useListKeyboardNavigation = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!listItems?.length) {
|
if (!listItems?.length) {
|
||||||
setListItems(container.current?.querySelectorAll('button'));
|
setListItems(
|
||||||
|
Array.from(
|
||||||
|
container.current?.querySelectorAll(
|
||||||
|
'button'
|
||||||
|
) as NodeListOf<HTMLButtonElement>
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (listItems) {
|
if (listItems) {
|
||||||
@@ -143,16 +153,25 @@ export const useListKeyboardNavigation = (
|
|||||||
const FIRST_ITEM_FOCUS_TIMEOUT = 20;
|
const FIRST_ITEM_FOCUS_TIMEOUT = 20;
|
||||||
|
|
||||||
const containerFocusHandler = useCallback(() => {
|
const containerFocusHandler = useCallback(() => {
|
||||||
if (listItems) {
|
let temporaryItems = listItems && listItems?.length > 0 ? listItems : [];
|
||||||
const selectedItemIndex = Array.from(listItems).findIndex(
|
if (!temporaryItems.length) {
|
||||||
|
temporaryItems = Array.from(
|
||||||
|
container.current?.querySelectorAll(
|
||||||
|
'button'
|
||||||
|
) as NodeListOf<HTMLButtonElement>
|
||||||
|
);
|
||||||
|
setListItems(temporaryItems);
|
||||||
|
}
|
||||||
|
if (temporaryItems.length > 0) {
|
||||||
|
const selectedItemIndex = Array.from(temporaryItems).findIndex(
|
||||||
(item) => item.dataset.selected
|
(item) => item.dataset.selected
|
||||||
);
|
);
|
||||||
const indexToFocus = selectedItemIndex > -1 ? selectedItemIndex : 0;
|
const indexToFocus = selectedItemIndex > -1 ? selectedItemIndex : 0;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
focusItemWithIndex(indexToFocus);
|
focusItemWithIndex(indexToFocus, temporaryItems);
|
||||||
}, FIRST_ITEM_FOCUS_TIMEOUT);
|
}, FIRST_ITEM_FOCUS_TIMEOUT);
|
||||||
}
|
}
|
||||||
}, [focusItemWithIndex, listItems]);
|
}, [container, focusItemWithIndex, listItems]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const containerElement = container.current;
|
const containerElement = container.current;
|
||||||
|
|||||||
Reference in New Issue
Block a user