useOverlay.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import {useMemo, useState} from 'react';
  2. import {PopperProps, usePopper} from 'react-popper';
  3. import {useButton} from '@react-aria/button';
  4. import {
  5. OverlayProps,
  6. OverlayTriggerProps,
  7. useOverlay as useAriaOverlay,
  8. useOverlayTrigger,
  9. } from '@react-aria/overlays';
  10. import {mergeProps} from '@react-aria/utils';
  11. import {useOverlayTriggerState} from '@react-stately/overlays';
  12. import {OverlayTriggerProps as OverlayTriggerStateProps} from '@react-types/overlays';
  13. interface UseOverlayProps
  14. extends Partial<OverlayProps>,
  15. Partial<OverlayTriggerProps>,
  16. Partial<OverlayTriggerStateProps> {
  17. /**
  18. * Offset along the main axis.
  19. */
  20. offset?: number;
  21. /**
  22. * Position for the overlay.
  23. */
  24. position?: PopperProps<any>['placement'];
  25. }
  26. function useOverlay({
  27. isOpen,
  28. defaultOpen,
  29. onOpenChange,
  30. type = 'dialog',
  31. offset = 8,
  32. position = 'top',
  33. isDismissable = true,
  34. shouldCloseOnBlur = false,
  35. isKeyboardDismissDisabled,
  36. shouldCloseOnInteractOutside,
  37. }: UseOverlayProps = {}) {
  38. // Callback refs for react-popper
  39. const [triggerElement, setTriggerElement] = useState<HTMLButtonElement | null>(null);
  40. const [overlayElement, setOverlayElement] = useState<HTMLDivElement | null>(null);
  41. const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null);
  42. // Ref objects for react-aria (useOverlayTrigger & useOverlay)
  43. const triggerRef = useMemo(() => ({current: triggerElement}), [triggerElement]);
  44. const overlayRef = useMemo(() => ({current: overlayElement}), [overlayElement]);
  45. // Get props for trigger button
  46. const openState = useOverlayTriggerState({isOpen, defaultOpen, onOpenChange});
  47. const {buttonProps} = useButton({onPress: openState.open}, triggerRef);
  48. const {triggerProps, overlayProps: overlayTriggerProps} = useOverlayTrigger(
  49. {type},
  50. openState,
  51. triggerRef
  52. );
  53. // Get props for overlay element
  54. const {overlayProps} = useAriaOverlay(
  55. {
  56. onClose: openState.close,
  57. isOpen: openState.isOpen,
  58. isDismissable,
  59. shouldCloseOnBlur,
  60. isKeyboardDismissDisabled,
  61. shouldCloseOnInteractOutside,
  62. },
  63. overlayRef
  64. );
  65. const modifiers = useMemo(
  66. () => [
  67. {
  68. name: 'hide',
  69. enabled: false,
  70. },
  71. {
  72. name: 'computeStyles',
  73. options: {
  74. // Using the `transform` attribute causes our borders to get blurry
  75. // in chrome. See [0]. This just causes it to use `top` / `left`
  76. // positions, which should be fine.
  77. //
  78. // [0]: https://stackoverflow.com/questions/29543142/css3-transformation-blurry-borders
  79. gpuAcceleration: false,
  80. },
  81. },
  82. {
  83. name: 'arrow',
  84. options: {
  85. element: arrowElement,
  86. // Set padding to avoid the arrow reaching the side of the tooltip
  87. // and overflowing out of the rounded border
  88. padding: 4,
  89. },
  90. },
  91. {
  92. name: 'offset',
  93. options: {
  94. offset: [0, offset],
  95. },
  96. },
  97. {
  98. name: 'preventOverflow',
  99. enabled: true,
  100. options: {
  101. padding: 12,
  102. altAxis: true,
  103. },
  104. },
  105. ],
  106. [arrowElement, offset]
  107. );
  108. const {styles: popperStyles, state: popperState} = usePopper(
  109. triggerElement,
  110. overlayElement,
  111. {modifiers, placement: position}
  112. );
  113. return {
  114. isOpen: openState.isOpen,
  115. triggerProps: {
  116. ref: setTriggerElement,
  117. ...mergeProps(buttonProps, triggerProps),
  118. },
  119. overlayProps: {
  120. ref: setOverlayElement,
  121. style: popperStyles.popper,
  122. ...mergeProps(overlayTriggerProps, overlayProps),
  123. },
  124. arrowProps: {
  125. ref: setArrowElement,
  126. style: popperStyles.arrow,
  127. placement: popperState?.placement,
  128. },
  129. };
  130. }
  131. export default useOverlay;