feat: responsive popovers & menus (#1323)

This commit is contained in:
Aman Harwara
2022-07-21 02:20:14 +05:30
committed by GitHub
parent baf7fb0019
commit 2573407851
44 changed files with 1308 additions and 1415 deletions

View File

@@ -1,26 +1,24 @@
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
import { ViewControllerManager } from '@/Services/ViewControllerManager' import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { useCallback, useRef, FunctionComponent, KeyboardEventHandler } from 'react' import { useCallback, FunctionComponent, KeyboardEventHandler } from 'react'
import { ApplicationGroup } from '@/Application/ApplicationGroup' import { ApplicationGroup } from '@/Application/ApplicationGroup'
import { AccountMenuPane } from './AccountMenuPane' import { AccountMenuPane } from './AccountMenuPane'
import MenuPaneSelector from './MenuPaneSelector' import MenuPaneSelector from './MenuPaneSelector'
type Props = { export type AccountMenuProps = {
viewControllerManager: ViewControllerManager viewControllerManager: ViewControllerManager
application: WebApplication application: WebApplication
onClickOutside: () => void onClickOutside: () => void
mainApplicationGroup: ApplicationGroup mainApplicationGroup: ApplicationGroup
} }
const AccountMenu: FunctionComponent<Props> = ({ const AccountMenu: FunctionComponent<AccountMenuProps> = ({
application, application,
viewControllerManager, viewControllerManager,
onClickOutside,
mainApplicationGroup, mainApplicationGroup,
}) => { }) => {
const { currentPane, shouldAnimateCloseMenu } = viewControllerManager.accountMenuController const { currentPane } = viewControllerManager.accountMenuController
const closeAccountMenu = useCallback(() => { const closeAccountMenu = useCallback(() => {
viewControllerManager.accountMenuController.closeAccountMenu() viewControllerManager.accountMenuController.closeAccountMenu()
@@ -33,11 +31,6 @@ const AccountMenu: FunctionComponent<Props> = ({
[viewControllerManager], [viewControllerManager],
) )
const ref = useRef<HTMLDivElement>(null)
useCloseOnClickOutside(ref, () => {
onClickOutside()
})
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = useCallback( const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = useCallback(
(event) => { (event) => {
switch (event.key) { switch (event.key) {
@@ -56,22 +49,15 @@ const AccountMenu: FunctionComponent<Props> = ({
) )
return ( return (
<div ref={ref} id="account-menu" className="sn-component"> <div id="account-menu" className="sn-component" onKeyDown={handleKeyDown}>
<div <MenuPaneSelector
className={`max-h-120 absolute bottom-full left-0 z-footer-bar-item-panel flex min-w-80 max-w-xs cursor-auto flex-col overflow-y-auto rounded bg-default py-2 shadow-main ${ viewControllerManager={viewControllerManager}
shouldAnimateCloseMenu ? 'slide-up-animation' : 'slide-down-animation transition-transform duration-150' application={application}
}`} mainApplicationGroup={mainApplicationGroup}
onKeyDown={handleKeyDown} menuPane={currentPane}
> setMenuPane={setCurrentPane}
<MenuPaneSelector closeMenu={closeAccountMenu}
viewControllerManager={viewControllerManager} />
application={application}
mainApplicationGroup={mainApplicationGroup}
menuPane={currentPane}
setMenuPane={setCurrentPane}
closeMenu={closeAccountMenu}
/>
</div>
</div> </div>
) )
} }

View File

@@ -23,7 +23,9 @@ const WorkspaceSwitcherMenu: FunctionComponent<Props> = ({
isOpen, isOpen,
hideWorkspaceOptions = false, hideWorkspaceOptions = false,
}: Props) => { }: Props) => {
const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>([]) const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>(
mainApplicationGroup.getDescriptors(),
)
useEffect(() => { useEffect(() => {
const applicationDescriptors = mainApplicationGroup.getDescriptors() const applicationDescriptors = mainApplicationGroup.getDescriptors()

View File

@@ -1,13 +1,13 @@
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { ApplicationGroup } from '@/Application/ApplicationGroup' import { ApplicationGroup } from '@/Application/ApplicationGroup'
import { ViewControllerManager } from '@/Services/ViewControllerManager' import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' import { FunctionComponent, useCallback, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import WorkspaceSwitcherMenu from './WorkspaceSwitcherMenu' import WorkspaceSwitcherMenu from './WorkspaceSwitcherMenu'
import MenuItem from '@/Components/Menu/MenuItem' import MenuItem from '@/Components/Menu/MenuItem'
import { MenuItemType } from '@/Components/Menu/MenuItemType' import { MenuItemType } from '@/Components/Menu/MenuItemType'
import Popover from '@/Components/Popover/Popover'
type Props = { type Props = {
mainApplicationGroup: ApplicationGroup mainApplicationGroup: ApplicationGroup
@@ -16,32 +16,11 @@ type Props = {
const WorkspaceSwitcherOption: FunctionComponent<Props> = ({ mainApplicationGroup, viewControllerManager }) => { const WorkspaceSwitcherOption: FunctionComponent<Props> = ({ mainApplicationGroup, viewControllerManager }) => {
const buttonRef = useRef<HTMLButtonElement>(null) const buttonRef = useRef<HTMLButtonElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>()
const toggleMenu = useCallback(() => { const toggleMenu = useCallback(() => {
if (!isOpen) { setIsOpen((isOpen) => !isOpen)
const menuPosition = calculateSubmenuStyle(buttonRef.current) }, [])
if (menuPosition) {
setMenuStyle(menuPosition)
}
}
setIsOpen(!isOpen)
}, [isOpen, setIsOpen])
useEffect(() => {
if (isOpen) {
setTimeout(() => {
const newMenuPosition = calculateSubmenuStyle(buttonRef.current, menuRef.current)
if (newMenuPosition) {
setMenuStyle(newMenuPosition)
}
})
}
}, [isOpen])
return ( return (
<> <>
@@ -58,19 +37,20 @@ const WorkspaceSwitcherOption: FunctionComponent<Props> = ({ mainApplicationGrou
</div> </div>
<Icon type="chevron-right" className="text-neutral" /> <Icon type="chevron-right" className="text-neutral" />
</MenuItem> </MenuItem>
{isOpen && ( <Popover
<div align="end"
ref={menuRef} anchorElement={buttonRef.current}
className="max-h-120 fixed min-w-68 overflow-y-auto rounded bg-default py-2 shadow-main" className="py-2"
style={menuStyle} open={isOpen}
> side="right"
<WorkspaceSwitcherMenu togglePopover={toggleMenu}
mainApplicationGroup={mainApplicationGroup} >
viewControllerManager={viewControllerManager} <WorkspaceSwitcherMenu
isOpen={isOpen} mainApplicationGroup={mainApplicationGroup}
/> viewControllerManager={viewControllerManager}
</div> isOpen={isOpen}
)} />
</Popover>
</> </>
) )
} }

View File

@@ -1,11 +1,7 @@
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import VisuallyHidden from '@reach/visually-hidden'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import AttachedFilesPopover from './AttachedFilesPopover' import AttachedFilesPopover from './AttachedFilesPopover'
import { usePremiumModal } from '@/Hooks/usePremiumModal' import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { PopoverTabs } from './PopoverTabs' import { PopoverTabs } from './PopoverTabs'
@@ -18,6 +14,8 @@ import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import { useFileDragNDrop } from '@/Components/FileDragNDropProvider/FileDragNDropProvider' import { useFileDragNDrop } from '@/Components/FileDragNDropProvider/FileDragNDropProvider'
import { FileItem, SNNote } from '@standardnotes/snjs' import { FileItem, SNNote } from '@standardnotes/snjs'
import { addToast, ToastType } from '@standardnotes/toast' import { addToast, ToastType } from '@standardnotes/toast'
import { classNames } from '@/Utils/ConcatenateClassNames'
import Popover from '../Popover/Popover'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -34,7 +32,6 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
application, application,
featuresController, featuresController,
filesController, filesController,
filePreviewModalController,
navigationController, navigationController,
notesController, notesController,
selectionController, selectionController,
@@ -46,24 +43,9 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
const premiumModal = usePremiumModal() const premiumModal = usePremiumModal()
const note: SNNote | undefined = notesController.firstSelectedNote const note: SNNote | undefined = notesController.firstSelectedNote
const [open, setOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [position, setPosition] = useState({
top: 0,
right: 0,
})
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
const buttonRef = useRef<HTMLButtonElement>(null) const buttonRef = useRef<HTMLButtonElement>(null)
const panelRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const [closeOnBlur, keepMenuOpen] = useCloseOnBlur(containerRef, setOpen)
useEffect(() => {
if (filePreviewModalController.isOpen) {
keepMenuOpen(true)
} else {
keepMenuOpen(false)
}
}, [filePreviewModalController.isOpen, keepMenuOpen])
const [currentTab, setCurrentTab] = useState( const [currentTab, setCurrentTab] = useState(
navigationController.isInFilesView ? PopoverTabs.AllFiles : PopoverTabs.AttachedFiles, navigationController.isInFilesView ? PopoverTabs.AllFiles : PopoverTabs.AttachedFiles,
@@ -78,29 +60,14 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
}, [currentTab, isAttachedTabDisabled]) }, [currentTab, isAttachedTabDisabled])
const toggleAttachedFilesMenu = useCallback(async () => { const toggleAttachedFilesMenu = useCallback(async () => {
const rect = buttonRef.current?.getBoundingClientRect() const newOpenState = !isOpen
if (rect) {
const { clientHeight } = document.documentElement
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
const footerHeightInPx = footerElementRect?.height
if (footerHeightInPx) { if (newOpenState && onClickPreprocessing) {
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - MENU_MARGIN_FROM_APP_BORDER) await onClickPreprocessing()
}
setPosition({
top: rect.bottom,
right: document.body.clientWidth - rect.right,
})
const newOpenState = !open
if (newOpenState && onClickPreprocessing) {
await onClickPreprocessing()
}
setOpen(newOpenState)
} }
}, [onClickPreprocessing, open])
setIsOpen(newOpenState)
}, [onClickPreprocessing, isOpen])
const prospectivelyShowFilesPremiumModal = useCallback(() => { const prospectivelyShowFilesPremiumModal = useCallback(() => {
if (!featuresController.hasFiles) { if (!featuresController.hasFiles) {
@@ -132,10 +99,10 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
const { isDraggingFiles, addFilesDragInCallback, addFilesDropCallback } = useFileDragNDrop() const { isDraggingFiles, addFilesDragInCallback, addFilesDropCallback } = useFileDragNDrop()
useEffect(() => { useEffect(() => {
if (isDraggingFiles && !open) { if (isDraggingFiles && !isOpen) {
void toggleAttachedFilesMenu() void toggleAttachedFilesMenu()
} }
}, [isDraggingFiles, open, toggleAttachedFilesMenu]) }, [isDraggingFiles, isOpen, toggleAttachedFilesMenu])
const filesDragInCallback = useCallback((tab: PopoverTabs) => { const filesDragInCallback = useCallback((tab: PopoverTabs) => {
setCurrentTab(tab) setCurrentTab(tab)
@@ -162,53 +129,41 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
return ( return (
<div ref={containerRef}> <div ref={containerRef}>
<Disclosure open={open} onChange={toggleAttachedFilesMenuWithEntitlementCheck}> <button
<DisclosureButton className={classNames(
onKeyDown={(event) => { 'bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast',
if (event.key === 'Escape') { attachedFilesCount > 0 ? 'py-1 px-3' : '',
setOpen(false) )}
} title="Attached files"
}} aria-label="Attached files"
ref={buttonRef} onClick={toggleAttachedFilesMenuWithEntitlementCheck}
className={`bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast ${ ref={buttonRef}
attachedFilesCount > 0 ? 'py-1 px-3' : '' onKeyDown={(event) => {
}`} if (event.key === 'Escape') {
onBlur={closeOnBlur} setIsOpen(false)
> }
<VisuallyHidden>Attached files</VisuallyHidden> }}
<Icon type="attachment-file" className="block" /> >
{attachedFilesCount > 0 && <span className="ml-2 text-sm">{attachedFilesCount}</span>} <Icon type="attachment-file" />
</DisclosureButton> {attachedFilesCount > 0 && <span className="ml-2 text-sm">{attachedFilesCount}</span>}
<DisclosurePanel </button>
onKeyDown={(event) => { <Popover
if (event.key === 'Escape') { togglePopover={toggleAttachedFilesMenuWithEntitlementCheck}
setOpen(false) anchorElement={buttonRef.current}
buttonRef.current?.focus() open={isOpen}
} className="pt-2 md:pt-0"
}} >
ref={panelRef} <AttachedFilesPopover
style={{ application={application}
...position, filesController={filesController}
maxHeight, attachedFiles={attachedFiles}
}} allFiles={allFiles}
className="slide-down-animation max-h-120 fixed flex min-w-80 max-w-xs flex-col overflow-y-auto rounded bg-default shadow-main transition-transform duration-150" currentTab={currentTab}
onBlur={closeOnBlur} isDraggingFiles={isDraggingFiles}
> setCurrentTab={setCurrentTab}
{open && ( attachedTabDisabled={isAttachedTabDisabled}
<AttachedFilesPopover />
application={application} </Popover>
filesController={filesController}
attachedFiles={attachedFiles}
allFiles={allFiles}
closeOnBlur={closeOnBlur}
currentTab={currentTab}
isDraggingFiles={isDraggingFiles}
setCurrentTab={setCurrentTab}
attachedTabDisabled={isAttachedTabDisabled}
/>
)}
</DisclosurePanel>
</Disclosure>
</div> </div>
) )
} }

View File

@@ -16,7 +16,6 @@ type Props = {
filesController: FilesController filesController: FilesController
allFiles: FileItem[] allFiles: FileItem[]
attachedFiles: FileItem[] attachedFiles: FileItem[]
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
currentTab: PopoverTabs currentTab: PopoverTabs
isDraggingFiles: boolean isDraggingFiles: boolean
setCurrentTab: Dispatch<SetStateAction<PopoverTabs>> setCurrentTab: Dispatch<SetStateAction<PopoverTabs>>
@@ -28,7 +27,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
filesController, filesController,
allFiles, allFiles,
attachedFiles, attachedFiles,
closeOnBlur,
currentTab, currentTab,
isDraggingFiles, isDraggingFiles,
setCurrentTab, setCurrentTab,
@@ -87,7 +85,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
onClick={() => { onClick={() => {
setCurrentTab(PopoverTabs.AttachedFiles) setCurrentTab(PopoverTabs.AttachedFiles)
}} }}
onBlur={closeOnBlur}
disabled={attachedTabDisabled} disabled={attachedTabDisabled}
> >
Attached Attached
@@ -100,7 +97,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
onClick={() => { onClick={() => {
setCurrentTab(PopoverTabs.AllFiles) setCurrentTab(PopoverTabs.AllFiles)
}} }}
onBlur={closeOnBlur}
> >
All files All files
</button> </button>
@@ -117,7 +113,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
onInput={(e) => { onInput={(e) => {
setSearchQuery((e.target as HTMLInputElement).value) setSearchQuery((e.target as HTMLInputElement).value)
}} }}
onBlur={closeOnBlur}
ref={searchInputRef} ref={searchInputRef}
/> />
{searchQuery.length > 0 && ( {searchQuery.length > 0 && (
@@ -127,7 +122,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
setSearchQuery('') setSearchQuery('')
searchInputRef.current?.focus() searchInputRef.current?.focus()
}} }}
onBlur={closeOnBlur}
> >
<Icon type="clear-circle-filled" className="text-neutral" /> <Icon type="clear-circle-filled" className="text-neutral" />
</button> </button>
@@ -144,7 +138,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
isAttachedToNote={attachedFiles.includes(file)} isAttachedToNote={attachedFiles.includes(file)}
handleFileAction={filesController.handleFileAction} handleFileAction={filesController.handleFileAction}
getIconType={application.iconsController.getIconForFileType} getIconType={application.iconsController.getIconForFileType}
closeOnBlur={closeOnBlur}
previewHandler={previewHandler} previewHandler={previewHandler}
/> />
) )
@@ -161,7 +154,7 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
? 'No files attached to this note' ? 'No files attached to this note'
: 'No files found in this account'} : 'No files found in this account'}
</div> </div>
<Button onClick={handleAttachFilesClick} onBlur={closeOnBlur}> <Button onClick={handleAttachFilesClick}>
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files {currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files
</Button> </Button>
<div className="mt-3 text-xs text-passive-0">Or drop your files here</div> <div className="mt-3 text-xs text-passive-0">Or drop your files here</div>
@@ -172,7 +165,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
<button <button
className="flex w-full cursor-pointer items-center border-0 border-t border-solid border-border bg-transparent px-3 py-3 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 border-t border-solid border-border bg-transparent px-3 py-3 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={handleAttachFilesClick} onClick={handleAttachFilesClick}
onBlur={closeOnBlur}
> >
<Icon type="add" className="mr-2 text-neutral" /> <Icon type="add" className="mr-2 text-neutral" />
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files {currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files

View File

@@ -22,7 +22,6 @@ const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
isAttachedToNote, isAttachedToNote,
handleFileAction, handleFileAction,
getIconType, getIconType,
closeOnBlur,
previewHandler, previewHandler,
}) => { }) => {
const [fileName, setFileName] = useState(file.name) const [fileName, setFileName] = useState(file.name)
@@ -116,7 +115,6 @@ const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
isAttachedToNote={isAttachedToNote} isAttachedToNote={isAttachedToNote}
handleFileAction={handleFileAction} handleFileAction={handleFileAction}
setIsRenamingFile={setIsRenamingFile} setIsRenamingFile={setIsRenamingFile}
closeOnBlur={closeOnBlur}
previewHandler={previewHandler} previewHandler={previewHandler}
/> />
</div> </div>

View File

@@ -8,7 +8,6 @@ type CommonProps = {
handleFileAction: (action: PopoverFileItemAction) => Promise<{ handleFileAction: (action: PopoverFileItemAction) => Promise<{
didHandleAction: boolean didHandleAction: boolean
}> }>
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
previewHandler: (file: FileItem) => void previewHandler: (file: FileItem) => void
} }

View File

@@ -1,14 +1,12 @@
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle' import { FunctionComponent, useCallback, useRef, useState } from 'react'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import Switch from '@/Components/Switch/Switch' import Switch from '@/Components/Switch/Switch'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { PopoverFileSubmenuProps } from './PopoverFileItemProps' import { PopoverFileSubmenuProps } from './PopoverFileItemProps'
import { PopoverFileItemActionType } from './PopoverFileItemAction' import { PopoverFileItemActionType } from './PopoverFileItemAction'
import HorizontalSeparator from '../Shared/HorizontalSeparator' import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { formatSizeToReadableString } from '@standardnotes/filepicker' import { formatSizeToReadableString } from '@standardnotes/filepicker'
import Popover from '../Popover/Popover'
const PopoverFileSubmenu: FunctionComponent<PopoverFileSubmenuProps> = ({ const PopoverFileSubmenu: FunctionComponent<PopoverFileSubmenuProps> = ({
file, file,
@@ -19,187 +17,135 @@ const PopoverFileSubmenu: FunctionComponent<PopoverFileSubmenuProps> = ({
}) => { }) => {
const menuContainerRef = useRef<HTMLDivElement>(null) const menuContainerRef = useRef<HTMLDivElement>(null)
const menuButtonRef = useRef<HTMLButtonElement>(null) const menuButtonRef = useRef<HTMLButtonElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
const [isMenuOpen, setIsMenuOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [isFileProtected, setIsFileProtected] = useState(file.protected) const [isFileProtected, setIsFileProtected] = useState(file.protected)
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
right: 0,
bottom: 0,
maxHeight: 'auto',
})
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen)
const closeMenu = useCallback(() => { const closeMenu = useCallback(() => {
setIsMenuOpen(false) setIsOpen(false)
}, []) }, [])
const toggleMenu = useCallback(() => { const toggleMenu = useCallback(() => {
if (!isMenuOpen) { setIsOpen((isOpen) => !isOpen)
const menuPosition = calculateSubmenuStyle(menuButtonRef.current)
if (menuPosition) {
setMenuStyle(menuPosition)
}
}
setIsMenuOpen(!isMenuOpen)
}, [isMenuOpen])
const recalculateMenuStyle = useCallback(() => {
const newMenuPosition = calculateSubmenuStyle(menuButtonRef.current, menuRef.current)
if (newMenuPosition) {
setMenuStyle(newMenuPosition)
}
}, []) }, [])
useEffect(() => {
if (isMenuOpen) {
setTimeout(() => {
recalculateMenuStyle()
})
}
}, [isMenuOpen, recalculateMenuStyle])
return ( return (
<div ref={menuContainerRef}> <div ref={menuContainerRef}>
<Disclosure open={isMenuOpen} onChange={toggleMenu}> <button
<DisclosureButton ref={menuButtonRef}
ref={menuButtonRef} onClick={toggleMenu}
onBlur={closeOnBlur} className="h-7 w-7 cursor-pointer rounded-full border-0 bg-transparent p-1 hover:bg-contrast"
className="h-7 w-7 cursor-pointer rounded-full border-0 bg-transparent p-1 hover:bg-contrast" >
> <Icon type="more" className="text-neutral" />
<Icon type="more" className="text-neutral" /> </button>
</DisclosureButton> <Popover anchorElement={menuButtonRef.current} open={isOpen} togglePopover={toggleMenu} className="py-2">
<DisclosurePanel <button
ref={menuRef} className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
style={{ onClick={() => {
...menuStyle, previewHandler(file)
position: 'fixed', closeMenu()
}} }}
className={`${
isMenuOpen ? 'flex' : 'hidden'
} max-h-120 fixed min-w-60 flex-col overflow-y-auto rounded bg-default py-1 shadow-main`}
> >
{isMenuOpen && ( <Icon type="file" className="mr-2 text-neutral" />
<> Preview file
<button </button>
onBlur={closeOnBlur} {isAttachedToNote ? (
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" <button
onClick={() => { className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
previewHandler(file) onClick={() => {
closeMenu() handleFileAction({
}} type: PopoverFileItemActionType.DetachFileToNote,
> payload: { file },
<Icon type="file" className="mr-2 text-neutral" /> }).catch(console.error)
Preview file closeMenu()
</button> }}
{isAttachedToNote ? ( >
<button <Icon type="link-off" className="mr-2 text-neutral" />
onBlur={closeOnBlur} Detach from note
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" </button>
onClick={() => { ) : (
handleFileAction({ <button
type: PopoverFileItemActionType.DetachFileToNote, className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
payload: { file }, onClick={() => {
}).catch(console.error) handleFileAction({
closeMenu() type: PopoverFileItemActionType.AttachFileToNote,
}} payload: { file },
> }).catch(console.error)
<Icon type="link-off" className="mr-2 text-neutral" /> closeMenu()
Detach from note }}
</button> >
) : ( <Icon type="link" className="mr-2 text-neutral" />
<button Attach to note
onBlur={closeOnBlur} </button>
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" )}
onClick={() => { <HorizontalSeparator classes="my-1" />
handleFileAction({ <button
type: PopoverFileItemActionType.AttachFileToNote, className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
payload: { file }, onClick={() => {
}).catch(console.error) handleFileAction({
closeMenu() type: PopoverFileItemActionType.ToggleFileProtection,
}} payload: { file },
> callback: (isProtected: boolean) => {
<Icon type="link" className="mr-2 text-neutral" /> setIsFileProtected(isProtected)
Attach to note },
</button> }).catch(console.error)
)} }}
<HorizontalSeparator classes="my-1" /> >
<button <span className="flex items-center">
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" <Icon type="password" className="mr-2 text-neutral" />
onClick={() => { Password protection
handleFileAction({ </span>
type: PopoverFileItemActionType.ToggleFileProtection, <Switch
payload: { file }, className="pointer-events-none px-0"
callback: (isProtected: boolean) => { tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
setIsFileProtected(isProtected) checked={isFileProtected}
}, />
}).catch(console.error) </button>
}} <HorizontalSeparator classes="my-1" />
onBlur={closeOnBlur} <button
> className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
<span className="flex items-center"> onClick={() => {
<Icon type="password" className="mr-2 text-neutral" /> handleFileAction({
Password protection type: PopoverFileItemActionType.DownloadFile,
</span> payload: { file },
<Switch }).catch(console.error)
className="pointer-events-none px-0" closeMenu()
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE} }}
checked={isFileProtected} >
/> <Icon type="download" className="mr-2 text-neutral" />
</button> Download
<HorizontalSeparator classes="my-1" /> </button>
<button <button
onBlur={closeOnBlur} className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" onClick={() => {
onClick={() => { setIsRenamingFile(true)
handleFileAction({ }}
type: PopoverFileItemActionType.DownloadFile, >
payload: { file }, <Icon type="pencil" className="mr-2 text-neutral" />
}).catch(console.error) Rename
closeMenu() </button>
}} <button
> className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
<Icon type="download" className="mr-2 text-neutral" /> onClick={() => {
Download handleFileAction({
</button> type: PopoverFileItemActionType.DeleteFile,
<button payload: { file },
onBlur={closeOnBlur} }).catch(console.error)
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" closeMenu()
onClick={() => { }}
setIsRenamingFile(true) >
}} <Icon type="trash" className="mr-2 text-danger" />
> <span className="text-danger">Delete permanently</span>
<Icon type="pencil" className="mr-2 text-neutral" /> </button>
Rename <div className="px-3 py-1 text-xs font-medium text-neutral">
</button> <div className="mb-1">
<button <span className="font-semibold">File ID:</span> {file.uuid}
onBlur={closeOnBlur} </div>
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" <div>
onClick={() => { <span className="font-semibold">Size:</span> {formatSizeToReadableString(file.decryptedSize)}
handleFileAction({ </div>
type: PopoverFileItemActionType.DeleteFile, </div>
payload: { file }, </Popover>
}).catch(console.error)
closeMenu()
}}
>
<Icon type="trash" className="mr-2 text-danger" />
<span className="text-danger">Delete permanently</span>
</button>
<div className="px-3 py-1 text-xs font-medium text-neutral">
<div className="mb-1">
<span className="font-semibold">File ID:</span> {file.uuid}
</div>
<div>
<span className="font-semibold">Size:</span> {formatSizeToReadableString(file.decryptedSize)}
</div>
</div>
</>
)}
</DisclosurePanel>
</Disclosure>
</div> </div>
) )
} }

View File

@@ -1,11 +1,10 @@
import { ApplicationGroup } from '@/Application/ApplicationGroup' import { ApplicationGroup } from '@/Application/ApplicationGroup'
import { ViewControllerManager } from '@/Services/ViewControllerManager' import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle' import { FunctionComponent, useCallback, useRef, useState } from 'react'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import WorkspaceSwitcherMenu from '@/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu' import WorkspaceSwitcherMenu from '@/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu'
import Button from '@/Components/Button/Button' import Button from '@/Components/Button/Button'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside' import Popover from '../Popover/Popover'
type Props = { type Props = {
mainApplicationGroup: ApplicationGroup mainApplicationGroup: ApplicationGroup
@@ -14,36 +13,12 @@ type Props = {
const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainApplicationGroup, viewControllerManager }) => { const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainApplicationGroup, viewControllerManager }) => {
const buttonRef = useRef<HTMLButtonElement>(null) const buttonRef = useRef<HTMLButtonElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>()
useCloseOnClickOutside(containerRef, () => setIsOpen(false))
const toggleMenu = useCallback(() => { const toggleMenu = useCallback(() => {
if (!isOpen) { setIsOpen((isOpen) => !isOpen)
const menuPosition = calculateSubmenuStyle(buttonRef.current) }, [])
if (menuPosition) {
setMenuStyle(menuPosition)
}
}
setIsOpen(!isOpen)
}, [isOpen])
useEffect(() => {
if (isOpen) {
const timeToWaitBeforeCheckingMenuCollision = 0
setTimeout(() => {
const newMenuPosition = calculateSubmenuStyle(buttonRef.current, menuRef.current)
if (newMenuPosition) {
setMenuStyle(newMenuPosition)
}
}, timeToWaitBeforeCheckingMenuCollision)
}
}, [isOpen])
return ( return (
<div ref={containerRef}> <div ref={containerRef}>
@@ -51,20 +26,22 @@ const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainApplication
<Icon type="user-switch" className="mr-2 text-neutral" /> <Icon type="user-switch" className="mr-2 text-neutral" />
Switch workspace Switch workspace
</Button> </Button>
{isOpen && ( <Popover
<div align="center"
ref={menuRef} anchorElement={buttonRef.current}
className="max-h-120 fixed min-w-68 overflow-y-auto rounded-md bg-default py-2 shadow-main" className="py-2"
style={menuStyle} open={isOpen}
> overrideZIndex="z-modal"
<WorkspaceSwitcherMenu side="right"
mainApplicationGroup={mainApplicationGroup} togglePopover={toggleMenu}
viewControllerManager={viewControllerManager} >
isOpen={isOpen} <WorkspaceSwitcherMenu
hideWorkspaceOptions={true} mainApplicationGroup={mainApplicationGroup}
/> viewControllerManager={viewControllerManager}
</div> isOpen={isOpen}
)} hideWorkspaceOptions={true}
/>
</Popover>
</div> </div>
) )
} }

View File

@@ -1,13 +1,10 @@
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Services/ViewControllerManager' import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import VisuallyHidden from '@reach/visually-hidden'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, useRef, useState } from 'react' import { FunctionComponent, useCallback, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import ChangeEditorMenu from './ChangeEditorMenu' import ChangeEditorMenu from './ChangeEditorMenu'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import Popover from '../Popover/Popover'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -22,89 +19,38 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
}: Props) => { }: Props) => {
const note = viewControllerManager.notesController.firstSelectedNote const note = viewControllerManager.notesController.firstSelectedNote
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [isVisible, setIsVisible] = useState(false)
const [position, setPosition] = useState({
top: 0,
right: 0,
})
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
const buttonRef = useRef<HTMLButtonElement>(null) const buttonRef = useRef<HTMLButtonElement>(null)
const panelRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const [closeOnBlur] = useCloseOnBlur(containerRef, setIsOpen)
const toggleChangeEditorMenu = async () => { const toggleMenu = useCallback(async () => {
const rect = buttonRef.current?.getBoundingClientRect() const willMenuOpen = !isOpen
if (rect) { if (willMenuOpen && onClickPreprocessing) {
const { clientHeight } = document.documentElement await onClickPreprocessing()
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
const footerHeightInPx = footerElementRect?.height
if (footerHeightInPx) {
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - MENU_MARGIN_FROM_APP_BORDER)
}
setPosition({
top: rect.bottom,
right: document.body.clientWidth - rect.right,
})
const newOpenState = !isOpen
if (newOpenState && onClickPreprocessing) {
await onClickPreprocessing()
}
setIsOpen(newOpenState)
setTimeout(() => {
setIsVisible(newOpenState)
})
} }
} setIsOpen(willMenuOpen)
}, [onClickPreprocessing, isOpen])
return ( return (
<div ref={containerRef}> <div ref={containerRef}>
<Disclosure open={isOpen} onChange={toggleChangeEditorMenu}> <button
<DisclosureButton className="bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast"
onKeyDown={(event) => { title="Change note type"
if (event.key === 'Escape') { aria-label="Change note type"
setIsOpen(false) onClick={toggleMenu}
} ref={buttonRef}
>
<Icon type="dashboard" />
</button>
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="pt-2 md:pt-0">
<ChangeEditorMenu
application={application}
isVisible={isOpen}
note={note}
closeMenu={() => {
setIsOpen(false)
}} }}
onBlur={closeOnBlur} />
ref={buttonRef} </Popover>
className="flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast"
>
<VisuallyHidden>Change note type</VisuallyHidden>
<Icon type="dashboard" className="block" />
</DisclosureButton>
<DisclosurePanel
onKeyDown={(event) => {
if (event.key === 'Escape') {
setIsOpen(false)
buttonRef.current?.focus()
}
}}
ref={panelRef}
style={{
...position,
maxHeight,
}}
className="slide-down-animation max-h-120 fixed flex min-w-68 max-w-xs flex-col overflow-y-auto rounded bg-default shadow-main transition-transform duration-150"
onBlur={closeOnBlur}
>
{isOpen && (
<ChangeEditorMenu
closeOnBlur={closeOnBlur}
application={application}
isVisible={isVisible}
note={note}
closeMenu={() => {
setIsOpen(false)
}}
/>
)}
</DisclosurePanel>
</Disclosure>
</div> </div>
) )
} }

View File

@@ -14,7 +14,7 @@ import {
SNNote, SNNote,
TransactionalMutation, TransactionalMutation,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
import { Fragment, FunctionComponent, useCallback, useEffect, useState } from 'react' import { Fragment, FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup' import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem' import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
import { createEditorMenuGroups } from './createEditorMenuGroups' import { createEditorMenuGroups } from './createEditorMenuGroups'
@@ -28,7 +28,6 @@ import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/Premium
type ChangeEditorMenuProps = { type ChangeEditorMenuProps = {
application: WebApplication application: WebApplication
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
closeMenu: () => void closeMenu: () => void
isVisible: boolean isVisible: boolean
note: SNNote | undefined note: SNNote | undefined
@@ -36,25 +35,17 @@ type ChangeEditorMenuProps = {
const getGroupId = (group: EditorMenuGroup) => group.title.toLowerCase().replace(/\s/, '-') const getGroupId = (group: EditorMenuGroup) => group.title.toLowerCase().replace(/\s/, '-')
const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({ application, closeMenu, isVisible, note }) => {
application, const editors = useMemo(
closeOnBlur, () =>
closeMenu, application.componentManager.componentsForArea(ComponentArea.Editor).sort((a, b) => {
isVisible, return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1
note, }),
}) => { [application.componentManager],
const [editors] = useState<SNComponent[]>(() =>
application.componentManager.componentsForArea(ComponentArea.Editor).sort((a, b) => {
return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1
}),
) )
const [groups, setGroups] = useState<EditorMenuGroup[]>([]) const groups = useMemo(() => createEditorMenuGroups(application, editors), [application, editors])
const [currentEditor, setCurrentEditor] = useState<SNComponent>() const [currentEditor, setCurrentEditor] = useState<SNComponent>()
useEffect(() => {
setGroups(createEditorMenuGroups(application, editors))
}, [application, editors])
useEffect(() => { useEffect(() => {
if (note) { if (note) {
setCurrentEditor(application.componentManager.editorForNote(note)) setCurrentEditor(application.componentManager.editorForNote(note))
@@ -195,7 +186,6 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
type={MenuItemType.RadioButton} type={MenuItemType.RadioButton}
onClick={onClickEditorItem} onClick={onClickEditorItem}
className={'flex-row-reverse py-2'} className={'flex-row-reverse py-2'}
onBlur={closeOnBlur}
checked={item.isEntitled ? isSelectedEditor(item) : undefined} checked={item.isEntitled ? isSelectedEditor(item) : undefined}
> >
<div className="flex flex-grow items-center justify-between"> <div className="flex flex-grow items-center justify-between">

View File

@@ -1,10 +1,9 @@
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { Disclosure, DisclosurePanel } from '@reach/disclosure'
import { memo, useCallback, useRef, useState } from 'react' import { memo, useCallback, useRef, useState } from 'react'
import Icon from '../../Icon/Icon' import Icon from '../../Icon/Icon'
import { DisplayOptionsMenuPositionProps } from './DisplayOptionsMenuProps' import { classNames } from '@/Utils/ConcatenateClassNames'
import DisplayOptionsMenuPortal from './DisplayOptionsMenuPortal' import Popover from '@/Components/Popover/Popover'
import StyledDisplayOptionsButton from './StyledDisplayOptionsButton' import DisplayOptionsMenu from './DisplayOptionsMenu'
type Props = { type Props = {
application: { application: {
@@ -26,21 +25,12 @@ const ContentListHeader = ({
isFilesSmartView, isFilesSmartView,
optionsSubtitle, optionsSubtitle,
}: Props) => { }: Props) => {
const [displayOptionsMenuPosition, setDisplayOptionsMenuPosition] = useState<DisplayOptionsMenuPositionProps>()
const displayOptionsContainerRef = useRef<HTMLDivElement>(null) const displayOptionsContainerRef = useRef<HTMLDivElement>(null)
const displayOptionsButtonRef = useRef<HTMLButtonElement>(null) const displayOptionsButtonRef = useRef<HTMLButtonElement>(null)
const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false) const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false)
const toggleDisplayOptionsMenu = useCallback(() => { const toggleDisplayOptionsMenu = useCallback(() => {
if (displayOptionsButtonRef.current) {
const buttonBoundingRect = displayOptionsButtonRef.current.getBoundingClientRect()
setDisplayOptionsMenuPosition({
top: buttonBoundingRect.bottom,
left: buttonBoundingRect.right - buttonBoundingRect.width,
})
}
setShowDisplayOptionsMenu((show) => !show) setShowDisplayOptionsMenu((show) => !show)
}, []) }, [])
@@ -52,24 +42,30 @@ const ContentListHeader = ({
</div> </div>
<div className="flex"> <div className="flex">
<div className="relative" ref={displayOptionsContainerRef}> <div className="relative" ref={displayOptionsContainerRef}>
<Disclosure open={showDisplayOptionsMenu} onChange={toggleDisplayOptionsMenu}> <button
<StyledDisplayOptionsButton $pressed={showDisplayOptionsMenu} ref={displayOptionsButtonRef}> className={classNames(
<Icon type="sort-descending" /> 'bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast',
</StyledDisplayOptionsButton> showDisplayOptionsMenu && 'bg-contrast',
<DisclosurePanel> )}
{showDisplayOptionsMenu && displayOptionsMenuPosition && ( onClick={toggleDisplayOptionsMenu}
<DisplayOptionsMenuPortal ref={displayOptionsButtonRef}
application={application} >
closeDisplayOptionsMenu={toggleDisplayOptionsMenu} <Icon type="sort-descending" />
containerRef={displayOptionsContainerRef} </button>
isOpen={showDisplayOptionsMenu} <Popover
isFilesSmartView={isFilesSmartView} open={showDisplayOptionsMenu}
top={displayOptionsMenuPosition.top} anchorElement={displayOptionsButtonRef.current}
left={displayOptionsMenuPosition.left} togglePopover={toggleDisplayOptionsMenu}
/> align="start"
)} className="py-2"
</DisclosurePanel> >
</Disclosure> <DisplayOptionsMenu
application={application}
closeDisplayOptionsMenu={toggleDisplayOptionsMenu}
isFilesSmartView={isFilesSmartView}
isOpen={showDisplayOptionsMenu}
/>
</Popover>
</div> </div>
<button <button
className="ml-3 flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-transparent bg-info text-info-contrast hover:brightness-125" className="ml-3 flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-transparent bg-info text-info-contrast hover:brightness-125"

View File

@@ -97,14 +97,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
}, [application, hideEditorIcon]) }, [application, hideEditorIcon])
return ( return (
<Menu <Menu className="text-sm" a11yLabel="Notes list options menu" closeMenu={closeDisplayOptionsMenu} isOpen={isOpen}>
className={
'slide-down-animation z-index-dropdown-menu flex min-w-70 flex-col overflow-y-auto rounded border border-solid border-border bg-default py-1 text-sm shadow-main transition-transform duration-150'
}
a11yLabel="Notes list options menu"
closeMenu={closeDisplayOptionsMenu}
isOpen={isOpen}
>
<div className="my-1 px-3 text-xs font-semibold uppercase text-text">Sort by</div> <div className="my-1 px-3 text-xs font-semibold uppercase text-text">Sort by</div>
<MenuItem <MenuItem
className="py-2" className="py-2"

View File

@@ -1,63 +0,0 @@
import { createPortal } from 'react-dom'
import styled from 'styled-components'
import { DisplayOptionsMenuPositionProps, DisplayOptionsMenuProps } from './DisplayOptionsMenuProps'
import DisplayOptionsMenu from './DisplayOptionsMenu'
import { useRef, useEffect, RefObject } from 'react'
type Props = DisplayOptionsMenuProps &
DisplayOptionsMenuPositionProps & {
containerRef: RefObject<HTMLDivElement>
}
const PositionedOptionsMenu = styled.div<DisplayOptionsMenuPositionProps>`
position: absolute;
top: ${(props) => props.top}px;
left: ${(props) => props.left}px;
z-index: var(--z-index-dropdown-menu);
`
const DisplayOptionsMenuPortal = ({
application,
closeDisplayOptionsMenu,
containerRef,
isFilesSmartView,
isOpen,
top,
left,
}: Props) => {
const menuRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const closeIfClickedOutside = (event: MouseEvent) => {
const isDescendantOfMenu = menuRef.current?.contains(event.target as Node)
const isDescendantOfContainer = containerRef.current?.contains(event.target as Node)
if (!isDescendantOfMenu && !isDescendantOfContainer) {
closeDisplayOptionsMenu()
}
}
document.addEventListener('click', closeIfClickedOutside, { capture: true })
return () => {
document.removeEventListener('click', closeIfClickedOutside, {
capture: true,
})
}
}, [closeDisplayOptionsMenu, containerRef])
return createPortal(
<PositionedOptionsMenu top={top} left={left} ref={menuRef}>
<div className="sn-component">
<DisplayOptionsMenu
application={application}
closeDisplayOptionsMenu={closeDisplayOptionsMenu}
isFilesSmartView={isFilesSmartView}
isOpen={isOpen}
/>
</div>
</PositionedOptionsMenu>,
document.body,
)
}
export default DisplayOptionsMenuPortal

View File

@@ -1,10 +1,8 @@
import { MAX_MENU_SIZE_MULTIPLIER, MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants'
import { FilesController } from '@/Controllers/FilesController' import { FilesController } from '@/Controllers/FilesController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController' import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' import { FunctionComponent, useRef } from 'react'
import Popover from '../Popover/Popover'
import FileMenuOptions from './FileMenuOptions' import FileMenuOptions from './FileMenuOptions'
type Props = { type Props = {
@@ -15,92 +13,27 @@ type Props = {
const FileContextMenu: FunctionComponent<Props> = observer(({ filesController, selectionController }) => { const FileContextMenu: FunctionComponent<Props> = observer(({ filesController, selectionController }) => {
const { showFileContextMenu, setShowFileContextMenu, fileContextMenuLocation } = filesController const { showFileContextMenu, setShowFileContextMenu, fileContextMenuLocation } = filesController
const [contextMenuStyle, setContextMenuStyle] = useState<React.CSSProperties>({
top: 0,
left: 0,
visibility: 'hidden',
})
const [contextMenuMaxHeight, setContextMenuMaxHeight] = useState<number | 'auto'>('auto')
const contextMenuRef = useRef<HTMLDivElement>(null) const contextMenuRef = useRef<HTMLDivElement>(null)
const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) => setShowFileContextMenu(open))
useCloseOnClickOutside(contextMenuRef, () => filesController.setShowFileContextMenu(false))
const reloadContextMenuLayout = useCallback(() => {
const { clientHeight } = document.documentElement
const defaultFontSize = window.getComputedStyle(document.documentElement).fontSize
const maxContextMenuHeight = parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
const footerHeightInPx = footerElementRect?.height
let openUpBottom = true
if (footerHeightInPx) {
const bottomSpace = clientHeight - footerHeightInPx - fileContextMenuLocation.y
const upSpace = fileContextMenuLocation.y
if (maxContextMenuHeight > bottomSpace) {
if (upSpace > maxContextMenuHeight) {
openUpBottom = false
setContextMenuMaxHeight('auto')
} else {
if (upSpace > bottomSpace) {
setContextMenuMaxHeight(upSpace - MENU_MARGIN_FROM_APP_BORDER)
openUpBottom = false
} else {
setContextMenuMaxHeight(bottomSpace - MENU_MARGIN_FROM_APP_BORDER)
}
}
} else {
setContextMenuMaxHeight('auto')
}
}
if (openUpBottom) {
setContextMenuStyle({
top: fileContextMenuLocation.y,
left: fileContextMenuLocation.x,
visibility: 'visible',
})
} else {
setContextMenuStyle({
bottom: clientHeight - fileContextMenuLocation.y,
left: fileContextMenuLocation.x,
visibility: 'visible',
})
}
}, [fileContextMenuLocation.x, fileContextMenuLocation.y])
useEffect(() => {
if (showFileContextMenu) {
reloadContextMenuLayout()
}
}, [reloadContextMenuLayout, showFileContextMenu])
useEffect(() => {
window.addEventListener('resize', reloadContextMenuLayout)
return () => {
window.removeEventListener('resize', reloadContextMenuLayout)
}
}, [reloadContextMenuLayout])
return ( return (
<div <Popover
ref={contextMenuRef} open={showFileContextMenu}
className="max-h-120 fixed z-dropdown-menu flex min-w-60 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main" anchorPoint={fileContextMenuLocation}
style={{ togglePopover={() => setShowFileContextMenu(!showFileContextMenu)}
...contextMenuStyle, side="right"
maxHeight: contextMenuMaxHeight, align="start"
}} className="py-2"
> >
<FileMenuOptions <div ref={contextMenuRef}>
filesController={filesController} <FileMenuOptions
selectionController={selectionController} filesController={filesController}
closeOnBlur={closeOnBlur} selectionController={selectionController}
closeMenu={() => setShowFileContextMenu(false)} closeMenu={() => setShowFileContextMenu(false)}
shouldShowRenameOption={false} shouldShowRenameOption={false}
shouldShowAttachOption={false} shouldShowAttachOption={false}
/> />
</div> </div>
</Popover>
) )
}) })

View File

@@ -11,7 +11,6 @@ import { formatSizeToReadableString } from '@standardnotes/filepicker'
type Props = { type Props = {
closeMenu: () => void closeMenu: () => void
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
filesController: FilesController filesController: FilesController
selectionController: SelectedItemsController selectionController: SelectedItemsController
isFileAttachedToNote?: boolean isFileAttachedToNote?: boolean
@@ -22,7 +21,6 @@ type Props = {
const FileMenuOptions: FunctionComponent<Props> = ({ const FileMenuOptions: FunctionComponent<Props> = ({
closeMenu, closeMenu,
closeOnBlur,
filesController, filesController,
selectionController, selectionController,
isFileAttachedToNote, isFileAttachedToNote,
@@ -73,7 +71,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
return ( return (
<> <>
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={onPreview} onClick={onPreview}
> >
@@ -84,7 +81,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
<> <>
{isFileAttachedToNote ? ( {isFileAttachedToNote ? (
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={onDetach} onClick={onDetach}
> >
@@ -93,7 +89,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
</button> </button>
) : shouldShowAttachOption ? ( ) : shouldShowAttachOption ? (
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={onAttach} onClick={onAttach}
> >
@@ -109,7 +104,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
onClick={() => { onClick={() => {
void filesController.setProtectionForFiles(!hasProtectedFiles, selectionController.selectedFiles) void filesController.setProtectionForFiles(!hasProtectedFiles, selectionController.selectedFiles)
}} }}
onBlur={closeOnBlur}
> >
<span className="flex items-center"> <span className="flex items-center">
<Icon type="password" className="mr-2 text-neutral" /> <Icon type="password" className="mr-2 text-neutral" />
@@ -123,7 +117,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
</button> </button>
<HorizontalSeparator classes="my-1" /> <HorizontalSeparator classes="my-1" />
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => { onClick={() => {
void filesController.downloadFiles(selectionController.selectedFiles) void filesController.downloadFiles(selectionController.selectedFiles)
@@ -134,7 +127,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
</button> </button>
{shouldShowRenameOption && ( {shouldShowRenameOption && (
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => { onClick={() => {
renameToggleCallback?.(true) renameToggleCallback?.(true)
@@ -145,7 +137,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
</button> </button>
)} )}
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => { onClick={() => {
void filesController.deleteFilesPermanently(selectionController.selectedFiles) void filesController.deleteFilesPermanently(selectionController.selectedFiles)

View File

@@ -1,13 +1,10 @@
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import VisuallyHidden from '@reach/visually-hidden'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { useCallback, useRef, useState } from 'react' import { useCallback, useRef, useState } from 'react'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import FileMenuOptions from './FileMenuOptions' import FileMenuOptions from './FileMenuOptions'
import { FilesController } from '@/Controllers/FilesController' import { FilesController } from '@/Controllers/FilesController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController' import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import Popover from '../Popover/Popover'
type Props = { type Props = {
filesController: FilesController filesController: FilesController
@@ -15,80 +12,34 @@ type Props = {
} }
const FilesOptionsPanel = ({ filesController, selectionController }: Props) => { const FilesOptionsPanel = ({ filesController, selectionController }: Props) => {
const [open, setOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [position, setPosition] = useState({
top: 0,
right: 0,
})
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
const buttonRef = useRef<HTMLButtonElement>(null) const buttonRef = useRef<HTMLButtonElement>(null)
const panelRef = useRef<HTMLDivElement>(null)
const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen)
const onDisclosureChange = useCallback(async () => { const toggleMenu = useCallback(() => setIsOpen((isOpen) => !isOpen), [])
const rect = buttonRef.current?.getBoundingClientRect()
if (rect) {
const { clientHeight } = document.documentElement
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
const footerHeightInPx = footerElementRect?.height
if (footerHeightInPx) {
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - 2)
}
setPosition({
top: rect.bottom,
right: document.body.clientWidth - rect.right,
})
setOpen((open) => !open)
}
}, [])
return ( return (
<Disclosure open={open} onChange={onDisclosureChange}> <>
<DisclosureButton <button
onKeyDown={(event) => {
if (event.key === 'Escape') {
setOpen(false)
}
}}
onBlur={closeOnBlur}
ref={buttonRef}
className="bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast" className="bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast"
title="File options menu"
aria-label="File options menu"
onClick={toggleMenu}
ref={buttonRef}
> >
<VisuallyHidden>Actions</VisuallyHidden> <Icon type="more" />
<Icon type="more" className="block" /> </button>
</DisclosureButton> <Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="py-2">
<DisclosurePanel <FileMenuOptions
onKeyDown={(event) => { filesController={filesController}
if (event.key === 'Escape') { selectionController={selectionController}
setOpen(false) closeMenu={() => {
buttonRef.current?.focus() setIsOpen(false)
} }}
}} shouldShowAttachOption={false}
ref={panelRef} shouldShowRenameOption={false}
style={{ />
...position, </Popover>
maxHeight, </>
}}
className={`${
open ? 'flex' : 'hidden'
} max-h-120 slide-down-animation fixed min-w-80 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main transition-transform duration-150`}
onBlur={closeOnBlur}
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
>
{open && (
<FileMenuOptions
filesController={filesController}
selectionController={selectionController}
closeOnBlur={closeOnBlur}
closeMenu={() => {
setOpen(false)
}}
shouldShowAttachOption={false}
shouldShowRenameOption={false}
/>
)}
</DisclosurePanel>
</Disclosure>
) )
} }

View File

@@ -0,0 +1,52 @@
import { classNames } from '@/Utils/ConcatenateClassNames'
import { useRef } from 'react'
import AccountMenu, { AccountMenuProps } from '../AccountMenu/AccountMenu'
import Icon from '../Icon/Icon'
import Popover from '../Popover/Popover'
type Props = AccountMenuProps & {
isOpen: boolean
hasError: boolean
toggleMenu: () => void
user: unknown
}
const AccountMenuButton = ({
application,
hasError,
isOpen,
mainApplicationGroup,
onClickOutside,
toggleMenu,
user,
viewControllerManager,
}: Props) => {
const buttonRef = useRef<HTMLButtonElement>(null)
return (
<>
<button
ref={buttonRef}
onClick={toggleMenu}
className={classNames(
isOpen ? 'bg-border' : '',
'flex h-full w-8 cursor-pointer items-center justify-center rounded-full',
)}
>
<div className={hasError ? 'text-danger' : (user ? 'text-info' : 'text-neutral') + ' h-5 w-5'}>
<Icon type="account-circle" className="max-h-5 hover:text-info" />
</div>
</button>
<Popover anchorElement={buttonRef.current} open={isOpen} togglePopover={toggleMenu} side="top" className="py-2">
<AccountMenu
onClickOutside={onClickOutside}
viewControllerManager={viewControllerManager}
application={application}
mainApplicationGroup={mainApplicationGroup}
/>
</Popover>
</>
)
}
export default AccountMenuButton

View File

@@ -12,13 +12,13 @@ import {
STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON, STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON,
} from '@/Constants/Strings' } from '@/Constants/Strings'
import { alertDialog, confirmDialog } from '@/Services/AlertService' import { alertDialog, confirmDialog } from '@/Services/AlertService'
import AccountMenu from '@/Components/AccountMenu/AccountMenu'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import QuickSettingsMenu from '@/Components/QuickSettingsMenu/QuickSettingsMenu'
import SyncResolutionMenu from '@/Components/SyncResolutionMenu/SyncResolutionMenu' import SyncResolutionMenu from '@/Components/SyncResolutionMenu/SyncResolutionMenu'
import { Fragment } from 'react' import { Fragment } from 'react'
import { AccountMenuPane } from '../AccountMenu/AccountMenuPane' import { AccountMenuPane } from '../AccountMenu/AccountMenuPane'
import { EditorEventSource } from '@/Types/EditorEventSource' import { EditorEventSource } from '@/Types/EditorEventSource'
import QuickSettingsButton from './QuickSettingsButton'
import AccountMenuButton from './AccountMenuButton'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -287,12 +287,10 @@ class Footer extends PureComponent<Props, State> {
} }
accountMenuClickHandler = () => { accountMenuClickHandler = () => {
this.viewControllerManager.quickSettingsMenuController.closeQuickSettingsMenu()
this.viewControllerManager.accountMenuController.toggleShow() this.viewControllerManager.accountMenuController.toggleShow()
} }
quickSettingsClickHandler = () => { quickSettingsClickHandler = () => {
this.viewControllerManager.accountMenuController.closeAccountMenu()
this.viewControllerManager.quickSettingsMenuController.toggle() this.viewControllerManager.quickSettingsMenuController.toggle()
} }
@@ -342,55 +340,31 @@ class Footer extends PureComponent<Props, State> {
override render() { override render() {
return ( return (
<div className="sn-component"> <div className="sn-component">
<div <footer
id="footer-bar" id="footer-bar"
className="z-footer-bar flex h-6 w-full select-none items-center justify-between border-t border-border bg-contrast px-3 text-text" className="z-footer-bar flex h-6 w-full select-none items-center justify-between border-t border-border bg-contrast px-3 text-text"
> >
<div className="left flex h-full"> <div className="left flex h-full">
<div className="sk-app-bar-item relative z-footer-bar-item ml-0 select-none"> <div className="sk-app-bar-item relative z-footer-bar-item ml-0 select-none">
<div <AccountMenuButton
onClick={this.accountMenuClickHandler} application={this.application}
className={ hasError={this.state.hasError}
(this.state.showAccountMenu ? 'bg-border' : '') + isOpen={this.state.showAccountMenu}
' flex h-full w-8 cursor-pointer items-center justify-center rounded-full' mainApplicationGroup={this.props.applicationGroup}
} onClickOutside={this.clickOutsideAccountMenu}
> toggleMenu={this.accountMenuClickHandler}
<div user={this.user}
className={ viewControllerManager={this.viewControllerManager}
this.state.hasError ? 'text-danger' : (this.user ? 'text-info' : 'text-neutral') + ' h-5 w-5' />
}
>
<Icon type="account-circle" className="max-h-5 hover:text-info" />
</div>
</div>
{this.state.showAccountMenu && (
<AccountMenu
onClickOutside={this.clickOutsideAccountMenu}
viewControllerManager={this.viewControllerManager}
application={this.application}
mainApplicationGroup={this.props.applicationGroup}
/>
)}
</div> </div>
<div className="sk-app-bar-item ml-0-important relative z-footer-bar-item select-none"> <div className="relative z-footer-bar-item select-none">
<div <QuickSettingsButton
onClick={this.quickSettingsClickHandler} isOpen={this.state.showQuickSettingsMenu}
className="flex h-full w-8 cursor-pointer items-center justify-center" toggleMenu={this.quickSettingsClickHandler}
> application={this.application}
<div className="h-5"> preferencesController={this.viewControllerManager.preferencesController}
<Icon quickSettingsMenuController={this.viewControllerManager.quickSettingsMenuController}
type="tune" />
className={(this.state.showQuickSettingsMenu ? 'text-info' : '') + ' rounded hover:text-info'}
/>
</div>
</div>
{this.state.showQuickSettingsMenu && (
<QuickSettingsMenu
onClickOutside={this.clickOutsideQuickSettingsMenu}
viewControllerManager={this.viewControllerManager}
application={this.application}
/>
)}
</div> </div>
{this.state.showBetaWarning && ( {this.state.showBetaWarning && (
<Fragment> <Fragment>
@@ -454,7 +428,7 @@ class Footer extends PureComponent<Props, State> {
</div> </div>
)} )}
</div> </div>
</div> </footer>
</div> </div>
) )
} }

View File

@@ -0,0 +1,55 @@
import { WebApplication } from '@/Application/Application'
import { PreferencesController } from '@/Controllers/PreferencesController'
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
import { useRef } from 'react'
import Icon from '../Icon/Icon'
import Popover from '../Popover/Popover'
import QuickSettingsMenu from '../QuickSettingsMenu/QuickSettingsMenu'
type Props = {
isOpen: boolean
toggleMenu: () => void
application: WebApplication
preferencesController: PreferencesController
quickSettingsMenuController: QuickSettingsController
}
const QuickSettingsButton = ({
application,
isOpen,
toggleMenu,
preferencesController,
quickSettingsMenuController,
}: Props) => {
const buttonRef = useRef<HTMLButtonElement>(null)
return (
<>
<button
onClick={toggleMenu}
className="flex h-full w-8 cursor-pointer items-center justify-center"
ref={buttonRef}
>
<div className="h-5">
<Icon type="tune" className={(isOpen ? 'text-info' : '') + ' rounded hover:text-info'} />
</div>
</button>
<Popover
togglePopover={toggleMenu}
anchorElement={buttonRef.current}
open={isOpen}
side="top"
align="start"
className="py-2"
>
<QuickSettingsMenu
preferencesController={preferencesController}
quickSettingsMenuController={quickSettingsMenuController}
application={application}
/>
</Popover>
</>
)
}
export default QuickSettingsButton

View File

@@ -1,13 +1,12 @@
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import NotesOptions from '@/Components/NotesOptions/NotesOptions' import NotesOptions from '@/Components/NotesOptions/NotesOptions'
import { useCallback, useEffect, useRef } from 'react' import { useRef } from 'react'
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { NotesController } from '@/Controllers/NotesController' import { NotesController } from '@/Controllers/NotesController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController' import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { NoteTagsController } from '@/Controllers/NoteTagsController' import { NoteTagsController } from '@/Controllers/NoteTagsController'
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController' import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
import Popover from '../Popover/Popover'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -24,43 +23,33 @@ const NotesContextMenu = ({
noteTagsController, noteTagsController,
historyModalController, historyModalController,
}: Props) => { }: Props) => {
const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = notesController const { contextMenuOpen, contextMenuClickLocation } = notesController
const contextMenuRef = useRef<HTMLDivElement>(null) const contextMenuRef = useRef<HTMLDivElement>(null)
const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) => notesController.setContextMenuOpen(open))
useCloseOnClickOutside(contextMenuRef, () => notesController.setContextMenuOpen(false)) return (
<Popover
const reloadContextMenuLayout = useCallback(() => { align="start"
notesController.reloadContextMenuLayout() anchorPoint={{
}, [notesController]) x: contextMenuClickLocation.x,
y: contextMenuClickLocation.y,
useEffect(() => {
window.addEventListener('resize', reloadContextMenuLayout)
return () => {
window.removeEventListener('resize', reloadContextMenuLayout)
}
}, [reloadContextMenuLayout])
return contextMenuOpen ? (
<div
ref={contextMenuRef}
className="max-h-120 fixed z-dropdown-menu flex min-w-80 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main"
style={{
...contextMenuPosition,
maxHeight: contextMenuMaxHeight,
}} }}
className="py-2"
open={contextMenuOpen}
side="right"
togglePopover={() => notesController.setContextMenuOpen(!contextMenuOpen)}
> >
<NotesOptions <div ref={contextMenuRef}>
application={application} <NotesOptions
closeOnBlur={closeOnBlur} application={application}
navigationController={navigationController} navigationController={navigationController}
notesController={notesController} notesController={notesController}
noteTagsController={noteTagsController} noteTagsController={noteTagsController}
historyModalController={historyModalController} historyModalController={historyModalController}
/> />
</div> </div>
) : null </Popover>
)
} }
export default observer(NotesContextMenu) export default observer(NotesContextMenu)

View File

@@ -1,12 +1,11 @@
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' import { FunctionComponent, useCallback, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { NavigationController } from '@/Controllers/Navigation/NavigationController' import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { NotesController } from '@/Controllers/NotesController' import { NotesController } from '@/Controllers/NotesController'
import { NoteTagsController } from '@/Controllers/NoteTagsController' import { NoteTagsController } from '@/Controllers/NoteTagsController'
import { KeyboardKey } from '@/Services/IOService'
import Popover from '../Popover/Popover'
type Props = { type Props = {
navigationController: NavigationController navigationController: NavigationController
@@ -16,101 +15,59 @@ type Props = {
const AddTagOption: FunctionComponent<Props> = ({ navigationController, notesController, noteTagsController }) => { const AddTagOption: FunctionComponent<Props> = ({ navigationController, notesController, noteTagsController }) => {
const menuContainerRef = useRef<HTMLDivElement>(null) const menuContainerRef = useRef<HTMLDivElement>(null)
const menuRef = useRef<HTMLDivElement>(null) const buttonRef = useRef<HTMLButtonElement>(null)
const menuButtonRef = useRef<HTMLButtonElement>(null)
const [isMenuOpen, setIsMenuOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
right: 0,
bottom: 0,
maxHeight: 'auto',
})
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen) const toggleMenu = useCallback(() => {
setIsOpen((isOpen) => !isOpen)
const toggleTagsMenu = useCallback(() => {
if (!isMenuOpen) {
const menuPosition = calculateSubmenuStyle(menuButtonRef.current)
if (menuPosition) {
setMenuStyle(menuPosition)
}
}
setIsMenuOpen(!isMenuOpen)
}, [isMenuOpen])
const recalculateMenuStyle = useCallback(() => {
const newMenuPosition = calculateSubmenuStyle(menuButtonRef.current, menuRef.current)
if (newMenuPosition) {
setMenuStyle(newMenuPosition)
}
}, []) }, [])
useEffect(() => {
if (isMenuOpen) {
setTimeout(() => {
recalculateMenuStyle()
})
}
}, [isMenuOpen, recalculateMenuStyle])
return ( return (
<div ref={menuContainerRef}> <div ref={menuContainerRef}>
<Disclosure open={isMenuOpen} onChange={toggleTagsMenu}> <button
<DisclosureButton onClick={toggleMenu}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === 'Escape') { if (event.key === KeyboardKey.Escape) {
setIsMenuOpen(false) setIsOpen(false)
} }
}} }}
onBlur={closeOnBlur} ref={buttonRef}
ref={menuButtonRef} className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" >
> <div className="flex items-center">
<div className="flex items-center"> <Icon type="hashtag" className="mr-2 text-neutral" />
<Icon type="hashtag" className="mr-2 text-neutral" /> Add tag
Add tag </div>
</div> <Icon type="chevron-right" className="text-neutral" />
<Icon type="chevron-right" className="text-neutral" /> </button>
</DisclosureButton> <Popover
<DisclosurePanel togglePopover={toggleMenu}
ref={menuRef} anchorElement={buttonRef.current}
onKeyDown={(event) => { open={isOpen}
if (event.key === 'Escape') { side="right"
setIsMenuOpen(false) align="start"
menuButtonRef.current?.focus() className="py-2"
} >
}} {navigationController.tags.map((tag) => (
style={{ <button
...menuStyle, key={tag.uuid}
position: 'fixed', className="max-w-80 flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-2 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
}} onClick={() => {
className={`${ notesController.isTagInSelectedNotes(tag)
isMenuOpen ? 'flex' : 'hidden' ? notesController.removeTagFromSelectedNotes(tag).catch(console.error)
} max-h-120 fixed min-w-80 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main`} : notesController.addTagToSelectedNotes(tag).catch(console.error)
> }}
{navigationController.tags.map((tag) => ( >
<button <span
key={tag.uuid} className={`overflow-hidden overflow-ellipsis whitespace-nowrap
className="max-w-80 flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-2 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onBlur={closeOnBlur}
onClick={() => {
notesController.isTagInSelectedNotes(tag)
? notesController.removeTagFromSelectedNotes(tag).catch(console.error)
: notesController.addTagToSelectedNotes(tag).catch(console.error)
}}
>
<span
className={`overflow-hidden overflow-ellipsis whitespace-nowrap
${notesController.isTagInSelectedNotes(tag) ? 'font-bold' : ''}`} ${notesController.isTagInSelectedNotes(tag) ? 'font-bold' : ''}`}
> >
{noteTagsController.getLongTitle(tag)} {noteTagsController.getLongTitle(tag)}
</span> </span>
</button> </button>
))} ))}
</DisclosurePanel> </Popover>
</Disclosure>
</div> </div>
) )
} }

View File

@@ -1,12 +1,10 @@
import { KeyboardKey } from '@/Services/IOService' import { KeyboardKey } from '@/Services/IOService'
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { SNNote } from '@standardnotes/snjs' import { SNNote } from '@standardnotes/snjs'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' import { FunctionComponent, useCallback, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import ChangeEditorMenu from '@/Components/ChangeEditor/ChangeEditorMenu' import ChangeEditorMenu from '@/Components/ChangeEditor/ChangeEditorMenu'
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle' import Popover from '../Popover/Popover'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
type ChangeEditorOptionProps = { type ChangeEditorOptionProps = {
application: WebApplication application: WebApplication
@@ -15,91 +13,48 @@ type ChangeEditorOptionProps = {
const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ application, note }) => { const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ application, note }) => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [isVisible, setIsVisible] = useState(false)
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
right: 0,
bottom: 0,
maxHeight: 'auto',
})
const menuContainerRef = useRef<HTMLDivElement>(null) const menuContainerRef = useRef<HTMLDivElement>(null)
const menuRef = useRef<HTMLDivElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null) const buttonRef = useRef<HTMLButtonElement>(null)
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, (open: boolean) => { const toggleMenu = useCallback(async () => {
setIsOpen(open) setIsOpen((isOpen) => !isOpen)
setIsVisible(open) }, [])
})
const toggleChangeEditorMenu = useCallback(() => {
if (!isOpen) {
const menuStyle = calculateSubmenuStyle(buttonRef.current)
if (menuStyle) {
setMenuStyle(menuStyle)
}
}
setIsOpen(!isOpen)
}, [isOpen])
useEffect(() => {
if (isOpen) {
setTimeout(() => {
const newMenuStyle = calculateSubmenuStyle(buttonRef.current, menuRef.current)
if (newMenuStyle) {
setMenuStyle(newMenuStyle)
setIsVisible(true)
}
}, 5)
}
}, [isOpen])
return ( return (
<div ref={menuContainerRef}> <div ref={menuContainerRef}>
<Disclosure open={isOpen} onChange={toggleChangeEditorMenu}> <button
<DisclosureButton onClick={toggleMenu}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.key === KeyboardKey.Escape) { if (event.key === KeyboardKey.Escape) {
setIsOpen(false) setIsOpen(false)
} }
}}
ref={buttonRef}
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
>
<div className="flex items-center">
<Icon type="dashboard" className="mr-2 text-neutral" />
Change note type
</div>
<Icon type="chevron-right" className="text-neutral" />
</button>
<Popover
align="start"
anchorElement={buttonRef.current}
className="pt-2 md:pt-0"
open={isOpen}
side="right"
togglePopover={toggleMenu}
>
<ChangeEditorMenu
application={application}
note={note}
isVisible={isOpen}
closeMenu={() => {
setIsOpen(false)
}} }}
onBlur={closeOnBlur} />
ref={buttonRef} </Popover>
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
>
<div className="flex items-center">
<Icon type="dashboard" className="mr-2 text-neutral" />
Change note type
</div>
<Icon type="chevron-right" className="text-neutral" />
</DisclosureButton>
<DisclosurePanel
ref={menuRef}
onKeyDown={(event) => {
if (event.key === KeyboardKey.Escape) {
setIsOpen(false)
buttonRef.current?.focus()
}
}}
style={{
...menuStyle,
position: 'fixed',
}}
className="max-h-120 fixed flex min-w-68 flex-col overflow-y-auto rounded bg-default shadow-main"
>
{isOpen && (
<ChangeEditorMenu
application={application}
closeOnBlur={closeOnBlur}
note={note}
isVisible={isVisible}
closeMenu={() => {
setIsOpen(false)
}}
/>
)}
</DisclosurePanel>
</Disclosure>
</div> </div>
) )
} }

View File

@@ -9,10 +9,9 @@ import Spinner from '@/Components/Spinner/Spinner'
type ListedActionsMenuProps = { type ListedActionsMenuProps = {
application: WebApplication application: WebApplication
note: SNNote note: SNNote
recalculateMenuStyle: () => void
} }
const ListedActionsMenu = ({ application, note, recalculateMenuStyle }: ListedActionsMenuProps) => { const ListedActionsMenu = ({ application, note }: ListedActionsMenuProps) => {
const [menuGroups, setMenuGroups] = useState<ListedMenuGroup[]>([]) const [menuGroups, setMenuGroups] = useState<ListedMenuGroup[]>([])
const [isFetchingAccounts, setIsFetchingAccounts] = useState(true) const [isFetchingAccounts, setIsFetchingAccounts] = useState(true)
@@ -88,14 +87,11 @@ const ListedActionsMenu = ({ application, note, recalculateMenuStyle }: ListedAc
console.error(err) console.error(err)
} finally { } finally {
setIsFetchingAccounts(false) setIsFetchingAccounts(false)
setTimeout(() => {
recalculateMenuStyle()
})
} }
} }
void fetchListedAccounts() void fetchListedAccounts()
}, [application, note.uuid, recalculateMenuStyle]) }, [application, note.uuid])
return ( return (
<> <>

View File

@@ -1,11 +1,10 @@
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { SNNote } from '@standardnotes/snjs' import { SNNote } from '@standardnotes/snjs'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' import { FunctionComponent, useCallback, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import ListedActionsMenu from './ListedActionsMenu' import ListedActionsMenu from './ListedActionsMenu'
import { KeyboardKey } from '@/Services/IOService'
import Popover from '../Popover/Popover'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -14,74 +13,42 @@ type Props = {
const ListedActionsOption: FunctionComponent<Props> = ({ application, note }) => { const ListedActionsOption: FunctionComponent<Props> = ({ application, note }) => {
const menuContainerRef = useRef<HTMLDivElement>(null) const menuContainerRef = useRef<HTMLDivElement>(null)
const menuRef = useRef<HTMLDivElement>(null) const buttonRef = useRef<HTMLButtonElement>(null)
const menuButtonRef = useRef<HTMLButtonElement>(null)
const [isMenuOpen, setIsMenuOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
right: 0,
bottom: 0,
maxHeight: 'auto',
})
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen) const toggleMenu = useCallback(() => {
setIsOpen((isOpen) => !isOpen)
const toggleListedMenu = useCallback(() => {
if (!isMenuOpen) {
const menuPosition = calculateSubmenuStyle(menuButtonRef.current)
if (menuPosition) {
setMenuStyle(menuPosition)
}
}
setIsMenuOpen(!isMenuOpen)
}, [isMenuOpen])
const recalculateMenuStyle = useCallback(() => {
const newMenuPosition = calculateSubmenuStyle(menuButtonRef.current, menuRef.current)
if (newMenuPosition) {
setMenuStyle(newMenuPosition)
}
}, []) }, [])
useEffect(() => {
if (isMenuOpen) {
setTimeout(() => {
recalculateMenuStyle()
})
}
}, [isMenuOpen, recalculateMenuStyle])
return ( return (
<div ref={menuContainerRef}> <div ref={menuContainerRef}>
<Disclosure open={isMenuOpen} onChange={toggleListedMenu}> <button
<DisclosureButton onClick={toggleMenu}
ref={menuButtonRef} onKeyDown={(event) => {
onBlur={closeOnBlur} if (event.key === KeyboardKey.Escape) {
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" setIsOpen(false)
> }
<div className="flex items-center"> }}
<Icon type="listed" className="mr-2 text-neutral" /> ref={buttonRef}
Listed actions className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
</div> >
<Icon type="chevron-right" className="text-neutral" /> <div className="flex items-center">
</DisclosureButton> <Icon type="listed" className="mr-2 text-neutral" />
<DisclosurePanel Listed actions
ref={menuRef} </div>
style={{ <Icon type="chevron-right" className="text-neutral" />
...menuStyle, </button>
position: 'fixed', <Popover
}} togglePopover={toggleMenu}
className={`${ anchorElement={buttonRef.current}
isMenuOpen ? 'flex' : 'hidden' open={isOpen}
} max-h-120 fixed min-w-68 flex-col overflow-y-auto rounded bg-default pb-1 shadow-main`} side="right"
> align="end"
{isMenuOpen && ( className="pt-2 md:pt-0"
<ListedActionsMenu application={application} note={note} recalculateMenuStyle={recalculateMenuStyle} /> >
)} <ListedActionsMenu application={application} note={note} />
</DisclosurePanel> </Popover>
</Disclosure>
</div> </div>
) )
} }

View File

@@ -15,13 +15,11 @@ import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { formatDateForContextMenu } from '@/Utils/DateUtils' import { formatDateForContextMenu } from '@/Utils/DateUtils'
type DeletePermanentlyButtonProps = { type DeletePermanentlyButtonProps = {
closeOnBlur: NotesOptionsProps['closeOnBlur']
onClick: () => void onClick: () => void
} }
const DeletePermanentlyButton = ({ closeOnBlur, onClick }: DeletePermanentlyButtonProps) => ( const DeletePermanentlyButton = ({ onClick }: DeletePermanentlyButtonProps) => (
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={onClick} onClick={onClick}
> >
@@ -177,7 +175,6 @@ const NotesOptions = ({
notesController, notesController,
noteTagsController, noteTagsController,
historyModalController, historyModalController,
closeOnBlur,
}: NotesOptionsProps) => { }: NotesOptionsProps) => {
const [altKeyDown, setAltKeyDown] = useState(false) const [altKeyDown, setAltKeyDown] = useState(false)
@@ -270,7 +267,6 @@ const NotesOptions = ({
{notes.length === 1 && ( {notes.length === 1 && (
<> <>
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={openRevisionHistoryModal} onClick={openRevisionHistoryModal}
> >
@@ -285,7 +281,6 @@ const NotesOptions = ({
onClick={() => { onClick={() => {
notesController.setLockSelectedNotes(!locked) notesController.setLockSelectedNotes(!locked)
}} }}
onBlur={closeOnBlur}
> >
<span className="flex items-center"> <span className="flex items-center">
<Icon type="pencil-off" className={iconClass} /> <Icon type="pencil-off" className={iconClass} />
@@ -298,7 +293,6 @@ const NotesOptions = ({
onClick={() => { onClick={() => {
notesController.setHideSelectedNotePreviews(!hidePreviews) notesController.setHideSelectedNotePreviews(!hidePreviews)
}} }}
onBlur={closeOnBlur}
> >
<span className="flex items-center"> <span className="flex items-center">
<Icon type="rich-text" className={iconClass} /> <Icon type="rich-text" className={iconClass} />
@@ -311,7 +305,6 @@ const NotesOptions = ({
onClick={() => { onClick={() => {
notesController.setProtectSelectedNotes(!protect).catch(console.error) notesController.setProtectSelectedNotes(!protect).catch(console.error)
}} }}
onBlur={closeOnBlur}
> >
<span className="flex items-center"> <span className="flex items-center">
<Icon type="password" className={iconClass} /> <Icon type="password" className={iconClass} />
@@ -335,7 +328,6 @@ const NotesOptions = ({
)} )}
{unpinned && ( {unpinned && (
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => { onClick={() => {
notesController.setPinSelectedNotes(true) notesController.setPinSelectedNotes(true)
@@ -347,7 +339,6 @@ const NotesOptions = ({
)} )}
{pinned && ( {pinned && (
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => { onClick={() => {
notesController.setPinSelectedNotes(false) notesController.setPinSelectedNotes(false)
@@ -358,7 +349,6 @@ const NotesOptions = ({
</button> </button>
)} )}
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={downloadSelectedItems} onClick={downloadSelectedItems}
> >
@@ -366,7 +356,6 @@ const NotesOptions = ({
Export Export
</button> </button>
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={duplicateSelectedItems} onClick={duplicateSelectedItems}
> >
@@ -375,7 +364,6 @@ const NotesOptions = ({
</button> </button>
{unarchived && ( {unarchived && (
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => { onClick={() => {
notesController.setArchiveSelectedNotes(true).catch(console.error) notesController.setArchiveSelectedNotes(true).catch(console.error)
@@ -387,7 +375,6 @@ const NotesOptions = ({
)} )}
{archived && ( {archived && (
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => { onClick={() => {
notesController.setArchiveSelectedNotes(false).catch(console.error) notesController.setArchiveSelectedNotes(false).catch(console.error)
@@ -400,14 +387,12 @@ const NotesOptions = ({
{notTrashed && {notTrashed &&
(altKeyDown ? ( (altKeyDown ? (
<DeletePermanentlyButton <DeletePermanentlyButton
closeOnBlur={closeOnBlur}
onClick={async () => { onClick={async () => {
await notesController.deleteNotesPermanently() await notesController.deleteNotesPermanently()
}} }}
/> />
) : ( ) : (
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={async () => { onClick={async () => {
await notesController.setTrashSelectedNotes(true) await notesController.setTrashSelectedNotes(true)
@@ -420,7 +405,6 @@ const NotesOptions = ({
{trashed && ( {trashed && (
<> <>
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={async () => { onClick={async () => {
await notesController.setTrashSelectedNotes(false) await notesController.setTrashSelectedNotes(false)
@@ -430,13 +414,11 @@ const NotesOptions = ({
<span className="text-success">Restore</span> <span className="text-success">Restore</span>
</button> </button>
<DeletePermanentlyButton <DeletePermanentlyButton
closeOnBlur={closeOnBlur}
onClick={async () => { onClick={async () => {
await notesController.deleteNotesPermanently() await notesController.deleteNotesPermanently()
}} }}
/> />
<button <button
onBlur={closeOnBlur}
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={async () => { onClick={async () => {
await notesController.emptyTrash() await notesController.emptyTrash()

View File

@@ -1,16 +1,13 @@
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import VisuallyHidden from '@reach/visually-hidden' import { useCallback, useRef, useState } from 'react'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { useRef, useState } from 'react'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import NotesOptions from './NotesOptions' import NotesOptions from './NotesOptions'
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { NotesController } from '@/Controllers/NotesController' import { NotesController } from '@/Controllers/NotesController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController' import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { NoteTagsController } from '@/Controllers/NoteTagsController' import { NoteTagsController } from '@/Controllers/NoteTagsController'
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController' import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
import Popover from '../Popover/Popover'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -29,83 +26,38 @@ const NotesOptionsPanel = ({
historyModalController, historyModalController,
onClickPreprocessing, onClickPreprocessing,
}: Props) => { }: Props) => {
const [open, setOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const [position, setPosition] = useState({
top: 0,
right: 0,
})
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
const buttonRef = useRef<HTMLButtonElement>(null) const buttonRef = useRef<HTMLButtonElement>(null)
const panelRef = useRef<HTMLDivElement>(null)
const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen) const toggleMenu = useCallback(async () => {
const willMenuOpen = !isOpen
if (willMenuOpen && onClickPreprocessing) {
await onClickPreprocessing()
}
setIsOpen(willMenuOpen)
}, [onClickPreprocessing, isOpen])
return ( return (
<Disclosure <>
open={open} <button
onChange={async () => {
const rect = buttonRef.current?.getBoundingClientRect()
if (rect) {
const { clientHeight } = document.documentElement
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
const footerHeightInPx = footerElementRect?.height
if (footerHeightInPx) {
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - 2)
}
setPosition({
top: rect.bottom,
right: document.body.clientWidth - rect.right,
})
const newOpenState = !open
if (newOpenState && onClickPreprocessing) {
await onClickPreprocessing()
}
setOpen(newOpenState)
}
}}
>
<DisclosureButton
onKeyDown={(event) => {
if (event.key === 'Escape') {
setOpen(false)
}
}}
onBlur={closeOnBlur}
ref={buttonRef}
className="bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast" className="bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast"
title="Note options menu"
aria-label="Note options menu"
onClick={toggleMenu}
ref={buttonRef}
> >
<VisuallyHidden>Actions</VisuallyHidden> <Icon type="more" />
<Icon type="more" className="block" /> </button>
</DisclosureButton> <Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="py-2">
<DisclosurePanel <NotesOptions
onKeyDown={(event) => { application={application}
if (event.key === 'Escape') { navigationController={navigationController}
setOpen(false) notesController={notesController}
buttonRef.current?.focus() noteTagsController={noteTagsController}
} historyModalController={historyModalController}
}} />
ref={panelRef} </Popover>
style={{ </>
...position,
maxHeight,
}}
className={`${
open ? 'flex' : 'hidden'
} max-h-120 slide-down-animation fixed min-w-80 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main transition-transform duration-150`}
onBlur={closeOnBlur}
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
>
{open && (
<NotesOptions
application={application}
navigationController={navigationController}
notesController={notesController}
noteTagsController={noteTagsController}
historyModalController={historyModalController}
closeOnBlur={closeOnBlur}
/>
)}
</DisclosurePanel>
</Disclosure>
) )
} }

View File

@@ -10,5 +10,4 @@ export type NotesOptionsProps = {
notesController: NotesController notesController: NotesController
noteTagsController: NoteTagsController noteTagsController: NoteTagsController
historyModalController: HistoryModalController historyModalController: HistoryModalController
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
} }

View File

@@ -0,0 +1,54 @@
import { CSSProperties } from 'react'
import { PopoverAlignment, PopoverSide } from './Types'
import { OppositeSide, checkCollisions, getNonCollidingSide, getNonCollidingAlignment } from './Utils/Collisions'
import { getPositionedPopoverRect } from './Utils/Rect'
const getStylesFromRect = (rect: DOMRect): CSSProperties => {
return {
willChange: 'transform',
transform: `translate(${rect.x}px, ${rect.y}px)`,
}
}
type Options = {
align: PopoverAlignment
anchorRect?: DOMRect
documentRect: DOMRect
popoverRect?: DOMRect
side: PopoverSide
}
export const getPositionedPopoverStyles = ({
align,
anchorRect,
documentRect,
popoverRect,
side,
}: Options): [CSSProperties | null, PopoverSide, PopoverAlignment] => {
if (!popoverRect || !anchorRect) {
return [null, side, align]
}
const matchesMediumBreakpoint = matchMedia('(min-width: 768px)').matches
if (!matchesMediumBreakpoint) {
return [null, side, align]
}
const rectForPreferredSide = getPositionedPopoverRect(popoverRect, anchorRect, side, align)
const preferredSideRectCollisions = checkCollisions(rectForPreferredSide, documentRect)
const oppositeSide = OppositeSide[side]
const rectForOppositeSide = getPositionedPopoverRect(popoverRect, anchorRect, oppositeSide, align)
const oppositeSideRectCollisions = checkCollisions(rectForOppositeSide, documentRect)
const finalSide = getNonCollidingSide(side, preferredSideRectCollisions, oppositeSideRectCollisions)
const finalAlignment = getNonCollidingAlignment(finalSide, align, preferredSideRectCollisions, {
popoverRect,
buttonRect: anchorRect,
documentRect,
})
const finalPositionedRect = getPositionedPopoverRect(popoverRect, anchorRect, finalSide, finalAlignment)
return [getStylesFromRect(finalPositionedRect), finalSide, finalAlignment]
}

View File

@@ -0,0 +1,36 @@
import PositionedPopoverContent from './PositionedPopoverContent'
import { PopoverProps } from './Types'
type Props = PopoverProps & {
open: boolean
}
const Popover = ({
align,
anchorElement,
anchorPoint,
children,
className,
open,
overrideZIndex,
side,
togglePopover,
}: Props) => {
return open ? (
<>
<PositionedPopoverContent
align={align}
anchorElement={anchorElement}
anchorPoint={anchorPoint}
className={className}
overrideZIndex={overrideZIndex}
side={side}
togglePopover={togglePopover}
>
{children}
</PositionedPopoverContent>
</>
) : null
}
export default Popover

View File

@@ -0,0 +1,80 @@
import { useDocumentRect } from '@/Hooks/useDocumentRect'
import { useAutoElementRect } from '@/Hooks/useElementRect'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { useState } from 'react'
import Icon from '../Icon/Icon'
import Portal from '../Portal/Portal'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { getPositionedPopoverStyles } from './GetPositionedPopoverStyles'
import { PopoverContentProps } from './Types'
import { getPopoverMaxHeight, getAppRect } from './Utils/Rect'
import { usePopoverCloseOnClickOutside } from './Utils/usePopoverCloseOnClickOutside'
const PositionedPopoverContent = ({
align = 'end',
anchorElement,
anchorPoint,
children,
className,
overrideZIndex,
side = 'bottom',
togglePopover,
}: PopoverContentProps) => {
const [popoverElement, setPopoverElement] = useState<HTMLDivElement | null>(null)
const popoverRect = useAutoElementRect(popoverElement)
const anchorElementRect = useAutoElementRect(anchorElement, {
updateOnWindowResize: true,
})
const anchorPointRect = DOMRect.fromRect({
x: anchorPoint?.x,
y: anchorPoint?.y,
})
const anchorRect = anchorPoint ? anchorPointRect : anchorElementRect
const documentRect = useDocumentRect()
const [styles, positionedSide, positionedAlignment] = getPositionedPopoverStyles({
align,
anchorRect,
documentRect,
popoverRect: popoverRect ?? popoverElement?.getBoundingClientRect(),
side,
})
usePopoverCloseOnClickOutside({
popoverElement,
anchorElement,
togglePopover,
})
return (
<Portal>
<div
className={classNames(
'absolute top-0 left-0 flex h-full w-full min-w-80 cursor-auto flex-col overflow-y-auto rounded bg-default shadow-main md:h-auto md:max-w-xs',
overrideZIndex ? overrideZIndex : 'z-dropdown-menu',
className,
)}
style={{
...styles,
maxHeight: getPopoverMaxHeight(getAppRect(documentRect), anchorRect, positionedSide, positionedAlignment),
}}
ref={(node) => {
setPopoverElement(node)
}}
data-popover
>
<div className="md:hidden">
<div className="flex items-center justify-end px-3">
<button className="rounded-full border border-border p-1" onClick={togglePopover}>
<Icon type="close" className="h-4 w-4" />
</button>
</div>
<HorizontalSeparator classes="my-2" />
</div>
{children}
</div>
</Portal>
)
}
export default PositionedPopoverContent

View File

@@ -0,0 +1,49 @@
import { ReactNode } from 'react'
export type PopoverState = 'closed' | 'positioning' | 'open'
export type PopoverElement = HTMLDivElement | HTMLMenuElement
export type PopoverSide = 'top' | 'left' | 'bottom' | 'right'
export type PopoverAlignment = 'start' | 'center' | 'end'
export type PopoverOptions = {
side: PopoverSide
align: PopoverAlignment
}
export type RectCollisions = Record<PopoverSide, boolean>
type Point = {
x: number
y: number
}
type PopoverAnchorElementProps = {
anchorElement: HTMLElement | null
anchorPoint?: never
}
type PopoverAnchorPointProps = {
anchorPoint: Point
anchorElement?: never
}
type CommonPopoverProps = {
align?: PopoverAlignment
children: ReactNode
side?: PopoverSide
overrideZIndex?: string
togglePopover: () => void
className?: string
}
export type PopoverContentProps = CommonPopoverProps & {
anchorElement?: HTMLElement | null
anchorPoint?: Point
}
export type PopoverProps =
| (CommonPopoverProps & PopoverAnchorElementProps)
| (CommonPopoverProps & PopoverAnchorPointProps)

View File

@@ -0,0 +1,86 @@
import { PopoverSide, PopoverAlignment, RectCollisions } from '../Types'
import { getAppRect, getPositionedPopoverRect } from './Rect'
export const OppositeSide: Record<PopoverSide, PopoverSide> = {
top: 'bottom',
bottom: 'top',
left: 'right',
right: 'left',
}
export const checkCollisions = (popoverRect: DOMRect, containerRect: DOMRect): RectCollisions => {
const appRect = getAppRect(containerRect)
return {
top: popoverRect.top < appRect.top,
left: popoverRect.left < appRect.left,
bottom: popoverRect.bottom > appRect.bottom,
right: popoverRect.right > appRect.right,
}
}
export const getNonCollidingSide = (
preferredSide: PopoverSide,
preferredSideCollisions: RectCollisions,
oppositeSideCollisions: RectCollisions,
): PopoverSide => {
const oppositeSide = OppositeSide[preferredSide]
return preferredSideCollisions[preferredSide] && !oppositeSideCollisions[oppositeSide] ? oppositeSide : preferredSide
}
const OppositeAlignment: Record<Exclude<PopoverAlignment, 'center'>, PopoverAlignment> = {
start: 'end',
end: 'start',
}
export const getNonCollidingAlignment = (
finalSide: PopoverSide,
preferredAlignment: PopoverAlignment,
collisions: RectCollisions,
{
popoverRect,
buttonRect,
documentRect,
}: {
popoverRect: DOMRect
buttonRect: DOMRect
documentRect: DOMRect
},
): PopoverAlignment => {
const isHorizontalSide = finalSide === 'top' || finalSide === 'bottom'
const boundToCheckForStart = isHorizontalSide ? 'right' : 'bottom'
const boundToCheckForEnd = isHorizontalSide ? 'left' : 'top'
const prefersAligningAtStart = preferredAlignment === 'start'
const prefersAligningAtCenter = preferredAlignment === 'center'
const prefersAligningAtEnd = preferredAlignment === 'end'
if (prefersAligningAtCenter) {
if (collisions[boundToCheckForStart]) {
return 'end'
}
if (collisions[boundToCheckForEnd]) {
return 'start'
}
} else {
const oppositeAlignmentCollisions = checkCollisions(
getPositionedPopoverRect(popoverRect, buttonRect, finalSide, OppositeAlignment[preferredAlignment]),
documentRect,
)
if (
prefersAligningAtStart &&
collisions[boundToCheckForStart] &&
!oppositeAlignmentCollisions[boundToCheckForEnd]
) {
return 'end'
}
if (prefersAligningAtEnd && collisions[boundToCheckForEnd] && !oppositeAlignmentCollisions[boundToCheckForStart]) {
return 'start'
}
}
return preferredAlignment
}

View File

@@ -0,0 +1,120 @@
import { PopoverSide, PopoverAlignment } from '../Types'
export const getPopoverMaxHeight = (
appRect: DOMRect,
buttonRect: DOMRect | undefined,
side: PopoverSide,
alignment: PopoverAlignment,
): number | 'none' => {
const matchesMediumBreakpoint = matchMedia('(min-width: 768px)').matches
if (!matchesMediumBreakpoint) {
return 'none'
}
const MarginFromAppBorderInPX = 10
let constraint = 0
if (buttonRect) {
switch (side) {
case 'top':
constraint = appRect.height - buttonRect.top
break
case 'bottom':
constraint = buttonRect.bottom
break
case 'left':
case 'right':
switch (alignment) {
case 'start':
constraint = buttonRect.top
break
case 'end':
constraint = appRect.height - buttonRect.bottom
break
}
break
}
}
return appRect.height - constraint - MarginFromAppBorderInPX
}
export const getMaxHeightAdjustedRect = (rect: DOMRect, maxHeight: number) => {
return DOMRect.fromRect({
width: rect.width,
height: rect.height < maxHeight ? rect.height : maxHeight,
x: rect.x,
y: rect.y,
})
}
export const getAppRect = (updatedDocumentRect?: DOMRect) => {
const footerRect = document.querySelector('footer')?.getBoundingClientRect()
const documentRect = updatedDocumentRect ? updatedDocumentRect : document.documentElement.getBoundingClientRect()
const appRect = footerRect
? DOMRect.fromRect({
width: documentRect.width,
height: documentRect.height - footerRect.height,
})
: documentRect
return appRect
}
export const getPositionedPopoverRect = (
popoverRect: DOMRect,
buttonRect: DOMRect,
side: PopoverSide,
align: PopoverAlignment,
): DOMRect => {
const { width, height } = popoverRect
const positionPopoverRect = DOMRect.fromRect(popoverRect)
switch (side) {
case 'top': {
positionPopoverRect.y = buttonRect.top - height
break
}
case 'bottom':
positionPopoverRect.y = buttonRect.bottom
break
case 'left':
positionPopoverRect.x = buttonRect.left - width
break
case 'right':
positionPopoverRect.x = buttonRect.right
break
}
if (side === 'top' || side === 'bottom') {
switch (align) {
case 'start':
positionPopoverRect.x = buttonRect.left
break
case 'center':
positionPopoverRect.x = buttonRect.left - width / 2 + buttonRect.width / 2
break
case 'end':
positionPopoverRect.x = buttonRect.right - width
break
}
} else {
switch (align) {
case 'start':
positionPopoverRect.y = buttonRect.top
break
case 'center':
positionPopoverRect.y = buttonRect.top - height / 2 + buttonRect.height / 2
break
case 'end':
positionPopoverRect.y = buttonRect.bottom - height
break
}
}
return positionPopoverRect
}

View File

@@ -0,0 +1,36 @@
import { useEffect } from 'react'
type Options = {
popoverElement: HTMLElement | null
anchorElement: HTMLElement | null | undefined
togglePopover: () => void
}
export const usePopoverCloseOnClickOutside = ({ popoverElement, anchorElement, togglePopover }: Options) => {
useEffect(() => {
const closeIfClickedOutside = (event: MouseEvent) => {
const matchesMediumBreakpoint = matchMedia('(min-width: 768px)').matches
if (!matchesMediumBreakpoint) {
return
}
const target = event.target as Element
const isDescendantOfMenu = popoverElement?.contains(target)
const isAnchorElement = anchorElement ? anchorElement === event.target || anchorElement.contains(target) : false
const isDescendantOfPopover = target.closest('[data-popover]')
if (!isDescendantOfMenu && !isAnchorElement && !isDescendantOfPopover) {
togglePopover()
}
}
document.addEventListener('click', closeIfClickedOutside, { capture: true })
return () => {
document.removeEventListener('click', closeIfClickedOutside, {
capture: true,
})
}
}, [anchorElement, popoverElement, togglePopover])
}

View File

@@ -0,0 +1,24 @@
import { ReactNode, useState, useEffect } from 'react'
import { createPortal } from 'react-dom'
type Props = {
children: ReactNode
}
const randomPortalId = () => Math.random()
const Portal = ({ children }: Props) => {
const [container, setContainer] = useState<HTMLElement>()
useEffect(() => {
const container = document.createElement('div')
container.id = `react-portal-${randomPortalId()}`
document.body.append(container)
setContainer(container)
return () => container.remove()
}, [])
return container ? createPortal(children, container) : null
}
export default Portal

View File

@@ -1,27 +1,26 @@
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { ComponentArea, ContentType, FeatureIdentifier, GetFeatures, SNComponent } from '@standardnotes/snjs' import { ComponentArea, ContentType, FeatureIdentifier, GetFeatures, SNComponent } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, KeyboardEventHandler, useCallback, useEffect, useRef, useState } from 'react' import { FunctionComponent, KeyboardEventHandler, useCallback, useEffect, useRef, useState } from 'react'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import Switch from '@/Components/Switch/Switch' import Switch from '@/Components/Switch/Switch'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { quickSettingsKeyDownHandler } from './EventHandlers'
import { quickSettingsKeyDownHandler, themesMenuKeyDownHandler } from './EventHandlers'
import FocusModeSwitch from './FocusModeSwitch' import FocusModeSwitch from './FocusModeSwitch'
import ThemesMenuButton from './ThemesMenuButton' import ThemesMenuButton from './ThemesMenuButton'
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
import { ThemeItem } from './ThemeItem' import { ThemeItem } from './ThemeItem'
import { sortThemes } from '@/Utils/SortThemes' import { sortThemes } from '@/Utils/SortThemes'
import RadioIndicator from '../RadioIndicator/RadioIndicator' import RadioIndicator from '../RadioIndicator/RadioIndicator'
import HorizontalSeparator from '../Shared/HorizontalSeparator' import HorizontalSeparator from '../Shared/HorizontalSeparator'
import Popover from '../Popover/Popover'
import { PreferencesController } from '@/Controllers/PreferencesController'
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
const focusModeAnimationDuration = 1255 const focusModeAnimationDuration = 1255
type MenuProps = { type MenuProps = {
viewControllerManager: ViewControllerManager preferencesController: PreferencesController
quickSettingsMenuController: QuickSettingsController
application: WebApplication application: WebApplication
onClickOutside: () => void
} }
const toggleFocusMode = (enabled: boolean) => { const toggleFocusMode = (enabled: boolean) => {
@@ -38,25 +37,23 @@ const toggleFocusMode = (enabled: boolean) => {
} }
} }
const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, viewControllerManager, onClickOutside }) => { const QuickSettingsMenu: FunctionComponent<MenuProps> = ({
const { closeQuickSettingsMenu, shouldAnimateCloseMenu, focusModeEnabled, setFocusModeEnabled } = application,
viewControllerManager.quickSettingsMenuController preferencesController,
quickSettingsMenuController,
}) => {
const { closeQuickSettingsMenu, focusModeEnabled, setFocusModeEnabled } = quickSettingsMenuController
const [themes, setThemes] = useState<ThemeItem[]>([]) const [themes, setThemes] = useState<ThemeItem[]>([])
const [toggleableComponents, setToggleableComponents] = useState<SNComponent[]>([]) const [toggleableComponents, setToggleableComponents] = useState<SNComponent[]>([])
const [themesMenuOpen, setThemesMenuOpen] = useState(false) const [themesMenuOpen, setThemesMenuOpen] = useState(false)
const [themesMenuPosition, setThemesMenuPosition] = useState({})
const [defaultThemeOn, setDefaultThemeOn] = useState(false) const [defaultThemeOn, setDefaultThemeOn] = useState(false)
const themesMenuRef = useRef<HTMLDivElement>(null)
const themesButtonRef = useRef<HTMLButtonElement>(null) const themesButtonRef = useRef<HTMLButtonElement>(null)
const prefsButtonRef = useRef<HTMLButtonElement>(null) const prefsButtonRef = useRef<HTMLButtonElement>(null)
const quickSettingsMenuRef = useRef<HTMLDivElement>(null) const quickSettingsMenuRef = useRef<HTMLDivElement>(null)
const defaultThemeButtonRef = useRef<HTMLButtonElement>(null) const defaultThemeButtonRef = useRef<HTMLButtonElement>(null)
const mainRef = useRef<HTMLDivElement>(null) const mainRef = useRef<HTMLDivElement>(null)
useCloseOnClickOutside(mainRef, () => {
onClickOutside()
})
useEffect(() => { useEffect(() => {
toggleFocusMode(focusModeEnabled) toggleFocusMode(focusModeEnabled)
@@ -139,25 +136,14 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, viewCont
prefsButtonRef.current?.focus() prefsButtonRef.current?.focus()
}, []) }, [])
const [closeOnBlur] = useCloseOnBlur(themesMenuRef, setThemesMenuOpen)
const toggleThemesMenu = useCallback(() => { const toggleThemesMenu = useCallback(() => {
if (!themesMenuOpen && themesButtonRef.current) { setThemesMenuOpen((isOpen) => !isOpen)
const themesButtonRect = themesButtonRef.current.getBoundingClientRect() }, [])
setThemesMenuPosition({
left: themesButtonRect.right,
bottom: document.documentElement.clientHeight - themesButtonRect.bottom,
})
setThemesMenuOpen(true)
} else {
setThemesMenuOpen(false)
}
}, [themesMenuOpen])
const openPreferences = useCallback(() => { const openPreferences = useCallback(() => {
closeQuickSettingsMenu() closeQuickSettingsMenu()
viewControllerManager.preferencesController.openPreferences() preferencesController.openPreferences()
}, [viewControllerManager, closeQuickSettingsMenu]) }, [closeQuickSettingsMenu, preferencesController])
const toggleComponent = useCallback( const toggleComponent = useCallback(
(component: SNComponent) => { (component: SNComponent) => {
@@ -193,10 +179,6 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, viewCont
[closeQuickSettingsMenu, themesMenuOpen], [closeQuickSettingsMenu, themesMenuOpen],
) )
const handlePanelKeyDown: React.KeyboardEventHandler<HTMLDivElement> = useCallback((event) => {
themesMenuKeyDownHandler(event, themesMenuRef, setThemesMenuOpen, themesButtonRef)
}, [])
const toggleDefaultTheme = useCallback(() => { const toggleDefaultTheme = useCallback(() => {
const activeTheme = themes.map((item) => item.component).find((theme) => theme?.active && !theme.isLayerable()) const activeTheme = themes.map((item) => item.component).find((theme) => theme?.active && !theme.isLayerable())
if (activeTheme) { if (activeTheme) {
@@ -205,90 +187,71 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, viewCont
}, [application, themes]) }, [application, themes])
return ( return (
<div ref={mainRef} className="sn-component"> <div ref={mainRef} onKeyDown={handleQuickSettingsKeyDown}>
<div <div className="mt-1 mb-2 px-3 text-sm font-semibold uppercase text-text">Quick Settings</div>
className={`max-h-120 absolute bottom-full left-0 z-footer-bar-item-panel flex min-w-80 max-w-xs cursor-auto flex-col overflow-y-auto rounded bg-default py-2 shadow-main ${ <button
shouldAnimateCloseMenu ? 'slide-up-animation' : 'slide-down-animation transition-transform duration-150' onClick={toggleThemesMenu}
}`} onKeyDown={handleBtnKeyDown}
ref={quickSettingsMenuRef} ref={themesButtonRef}
onKeyDown={handleQuickSettingsKeyDown} className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
> >
<div className="mt-1 mb-2 px-3 text-sm font-semibold uppercase text-text">Quick Settings</div> <div className="flex items-center">
<Disclosure open={themesMenuOpen} onChange={toggleThemesMenu}> <Icon type="themes" className="mr-2 text-neutral" />
<DisclosureButton Themes
onKeyDown={handleBtnKeyDown} </div>
onBlur={closeOnBlur} <Icon type="chevron-right" className="text-neutral" />
ref={themesButtonRef} </button>
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" <Popover
> togglePopover={toggleThemesMenu}
<div className="flex items-center"> anchorElement={themesButtonRef.current}
<Icon type="themes" className="mr-2 text-neutral" /> open={themesMenuOpen}
Themes side="right"
</div> align="end"
<Icon type="chevron-right" className="text-neutral" /> className="py-2"
</DisclosureButton> >
<DisclosurePanel <div className="my-1 px-3 text-sm font-semibold uppercase text-text">Themes</div>
onBlur={closeOnBlur}
ref={themesMenuRef}
onKeyDown={handlePanelKeyDown}
style={{
...themesMenuPosition,
}}
className={`${
themesMenuOpen ? 'flex' : 'hidden'
} max-h-120 slide-down-animation fixed min-w-80 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main transition-transform duration-150`}
>
<div className="my-1 px-3 text-sm font-semibold uppercase text-text">Themes</div>
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={toggleDefaultTheme}
onBlur={closeOnBlur}
ref={defaultThemeButtonRef}
>
<RadioIndicator checked={defaultThemeOn} className="mr-2" />
Default
</button>
{themes.map((theme) => (
<ThemesMenuButton
item={theme}
application={application}
key={theme.component?.uuid ?? theme.identifier}
onBlur={closeOnBlur}
/>
))}
</DisclosurePanel>
</Disclosure>
{toggleableComponents.map((component) => (
<button
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
toggleComponent(component)
}}
key={component.uuid}
>
<div className="flex items-center">
<Icon type="window" className="mr-2 text-neutral" />
{component.displayName}
</div>
<Switch checked={component.active} className="px-0" />
</button>
))}
<FocusModeSwitch
application={application}
onToggle={setFocusModeEnabled}
onClose={closeQuickSettingsMenu}
isEnabled={focusModeEnabled}
/>
<HorizontalSeparator classes="my-2" />
<button <button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none" className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={openPreferences} onClick={toggleDefaultTheme}
ref={prefsButtonRef} ref={defaultThemeButtonRef}
> >
<Icon type="more" className="mr-2 text-neutral" /> <RadioIndicator checked={defaultThemeOn} className="mr-2" />
Open Preferences Default
</button> </button>
</div> {themes.map((theme) => (
<ThemesMenuButton item={theme} application={application} key={theme.component?.uuid ?? theme.identifier} />
))}
</Popover>
{toggleableComponents.map((component) => (
<button
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={() => {
toggleComponent(component)
}}
key={component.uuid}
>
<div className="flex items-center">
<Icon type="window" className="mr-2 text-neutral" />
{component.displayName}
</div>
<Switch checked={component.active} className="px-0" />
</button>
))}
<FocusModeSwitch
application={application}
onToggle={setFocusModeEnabled}
onClose={closeQuickSettingsMenu}
isEnabled={focusModeEnabled}
/>
<HorizontalSeparator classes="my-2" />
<button
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
onClick={openPreferences}
ref={prefsButtonRef}
>
<Icon type="more" className="mr-2 text-neutral" />
Open Preferences
</button>
</div> </div>
) )
} }

View File

@@ -11,10 +11,9 @@ import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/Premium
type Props = { type Props = {
item: ThemeItem item: ThemeItem
application: WebApplication application: WebApplication
onBlur: (event: { relatedTarget: EventTarget | null }) => void
} }
const ThemesMenuButton: FunctionComponent<Props> = ({ application, item, onBlur }) => { const ThemesMenuButton: FunctionComponent<Props> = ({ application, item }) => {
const premiumModal = usePremiumModal() const premiumModal = usePremiumModal()
const isThirdPartyTheme = useMemo( const isThirdPartyTheme = useMemo(
@@ -50,7 +49,6 @@ const ThemesMenuButton: FunctionComponent<Props> = ({ application, item, onBlur
'flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:bg-info-backdrop focus:shadow-none focus:shadow-none' 'flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:bg-info-backdrop focus:shadow-none focus:shadow-none'
} }
onClick={toggleTheme} onClick={toggleTheme}
onBlur={onBlur}
> >
{item.component?.isLayerable() ? ( {item.component?.isLayerable() ? (
<> <>

View File

@@ -1,5 +1,5 @@
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { useCallback, useEffect, useRef, useMemo } from 'react' import { useCallback, useRef, useMemo } from 'react'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import Menu from '@/Components/Menu/Menu' import Menu from '@/Components/Menu/Menu'
import MenuItem from '@/Components/Menu/MenuItem' import MenuItem from '@/Components/Menu/MenuItem'
@@ -11,6 +11,7 @@ import { NavigationController } from '@/Controllers/Navigation/NavigationControl
import HorizontalSeparator from '../Shared/HorizontalSeparator' import HorizontalSeparator from '../Shared/HorizontalSeparator'
import { formatDateForContextMenu } from '@/Utils/DateUtils' import { formatDateForContextMenu } from '@/Utils/DateUtils'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon' import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
import Popover from '../Popover/Popover'
type ContextMenuProps = { type ContextMenuProps = {
navigationController: NavigationController navigationController: NavigationController
@@ -21,22 +22,11 @@ type ContextMenuProps = {
const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag }: ContextMenuProps) => { const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag }: ContextMenuProps) => {
const premiumModal = usePremiumModal() const premiumModal = usePremiumModal()
const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = navigationController const { contextMenuOpen, contextMenuClickLocation } = navigationController
const contextMenuRef = useRef<HTMLDivElement>(null) const contextMenuRef = useRef<HTMLDivElement>(null)
useCloseOnClickOutside(contextMenuRef, () => navigationController.setContextMenuOpen(false)) useCloseOnClickOutside(contextMenuRef, () => navigationController.setContextMenuOpen(false))
const reloadContextMenuLayout = useCallback(() => {
navigationController.reloadContextMenuLayout()
}, [navigationController])
useEffect(() => {
window.addEventListener('resize', reloadContextMenuLayout)
return () => {
window.removeEventListener('resize', reloadContextMenuLayout)
}
}, [reloadContextMenuLayout])
const onClickAddSubtag = useCallback(() => { const onClickAddSubtag = useCallback(() => {
if (!isEntitledToFolders) { if (!isEntitledToFolders) {
premiumModal.activate('Folders') premiumModal.activate('Folders')
@@ -63,52 +53,46 @@ const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag
const tagCreatedAt = useMemo(() => formatDateForContextMenu(selectedTag.created_at), [selectedTag.created_at]) const tagCreatedAt = useMemo(() => formatDateForContextMenu(selectedTag.created_at), [selectedTag.created_at])
return contextMenuOpen ? ( return (
<div <Popover
ref={contextMenuRef} open={contextMenuOpen}
className="max-h-120 fixed z-dropdown-menu flex min-w-60 flex-col overflow-y-auto rounded bg-default py-2 shadow-main" anchorPoint={contextMenuClickLocation}
style={{ togglePopover={() => navigationController.setContextMenuOpen(!contextMenuOpen)}
...contextMenuPosition, className="py-2"
maxHeight: contextMenuMaxHeight,
}}
> >
<Menu <div ref={contextMenuRef}>
a11yLabel="Tag context menu" <Menu a11yLabel="Tag context menu" isOpen={contextMenuOpen}>
isOpen={contextMenuOpen} <MenuItem type={MenuItemType.IconButton} className={'justify-between py-1.5'} onClick={onClickAddSubtag}>
closeMenu={() => { <div className="flex items-center">
navigationController.setContextMenuOpen(false) <Icon type="add" className="mr-2 text-neutral" />
}} Add subtag
> </div>
<MenuItem type={MenuItemType.IconButton} className={'justify-between py-1.5'} onClick={onClickAddSubtag}> {!isEntitledToFolders && <Icon type={PremiumFeatureIconName} className={PremiumFeatureIconClass} />}
<div className="flex items-center"> </MenuItem>
<Icon type="add" className="mr-2 text-neutral" /> <MenuItem type={MenuItemType.IconButton} className={'py-1.5'} onClick={onClickRename}>
Add subtag <Icon type="pencil-filled" className="mr-2 text-neutral" />
Rename
</MenuItem>
<MenuItem type={MenuItemType.IconButton} className={'py-1.5'} onClick={onClickDelete}>
<Icon type="trash" className="mr-2 text-danger" />
<span className="text-danger">Delete</span>
</MenuItem>
</Menu>
<HorizontalSeparator classes="my-2" />
<div className="px-3 pt-1 pb-1.5 text-xs font-medium text-neutral">
<div className="mb-1">
<span className="font-semibold">Last modified:</span> {tagLastModified}
</div>
<div className="mb-1">
<span className="font-semibold">Created:</span> {tagCreatedAt}
</div>
<div>
<span className="font-semibold">Tag ID:</span> {selectedTag.uuid}
</div> </div>
{!isEntitledToFolders && <Icon type={PremiumFeatureIconName} className={PremiumFeatureIconClass} />}
</MenuItem>
<MenuItem type={MenuItemType.IconButton} className={'py-1.5'} onClick={onClickRename}>
<Icon type="pencil-filled" className="mr-2 text-neutral" />
Rename
</MenuItem>
<MenuItem type={MenuItemType.IconButton} className={'py-1.5'} onClick={onClickDelete}>
<Icon type="trash" className="mr-2 text-danger" />
<span className="text-danger">Delete</span>
</MenuItem>
</Menu>
<HorizontalSeparator classes="my-2" />
<div className="px-3 pt-1 pb-1.5 text-xs font-medium text-neutral">
<div className="mb-1">
<span className="font-semibold">Last modified:</span> {tagLastModified}
</div>
<div className="mb-1">
<span className="font-semibold">Created:</span> {tagCreatedAt}
</div>
<div>
<span className="font-semibold">Tag ID:</span> {selectedTag.uuid}
</div> </div>
</div> </div>
</div> </Popover>
) : null )
} }
export default observer(TagContextMenu) export default observer(TagContextMenu)

View File

@@ -0,0 +1,25 @@
import { useEffect, useState } from 'react'
const DebounceTimeInMs = 100
export const useDocumentRect = (): DOMRect => {
const [documentRect, setDocumentRect] = useState<DOMRect>(document.documentElement.getBoundingClientRect())
useEffect(() => {
let debounceTimeout: number
const handleWindowResize = () => {
window.clearTimeout(debounceTimeout)
window.setTimeout(() => {
setDocumentRect(document.documentElement.getBoundingClientRect())
}, DebounceTimeInMs)
}
window.addEventListener('resize', handleWindowResize)
return () => window.removeEventListener('resize', handleWindowResize)
}, [])
return documentRect
}

View File

@@ -0,0 +1,53 @@
import { useEffect, useState } from 'react'
const DebounceTimeInMs = 100
type Options = {
updateOnWindowResize: boolean
}
/**
* Returns the bounding rect of an element, auto-updated when the element resizes.
* Can optionally be auto-update on window resize.
*/
export const useAutoElementRect = (
element: HTMLElement | null | undefined,
{ updateOnWindowResize }: Options = { updateOnWindowResize: false },
) => {
const [rect, setRect] = useState<DOMRect>()
useEffect(() => {
let windowResizeDebounceTimeout: number
let windowResizeHandler: () => void
if (element) {
const resizeObserver = new ResizeObserver(() => {
setRect(element.getBoundingClientRect())
})
resizeObserver.observe(element)
if (updateOnWindowResize) {
windowResizeHandler = () => {
window.clearTimeout(windowResizeDebounceTimeout)
window.setTimeout(() => {
setRect(element.getBoundingClientRect())
}, DebounceTimeInMs)
}
window.addEventListener('resize', windowResizeHandler)
}
return () => {
resizeObserver.unobserve(element)
if (windowResizeHandler) {
window.removeEventListener('resize', windowResizeHandler)
}
}
} else {
setRect(undefined)
return
}
}, [element, updateOnWindowResize])
return rect
}

View File

@@ -1,64 +0,0 @@
import { MAX_MENU_SIZE_MULTIPLIER, MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants'
export type SubmenuStyle = {
top?: number | 'auto'
right?: number | 'auto'
bottom: number | 'auto'
left?: number | 'auto'
visibility?: 'hidden' | 'visible'
maxHeight: number | 'auto'
}
export const calculateSubmenuStyle = (
button: HTMLButtonElement | null,
menu?: HTMLDivElement | HTMLMenuElement | null,
): SubmenuStyle | undefined => {
const defaultFontSize = window.getComputedStyle(document.documentElement).fontSize
const maxChangeEditorMenuSize = parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER
const { clientWidth, clientHeight } = document.documentElement
const buttonRect = button?.getBoundingClientRect()
const buttonParentRect = button?.parentElement?.getBoundingClientRect()
const menuBoundingRect = menu?.getBoundingClientRect()
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
const footerHeightInPx = footerElementRect?.height ?? 0
let position: SubmenuStyle = {
bottom: 'auto',
maxHeight: 'auto',
}
if (buttonRect && buttonParentRect) {
let positionBottom = clientHeight - buttonRect.bottom - buttonRect.height / 2
if (positionBottom < footerHeightInPx) {
positionBottom = footerHeightInPx + MENU_MARGIN_FROM_APP_BORDER
}
position = {
bottom: positionBottom,
visibility: 'hidden',
maxHeight: 'auto',
}
if (buttonRect.right + maxChangeEditorMenuSize > clientWidth) {
position.right = clientWidth - buttonRect.left
} else {
position.left = buttonRect.right
}
}
if (menuBoundingRect?.height && buttonRect && position.bottom !== 'auto') {
position.visibility = 'visible'
if (menuBoundingRect.y < MENU_MARGIN_FROM_APP_BORDER) {
position.bottom = position.bottom + menuBoundingRect.y - MENU_MARGIN_FROM_APP_BORDER * 2
}
if (footerElementRect && menuBoundingRect.height > footerElementRect.y) {
position.bottom = footerElementRect.height + MENU_MARGIN_FROM_APP_BORDER
position.maxHeight = clientHeight - footerElementRect.height - MENU_MARGIN_FROM_APP_BORDER * 2
}
}
return position
}

View File

@@ -1,4 +1,3 @@
export * from './CalculateSubmenuStyle'
export * from './ConcatenateUint8Arrays' export * from './ConcatenateUint8Arrays'
export * from './IsMobile' export * from './IsMobile'
export * from './StringUtils' export * from './StringUtils'

View File

@@ -4,10 +4,10 @@
--z-index-resizer-overlay: 1000; --z-index-resizer-overlay: 1000;
--z-index-component-view: 1000; --z-index-component-view: 1000;
--z-index-panel-resizer: 1001; --z-index-panel-resizer: 1001;
--z-index-dropdown-menu: 1002;
--z-index-footer-bar: 2000; --z-index-footer-bar: 2000;
--z-index-footer-bar-item: 2000; --z-index-footer-bar-item: 2000;
--z-index-footer-bar-item-panel: 2000; --z-index-footer-bar-item-panel: 2000;
--z-index-dropdown-menu: 2500;
--z-index-preferences: 3000; --z-index-preferences: 3000;
--z-index-purchase-flow: 4000; --z-index-purchase-flow: 4000;
--z-index-lock-screen: 10000; --z-index-lock-screen: 10000;