feat: Added search bar to navigation panel for searching tags and smart views (#2815)
This commit is contained in:
@@ -19,7 +19,10 @@ export interface NotesAndFilesDisplayOptions extends GenericDisplayOptions {
|
|||||||
customFilter?: DisplayControllerCustomFilter
|
customFilter?: DisplayControllerCustomFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TagsDisplayOptions = GenericDisplayOptions
|
export interface TagsAndViewsDisplayOptions extends GenericDisplayOptions {
|
||||||
|
searchQuery?: SearchQuery
|
||||||
|
customFilter?: DisplayControllerCustomFilter
|
||||||
|
}
|
||||||
|
|
||||||
export interface DisplayControllerDisplayOptions extends GenericDisplayOptions {
|
export interface DisplayControllerDisplayOptions extends GenericDisplayOptions {
|
||||||
sortBy: CollectionSortProperty
|
sortBy: CollectionSortProperty
|
||||||
@@ -27,5 +30,5 @@ export interface DisplayControllerDisplayOptions extends GenericDisplayOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type NotesAndFilesDisplayControllerOptions = NotesAndFilesDisplayOptions & DisplayControllerDisplayOptions
|
export type NotesAndFilesDisplayControllerOptions = NotesAndFilesDisplayOptions & DisplayControllerDisplayOptions
|
||||||
export type TagsDisplayControllerOptions = TagsDisplayOptions & DisplayControllerDisplayOptions
|
export type TagsDisplayControllerOptions = TagsAndViewsDisplayOptions & DisplayControllerDisplayOptions
|
||||||
export type AnyDisplayOptions = NotesAndFilesDisplayOptions | TagsDisplayOptions | GenericDisplayOptions
|
export type AnyDisplayOptions = NotesAndFilesDisplayOptions | TagsAndViewsDisplayOptions | GenericDisplayOptions
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
NotesAndFilesDisplayControllerOptions,
|
NotesAndFilesDisplayControllerOptions,
|
||||||
ComponentInterface,
|
ComponentInterface,
|
||||||
ItemStream,
|
ItemStream,
|
||||||
|
TagsAndViewsDisplayOptions,
|
||||||
} from '@standardnotes/models'
|
} from '@standardnotes/models'
|
||||||
import { AbstractService } from '../Service/AbstractService'
|
import { AbstractService } from '../Service/AbstractService'
|
||||||
|
|
||||||
@@ -130,6 +131,7 @@ export interface ItemManagerInterface extends AbstractService {
|
|||||||
getDisplayableNotes(): SNNote[]
|
getDisplayableNotes(): SNNote[]
|
||||||
getDisplayableNotesAndFiles(): (SNNote | FileItem)[]
|
getDisplayableNotesAndFiles(): (SNNote | FileItem)[]
|
||||||
setPrimaryItemDisplayOptions(options: NotesAndFilesDisplayControllerOptions): void
|
setPrimaryItemDisplayOptions(options: NotesAndFilesDisplayControllerOptions): void
|
||||||
|
setTagsAndViewsDisplayOptions(options: TagsAndViewsDisplayOptions): void
|
||||||
getTagPrefixTitle(tag: SNTag): string | undefined
|
getTagPrefixTitle(tag: SNTag): string | undefined
|
||||||
getItemLinkedFiles(item: DecryptedItemInterface): FileItem[]
|
getItemLinkedFiles(item: DecryptedItemInterface): FileItem[]
|
||||||
getItemLinkedNotes(item: DecryptedItemInterface): SNNote[]
|
getItemLinkedNotes(item: DecryptedItemInterface): SNNote[]
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ export class ItemManager extends Services.AbstractService implements Services.It
|
|||||||
Models.SNNote | Models.FileItem,
|
Models.SNNote | Models.FileItem,
|
||||||
Models.NotesAndFilesDisplayOptions
|
Models.NotesAndFilesDisplayOptions
|
||||||
>
|
>
|
||||||
private tagDisplayController!: Models.ItemDisplayController<Models.SNTag, Models.TagsDisplayOptions>
|
private tagDisplayController!: Models.ItemDisplayController<Models.SNTag, Models.TagsAndViewsDisplayOptions>
|
||||||
private itemsKeyDisplayController!: Models.ItemDisplayController<SNItemsKey>
|
private itemsKeyDisplayController!: Models.ItemDisplayController<SNItemsKey>
|
||||||
private componentDisplayController!: Models.ItemDisplayController<Models.ComponentInterface>
|
private componentDisplayController!: Models.ItemDisplayController<Models.ComponentInterface>
|
||||||
private themeDisplayController!: Models.ItemDisplayController<Models.ComponentInterface>
|
private themeDisplayController!: Models.ItemDisplayController<Models.ComponentInterface>
|
||||||
private fileDisplayController!: Models.ItemDisplayController<Models.FileItem>
|
private fileDisplayController!: Models.ItemDisplayController<Models.FileItem>
|
||||||
private smartViewDisplayController!: Models.ItemDisplayController<Models.SmartView>
|
private smartViewDisplayController!: Models.ItemDisplayController<Models.SmartView, Models.TagsAndViewsDisplayOptions>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private payloadManager: PayloadManager,
|
private payloadManager: PayloadManager,
|
||||||
@@ -73,10 +73,14 @@ export class ItemManager extends Services.AbstractService implements Services.It
|
|||||||
hiddenContentTypes: [],
|
hiddenContentTypes: [],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
this.tagDisplayController = new Models.ItemDisplayController(this.collection, [ContentType.TYPES.Tag], {
|
this.tagDisplayController = new Models.ItemDisplayController<Models.SNTag, Models.TagsAndViewsDisplayOptions>(
|
||||||
sortBy: 'title',
|
this.collection,
|
||||||
sortDirection: 'asc',
|
[ContentType.TYPES.Tag],
|
||||||
})
|
{
|
||||||
|
sortBy: 'title',
|
||||||
|
sortDirection: 'asc',
|
||||||
|
},
|
||||||
|
)
|
||||||
this.itemsKeyDisplayController = new Models.ItemDisplayController(this.collection, [ContentType.TYPES.ItemsKey], {
|
this.itemsKeyDisplayController = new Models.ItemDisplayController(this.collection, [ContentType.TYPES.ItemsKey], {
|
||||||
sortBy: 'created_at',
|
sortBy: 'created_at',
|
||||||
sortDirection: 'asc',
|
sortDirection: 'asc',
|
||||||
@@ -89,7 +93,10 @@ export class ItemManager extends Services.AbstractService implements Services.It
|
|||||||
sortBy: 'title',
|
sortBy: 'title',
|
||||||
sortDirection: 'asc',
|
sortDirection: 'asc',
|
||||||
})
|
})
|
||||||
this.smartViewDisplayController = new Models.ItemDisplayController(this.collection, [ContentType.TYPES.SmartView], {
|
this.smartViewDisplayController = new Models.ItemDisplayController<
|
||||||
|
Models.SmartView,
|
||||||
|
Models.TagsAndViewsDisplayOptions
|
||||||
|
>(this.collection, [ContentType.TYPES.SmartView], {
|
||||||
sortBy: 'title',
|
sortBy: 'title',
|
||||||
sortDirection: 'asc',
|
sortDirection: 'asc',
|
||||||
})
|
})
|
||||||
@@ -194,6 +201,16 @@ export class ItemManager extends Services.AbstractService implements Services.It
|
|||||||
this.itemCounter.setDisplayOptions(updatedOptions)
|
this.itemCounter.setDisplayOptions(updatedOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setTagsAndViewsDisplayOptions(options: Models.TagsAndViewsDisplayOptions): void {
|
||||||
|
const updatedOptions: Models.TagsAndViewsDisplayOptions = {
|
||||||
|
customFilter: Models.computeUnifiedFilterForDisplayOptions(options, this.collection),
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tagDisplayController.setDisplayOptions(updatedOptions)
|
||||||
|
this.smartViewDisplayController.setDisplayOptions(updatedOptions)
|
||||||
|
}
|
||||||
|
|
||||||
public setVaultDisplayOptions(options: Models.VaultDisplayOptions): void {
|
public setVaultDisplayOptions(options: Models.VaultDisplayOptions): void {
|
||||||
this.navigationDisplayController.setVaultDisplayOptions(options)
|
this.navigationDisplayController.setVaultDisplayOptions(options)
|
||||||
this.tagDisplayController.setVaultDisplayOptions(options)
|
this.tagDisplayController.setVaultDisplayOptions(options)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
KeyboardEventHandler,
|
KeyboardEventHandler,
|
||||||
useCallback,
|
useCallback,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useRef,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||||
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
|
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
|
||||||
@@ -33,7 +33,7 @@ const Menu = forwardRef(
|
|||||||
}: MenuProps,
|
}: MenuProps,
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
) => {
|
) => {
|
||||||
const menuElementRef = useRef<HTMLMenuElement>(null)
|
const [menuElement, setMenuElement] = useState<HTMLMenuElement | null>(null)
|
||||||
|
|
||||||
const handleKeyDown: KeyboardEventHandler<HTMLMenuElement> = useCallback(
|
const handleKeyDown: KeyboardEventHandler<HTMLMenuElement> = useCallback(
|
||||||
(event) => {
|
(event) => {
|
||||||
@@ -49,11 +49,10 @@ const Menu = forwardRef(
|
|||||||
|
|
||||||
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||||
|
|
||||||
const { setInitialFocus } = useListKeyboardNavigation(
|
const { setInitialFocus } = useListKeyboardNavigation(menuElement, {
|
||||||
menuElementRef,
|
|
||||||
initialFocus,
|
initialFocus,
|
||||||
isMobileScreen ? false : shouldAutoFocus,
|
shouldAutoFocus: isMobileScreen ? false : shouldAutoFocus,
|
||||||
)
|
})
|
||||||
|
|
||||||
useImperativeHandle(forwardedRef, () => ({
|
useImperativeHandle(forwardedRef, () => ({
|
||||||
focus: () => {
|
focus: () => {
|
||||||
@@ -65,7 +64,7 @@ const Menu = forwardRef(
|
|||||||
<menu
|
<menu
|
||||||
className={`m-0 list-none px-4 focus:shadow-none md:px-0 ${className}`}
|
className={`m-0 list-none px-4 focus:shadow-none md:px-0 ${className}`}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
ref={mergeRefs([menuElementRef, forwardedRef])}
|
ref={mergeRefs([setMenuElement, forwardedRef])}
|
||||||
style={style}
|
style={style}
|
||||||
aria-label={a11yLabel}
|
aria-label={a11yLabel}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NoteType, SNNote, classNames } from '@standardnotes/snjs'
|
import { NoteType, SNNote, classNames } from '@standardnotes/snjs'
|
||||||
import Modal, { ModalAction } from '../../Modal/Modal'
|
import Modal, { ModalAction } from '../../Modal/Modal'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||||
import { useApplication } from '../../ApplicationProvider'
|
import { useApplication } from '../../ApplicationProvider'
|
||||||
import { confirmDialog } from '@standardnotes/ui-services'
|
import { confirmDialog } from '@standardnotes/ui-services'
|
||||||
@@ -134,8 +134,8 @@ const NoteConflictResolutionModal = ({
|
|||||||
[close],
|
[close],
|
||||||
)
|
)
|
||||||
|
|
||||||
const listRef = useRef<HTMLDivElement>(null)
|
const [listElement, setListElement] = useState<HTMLDivElement | null>(null)
|
||||||
useListKeyboardNavigation(listRef)
|
useListKeyboardNavigation(listElement)
|
||||||
|
|
||||||
const [selectedMobileTab, setSelectedMobileTab] = useState<'list' | 'preview'>('list')
|
const [selectedMobileTab, setSelectedMobileTab] = useState<'list' | 'preview'>('list')
|
||||||
|
|
||||||
@@ -279,7 +279,7 @@ const NoteConflictResolutionModal = ({
|
|||||||
'w-full overflow-y-auto border-r border-border py-1.5 md:flex md:w-auto md:min-w-60 md:flex-col',
|
'w-full overflow-y-auto border-r border-border py-1.5 md:flex md:w-auto md:min-w-60 md:flex-col',
|
||||||
selectedMobileTab !== 'list' && 'hidden md:flex',
|
selectedMobileTab !== 'list' && 'hidden md:flex',
|
||||||
)}
|
)}
|
||||||
ref={listRef}
|
ref={setListElement}
|
||||||
>
|
>
|
||||||
{allVersions.map((note, index) => (
|
{allVersions.map((note, index) => (
|
||||||
<ConflictListItem
|
<ConflictListItem
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Action } from '@standardnotes/snjs'
|
import { Action } from '@standardnotes/snjs'
|
||||||
import { FunctionComponent, useRef } from 'react'
|
import { FunctionComponent, useState } from 'react'
|
||||||
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
|
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
|
||||||
import HistoryListItem from './HistoryListItem'
|
import HistoryListItem from './HistoryListItem'
|
||||||
import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController'
|
import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController'
|
||||||
@@ -13,16 +13,16 @@ type Props = {
|
|||||||
const LegacyHistoryList: FunctionComponent<Props> = ({ legacyHistory, noteHistoryController, onSelectRevision }) => {
|
const LegacyHistoryList: FunctionComponent<Props> = ({ legacyHistory, noteHistoryController, onSelectRevision }) => {
|
||||||
const { selectLegacyRevision, selectedEntry } = noteHistoryController
|
const { selectLegacyRevision, selectedEntry } = noteHistoryController
|
||||||
|
|
||||||
const legacyHistoryListRef = useRef<HTMLDivElement>(null)
|
const [listElement, setListElement] = useState<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
useListKeyboardNavigation(legacyHistoryListRef)
|
useListKeyboardNavigation(listElement)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex h-full w-full flex-col focus:shadow-none ${
|
className={`flex h-full w-full flex-col focus:shadow-none ${
|
||||||
!legacyHistory?.length ? 'items-center justify-center' : ''
|
!legacyHistory?.length ? 'items-center justify-center' : ''
|
||||||
}`}
|
}`}
|
||||||
ref={legacyHistoryListRef}
|
ref={setListElement}
|
||||||
>
|
>
|
||||||
{legacyHistory?.map((entry) => {
|
{legacyHistory?.map((entry) => {
|
||||||
const selectedEntryUrl = (selectedEntry as Action)?.subactions?.[0].url
|
const selectedEntryUrl = (selectedEntry as Action)?.subactions?.[0].url
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { Fragment, FunctionComponent, useMemo, useRef } from 'react'
|
import { Fragment, FunctionComponent, useMemo, useState } from 'react'
|
||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
|
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
|
||||||
import HistoryListItem from './HistoryListItem'
|
import HistoryListItem from './HistoryListItem'
|
||||||
@@ -22,9 +22,9 @@ const RemoteHistoryList: FunctionComponent<RemoteHistoryListProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { remoteHistory, isFetchingRemoteHistory, selectRemoteRevision, selectedEntry } = noteHistoryController
|
const { remoteHistory, isFetchingRemoteHistory, selectRemoteRevision, selectedEntry } = noteHistoryController
|
||||||
|
|
||||||
const remoteHistoryListRef = useRef<HTMLDivElement>(null)
|
const [listElement, setListElement] = useState<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
useListKeyboardNavigation(remoteHistoryListRef)
|
useListKeyboardNavigation(listElement)
|
||||||
|
|
||||||
const remoteHistoryLength = useMemo(() => remoteHistory?.map((group) => group.entries).flat().length, [remoteHistory])
|
const remoteHistoryLength = useMemo(() => remoteHistory?.map((group) => group.entries).flat().length, [remoteHistory])
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ const RemoteHistoryList: FunctionComponent<RemoteHistoryListProps> = ({
|
|||||||
className={`flex h-full w-full flex-col focus:shadow-none ${
|
className={`flex h-full w-full flex-col focus:shadow-none ${
|
||||||
isFetchingRemoteHistory || !remoteHistoryLength ? 'items-center justify-center' : ''
|
isFetchingRemoteHistory || !remoteHistoryLength ? 'items-center justify-center' : ''
|
||||||
}`}
|
}`}
|
||||||
ref={remoteHistoryListRef}
|
ref={setListElement}
|
||||||
>
|
>
|
||||||
{isFetchingRemoteHistory && <Spinner className="h-5 w-5" />}
|
{isFetchingRemoteHistory && <Spinner className="h-5 w-5" />}
|
||||||
{remoteHistory?.map((group) => {
|
{remoteHistory?.map((group) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Fragment, FunctionComponent, useMemo, useRef } from 'react'
|
import { Fragment, FunctionComponent, useMemo, useState } from 'react'
|
||||||
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
|
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
|
||||||
import HistoryListItem from './HistoryListItem'
|
import HistoryListItem from './HistoryListItem'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
@@ -12,9 +12,9 @@ type Props = {
|
|||||||
const SessionHistoryList: FunctionComponent<Props> = ({ noteHistoryController, onSelectRevision }) => {
|
const SessionHistoryList: FunctionComponent<Props> = ({ noteHistoryController, onSelectRevision }) => {
|
||||||
const { sessionHistory, selectedRevision, selectSessionRevision } = noteHistoryController
|
const { sessionHistory, selectedRevision, selectSessionRevision } = noteHistoryController
|
||||||
|
|
||||||
const sessionHistoryListRef = useRef<HTMLDivElement>(null)
|
const [listElement, setListElement] = useState<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
useListKeyboardNavigation(sessionHistoryListRef)
|
useListKeyboardNavigation(listElement)
|
||||||
|
|
||||||
const sessionHistoryLength = useMemo(
|
const sessionHistoryLength = useMemo(
|
||||||
() => sessionHistory?.map((group) => group.entries).flat().length,
|
() => sessionHistory?.map((group) => group.entries).flat().length,
|
||||||
@@ -26,7 +26,7 @@ const SessionHistoryList: FunctionComponent<Props> = ({ noteHistoryController, o
|
|||||||
className={`flex h-full w-full flex-col focus:shadow-none ${
|
className={`flex h-full w-full flex-col focus:shadow-none ${
|
||||||
!sessionHistoryLength ? 'items-center justify-center' : ''
|
!sessionHistoryLength ? 'items-center justify-center' : ''
|
||||||
}`}
|
}`}
|
||||||
ref={sessionHistoryListRef}
|
ref={setListElement}
|
||||||
>
|
>
|
||||||
{sessionHistory?.map((group) => {
|
{sessionHistory?.map((group) => {
|
||||||
if (group.entries && group.entries.length) {
|
if (group.entries && group.entries.length) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { useAvailableSafeAreaPadding } from '@/Hooks/useSafeAreaPadding'
|
|||||||
import QuickSettingsButton from '../Footer/QuickSettingsButton'
|
import QuickSettingsButton from '../Footer/QuickSettingsButton'
|
||||||
import VaultSelectionButton from '../Footer/VaultSelectionButton'
|
import VaultSelectionButton from '../Footer/VaultSelectionButton'
|
||||||
import PreferencesButton from '../Footer/PreferencesButton'
|
import PreferencesButton from '../Footer/PreferencesButton'
|
||||||
|
import TagSearchBar from './TagSearchBar'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -78,6 +79,7 @@ const Navigation = forwardRef<HTMLDivElement, Props>(({ application, className,
|
|||||||
'md:hover:[overflow-y:_overlay] pointer-coarse:md:overflow-y-auto',
|
'md:hover:[overflow-y:_overlay] pointer-coarse:md:overflow-y-auto',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<TagSearchBar navigationController={application.navigationController} />
|
||||||
<SmartViewsSection
|
<SmartViewsSection
|
||||||
application={application}
|
application={application}
|
||||||
featuresController={application.featuresController}
|
featuresController={application.featuresController}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { FeaturesController } from '@/Controllers/FeaturesController'
|
|||||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||||
import { SmartView } from '@standardnotes/snjs'
|
import { SmartView } from '@standardnotes/snjs'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { FunctionComponent } from 'react'
|
import { FunctionComponent, useState } from 'react'
|
||||||
import SmartViewsListItem from './SmartViewsListItem'
|
import SmartViewsListItem from './SmartViewsListItem'
|
||||||
|
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
navigationController: NavigationController
|
navigationController: NavigationController
|
||||||
@@ -18,8 +19,23 @@ const SmartViewsList: FunctionComponent<Props> = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const allViews = navigationController.smartViews
|
const allViews = navigationController.smartViews
|
||||||
|
|
||||||
|
const [container, setContainer] = useState<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
useListKeyboardNavigation(container, {
|
||||||
|
initialFocus: 0,
|
||||||
|
shouldAutoFocus: false,
|
||||||
|
shouldWrapAround: false,
|
||||||
|
resetLastFocusedOnBlur: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (allViews.length === 0 && navigationController.isSearching) {
|
||||||
|
return (
|
||||||
|
<div className="px-4 py-1 text-base opacity-60 lg:text-sm">No smart views found. Try a different search.</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div ref={setContainer}>
|
||||||
{allViews.map((view) => {
|
{allViews.map((view) => {
|
||||||
return (
|
return (
|
||||||
<SmartViewsListItem
|
<SmartViewsListItem
|
||||||
@@ -31,7 +47,7 @@ const SmartViewsList: FunctionComponent<Props> = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,74 +111,73 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState, setEdit
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<button
|
||||||
<div
|
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||||
role="button"
|
className={classNames(
|
||||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
'tag group px-3.5 py-0.5 focus-visible:!shadow-inner md:py-0',
|
||||||
className={classNames('tag group px-3.5 py-1 md:py-0', isSelected && 'selected', isFaded && 'opacity-50')}
|
isSelected && 'selected',
|
||||||
onClick={selectCurrentTag}
|
isFaded && 'opacity-50',
|
||||||
onContextMenu={(event) => {
|
)}
|
||||||
event.preventDefault()
|
onClick={selectCurrentTag}
|
||||||
event.stopPropagation()
|
onContextMenu={(event) => {
|
||||||
if (isSystemView(view)) {
|
event.preventDefault()
|
||||||
return
|
event.stopPropagation()
|
||||||
}
|
if (isSystemView(view)) {
|
||||||
onClickEdit()
|
return
|
||||||
}}
|
}
|
||||||
style={{
|
onClickEdit()
|
||||||
paddingLeft: `${level * PADDING_PER_LEVEL_PX + PADDING_BASE_PX}px`,
|
}}
|
||||||
}}
|
style={{
|
||||||
>
|
paddingLeft: `${level * PADDING_PER_LEVEL_PX + PADDING_BASE_PX}px`,
|
||||||
<div className="tag-info">
|
}}
|
||||||
<div className={'tag-icon mr-2'}>
|
>
|
||||||
<Icon type={view.iconString} className={classNames(iconClass, 'group-hover:text-text')} />
|
<div className="tag-info">
|
||||||
</div>
|
<div className={'tag-icon mr-2'}>
|
||||||
{isEditing ? (
|
<Icon type={view.iconString} className={classNames(iconClass, 'group-hover:text-text')} />
|
||||||
<input
|
|
||||||
className={'title editing text-mobile-navigation-list-item lg:text-navigation-list-item'}
|
|
||||||
id={`react-tag-${view.uuid}`}
|
|
||||||
onBlur={onBlur}
|
|
||||||
onInput={onInput}
|
|
||||||
value={title}
|
|
||||||
onKeyUp={onKeyUp}
|
|
||||||
spellCheck={false}
|
|
||||||
ref={inputRef}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
'title overflow-hidden text-left text-mobile-navigation-list-item lg:text-navigation-list-item'
|
|
||||||
}
|
|
||||||
id={`react-tag-${view.uuid}`}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={'count text-base lg:text-sm'}>
|
|
||||||
{view.uuid === SystemViewId.AllNotes && tagsState.allNotesCount}
|
|
||||||
{view.uuid === SystemViewId.Files && tagsState.allFilesCount}
|
|
||||||
{view.uuid === SystemViewId.Conflicts && conflictsCount}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{isEditing ? (
|
||||||
{!isSystemView(view) && (
|
<input
|
||||||
<div className="meta">
|
className={'title editing text-mobile-navigation-list-item lg:text-navigation-list-item'}
|
||||||
{view.conflictOf && <div className="-mt-1 text-[0.625rem] font-bold text-danger">Conflicted Copy</div>}
|
id={`react-tag-${view.uuid}`}
|
||||||
|
onBlur={onBlur}
|
||||||
{isSelected && (
|
onInput={onInput}
|
||||||
<div className="menu">
|
value={title}
|
||||||
<a className="item" onClick={onClickEdit}>
|
onKeyUp={onKeyUp}
|
||||||
Edit
|
spellCheck={false}
|
||||||
</a>
|
ref={inputRef}
|
||||||
<a className="item" onClick={onClickDelete}>
|
/>
|
||||||
Delete
|
) : (
|
||||||
</a>
|
<div
|
||||||
</div>
|
className={'title overflow-hidden text-left text-mobile-navigation-list-item lg:text-navigation-list-item'}
|
||||||
)}
|
id={`react-tag-${view.uuid}`}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className={'count text-base lg:text-sm'}>
|
||||||
|
{view.uuid === SystemViewId.AllNotes && tagsState.allNotesCount}
|
||||||
|
{view.uuid === SystemViewId.Files && tagsState.allFilesCount}
|
||||||
|
{view.uuid === SystemViewId.Conflicts && conflictsCount}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
|
{!isSystemView(view) && (
|
||||||
|
<div className="meta">
|
||||||
|
{view.conflictOf && <div className="-mt-1 text-[0.625rem] font-bold text-danger">Conflicted Copy</div>}
|
||||||
|
|
||||||
|
{isSelected && (
|
||||||
|
<div className="menu">
|
||||||
|
<a className="item" onClick={onClickEdit}>
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
<a className="item" onClick={onClickDelete}>
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,13 +40,15 @@ const SmartViewsSection: FunctionComponent<Props> = ({ application, navigationCo
|
|||||||
<div className="title text-base md:text-sm">
|
<div className="title text-base md:text-sm">
|
||||||
<span className="font-bold">Views</span>
|
<span className="font-bold">Views</span>
|
||||||
</div>
|
</div>
|
||||||
<IconButton
|
{!navigationController.isSearching && (
|
||||||
focusable={true}
|
<IconButton
|
||||||
icon="add"
|
focusable={true}
|
||||||
title="Create a new smart view"
|
icon="add"
|
||||||
className="p-0 text-neutral"
|
title="Create a new smart view"
|
||||||
onClick={createNewSmartView}
|
className="p-0 text-neutral"
|
||||||
/>
|
onClick={createNewSmartView}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SmartViewsList
|
<SmartViewsList
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||||
|
import Icon from '../Icon/Icon'
|
||||||
|
import DecoratedInput from '../Input/DecoratedInput'
|
||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import ClearInputButton from '../ClearInputButton/ClearInputButton'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { classNames } from '@standardnotes/snjs'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
navigationController: NavigationController
|
||||||
|
}
|
||||||
|
|
||||||
|
const TagSearchBar = ({ navigationController }: Props) => {
|
||||||
|
const { searchQuery, setSearchQuery } = navigationController
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const onClearSearch = useCallback(() => {
|
||||||
|
setSearchQuery('')
|
||||||
|
inputRef.current?.focus()
|
||||||
|
}, [setSearchQuery])
|
||||||
|
|
||||||
|
const [isParentScrolling, setIsParentScrolling] = useState(false)
|
||||||
|
const searchBarRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const searchBar = searchBarRef.current
|
||||||
|
if (!searchBar) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = searchBar.parentElement
|
||||||
|
if (!parent) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollListener = () => {
|
||||||
|
const { scrollTop } = parent
|
||||||
|
setIsParentScrolling(scrollTop > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.addEventListener('scroll', scrollListener)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
parent.removeEventListener('scroll', scrollListener)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'sticky top-0 bg-[inherit] px-4 pt-4',
|
||||||
|
isParentScrolling &&
|
||||||
|
'after:absolute after:left-0 after:top-full after:-z-[1] after:block after:h-4 after:w-full after:border-b after:border-border after:bg-[inherit]',
|
||||||
|
)}
|
||||||
|
role="search"
|
||||||
|
ref={searchBarRef}
|
||||||
|
>
|
||||||
|
<DecoratedInput
|
||||||
|
ref={inputRef}
|
||||||
|
autocomplete={false}
|
||||||
|
className={{
|
||||||
|
container: '!bg-default px-1',
|
||||||
|
input: 'text-base placeholder:text-passive-0 lg:text-sm',
|
||||||
|
}}
|
||||||
|
placeholder={'Search tags...'}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={setSearchQuery}
|
||||||
|
left={[<Icon type="search" className="mr-1 h-4.5 w-4.5 flex-shrink-0 text-passive-1" />]}
|
||||||
|
right={[searchQuery && <ClearInputButton onClick={onClearSearch} />]}
|
||||||
|
roundedFull
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(TagSearchBar)
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { SNTag } from '@standardnotes/snjs'
|
import { SNTag } from '@standardnotes/snjs'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { FunctionComponent, useCallback } from 'react'
|
import { FunctionComponent, useCallback, useState } from 'react'
|
||||||
import RootTagDropZone from './RootTagDropZone'
|
import RootTagDropZone from './RootTagDropZone'
|
||||||
import { TagListSectionType } from './TagListSection'
|
import { TagListSectionType } from './TagListSection'
|
||||||
import { TagsListItem } from './TagsListItem'
|
import { TagsListItem } from './TagsListItem'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
import { useApplication } from '../ApplicationProvider'
|
||||||
|
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type: TagListSectionType
|
type: TagListSectionType
|
||||||
@@ -32,31 +33,44 @@ const TagsList: FunctionComponent<Props> = ({ type }: Props) => {
|
|||||||
[application, openTagContextMenu],
|
[application, openTagContextMenu],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const [container, setContainer] = useState<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
useListKeyboardNavigation(container, {
|
||||||
|
initialFocus: 0,
|
||||||
|
shouldAutoFocus: false,
|
||||||
|
shouldWrapAround: false,
|
||||||
|
resetLastFocusedOnBlur: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (allTags.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="px-4 text-base opacity-50 lg:text-sm">
|
||||||
|
{application.navigationController.isSearching
|
||||||
|
? 'No tags found. Try a different search.'
|
||||||
|
: 'No tags or folders. Create one using the add button above.'}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{allTags.length === 0 ? (
|
<div ref={setContainer}>
|
||||||
<div className="no-tags-placeholder text-base opacity-[0.4] lg:text-sm">
|
{allTags.map((tag) => {
|
||||||
No tags or folders. Create one using the add button above.
|
return (
|
||||||
</div>
|
<TagsListItem
|
||||||
) : (
|
level={0}
|
||||||
<>
|
key={tag.uuid}
|
||||||
{allTags.map((tag) => {
|
tag={tag}
|
||||||
return (
|
type={type}
|
||||||
<TagsListItem
|
navigationController={application.navigationController}
|
||||||
level={0}
|
features={application.featuresController}
|
||||||
key={tag.uuid}
|
linkingController={application.linkingController}
|
||||||
tag={tag}
|
onContextMenu={onContextMenu}
|
||||||
type={type}
|
/>
|
||||||
navigationController={application.navigationController}
|
)
|
||||||
features={application.featuresController}
|
})}
|
||||||
linkingController={application.linkingController}
|
</div>
|
||||||
onContextMenu={onContextMenu}
|
{type === 'all' && <RootTagDropZone tagsState={application.navigationController} />}
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
{type === 'all' && <RootTagDropZone tagsState={application.navigationController} />}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,11 +91,19 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
|||||||
e?.stopPropagation()
|
e?.stopPropagation()
|
||||||
const shouldShowChildren = !showChildren
|
const shouldShowChildren = !showChildren
|
||||||
setShowChildren(shouldShowChildren)
|
setShowChildren(shouldShowChildren)
|
||||||
navigationController.setExpanded(tag, shouldShowChildren)
|
if (!navigationController.isSearching) {
|
||||||
|
navigationController.setExpanded(tag, shouldShowChildren)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[showChildren, tag, navigationController],
|
[showChildren, tag, navigationController],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!navigationController.isSearching) {
|
||||||
|
setShowChildren(tag.expanded)
|
||||||
|
}
|
||||||
|
}, [navigationController.isSearching, tag])
|
||||||
|
|
||||||
const selectCurrentTag = useCallback(async () => {
|
const selectCurrentTag = useCallback(async () => {
|
||||||
await navigationController.setSelectedTag(tag, type, {
|
await navigationController.setSelectedTag(tag, type, {
|
||||||
userTriggered: true,
|
userTriggered: true,
|
||||||
@@ -269,11 +277,16 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'tag group px-3.5 py-1 md:py-0',
|
'tag group px-3.5 py-0.5 focus-visible:!shadow-inner md:py-0',
|
||||||
(isSelected || isContextMenuOpenForTag) && 'selected',
|
(isSelected || isContextMenuOpenForTag) && 'selected',
|
||||||
isBeingDraggedOver && 'is-drag-over',
|
isBeingDraggedOver && 'is-drag-over',
|
||||||
)}
|
)}
|
||||||
onClick={selectCurrentTag}
|
onClick={selectCurrentTag}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === KeyboardKey.Enter || event.key === KeyboardKey.Space) {
|
||||||
|
selectCurrentTag().catch(console.error)
|
||||||
|
}
|
||||||
|
}}
|
||||||
ref={tagRef}
|
ref={tagRef}
|
||||||
style={{
|
style={{
|
||||||
paddingLeft: `${level * PADDING_PER_LEVEL_PX + PADDING_BASE_PX}px`,
|
paddingLeft: `${level * PADDING_PER_LEVEL_PX + PADDING_BASE_PX}px`,
|
||||||
@@ -282,7 +295,7 @@ export const TagsListItem: FunctionComponent<Props> = observer(
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
onContextMenu(tag, type, e.clientX, e.clientY)
|
onContextMenu(tag, type, e.clientX, e.clientY)
|
||||||
}}
|
}}
|
||||||
draggable={true}
|
draggable={!navigationController.isSearching}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnter={onDragEnter}
|
onDragEnter={onDragEnter}
|
||||||
onDragExit={removeDragIndicator}
|
onDragExit={removeDragIndicator}
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ const TagsSection: FunctionComponent = () => {
|
|||||||
hasMigration={hasMigration}
|
hasMigration={hasMigration}
|
||||||
onClickMigration={runMigration}
|
onClickMigration={runMigration}
|
||||||
/>
|
/>
|
||||||
<TagsSectionAddButton tags={application.navigationController} features={application.featuresController} />
|
{!application.navigationController.isSearching && (
|
||||||
|
<TagsSectionAddButton tags={application.navigationController} features={application.featuresController} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TagsList type="all" />
|
<TagsList type="all" />
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
|
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
|
||||||
import { FeaturesController } from '../FeaturesController'
|
import { FeaturesController } from '../FeaturesController'
|
||||||
import { destroyAllObjectProperties } from '@/Utils'
|
import { debounce, destroyAllObjectProperties } from '@/Utils'
|
||||||
import { isValidFutureSiblings, rootTags, tagSiblings } from './Utils'
|
import { isValidFutureSiblings, rootTags, tagSiblings } from './Utils'
|
||||||
import { AnyTag } from './AnyTagType'
|
import { AnyTag } from './AnyTagType'
|
||||||
import { CrossControllerEvent } from '../CrossControllerEvent'
|
import { CrossControllerEvent } from '../CrossControllerEvent'
|
||||||
@@ -65,6 +65,8 @@ export class NavigationController
|
|||||||
contextMenuTag: SNTag | undefined = undefined
|
contextMenuTag: SNTag | undefined = undefined
|
||||||
contextMenuTagSection: TagListSectionType | undefined = undefined
|
contextMenuTagSection: TagListSectionType | undefined = undefined
|
||||||
|
|
||||||
|
searchQuery = ''
|
||||||
|
|
||||||
private readonly tagsCountsState: TagsCountsState
|
private readonly tagsCountsState: TagsCountsState
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -130,6 +132,9 @@ export class NavigationController
|
|||||||
isInFilesView: computed,
|
isInFilesView: computed,
|
||||||
|
|
||||||
hydrateFromPersistedValue: action,
|
hydrateFromPersistedValue: action,
|
||||||
|
|
||||||
|
searchQuery: observable,
|
||||||
|
setSearchQuery: action,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
@@ -196,13 +201,20 @@ export class NavigationController
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.setDisplayOptionsAndReloadTags = debounce(this.setDisplayOptionsAndReloadTags, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
private reloadTags(): void {
|
private reloadTags(): void {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.tags = this.items.getDisplayableTags()
|
this.tags = this.items.getDisplayableTags()
|
||||||
this.starredTags = this.tags.filter((tag) => tag.starred)
|
this.starredTags = this.tags.filter((tag) => tag.starred)
|
||||||
this.smartViews = this.items.getSmartViews()
|
this.smartViews = this.items.getSmartViews().filter((view) => {
|
||||||
|
if (!this.isSearching) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return !isSystemView(view)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +389,7 @@ export class NavigationController
|
|||||||
const children = this.items.getTagChildren(tag)
|
const children = this.items.getTagChildren(tag)
|
||||||
|
|
||||||
const childrenUuids = children.map((childTag) => childTag.uuid)
|
const childrenUuids = children.map((childTag) => childTag.uuid)
|
||||||
const childrenTags = this.tags.filter((tag) => childrenUuids.includes(tag.uuid))
|
const childrenTags = this.isSearching ? children : this.tags.filter((tag) => childrenUuids.includes(tag.uuid))
|
||||||
return childrenTags
|
return childrenTags
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,4 +668,23 @@ export class NavigationController
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setDisplayOptionsAndReloadTags = () => {
|
||||||
|
this.items.setTagsAndViewsDisplayOptions({
|
||||||
|
searchQuery: {
|
||||||
|
query: this.searchQuery,
|
||||||
|
includeProtectedNoteText: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.reloadTags()
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSearchQuery = (query: string) => {
|
||||||
|
this.searchQuery = query
|
||||||
|
this.setDisplayOptionsAndReloadTags()
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isSearching(): boolean {
|
||||||
|
return this.searchQuery.length > 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,81 @@
|
|||||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||||
import { useCallback, useEffect, RefObject, useRef } from 'react'
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
initialFocus?: number
|
||||||
|
shouldAutoFocus?: boolean
|
||||||
|
shouldWrapAround?: boolean
|
||||||
|
resetLastFocusedOnBlur?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useListKeyboardNavigation = (containerElement: HTMLElement | null, options?: Options) => {
|
||||||
|
const {
|
||||||
|
initialFocus = 0,
|
||||||
|
shouldAutoFocus = false,
|
||||||
|
shouldWrapAround = true,
|
||||||
|
resetLastFocusedOnBlur = false,
|
||||||
|
} = options || {}
|
||||||
|
|
||||||
export const useListKeyboardNavigation = (
|
|
||||||
container: RefObject<HTMLElement | null>,
|
|
||||||
initialFocus = 0,
|
|
||||||
shouldAutoFocus = false,
|
|
||||||
) => {
|
|
||||||
const listItems = useRef<HTMLButtonElement[]>([])
|
const listItems = useRef<HTMLButtonElement[]>([])
|
||||||
|
const setLatestListItems = useCallback(() => {
|
||||||
|
if (!containerElement) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
listItems.current = Array.from(containerElement.querySelectorAll('button, div[role="button"]'))
|
||||||
|
if (listItems.current.length > 0) {
|
||||||
|
listItems.current[0].tabIndex = 0
|
||||||
|
}
|
||||||
|
}, [containerElement])
|
||||||
|
|
||||||
const focusedItemIndex = useRef<number>(initialFocus)
|
const focusedItemIndex = useRef<number>(initialFocus)
|
||||||
|
|
||||||
const focusItemWithIndex = useCallback((index: number, items?: HTMLButtonElement[]) => {
|
const focusItemWithIndex = useCallback((index: number) => {
|
||||||
focusedItemIndex.current = index
|
focusedItemIndex.current = index
|
||||||
if (items && items.length > 0) {
|
listItems.current[index]?.focus()
|
||||||
items[index]?.focus()
|
|
||||||
} else {
|
|
||||||
listItems.current[index]?.focus()
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const getNextFocusableIndex = useCallback((currentIndex: number, items: HTMLButtonElement[]) => {
|
const getNextFocusableIndex = useCallback(
|
||||||
let nextIndex = currentIndex + 1
|
(currentIndex: number, items: HTMLButtonElement[]) => {
|
||||||
if (nextIndex > items.length - 1) {
|
let nextIndex = currentIndex + 1
|
||||||
nextIndex = 0
|
|
||||||
}
|
|
||||||
while (items[nextIndex].disabled) {
|
|
||||||
nextIndex++
|
|
||||||
if (nextIndex > items.length - 1) {
|
if (nextIndex > items.length - 1) {
|
||||||
nextIndex = 0
|
nextIndex = shouldWrapAround ? 0 : currentIndex
|
||||||
}
|
}
|
||||||
}
|
while (items[nextIndex].disabled) {
|
||||||
return nextIndex
|
nextIndex++
|
||||||
}, [])
|
if (nextIndex > items.length - 1) {
|
||||||
|
nextIndex = shouldWrapAround ? 0 : currentIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nextIndex
|
||||||
|
},
|
||||||
|
[shouldWrapAround],
|
||||||
|
)
|
||||||
|
|
||||||
const getPreviousFocusableIndex = useCallback((currentIndex: number, items: HTMLButtonElement[]) => {
|
const getPreviousFocusableIndex = useCallback(
|
||||||
let previousIndex = currentIndex - 1
|
(currentIndex: number, items: HTMLButtonElement[]) => {
|
||||||
if (previousIndex < 0) {
|
let previousIndex = currentIndex - 1
|
||||||
previousIndex = items.length - 1
|
|
||||||
}
|
|
||||||
while (items[previousIndex].disabled) {
|
|
||||||
previousIndex--
|
|
||||||
if (previousIndex < 0) {
|
if (previousIndex < 0) {
|
||||||
previousIndex = items.length - 1
|
previousIndex = shouldWrapAround ? items.length - 1 : currentIndex
|
||||||
}
|
}
|
||||||
}
|
while (items[previousIndex].disabled) {
|
||||||
return previousIndex
|
previousIndex--
|
||||||
}, [])
|
if (previousIndex < 0) {
|
||||||
|
previousIndex = shouldWrapAround ? items.length - 1 : currentIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return previousIndex
|
||||||
|
},
|
||||||
|
[shouldWrapAround],
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (container.current) {
|
if (containerElement) {
|
||||||
container.current.tabIndex = FOCUSABLE_BUT_NOT_TABBABLE
|
containerElement.tabIndex = FOCUSABLE_BUT_NOT_TABBABLE
|
||||||
listItems.current = Array.from(container.current.querySelectorAll('button'))
|
setLatestListItems()
|
||||||
|
listItems.current[0].tabIndex = 0
|
||||||
}
|
}
|
||||||
}, [container])
|
}, [containerElement, setLatestListItems])
|
||||||
|
|
||||||
const keyDownHandler = useCallback(
|
const keyDownHandler = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
@@ -95,7 +118,7 @@ export const useListKeyboardNavigation = (
|
|||||||
let indexToFocus = selectedItemIndex > -1 ? selectedItemIndex : initialFocus
|
let indexToFocus = selectedItemIndex > -1 ? selectedItemIndex : initialFocus
|
||||||
indexToFocus = getNextFocusableIndex(indexToFocus - 1, items)
|
indexToFocus = getNextFocusableIndex(indexToFocus - 1, items)
|
||||||
|
|
||||||
focusItemWithIndex(indexToFocus, items)
|
focusItemWithIndex(indexToFocus)
|
||||||
}, [focusItemWithIndex, getNextFocusableIndex, initialFocus])
|
}, [focusItemWithIndex, getNextFocusableIndex, initialFocus])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -106,23 +129,27 @@ export const useListKeyboardNavigation = (
|
|||||||
}
|
}
|
||||||
}, [setInitialFocus, shouldAutoFocus])
|
}, [setInitialFocus, shouldAutoFocus])
|
||||||
|
|
||||||
useEffect(() => {
|
const focusOutHandler = useCallback(
|
||||||
if (listItems.current.length > 0) {
|
(event: FocusEvent) => {
|
||||||
listItems.current[0].tabIndex = 0
|
const isFocusInContainer = containerElement && containerElement.contains(event.relatedTarget as Node)
|
||||||
}
|
if (isFocusInContainer || !resetLastFocusedOnBlur) {
|
||||||
}, [])
|
return
|
||||||
|
}
|
||||||
|
focusedItemIndex.current = initialFocus
|
||||||
|
},
|
||||||
|
[containerElement, initialFocus, resetLastFocusedOnBlur],
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const containerElement = container.current
|
|
||||||
|
|
||||||
if (!containerElement) {
|
if (!containerElement) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
containerElement.addEventListener('keydown', keyDownHandler)
|
containerElement.addEventListener('keydown', keyDownHandler)
|
||||||
|
containerElement.addEventListener('focusout', focusOutHandler)
|
||||||
|
|
||||||
const containerMutationObserver = new MutationObserver(() => {
|
const containerMutationObserver = new MutationObserver(() => {
|
||||||
listItems.current = Array.from(containerElement.querySelectorAll('button'))
|
setLatestListItems()
|
||||||
})
|
})
|
||||||
|
|
||||||
containerMutationObserver.observe(containerElement, {
|
containerMutationObserver.observe(containerElement, {
|
||||||
@@ -131,10 +158,11 @@ export const useListKeyboardNavigation = (
|
|||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
containerElement?.removeEventListener('keydown', keyDownHandler)
|
containerElement.removeEventListener('keydown', keyDownHandler)
|
||||||
|
containerElement.removeEventListener('focusout', focusOutHandler)
|
||||||
containerMutationObserver.disconnect()
|
containerMutationObserver.disconnect()
|
||||||
}
|
}
|
||||||
}, [container, setInitialFocus, keyDownHandler])
|
}, [setInitialFocus, keyDownHandler, focusOutHandler, containerElement, setLatestListItems])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setInitialFocus,
|
setInitialFocus,
|
||||||
|
|||||||
@@ -26,10 +26,6 @@ $content-horizontal-padding: 16px;
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-tags-placeholder {
|
|
||||||
padding: 0px $content-horizontal-padding;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root-drop {
|
.root-drop {
|
||||||
width: '100%';
|
width: '100%';
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
@@ -50,6 +46,10 @@ $content-horizontal-padding: 16px;
|
|||||||
.tag {
|
.tag {
|
||||||
border: 0;
|
border: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
|
&:focus:not(.selected) {
|
||||||
|
background-color: var(--navigation-item-selected-background-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag,
|
.tag,
|
||||||
|
|||||||
Reference in New Issue
Block a user