select.tsx 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. /* eslint-disable jsx-a11y/label-has-for */
  2. /* eslint-disable jsx-a11y/label-has-associated-control */
  3. import classNames from 'classnames'
  4. import { useSelect } from 'downshift'
  5. import Icon from './icon'
  6. import { useTranslation } from 'react-i18next'
  7. type SelectProps<T> = {
  8. items: T[]
  9. itemToString: (item: T | null) => string
  10. label?: string
  11. defaultText?: string
  12. itemToSubtitle?: (item: T | null) => string
  13. itemToKey: (item: T) => string
  14. onSelectedItemChanged?: (item: T | null | undefined) => void
  15. disabled?: boolean
  16. optionalLabel?: boolean
  17. loading?: boolean
  18. }
  19. export const Select = <T,>({
  20. items,
  21. itemToString = item => (item === null ? '' : String(item)),
  22. label,
  23. defaultText = 'Items',
  24. itemToSubtitle,
  25. itemToKey,
  26. onSelectedItemChanged,
  27. disabled = false,
  28. optionalLabel = false,
  29. loading = false,
  30. }: SelectProps<T>) => {
  31. const { t } = useTranslation()
  32. const {
  33. isOpen,
  34. selectedItem,
  35. getToggleButtonProps,
  36. getLabelProps,
  37. getMenuProps,
  38. getItemProps,
  39. highlightedIndex,
  40. } = useSelect({
  41. items: items ?? [],
  42. itemToString,
  43. onSelectedItemChange: changes => {
  44. if (onSelectedItemChanged) {
  45. onSelectedItemChanged(changes.selectedItem)
  46. }
  47. },
  48. })
  49. return (
  50. <div className="select-wrapper">
  51. <div>
  52. {label ? (
  53. <label {...getLabelProps()}>
  54. {label}{' '}
  55. {optionalLabel && (
  56. <span className="select-optional-label text-muted">
  57. ({t('optional')})
  58. </span>
  59. )}{' '}
  60. {loading && <Icon fw type="spinner" spin />}
  61. </label>
  62. ) : null}
  63. <div
  64. className={classNames({ disabled }, 'select-trigger')}
  65. {...getToggleButtonProps({ disabled })}
  66. >
  67. <div>{selectedItem ? itemToString(selectedItem) : defaultText}</div>
  68. <div>
  69. {isOpen ? (
  70. <Icon type="chevron-up" fw />
  71. ) : (
  72. <Icon type="chevron-down" fw />
  73. )}
  74. </div>
  75. </div>
  76. </div>
  77. <ul
  78. className={classNames({ hidden: !isOpen }, 'select-items')}
  79. {...getMenuProps({ disabled })}
  80. >
  81. {isOpen &&
  82. items?.map((item, index) => (
  83. <li
  84. className={classNames({
  85. 'select-highlighted': highlightedIndex === index,
  86. 'selected-active': selectedItem === item,
  87. })}
  88. key={itemToKey(item)}
  89. {...getItemProps({ item, index })}
  90. >
  91. <span className="select-item-title">{itemToString(item)}</span>
  92. {itemToSubtitle ? (
  93. <span className="text-muted select-item-subtitle">
  94. {itemToSubtitle(item)}
  95. </span>
  96. ) : null}
  97. </li>
  98. ))}
  99. </ul>
  100. </div>
  101. )
  102. }