You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

240 lines
6.3 KiB

  1. // @flow
  2. import deepEqual from "deep-equal";
  3. import * as React from 'react';
  4. import PopperJS, {
  5. type Placement,
  6. type Instance,
  7. type Data,
  8. type Modifiers,
  9. type ReferenceObject,
  10. } from 'popper.js';
  11. import type { Style } from 'typed-styles';
  12. import { ManagerReferenceNodeContext } from './Manager';
  13. import { unwrapArray, setRef, shallowEqual } from './utils';
  14. import { type Ref } from "./RefTypes";
  15. type ReferenceElement = ReferenceObject | HTMLElement | null;
  16. type StyleOffsets = { top: number, left: number };
  17. type StylePosition = { position: 'absolute' | 'fixed' };
  18. export type PopperArrowProps = {
  19. ref: Ref,
  20. style: StyleOffsets & Style,
  21. };
  22. export type PopperChildrenProps = {|
  23. ref: Ref,
  24. style: StyleOffsets & StylePosition & Style,
  25. placement: Placement,
  26. outOfBoundaries: ?boolean,
  27. scheduleUpdate: () => void,
  28. arrowProps: PopperArrowProps,
  29. |};
  30. export type PopperChildren = PopperChildrenProps => React.Node;
  31. export type PopperProps = {
  32. children: PopperChildren,
  33. eventsEnabled?: boolean,
  34. innerRef?: Ref,
  35. modifiers?: Modifiers,
  36. placement?: Placement,
  37. positionFixed?: boolean,
  38. referenceElement?: ReferenceElement,
  39. };
  40. type PopperState = {
  41. data: ?Data,
  42. placement: ?Placement,
  43. };
  44. const initialStyle = {
  45. position: 'absolute',
  46. top: 0,
  47. left: 0,
  48. opacity: 0,
  49. pointerEvents: 'none',
  50. };
  51. const initialArrowStyle = {};
  52. export class InnerPopper extends React.Component<PopperProps, PopperState> {
  53. static defaultProps = {
  54. placement: 'bottom',
  55. eventsEnabled: true,
  56. referenceElement: undefined,
  57. positionFixed: false,
  58. };
  59. state = {
  60. data: undefined,
  61. placement: undefined,
  62. };
  63. popperInstance: ?Instance;
  64. popperNode: ?HTMLElement = null;
  65. arrowNode: ?HTMLElement = null;
  66. setPopperNode = (popperNode: ?HTMLElement) => {
  67. if (!popperNode || this.popperNode === popperNode) return;
  68. setRef(this.props.innerRef, popperNode);
  69. this.popperNode = popperNode;
  70. this.updatePopperInstance();
  71. };
  72. setArrowNode = (arrowNode: ?HTMLElement) => {
  73. this.arrowNode = arrowNode;
  74. };
  75. updateStateModifier = {
  76. enabled: true,
  77. order: 900,
  78. fn: (data: Object) => {
  79. const { placement } = data;
  80. this.setState({ data, placement });
  81. return data;
  82. },
  83. };
  84. getOptions = () => ({
  85. placement: this.props.placement,
  86. eventsEnabled: this.props.eventsEnabled,
  87. positionFixed: this.props.positionFixed,
  88. modifiers: {
  89. ...this.props.modifiers,
  90. arrow: {
  91. ...(this.props.modifiers && this.props.modifiers.arrow),
  92. enabled: !!this.arrowNode,
  93. element: this.arrowNode,
  94. },
  95. applyStyle: { enabled: false },
  96. updateStateModifier: this.updateStateModifier,
  97. },
  98. });
  99. getPopperStyle = () =>
  100. !this.popperNode || !this.state.data
  101. ? initialStyle
  102. : {
  103. position: this.state.data.offsets.popper.position,
  104. ...this.state.data.styles,
  105. };
  106. getPopperPlacement = () =>
  107. !this.state.data ? undefined : this.state.placement;
  108. getArrowStyle = () =>
  109. !this.arrowNode || !this.state.data
  110. ? initialArrowStyle
  111. : this.state.data.arrowStyles;
  112. getOutOfBoundariesState = () =>
  113. this.state.data ? this.state.data.hide : undefined;
  114. destroyPopperInstance = () => {
  115. if (!this.popperInstance) return;
  116. this.popperInstance.destroy();
  117. this.popperInstance = null;
  118. };
  119. updatePopperInstance = () => {
  120. this.destroyPopperInstance();
  121. const { popperNode } = this;
  122. const { referenceElement } = this.props;
  123. if (!referenceElement || !popperNode) return;
  124. this.popperInstance = new PopperJS(
  125. referenceElement,
  126. popperNode,
  127. this.getOptions()
  128. );
  129. };
  130. scheduleUpdate = () => {
  131. if (this.popperInstance) {
  132. this.popperInstance.scheduleUpdate();
  133. }
  134. };
  135. componentDidUpdate(prevProps: PopperProps, prevState: PopperState) {
  136. // If the Popper.js options have changed, update the instance (destroy + create)
  137. if (
  138. this.props.placement !== prevProps.placement ||
  139. this.props.referenceElement !== prevProps.referenceElement ||
  140. this.props.positionFixed !== prevProps.positionFixed ||
  141. !deepEqual(this.props.modifiers, prevProps.modifiers, {strict: true})
  142. ) {
  143. // develop only check that modifiers isn't being updated needlessly
  144. if (process.env.NODE_ENV === "development") {
  145. if (
  146. this.props.modifiers !== prevProps.modifiers &&
  147. this.props.modifiers != null &&
  148. prevProps.modifiers != null &&
  149. shallowEqual(this.props.modifiers, prevProps.modifiers)
  150. ) {
  151. console.warn("'modifiers' prop reference updated even though all values appear the same.\nConsider memoizing the 'modifiers' object to avoid needless rendering.");
  152. }
  153. }
  154. this.updatePopperInstance();
  155. } else if (
  156. this.props.eventsEnabled !== prevProps.eventsEnabled &&
  157. this.popperInstance
  158. ) {
  159. this.props.eventsEnabled
  160. ? this.popperInstance.enableEventListeners()
  161. : this.popperInstance.disableEventListeners();
  162. }
  163. // A placement difference in state means popper determined a new placement
  164. // apart from the props value. By the time the popper element is rendered with
  165. // the new position Popper has already measured it, if the place change triggers
  166. // a size change it will result in a misaligned popper. So we schedule an update to be sure.
  167. if (prevState.placement !== this.state.placement) {
  168. this.scheduleUpdate();
  169. }
  170. }
  171. componentWillUnmount() {
  172. setRef(this.props.innerRef, null)
  173. this.destroyPopperInstance();
  174. }
  175. render() {
  176. return unwrapArray(this.props.children)({
  177. ref: this.setPopperNode,
  178. style: this.getPopperStyle(),
  179. placement: this.getPopperPlacement(),
  180. outOfBoundaries: this.getOutOfBoundariesState(),
  181. scheduleUpdate: this.scheduleUpdate,
  182. arrowProps: {
  183. ref: this.setArrowNode,
  184. style: this.getArrowStyle(),
  185. },
  186. });
  187. }
  188. }
  189. const placements = PopperJS.placements;
  190. export { placements };
  191. export default function Popper({ referenceElement, ...props }: PopperProps) {
  192. return (
  193. <ManagerReferenceNodeContext.Consumer>
  194. {(referenceNode) => (
  195. <InnerPopper
  196. referenceElement={
  197. referenceElement !== undefined ? referenceElement : referenceNode
  198. }
  199. {...props}
  200. />
  201. )}
  202. </ManagerReferenceNodeContext.Consumer>
  203. );
  204. }