chore: only show links container toggle if container can be truncated (#2326)
This commit is contained in:
@@ -1,4 +1,12 @@
|
|||||||
import { FormEventHandler, KeyboardEventHandler, useDeferredValue, useEffect, useRef } from 'react'
|
import {
|
||||||
|
FormEventHandler,
|
||||||
|
ForwardedRef,
|
||||||
|
KeyboardEventHandler,
|
||||||
|
forwardRef,
|
||||||
|
useDeferredValue,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
} from 'react'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { classNames } from '@standardnotes/utils'
|
import { classNames } from '@standardnotes/utils'
|
||||||
import { LinkingController } from '@/Controllers/LinkingController'
|
import { LinkingController } from '@/Controllers/LinkingController'
|
||||||
@@ -13,6 +21,7 @@ import { Slot } from '@radix-ui/react-slot'
|
|||||||
import Icon from '../Icon/Icon'
|
import Icon from '../Icon/Icon'
|
||||||
import { PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
|
import { PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
|
||||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||||
|
import { mergeRefs } from '@/Hooks/mergeRefs'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
linkingController: LinkingController
|
linkingController: LinkingController
|
||||||
@@ -23,118 +32,116 @@ type Props = {
|
|||||||
item: DecryptedItem
|
item: DecryptedItem
|
||||||
}
|
}
|
||||||
|
|
||||||
const ItemLinkAutocompleteInput = ({
|
const ItemLinkAutocompleteInput = forwardRef(
|
||||||
linkingController,
|
(
|
||||||
focusPreviousItem,
|
{ linkingController, focusPreviousItem, focusedId, setFocusedId, hoverLabel, item }: Props,
|
||||||
focusedId,
|
forwardedRef: ForwardedRef<HTMLInputElement>,
|
||||||
setFocusedId,
|
) => {
|
||||||
hoverLabel,
|
const application = useApplication()
|
||||||
item,
|
|
||||||
}: Props) => {
|
|
||||||
const application = useApplication()
|
|
||||||
|
|
||||||
const { getLinkedTagsForItem, linkItems, createAndAddNewTag, isEntitledToNoteLinking } = linkingController
|
const { getLinkedTagsForItem, linkItems, createAndAddNewTag, isEntitledToNoteLinking } = linkingController
|
||||||
|
|
||||||
const tagsLinkedToItem = getLinkedTagsForItem(item) || []
|
const tagsLinkedToItem = getLinkedTagsForItem(item) || []
|
||||||
|
|
||||||
const combobox = useComboboxStore()
|
const combobox = useComboboxStore()
|
||||||
const value = combobox.useState('value')
|
const value = combobox.useState('value')
|
||||||
const searchQuery = useDeferredValue(value)
|
const searchQuery = useDeferredValue(value)
|
||||||
|
|
||||||
const { unlinkedItems, shouldShowCreateTag } = getLinkingSearchResults(searchQuery, application, item)
|
const { unlinkedItems, shouldShowCreateTag } = getLinkingSearchResults(searchQuery, application, item)
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
const onFormSubmit: FormEventHandler = async (event) => {
|
const onFormSubmit: FormEventHandler = async (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (searchQuery !== '') {
|
if (searchQuery !== '') {
|
||||||
await createAndAddNewTag(searchQuery)
|
await createAndAddNewTag(searchQuery)
|
||||||
combobox.setValue('')
|
combobox.setValue('')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
if (focusedId !== ElementIds.ItemLinkAutocompleteInput) {
|
if (focusedId !== ElementIds.ItemLinkAutocompleteInput) {
|
||||||
setFocusedId(ElementIds.ItemLinkAutocompleteInput)
|
setFocusedId(ElementIds.ItemLinkAutocompleteInput)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyDown: KeyboardEventHandler = (event) => {
|
const onKeyDown: KeyboardEventHandler = (event) => {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case KeyboardKey.Left:
|
case KeyboardKey.Left:
|
||||||
if (searchQuery.length === 0) {
|
if (searchQuery.length === 0) {
|
||||||
focusPreviousItem()
|
focusPreviousItem()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (focusedId === ElementIds.ItemLinkAutocompleteInput) {
|
if (focusedId === ElementIds.ItemLinkAutocompleteInput) {
|
||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
}
|
}
|
||||||
}, [focusedId])
|
}, [focusedId])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={onFormSubmit}>
|
<form onSubmit={onFormSubmit}>
|
||||||
<label>
|
<label>
|
||||||
<VisuallyHidden>Link tags, notes or files</VisuallyHidden>
|
<VisuallyHidden>Link tags, notes or files</VisuallyHidden>
|
||||||
<Combobox
|
<Combobox
|
||||||
|
store={combobox}
|
||||||
|
placeholder="Link tags, notes, files..."
|
||||||
|
className={classNames(
|
||||||
|
`${tagsLinkedToItem.length > 0 ? 'w-80' : 'mr-10 w-70'}`,
|
||||||
|
'h-7 w-70 bg-transparent text-sm text-text focus:border-b-2 focus:border-info focus:shadow-none focus:outline-none lg:text-xs',
|
||||||
|
)}
|
||||||
|
title={hoverLabel}
|
||||||
|
id={ElementIds.ItemLinkAutocompleteInput}
|
||||||
|
ref={mergeRefs([inputRef, forwardedRef])}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ComboboxPopover
|
||||||
store={combobox}
|
store={combobox}
|
||||||
placeholder="Link tags, notes, files..."
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
`${tagsLinkedToItem.length > 0 ? 'w-80' : 'mr-10 w-70'}`,
|
'z-dropdown-menu max-h-[var(--popover-available-height)] w-[var(--popover-anchor-width)] overflow-y-auto rounded bg-default py-2 shadow-main',
|
||||||
'h-7 w-70 bg-transparent text-sm text-text focus:border-b-2 focus:border-info focus:shadow-none focus:outline-none lg:text-xs',
|
unlinkedItems.length === 0 && !shouldShowCreateTag && 'hidden',
|
||||||
)}
|
)}
|
||||||
title={hoverLabel}
|
>
|
||||||
id={ElementIds.ItemLinkAutocompleteInput}
|
{unlinkedItems.map((result) => {
|
||||||
ref={inputRef}
|
const cannotLinkItem = !isEntitledToNoteLinking && result instanceof SNNote
|
||||||
onFocus={handleFocus}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<ComboboxPopover
|
|
||||||
store={combobox}
|
|
||||||
className={classNames(
|
|
||||||
'z-dropdown-menu max-h-[var(--popover-available-height)] w-[var(--popover-anchor-width)] overflow-y-auto rounded bg-default py-2 shadow-main',
|
|
||||||
unlinkedItems.length === 0 && !shouldShowCreateTag && 'hidden',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{unlinkedItems.map((result) => {
|
|
||||||
const cannotLinkItem = !isEntitledToNoteLinking && result instanceof SNNote
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ComboboxItem
|
||||||
|
key={result.uuid}
|
||||||
|
className="flex w-full cursor-pointer items-center justify-between gap-4 overflow-hidden py-2 px-3 hover:bg-contrast hover:text-foreground [&[data-active-item]]:bg-info-backdrop"
|
||||||
|
hideOnClick
|
||||||
|
onClick={() => {
|
||||||
|
linkItems(item, result).catch(console.error)
|
||||||
|
combobox.setValue('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LinkedItemMeta item={result} searchQuery={searchQuery} />
|
||||||
|
{cannotLinkItem && <Icon type={PremiumFeatureIconName} className="ml-auto flex-shrink-0 text-info" />}
|
||||||
|
</ComboboxItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{shouldShowCreateTag && (
|
||||||
<ComboboxItem
|
<ComboboxItem
|
||||||
key={result.uuid}
|
|
||||||
className="flex w-full cursor-pointer items-center justify-between gap-4 overflow-hidden py-2 px-3 hover:bg-contrast hover:text-foreground [&[data-active-item]]:bg-info-backdrop"
|
|
||||||
hideOnClick
|
hideOnClick
|
||||||
|
as={Slot}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
linkItems(item, result).catch(console.error)
|
void createAndAddNewTag(searchQuery)
|
||||||
combobox.setValue('')
|
combobox.setValue('')
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LinkedItemMeta item={result} searchQuery={searchQuery} />
|
<LinkedItemSearchResultsAddTagOption searchQuery={searchQuery} />
|
||||||
{cannotLinkItem && <Icon type={PremiumFeatureIconName} className="ml-auto flex-shrink-0 text-info" />}
|
|
||||||
</ComboboxItem>
|
</ComboboxItem>
|
||||||
)
|
)}
|
||||||
})}
|
</ComboboxPopover>
|
||||||
{shouldShowCreateTag && (
|
</form>
|
||||||
<ComboboxItem
|
</div>
|
||||||
hideOnClick
|
)
|
||||||
as={Slot}
|
},
|
||||||
onClick={() => {
|
)
|
||||||
void createAndAddNewTag(searchQuery)
|
|
||||||
combobox.setValue('')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LinkedItemSearchResultsAddTagOption searchQuery={searchQuery} />
|
|
||||||
</ComboboxItem>
|
|
||||||
)}
|
|
||||||
</ComboboxPopover>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default observer(ItemLinkAutocompleteInput)
|
export default observer(ItemLinkAutocompleteInput)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { observer } from 'mobx-react-lite'
|
|||||||
import ItemLinkAutocompleteInput from './ItemLinkAutocompleteInput'
|
import ItemLinkAutocompleteInput from './ItemLinkAutocompleteInput'
|
||||||
import { LinkingController } from '@/Controllers/LinkingController'
|
import { LinkingController } from '@/Controllers/LinkingController'
|
||||||
import LinkedItemBubble from './LinkedItemBubble'
|
import LinkedItemBubble from './LinkedItemBubble'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||||
import { ElementIds } from '@/Constants/ElementIDs'
|
import { ElementIds } from '@/Constants/ElementIDs'
|
||||||
import { classNames } from '@standardnotes/utils'
|
import { classNames } from '@standardnotes/utils'
|
||||||
@@ -112,6 +112,34 @@ const LinkedItemBubblesContainer = ({ item, linkingController, hideToggle = fals
|
|||||||
const visibleItems = isCollapsed ? itemsToDisplay.slice(0, 5) : itemsToDisplay
|
const visibleItems = isCollapsed ? itemsToDisplay.slice(0, 5) : itemsToDisplay
|
||||||
const nonVisibleItems = itemsToDisplay.length - visibleItems.length
|
const nonVisibleItems = itemsToDisplay.length - visibleItems.length
|
||||||
|
|
||||||
|
const [canShowContainerToggle, setCanShowContainerToggle] = useState(false)
|
||||||
|
const linkInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const linkContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
useEffect(() => {
|
||||||
|
const container = linkContainerRef.current
|
||||||
|
const linkInput = linkInputRef.current
|
||||||
|
|
||||||
|
if (!container || !linkInput) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
if (container.clientHeight > linkInput.clientHeight) {
|
||||||
|
setCanShowContainerToggle(true)
|
||||||
|
} else {
|
||||||
|
setCanShowContainerToggle(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
resizeObserver.observe(linkContainerRef.current)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const shouldHideToggle = hideToggle || (!canShowContainerToggle && !isCollapsed)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -126,6 +154,7 @@ const LinkedItemBubblesContainer = ({ item, linkingController, hideToggle = fals
|
|||||||
allItemsLinkedToItem.length || notesLinkingToItem.length ? 'mt-1' : 'mt-0.5',
|
allItemsLinkedToItem.length || notesLinkingToItem.length ? 'mt-1' : 'mt-0.5',
|
||||||
isCollapsed ? 'overflow-hidden' : 'flex-wrap',
|
isCollapsed ? 'overflow-hidden' : 'flex-wrap',
|
||||||
)}
|
)}
|
||||||
|
ref={linkContainerRef}
|
||||||
>
|
>
|
||||||
{visibleItems.map((link) => (
|
{visibleItems.map((link) => (
|
||||||
<LinkedItemBubble
|
<LinkedItemBubble
|
||||||
@@ -148,9 +177,10 @@ const LinkedItemBubblesContainer = ({ item, linkingController, hideToggle = fals
|
|||||||
setFocusedId={setFocusedId}
|
setFocusedId={setFocusedId}
|
||||||
hoverLabel={`Focus input to add a link (${shortcut})`}
|
hoverLabel={`Focus input to add a link (${shortcut})`}
|
||||||
item={item}
|
item={item}
|
||||||
|
ref={linkInputRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{itemsToDisplay.length > 0 && !hideToggle && (
|
{itemsToDisplay.length > 0 && !shouldHideToggle && (
|
||||||
<RoundIconButton
|
<RoundIconButton
|
||||||
id="toggle-linking-container"
|
id="toggle-linking-container"
|
||||||
label="Toggle linked items container"
|
label="Toggle linked items container"
|
||||||
|
|||||||
Reference in New Issue
Block a user