feat: responsive popovers & menus (#1323)
This commit is contained in:
@@ -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,13 +49,7 @@ 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
|
|
||||||
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 ${
|
|
||||||
shouldAnimateCloseMenu ? 'slide-up-animation' : 'slide-down-animation transition-transform duration-150'
|
|
||||||
}`}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
>
|
|
||||||
<MenuPaneSelector
|
<MenuPaneSelector
|
||||||
viewControllerManager={viewControllerManager}
|
viewControllerManager={viewControllerManager}
|
||||||
application={application}
|
application={application}
|
||||||
@@ -72,7 +59,6 @@ const AccountMenu: FunctionComponent<Props> = ({
|
|||||||
closeMenu={closeAccountMenu}
|
closeMenu={closeAccountMenu}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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"
|
||||||
|
togglePopover={toggleMenu}
|
||||||
>
|
>
|
||||||
<WorkspaceSwitcherMenu
|
<WorkspaceSwitcherMenu
|
||||||
mainApplicationGroup={mainApplicationGroup}
|
mainApplicationGroup={mainApplicationGroup}
|
||||||
viewControllerManager={viewControllerManager}
|
viewControllerManager={viewControllerManager}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Popover>
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
|
||||||
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - MENU_MARGIN_FROM_APP_BORDER)
|
|
||||||
}
|
|
||||||
|
|
||||||
setPosition({
|
|
||||||
top: rect.bottom,
|
|
||||||
right: document.body.clientWidth - rect.right,
|
|
||||||
})
|
|
||||||
|
|
||||||
const newOpenState = !open
|
|
||||||
if (newOpenState && onClickPreprocessing) {
|
if (newOpenState && onClickPreprocessing) {
|
||||||
await onClickPreprocessing()
|
await onClickPreprocessing()
|
||||||
}
|
}
|
||||||
|
|
||||||
setOpen(newOpenState)
|
setIsOpen(newOpenState)
|
||||||
}
|
}, [onClickPreprocessing, isOpen])
|
||||||
}, [onClickPreprocessing, open])
|
|
||||||
|
|
||||||
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"
|
||||||
|
onClick={toggleAttachedFilesMenuWithEntitlementCheck}
|
||||||
ref={buttonRef}
|
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 ${
|
|
||||||
attachedFilesCount > 0 ? 'py-1 px-3' : ''
|
|
||||||
}`}
|
|
||||||
onBlur={closeOnBlur}
|
|
||||||
>
|
|
||||||
<VisuallyHidden>Attached files</VisuallyHidden>
|
|
||||||
<Icon type="attachment-file" className="block" />
|
|
||||||
{attachedFilesCount > 0 && <span className="ml-2 text-sm">{attachedFilesCount}</span>}
|
|
||||||
</DisclosureButton>
|
|
||||||
<DisclosurePanel
|
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
setOpen(false)
|
setIsOpen(false)
|
||||||
buttonRef.current?.focus()
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
ref={panelRef}
|
|
||||||
style={{
|
|
||||||
...position,
|
|
||||||
maxHeight,
|
|
||||||
}}
|
|
||||||
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"
|
|
||||||
onBlur={closeOnBlur}
|
|
||||||
>
|
>
|
||||||
{open && (
|
<Icon type="attachment-file" />
|
||||||
|
{attachedFilesCount > 0 && <span className="ml-2 text-sm">{attachedFilesCount}</span>}
|
||||||
|
</button>
|
||||||
|
<Popover
|
||||||
|
togglePopover={toggleAttachedFilesMenuWithEntitlementCheck}
|
||||||
|
anchorElement={buttonRef.current}
|
||||||
|
open={isOpen}
|
||||||
|
className="pt-2 md:pt-0"
|
||||||
|
>
|
||||||
<AttachedFilesPopover
|
<AttachedFilesPopover
|
||||||
application={application}
|
application={application}
|
||||||
filesController={filesController}
|
filesController={filesController}
|
||||||
attachedFiles={attachedFiles}
|
attachedFiles={attachedFiles}
|
||||||
allFiles={allFiles}
|
allFiles={allFiles}
|
||||||
closeOnBlur={closeOnBlur}
|
|
||||||
currentTab={currentTab}
|
currentTab={currentTab}
|
||||||
isDraggingFiles={isDraggingFiles}
|
isDraggingFiles={isDraggingFiles}
|
||||||
setCurrentTab={setCurrentTab}
|
setCurrentTab={setCurrentTab}
|
||||||
attachedTabDisabled={isAttachedTabDisabled}
|
attachedTabDisabled={isAttachedTabDisabled}
|
||||||
/>
|
/>
|
||||||
)}
|
</Popover>
|
||||||
</DisclosurePanel>
|
|
||||||
</Disclosure>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,72 +17,29 @@ 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}
|
||||||
onBlur={closeOnBlur}
|
onClick={toggleMenu}
|
||||||
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" />
|
||||||
</DisclosureButton>
|
</button>
|
||||||
<DisclosurePanel
|
<Popover anchorElement={menuButtonRef.current} open={isOpen} togglePopover={toggleMenu} className="py-2">
|
||||||
ref={menuRef}
|
|
||||||
style={{
|
|
||||||
...menuStyle,
|
|
||||||
position: 'fixed',
|
|
||||||
}}
|
|
||||||
className={`${
|
|
||||||
isMenuOpen ? 'flex' : 'hidden'
|
|
||||||
} max-h-120 fixed min-w-60 flex-col overflow-y-auto rounded bg-default py-1 shadow-main`}
|
|
||||||
>
|
|
||||||
{isMenuOpen && (
|
|
||||||
<>
|
|
||||||
<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={() => {
|
||||||
previewHandler(file)
|
previewHandler(file)
|
||||||
@@ -96,7 +51,6 @@ const PopoverFileSubmenu: FunctionComponent<PopoverFileSubmenuProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
{isAttachedToNote ? (
|
{isAttachedToNote ? (
|
||||||
<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={() => {
|
||||||
handleFileAction({
|
handleFileAction({
|
||||||
@@ -111,7 +65,6 @@ const PopoverFileSubmenu: FunctionComponent<PopoverFileSubmenuProps> = ({
|
|||||||
</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={() => {
|
||||||
handleFileAction({
|
handleFileAction({
|
||||||
@@ -137,7 +90,6 @@ const PopoverFileSubmenu: FunctionComponent<PopoverFileSubmenuProps> = ({
|
|||||||
},
|
},
|
||||||
}).catch(console.error)
|
}).catch(console.error)
|
||||||
}}
|
}}
|
||||||
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" />
|
||||||
@@ -151,7 +103,6 @@ const PopoverFileSubmenu: FunctionComponent<PopoverFileSubmenuProps> = ({
|
|||||||
</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={() => {
|
||||||
handleFileAction({
|
handleFileAction({
|
||||||
@@ -165,7 +116,6 @@ const PopoverFileSubmenu: FunctionComponent<PopoverFileSubmenuProps> = ({
|
|||||||
Download
|
Download
|
||||||
</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={() => {
|
||||||
setIsRenamingFile(true)
|
setIsRenamingFile(true)
|
||||||
@@ -175,7 +125,6 @@ const PopoverFileSubmenu: FunctionComponent<PopoverFileSubmenuProps> = ({
|
|||||||
Rename
|
Rename
|
||||||
</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={() => {
|
||||||
handleFileAction({
|
handleFileAction({
|
||||||
@@ -196,10 +145,7 @@ const PopoverFileSubmenu: FunctionComponent<PopoverFileSubmenuProps> = ({
|
|||||||
<span className="font-semibold">Size:</span> {formatSizeToReadableString(file.decryptedSize)}
|
<span className="font-semibold">Size:</span> {formatSizeToReadableString(file.decryptedSize)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</Popover>
|
||||||
)}
|
|
||||||
</DisclosurePanel>
|
|
||||||
</Disclosure>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,11 +26,14 @@ 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"
|
||||||
|
side="right"
|
||||||
|
togglePopover={toggleMenu}
|
||||||
>
|
>
|
||||||
<WorkspaceSwitcherMenu
|
<WorkspaceSwitcherMenu
|
||||||
mainApplicationGroup={mainApplicationGroup}
|
mainApplicationGroup={mainApplicationGroup}
|
||||||
@@ -63,8 +41,7 @@ const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainApplication
|
|||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
hideWorkspaceOptions={true}
|
hideWorkspaceOptions={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Popover>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
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()
|
await onClickPreprocessing()
|
||||||
}
|
}
|
||||||
|
setIsOpen(willMenuOpen)
|
||||||
setIsOpen(newOpenState)
|
}, [onClickPreprocessing, isOpen])
|
||||||
setTimeout(() => {
|
|
||||||
setIsVisible(newOpenState)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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}
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={closeOnBlur}
|
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
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" />
|
||||||
<Icon type="dashboard" className="block" />
|
</button>
|
||||||
</DisclosureButton>
|
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="pt-2 md:pt-0">
|
||||||
<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
|
<ChangeEditorMenu
|
||||||
closeOnBlur={closeOnBlur}
|
|
||||||
application={application}
|
application={application}
|
||||||
isVisible={isVisible}
|
isVisible={isOpen}
|
||||||
note={note}
|
note={note}
|
||||||
closeMenu={() => {
|
closeMenu={() => {
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</Popover>
|
||||||
</DisclosurePanel>
|
|
||||||
</Disclosure>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
isVisible,
|
|
||||||
note,
|
|
||||||
}) => {
|
|
||||||
const [editors] = useState<SNComponent[]>(() =>
|
|
||||||
application.componentManager.componentsForArea(ComponentArea.Editor).sort((a, b) => {
|
application.componentManager.componentsForArea(ComponentArea.Editor).sort((a, b) => {
|
||||||
return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1
|
return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1
|
||||||
}),
|
}),
|
||||||
|
[application.componentManager],
|
||||||
)
|
)
|
||||||
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">
|
||||||
|
|||||||
@@ -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(
|
||||||
|
'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',
|
||||||
|
showDisplayOptionsMenu && 'bg-contrast',
|
||||||
|
)}
|
||||||
|
onClick={toggleDisplayOptionsMenu}
|
||||||
|
ref={displayOptionsButtonRef}
|
||||||
|
>
|
||||||
<Icon type="sort-descending" />
|
<Icon type="sort-descending" />
|
||||||
</StyledDisplayOptionsButton>
|
</button>
|
||||||
<DisclosurePanel>
|
<Popover
|
||||||
{showDisplayOptionsMenu && displayOptionsMenuPosition && (
|
open={showDisplayOptionsMenu}
|
||||||
<DisplayOptionsMenuPortal
|
anchorElement={displayOptionsButtonRef.current}
|
||||||
|
togglePopover={toggleDisplayOptionsMenu}
|
||||||
|
align="start"
|
||||||
|
className="py-2"
|
||||||
|
>
|
||||||
|
<DisplayOptionsMenu
|
||||||
application={application}
|
application={application}
|
||||||
closeDisplayOptionsMenu={toggleDisplayOptionsMenu}
|
closeDisplayOptionsMenu={toggleDisplayOptionsMenu}
|
||||||
containerRef={displayOptionsContainerRef}
|
|
||||||
isOpen={showDisplayOptionsMenu}
|
|
||||||
isFilesSmartView={isFilesSmartView}
|
isFilesSmartView={isFilesSmartView}
|
||||||
top={displayOptionsMenuPosition.top}
|
isOpen={showDisplayOptionsMenu}
|
||||||
left={displayOptionsMenuPosition.left}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</Popover>
|
||||||
</DisclosurePanel>
|
|
||||||
</Disclosure>
|
|
||||||
</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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
<div ref={contextMenuRef}>
|
||||||
<FileMenuOptions
|
<FileMenuOptions
|
||||||
filesController={filesController}
|
filesController={filesController}
|
||||||
selectionController={selectionController}
|
selectionController={selectionController}
|
||||||
closeOnBlur={closeOnBlur}
|
|
||||||
closeMenu={() => setShowFileContextMenu(false)}
|
closeMenu={() => setShowFileContextMenu(false)}
|
||||||
shouldShowRenameOption={false}
|
shouldShowRenameOption={false}
|
||||||
shouldShowAttachOption={false}
|
shouldShowAttachOption={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Popover>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
setOpen(false)
|
|
||||||
buttonRef.current?.focus()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
ref={panelRef}
|
|
||||||
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 && (
|
|
||||||
<FileMenuOptions
|
<FileMenuOptions
|
||||||
filesController={filesController}
|
filesController={filesController}
|
||||||
selectionController={selectionController}
|
selectionController={selectionController}
|
||||||
closeOnBlur={closeOnBlur}
|
|
||||||
closeMenu={() => {
|
closeMenu={() => {
|
||||||
setOpen(false)
|
setIsOpen(false)
|
||||||
}}
|
}}
|
||||||
shouldShowAttachOption={false}
|
shouldShowAttachOption={false}
|
||||||
shouldShowRenameOption={false}
|
shouldShowRenameOption={false}
|
||||||
/>
|
/>
|
||||||
)}
|
</Popover>
|
||||||
</DisclosurePanel>
|
</>
|
||||||
</Disclosure>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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}
|
|
||||||
className={
|
|
||||||
(this.state.showAccountMenu ? 'bg-border' : '') +
|
|
||||||
' flex h-full w-8 cursor-pointer items-center justify-center rounded-full'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
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}
|
application={this.application}
|
||||||
|
hasError={this.state.hasError}
|
||||||
|
isOpen={this.state.showAccountMenu}
|
||||||
mainApplicationGroup={this.props.applicationGroup}
|
mainApplicationGroup={this.props.applicationGroup}
|
||||||
/>
|
onClickOutside={this.clickOutsideAccountMenu}
|
||||||
)}
|
toggleMenu={this.accountMenuClickHandler}
|
||||||
</div>
|
user={this.user}
|
||||||
<div className="sk-app-bar-item ml-0-important relative z-footer-bar-item select-none">
|
|
||||||
<div
|
|
||||||
onClick={this.quickSettingsClickHandler}
|
|
||||||
className="flex h-full w-8 cursor-pointer items-center justify-center"
|
|
||||||
>
|
|
||||||
<div className="h-5">
|
|
||||||
<Icon
|
|
||||||
type="tune"
|
|
||||||
className={(this.state.showQuickSettingsMenu ? 'text-info' : '') + ' rounded hover:text-info'}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{this.state.showQuickSettingsMenu && (
|
|
||||||
<QuickSettingsMenu
|
|
||||||
onClickOutside={this.clickOutsideQuickSettingsMenu}
|
|
||||||
viewControllerManager={this.viewControllerManager}
|
viewControllerManager={this.viewControllerManager}
|
||||||
application={this.application}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
<div className="relative z-footer-bar-item select-none">
|
||||||
|
<QuickSettingsButton
|
||||||
|
isOpen={this.state.showQuickSettingsMenu}
|
||||||
|
toggleMenu={this.quickSettingsClickHandler}
|
||||||
|
application={this.application}
|
||||||
|
preferencesController={this.viewControllerManager.preferencesController}
|
||||||
|
quickSettingsMenuController={this.viewControllerManager.quickSettingsMenuController}
|
||||||
|
/>
|
||||||
</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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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)}
|
||||||
>
|
>
|
||||||
|
<div ref={contextMenuRef}>
|
||||||
<NotesOptions
|
<NotesOptions
|
||||||
application={application}
|
application={application}
|
||||||
closeOnBlur={closeOnBlur}
|
|
||||||
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)
|
||||||
|
|||||||
@@ -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,56 +15,24 @@ 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">
|
||||||
@@ -73,28 +40,19 @@ const AddTagOption: FunctionComponent<Props> = ({ navigationController, notesCon
|
|||||||
Add tag
|
Add tag
|
||||||
</div>
|
</div>
|
||||||
<Icon type="chevron-right" className="text-neutral" />
|
<Icon type="chevron-right" className="text-neutral" />
|
||||||
</DisclosureButton>
|
</button>
|
||||||
<DisclosurePanel
|
<Popover
|
||||||
ref={menuRef}
|
togglePopover={toggleMenu}
|
||||||
onKeyDown={(event) => {
|
anchorElement={buttonRef.current}
|
||||||
if (event.key === 'Escape') {
|
open={isOpen}
|
||||||
setIsMenuOpen(false)
|
side="right"
|
||||||
menuButtonRef.current?.focus()
|
align="start"
|
||||||
}
|
className="py-2"
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
...menuStyle,
|
|
||||||
position: 'fixed',
|
|
||||||
}}
|
|
||||||
className={`${
|
|
||||||
isMenuOpen ? 'flex' : 'hidden'
|
|
||||||
} max-h-120 fixed min-w-80 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main`}
|
|
||||||
>
|
>
|
||||||
{navigationController.tags.map((tag) => (
|
{navigationController.tags.map((tag) => (
|
||||||
<button
|
<button
|
||||||
key={tag.uuid}
|
key={tag.uuid}
|
||||||
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"
|
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={() => {
|
onClick={() => {
|
||||||
notesController.isTagInSelectedNotes(tag)
|
notesController.isTagInSelectedNotes(tag)
|
||||||
? notesController.removeTagFromSelectedNotes(tag).catch(console.error)
|
? notesController.removeTagFromSelectedNotes(tag).catch(console.error)
|
||||||
@@ -109,8 +67,7 @@ const AddTagOption: FunctionComponent<Props> = ({ navigationController, notesCon
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</DisclosurePanel>
|
</Popover>
|
||||||
</Disclosure>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,55 +13,22 @@ 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)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={closeOnBlur}
|
|
||||||
ref={buttonRef}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
@@ -72,34 +37,24 @@ const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ applic
|
|||||||
Change note type
|
Change note type
|
||||||
</div>
|
</div>
|
||||||
<Icon type="chevron-right" className="text-neutral" />
|
<Icon type="chevron-right" className="text-neutral" />
|
||||||
</DisclosureButton>
|
</button>
|
||||||
<DisclosurePanel
|
<Popover
|
||||||
ref={menuRef}
|
align="start"
|
||||||
onKeyDown={(event) => {
|
anchorElement={buttonRef.current}
|
||||||
if (event.key === KeyboardKey.Escape) {
|
className="pt-2 md:pt-0"
|
||||||
setIsOpen(false)
|
open={isOpen}
|
||||||
buttonRef.current?.focus()
|
side="right"
|
||||||
}
|
togglePopover={toggleMenu}
|
||||||
}}
|
|
||||||
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
|
<ChangeEditorMenu
|
||||||
application={application}
|
application={application}
|
||||||
closeOnBlur={closeOnBlur}
|
|
||||||
note={note}
|
note={note}
|
||||||
isVisible={isVisible}
|
isVisible={isOpen}
|
||||||
closeMenu={() => {
|
closeMenu={() => {
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
</Popover>
|
||||||
</DisclosurePanel>
|
|
||||||
</Disclosure>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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,51 +13,24 @@ 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) {
|
||||||
|
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"
|
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">
|
||||||
@@ -66,22 +38,17 @@ const ListedActionsOption: FunctionComponent<Props> = ({ application, note }) =>
|
|||||||
Listed actions
|
Listed actions
|
||||||
</div>
|
</div>
|
||||||
<Icon type="chevron-right" className="text-neutral" />
|
<Icon type="chevron-right" className="text-neutral" />
|
||||||
</DisclosureButton>
|
</button>
|
||||||
<DisclosurePanel
|
<Popover
|
||||||
ref={menuRef}
|
togglePopover={toggleMenu}
|
||||||
style={{
|
anchorElement={buttonRef.current}
|
||||||
...menuStyle,
|
open={isOpen}
|
||||||
position: 'fixed',
|
side="right"
|
||||||
}}
|
align="end"
|
||||||
className={`${
|
className="pt-2 md:pt-0"
|
||||||
isMenuOpen ? 'flex' : 'hidden'
|
|
||||||
} max-h-120 fixed min-w-68 flex-col overflow-y-auto rounded bg-default pb-1 shadow-main`}
|
|
||||||
>
|
>
|
||||||
{isMenuOpen && (
|
<ListedActionsMenu application={application} note={note} />
|
||||||
<ListedActionsMenu application={application} note={note} recalculateMenuStyle={recalculateMenuStyle} />
|
</Popover>
|
||||||
)}
|
|
||||||
</DisclosurePanel>
|
|
||||||
</Disclosure>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|
||||||
return (
|
const toggleMenu = useCallback(async () => {
|
||||||
<Disclosure
|
const willMenuOpen = !isOpen
|
||||||
open={open}
|
if (willMenuOpen && onClickPreprocessing) {
|
||||||
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()
|
await onClickPreprocessing()
|
||||||
}
|
}
|
||||||
setOpen(newOpenState)
|
setIsOpen(willMenuOpen)
|
||||||
}
|
}, [onClickPreprocessing, isOpen])
|
||||||
}}
|
|
||||||
>
|
return (
|
||||||
<DisclosureButton
|
<>
|
||||||
onKeyDown={(event) => {
|
<button
|
||||||
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
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
setOpen(false)
|
|
||||||
buttonRef.current?.focus()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
ref={panelRef}
|
|
||||||
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
|
<NotesOptions
|
||||||
application={application}
|
application={application}
|
||||||
navigationController={navigationController}
|
navigationController={navigationController}
|
||||||
notesController={notesController}
|
notesController={notesController}
|
||||||
noteTagsController={noteTagsController}
|
noteTagsController={noteTagsController}
|
||||||
historyModalController={historyModalController}
|
historyModalController={historyModalController}
|
||||||
closeOnBlur={closeOnBlur}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</Popover>
|
||||||
</DisclosurePanel>
|
</>
|
||||||
</Disclosure>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
}
|
||||||
36
packages/web/src/javascripts/Components/Popover/Popover.tsx
Normal file
36
packages/web/src/javascripts/Components/Popover/Popover.tsx
Normal 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
|
||||||
@@ -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
|
||||||
49
packages/web/src/javascripts/Components/Popover/Types.ts
Normal file
49
packages/web/src/javascripts/Components/Popover/Types.ts
Normal 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)
|
||||||
@@ -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
|
||||||
|
}
|
||||||
120
packages/web/src/javascripts/Components/Popover/Utils/Rect.ts
Normal file
120
packages/web/src/javascripts/Components/Popover/Utils/Rect.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -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])
|
||||||
|
}
|
||||||
24
packages/web/src/javascripts/Components/Portal/Portal.tsx
Normal file
24
packages/web/src/javascripts/Components/Portal/Portal.tsx
Normal 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
|
||||||
@@ -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,19 +187,11 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, viewCont
|
|||||||
}, [application, themes])
|
}, [application, themes])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={mainRef} className="sn-component">
|
<div ref={mainRef} onKeyDown={handleQuickSettingsKeyDown}>
|
||||||
<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 ${
|
|
||||||
shouldAnimateCloseMenu ? 'slide-up-animation' : 'slide-down-animation transition-transform duration-150'
|
|
||||||
}`}
|
|
||||||
ref={quickSettingsMenuRef}
|
|
||||||
onKeyDown={handleQuickSettingsKeyDown}
|
|
||||||
>
|
|
||||||
<div className="mt-1 mb-2 px-3 text-sm font-semibold uppercase text-text">Quick Settings</div>
|
<div className="mt-1 mb-2 px-3 text-sm font-semibold uppercase text-text">Quick Settings</div>
|
||||||
<Disclosure open={themesMenuOpen} onChange={toggleThemesMenu}>
|
<button
|
||||||
<DisclosureButton
|
onClick={toggleThemesMenu}
|
||||||
onKeyDown={handleBtnKeyDown}
|
onKeyDown={handleBtnKeyDown}
|
||||||
onBlur={closeOnBlur}
|
|
||||||
ref={themesButtonRef}
|
ref={themesButtonRef}
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
@@ -226,38 +200,28 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, viewCont
|
|||||||
Themes
|
Themes
|
||||||
</div>
|
</div>
|
||||||
<Icon type="chevron-right" className="text-neutral" />
|
<Icon type="chevron-right" className="text-neutral" />
|
||||||
</DisclosureButton>
|
</button>
|
||||||
<DisclosurePanel
|
<Popover
|
||||||
onBlur={closeOnBlur}
|
togglePopover={toggleThemesMenu}
|
||||||
ref={themesMenuRef}
|
anchorElement={themesButtonRef.current}
|
||||||
onKeyDown={handlePanelKeyDown}
|
open={themesMenuOpen}
|
||||||
style={{
|
side="right"
|
||||||
...themesMenuPosition,
|
align="end"
|
||||||
}}
|
className="py-2"
|
||||||
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>
|
<div className="my-1 px-3 text-sm font-semibold uppercase text-text">Themes</div>
|
||||||
<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={toggleDefaultTheme}
|
onClick={toggleDefaultTheme}
|
||||||
onBlur={closeOnBlur}
|
|
||||||
ref={defaultThemeButtonRef}
|
ref={defaultThemeButtonRef}
|
||||||
>
|
>
|
||||||
<RadioIndicator checked={defaultThemeOn} className="mr-2" />
|
<RadioIndicator checked={defaultThemeOn} className="mr-2" />
|
||||||
Default
|
Default
|
||||||
</button>
|
</button>
|
||||||
{themes.map((theme) => (
|
{themes.map((theme) => (
|
||||||
<ThemesMenuButton
|
<ThemesMenuButton item={theme} application={application} key={theme.component?.uuid ?? theme.identifier} />
|
||||||
item={theme}
|
|
||||||
application={application}
|
|
||||||
key={theme.component?.uuid ?? theme.identifier}
|
|
||||||
onBlur={closeOnBlur}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</DisclosurePanel>
|
</Popover>
|
||||||
</Disclosure>
|
|
||||||
{toggleableComponents.map((component) => (
|
{toggleableComponents.map((component) => (
|
||||||
<button
|
<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"
|
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"
|
||||||
@@ -289,7 +253,6 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, viewCont
|
|||||||
Open Preferences
|
Open Preferences
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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,22 +53,15 @@ 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
|
|
||||||
a11yLabel="Tag context menu"
|
|
||||||
isOpen={contextMenuOpen}
|
|
||||||
closeMenu={() => {
|
|
||||||
navigationController.setContextMenuOpen(false)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
|
<div ref={contextMenuRef}>
|
||||||
|
<Menu a11yLabel="Tag context menu" isOpen={contextMenuOpen}>
|
||||||
<MenuItem type={MenuItemType.IconButton} className={'justify-between py-1.5'} onClick={onClickAddSubtag}>
|
<MenuItem type={MenuItemType.IconButton} className={'justify-between py-1.5'} onClick={onClickAddSubtag}>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Icon type="add" className="mr-2 text-neutral" />
|
<Icon type="add" className="mr-2 text-neutral" />
|
||||||
@@ -108,7 +91,8 @@ const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null
|
</Popover>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(TagContextMenu)
|
export default observer(TagContextMenu)
|
||||||
|
|||||||
25
packages/web/src/javascripts/Hooks/useDocumentRect.ts
Normal file
25
packages/web/src/javascripts/Hooks/useDocumentRect.ts
Normal 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
|
||||||
|
}
|
||||||
53
packages/web/src/javascripts/Hooks/useElementRect.ts
Normal file
53
packages/web/src/javascripts/Hooks/useElementRect.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user