fix: make menus scrollable when there's not enough space
This commit is contained in:
@@ -31,13 +31,13 @@ const NotesContextMenu = observer(({ appState }: Props) => {
|
||||
return appState.notes.contextMenuOpen ? (
|
||||
<div
|
||||
ref={contextMenuRef}
|
||||
className="sn-dropdown max-w-80 flex flex-col py-2 absolute"
|
||||
style={{ ...appState.notes.contextMenuPosition }}
|
||||
className="sn-dropdown max-h-120 max-w-80 flex flex-col py-2 overflow-y-scroll fixed"
|
||||
style={{
|
||||
...appState.notes.contextMenuPosition,
|
||||
maxHeight: appState.notes.contextMenuMaxHeight,
|
||||
}}
|
||||
>
|
||||
<NotesOptions
|
||||
appState={appState}
|
||||
closeOnBlur={closeOnBlur}
|
||||
/>
|
||||
<NotesOptions appState={appState} closeOnBlur={closeOnBlur} />
|
||||
</div>
|
||||
) : null;
|
||||
});
|
||||
|
||||
@@ -19,11 +19,16 @@ type Props = {
|
||||
export const NotesOptions = observer(
|
||||
({ appState, closeOnBlur, onSubmenuChange }: Props) => {
|
||||
const [tagsMenuOpen, setTagsMenuOpen] = useState(false);
|
||||
const [tagsMenuPosition, setTagsMenuPosition] = useState({
|
||||
const [tagsMenuPosition, setTagsMenuPosition] = useState<{
|
||||
top: number;
|
||||
right?: number;
|
||||
left?: number;
|
||||
}>({
|
||||
top: 0,
|
||||
right: 0,
|
||||
});
|
||||
const [tagsMenuMaxHeight, setTagsMenuMaxHeight] = useState<number | 'auto'>('auto');
|
||||
const [tagsMenuMaxHeight, setTagsMenuMaxHeight] =
|
||||
useState<number | 'auto'>('auto');
|
||||
|
||||
const toggleOn = (condition: (note: SNNote) => boolean) => {
|
||||
const notesMatchingAttribute = notes.filter(condition);
|
||||
@@ -62,24 +67,27 @@ export const NotesOptions = observer(
|
||||
const defaultFontSize = window.getComputedStyle(
|
||||
document.documentElement
|
||||
).fontSize;
|
||||
const maxTagsMenuSize = parseFloat(defaultFontSize) * 20;
|
||||
const { clientWidth, clientHeight } = document.body;
|
||||
const maxTagsMenuSize = parseFloat(defaultFontSize) * 30;
|
||||
const { clientWidth, clientHeight } = document.documentElement;
|
||||
const buttonRect = tagsButtonRef.current.getBoundingClientRect();
|
||||
const { offsetTop, offsetWidth } = tagsButtonRef.current;
|
||||
const footerHeight = 32;
|
||||
|
||||
if (buttonRect.top + maxTagsMenuSize > clientHeight - footerHeight) {
|
||||
if ((buttonRect.top + maxTagsMenuSize) > (clientHeight - footerHeight)) {
|
||||
setTagsMenuMaxHeight(clientHeight - buttonRect.top - footerHeight - 2);
|
||||
}
|
||||
|
||||
setTagsMenuPosition({
|
||||
top: offsetTop,
|
||||
right:
|
||||
buttonRect.right + maxTagsMenuSize >
|
||||
clientWidth
|
||||
? offsetWidth
|
||||
: -offsetWidth,
|
||||
});
|
||||
if ((buttonRect.right + maxTagsMenuSize) > clientWidth) {
|
||||
setTagsMenuPosition({
|
||||
top: buttonRect.top,
|
||||
right: clientWidth - buttonRect.left,
|
||||
});
|
||||
} else {
|
||||
setTagsMenuPosition({
|
||||
top: buttonRect.top,
|
||||
left: buttonRect.right,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
setTagsMenuOpen(!tagsMenuOpen);
|
||||
};
|
||||
@@ -127,10 +135,7 @@ export const NotesOptions = observer(
|
||||
</Switch>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
{appState.tags.tagsCount > 0 && (
|
||||
<Disclosure
|
||||
open={tagsMenuOpen}
|
||||
onChange={openTagsMenu}
|
||||
>
|
||||
<Disclosure open={tagsMenuOpen} onChange={openTagsMenu}>
|
||||
<DisclosureButton
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
@@ -145,10 +150,7 @@ export const NotesOptions = observer(
|
||||
<Icon type="hashtag" className={iconClass} />
|
||||
{'Add tag'}
|
||||
</div>
|
||||
<Icon
|
||||
type="chevron-right"
|
||||
className="color-neutral"
|
||||
/>
|
||||
<Icon type="chevron-right" className="color-neutral" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
onKeyUp={(event) => {
|
||||
@@ -160,8 +162,9 @@ export const NotesOptions = observer(
|
||||
style={{
|
||||
...tagsMenuPosition,
|
||||
maxHeight: tagsMenuMaxHeight,
|
||||
position: 'fixed',
|
||||
}}
|
||||
className="sn-dropdown sn-dropdown-anchor-right flex flex-col py-2 max-h-80 absolute overflow-y-scroll"
|
||||
className="sn-dropdown flex flex-col py-2 max-h-120 max-w-80 fixed overflow-y-scroll"
|
||||
>
|
||||
{appState.tags.tags.map((tag) => (
|
||||
<button
|
||||
@@ -278,10 +281,7 @@ export const NotesOptions = observer(
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start">
|
||||
<Icon
|
||||
type="trash-sweep"
|
||||
className="color-danger mr-2"
|
||||
/>
|
||||
<Icon type="trash-sweep" className="color-danger mr-2" />
|
||||
<div className="flex-row">
|
||||
<div className="color-danger">Empty Trash</div>
|
||||
<div className="text-xs">
|
||||
|
||||
@@ -21,6 +21,7 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
|
||||
top: 0,
|
||||
right: 0,
|
||||
});
|
||||
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto');
|
||||
const buttonRef = useRef<HTMLButtonElement>();
|
||||
const panelRef = useRef<HTMLDivElement>();
|
||||
const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen);
|
||||
@@ -35,6 +36,9 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
|
||||
open={open}
|
||||
onChange={() => {
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
const { clientHeight } = document.documentElement;
|
||||
const footerHeight = 32;
|
||||
setMaxHeight(clientHeight - rect.bottom - footerHeight - 2);
|
||||
setPosition({
|
||||
top: rect.bottom,
|
||||
right: document.body.clientWidth - rect.right,
|
||||
@@ -65,8 +69,9 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
|
||||
ref={panelRef}
|
||||
style={{
|
||||
...position,
|
||||
maxHeight
|
||||
}}
|
||||
className="sn-dropdown flex flex-col py-2"
|
||||
className="sn-dropdown max-h-120 max-w-80 flex flex-col py-2 overflow-y-scroll fixed"
|
||||
>
|
||||
{open && (
|
||||
<NotesOptions
|
||||
|
||||
@@ -63,7 +63,7 @@ const SearchOptions = observer(({ appState }: Props) => {
|
||||
style={{
|
||||
top: optionsPanelTop,
|
||||
}}
|
||||
className="sn-dropdown sn-dropdown-anchor-right grid gap-2 py-2"
|
||||
className="sn-dropdown sn-dropdown-anchor-right absolute grid gap-2 py-2"
|
||||
>
|
||||
<Switch
|
||||
className="h-10"
|
||||
|
||||
@@ -27,6 +27,7 @@ export class NotesState {
|
||||
top: 0,
|
||||
left: 0,
|
||||
};
|
||||
contextMenuMaxHeight: number | 'auto' = 'auto';
|
||||
showProtectedWarning = false;
|
||||
|
||||
constructor(
|
||||
@@ -45,6 +46,7 @@ export class NotesState {
|
||||
|
||||
setContextMenuOpen: action,
|
||||
setContextMenuPosition: action,
|
||||
setContextMenuMaxHeight: action,
|
||||
setShowProtectedWarning: action,
|
||||
unselectNotes: action,
|
||||
});
|
||||
@@ -181,6 +183,10 @@ export class NotesState {
|
||||
this.contextMenuPosition = position;
|
||||
}
|
||||
|
||||
setContextMenuMaxHeight(maxHeight: number | 'auto'): void {
|
||||
this.contextMenuMaxHeight = maxHeight;
|
||||
}
|
||||
|
||||
async changeSelectedNotes(
|
||||
mutate: (mutator: NoteMutator) => void
|
||||
): Promise<void> {
|
||||
|
||||
@@ -308,12 +308,16 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
|
||||
await this.selectNote(note);
|
||||
}
|
||||
if (this.state.selectedNotes[note.uuid]) {
|
||||
const clientHeight = document.documentElement.clientHeight;
|
||||
const { clientHeight } = document.documentElement;
|
||||
const defaultFontSize = window.getComputedStyle(
|
||||
document.documentElement
|
||||
).fontSize;
|
||||
const maxContextMenuHeight = parseFloat(defaultFontSize) * 20;
|
||||
const maxContextMenuHeight = parseFloat(defaultFontSize) * 30;
|
||||
const footerHeight = 32;
|
||||
|
||||
// Open up-bottom is default behavior
|
||||
let openUpBottom = true;
|
||||
|
||||
const bottomSpace = clientHeight - footerHeight - e.clientY;
|
||||
const upSpace = e.clientY;
|
||||
|
||||
@@ -321,23 +325,41 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
|
||||
if (maxContextMenuHeight > bottomSpace) {
|
||||
// If there's enough space, open bottom-up
|
||||
if (upSpace > maxContextMenuHeight) {
|
||||
this.appState.notes.setContextMenuPosition({
|
||||
bottom: clientHeight - e.clientY,
|
||||
left: e.clientX,
|
||||
});
|
||||
// Else, open on top of screen
|
||||
openUpBottom = false;
|
||||
this.appState.notes.setContextMenuMaxHeight(
|
||||
'auto'
|
||||
);
|
||||
// Else, reduce max height (menu will be scrollable) and open in whichever direction there's more space
|
||||
} else {
|
||||
this.appState.notes.setContextMenuPosition({
|
||||
top: 2,
|
||||
left: e.clientX,
|
||||
});
|
||||
if (upSpace > bottomSpace) {
|
||||
this.appState.notes.setContextMenuMaxHeight(
|
||||
upSpace - 2
|
||||
);
|
||||
openUpBottom = false;
|
||||
} else {
|
||||
this.appState.notes.setContextMenuMaxHeight(
|
||||
bottomSpace - 2
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.appState.notes.setContextMenuMaxHeight(
|
||||
'auto'
|
||||
);
|
||||
}
|
||||
|
||||
if (openUpBottom) {
|
||||
this.appState.notes.setContextMenuPosition({
|
||||
top: e.clientY,
|
||||
left: e.clientX,
|
||||
});
|
||||
} else {
|
||||
this.appState.notes.setContextMenuPosition({
|
||||
bottom: clientHeight - e.clientY,
|
||||
left: e.clientX,
|
||||
});
|
||||
}
|
||||
|
||||
this.appState.notes.setContextMenuOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,10 @@
|
||||
max-width: 15rem;
|
||||
}
|
||||
|
||||
.max-w-80 {
|
||||
max-width: 20rem;
|
||||
}
|
||||
|
||||
.h-1px {
|
||||
height: 1px;
|
||||
}
|
||||
@@ -151,8 +155,12 @@
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.max-h-80 {
|
||||
max-height: 20rem;
|
||||
.max-h-120 {
|
||||
max-height: 30rem;
|
||||
}
|
||||
|
||||
.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.overflow-y-scroll {
|
||||
@@ -198,7 +206,6 @@
|
||||
}
|
||||
|
||||
.sn-dropdown {
|
||||
@extend .absolute;
|
||||
@extend .bg-default;
|
||||
@extend .min-w-80;
|
||||
@extend .transition-transform;
|
||||
|
||||
Reference in New Issue
Block a user