select.tsx 2.6 KB

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