ElectronicBabylonianLiterature/ebl-frontend

View on GitHub
src/afo-register/ui/AfoRegisterTextSelect.tsx

Summary

Maintainability
A
0 mins
Test Coverage
A
90%
import React, { useEffect, useState } from 'react'
import { AfoRegisterRecordSuggestion } from 'afo-register/domain/Record'
import { usePrevious } from 'common/usePrevious'
import { ValueType } from 'react-select'
import AsyncSelect from 'react-select/async'
import { Markdown } from 'common/Markdown'

interface SelectProps {
  ariaLabel: string
  value: AfoRegisterRecordSuggestion | null
  searchSuggestions: (
    query: string
  ) => Promise<readonly AfoRegisterRecordSuggestion[]>
  onChange: (event: AfoRegisterRecordSuggestion) => void
  isClearable: boolean
}

interface TextSuggestionOption {
  value: string
  label: string | JSX.Element
  entry: AfoRegisterRecordSuggestion
}

const collator = new Intl.Collator([], { numeric: true })
function sorter(a: string, b: string, inputValue: string) {
  const removeSpecialChars = /[*^]/g
  const cleanedA = a.replace(removeSpecialChars, '')
  const cleanedB = b.replace(removeSpecialChars, '')
  const regex = new RegExp(`^${inputValue}`, 'i')
  const startsWithQueryA = regex.test(cleanedA)
  const startsWithQueryB = regex.test(cleanedB)
  if (startsWithQueryA === startsWithQueryB) {
    return collator.compare(cleanedA, cleanedB)
  }
  return startsWithQueryA ? -1 : 1
}

function createOption(
  recordSuggestion: AfoRegisterRecordSuggestion
): TextSuggestionOption {
  return {
    value: recordSuggestion.text,
    label: recordSuggestion.text,
    entry: recordSuggestion,
  }
}

export default function AfoRegisterTextSelect({
  ariaLabel,
  value,
  searchSuggestions,
  onChange,
  isClearable,
}: SelectProps): JSX.Element {
  const [
    selectedOption,
    setSelectedOption,
  ] = useState<TextSuggestionOption | null>(value ? createOption(value) : null)
  const prevValue = usePrevious(value)

  useEffect(() => {
    if (value && value !== prevValue) {
      setSelectedOption(createOption(value))
    }
  }, [value, prevValue])

  const loadOptions = (
    inputValue: string,
    callback: (options: TextSuggestionOption[]) => void
  ) => {
    searchSuggestions(inputValue).then((entries) => {
      const options = entries
        .map(createOption)
        .filter((option) => option !== null) as TextSuggestionOption[]
      options.sort((a, b) => sorter(a.value, b.value, inputValue))
      callback(options)
    })
  }

  const handleChange = (
    selectedOption: ValueType<TextSuggestionOption, false>
  ) => {
    if (selectedOption) {
      setSelectedOption(selectedOption)
      onChange(selectedOption.entry)
    } else {
      setSelectedOption(null)
      onChange(new AfoRegisterRecordSuggestion({ text: '', textNumbers: [] }))
    }
  }

  function formatOptionLabel(option: TextSuggestionOption): JSX.Element {
    return <Markdown text={option.label as string} />
  }

  return (
    <AsyncSelect
      isClearable={isClearable}
      aria-label={ariaLabel}
      placeholder="Text or Publication"
      cacheOptions
      loadOptions={loadOptions}
      onChange={handleChange}
      value={selectedOption}
      formatOptionLabel={formatOptionLabel}
    />
  )
}