index.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import {useMemo} from 'react';
  2. import {Item, Section} from '@react-stately/collections';
  3. import domId from 'sentry/utils/domId';
  4. import {Control, ControlProps} from './control';
  5. import {List, MultipleListProps, SingleListProps} from './list';
  6. import type {
  7. SelectOption,
  8. SelectOptionOrSection,
  9. SelectOptionOrSectionWithKey,
  10. SelectSection,
  11. } from './types';
  12. export type {SelectOption, SelectOptionOrSection, SelectSection};
  13. interface BaseSelectProps<Value extends React.Key> extends ControlProps {
  14. options: SelectOptionOrSection<Value>[];
  15. }
  16. export interface SingleSelectProps<Value extends React.Key>
  17. extends BaseSelectProps<Value>,
  18. Omit<
  19. SingleListProps<Value>,
  20. 'children' | 'items' | 'grid' | 'compositeIndex' | 'label'
  21. > {}
  22. export interface MultipleSelectProps<Value extends React.Key>
  23. extends BaseSelectProps<Value>,
  24. Omit<
  25. MultipleListProps<Value>,
  26. 'children' | 'items' | 'grid' | 'compositeIndex' | 'label'
  27. > {}
  28. export type SelectProps<Value extends React.Key> =
  29. | SingleSelectProps<Value>
  30. | MultipleSelectProps<Value>;
  31. // A series of TS function overloads to properly parse prop types across 2 dimensions:
  32. // option value types (number vs string), and selection mode (singular vs multiple)
  33. function CompactSelect<Value extends number>(props: SelectProps<Value>): JSX.Element;
  34. function CompactSelect<Value extends string>(props: SelectProps<Value>): JSX.Element;
  35. function CompactSelect<Value extends React.Key>(props: SelectProps<Value>): JSX.Element;
  36. /**
  37. * Flexible select component with a customizable trigger button
  38. */
  39. function CompactSelect<Value extends React.Key>({
  40. // List props
  41. options,
  42. value,
  43. defaultValue,
  44. onChange,
  45. multiple,
  46. disallowEmptySelection,
  47. isOptionDisabled,
  48. // Control props
  49. grid,
  50. disabled,
  51. size = 'md',
  52. closeOnSelect,
  53. triggerProps,
  54. ...controlProps
  55. }: SelectProps<Value>) {
  56. const triggerId = useMemo(() => domId('select-trigger-'), []);
  57. // Combine list props into an object with two clearly separated types, one where
  58. // `multiple` is true and the other where it's not. Necessary to avoid TS errors.
  59. const listProps = useMemo(() => {
  60. if (multiple) {
  61. return {multiple, value, defaultValue, onChange, closeOnSelect, grid};
  62. }
  63. return {multiple, value, defaultValue, onChange, closeOnSelect, grid};
  64. }, [multiple, value, defaultValue, onChange, closeOnSelect, grid]);
  65. const optionsWithKey = useMemo<SelectOptionOrSectionWithKey<Value>[]>(
  66. () =>
  67. options.map((item, i) => ({
  68. ...item,
  69. key: 'options' in item ? item.key ?? i : item.value,
  70. })),
  71. [options]
  72. );
  73. const controlDisabled = useMemo(
  74. () => disabled ?? options?.length === 0,
  75. [disabled, options]
  76. );
  77. return (
  78. <Control
  79. {...controlProps}
  80. triggerProps={{...triggerProps, id: triggerId}}
  81. disabled={controlDisabled}
  82. grid={grid}
  83. size={size}
  84. >
  85. <List
  86. {...listProps}
  87. items={optionsWithKey}
  88. disallowEmptySelection={disallowEmptySelection}
  89. isOptionDisabled={isOptionDisabled}
  90. size={size}
  91. aria-labelledby={triggerId}
  92. >
  93. {(item: SelectOptionOrSection<Value>) => {
  94. if ('options' in item) {
  95. return (
  96. <Section key={item.key} title={item.label}>
  97. {item.options.map(opt => (
  98. <Item key={opt.value} {...opt}>
  99. {opt.label}
  100. </Item>
  101. ))}
  102. </Section>
  103. );
  104. }
  105. return (
  106. <Item key={item.value} {...item}>
  107. {item.label}
  108. </Item>
  109. );
  110. }}
  111. </List>
  112. </Control>
  113. );
  114. }
  115. export {CompactSelect};