downshift-input.tsx 3.3 KB

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