Skip to content

Commit

Permalink
feat: upgrade popper to v2 (#766)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: popper upgrade to v2, slightly different modifiers format now
  • Loading branch information
jquense authored Feb 14, 2020
1 parent dd2ffdd commit 02c8e6d
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 89 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@
},
"dependencies": {
"@babel/runtime": "^7.4.5",
"@popperjs/core": "^2.0.0",
"@restart/hooks": "^0.3.12",
"dom-helpers": "^5.1.0",
"popper.js": "^1.15.0",
"prop-types": "^15.7.2",
"uncontrollable": "^7.0.0",
"warning": "^4.0.3"
Expand Down
22 changes: 16 additions & 6 deletions src/DropdownMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import { useContext, useRef } from 'react';
import useCallbackRef from '@restart/hooks/useCallbackRef';
import DropdownContext from './DropdownContext';
import usePopper from './usePopper';
import usePopper, { toModifierMap } from './usePopper';
import useRootClose from './useRootClose';

export function useDropdownMenu(options = {}) {
Expand Down Expand Up @@ -39,18 +39,28 @@ export function useDropdownMenu(options = {}) {
else if (drop === 'right') placement = alignEnd ? 'right-end' : 'right-start';
else if (drop === 'left') placement = alignEnd ? 'left-end' : 'left-start';

const modifiers = toModifierMap(popperConfig.modifiers);

const popper = usePopper(toggleElement, menuElement, {
placement,
enabled: !!(shouldUsePopper && show),
eventsEnabled: !!show,
modifiers: {
flip: { enabled: !!flip },
...modifiers,
eventListeners: {
enabled: !!show,
},
arrow: {
...(popperConfig.modifiers && popperConfig.modifiers.arrow),
...modifiers.arrow,
enabled: !!arrowElement,
element: arrowElement,
options: {
...modifiers.arrow?.options,
element: arrowElement,
},
},
flip: {
enabled: !!flip,
...modifiers.flip,
},
...popperConfig.modifiers,
},
});

Expand Down
25 changes: 17 additions & 8 deletions src/Overlay.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import PopperJS from 'popper.js';
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import { placements } from '@popperjs/core/lib/enums';
import useCallbackRef from '@restart/hooks/useCallbackRef';
import useMergedRefs from '@restart/hooks/useMergedRefs';
import usePopper from './usePopper';
import usePopper, { toModifierMap } from './usePopper';
import useRootClose from './useRootClose';
import useWaitForDOMRef from './utils/useWaitForDOMRef';

Expand All @@ -30,26 +30,35 @@ const Overlay = React.forwardRef((props, outerRef) => {

const [exited, setExited] = useState(!props.show);

const { modifiers = {} } = popperConfig;
const modifiers = toModifierMap(popperConfig.modifiers);

const { styles, arrowStyles, ...popper } = usePopper(target, rootElement, {
...popperConfig,
placement: placement || 'bottom',
enableEvents: props.show,

modifiers: {
...modifiers,
eventListeners: {
enabled: !!props.show,
},
preventOverflow: {
padding: containerPadding || 5,
...modifiers.preventOverflow,
options: {
padding: containerPadding || 5,
...modifiers.preventOverflow?.options,
},
},
arrow: {
...modifiers.arrow,
enabled: !!arrowElement,
element: arrowElement,
options: {
...modifiers.arrow?.options,
element: arrowElement,
},
},
flip: {
enabled: !!flip,
...modifiers.preventOverflow,
...modifiers.flip,
},
},
});
Expand Down Expand Up @@ -125,7 +134,7 @@ Overlay.propTypes = {
show: PropTypes.bool,

/** Specify where the overlay element is positioned in relation to the target element */
placement: PropTypes.oneOf(PopperJS.placements),
placement: PropTypes.oneOf(placements),

/**
* A DOM Element, Ref to an element, or function that returns either. The `target` element is where
Expand Down
167 changes: 108 additions & 59 deletions src/usePopper.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
import PopperJS from 'popper.js';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import arrow from '@popperjs/core/lib/modifiers/arrow';
import computeStyles from '@popperjs/core/lib/modifiers/computeStyles';
import eventListeners from '@popperjs/core/lib/modifiers/eventListeners';
import flip from '@popperjs/core/lib/modifiers/flip';
import hide from '@popperjs/core/lib/modifiers/hide';
import popperOffsets from '@popperjs/core/lib/modifiers/popperOffsets';
import preventOverflow from '@popperjs/core/lib/modifiers/preventOverflow';
import { popperGenerator } from '@popperjs/core/lib/popper-base';
import useSafeState from '@restart/hooks/useSafeState';

const createPopper = popperGenerator({
defaultModifiers: [
hide,
popperOffsets,
computeStyles,
eventListeners,
flip,
preventOverflow,
arrow,
],
});

const initialPopperStyles = {
position: 'absolute',
Expand All @@ -11,6 +31,28 @@ const initialPopperStyles = {

const initialArrowStyles = {};

export function toModifierMap(modifiers) {
const result = {};

if (!Array.isArray(modifiers)) {
return modifiers || result;
}

// eslint-disable-next-line no-unused-expressions
modifiers?.forEach(m => {
result[m.name] = m;
});
return result;
}

export function toModifierArray(map) {
if (Array.isArray(map)) return map;
return Object.keys(map || {}).map(k => {
map[k].name = k;
return map[k];
});
}

/**
* Position an element relative some reference element using Popper.js
*
Expand All @@ -29,28 +71,60 @@ export default function usePopper(
{
enabled = true,
placement = 'bottom',
positionFixed = false,
strategy = 'absolute',
eventsEnabled = true,
modifiers = {},
modifiers: userModifiers,
} = {},
) {
const popperInstanceRef = useRef();

const hasArrow = !!(modifiers.arrow && modifiers.arrow.element);

const scheduleUpdate = useCallback(() => {
if (popperInstanceRef.current) {
popperInstanceRef.current.scheduleUpdate();
popperInstanceRef.current.update();
}
}, []);

const [state, setState] = useState({
placement,
scheduleUpdate,
outOfBoundaries: false,
styles: initialPopperStyles,
arrowStyles: initialArrowStyles,
});
const [state, setState] = useSafeState(
useState({
placement,
scheduleUpdate,
outOfBoundaries: false,
styles: initialPopperStyles,
arrowStyles: initialArrowStyles,
}),
);

const updateModifier = useMemo(
() => ({
name: 'updateStateModifier',
enabled: true,
phase: 'afterWrite',
requires: ['computeStyles'],
fn: data => {
setState({
scheduleUpdate,
outOfBoundries: data.state.modifiersData.hide?.isReferenceHidden,
placement: data.state.placement,
styles: { ...data.state.styles?.popper },
arrowStyles: { ...data.state.styles?.arrow },
state: data.state,
});
},
}),
[scheduleUpdate, setState],
);

let modifiers = toModifierArray(userModifiers);

let eventsModifier = modifiers.find(m => m.name === 'eventListeners');

if (!eventsModifier && eventsEnabled) {
eventsModifier = {
name: 'eventListeners',
enabled: true,
};
modifiers = [...modifiers, eventsModifier];
}

// A placement difference in state means popper determined a new placement
// apart from the props value. By the time the popper element is rendered with
Expand All @@ -60,68 +134,43 @@ export default function usePopper(
scheduleUpdate();
}, [state.placement, scheduleUpdate]);

/** Toggle Events */
useEffect(() => {
if (popperInstanceRef.current) {
// eslint-disable-next-line no-unused-expressions
eventsEnabled
? popperInstanceRef.current.enableEventListeners()
: popperInstanceRef.current.disableEventListeners();
}
}, [eventsEnabled]);
if (!popperInstanceRef.current || !enabled) return;

popperInstanceRef.current.setOptions({
placement,
strategy,
modifiers: [...modifiers, updateModifier],
});
// intentionally NOT re-running on new modifiers
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [strategy, placement, eventsModifier.enabled, updateModifier, enabled]);

useEffect(() => {
if (!enabled || referenceElement == null || popperElement == null) {
return undefined;
}

const arrow = modifiers.arrow && {
...modifiers.arrow,
element: modifiers.arrow.element,
};

popperInstanceRef.current = new PopperJS(referenceElement, popperElement, {
popperInstanceRef.current = createPopper(referenceElement, popperElement, {
placement,
positionFixed,
modifiers: {
...modifiers,
arrow,
applyStyle: { enabled: false },
updateStateModifier: {
enabled: true,
order: 900,
fn(data) {
setState({
scheduleUpdate,
styles: {
position: data.offsets.popper.position,
...data.styles,
},
arrowStyles: data.arrowStyles,
outOfBoundaries: data.hide,
placement: data.placement,
});
},
},
},
strategy,
modifiers: [...modifiers, updateModifier],
});

return () => {
if (popperInstanceRef.current !== null) {
popperInstanceRef.current.destroy();
popperInstanceRef.current = null;
setState(s => ({
...s,
styles: initialPopperStyles,
arrowStyles: initialArrowStyles,
}));
}
};
// intentionally NOT re-running on new modifiers
// This is only run once to _create_ the popper
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
enabled,
placement,
positionFixed,
referenceElement,
popperElement,
hasArrow,
]);
}, [enabled, referenceElement, popperElement]);

return state;
}
Loading

0 comments on commit 02c8e6d

Please sign in to comment.