option.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. import {Fragment, useMemo, useRef} from 'react';
  2. import type {AriaOptionProps} from '@react-aria/listbox';
  3. import {useOption} from '@react-aria/listbox';
  4. import type {ListState} from '@react-stately/list';
  5. import type {Node} from '@react-types/shared';
  6. import Checkbox from 'sentry/components/checkbox';
  7. import MenuListItem from 'sentry/components/menuListItem';
  8. import {IconCheckmark} from 'sentry/icons';
  9. import type {FormSize} from 'sentry/utils/theme';
  10. import {CheckWrap} from '../styles';
  11. interface ListBoxOptionProps extends AriaOptionProps {
  12. item: Node<any>;
  13. listState: ListState<any>;
  14. size: FormSize;
  15. }
  16. /**
  17. * A <li /> element with accessibile behaviors & attributes.
  18. * https://react-spectrum.adobe.com/react-aria/useListBox.html
  19. */
  20. export function ListBoxOption({item, listState, size}: ListBoxOptionProps) {
  21. const ref = useRef<HTMLLIElement>(null);
  22. const {
  23. label,
  24. details,
  25. leadingItems,
  26. trailingItems,
  27. priority,
  28. hideCheck,
  29. tooltip,
  30. tooltipOptions,
  31. selectionMode,
  32. showDetailsInOverlay,
  33. } = item.props;
  34. const multiple = selectionMode
  35. ? selectionMode === 'multiple'
  36. : listState.selectionManager.selectionMode === 'multiple';
  37. const {optionProps, labelProps, isSelected, isFocused, isDisabled, isPressed} =
  38. useOption({key: item.key, 'aria-label': item['aria-label']}, listState, ref);
  39. const optionPropsMemo = useMemo(
  40. () => optionProps,
  41. // Only update optionProps when a relevant state (selection/focus/disable) changes
  42. // eslint-disable-next-line react-hooks/exhaustive-deps
  43. [isSelected, isFocused, isDisabled]
  44. );
  45. const labelPropsMemo = useMemo(
  46. () => ({...labelProps, as: typeof label === 'string' ? 'p' : 'div'}),
  47. // eslint-disable-next-line react-hooks/exhaustive-deps
  48. [labelProps.id, label]
  49. );
  50. const leadingItemsMemo = useMemo(() => {
  51. const checkboxSize = size === 'xs' ? 'xs' : 'sm';
  52. if (hideCheck && !leadingItems) {
  53. return null;
  54. }
  55. return (
  56. <Fragment>
  57. {!hideCheck && (
  58. <CheckWrap multiple={multiple} isSelected={isSelected} aria-hidden="true">
  59. {multiple ? (
  60. <Checkbox
  61. size={checkboxSize}
  62. checked={isSelected}
  63. disabled={isDisabled}
  64. readOnly
  65. />
  66. ) : (
  67. isSelected && <IconCheckmark size={checkboxSize} />
  68. )}
  69. </CheckWrap>
  70. )}
  71. {leadingItems}
  72. </Fragment>
  73. );
  74. }, [multiple, isSelected, isDisabled, size, leadingItems, hideCheck]);
  75. return (
  76. <MenuListItem
  77. {...optionPropsMemo}
  78. ref={ref}
  79. size={size}
  80. label={label}
  81. details={details}
  82. disabled={isDisabled}
  83. isPressed={isPressed}
  84. isSelected={isSelected}
  85. isFocused={listState.selectionManager.isFocused && isFocused}
  86. priority={priority ?? (isSelected && !multiple) ? 'primary' : 'default'}
  87. labelProps={labelPropsMemo}
  88. leadingItems={leadingItemsMemo}
  89. trailingItems={trailingItems}
  90. showDetailsInOverlay={showDetailsInOverlay}
  91. tooltip={tooltip}
  92. tooltipOptions={tooltipOptions}
  93. data-test-id={item.key}
  94. />
  95. );
  96. }