useOverlay.tsx 3.8 KB

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