Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.

chore: update to Popper v2 #2380

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
"@fluentui/react-bindings": "^0.44.0",
"@fluentui/react-component-event-listener": "^0.44.0",
"@fluentui/react-component-nesting-registry": "^0.44.0",
"@fluentui/react-context-selector": "^0.44.0",
"@fluentui/react-component-ref": "^0.44.0",
"@fluentui/react-context-selector": "^0.44.0",
"@fluentui/react-proptypes": "^0.44.0",
"@fluentui/styles": "^0.44.0",
"@popperjs/core": "^2.0.6",
"classnames": "^2.2.6",
"downshift": "3.2.14",
"fast-memoize": "^2.5.1",
Expand All @@ -25,7 +26,6 @@
"fela-plugin-rtl": "^10.6.1",
"keyboard-key": "^1.0.1",
"lodash": "^4.17.15",
"popper.js": "^1.15.0",
"prop-types": "^15.7.2",
"react-fela": "^10.6.1",
"react-transition-group": "^4.3.0"
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/components/Tooltip/TooltipContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
useTelemetry,
} from '@fluentui/react-bindings'
import * as customPropTypes from '@fluentui/react-proptypes'
import Popper from 'popper.js'
import { Placement } from '@popperjs/core'
import * as PropTypes from 'prop-types'
import * as React from 'react'
// @ts-ignore
Expand Down Expand Up @@ -121,7 +121,7 @@ TooltipContent.className = 'ui-tooltip__content'

TooltipContent.propTypes = {
...commonPropTypes.createCommon(),
placement: PropTypes.oneOf<Popper.Placement>([
placement: PropTypes.oneOf<Placement>([
'auto-start',
'auto',
'auto-end',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const chatMessageStyles: ComponentSlotStylesPrepared<
opacity: 1,
width: 'auto',

'[x-out-of-boundaries]': {
'[data-popper-reference-hidden]': {
opacity: 0,
},
},
Expand Down Expand Up @@ -112,7 +112,7 @@ const chatMessageStyles: ComponentSlotStylesPrepared<
width: v.showActionMenu ? 'auto' : 0,
}),

'[x-out-of-boundaries]': {
'[data-popper-reference-hidden]': {
opacity: 0,
},
}),
Expand Down
174 changes: 95 additions & 79 deletions packages/react/src/utils/positioner/Popper.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { useIsomorphicLayoutEffect } from '@fluentui/react-bindings'
import { Ref, isRefObject } from '@fluentui/react-component-ref'
import * as _ from 'lodash'
import PopperJS, * as _PopperJS from 'popper.js'
import {
createPopper,
Instance,
Placement,
Options,
State,
VirtualElement,
ModifierPhases,
} from '@popperjs/core'
import * as React from 'react'

import isBrowser from '../isBrowser'
Expand All @@ -28,34 +36,32 @@ function useDeepMemo<TKey, TValue>(memoFn: () => TValue, key: TKey): TValue {
return ref.current.value
}

// `popper.js` has a UMD build without `.default`, it breaks CJS builds:
// https://github.com/rollup/rollup/issues/1267#issuecomment-446681320
const createPopper = (
reference: Element | _PopperJS.ReferenceObject,
popper: HTMLElement,
options?: PopperJS.PopperOptions,
): PopperJS => {
const instance = new ((_PopperJS as any).default || _PopperJS)(reference, popper, {
...options,
eventsEnabled: false,
})

const originalUpdate = instance.update
instance.update = () => {
// Fix Popper.js initial positioning display issue
// https://github.com/popperjs/popper.js/issues/457#issuecomment-367692177
popper.style.left = '0'
popper.style.top = '0'

originalUpdate()
}

const actualWindow = popper.ownerDocument.defaultView
instance.scheduleUpdate = () => actualWindow.requestAnimationFrame(instance.update)
instance.enableEventListeners()

return instance
}
// const createPopper = (
// reference: Element | VirtualElement,
// popper: HTMLElement,
// options?: any /* TODO */,
// ): Instance => {
// return createPopper(reference, popper, {
// ...options,
// eventsEnabled: false,
// })

// const originalUpdate = instance.update
// instance.update = () => {
// // Fix Popper.js initial positioning display issue
// // https://github.com/popperjs/popper.js/issues/457#issuecomment-367692177
// popper.style.left = '0'
// popper.style.top = '0'
//
// originalUpdate()
// }
//
// const actualWindow = popper.ownerDocument.defaultView
// instance.scheduleUpdate = () => actualWindow.requestAnimationFrame(instance.update)
// instance.enableEventListeners()

// return instance
// }

/**
* Popper relies on the 3rd party library [Popper.js](https://github.com/FezVrasta/popper.js) for positioning.
Expand All @@ -78,13 +84,11 @@ const Popper: React.FunctionComponent<PopperProps> = props => {

const proposedPlacement = getPlacement({ align, position, rtl })

const popperRef = React.useRef<PopperJS>()
const popperRef = React.useRef<Instance>()
const contentRef = React.useRef<HTMLElement>(null)

const latestPlacement = React.useRef<PopperJS.Placement>(proposedPlacement)
const [computedPlacement, setComputedPlacement] = React.useState<PopperJS.Placement>(
proposedPlacement,
)
const latestPlacement = React.useRef<Placement>(proposedPlacement)
const [computedPlacement, setComputedPlacement] = React.useState<Placement>(proposedPlacement)

const hasDocument = isBrowser()
const hasScrollableElement = React.useMemo(() => {
Expand All @@ -99,35 +103,38 @@ const Popper: React.FunctionComponent<PopperProps> = props => {
// Is a broken dependency and can cause potential bugs, we should rethink this as all other refs
// in this component.

const computedModifiers: PopperJS.Modifiers = useDeepMemo(
const computedModifiers: Options['modifiers'] = useDeepMemo(
() =>
_.merge(
/**
* This prevents blurrines in chrome, when the coordinates are odd numbers alternative
* would be to use `fn` and manipulate the computed style or ask popper to fix it but
* since there is presumably only handful of poppers displayed on the page, the
* performance impact should be minimal.
*/
{ computeStyle: { gpuAcceleration: false } },

{ flip: { padding: 0, flipVariationsByContent: true } },
{ preventOverflow: { padding: 0 } },

offset && {
offset: { offset: rtl ? applyRtlToOffset(offset, position) : offset },
keepTogether: { enabled: false },
},

[
/**
* This prevents blurrines in chrome, when the coordinates are odd numbers alternative
* would be to use `fn` and manipulate the computed style or ask popper to fix it but
* since there is presumably only handful of poppers displayed on the page, the
* performance impact should be minimal.
*/
{
name: 'computeStyles',
options: { gpuAcceleration: false },
},
Comment on lines +109 to +119
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be required anymore. Popper rounds to the nearest suitable subpixel, and in addition detects the DPI and uses 2D transforms instead of 3D (to preserve subpixel anti-aliasing).

{ name: 'flip', options: { padding: 0, flipVariations: true } },
{ name: 'preventOverflow', options: { padding: 0 } },
Comment on lines +120 to +121
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These seem redundant as they are the same as the defaults


offset && {
name: 'offset',
options: { offset: rtl ? applyRtlToOffset(offset, position) : offset },
},
],
/**
* When the popper box is placed in the context of a scrollable element, we need to set
* preventOverflow.escapeWithReference to true and flip.boundariesElement to 'scrollParent'
* (default is 'viewport') so that the popper box will stick with the targetRef when we
* scroll targetRef out of the viewport.
*/
hasScrollableElement && {
preventOverflow: { escapeWithReference: true },
flip: { boundariesElement: 'scrollParent' },
},
hasScrollableElement && [
// preventOverflow: { escapeWithReference: true }, TODO
{ name: 'flip', options: { boundary: 'scrollParent' } },
],

userModifiers,

Expand All @@ -137,25 +144,25 @@ const Popper: React.FunctionComponent<PopperProps> = props => {
* the values of `align` and `position` props, regardless of the size of the component, the
* reference element or the viewport.
*/
unstable_pinned && { flip: { enabled: false } },
unstable_pinned && [{ name: 'flip', enabled: false }],
),
[hasScrollableElement, position, offset, rtl, unstable_pinned, userModifiers],
)

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

const destroyInstance = React.useCallback(() => {
if (popperRef.current) {
popperRef.current.destroy()
if (popperRef.current.popper) {
// Popper keeps a reference to the DOM node, which needs to be cleaned up
// temporarily fix it here until fixed properly in popper
popperRef.current.popper = null
}
// if (popperRef.current.popper) {
// Popper keeps a reference to the DOM node, which needs to be cleaned up
// temporarily fix it here until fixed properly in popper
// popperRef.current.popper = null
// }
popperRef.current = null
}
}, [])
Expand All @@ -166,41 +173,50 @@ const Popper: React.FunctionComponent<PopperProps> = props => {
const reference =
targetRef && isRefObject(targetRef)
? (targetRef as React.RefObject<Element>).current
: (targetRef as _PopperJS.ReferenceObject)
: (targetRef as VirtualElement)

if (!enabled || !reference || !contentRef.current) {
return
}

const hasPointer = !!(pointerTargetRef && pointerTargetRef.current)
const handleUpdate = (data: PopperJS.Data) => {
const handleUpdate = ({ state }: { state: Partial<State> }) => {
console.log(state)
// PopperJS performs computations that might update the computed placement: auto positioning, flipping the
// placement in case the popper box should be rendered at the edge of the viewport and does not fit
if (data.placement !== latestPlacement.current) {
latestPlacement.current = data.placement
setComputedPlacement(data.placement)
if (state.placement !== latestPlacement.current) {
latestPlacement.current = state.placement
setComputedPlacement(state.placement)
}
}

const options: PopperJS.PopperOptions = {
console.log(computedModifiers)
const options: Options = {
placement: proposedPlacement,
positionFixed,
modifiers: {
strategy: positionFixed ? 'fixed' : 'absolute',
modifiers: [
...computedModifiers,

/**
* This modifier is necessary in order to render the pointer. Refs are resolved in effects, so it can't be
* placed under computed modifiers. Deep merge is not required as this modifier has only these properties.
* `arrow` modifier also requires `keepTogether`.
*/
keepTogether: { enabled: hasPointer },
arrow: {
{
name: 'arrow',
enabled: hasPointer,
element: pointerTargetRef && pointerTargetRef.current,
options: {
element: pointerTargetRef && pointerTargetRef.current,
},
},

// afterWrite
{
name: 'onUpdate',
enabled: true,
phase: 'afterWrite' as ModifierPhases,
fn: handleUpdate,
},
},
onCreate: handleUpdate,
onUpdate: handleUpdate,
].filter(Boolean),
onFirstUpdate: state => handleUpdate({ state }),
}

popperRef.current = createPopper(reference, contentRef.current, options)
Expand All @@ -213,7 +229,7 @@ const Popper: React.FunctionComponent<PopperProps> = props => {
proposedPlacement,
targetRef,
unstable_pinned,
])
]) // TODO: use options instead of recreate

useIsomorphicLayoutEffect(() => {
createInstance()
Expand Down
3 changes: 1 addition & 2 deletions packages/react/src/utils/positioner/positioningHelper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Placement } from 'popper.js'

import { Placement } from '@popperjs/core'
import { Alignment, Position } from './types'

enum PlacementParts {
Expand Down
8 changes: 4 additions & 4 deletions packages/react/src/utils/positioner/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react'
import PopperJS from 'popper.js'
import { Placement, VirtualElement } from '@popperjs/core'

export type Position = 'above' | 'below' | 'before' | 'after'
export type Alignment = 'top' | 'bottom' | 'start' | 'end' | 'center'
Expand Down Expand Up @@ -59,7 +59,7 @@ export interface PopperProps extends PositioningProps {
* List of modifiers used to modify the offsets before they are applied to the Popper box.
* They provide most of the functionality of Popper.js.
*/
modifiers?: PopperJS.Modifiers
modifiers?: any /* TODO */

/**
* Array of conditions to be met in order to trigger a subsequent render to reposition the elements.
Expand All @@ -75,7 +75,7 @@ export interface PopperProps extends PositioningProps {
/**
* Ref object containing the target node (the element that we're using as reference for Popper box).
*/
targetRef: React.RefObject<Element> | PopperJS.ReferenceObject
targetRef: React.RefObject<Element> | VirtualElement

/**
* Rtl attribute for the component.
Expand All @@ -87,7 +87,7 @@ export interface PopperChildrenProps {
/**
* Popper's placement.
*/
placement: PopperJS.Placement
placement: Placement

/**
* Function that updates the position of the Popper box, computing the new offsets and applying the new style.
Expand Down
4 changes: 2 additions & 2 deletions packages/react/test/__mocks__/popper.js.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as PopperJs from 'popper.js'
import { placements } from '@popperjs/core'

// Popper.js does not work with JSDOM: https://github.com/FezVrasta/popper.js/issues/478
export default class Popper {
static placements = (PopperJs as any).placements
static placements = placements

constructor() {
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Placement } from 'popper.js'
import { Placement } from '@popperjs/core'

import { Alignment, Position } from 'src/utils/positioner'
import { getPlacement, applyRtlToOffset } from 'src/utils/positioner/positioningHelper'
Expand Down
Loading