Files
tolaria/src/hooks/useNeighborhoodSelection.ts

148 lines
4.6 KiB
TypeScript

import { useCallback, useEffect } from 'react'
import type { MutableRefObject } from 'react'
import type { SidebarSelection, VaultEntry } from '../types'
import type { NoteListFilter } from '../utils/noteListHelpers'
import { trackEvent } from '../lib/telemetry'
import {
focusNoteListContainer,
isEditableElement,
isEditorEscapeTarget,
popNeighborhoodHistory,
pushNeighborhoodHistory,
resolveNeighborhoodSelection,
shouldProcessNeighborhoodEscape,
} from '../utils/neighborhoodHistory'
interface SetSelectionOptions {
preserveNeighborhoodHistory?: boolean
}
type SetSelection = (selection: SidebarSelection, options?: SetSelectionOptions) => void
interface NeighborhoodRefs {
neighborhoodHistoryRef: MutableRefObject<SidebarSelection[]>
selectionRef: MutableRefObject<SidebarSelection>
}
interface UseNeighborhoodEntryOptions extends NeighborhoodRefs {
setSelection: SetSelection
}
interface UseSelectionSanitizerOptions extends NeighborhoodRefs {
effectiveSelection: SidebarSelection
selection: SidebarSelection
setNoteListFilter: (filter: NoteListFilter) => void
setSelection: (selection: SidebarSelection) => void
}
interface UseNeighborhoodHistoryBackOptions {
neighborhoodHistoryRef: MutableRefObject<SidebarSelection[]>
setSelection: SetSelection
}
interface UseNeighborhoodEscapeOptions {
onBack: () => boolean
selectionRef: MutableRefObject<SidebarSelection>
shouldBlockNeighborhoodEscape: boolean
}
function focusNoteListOnNextFrame(): void {
requestAnimationFrame(() => {
focusNoteListContainer(document)
})
}
export function useNeighborhoodEntry({
neighborhoodHistoryRef,
selectionRef,
setSelection,
}: UseNeighborhoodEntryOptions) {
return useCallback((entry: VaultEntry) => {
const currentSelection = selectionRef.current
const nextSelection = resolveNeighborhoodSelection(currentSelection, entry)
trackEvent('neighborhood_mode_toggled', { action: nextSelection.action })
if (nextSelection.action === 'exit') {
const { previousSelection, nextHistory } = popNeighborhoodHistory(neighborhoodHistoryRef.current)
neighborhoodHistoryRef.current = nextHistory
setSelection(previousSelection ?? nextSelection.selection, previousSelection ? { preserveNeighborhoodHistory: true } : undefined)
focusNoteListOnNextFrame()
return
}
neighborhoodHistoryRef.current = pushNeighborhoodHistory(
neighborhoodHistoryRef.current,
currentSelection,
nextSelection.selection,
)
setSelection(nextSelection.selection, { preserveNeighborhoodHistory: true })
}, [neighborhoodHistoryRef, selectionRef, setSelection])
}
export function useSelectionSanitizer({
effectiveSelection,
neighborhoodHistoryRef,
selection,
selectionRef,
setNoteListFilter,
setSelection,
}: UseSelectionSanitizerOptions): void {
useEffect(() => {
selectionRef.current = effectiveSelection
}, [effectiveSelection, selectionRef])
useEffect(() => {
if (effectiveSelection === selection) return
if (effectiveSelection.kind !== 'entity') {
neighborhoodHistoryRef.current = []
}
setSelection(effectiveSelection)
setNoteListFilter('open')
}, [effectiveSelection, neighborhoodHistoryRef, selection, setNoteListFilter, setSelection])
}
export function useNeighborhoodHistoryBack({
neighborhoodHistoryRef,
setSelection,
}: UseNeighborhoodHistoryBackOptions) {
return useCallback(() => {
const { previousSelection, nextHistory } = popNeighborhoodHistory(neighborhoodHistoryRef.current)
if (!previousSelection) return false
neighborhoodHistoryRef.current = nextHistory
setSelection(previousSelection, { preserveNeighborhoodHistory: true })
focusNoteListOnNextFrame()
return true
}, [neighborhoodHistoryRef, setSelection])
}
export function useNeighborhoodEscape({
onBack,
selectionRef,
shouldBlockNeighborhoodEscape,
}: UseNeighborhoodEscapeOptions): void {
useEffect(() => {
const handleWindowKeyDown = (event: KeyboardEvent) => {
if (!shouldProcessNeighborhoodEscape(event, selectionRef.current, shouldBlockNeighborhoodEscape)) return
const activeElement = document.activeElement
if (isEditorEscapeTarget(activeElement)) {
event.preventDefault()
activeElement.blur()
focusNoteListOnNextFrame()
return
}
if (isEditableElement(activeElement)) return
if (onBack()) {
event.preventDefault()
}
}
window.addEventListener('keydown', handleWindowKeyDown)
return () => window.removeEventListener('keydown', handleWindowKeyDown)
}, [onBack, selectionRef, shouldBlockNeighborhoodEscape])
}