refactor: mobile popover UX (#2140)

This commit is contained in:
Aman Harwara
2023-01-18 01:00:23 +05:30
committed by GitHub
parent 7af4ecbc3d
commit baf77516fe
33 changed files with 237 additions and 117 deletions

View File

@@ -89,7 +89,7 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({
return (
<>
<div className="mt-1 mb-1 flex items-center justify-between px-3">
<div className="mt-1 mb-1 hidden items-center justify-between px-3 md:flex">
<div className="text-lg font-bold lg:text-base">Account</div>
<div className="flex cursor-pointer" onClick={closeMenu}>
<Icon type="close" className="text-neutral" />

View File

@@ -32,6 +32,7 @@ const WorkspaceSwitcherOption: FunctionComponent<Props> = ({ mainApplicationGrou
<Icon type="chevron-right" className={`text-neutral ${MenuItemIconSize}`} />
</MenuItem>
<Popover
title="Switch workspace"
align="end"
anchorElement={buttonRef.current}
className="py-2"

View File

@@ -27,6 +27,7 @@ const LockscreenWorkspaceSwitcher: FunctionComponent<Props> = ({ mainApplication
Switch workspace
</Button>
<Popover
title="Switch workspace"
align="center"
anchorElement={buttonRef.current}
className="py-2"

View File

@@ -68,6 +68,7 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
iconClassName={`text-accessory-tint-${selectedEditorIconTint}`}
/>
<Popover
title="Change note type"
togglePopover={toggleMenu}
disableClickOutside={isClickOutsideDisabled}
anchorElement={buttonRef.current}

View File

@@ -61,6 +61,7 @@ const AddItemMenuButton = ({
<Icon type="add" size="custom" className="h-5 w-5" />
</button>
<Popover
title="Add item"
open={canShowMenu && isMenuOpen}
anchorElement={addItemButtonRef.current}
togglePopover={() => {

View File

@@ -76,6 +76,7 @@ const ContentListHeader = ({
togglePopover={toggleDisplayOptionsMenu}
align="start"
className="py-2"
title="Display options"
>
<DisplayOptionsMenu
application={application}

View File

@@ -83,6 +83,7 @@ const ContextMenuCell = ({
<Icon type="more" />
</button>
<Popover
title="File options"
open={contextMenuVisible}
anchorElement={anchorElementRef.current}
togglePopover={() => {
@@ -153,6 +154,7 @@ const ItemLinksCell = ({
<Icon type="link" />
</button>
<Popover
title="Linked items"
open={contextMenuVisible}
anchorElement={anchorElementRef.current}
togglePopover={() => {
@@ -425,6 +427,7 @@ const ContentTableView = ({
<Table table={table} />
{contextMenuPosition && contextMenuItem && (
<Popover
title="Options"
open={true}
anchorPoint={contextMenuPosition}
togglePopover={() => {

View File

@@ -22,6 +22,7 @@ const FileContextMenu: FunctionComponent<Props> = observer(
return (
<Popover
title="File options"
open={showFileContextMenu}
anchorPoint={fileContextMenuLocation}
togglePopover={() => setShowFileContextMenu(!showFileContextMenu)}

View File

@@ -30,7 +30,13 @@ const FilesOptionsPanel = ({
return (
<>
<RoundIconButton label="File options menu" onClick={toggleMenu} ref={buttonRef} icon="more" />
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="py-2">
<Popover
title="File options"
togglePopover={toggleMenu}
anchorElement={buttonRef.current}
open={isOpen}
className="py-2"
>
<Menu a11yLabel="File options panel" isOpen={isOpen}>
<FileMenuOptions
filesController={filesController}

View File

@@ -112,6 +112,7 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, view
<Icon type="more" className="text-neutral" />
</button>
<Popover
title="File options"
open={showOptionsMenu}
anchorElement={menuButtonRef.current}
togglePopover={() => {
@@ -153,7 +154,7 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, view
</div>
</div>
{showLinkedBubblesContainer && (
<div className="-mt-1 border-b border-border py-1.5 px-3.5">
<div className="-mt-1 min-h-0 flex-shrink-0 border-b border-border py-1.5 px-3.5">
<LinkedItemBubblesContainer
linkingController={viewControllerManager.linkingController}
item={currentFile}

View File

@@ -101,6 +101,7 @@ const FileViewWithoutProtection = ({ application, viewControllerManager, file }:
icon="info"
/>
<Popover
title="Details"
open={isFileInfoPanelOpen}
togglePopover={toggleFileInfoPanel}
anchorElement={fileInfoButtonRef.current}
@@ -117,7 +118,9 @@ const FileViewWithoutProtection = ({ application, viewControllerManager, file }:
/>
</div>
</div>
<LinkedItemBubblesContainer item={file} linkingController={viewControllerManager.linkingController} />
<div className="hidden md:flex">
<LinkedItemBubblesContainer item={file} linkingController={viewControllerManager.linkingController} />
</div>
</div>
</div>
<div className="flex min-h-0 flex-grow flex-col">

View File

@@ -41,6 +41,7 @@ const AccountMenuButton = ({
</button>
</StyledTooltip>
<Popover
title="Account"
anchorElement={buttonRef.current}
open={isOpen}
togglePopover={toggleMenu}

View File

@@ -49,6 +49,7 @@ const QuickSettingsButton = ({ application, isOpen, toggleMenu, quickSettingsMen
</button>
</StyledTooltip>
<Popover
title="Quick settings"
togglePopover={toggleMenu}
anchorElement={buttonRef.current}
open={isOpen}

View File

@@ -5,6 +5,7 @@ import { useState } from 'react'
import { FileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
import { FileContextMenuBackupOption } from '../FileContextMenu/FileContextMenuBackupOption'
import Icon from '../Icon/Icon'
import MenuItem from '../Menu/MenuItem'
import HorizontalSeparator from '../Shared/HorizontalSeparator'
import Switch from '../Switch/Switch'
@@ -20,8 +21,7 @@ const LinkedFileMenuOptions = ({ file, closeMenu, handleFileAction, setIsRenamin
return (
<>
<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"
<MenuItem
onClick={() => {
void handleFileAction({
type: FileItemActionType.PreviewFile,
@@ -35,10 +35,10 @@ const LinkedFileMenuOptions = ({ file, closeMenu, handleFileAction, setIsRenamin
>
<Icon type="file" className="mr-2 text-neutral" />
Preview file
</button>
</MenuItem>
<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"
<MenuItem
className="justify-between"
onClick={() => {
handleFileAction({
type: FileItemActionType.ToggleFileProtection,
@@ -54,10 +54,9 @@ const LinkedFileMenuOptions = ({ file, closeMenu, handleFileAction, setIsRenamin
Password protect
</span>
<Switch className="pointer-events-none px-0" tabIndex={FOCUSABLE_BUT_NOT_TABBABLE} checked={isFileProtected} />
</button>
</MenuItem>
<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"
<MenuItem
onClick={() => {
handleFileAction({
type: FileItemActionType.DownloadFile,
@@ -68,9 +67,8 @@ const LinkedFileMenuOptions = ({ file, closeMenu, handleFileAction, setIsRenamin
>
<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"
</MenuItem>
<MenuItem
onClick={() => {
setIsRenamingFile(true)
closeMenu()
@@ -78,9 +76,8 @@ const LinkedFileMenuOptions = ({ file, closeMenu, handleFileAction, setIsRenamin
>
<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"
</MenuItem>
<MenuItem
onClick={() => {
handleFileAction({
type: FileItemActionType.DeleteFile,
@@ -91,7 +88,7 @@ const LinkedFileMenuOptions = ({ file, closeMenu, handleFileAction, setIsRenamin
>
<Icon type="trash" className="mr-2 text-danger" />
<span className="text-danger">Delete permanently</span>
</button>
</MenuItem>
<FileContextMenuBackupOption file={file} />
</>

View File

@@ -101,7 +101,7 @@ const LinkedItemBubblesContainer = ({ item, linkingController }: Props) => {
return (
<div
className={classNames(
'note-view-linking-container hidden min-w-80 max-w-full flex-wrap items-center gap-2 bg-transparent md:-mr-2 md:flex',
'note-view-linking-container flex min-w-80 max-w-full flex-wrap items-center gap-2 bg-transparent md:-mr-2',
allItemsLinkedToItem.length || notesLinkingToItem.length ? 'mt-1' : 'mt-0.5',
)}
>

View File

@@ -36,7 +36,13 @@ const LinkedItemsButton = ({ linkingController, filesController, onClickPreproce
<StyledTooltip label="Linked items panel">
<RoundIconButton label="Linked items panel" onClick={toggleMenu} ref={buttonRef} icon="link" />
</StyledTooltip>
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isLinkingPanelOpen} className="pb-2">
<Popover
title="Linked items"
togglePopover={toggleMenu}
anchorElement={buttonRef.current}
open={isLinkingPanelOpen}
className="pb-2"
>
<LinkedItemsPanel
item={activeItem}
isOpen={isLinkingPanelOpen}

View File

@@ -98,6 +98,7 @@ export const LinkedItemsSectionItem = ({
<Icon type="more" className="text-neutral" />
</button>
<Popover
title="Options"
open={isMenuOpen}
togglePopover={toggleMenu}
anchorElement={menuButtonRef.current}

View File

@@ -905,10 +905,12 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
</div>
)}
</div>
<LinkedItemBubblesContainer
item={this.note}
linkingController={this.viewControllerManager.linkingController}
/>
<div className="hidden md:block">
<LinkedItemBubblesContainer
item={this.note}
linkingController={this.viewControllerManager.linkingController}
/>
</div>
</div>
)}

View File

@@ -109,6 +109,7 @@ export default function BlockPickerMenuPlugin(): JSX.Element {
return (
<Popover
title="Block picker"
align="start"
anchorElement={anchorElementRef.current}
open={true}

View File

@@ -96,6 +96,7 @@ export const ItemSelectionPlugin: FunctionComponent<Props> = ({ currentNote }) =
return (
<Popover
title="Select item"
align="start"
anchorElement={anchorElementRef.current}
open={true}

View File

@@ -35,6 +35,7 @@ const NotesContextMenu = ({
return (
<Popover
title="Note options"
align="start"
anchorPoint={{
x: contextMenuClickLocation.x,

View File

@@ -65,6 +65,7 @@ const AddTagOption: FunctionComponent<Props> = ({
<Icon type="chevron-right" className="text-neutral" />
</MenuItem>
<Popover
title="Add tag"
togglePopover={toggleMenu}
anchorElement={buttonRef.current}
open={isOpen}

View File

@@ -50,6 +50,7 @@ const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ applic
</div>
</MenuItem>
<Popover
title="Change note type"
align="start"
anchorElement={buttonRef.current}
className="pt-2 md:pt-0"

View File

@@ -48,6 +48,7 @@ const ListedActionsOption: FunctionComponent<Props> = ({ application, note, icon
<Icon type="chevron-right" className="text-neutral" />
</MenuItem>
<Popover
title="Listed"
togglePopover={toggleMenu}
anchorElement={buttonRef.current}
open={isOpen}

View File

@@ -47,6 +47,7 @@ const NotesOptionsPanel = ({
<>
<RoundIconButton label="Note options menu" onClick={toggleMenu} ref={buttonRef} icon="more" />
<Popover
title="Note options"
disableClickOutside={disableClickOutside}
togglePopover={toggleMenu}
anchorElement={buttonRef.current}

View File

@@ -52,6 +52,7 @@ const SuperNoteOptions = ({ note, markdownShortcut, enableSuperMarkdownPreview }
<Icon type="chevron-right" className="ml-auto text-neutral" />
</MenuItem>
<Popover
title="Export note"
side="left"
align="start"
open={isExportMenuOpen}

View File

@@ -0,0 +1,92 @@
import { useDisableBodyScrollOnMobile } from '@/Hooks/useDisableBodyScrollOnMobile'
import { useLifecycleAnimation } from '@/Hooks/useLifecycleAnimation'
import { classNames } from '@standardnotes/snjs'
import { ReactNode } from 'react'
import Portal from '../Portal/Portal'
const MobilePopoverContent = ({
open,
requestClose,
children,
title,
className,
}: {
open: boolean
requestClose: () => void
children: ReactNode
title: string
className?: string
}) => {
const [isMounted, setPopoverElement] = useLifecycleAnimation({
open,
enter: {
keyframes: [
{
opacity: 0.25,
transform: 'translateY(1rem)',
},
{
opacity: 1,
transform: 'translateY(0)',
},
],
options: {
easing: 'cubic-bezier(.36,.66,.04,1)',
duration: 150,
fill: 'forwards',
},
initialStyle: {
transformOrigin: 'bottom',
},
},
enterCallback: (element) => {
element.scrollTop = 0
},
exit: {
keyframes: [
{
opacity: 1,
transform: 'translateY(0)',
},
{
opacity: 0,
transform: 'translateY(1rem)',
},
],
options: {
easing: 'cubic-bezier(.36,.66,.04,1)',
duration: 150,
fill: 'forwards',
},
initialStyle: {
transformOrigin: 'bottom',
},
},
})
useDisableBodyScrollOnMobile()
if (!isMounted) {
return null
}
return (
<Portal>
<div
ref={setPopoverElement}
className="absolute top-0 left-0 z-modal flex h-full w-full origin-bottom flex-col bg-default pt-safe-top pb-safe-bottom opacity-0"
>
<div className="flex items-center justify-between border-b border-border py-2.5 px-3 text-base">
<div />
<div className="font-semibold">{title}</div>
<button className="font-semibold active:shadow-none active:outline-none" onClick={requestClose}>
Done
</button>
</div>
<div className={classNames('h-full overflow-y-auto', className)}>{children}</div>
</div>
</Portal>
)
}
export default MobilePopoverContent

View File

@@ -1,6 +1,8 @@
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
import { UuidGenerator } from '@standardnotes/snjs'
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
import MobilePopoverContent from './MobilePopoverContent'
import PositionedPopoverContent from './PositionedPopoverContent'
import { PopoverProps } from './Types'
@@ -38,6 +40,7 @@ const Popover = ({
open,
overrideZIndex,
side,
title,
togglePopover,
disableClickOutside,
disableMobileFullscreenTakeover,
@@ -87,6 +90,23 @@ const Popover = ({
}
}, [addAndroidBackHandler, open, togglePopover])
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
if (isMobileScreen && !disableMobileFullscreenTakeover) {
return (
<MobilePopoverContent
open={open}
requestClose={() => {
togglePopover?.()
}}
title={title}
className={className}
>
{children}
</MobilePopoverContent>
)
}
return open ? (
<PopoverContext.Provider value={contextValue}>
<PositionedPopoverContent
@@ -95,13 +115,14 @@ const Popover = ({
anchorPoint={anchorPoint}
childPopovers={childPopovers}
className={`popover-content-container ${className ?? ''}`}
id={popoverId.current}
overrideZIndex={overrideZIndex}
side={side}
togglePopover={togglePopover}
disableClickOutside={disableClickOutside}
disableMobileFullscreenTakeover={disableMobileFullscreenTakeover}
id={popoverId.current}
maxHeight={maxHeight}
overrideZIndex={overrideZIndex}
side={side}
title={title}
togglePopover={togglePopover}
>
{children}
</PositionedPopoverContent>

View File

@@ -30,25 +30,36 @@ type PopoverAnchorPointProps = {
anchorElement?: never
}
type PopoverMutuallyExclusiveProps =
| {
togglePopover: () => void
disableMobileFullscreenTakeover?: never
}
| {
togglePopover?: never
disableMobileFullscreenTakeover: boolean
}
type CommonPopoverProps = {
align?: PopoverAlignment
children: ReactNode
side?: PopoverSide
overrideZIndex?: string
togglePopover?: () => void
className?: string
disableClickOutside?: boolean
disableMobileFullscreenTakeover?: boolean
maxHeight?: (calculatedMaxHeight: number) => number
title: string
}
export type PopoverContentProps = CommonPopoverProps & {
anchorElement?: HTMLElement | null
anchorPoint?: Point
childPopovers: Set<string>
togglePopover?: () => void
disableMobileFullscreenTakeover?: boolean
id: string
}
export type PopoverProps =
| (CommonPopoverProps & PopoverAnchorElementProps)
| (CommonPopoverProps & PopoverAnchorPointProps)
| (CommonPopoverProps & PopoverMutuallyExclusiveProps & PopoverAnchorElementProps)
| (CommonPopoverProps & PopoverMutuallyExclusiveProps & PopoverAnchorPointProps)

View File

@@ -94,6 +94,7 @@ const EditSmartViewModal = ({ controller, platform }: Props) => {
<Icon type={icon || SmartViewDefaultIconName} />
</button>
<Popover
title="Choose icon"
open={shouldShowIconPicker}
anchorElement={iconPickerButtonRef.current}
togglePopover={toggleIconPicker}

View File

@@ -144,6 +144,7 @@ const AddSmartViewModal = ({ controller, platform }: Props) => {
<Icon type={icon || SmartViewDefaultIconName} />
</button>
<Popover
title="Choose icon"
open={shouldShowIconPicker}
anchorElement={iconPickerButtonRef.current}
togglePopover={toggleIconPicker}

View File

@@ -64,6 +64,7 @@ const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag
return (
<Popover
title="Tag options"
open={contextMenuOpen}
anchorPoint={contextMenuClickLocation}
togglePopover={() => navigationController.setContextMenuOpen(!contextMenuOpen)}

View File

@@ -4,90 +4,6 @@ export type AnimationConfig = {
initialStyle?: Partial<CSSStyleDeclaration>
}
export const EnterFromTopAnimation: AnimationConfig = {
keyframes: [
{
opacity: 0,
transform: 'scaleY(0)',
},
{
opacity: 1,
transform: 'scaleY(1)',
},
],
options: {
easing: 'ease-in-out',
duration: 150,
fill: 'forwards',
},
initialStyle: {
transformOrigin: 'top',
},
}
export const EnterFromBelowAnimation: AnimationConfig = {
keyframes: [
{
opacity: 0,
transform: 'scaleY(0)',
},
{
opacity: 1,
transform: 'scaleY(1)',
},
],
options: {
easing: 'ease-in-out',
duration: 150,
fill: 'forwards',
},
initialStyle: {
transformOrigin: 'bottom',
},
}
export const ExitToTopAnimation: AnimationConfig = {
keyframes: [
{
opacity: 1,
transform: 'scaleY(1)',
},
{
opacity: 0,
transform: 'scaleY(0)',
},
],
options: {
easing: 'ease-in-out',
duration: 150,
fill: 'forwards',
},
initialStyle: {
transformOrigin: 'top',
},
}
export const ExitToBelowAnimation: AnimationConfig = {
keyframes: [
{
opacity: 1,
transform: 'scaleY(1)',
},
{
opacity: 0,
transform: 'scaleY(0)',
},
],
options: {
easing: 'ease-in-out',
duration: 150,
fill: 'forwards',
},
initialStyle: {
transformOrigin: 'bottom',
},
}
export const TranslateFromTopAnimation: AnimationConfig = {
keyframes: [
{
@@ -129,3 +45,45 @@ export const TranslateToTopAnimation: AnimationConfig = {
transformOrigin: 'top',
},
}
export const TranslateFromBelowAnimation: AnimationConfig = {
keyframes: [
{
opacity: 0,
transform: 'translateY(100%)',
},
{
opacity: 1,
transform: 'translateY(0)',
},
],
options: {
easing: 'ease-in-out',
duration: 150,
fill: 'forwards',
},
initialStyle: {
transformOrigin: 'bottom',
},
}
export const TranslateToBelowAnimation: AnimationConfig = {
keyframes: [
{
opacity: 1,
transform: 'translateY(0)',
},
{
opacity: 0,
transform: 'translateY(100%)',
},
],
options: {
easing: 'ease-in-out',
duration: 150,
fill: 'forwards',
},
initialStyle: {
transformOrigin: 'bottom',
},
}