feat: responsive popovers & menus (#1323)
This commit is contained in:
@@ -1,26 +1,24 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { useCallback, useRef, FunctionComponent, KeyboardEventHandler } from 'react'
|
||||
import { useCallback, FunctionComponent, KeyboardEventHandler } from 'react'
|
||||
import { ApplicationGroup } from '@/Application/ApplicationGroup'
|
||||
import { AccountMenuPane } from './AccountMenuPane'
|
||||
import MenuPaneSelector from './MenuPaneSelector'
|
||||
|
||||
type Props = {
|
||||
export type AccountMenuProps = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
application: WebApplication
|
||||
onClickOutside: () => void
|
||||
mainApplicationGroup: ApplicationGroup
|
||||
}
|
||||
|
||||
const AccountMenu: FunctionComponent<Props> = ({
|
||||
const AccountMenu: FunctionComponent<AccountMenuProps> = ({
|
||||
application,
|
||||
viewControllerManager,
|
||||
onClickOutside,
|
||||
mainApplicationGroup,
|
||||
}) => {
|
||||
const { currentPane, shouldAnimateCloseMenu } = viewControllerManager.accountMenuController
|
||||
const { currentPane } = viewControllerManager.accountMenuController
|
||||
|
||||
const closeAccountMenu = useCallback(() => {
|
||||
viewControllerManager.accountMenuController.closeAccountMenu()
|
||||
@@ -33,11 +31,6 @@ const AccountMenu: FunctionComponent<Props> = ({
|
||||
[viewControllerManager],
|
||||
)
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useCloseOnClickOutside(ref, () => {
|
||||
onClickOutside()
|
||||
})
|
||||
|
||||
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = useCallback(
|
||||
(event) => {
|
||||
switch (event.key) {
|
||||
@@ -56,22 +49,15 @@ const AccountMenu: FunctionComponent<Props> = ({
|
||||
)
|
||||
|
||||
return (
|
||||
<div ref={ref} id="account-menu" className="sn-component">
|
||||
<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
|
||||
viewControllerManager={viewControllerManager}
|
||||
application={application}
|
||||
mainApplicationGroup={mainApplicationGroup}
|
||||
menuPane={currentPane}
|
||||
setMenuPane={setCurrentPane}
|
||||
closeMenu={closeAccountMenu}
|
||||
/>
|
||||
</div>
|
||||
<div id="account-menu" className="sn-component" onKeyDown={handleKeyDown}>
|
||||
<MenuPaneSelector
|
||||
viewControllerManager={viewControllerManager}
|
||||
application={application}
|
||||
mainApplicationGroup={mainApplicationGroup}
|
||||
menuPane={currentPane}
|
||||
setMenuPane={setCurrentPane}
|
||||
closeMenu={closeAccountMenu}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ const WorkspaceSwitcherMenu: FunctionComponent<Props> = ({
|
||||
isOpen,
|
||||
hideWorkspaceOptions = false,
|
||||
}: Props) => {
|
||||
const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>([])
|
||||
const [applicationDescriptors, setApplicationDescriptors] = useState<ApplicationDescriptor[]>(
|
||||
mainApplicationGroup.getDescriptors(),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const applicationDescriptors = mainApplicationGroup.getDescriptors()
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||
import { ApplicationGroup } from '@/Application/ApplicationGroup'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
|
||||
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 WorkspaceSwitcherMenu from './WorkspaceSwitcherMenu'
|
||||
import MenuItem from '@/Components/Menu/MenuItem'
|
||||
import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
||||
import Popover from '@/Components/Popover/Popover'
|
||||
|
||||
type Props = {
|
||||
mainApplicationGroup: ApplicationGroup
|
||||
@@ -16,32 +16,11 @@ type Props = {
|
||||
|
||||
const WorkspaceSwitcherOption: FunctionComponent<Props> = ({ mainApplicationGroup, viewControllerManager }) => {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>()
|
||||
|
||||
const toggleMenu = useCallback(() => {
|
||||
if (!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])
|
||||
setIsOpen((isOpen) => !isOpen)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -58,19 +37,20 @@ const WorkspaceSwitcherOption: FunctionComponent<Props> = ({ mainApplicationGrou
|
||||
</div>
|
||||
<Icon type="chevron-right" className="text-neutral" />
|
||||
</MenuItem>
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="max-h-120 fixed min-w-68 overflow-y-auto rounded bg-default py-2 shadow-main"
|
||||
style={menuStyle}
|
||||
>
|
||||
<WorkspaceSwitcherMenu
|
||||
mainApplicationGroup={mainApplicationGroup}
|
||||
viewControllerManager={viewControllerManager}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Popover
|
||||
align="end"
|
||||
anchorElement={buttonRef.current}
|
||||
className="py-2"
|
||||
open={isOpen}
|
||||
side="right"
|
||||
togglePopover={toggleMenu}
|
||||
>
|
||||
<WorkspaceSwitcherMenu
|
||||
mainApplicationGroup={mainApplicationGroup}
|
||||
viewControllerManager={viewControllerManager}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
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 { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import AttachedFilesPopover from './AttachedFilesPopover'
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import { PopoverTabs } from './PopoverTabs'
|
||||
@@ -18,6 +14,8 @@ import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import { useFileDragNDrop } from '@/Components/FileDragNDropProvider/FileDragNDropProvider'
|
||||
import { FileItem, SNNote } from '@standardnotes/snjs'
|
||||
import { addToast, ToastType } from '@standardnotes/toast'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import Popover from '../Popover/Popover'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -34,7 +32,6 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
|
||||
application,
|
||||
featuresController,
|
||||
filesController,
|
||||
filePreviewModalController,
|
||||
navigationController,
|
||||
notesController,
|
||||
selectionController,
|
||||
@@ -46,24 +43,9 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
|
||||
const premiumModal = usePremiumModal()
|
||||
const note: SNNote | undefined = notesController.firstSelectedNote
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [position, setPosition] = useState({
|
||||
top: 0,
|
||||
right: 0,
|
||||
})
|
||||
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const panelRef = 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(
|
||||
navigationController.isInFilesView ? PopoverTabs.AllFiles : PopoverTabs.AttachedFiles,
|
||||
@@ -78,29 +60,14 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
|
||||
}, [currentTab, isAttachedTabDisabled])
|
||||
|
||||
const toggleAttachedFilesMenu = useCallback(async () => {
|
||||
const rect = buttonRef.current?.getBoundingClientRect()
|
||||
if (rect) {
|
||||
const { clientHeight } = document.documentElement
|
||||
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
|
||||
const footerHeightInPx = footerElementRect?.height
|
||||
const newOpenState = !isOpen
|
||||
|
||||
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) {
|
||||
await onClickPreprocessing()
|
||||
}
|
||||
|
||||
setOpen(newOpenState)
|
||||
if (newOpenState && onClickPreprocessing) {
|
||||
await onClickPreprocessing()
|
||||
}
|
||||
}, [onClickPreprocessing, open])
|
||||
|
||||
setIsOpen(newOpenState)
|
||||
}, [onClickPreprocessing, isOpen])
|
||||
|
||||
const prospectivelyShowFilesPremiumModal = useCallback(() => {
|
||||
if (!featuresController.hasFiles) {
|
||||
@@ -132,10 +99,10 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
|
||||
const { isDraggingFiles, addFilesDragInCallback, addFilesDropCallback } = useFileDragNDrop()
|
||||
|
||||
useEffect(() => {
|
||||
if (isDraggingFiles && !open) {
|
||||
if (isDraggingFiles && !isOpen) {
|
||||
void toggleAttachedFilesMenu()
|
||||
}
|
||||
}, [isDraggingFiles, open, toggleAttachedFilesMenu])
|
||||
}, [isDraggingFiles, isOpen, toggleAttachedFilesMenu])
|
||||
|
||||
const filesDragInCallback = useCallback((tab: PopoverTabs) => {
|
||||
setCurrentTab(tab)
|
||||
@@ -162,53 +129,41 @@ const AttachedFilesButton: FunctionComponent<Props> = ({
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<Disclosure open={open} onChange={toggleAttachedFilesMenuWithEntitlementCheck}>
|
||||
<DisclosureButton
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setOpen(false)
|
||||
}
|
||||
}}
|
||||
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) => {
|
||||
if (event.key === 'Escape') {
|
||||
setOpen(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 && (
|
||||
<AttachedFilesPopover
|
||||
application={application}
|
||||
filesController={filesController}
|
||||
attachedFiles={attachedFiles}
|
||||
allFiles={allFiles}
|
||||
closeOnBlur={closeOnBlur}
|
||||
currentTab={currentTab}
|
||||
isDraggingFiles={isDraggingFiles}
|
||||
setCurrentTab={setCurrentTab}
|
||||
attachedTabDisabled={isAttachedTabDisabled}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
<button
|
||||
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',
|
||||
attachedFilesCount > 0 ? 'py-1 px-3' : '',
|
||||
)}
|
||||
title="Attached files"
|
||||
aria-label="Attached files"
|
||||
onClick={toggleAttachedFilesMenuWithEntitlementCheck}
|
||||
ref={buttonRef}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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
|
||||
application={application}
|
||||
filesController={filesController}
|
||||
attachedFiles={attachedFiles}
|
||||
allFiles={allFiles}
|
||||
currentTab={currentTab}
|
||||
isDraggingFiles={isDraggingFiles}
|
||||
setCurrentTab={setCurrentTab}
|
||||
attachedTabDisabled={isAttachedTabDisabled}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ type Props = {
|
||||
filesController: FilesController
|
||||
allFiles: FileItem[]
|
||||
attachedFiles: FileItem[]
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
|
||||
currentTab: PopoverTabs
|
||||
isDraggingFiles: boolean
|
||||
setCurrentTab: Dispatch<SetStateAction<PopoverTabs>>
|
||||
@@ -28,7 +27,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
|
||||
filesController,
|
||||
allFiles,
|
||||
attachedFiles,
|
||||
closeOnBlur,
|
||||
currentTab,
|
||||
isDraggingFiles,
|
||||
setCurrentTab,
|
||||
@@ -87,7 +85,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
|
||||
onClick={() => {
|
||||
setCurrentTab(PopoverTabs.AttachedFiles)
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
disabled={attachedTabDisabled}
|
||||
>
|
||||
Attached
|
||||
@@ -100,7 +97,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
|
||||
onClick={() => {
|
||||
setCurrentTab(PopoverTabs.AllFiles)
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
All files
|
||||
</button>
|
||||
@@ -117,7 +113,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
|
||||
onInput={(e) => {
|
||||
setSearchQuery((e.target as HTMLInputElement).value)
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={searchInputRef}
|
||||
/>
|
||||
{searchQuery.length > 0 && (
|
||||
@@ -127,7 +122,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
|
||||
setSearchQuery('')
|
||||
searchInputRef.current?.focus()
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<Icon type="clear-circle-filled" className="text-neutral" />
|
||||
</button>
|
||||
@@ -144,7 +138,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
|
||||
isAttachedToNote={attachedFiles.includes(file)}
|
||||
handleFileAction={filesController.handleFileAction}
|
||||
getIconType={application.iconsController.getIconForFileType}
|
||||
closeOnBlur={closeOnBlur}
|
||||
previewHandler={previewHandler}
|
||||
/>
|
||||
)
|
||||
@@ -161,7 +154,7 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
|
||||
? 'No files attached to this note'
|
||||
: 'No files found in this account'}
|
||||
</div>
|
||||
<Button onClick={handleAttachFilesClick} onBlur={closeOnBlur}>
|
||||
<Button onClick={handleAttachFilesClick}>
|
||||
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files
|
||||
</Button>
|
||||
<div className="mt-3 text-xs text-passive-0">Or drop your files here</div>
|
||||
@@ -172,7 +165,6 @@ const AttachedFilesPopover: FunctionComponent<Props> = ({
|
||||
<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"
|
||||
onClick={handleAttachFilesClick}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<Icon type="add" className="mr-2 text-neutral" />
|
||||
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files
|
||||
|
||||
@@ -22,7 +22,6 @@ const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
|
||||
isAttachedToNote,
|
||||
handleFileAction,
|
||||
getIconType,
|
||||
closeOnBlur,
|
||||
previewHandler,
|
||||
}) => {
|
||||
const [fileName, setFileName] = useState(file.name)
|
||||
@@ -116,7 +115,6 @@ const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
|
||||
isAttachedToNote={isAttachedToNote}
|
||||
handleFileAction={handleFileAction}
|
||||
setIsRenamingFile={setIsRenamingFile}
|
||||
closeOnBlur={closeOnBlur}
|
||||
previewHandler={previewHandler}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,6 @@ type CommonProps = {
|
||||
handleFileAction: (action: PopoverFileItemAction) => Promise<{
|
||||
didHandleAction: boolean
|
||||
}>
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
|
||||
previewHandler: (file: FileItem) => void
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useRef, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import { PopoverFileSubmenuProps } from './PopoverFileItemProps'
|
||||
import { PopoverFileItemActionType } from './PopoverFileItemAction'
|
||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||
import Popover from '../Popover/Popover'
|
||||
|
||||
const PopoverFileSubmenu: FunctionComponent<PopoverFileSubmenuProps> = ({
|
||||
file,
|
||||
@@ -19,187 +17,135 @@ const PopoverFileSubmenu: FunctionComponent<PopoverFileSubmenuProps> = ({
|
||||
}) => {
|
||||
const menuContainerRef = useRef<HTMLDivElement>(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 [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
maxHeight: 'auto',
|
||||
})
|
||||
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen)
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
setIsMenuOpen(false)
|
||||
setIsOpen(false)
|
||||
}, [])
|
||||
|
||||
const toggleMenu = 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)
|
||||
}
|
||||
setIsOpen((isOpen) => !isOpen)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isMenuOpen) {
|
||||
setTimeout(() => {
|
||||
recalculateMenuStyle()
|
||||
})
|
||||
}
|
||||
}, [isMenuOpen, recalculateMenuStyle])
|
||||
|
||||
return (
|
||||
<div ref={menuContainerRef}>
|
||||
<Disclosure open={isMenuOpen} onChange={toggleMenu}>
|
||||
<DisclosureButton
|
||||
ref={menuButtonRef}
|
||||
onBlur={closeOnBlur}
|
||||
className="h-7 w-7 cursor-pointer rounded-full border-0 bg-transparent p-1 hover:bg-contrast"
|
||||
>
|
||||
<Icon type="more" className="text-neutral" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
ref={menuRef}
|
||||
style={{
|
||||
...menuStyle,
|
||||
position: 'fixed',
|
||||
<button
|
||||
ref={menuButtonRef}
|
||||
onClick={toggleMenu}
|
||||
className="h-7 w-7 cursor-pointer rounded-full border-0 bg-transparent p-1 hover:bg-contrast"
|
||||
>
|
||||
<Icon type="more" className="text-neutral" />
|
||||
</button>
|
||||
<Popover anchorElement={menuButtonRef.current} open={isOpen} togglePopover={toggleMenu} className="py-2">
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={() => {
|
||||
previewHandler(file)
|
||||
closeMenu()
|
||||
}}
|
||||
className={`${
|
||||
isMenuOpen ? 'flex' : 'hidden'
|
||||
} max-h-120 fixed min-w-60 flex-col overflow-y-auto rounded bg-default py-1 shadow-main`}
|
||||
>
|
||||
{isMenuOpen && (
|
||||
<>
|
||||
<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"
|
||||
onClick={() => {
|
||||
previewHandler(file)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="file" className="mr-2 text-neutral" />
|
||||
Preview file
|
||||
</button>
|
||||
{isAttachedToNote ? (
|
||||
<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"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.DetachFileToNote,
|
||||
payload: { file },
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="link-off" className="mr-2 text-neutral" />
|
||||
Detach from note
|
||||
</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"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.AttachFileToNote,
|
||||
payload: { file },
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="link" className="mr-2 text-neutral" />
|
||||
Attach to note
|
||||
</button>
|
||||
)}
|
||||
<HorizontalSeparator classes="my-1" />
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.ToggleFileProtection,
|
||||
payload: { file },
|
||||
callback: (isProtected: boolean) => {
|
||||
setIsFileProtected(isProtected)
|
||||
},
|
||||
}).catch(console.error)
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<Icon type="password" className="mr-2 text-neutral" />
|
||||
Password protection
|
||||
</span>
|
||||
<Switch
|
||||
className="pointer-events-none px-0"
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
checked={isFileProtected}
|
||||
/>
|
||||
</button>
|
||||
<HorizontalSeparator classes="my-1" />
|
||||
<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"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.DownloadFile,
|
||||
payload: { file },
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="download" className="mr-2 text-neutral" />
|
||||
Download
|
||||
</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"
|
||||
onClick={() => {
|
||||
setIsRenamingFile(true)
|
||||
}}
|
||||
>
|
||||
<Icon type="pencil" className="mr-2 text-neutral" />
|
||||
Rename
|
||||
</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"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.DeleteFile,
|
||||
payload: { file },
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="trash" className="mr-2 text-danger" />
|
||||
<span className="text-danger">Delete permanently</span>
|
||||
</button>
|
||||
<div className="px-3 py-1 text-xs font-medium text-neutral">
|
||||
<div className="mb-1">
|
||||
<span className="font-semibold">File ID:</span> {file.uuid}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Size:</span> {formatSizeToReadableString(file.decryptedSize)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
<Icon type="file" className="mr-2 text-neutral" />
|
||||
Preview file
|
||||
</button>
|
||||
{isAttachedToNote ? (
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.DetachFileToNote,
|
||||
payload: { file },
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="link-off" className="mr-2 text-neutral" />
|
||||
Detach from note
|
||||
</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"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.AttachFileToNote,
|
||||
payload: { file },
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="link" className="mr-2 text-neutral" />
|
||||
Attach to note
|
||||
</button>
|
||||
)}
|
||||
<HorizontalSeparator classes="my-1" />
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.ToggleFileProtection,
|
||||
payload: { file },
|
||||
callback: (isProtected: boolean) => {
|
||||
setIsFileProtected(isProtected)
|
||||
},
|
||||
}).catch(console.error)
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<Icon type="password" className="mr-2 text-neutral" />
|
||||
Password protection
|
||||
</span>
|
||||
<Switch
|
||||
className="pointer-events-none px-0"
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
checked={isFileProtected}
|
||||
/>
|
||||
</button>
|
||||
<HorizontalSeparator classes="my-1" />
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.DownloadFile,
|
||||
payload: { file },
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="download" className="mr-2 text-neutral" />
|
||||
Download
|
||||
</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"
|
||||
onClick={() => {
|
||||
setIsRenamingFile(true)
|
||||
}}
|
||||
>
|
||||
<Icon type="pencil" className="mr-2 text-neutral" />
|
||||
Rename
|
||||
</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"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.DeleteFile,
|
||||
payload: { file },
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="trash" className="mr-2 text-danger" />
|
||||
<span className="text-danger">Delete permanently</span>
|
||||
</button>
|
||||
<div className="px-3 py-1 text-xs font-medium text-neutral">
|
||||
<div className="mb-1">
|
||||
<span className="font-semibold">File ID:</span> {file.uuid}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Size:</span> {formatSizeToReadableString(file.decryptedSize)}
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { ApplicationGroup } from '@/Application/ApplicationGroup'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
|
||||
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useRef, useState } from 'react'
|
||||
import WorkspaceSwitcherMenu from '@/Components/AccountMenu/WorkspaceSwitcher/WorkspaceSwitcherMenu'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
|
||||
import Popover from '../Popover/Popover'
|
||||
|
||||
type Props = {
|
||||
mainApplicationGroup: ApplicationGroup
|
||||
@@ -14,36 +13,12 @@ type Props = {
|
||||
|
||||
const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainApplicationGroup, viewControllerManager }) => {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>()
|
||||
|
||||
useCloseOnClickOutside(containerRef, () => setIsOpen(false))
|
||||
|
||||
const toggleMenu = useCallback(() => {
|
||||
if (!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])
|
||||
setIsOpen((isOpen) => !isOpen)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
@@ -51,20 +26,22 @@ const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainApplication
|
||||
<Icon type="user-switch" className="mr-2 text-neutral" />
|
||||
Switch workspace
|
||||
</Button>
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="max-h-120 fixed min-w-68 overflow-y-auto rounded-md bg-default py-2 shadow-main"
|
||||
style={menuStyle}
|
||||
>
|
||||
<WorkspaceSwitcherMenu
|
||||
mainApplicationGroup={mainApplicationGroup}
|
||||
viewControllerManager={viewControllerManager}
|
||||
isOpen={isOpen}
|
||||
hideWorkspaceOptions={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Popover
|
||||
align="center"
|
||||
anchorElement={buttonRef.current}
|
||||
className="py-2"
|
||||
open={isOpen}
|
||||
overrideZIndex="z-modal"
|
||||
side="right"
|
||||
togglePopover={toggleMenu}
|
||||
>
|
||||
<WorkspaceSwitcherMenu
|
||||
mainApplicationGroup={mainApplicationGroup}
|
||||
viewControllerManager={viewControllerManager}
|
||||
isOpen={isOpen}
|
||||
hideWorkspaceOptions={true}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
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 { FunctionComponent, useRef, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useRef, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import ChangeEditorMenu from './ChangeEditorMenu'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import Popover from '../Popover/Popover'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -22,89 +19,38 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
|
||||
}: Props) => {
|
||||
const note = viewControllerManager.notesController.firstSelectedNote
|
||||
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 panelRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [closeOnBlur] = useCloseOnBlur(containerRef, setIsOpen)
|
||||
|
||||
const toggleChangeEditorMenu = 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 - MENU_MARGIN_FROM_APP_BORDER)
|
||||
}
|
||||
|
||||
setPosition({
|
||||
top: rect.bottom,
|
||||
right: document.body.clientWidth - rect.right,
|
||||
})
|
||||
|
||||
const newOpenState = !isOpen
|
||||
if (newOpenState && onClickPreprocessing) {
|
||||
await onClickPreprocessing()
|
||||
}
|
||||
|
||||
setIsOpen(newOpenState)
|
||||
setTimeout(() => {
|
||||
setIsVisible(newOpenState)
|
||||
})
|
||||
const toggleMenu = useCallback(async () => {
|
||||
const willMenuOpen = !isOpen
|
||||
if (willMenuOpen && onClickPreprocessing) {
|
||||
await onClickPreprocessing()
|
||||
}
|
||||
}
|
||||
setIsOpen(willMenuOpen)
|
||||
}, [onClickPreprocessing, isOpen])
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<Disclosure open={isOpen} onChange={toggleChangeEditorMenu}>
|
||||
<DisclosureButton
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsOpen(false)
|
||||
}
|
||||
<button
|
||||
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="Change note type"
|
||||
aria-label="Change note type"
|
||||
onClick={toggleMenu}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<Icon type="dashboard" />
|
||||
</button>
|
||||
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="pt-2 md:pt-0">
|
||||
<ChangeEditorMenu
|
||||
application={application}
|
||||
isVisible={isOpen}
|
||||
note={note}
|
||||
closeMenu={() => {
|
||||
setIsOpen(false)
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={buttonRef}
|
||||
className="flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast"
|
||||
>
|
||||
<VisuallyHidden>Change note type</VisuallyHidden>
|
||||
<Icon type="dashboard" className="block" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsOpen(false)
|
||||
buttonRef.current?.focus()
|
||||
}
|
||||
}}
|
||||
ref={panelRef}
|
||||
style={{
|
||||
...position,
|
||||
maxHeight,
|
||||
}}
|
||||
className="slide-down-animation max-h-120 fixed flex min-w-68 max-w-xs flex-col overflow-y-auto rounded bg-default shadow-main transition-transform duration-150"
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
{isOpen && (
|
||||
<ChangeEditorMenu
|
||||
closeOnBlur={closeOnBlur}
|
||||
application={application}
|
||||
isVisible={isVisible}
|
||||
note={note}
|
||||
closeMenu={() => {
|
||||
setIsOpen(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
SNNote,
|
||||
TransactionalMutation,
|
||||
} 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 { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
|
||||
import { createEditorMenuGroups } from './createEditorMenuGroups'
|
||||
@@ -28,7 +28,6 @@ import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/Premium
|
||||
|
||||
type ChangeEditorMenuProps = {
|
||||
application: WebApplication
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
|
||||
closeMenu: () => void
|
||||
isVisible: boolean
|
||||
note: SNNote | undefined
|
||||
@@ -36,25 +35,17 @@ type ChangeEditorMenuProps = {
|
||||
|
||||
const getGroupId = (group: EditorMenuGroup) => group.title.toLowerCase().replace(/\s/, '-')
|
||||
|
||||
const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
application,
|
||||
closeOnBlur,
|
||||
closeMenu,
|
||||
isVisible,
|
||||
note,
|
||||
}) => {
|
||||
const [editors] = useState<SNComponent[]>(() =>
|
||||
application.componentManager.componentsForArea(ComponentArea.Editor).sort((a, b) => {
|
||||
return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1
|
||||
}),
|
||||
const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({ application, closeMenu, isVisible, note }) => {
|
||||
const editors = useMemo(
|
||||
() =>
|
||||
application.componentManager.componentsForArea(ComponentArea.Editor).sort((a, b) => {
|
||||
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>()
|
||||
|
||||
useEffect(() => {
|
||||
setGroups(createEditorMenuGroups(application, editors))
|
||||
}, [application, editors])
|
||||
|
||||
useEffect(() => {
|
||||
if (note) {
|
||||
setCurrentEditor(application.componentManager.editorForNote(note))
|
||||
@@ -195,7 +186,6 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={onClickEditorItem}
|
||||
className={'flex-row-reverse py-2'}
|
||||
onBlur={closeOnBlur}
|
||||
checked={item.isEntitled ? isSelectedEditor(item) : undefined}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between">
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { Disclosure, DisclosurePanel } from '@reach/disclosure'
|
||||
import { memo, useCallback, useRef, useState } from 'react'
|
||||
import Icon from '../../Icon/Icon'
|
||||
import { DisplayOptionsMenuPositionProps } from './DisplayOptionsMenuProps'
|
||||
import DisplayOptionsMenuPortal from './DisplayOptionsMenuPortal'
|
||||
import StyledDisplayOptionsButton from './StyledDisplayOptionsButton'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import Popover from '@/Components/Popover/Popover'
|
||||
import DisplayOptionsMenu from './DisplayOptionsMenu'
|
||||
|
||||
type Props = {
|
||||
application: {
|
||||
@@ -26,21 +25,12 @@ const ContentListHeader = ({
|
||||
isFilesSmartView,
|
||||
optionsSubtitle,
|
||||
}: Props) => {
|
||||
const [displayOptionsMenuPosition, setDisplayOptionsMenuPosition] = useState<DisplayOptionsMenuPositionProps>()
|
||||
const displayOptionsContainerRef = useRef<HTMLDivElement>(null)
|
||||
const displayOptionsButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false)
|
||||
|
||||
const toggleDisplayOptionsMenu = useCallback(() => {
|
||||
if (displayOptionsButtonRef.current) {
|
||||
const buttonBoundingRect = displayOptionsButtonRef.current.getBoundingClientRect()
|
||||
setDisplayOptionsMenuPosition({
|
||||
top: buttonBoundingRect.bottom,
|
||||
left: buttonBoundingRect.right - buttonBoundingRect.width,
|
||||
})
|
||||
}
|
||||
|
||||
setShowDisplayOptionsMenu((show) => !show)
|
||||
}, [])
|
||||
|
||||
@@ -52,24 +42,30 @@ const ContentListHeader = ({
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="relative" ref={displayOptionsContainerRef}>
|
||||
<Disclosure open={showDisplayOptionsMenu} onChange={toggleDisplayOptionsMenu}>
|
||||
<StyledDisplayOptionsButton $pressed={showDisplayOptionsMenu} ref={displayOptionsButtonRef}>
|
||||
<Icon type="sort-descending" />
|
||||
</StyledDisplayOptionsButton>
|
||||
<DisclosurePanel>
|
||||
{showDisplayOptionsMenu && displayOptionsMenuPosition && (
|
||||
<DisplayOptionsMenuPortal
|
||||
application={application}
|
||||
closeDisplayOptionsMenu={toggleDisplayOptionsMenu}
|
||||
containerRef={displayOptionsContainerRef}
|
||||
isOpen={showDisplayOptionsMenu}
|
||||
isFilesSmartView={isFilesSmartView}
|
||||
top={displayOptionsMenuPosition.top}
|
||||
left={displayOptionsMenuPosition.left}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
<button
|
||||
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" />
|
||||
</button>
|
||||
<Popover
|
||||
open={showDisplayOptionsMenu}
|
||||
anchorElement={displayOptionsButtonRef.current}
|
||||
togglePopover={toggleDisplayOptionsMenu}
|
||||
align="start"
|
||||
className="py-2"
|
||||
>
|
||||
<DisplayOptionsMenu
|
||||
application={application}
|
||||
closeDisplayOptionsMenu={toggleDisplayOptionsMenu}
|
||||
isFilesSmartView={isFilesSmartView}
|
||||
isOpen={showDisplayOptionsMenu}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
<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"
|
||||
|
||||
@@ -97,14 +97,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
||||
}, [application, hideEditorIcon])
|
||||
|
||||
return (
|
||||
<Menu
|
||||
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}
|
||||
>
|
||||
<Menu className="text-sm" 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>
|
||||
<MenuItem
|
||||
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 { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
|
||||
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'
|
||||
|
||||
type Props = {
|
||||
@@ -15,92 +13,27 @@ type Props = {
|
||||
const FileContextMenu: FunctionComponent<Props> = observer(({ filesController, selectionController }) => {
|
||||
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 [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 (
|
||||
<div
|
||||
ref={contextMenuRef}
|
||||
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"
|
||||
style={{
|
||||
...contextMenuStyle,
|
||||
maxHeight: contextMenuMaxHeight,
|
||||
}}
|
||||
<Popover
|
||||
open={showFileContextMenu}
|
||||
anchorPoint={fileContextMenuLocation}
|
||||
togglePopover={() => setShowFileContextMenu(!showFileContextMenu)}
|
||||
side="right"
|
||||
align="start"
|
||||
className="py-2"
|
||||
>
|
||||
<FileMenuOptions
|
||||
filesController={filesController}
|
||||
selectionController={selectionController}
|
||||
closeOnBlur={closeOnBlur}
|
||||
closeMenu={() => setShowFileContextMenu(false)}
|
||||
shouldShowRenameOption={false}
|
||||
shouldShowAttachOption={false}
|
||||
/>
|
||||
</div>
|
||||
<div ref={contextMenuRef}>
|
||||
<FileMenuOptions
|
||||
filesController={filesController}
|
||||
selectionController={selectionController}
|
||||
closeMenu={() => setShowFileContextMenu(false)}
|
||||
shouldShowRenameOption={false}
|
||||
shouldShowAttachOption={false}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||
|
||||
type Props = {
|
||||
closeMenu: () => void
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
|
||||
filesController: FilesController
|
||||
selectionController: SelectedItemsController
|
||||
isFileAttachedToNote?: boolean
|
||||
@@ -22,7 +21,6 @@ type Props = {
|
||||
|
||||
const FileMenuOptions: FunctionComponent<Props> = ({
|
||||
closeMenu,
|
||||
closeOnBlur,
|
||||
filesController,
|
||||
selectionController,
|
||||
isFileAttachedToNote,
|
||||
@@ -73,7 +71,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
||||
return (
|
||||
<>
|
||||
<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"
|
||||
onClick={onPreview}
|
||||
>
|
||||
@@ -84,7 +81,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
||||
<>
|
||||
{isFileAttachedToNote ? (
|
||||
<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"
|
||||
onClick={onDetach}
|
||||
>
|
||||
@@ -93,7 +89,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
||||
</button>
|
||||
) : shouldShowAttachOption ? (
|
||||
<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"
|
||||
onClick={onAttach}
|
||||
>
|
||||
@@ -109,7 +104,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
||||
onClick={() => {
|
||||
void filesController.setProtectionForFiles(!hasProtectedFiles, selectionController.selectedFiles)
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<Icon type="password" className="mr-2 text-neutral" />
|
||||
@@ -123,7 +117,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
||||
</button>
|
||||
<HorizontalSeparator classes="my-1" />
|
||||
<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"
|
||||
onClick={() => {
|
||||
void filesController.downloadFiles(selectionController.selectedFiles)
|
||||
@@ -134,7 +127,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
||||
</button>
|
||||
{shouldShowRenameOption && (
|
||||
<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"
|
||||
onClick={() => {
|
||||
renameToggleCallback?.(true)
|
||||
@@ -145,7 +137,6 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
||||
</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"
|
||||
onClick={() => {
|
||||
void filesController.deleteFilesPermanently(selectionController.selectedFiles)
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
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 { observer } from 'mobx-react-lite'
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||
import FileMenuOptions from './FileMenuOptions'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import Popover from '../Popover/Popover'
|
||||
|
||||
type Props = {
|
||||
filesController: FilesController
|
||||
@@ -15,80 +12,34 @@ type Props = {
|
||||
}
|
||||
|
||||
const FilesOptionsPanel = ({ filesController, selectionController }: Props) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [position, setPosition] = useState({
|
||||
top: 0,
|
||||
right: 0,
|
||||
})
|
||||
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const panelRef = useRef<HTMLDivElement>(null)
|
||||
const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen)
|
||||
|
||||
const onDisclosureChange = useCallback(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,
|
||||
})
|
||||
setOpen((open) => !open)
|
||||
}
|
||||
}, [])
|
||||
const toggleMenu = useCallback(() => setIsOpen((isOpen) => !isOpen), [])
|
||||
|
||||
return (
|
||||
<Disclosure open={open} onChange={onDisclosureChange}>
|
||||
<DisclosureButton
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setOpen(false)
|
||||
}
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={buttonRef}
|
||||
<>
|
||||
<button
|
||||
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" className="block" />
|
||||
</DisclosureButton>
|
||||
<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
|
||||
filesController={filesController}
|
||||
selectionController={selectionController}
|
||||
closeOnBlur={closeOnBlur}
|
||||
closeMenu={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
shouldShowAttachOption={false}
|
||||
shouldShowRenameOption={false}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
<Icon type="more" />
|
||||
</button>
|
||||
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="py-2">
|
||||
<FileMenuOptions
|
||||
filesController={filesController}
|
||||
selectionController={selectionController}
|
||||
closeMenu={() => {
|
||||
setIsOpen(false)
|
||||
}}
|
||||
shouldShowAttachOption={false}
|
||||
shouldShowRenameOption={false}
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
} from '@/Constants/Strings'
|
||||
import { alertDialog, confirmDialog } from '@/Services/AlertService'
|
||||
import AccountMenu from '@/Components/AccountMenu/AccountMenu'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import QuickSettingsMenu from '@/Components/QuickSettingsMenu/QuickSettingsMenu'
|
||||
import SyncResolutionMenu from '@/Components/SyncResolutionMenu/SyncResolutionMenu'
|
||||
import { Fragment } from 'react'
|
||||
import { AccountMenuPane } from '../AccountMenu/AccountMenuPane'
|
||||
import { EditorEventSource } from '@/Types/EditorEventSource'
|
||||
import QuickSettingsButton from './QuickSettingsButton'
|
||||
import AccountMenuButton from './AccountMenuButton'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -287,12 +287,10 @@ class Footer extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
accountMenuClickHandler = () => {
|
||||
this.viewControllerManager.quickSettingsMenuController.closeQuickSettingsMenu()
|
||||
this.viewControllerManager.accountMenuController.toggleShow()
|
||||
}
|
||||
|
||||
quickSettingsClickHandler = () => {
|
||||
this.viewControllerManager.accountMenuController.closeAccountMenu()
|
||||
this.viewControllerManager.quickSettingsMenuController.toggle()
|
||||
}
|
||||
|
||||
@@ -342,55 +340,31 @@ class Footer extends PureComponent<Props, State> {
|
||||
override render() {
|
||||
return (
|
||||
<div className="sn-component">
|
||||
<div
|
||||
<footer
|
||||
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"
|
||||
>
|
||||
<div className="left flex h-full">
|
||||
<div className="sk-app-bar-item relative z-footer-bar-item ml-0 select-none">
|
||||
<div
|
||||
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}
|
||||
mainApplicationGroup={this.props.applicationGroup}
|
||||
/>
|
||||
)}
|
||||
<AccountMenuButton
|
||||
application={this.application}
|
||||
hasError={this.state.hasError}
|
||||
isOpen={this.state.showAccountMenu}
|
||||
mainApplicationGroup={this.props.applicationGroup}
|
||||
onClickOutside={this.clickOutsideAccountMenu}
|
||||
toggleMenu={this.accountMenuClickHandler}
|
||||
user={this.user}
|
||||
viewControllerManager={this.viewControllerManager}
|
||||
/>
|
||||
</div>
|
||||
<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}
|
||||
application={this.application}
|
||||
/>
|
||||
)}
|
||||
<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>
|
||||
{this.state.showBetaWarning && (
|
||||
<Fragment>
|
||||
@@ -454,7 +428,7 @@ class Footer extends PureComponent<Props, State> {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</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 NotesOptions from '@/Components/NotesOptions/NotesOptions'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useRef } from 'react'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { NoteTagsController } from '@/Controllers/NoteTagsController'
|
||||
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
|
||||
import Popover from '../Popover/Popover'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -24,43 +23,33 @@ const NotesContextMenu = ({
|
||||
noteTagsController,
|
||||
historyModalController,
|
||||
}: Props) => {
|
||||
const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = notesController
|
||||
const { contextMenuOpen, contextMenuClickLocation } = notesController
|
||||
|
||||
const contextMenuRef = useRef<HTMLDivElement>(null)
|
||||
const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) => notesController.setContextMenuOpen(open))
|
||||
|
||||
useCloseOnClickOutside(contextMenuRef, () => notesController.setContextMenuOpen(false))
|
||||
|
||||
const reloadContextMenuLayout = useCallback(() => {
|
||||
notesController.reloadContextMenuLayout()
|
||||
}, [notesController])
|
||||
|
||||
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,
|
||||
return (
|
||||
<Popover
|
||||
align="start"
|
||||
anchorPoint={{
|
||||
x: contextMenuClickLocation.x,
|
||||
y: contextMenuClickLocation.y,
|
||||
}}
|
||||
className="py-2"
|
||||
open={contextMenuOpen}
|
||||
side="right"
|
||||
togglePopover={() => notesController.setContextMenuOpen(!contextMenuOpen)}
|
||||
>
|
||||
<NotesOptions
|
||||
application={application}
|
||||
closeOnBlur={closeOnBlur}
|
||||
navigationController={navigationController}
|
||||
notesController={notesController}
|
||||
noteTagsController={noteTagsController}
|
||||
historyModalController={historyModalController}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
<div ref={contextMenuRef}>
|
||||
<NotesOptions
|
||||
application={application}
|
||||
navigationController={navigationController}
|
||||
notesController={notesController}
|
||||
noteTagsController={noteTagsController}
|
||||
historyModalController={historyModalController}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
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 { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useRef, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { NoteTagsController } from '@/Controllers/NoteTagsController'
|
||||
import { KeyboardKey } from '@/Services/IOService'
|
||||
import Popover from '../Popover/Popover'
|
||||
|
||||
type Props = {
|
||||
navigationController: NavigationController
|
||||
@@ -16,101 +15,59 @@ type Props = {
|
||||
|
||||
const AddTagOption: FunctionComponent<Props> = ({ navigationController, notesController, noteTagsController }) => {
|
||||
const menuContainerRef = useRef<HTMLDivElement>(null)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
maxHeight: 'auto',
|
||||
})
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen)
|
||||
|
||||
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)
|
||||
}
|
||||
const toggleMenu = useCallback(() => {
|
||||
setIsOpen((isOpen) => !isOpen)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isMenuOpen) {
|
||||
setTimeout(() => {
|
||||
recalculateMenuStyle()
|
||||
})
|
||||
}
|
||||
}, [isMenuOpen, recalculateMenuStyle])
|
||||
|
||||
return (
|
||||
<div ref={menuContainerRef}>
|
||||
<Disclosure open={isMenuOpen} onChange={toggleTagsMenu}>
|
||||
<DisclosureButton
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsMenuOpen(false)
|
||||
}
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={menuButtonRef}
|
||||
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="hashtag" className="mr-2 text-neutral" />
|
||||
Add tag
|
||||
</div>
|
||||
<Icon type="chevron-right" className="text-neutral" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
ref={menuRef}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsMenuOpen(false)
|
||||
menuButtonRef.current?.focus()
|
||||
}
|
||||
}}
|
||||
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) => (
|
||||
<button
|
||||
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"
|
||||
onBlur={closeOnBlur}
|
||||
onClick={() => {
|
||||
notesController.isTagInSelectedNotes(tag)
|
||||
? notesController.removeTagFromSelectedNotes(tag).catch(console.error)
|
||||
: notesController.addTagToSelectedNotes(tag).catch(console.error)
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`overflow-hidden overflow-ellipsis whitespace-nowrap
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
onKeyDown={(event) => {
|
||||
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"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="hashtag" className="mr-2 text-neutral" />
|
||||
Add tag
|
||||
</div>
|
||||
<Icon type="chevron-right" className="text-neutral" />
|
||||
</button>
|
||||
<Popover
|
||||
togglePopover={toggleMenu}
|
||||
anchorElement={buttonRef.current}
|
||||
open={isOpen}
|
||||
side="right"
|
||||
align="start"
|
||||
className="py-2"
|
||||
>
|
||||
{navigationController.tags.map((tag) => (
|
||||
<button
|
||||
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"
|
||||
onClick={() => {
|
||||
notesController.isTagInSelectedNotes(tag)
|
||||
? notesController.removeTagFromSelectedNotes(tag).catch(console.error)
|
||||
: notesController.addTagToSelectedNotes(tag).catch(console.error)
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`overflow-hidden overflow-ellipsis whitespace-nowrap
|
||||
${notesController.isTagInSelectedNotes(tag) ? 'font-bold' : ''}`}
|
||||
>
|
||||
{noteTagsController.getLongTitle(tag)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
>
|
||||
{noteTagsController.getLongTitle(tag)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { KeyboardKey } from '@/Services/IOService'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
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 ChangeEditorMenu from '@/Components/ChangeEditor/ChangeEditorMenu'
|
||||
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import Popover from '../Popover/Popover'
|
||||
|
||||
type ChangeEditorOptionProps = {
|
||||
application: WebApplication
|
||||
@@ -15,91 +13,48 @@ type ChangeEditorOptionProps = {
|
||||
|
||||
const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ application, note }) => {
|
||||
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 menuRef = useRef<HTMLDivElement>(null)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, (open: boolean) => {
|
||||
setIsOpen(open)
|
||||
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])
|
||||
const toggleMenu = useCallback(async () => {
|
||||
setIsOpen((isOpen) => !isOpen)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div ref={menuContainerRef}>
|
||||
<Disclosure open={isOpen} onChange={toggleChangeEditorMenu}>
|
||||
<DisclosureButton
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Escape) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
onKeyDown={(event) => {
|
||||
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"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="dashboard" className="mr-2 text-neutral" />
|
||||
Change note type
|
||||
</div>
|
||||
<Icon type="chevron-right" className="text-neutral" />
|
||||
</button>
|
||||
<Popover
|
||||
align="start"
|
||||
anchorElement={buttonRef.current}
|
||||
className="pt-2 md:pt-0"
|
||||
open={isOpen}
|
||||
side="right"
|
||||
togglePopover={toggleMenu}
|
||||
>
|
||||
<ChangeEditorMenu
|
||||
application={application}
|
||||
note={note}
|
||||
isVisible={isOpen}
|
||||
closeMenu={() => {
|
||||
setIsOpen(false)
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={buttonRef}
|
||||
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="dashboard" className="mr-2 text-neutral" />
|
||||
Change note type
|
||||
</div>
|
||||
<Icon type="chevron-right" className="text-neutral" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
ref={menuRef}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Escape) {
|
||||
setIsOpen(false)
|
||||
buttonRef.current?.focus()
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
...menuStyle,
|
||||
position: 'fixed',
|
||||
}}
|
||||
className="max-h-120 fixed flex min-w-68 flex-col overflow-y-auto rounded bg-default shadow-main"
|
||||
>
|
||||
{isOpen && (
|
||||
<ChangeEditorMenu
|
||||
application={application}
|
||||
closeOnBlur={closeOnBlur}
|
||||
note={note}
|
||||
isVisible={isVisible}
|
||||
closeMenu={() => {
|
||||
setIsOpen(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,10 +9,9 @@ import Spinner from '@/Components/Spinner/Spinner'
|
||||
type ListedActionsMenuProps = {
|
||||
application: WebApplication
|
||||
note: SNNote
|
||||
recalculateMenuStyle: () => void
|
||||
}
|
||||
|
||||
const ListedActionsMenu = ({ application, note, recalculateMenuStyle }: ListedActionsMenuProps) => {
|
||||
const ListedActionsMenu = ({ application, note }: ListedActionsMenuProps) => {
|
||||
const [menuGroups, setMenuGroups] = useState<ListedMenuGroup[]>([])
|
||||
const [isFetchingAccounts, setIsFetchingAccounts] = useState(true)
|
||||
|
||||
@@ -88,14 +87,11 @@ const ListedActionsMenu = ({ application, note, recalculateMenuStyle }: ListedAc
|
||||
console.error(err)
|
||||
} finally {
|
||||
setIsFetchingAccounts(false)
|
||||
setTimeout(() => {
|
||||
recalculateMenuStyle()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
void fetchListedAccounts()
|
||||
}, [application, note.uuid, recalculateMenuStyle])
|
||||
}, [application, note.uuid])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
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 { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import ListedActionsMenu from './ListedActionsMenu'
|
||||
import { KeyboardKey } from '@/Services/IOService'
|
||||
import Popover from '../Popover/Popover'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -14,74 +13,42 @@ type Props = {
|
||||
|
||||
const ListedActionsOption: FunctionComponent<Props> = ({ application, note }) => {
|
||||
const menuContainerRef = useRef<HTMLDivElement>(null)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
const [menuStyle, setMenuStyle] = useState<SubmenuStyle>({
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
maxHeight: 'auto',
|
||||
})
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const [closeOnBlur] = useCloseOnBlur(menuContainerRef, setIsMenuOpen)
|
||||
|
||||
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)
|
||||
}
|
||||
const toggleMenu = useCallback(() => {
|
||||
setIsOpen((isOpen) => !isOpen)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (isMenuOpen) {
|
||||
setTimeout(() => {
|
||||
recalculateMenuStyle()
|
||||
})
|
||||
}
|
||||
}, [isMenuOpen, recalculateMenuStyle])
|
||||
|
||||
return (
|
||||
<div ref={menuContainerRef}>
|
||||
<Disclosure open={isMenuOpen} onChange={toggleListedMenu}>
|
||||
<DisclosureButton
|
||||
ref={menuButtonRef}
|
||||
onBlur={closeOnBlur}
|
||||
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="listed" className="mr-2 text-neutral" />
|
||||
Listed actions
|
||||
</div>
|
||||
<Icon type="chevron-right" className="text-neutral" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
ref={menuRef}
|
||||
style={{
|
||||
...menuStyle,
|
||||
position: 'fixed',
|
||||
}}
|
||||
className={`${
|
||||
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} recalculateMenuStyle={recalculateMenuStyle} />
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
onKeyDown={(event) => {
|
||||
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"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="listed" className="mr-2 text-neutral" />
|
||||
Listed actions
|
||||
</div>
|
||||
<Icon type="chevron-right" className="text-neutral" />
|
||||
</button>
|
||||
<Popover
|
||||
togglePopover={toggleMenu}
|
||||
anchorElement={buttonRef.current}
|
||||
open={isOpen}
|
||||
side="right"
|
||||
align="end"
|
||||
className="pt-2 md:pt-0"
|
||||
>
|
||||
<ListedActionsMenu application={application} note={note} />
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,13 +15,11 @@ import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
import { formatDateForContextMenu } from '@/Utils/DateUtils'
|
||||
|
||||
type DeletePermanentlyButtonProps = {
|
||||
closeOnBlur: NotesOptionsProps['closeOnBlur']
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const DeletePermanentlyButton = ({ closeOnBlur, onClick }: DeletePermanentlyButtonProps) => (
|
||||
const DeletePermanentlyButton = ({ onClick }: DeletePermanentlyButtonProps) => (
|
||||
<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"
|
||||
onClick={onClick}
|
||||
>
|
||||
@@ -177,7 +175,6 @@ const NotesOptions = ({
|
||||
notesController,
|
||||
noteTagsController,
|
||||
historyModalController,
|
||||
closeOnBlur,
|
||||
}: NotesOptionsProps) => {
|
||||
const [altKeyDown, setAltKeyDown] = useState(false)
|
||||
|
||||
@@ -270,7 +267,6 @@ const NotesOptions = ({
|
||||
{notes.length === 1 && (
|
||||
<>
|
||||
<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"
|
||||
onClick={openRevisionHistoryModal}
|
||||
>
|
||||
@@ -285,7 +281,6 @@ const NotesOptions = ({
|
||||
onClick={() => {
|
||||
notesController.setLockSelectedNotes(!locked)
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<Icon type="pencil-off" className={iconClass} />
|
||||
@@ -298,7 +293,6 @@ const NotesOptions = ({
|
||||
onClick={() => {
|
||||
notesController.setHideSelectedNotePreviews(!hidePreviews)
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<Icon type="rich-text" className={iconClass} />
|
||||
@@ -311,7 +305,6 @@ const NotesOptions = ({
|
||||
onClick={() => {
|
||||
notesController.setProtectSelectedNotes(!protect).catch(console.error)
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<Icon type="password" className={iconClass} />
|
||||
@@ -335,7 +328,6 @@ const NotesOptions = ({
|
||||
)}
|
||||
{unpinned && (
|
||||
<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"
|
||||
onClick={() => {
|
||||
notesController.setPinSelectedNotes(true)
|
||||
@@ -347,7 +339,6 @@ const NotesOptions = ({
|
||||
)}
|
||||
{pinned && (
|
||||
<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"
|
||||
onClick={() => {
|
||||
notesController.setPinSelectedNotes(false)
|
||||
@@ -358,7 +349,6 @@ const NotesOptions = ({
|
||||
</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"
|
||||
onClick={downloadSelectedItems}
|
||||
>
|
||||
@@ -366,7 +356,6 @@ const NotesOptions = ({
|
||||
Export
|
||||
</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"
|
||||
onClick={duplicateSelectedItems}
|
||||
>
|
||||
@@ -375,7 +364,6 @@ const NotesOptions = ({
|
||||
</button>
|
||||
{unarchived && (
|
||||
<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"
|
||||
onClick={() => {
|
||||
notesController.setArchiveSelectedNotes(true).catch(console.error)
|
||||
@@ -387,7 +375,6 @@ const NotesOptions = ({
|
||||
)}
|
||||
{archived && (
|
||||
<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"
|
||||
onClick={() => {
|
||||
notesController.setArchiveSelectedNotes(false).catch(console.error)
|
||||
@@ -400,14 +387,12 @@ const NotesOptions = ({
|
||||
{notTrashed &&
|
||||
(altKeyDown ? (
|
||||
<DeletePermanentlyButton
|
||||
closeOnBlur={closeOnBlur}
|
||||
onClick={async () => {
|
||||
await notesController.deleteNotesPermanently()
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<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"
|
||||
onClick={async () => {
|
||||
await notesController.setTrashSelectedNotes(true)
|
||||
@@ -420,7 +405,6 @@ const NotesOptions = ({
|
||||
{trashed && (
|
||||
<>
|
||||
<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"
|
||||
onClick={async () => {
|
||||
await notesController.setTrashSelectedNotes(false)
|
||||
@@ -430,13 +414,11 @@ const NotesOptions = ({
|
||||
<span className="text-success">Restore</span>
|
||||
</button>
|
||||
<DeletePermanentlyButton
|
||||
closeOnBlur={closeOnBlur}
|
||||
onClick={async () => {
|
||||
await notesController.deleteNotesPermanently()
|
||||
}}
|
||||
/>
|
||||
<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"
|
||||
onClick={async () => {
|
||||
await notesController.emptyTrash()
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
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 { useRef, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import NotesOptions from './NotesOptions'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { NoteTagsController } from '@/Controllers/NoteTagsController'
|
||||
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
|
||||
import Popover from '../Popover/Popover'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -29,83 +26,38 @@ const NotesOptionsPanel = ({
|
||||
historyModalController,
|
||||
onClickPreprocessing,
|
||||
}: Props) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [position, setPosition] = useState({
|
||||
top: 0,
|
||||
right: 0,
|
||||
})
|
||||
const [maxHeight, setMaxHeight] = useState<number | 'auto'>('auto')
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const panelRef = useRef<HTMLDivElement>(null)
|
||||
const [closeOnBlur] = useCloseOnBlur(panelRef, setOpen)
|
||||
|
||||
const toggleMenu = useCallback(async () => {
|
||||
const willMenuOpen = !isOpen
|
||||
if (willMenuOpen && onClickPreprocessing) {
|
||||
await onClickPreprocessing()
|
||||
}
|
||||
setIsOpen(willMenuOpen)
|
||||
}, [onClickPreprocessing, isOpen])
|
||||
|
||||
return (
|
||||
<Disclosure
|
||||
open={open}
|
||||
onChange={async () => {
|
||||
const rect = buttonRef.current?.getBoundingClientRect()
|
||||
if (rect) {
|
||||
const { clientHeight } = document.documentElement
|
||||
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
|
||||
const footerHeightInPx = footerElementRect?.height
|
||||
if (footerHeightInPx) {
|
||||
setMaxHeight(clientHeight - rect.bottom - footerHeightInPx - 2)
|
||||
}
|
||||
setPosition({
|
||||
top: rect.bottom,
|
||||
right: document.body.clientWidth - rect.right,
|
||||
})
|
||||
const newOpenState = !open
|
||||
if (newOpenState && onClickPreprocessing) {
|
||||
await onClickPreprocessing()
|
||||
}
|
||||
setOpen(newOpenState)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DisclosureButton
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setOpen(false)
|
||||
}
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={buttonRef}
|
||||
<>
|
||||
<button
|
||||
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" className="block" />
|
||||
</DisclosureButton>
|
||||
<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
|
||||
application={application}
|
||||
navigationController={navigationController}
|
||||
notesController={notesController}
|
||||
noteTagsController={noteTagsController}
|
||||
historyModalController={historyModalController}
|
||||
closeOnBlur={closeOnBlur}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
<Icon type="more" />
|
||||
</button>
|
||||
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="py-2">
|
||||
<NotesOptions
|
||||
application={application}
|
||||
navigationController={navigationController}
|
||||
notesController={notesController}
|
||||
noteTagsController={noteTagsController}
|
||||
historyModalController={historyModalController}
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,5 +10,4 @@ export type NotesOptionsProps = {
|
||||
notesController: NotesController
|
||||
noteTagsController: NoteTagsController
|
||||
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 { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
import { ComponentArea, ContentType, FeatureIdentifier, GetFeatures, SNComponent } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, KeyboardEventHandler, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import { quickSettingsKeyDownHandler, themesMenuKeyDownHandler } from './EventHandlers'
|
||||
import { quickSettingsKeyDownHandler } from './EventHandlers'
|
||||
import FocusModeSwitch from './FocusModeSwitch'
|
||||
import ThemesMenuButton from './ThemesMenuButton'
|
||||
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
|
||||
import { ThemeItem } from './ThemeItem'
|
||||
import { sortThemes } from '@/Utils/SortThemes'
|
||||
import RadioIndicator from '../RadioIndicator/RadioIndicator'
|
||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
import Popover from '../Popover/Popover'
|
||||
import { PreferencesController } from '@/Controllers/PreferencesController'
|
||||
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
|
||||
|
||||
const focusModeAnimationDuration = 1255
|
||||
|
||||
type MenuProps = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
preferencesController: PreferencesController
|
||||
quickSettingsMenuController: QuickSettingsController
|
||||
application: WebApplication
|
||||
onClickOutside: () => void
|
||||
}
|
||||
|
||||
const toggleFocusMode = (enabled: boolean) => {
|
||||
@@ -38,25 +37,23 @@ const toggleFocusMode = (enabled: boolean) => {
|
||||
}
|
||||
}
|
||||
|
||||
const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, viewControllerManager, onClickOutside }) => {
|
||||
const { closeQuickSettingsMenu, shouldAnimateCloseMenu, focusModeEnabled, setFocusModeEnabled } =
|
||||
viewControllerManager.quickSettingsMenuController
|
||||
const QuickSettingsMenu: FunctionComponent<MenuProps> = ({
|
||||
application,
|
||||
preferencesController,
|
||||
quickSettingsMenuController,
|
||||
}) => {
|
||||
const { closeQuickSettingsMenu, focusModeEnabled, setFocusModeEnabled } = quickSettingsMenuController
|
||||
const [themes, setThemes] = useState<ThemeItem[]>([])
|
||||
const [toggleableComponents, setToggleableComponents] = useState<SNComponent[]>([])
|
||||
const [themesMenuOpen, setThemesMenuOpen] = useState(false)
|
||||
const [themesMenuPosition, setThemesMenuPosition] = useState({})
|
||||
const [defaultThemeOn, setDefaultThemeOn] = useState(false)
|
||||
|
||||
const themesMenuRef = useRef<HTMLDivElement>(null)
|
||||
const themesButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const prefsButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const quickSettingsMenuRef = useRef<HTMLDivElement>(null)
|
||||
const defaultThemeButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const mainRef = useRef<HTMLDivElement>(null)
|
||||
useCloseOnClickOutside(mainRef, () => {
|
||||
onClickOutside()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
toggleFocusMode(focusModeEnabled)
|
||||
@@ -139,25 +136,14 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, viewCont
|
||||
prefsButtonRef.current?.focus()
|
||||
}, [])
|
||||
|
||||
const [closeOnBlur] = useCloseOnBlur(themesMenuRef, setThemesMenuOpen)
|
||||
|
||||
const toggleThemesMenu = useCallback(() => {
|
||||
if (!themesMenuOpen && themesButtonRef.current) {
|
||||
const themesButtonRect = themesButtonRef.current.getBoundingClientRect()
|
||||
setThemesMenuPosition({
|
||||
left: themesButtonRect.right,
|
||||
bottom: document.documentElement.clientHeight - themesButtonRect.bottom,
|
||||
})
|
||||
setThemesMenuOpen(true)
|
||||
} else {
|
||||
setThemesMenuOpen(false)
|
||||
}
|
||||
}, [themesMenuOpen])
|
||||
setThemesMenuOpen((isOpen) => !isOpen)
|
||||
}, [])
|
||||
|
||||
const openPreferences = useCallback(() => {
|
||||
closeQuickSettingsMenu()
|
||||
viewControllerManager.preferencesController.openPreferences()
|
||||
}, [viewControllerManager, closeQuickSettingsMenu])
|
||||
preferencesController.openPreferences()
|
||||
}, [closeQuickSettingsMenu, preferencesController])
|
||||
|
||||
const toggleComponent = useCallback(
|
||||
(component: SNComponent) => {
|
||||
@@ -193,10 +179,6 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, viewCont
|
||||
[closeQuickSettingsMenu, themesMenuOpen],
|
||||
)
|
||||
|
||||
const handlePanelKeyDown: React.KeyboardEventHandler<HTMLDivElement> = useCallback((event) => {
|
||||
themesMenuKeyDownHandler(event, themesMenuRef, setThemesMenuOpen, themesButtonRef)
|
||||
}, [])
|
||||
|
||||
const toggleDefaultTheme = useCallback(() => {
|
||||
const activeTheme = themes.map((item) => item.component).find((theme) => theme?.active && !theme.isLayerable())
|
||||
if (activeTheme) {
|
||||
@@ -205,90 +187,71 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ application, viewCont
|
||||
}, [application, themes])
|
||||
|
||||
return (
|
||||
<div ref={mainRef} className="sn-component">
|
||||
<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 ref={mainRef} onKeyDown={handleQuickSettingsKeyDown}>
|
||||
<div className="mt-1 mb-2 px-3 text-sm font-semibold uppercase text-text">Quick Settings</div>
|
||||
<button
|
||||
onClick={toggleThemesMenu}
|
||||
onKeyDown={handleBtnKeyDown}
|
||||
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"
|
||||
>
|
||||
<div className="mt-1 mb-2 px-3 text-sm font-semibold uppercase text-text">Quick Settings</div>
|
||||
<Disclosure open={themesMenuOpen} onChange={toggleThemesMenu}>
|
||||
<DisclosureButton
|
||||
onKeyDown={handleBtnKeyDown}
|
||||
onBlur={closeOnBlur}
|
||||
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"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="themes" className="mr-2 text-neutral" />
|
||||
Themes
|
||||
</div>
|
||||
<Icon type="chevron-right" className="text-neutral" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
onBlur={closeOnBlur}
|
||||
ref={themesMenuRef}
|
||||
onKeyDown={handlePanelKeyDown}
|
||||
style={{
|
||||
...themesMenuPosition,
|
||||
}}
|
||||
className={`${
|
||||
themesMenuOpen ? 'flex' : 'hidden'
|
||||
} max-h-120 slide-down-animation fixed min-w-80 max-w-xs flex-col overflow-y-auto rounded bg-default py-2 shadow-main transition-transform duration-150`}
|
||||
>
|
||||
<div className="my-1 px-3 text-sm font-semibold uppercase text-text">Themes</div>
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={toggleDefaultTheme}
|
||||
onBlur={closeOnBlur}
|
||||
ref={defaultThemeButtonRef}
|
||||
>
|
||||
<RadioIndicator checked={defaultThemeOn} className="mr-2" />
|
||||
Default
|
||||
</button>
|
||||
{themes.map((theme) => (
|
||||
<ThemesMenuButton
|
||||
item={theme}
|
||||
application={application}
|
||||
key={theme.component?.uuid ?? theme.identifier}
|
||||
onBlur={closeOnBlur}
|
||||
/>
|
||||
))}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
{toggleableComponents.map((component) => (
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={() => {
|
||||
toggleComponent(component)
|
||||
}}
|
||||
key={component.uuid}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="window" className="mr-2 text-neutral" />
|
||||
{component.displayName}
|
||||
</div>
|
||||
<Switch checked={component.active} className="px-0" />
|
||||
</button>
|
||||
))}
|
||||
<FocusModeSwitch
|
||||
application={application}
|
||||
onToggle={setFocusModeEnabled}
|
||||
onClose={closeQuickSettingsMenu}
|
||||
isEnabled={focusModeEnabled}
|
||||
/>
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
<div className="flex items-center">
|
||||
<Icon type="themes" className="mr-2 text-neutral" />
|
||||
Themes
|
||||
</div>
|
||||
<Icon type="chevron-right" className="text-neutral" />
|
||||
</button>
|
||||
<Popover
|
||||
togglePopover={toggleThemesMenu}
|
||||
anchorElement={themesButtonRef.current}
|
||||
open={themesMenuOpen}
|
||||
side="right"
|
||||
align="end"
|
||||
className="py-2"
|
||||
>
|
||||
<div className="my-1 px-3 text-sm font-semibold uppercase text-text">Themes</div>
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={openPreferences}
|
||||
ref={prefsButtonRef}
|
||||
onClick={toggleDefaultTheme}
|
||||
ref={defaultThemeButtonRef}
|
||||
>
|
||||
<Icon type="more" className="mr-2 text-neutral" />
|
||||
Open Preferences
|
||||
<RadioIndicator checked={defaultThemeOn} className="mr-2" />
|
||||
Default
|
||||
</button>
|
||||
</div>
|
||||
{themes.map((theme) => (
|
||||
<ThemesMenuButton item={theme} application={application} key={theme.component?.uuid ?? theme.identifier} />
|
||||
))}
|
||||
</Popover>
|
||||
{toggleableComponents.map((component) => (
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={() => {
|
||||
toggleComponent(component)
|
||||
}}
|
||||
key={component.uuid}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon type="window" className="mr-2 text-neutral" />
|
||||
{component.displayName}
|
||||
</div>
|
||||
<Switch checked={component.active} className="px-0" />
|
||||
</button>
|
||||
))}
|
||||
<FocusModeSwitch
|
||||
application={application}
|
||||
onToggle={setFocusModeEnabled}
|
||||
onClose={closeQuickSettingsMenu}
|
||||
isEnabled={focusModeEnabled}
|
||||
/>
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={openPreferences}
|
||||
ref={prefsButtonRef}
|
||||
>
|
||||
<Icon type="more" className="mr-2 text-neutral" />
|
||||
Open Preferences
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,10 +11,9 @@ import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/Premium
|
||||
type Props = {
|
||||
item: ThemeItem
|
||||
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 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'
|
||||
}
|
||||
onClick={toggleTheme}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
{item.component?.isLayerable() ? (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 Menu from '@/Components/Menu/Menu'
|
||||
import MenuItem from '@/Components/Menu/MenuItem'
|
||||
@@ -11,6 +11,7 @@ import { NavigationController } from '@/Controllers/Navigation/NavigationControl
|
||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
import { formatDateForContextMenu } from '@/Utils/DateUtils'
|
||||
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
|
||||
import Popover from '../Popover/Popover'
|
||||
|
||||
type ContextMenuProps = {
|
||||
navigationController: NavigationController
|
||||
@@ -21,22 +22,11 @@ type ContextMenuProps = {
|
||||
const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag }: ContextMenuProps) => {
|
||||
const premiumModal = usePremiumModal()
|
||||
|
||||
const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = navigationController
|
||||
const { contextMenuOpen, contextMenuClickLocation } = navigationController
|
||||
|
||||
const contextMenuRef = useRef<HTMLDivElement>(null)
|
||||
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(() => {
|
||||
if (!isEntitledToFolders) {
|
||||
premiumModal.activate('Folders')
|
||||
@@ -63,52 +53,46 @@ const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag
|
||||
|
||||
const tagCreatedAt = useMemo(() => formatDateForContextMenu(selectedTag.created_at), [selectedTag.created_at])
|
||||
|
||||
return contextMenuOpen ? (
|
||||
<div
|
||||
ref={contextMenuRef}
|
||||
className="max-h-120 fixed z-dropdown-menu flex min-w-60 flex-col overflow-y-auto rounded bg-default py-2 shadow-main"
|
||||
style={{
|
||||
...contextMenuPosition,
|
||||
maxHeight: contextMenuMaxHeight,
|
||||
}}
|
||||
return (
|
||||
<Popover
|
||||
open={contextMenuOpen}
|
||||
anchorPoint={contextMenuClickLocation}
|
||||
togglePopover={() => navigationController.setContextMenuOpen(!contextMenuOpen)}
|
||||
className="py-2"
|
||||
>
|
||||
<Menu
|
||||
a11yLabel="Tag context menu"
|
||||
isOpen={contextMenuOpen}
|
||||
closeMenu={() => {
|
||||
navigationController.setContextMenuOpen(false)
|
||||
}}
|
||||
>
|
||||
<MenuItem type={MenuItemType.IconButton} className={'justify-between py-1.5'} onClick={onClickAddSubtag}>
|
||||
<div className="flex items-center">
|
||||
<Icon type="add" className="mr-2 text-neutral" />
|
||||
Add subtag
|
||||
<div ref={contextMenuRef}>
|
||||
<Menu a11yLabel="Tag context menu" isOpen={contextMenuOpen}>
|
||||
<MenuItem type={MenuItemType.IconButton} className={'justify-between py-1.5'} onClick={onClickAddSubtag}>
|
||||
<div className="flex items-center">
|
||||
<Icon type="add" className="mr-2 text-neutral" />
|
||||
Add subtag
|
||||
</div>
|
||||
{!isEntitledToFolders && <Icon type={PremiumFeatureIconName} className={PremiumFeatureIconClass} />}
|
||||
</MenuItem>
|
||||
<MenuItem type={MenuItemType.IconButton} className={'py-1.5'} onClick={onClickRename}>
|
||||
<Icon type="pencil-filled" className="mr-2 text-neutral" />
|
||||
Rename
|
||||
</MenuItem>
|
||||
<MenuItem type={MenuItemType.IconButton} className={'py-1.5'} onClick={onClickDelete}>
|
||||
<Icon type="trash" className="mr-2 text-danger" />
|
||||
<span className="text-danger">Delete</span>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
<div className="px-3 pt-1 pb-1.5 text-xs font-medium text-neutral">
|
||||
<div className="mb-1">
|
||||
<span className="font-semibold">Last modified:</span> {tagLastModified}
|
||||
</div>
|
||||
<div className="mb-1">
|
||||
<span className="font-semibold">Created:</span> {tagCreatedAt}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Tag ID:</span> {selectedTag.uuid}
|
||||
</div>
|
||||
{!isEntitledToFolders && <Icon type={PremiumFeatureIconName} className={PremiumFeatureIconClass} />}
|
||||
</MenuItem>
|
||||
<MenuItem type={MenuItemType.IconButton} className={'py-1.5'} onClick={onClickRename}>
|
||||
<Icon type="pencil-filled" className="mr-2 text-neutral" />
|
||||
Rename
|
||||
</MenuItem>
|
||||
<MenuItem type={MenuItemType.IconButton} className={'py-1.5'} onClick={onClickDelete}>
|
||||
<Icon type="trash" className="mr-2 text-danger" />
|
||||
<span className="text-danger">Delete</span>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
<div className="px-3 pt-1 pb-1.5 text-xs font-medium text-neutral">
|
||||
<div className="mb-1">
|
||||
<span className="font-semibold">Last modified:</span> {tagLastModified}
|
||||
</div>
|
||||
<div className="mb-1">
|
||||
<span className="font-semibold">Created:</span> {tagCreatedAt}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Tag ID:</span> {selectedTag.uuid}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
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 './IsMobile'
|
||||
export * from './StringUtils'
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
--z-index-resizer-overlay: 1000;
|
||||
--z-index-component-view: 1000;
|
||||
--z-index-panel-resizer: 1001;
|
||||
--z-index-dropdown-menu: 1002;
|
||||
--z-index-footer-bar: 2000;
|
||||
--z-index-footer-bar-item: 2000;
|
||||
--z-index-footer-bar-item-panel: 2000;
|
||||
--z-index-dropdown-menu: 2500;
|
||||
--z-index-preferences: 3000;
|
||||
--z-index-purchase-flow: 4000;
|
||||
--z-index-lock-screen: 10000;
|
||||
|
||||
Reference in New Issue
Block a user