fix: make menus scrollable when there's not enough space

This commit is contained in:
Antonella Sgarlatta
2021-05-20 16:57:34 -03:00
parent c7be130a94
commit 6a9d54c2ae
7 changed files with 89 additions and 49 deletions

View File

@@ -31,13 +31,13 @@ const NotesContextMenu = observer(({ appState }: Props) => {
return appState.notes.contextMenuOpen ? ( return appState.notes.contextMenuOpen ? (
<div <div
ref={contextMenuRef} ref={contextMenuRef}
className="sn-dropdown max-w-80 flex flex-col py-2 absolute" className="sn-dropdown max-h-120 max-w-80 flex flex-col py-2 overflow-y-scroll fixed"
style={{ ...appState.notes.contextMenuPosition }} style={{
...appState.notes.contextMenuPosition,
maxHeight: appState.notes.contextMenuMaxHeight,
}}
> >
<NotesOptions <NotesOptions appState={appState} closeOnBlur={closeOnBlur} />
appState={appState}
closeOnBlur={closeOnBlur}
/>
</div> </div>
) : null; ) : null;
}); });

View File

@@ -19,11 +19,16 @@ type Props = {
export const NotesOptions = observer( export const NotesOptions = observer(
({ appState, closeOnBlur, onSubmenuChange }: Props) => { ({ appState, closeOnBlur, onSubmenuChange }: Props) => {
const [tagsMenuOpen, setTagsMenuOpen] = useState(false); const [tagsMenuOpen, setTagsMenuOpen] = useState(false);
const [tagsMenuPosition, setTagsMenuPosition] = useState({ const [tagsMenuPosition, setTagsMenuPosition] = useState<{
top: number;
right?: number;
left?: number;
}>({
top: 0, top: 0,
right: 0, right: 0,
}); });
const [tagsMenuMaxHeight, setTagsMenuMaxHeight] = useState<number | 'auto'>('auto'); const [tagsMenuMaxHeight, setTagsMenuMaxHeight] =
useState<number | 'auto'>('auto');
const toggleOn = (condition: (note: SNNote) => boolean) => { const toggleOn = (condition: (note: SNNote) => boolean) => {
const notesMatchingAttribute = notes.filter(condition); const notesMatchingAttribute = notes.filter(condition);
@@ -62,24 +67,27 @@ export const NotesOptions = observer(
const defaultFontSize = window.getComputedStyle( const defaultFontSize = window.getComputedStyle(
document.documentElement document.documentElement
).fontSize; ).fontSize;
const maxTagsMenuSize = parseFloat(defaultFontSize) * 20; const maxTagsMenuSize = parseFloat(defaultFontSize) * 30;
const { clientWidth, clientHeight } = document.body; const { clientWidth, clientHeight } = document.documentElement;
const buttonRect = tagsButtonRef.current.getBoundingClientRect(); const buttonRect = tagsButtonRef.current.getBoundingClientRect();
const { offsetTop, offsetWidth } = tagsButtonRef.current;
const footerHeight = 32; const footerHeight = 32;
if (buttonRect.top + maxTagsMenuSize > clientHeight - footerHeight) { if ((buttonRect.top + maxTagsMenuSize) > (clientHeight - footerHeight)) {
setTagsMenuMaxHeight(clientHeight - buttonRect.top - footerHeight - 2); setTagsMenuMaxHeight(clientHeight - buttonRect.top - footerHeight - 2);
} }
setTagsMenuPosition({ if ((buttonRect.right + maxTagsMenuSize) > clientWidth) {
top: offsetTop, setTagsMenuPosition({
right: top: buttonRect.top,
buttonRect.right + maxTagsMenuSize > right: clientWidth - buttonRect.left,
clientWidth });
? offsetWidth } else {
: -offsetWidth, setTagsMenuPosition({
}); top: buttonRect.top,
left: buttonRect.right,
});
}
setTagsMenuOpen(!tagsMenuOpen); setTagsMenuOpen(!tagsMenuOpen);
}; };
@@ -127,10 +135,7 @@ export const NotesOptions = observer(
</Switch> </Switch>
<div className="h-1px my-2 bg-border"></div> <div className="h-1px my-2 bg-border"></div>
{appState.tags.tagsCount > 0 && ( {appState.tags.tagsCount > 0 && (
<Disclosure <Disclosure open={tagsMenuOpen} onChange={openTagsMenu}>
open={tagsMenuOpen}
onChange={openTagsMenu}
>
<DisclosureButton <DisclosureButton
onKeyUp={(event) => { onKeyUp={(event) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
@@ -145,10 +150,7 @@ export const NotesOptions = observer(
<Icon type="hashtag" className={iconClass} /> <Icon type="hashtag" className={iconClass} />
{'Add tag'} {'Add tag'}
</div> </div>
<Icon <Icon type="chevron-right" className="color-neutral" />
type="chevron-right"
className="color-neutral"
/>
</DisclosureButton> </DisclosureButton>
<DisclosurePanel <DisclosurePanel
onKeyUp={(event) => { onKeyUp={(event) => {
@@ -160,8 +162,9 @@ export const NotesOptions = observer(
style={{ style={{
...tagsMenuPosition, ...tagsMenuPosition,
maxHeight: tagsMenuMaxHeight, 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) => ( {appState.tags.tags.map((tag) => (
<button <button
@@ -278,10 +281,7 @@ export const NotesOptions = observer(
}} }}
> >
<div className="flex items-start"> <div className="flex items-start">
<Icon <Icon type="trash-sweep" className="color-danger mr-2" />
type="trash-sweep"
className="color-danger mr-2"
/>
<div className="flex-row"> <div className="flex-row">
<div className="color-danger">Empty Trash</div> <div className="color-danger">Empty Trash</div>
<div className="text-xs"> <div className="text-xs">

View File

@@ -21,6 +21,7 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
top: 0, top: 0,
right: 0, right: 0,
}); });
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto');
const buttonRef = useRef<HTMLButtonElement>(); const buttonRef = useRef<HTMLButtonElement>();
const panelRef = useRef<HTMLDivElement>(); const panelRef = useRef<HTMLDivElement>();
const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen); const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen);
@@ -35,6 +36,9 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
open={open} open={open}
onChange={() => { onChange={() => {
const rect = buttonRef.current.getBoundingClientRect(); const rect = buttonRef.current.getBoundingClientRect();
const { clientHeight } = document.documentElement;
const footerHeight = 32;
setMaxHeight(clientHeight - rect.bottom - footerHeight - 2);
setPosition({ setPosition({
top: rect.bottom, top: rect.bottom,
right: document.body.clientWidth - rect.right, right: document.body.clientWidth - rect.right,
@@ -65,8 +69,9 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
ref={panelRef} ref={panelRef}
style={{ style={{
...position, ...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 && ( {open && (
<NotesOptions <NotesOptions

View File

@@ -63,7 +63,7 @@ const SearchOptions = observer(({ appState }: Props) => {
style={{ style={{
top: optionsPanelTop, 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 <Switch
className="h-10" className="h-10"

View File

@@ -27,6 +27,7 @@ export class NotesState {
top: 0, top: 0,
left: 0, left: 0,
}; };
contextMenuMaxHeight: number | 'auto' = 'auto';
showProtectedWarning = false; showProtectedWarning = false;
constructor( constructor(
@@ -45,6 +46,7 @@ export class NotesState {
setContextMenuOpen: action, setContextMenuOpen: action,
setContextMenuPosition: action, setContextMenuPosition: action,
setContextMenuMaxHeight: action,
setShowProtectedWarning: action, setShowProtectedWarning: action,
unselectNotes: action, unselectNotes: action,
}); });
@@ -181,6 +183,10 @@ export class NotesState {
this.contextMenuPosition = position; this.contextMenuPosition = position;
} }
setContextMenuMaxHeight(maxHeight: number | 'auto'): void {
this.contextMenuMaxHeight = maxHeight;
}
async changeSelectedNotes( async changeSelectedNotes(
mutate: (mutator: NoteMutator) => void mutate: (mutator: NoteMutator) => void
): Promise<void> { ): Promise<void> {

View File

@@ -308,12 +308,16 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
await this.selectNote(note); await this.selectNote(note);
} }
if (this.state.selectedNotes[note.uuid]) { if (this.state.selectedNotes[note.uuid]) {
const clientHeight = document.documentElement.clientHeight; const { clientHeight } = document.documentElement;
const defaultFontSize = window.getComputedStyle( const defaultFontSize = window.getComputedStyle(
document.documentElement document.documentElement
).fontSize; ).fontSize;
const maxContextMenuHeight = parseFloat(defaultFontSize) * 20; const maxContextMenuHeight = parseFloat(defaultFontSize) * 30;
const footerHeight = 32; const footerHeight = 32;
// Open up-bottom is default behavior
let openUpBottom = true;
const bottomSpace = clientHeight - footerHeight - e.clientY; const bottomSpace = clientHeight - footerHeight - e.clientY;
const upSpace = e.clientY; const upSpace = e.clientY;
@@ -321,23 +325,41 @@ class NotesViewCtrl extends PureViewCtrl<unknown, NotesCtrlState> {
if (maxContextMenuHeight > bottomSpace) { if (maxContextMenuHeight > bottomSpace) {
// If there's enough space, open bottom-up // If there's enough space, open bottom-up
if (upSpace > maxContextMenuHeight) { if (upSpace > maxContextMenuHeight) {
this.appState.notes.setContextMenuPosition({ openUpBottom = false;
bottom: clientHeight - e.clientY, this.appState.notes.setContextMenuMaxHeight(
left: e.clientX, 'auto'
}); );
// Else, open on top of screen // Else, reduce max height (menu will be scrollable) and open in whichever direction there's more space
} else { } else {
this.appState.notes.setContextMenuPosition({ if (upSpace > bottomSpace) {
top: 2, this.appState.notes.setContextMenuMaxHeight(
left: e.clientX, upSpace - 2
}); );
openUpBottom = false;
} else {
this.appState.notes.setContextMenuMaxHeight(
bottomSpace - 2
);
}
} }
} else { } else {
this.appState.notes.setContextMenuMaxHeight(
'auto'
);
}
if (openUpBottom) {
this.appState.notes.setContextMenuPosition({ this.appState.notes.setContextMenuPosition({
top: e.clientY, top: e.clientY,
left: e.clientX, left: e.clientX,
}); });
} else {
this.appState.notes.setContextMenuPosition({
bottom: clientHeight - e.clientY,
left: e.clientX,
});
} }
this.appState.notes.setContextMenuOpen(true); this.appState.notes.setContextMenuOpen(true);
} }
} }

View File

@@ -127,6 +127,10 @@
max-width: 15rem; max-width: 15rem;
} }
.max-w-80 {
max-width: 20rem;
}
.h-1px { .h-1px {
height: 1px; height: 1px;
} }
@@ -151,8 +155,12 @@
height: 2.5rem; height: 2.5rem;
} }
.max-h-80 { .max-h-120 {
max-height: 20rem; max-height: 30rem;
}
.fixed {
position: fixed;
} }
.overflow-y-scroll { .overflow-y-scroll {
@@ -198,7 +206,6 @@
} }
.sn-dropdown { .sn-dropdown {
@extend .absolute;
@extend .bg-default; @extend .bg-default;
@extend .min-w-80; @extend .min-w-80;
@extend .transition-transform; @extend .transition-transform;