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.
		
		
		
		
		
			
		
			
				
					
					
						
							158 lines
						
					
					
						
							4.0 KiB
						
					
					
				
			
		
		
	
	
							158 lines
						
					
					
						
							4.0 KiB
						
					
					
				| // @flow strict
 | |
| import * as React from 'react';
 | |
| import * as ReactDOM from 'react-dom';
 | |
| import {
 | |
|   createPopper as defaultCreatePopper,
 | |
|   type Options as PopperOptions,
 | |
|   type VirtualElement,
 | |
|   type State as PopperState,
 | |
|   type Instance as PopperInstance,
 | |
| } from '@popperjs/core';
 | |
| import isEqual from 'react-fast-compare';
 | |
| import { fromEntries, useIsomorphicLayoutEffect } from './utils';
 | |
| 
 | |
| type Options = $Shape<{
 | |
|   ...PopperOptions,
 | |
|   createPopper: typeof defaultCreatePopper,
 | |
| }>;
 | |
| 
 | |
| type Styles = {
 | |
|   [key: string]: $Shape<CSSStyleDeclaration>,
 | |
| };
 | |
| 
 | |
| type Attributes = {
 | |
|   [key: string]: { [key: string]: string },
 | |
| };
 | |
| 
 | |
| type State = {
 | |
|   styles: Styles,
 | |
|   attributes: Attributes,
 | |
| };
 | |
| 
 | |
| const EMPTY_MODIFIERS = [];
 | |
| 
 | |
| type UsePopperResult = $ReadOnly<{
 | |
|   state: ?PopperState,
 | |
|   styles: Styles,
 | |
|   attributes: Attributes,
 | |
|   update: ?$PropertyType<PopperInstance, 'update'>,
 | |
|   forceUpdate: ?$PropertyType<PopperInstance, 'forceUpdate'>,
 | |
| }>;
 | |
| 
 | |
| export const usePopper = (
 | |
|   referenceElement: ?(Element | VirtualElement),
 | |
|   popperElement: ?HTMLElement,
 | |
|   options: Options = {}
 | |
| ): UsePopperResult => {
 | |
|   const prevOptions = React.useRef<?PopperOptions>(null);
 | |
| 
 | |
|   const optionsWithDefaults = {
 | |
|     onFirstUpdate: options.onFirstUpdate,
 | |
|     placement: options.placement || 'bottom',
 | |
|     strategy: options.strategy || 'absolute',
 | |
|     modifiers: options.modifiers || EMPTY_MODIFIERS,
 | |
|   };
 | |
| 
 | |
|   const [state, setState] = React.useState<State>({
 | |
|     styles: {
 | |
|       popper: {
 | |
|         position: optionsWithDefaults.strategy,
 | |
|         left: '0',
 | |
|         top: '0',
 | |
|       },
 | |
|       arrow: {
 | |
|         position: 'absolute',
 | |
|       },
 | |
|     },
 | |
|     attributes: {},
 | |
|   });
 | |
| 
 | |
|   const updateStateModifier = React.useMemo(
 | |
|     () => ({
 | |
|       name: 'updateState',
 | |
|       enabled: true,
 | |
|       phase: 'write',
 | |
|       fn: ({ state }) => {
 | |
|         const elements = Object.keys(state.elements);
 | |
| 
 | |
|         ReactDOM.flushSync(() => {
 | |
|           setState({
 | |
|             styles: fromEntries(
 | |
|               elements.map((element) => [element, state.styles[element] || {}])
 | |
|             ),
 | |
|             attributes: fromEntries(
 | |
|               elements.map((element) => [element, state.attributes[element]])
 | |
|             ),
 | |
|           });
 | |
|         });
 | |
|       },
 | |
|       requires: ['computeStyles'],
 | |
|     }),
 | |
|     []
 | |
|   );
 | |
| 
 | |
|   const popperOptions = React.useMemo(() => {
 | |
|     const newOptions = {
 | |
|       onFirstUpdate: optionsWithDefaults.onFirstUpdate,
 | |
|       placement: optionsWithDefaults.placement,
 | |
|       strategy: optionsWithDefaults.strategy,
 | |
|       modifiers: [
 | |
|         ...optionsWithDefaults.modifiers,
 | |
|         updateStateModifier,
 | |
|         { name: 'applyStyles', enabled: false },
 | |
|       ],
 | |
|     };
 | |
| 
 | |
|     if (isEqual(prevOptions.current, newOptions)) {
 | |
|       return prevOptions.current || newOptions;
 | |
|     } else {
 | |
|       prevOptions.current = newOptions;
 | |
|       return newOptions;
 | |
|     }
 | |
|   }, [
 | |
|     optionsWithDefaults.onFirstUpdate,
 | |
|     optionsWithDefaults.placement,
 | |
|     optionsWithDefaults.strategy,
 | |
|     optionsWithDefaults.modifiers,
 | |
|     updateStateModifier,
 | |
|   ]);
 | |
| 
 | |
|   const popperInstanceRef = React.useRef();
 | |
| 
 | |
|   useIsomorphicLayoutEffect(() => {
 | |
|     if (popperInstanceRef.current) {
 | |
|       popperInstanceRef.current.setOptions(popperOptions);
 | |
|     }
 | |
|   }, [popperOptions]);
 | |
| 
 | |
|   useIsomorphicLayoutEffect(() => {
 | |
|     if (referenceElement == null || popperElement == null) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const createPopper = options.createPopper || defaultCreatePopper;
 | |
|     const popperInstance = createPopper(
 | |
|       referenceElement,
 | |
|       popperElement,
 | |
|       popperOptions
 | |
|     );
 | |
| 
 | |
|     popperInstanceRef.current = popperInstance;
 | |
| 
 | |
|     return () => {
 | |
|       popperInstance.destroy();
 | |
|       popperInstanceRef.current = null;
 | |
|     };
 | |
|   }, [referenceElement, popperElement, options.createPopper]);
 | |
| 
 | |
|   return {
 | |
|     state: popperInstanceRef.current ? popperInstanceRef.current.state : null,
 | |
|     styles: state.styles,
 | |
|     attributes: state.attributes,
 | |
|     update: popperInstanceRef.current ? popperInstanceRef.current.update : null,
 | |
|     forceUpdate: popperInstanceRef.current
 | |
|       ? popperInstanceRef.current.forceUpdate
 | |
|       : null,
 | |
|   };
 | |
| };
 |