downshift-input.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import { useState, useEffect, forwardRef } from 'react'
  2. import { useCombobox } from 'downshift'
  3. import classnames from 'classnames'
  4. type DownshiftInputProps = {
  5. items: string[]
  6. itemsTitle?: string
  7. inputValue: string
  8. label: string
  9. setValue: (value: string) => void
  10. inputRef?: React.ForwardedRef<HTMLInputElement>
  11. showLabel?: boolean
  12. showSuggestedText?: boolean
  13. } & React.InputHTMLAttributes<HTMLInputElement>
  14. const filterItemsByInputValue = (
  15. items: DownshiftInputProps['items'],
  16. inputValue: DownshiftInputProps['inputValue']
  17. ) => items.filter(item => item.toLowerCase().includes(inputValue.toLowerCase()))
  18. function Downshift({
  19. items,
  20. itemsTitle,
  21. inputValue,
  22. placeholder,
  23. label,
  24. setValue,
  25. disabled,
  26. inputRef,
  27. showLabel = false,
  28. showSuggestedText = false,
  29. }: DownshiftInputProps) {
  30. const [inputItems, setInputItems] = useState(items)
  31. useEffect(() => {
  32. setInputItems(items)
  33. }, [items])
  34. const {
  35. isOpen,
  36. getLabelProps,
  37. getMenuProps,
  38. getInputProps,
  39. getComboboxProps,
  40. getItemProps,
  41. highlightedIndex,
  42. openMenu,
  43. selectedItem,
  44. } = useCombobox({
  45. inputValue,
  46. items: inputItems,
  47. initialSelectedItem: inputValue,
  48. onSelectedItemChange: ({ selectedItem }) => {
  49. setValue(selectedItem ?? '')
  50. },
  51. onInputValueChange: ({ inputValue = '' }) => {
  52. setInputItems(filterItemsByInputValue(items, inputValue))
  53. },
  54. onStateChange: ({ type }) => {
  55. if (type === useCombobox.stateChangeTypes.FunctionOpenMenu) {
  56. setInputItems(filterItemsByInputValue(items, inputValue))
  57. }
  58. },
  59. })
  60. return (
  61. <div
  62. className={classnames(
  63. 'ui-select-container ui-select-bootstrap dropdown',
  64. {
  65. open: isOpen && inputItems.length,
  66. }
  67. )}
  68. >
  69. <div {...getComboboxProps()}>
  70. {/* eslint-disable-next-line jsx-a11y/label-has-for */}
  71. <label {...getLabelProps()} className={showLabel ? '' : 'sr-only'}>
  72. {label}
  73. </label>
  74. <input
  75. {...getInputProps({
  76. onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
  77. setValue(event.target.value)
  78. },
  79. onFocus: () => {
  80. if (!isOpen) {
  81. openMenu()
  82. }
  83. },
  84. ref: inputRef,
  85. })}
  86. className="form-control"
  87. type="text"
  88. placeholder={placeholder}
  89. disabled={disabled}
  90. />
  91. </div>
  92. <ul
  93. {...getMenuProps()}
  94. className="ui-select-choices ui-select-choices-content ui-select-dropdown dropdown-menu"
  95. >
  96. {showSuggestedText && inputItems.length && (
  97. <li className="ui-select-title">{itemsTitle}</li>
  98. )}
  99. {inputItems.map((item, index) => (
  100. <li
  101. className="ui-select-choices-group"
  102. key={`${item}${index}`}
  103. {...getItemProps({ item, index })}
  104. >
  105. <div
  106. className={classnames('ui-select-choices-row', {
  107. active: selectedItem === item,
  108. 'ui-select-choices-row--highlighted':
  109. highlightedIndex === index,
  110. })}
  111. >
  112. <span className="ui-select-choices-row-inner">
  113. <span>{item}</span>
  114. </span>
  115. </div>
  116. </li>
  117. ))}
  118. </ul>
  119. </div>
  120. )
  121. }
  122. const DownshiftInput = forwardRef<
  123. HTMLInputElement,
  124. Omit<DownshiftInputProps, 'inputRef'>
  125. >((props, ref) => <Downshift {...props} inputRef={ref} />)
  126. DownshiftInput.displayName = 'DownshiftInput'
  127. export default DownshiftInput