refactor: allow opening tag context menu without selecting the tag first

This commit is contained in:
Aman Harwara
2023-11-20 15:22:10 +05:30
parent 25e3dd50b6
commit 0f938b4cd1
6 changed files with 101 additions and 111 deletions

View File

@@ -7,6 +7,7 @@ import { getEmojiLength } from './EmojiLength'
import Icon, { isIconEmoji } from './Icon'
import { IconNameToSvgMapping } from './IconNameToSvgMapping'
import { IconPickerType } from './IconPickerType'
import DecoratedInput from '../Input/DecoratedInput'
type Props = {
selectedValue: VectorIconNameOrEmoji
@@ -114,13 +115,13 @@ const IconPicker = ({ selectedValue, onIconChange, platform, className, useIconG
}, [])
return (
<div className={`flex h-full flex-grow flex-col overflow-auto ${className}`}>
<div className={`flex h-full flex-grow flex-col ${className}`}>
<div className="flex">
<TabButton label="Icon" type={'icon'} currentType={currentType} selectTab={selectTab} />
<TabButton label="Emoji" type={'emoji'} currentType={currentType} selectTab={selectTab} />
<TabButton label="Reset" type={'reset'} currentType={currentType} selectTab={selectTab} />
</div>
<div className={'mt-2 h-full min-h-0 overflow-auto'}>
<div className={classNames('mt-1 h-full min-h-0', currentType === 'icon' && 'overflow-auto')}>
{currentType === 'icon' &&
(useIconGrid ? (
<div
@@ -152,17 +153,14 @@ const IconPicker = ({ selectedValue, onIconChange, platform, className, useIconG
))}
{currentType === 'emoji' && (
<>
<div>
<input
ref={emojiInputRef}
autoComplete="off"
autoFocus={emojiInputFocused}
className="w-full flex-grow rounded border border-solid border-passive-3 bg-default px-2 py-1 text-base font-bold text-text focus:shadow-none focus:outline-none"
type="text"
value={emojiInputValue as string}
onChange={({ target: input }) => handleEmojiChange((input as HTMLInputElement)?.value)}
/>
</div>
<DecoratedInput
ref={emojiInputRef}
autocomplete={false}
autofocus={emojiInputFocused}
type="text"
value={emojiInputValue as string}
onChange={(value) => handleEmojiChange(value)}
/>
<div className="mt-2 text-sm text-passive-0 lg:text-xs">
Use your keyboard to enter or paste in an emoji character.
</div>

View File

@@ -1,5 +1,5 @@
import { observer } from 'mobx-react-lite'
import { useCallback, useMemo } from 'react'
import { useCallback, useMemo, useRef } from 'react'
import Icon from '@/Components/Icon/Icon'
import Menu from '@/Components/Menu/Menu'
import MenuItem from '@/Components/Menu/MenuItem'
@@ -14,6 +14,8 @@ import IconPicker from '../Icon/IconPicker'
import AddToVaultMenuOption from '../Vaults/AddToVaultMenuOption'
import { useApplication } from '../ApplicationProvider'
import MenuSection from '../Menu/MenuSection'
import DecoratedInput from '../Input/DecoratedInput'
import { KeyboardKey } from '@standardnotes/ui-services'
type ContextMenuProps = {
navigationController: NavigationController
@@ -38,11 +40,6 @@ const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag
navigationController.setAddingSubtagTo(selectedTag)
}, [isEntitledToFolders, navigationController, selectedTag, premiumModal])
const onClickRename = useCallback(() => {
navigationController.setContextMenuOpen(false)
navigationController.setEditingTag(selectedTag)
}, [navigationController, selectedTag])
const onClickDelete = useCallback(() => {
navigationController.remove(selectedTag, true).catch(console.error)
}, [navigationController, selectedTag])
@@ -63,6 +60,26 @@ const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag
const tagCreatedAt = useMemo(() => formatDateForContextMenu(selectedTag.created_at), [selectedTag.created_at])
const titleInputRef = useRef<HTMLInputElement>(null)
const saveTitle = useCallback(
(closeMenu = false) => {
if (!titleInputRef.current) {
return
}
const value = titleInputRef.current.value.trim()
navigationController
.save(selectedTag, value)
.catch(console.error)
.finally(() => {
if (closeMenu) {
navigationController.setContextMenuOpen(false)
}
})
},
[navigationController, selectedTag],
)
return (
<Popover
title="Tag options"
@@ -71,13 +88,41 @@ const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag
togglePopover={() => navigationController.setContextMenuOpen(!contextMenuOpen)}
className="py-2"
>
<div className="flex flex-col gap-1 px-4 py-0.5 text-mobile-menu-item md:px-3 md:text-tablet-menu-item lg:text-menu-item">
<div className="font-semibold">Name</div>
<div className="flex gap-2.5">
<DecoratedInput
ref={titleInputRef}
className={{
container: 'flex-grow',
input: 'text-mobile-menu-item md:text-tablet-menu-item lg:text-menu-item',
}}
defaultValue={selectedTag.title}
key={selectedTag.uuid}
onBlur={() => saveTitle()}
onKeyDown={(event) => {
if (event.key === KeyboardKey.Enter) {
saveTitle(true)
}
}}
/>
<button
aria-label="Save tag name"
className="rounded border border-border bg-transparent px-1.5 active:bg-default translucent-ui:border-[--popover-border-color] md:hidden"
onClick={() => saveTitle(true)}
>
<Icon type="check" />
</button>
</div>
</div>
<HorizontalSeparator classes="my-2" />
<Menu a11yLabel="Tag context menu">
<IconPicker
key={'icon-picker'}
key={selectedTag.uuid}
onIconChange={handleIconChange}
selectedValue={selectedTag.iconString}
platform={application.platform}
className={'px-3 py-1.5'}
className={'py-1.5 md:px-3'}
useIconGrid={true}
iconGridClassName="max-h-30"
/>
@@ -98,10 +143,6 @@ const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag
</div>
{!isEntitledToFolders && <Icon type={PremiumFeatureIconName} className={PremiumFeatureIconClass} />}
</MenuItem>
<MenuItem className={'py-1.5'} onClick={onClickRename}>
<Icon type="pencil-filled" className="mr-2 text-neutral" />
Rename
</MenuItem>
<MenuItem className={'py-1.5'} onClick={onClickDelete}>
<Icon type="trash" className="mr-2 text-danger" />
<span className="text-danger">Delete</span>
@@ -109,7 +150,7 @@ const TagContextMenu = ({ navigationController, isEntitledToFolders, selectedTag
</MenuSection>
</Menu>
<HorizontalSeparator classes="my-2" />
<div className="px-3 pb-1.5 pt-1 text-sm font-medium text-neutral lg:text-xs">
<div className="px-4 pb-1.5 pt-1 text-sm font-medium text-neutral md:px-3 lg:text-xs">
<div className="mb-1">
<span className="font-semibold">Last modified:</span> {tagLastModified}
</div>

View File

@@ -10,7 +10,7 @@ type Props = {
}
const TagContextMenuWrapper = ({ navigationController, featuresController }: Props) => {
const selectedTag = navigationController.selected
const selectedTag = navigationController.contextMenuTag
if (!selectedTag || !(selectedTag instanceof SNTag)) {
return null

View File

@@ -17,25 +17,19 @@ const TagsList: FunctionComponent<Props> = ({ type }: Props) => {
type === 'all' ? application.navigationController.allLocalRootTags : application.navigationController.starredTags
const openTagContextMenu = useCallback(
(posX: number, posY: number) => {
application.navigationController.setContextMenuClickLocation({
x: posX,
y: posY,
})
application.navigationController.reloadContextMenuLayout()
(x: number, y: number) => {
application.navigationController.setContextMenuClickLocation({ x, y })
application.navigationController.setContextMenuOpen(true)
},
[application],
)
const onContextMenu = useCallback(
(tag: SNTag, posX: number, posY: number) => {
if (application.navigationController.selected !== tag) {
void application.navigationController.setSelectedTag(tag, type)
}
(tag: SNTag, section: TagListSectionType, posX: number, posY: number) => {
application.navigationController.setContextMenuTag(tag, section)
openTagContextMenu(posX, posY)
},
[application, openTagContextMenu, type],
[application, openTagContextMenu],
)
return (

View File

@@ -34,7 +34,7 @@ type Props = {
features: FeaturesController
linkingController: LinkingController
level: number
onContextMenu: (tag: SNTag, posX: number, posY: number) => void
onContextMenu: (tag: SNTag, section: TagListSectionType, posX: number, posY: number) => void
}
const PADDING_BASE_PX = 14
@@ -50,9 +50,17 @@ export const TagsListItem: FunctionComponent<Props> = observer(
const subtagInputRef = useRef<HTMLInputElement>(null)
const menuButtonRef = useRef<HTMLAnchorElement>(null)
const isContextMenuOpenForTag =
navigationController.contextMenuTag === tag &&
navigationController.contextMenuOpen &&
navigationController.contextMenuTagSection === type
const isSelected = navigationController.selected === tag && navigationController.selectedLocation === type
const isEditing = navigationController.editingTag === tag && navigationController.selectedLocation === type
const isAddingSubtag = navigationController.addingSubtagTo === tag && navigationController.selectedLocation === type
const isAddingSubtag =
navigationController.addingSubtagTo === tag &&
(navigationController.contextMenuTag === tag
? navigationController.contextMenuTagSection === type
: navigationController.selectedLocation === type)
const noteCounts = computed(() => navigationController.getNotesCount(tag))
const childrenTags = computed(() => navigationController.getChildren(tag)).get()
@@ -164,10 +172,10 @@ export const TagsListItem: FunctionComponent<Props> = observer(
if (contextMenuOpen) {
navigationController.setContextMenuOpen(false)
} else {
onContextMenu(tag, menuButtonRect.right, menuButtonRect.top)
onContextMenu(tag, type, menuButtonRect.right, menuButtonRect.top)
}
},
[onContextMenu, navigationController, tag],
[navigationController, onContextMenu, tag, type],
)
const tagRef = useRef<HTMLDivElement>(null)
@@ -262,7 +270,7 @@ export const TagsListItem: FunctionComponent<Props> = observer(
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
className={classNames(
'tag group px-3.5 py-1 md:py-0',
isSelected && 'selected',
(isSelected || isContextMenuOpenForTag) && 'selected',
isBeingDraggedOver && 'is-drag-over',
)}
onClick={selectCurrentTag}
@@ -272,7 +280,7 @@ export const TagsListItem: FunctionComponent<Props> = observer(
}}
onContextMenu={(e) => {
e.preventDefault()
onContextMenu(tag, e.clientX, e.clientY)
onContextMenu(tag, type, e.clientX, e.clientY)
}}
draggable={true}
onDragStart={onDragStart}

View File

@@ -7,7 +7,7 @@ import {
VaultDisplayServiceEvent,
} from '@standardnotes/ui-services'
import { STRING_DELETE_TAG } from '@/Constants/Strings'
import { MAX_MENU_SIZE_MULTIPLIER, MENU_MARGIN_FROM_APP_BORDER, SMART_TAGS_FEATURE_NAME } from '@/Constants/Constants'
import { SMART_TAGS_FEATURE_NAME } from '@/Constants/Constants'
import {
ContentType,
SmartView,
@@ -61,12 +61,9 @@ export class NavigationController
addingSubtagTo: SNTag | undefined = undefined
contextMenuOpen = false
contextMenuPosition: { top?: number; left: number; bottom?: number } = {
top: 0,
left: 0,
}
contextMenuClickLocation: { x: number; y: number } = { x: 0, y: 0 }
contextMenuMaxHeight: number | 'auto' = 'auto'
contextMenuTag: SNTag | undefined = undefined
contextMenuTagSection: TagListSectionType | undefined = undefined
private readonly tagsCountsState: TagsCountsState
@@ -124,13 +121,11 @@ export class NavigationController
remove: action,
contextMenuOpen: observable,
contextMenuPosition: observable,
contextMenuMaxHeight: observable,
contextMenuClickLocation: observable,
setContextMenuOpen: action,
setContextMenuClickLocation: action,
setContextMenuPosition: action,
setContextMenuMaxHeight: action,
contextMenuTag: observable,
setContextMenuTag: action,
isInFilesView: computed,
@@ -356,58 +351,9 @@ export class NavigationController
this.contextMenuClickLocation = location
}
setContextMenuPosition(position: { top?: number; left: number; bottom?: number }): void {
this.contextMenuPosition = position
}
setContextMenuMaxHeight(maxHeight: number | 'auto'): void {
this.contextMenuMaxHeight = maxHeight
}
reloadContextMenuLayout(): void {
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 - this.contextMenuClickLocation.y
const upSpace = this.contextMenuClickLocation.y
const notEnoughSpaceToOpenUpBottom = maxContextMenuHeight > bottomSpace
if (notEnoughSpaceToOpenUpBottom) {
const enoughSpaceToOpenBottomUp = upSpace > maxContextMenuHeight
if (enoughSpaceToOpenBottomUp) {
openUpBottom = false
this.setContextMenuMaxHeight('auto')
} else {
const hasMoreUpSpace = upSpace > bottomSpace
if (hasMoreUpSpace) {
this.setContextMenuMaxHeight(upSpace - MENU_MARGIN_FROM_APP_BORDER)
openUpBottom = false
} else {
this.setContextMenuMaxHeight(bottomSpace - MENU_MARGIN_FROM_APP_BORDER)
}
}
} else {
this.setContextMenuMaxHeight('auto')
}
}
if (openUpBottom) {
this.setContextMenuPosition({
top: this.contextMenuClickLocation.y,
left: this.contextMenuClickLocation.x,
})
} else {
this.setContextMenuPosition({
bottom: clientHeight - this.contextMenuClickLocation.y,
left: this.contextMenuClickLocation.x,
})
}
setContextMenuTag(tag: SNTag | undefined, section: TagListSectionType = 'all'): void {
this.contextMenuTag = tag
this.contextMenuTagSection = section
}
public get allLocalRootTags(): SNTag[] {
@@ -640,6 +586,7 @@ export class NavigationController
let shouldDelete = !userTriggered
if (userTriggered) {
shouldDelete = await confirmDialog({
title: `Delete tag "${tag.title}"?`,
text: STRING_DELETE_TAG,
confirmButtonStyle: 'danger',
})
@@ -654,11 +601,13 @@ export class NavigationController
}
public async save(tag: SNTag | SmartView, newTitle: string) {
const hasEmptyTitle = newTitle.length === 0
const hasNotChangedTitle = newTitle === tag.title
const isTemplateChange = this.items.isTemplateItem(tag)
const latestVersion = this.items.findSureItem(tag.uuid)
const siblings = tag instanceof SNTag ? tagSiblings(this.items, tag) : []
const hasEmptyTitle = newTitle.length === 0
const hasNotChangedTitle = newTitle === latestVersion.title
const isTemplateChange = this.items.isTemplateItem(latestVersion)
const siblings = latestVersion instanceof SNTag ? tagSiblings(this.items, latestVersion) : []
const hasDuplicatedTitle = siblings.some((other) => other.title.toLowerCase() === newTitle.toLowerCase())
runInAction(() => {
@@ -699,7 +648,7 @@ export class NavigationController
void this.setSelectedTag(insertedTag, this.selectedLocation || 'views')
})
} else {
await this._changeAndSaveItem.execute<TagMutator>(tag, (mutator) => {
await this._changeAndSaveItem.execute<TagMutator>(latestVersion, (mutator) => {
mutator.title = newTitle
})
}