diff --git a/packages/react/package.json b/packages/react/package.json index c17e5dc8ec..1d09a7dc28 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -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", @@ -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" diff --git a/packages/react/src/components/Tooltip/TooltipContent.tsx b/packages/react/src/components/Tooltip/TooltipContent.tsx index 41af76b4a4..fecb537eec 100644 --- a/packages/react/src/components/Tooltip/TooltipContent.tsx +++ b/packages/react/src/components/Tooltip/TooltipContent.tsx @@ -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 @@ -121,7 +121,7 @@ TooltipContent.className = 'ui-tooltip__content' TooltipContent.propTypes = { ...commonPropTypes.createCommon(), - placement: PropTypes.oneOf([ + placement: PropTypes.oneOf([ 'auto-start', 'auto', 'auto-end', diff --git a/packages/react/src/themes/teams/components/Chat/chatMessageStyles.ts b/packages/react/src/themes/teams/components/Chat/chatMessageStyles.ts index 99661574da..0122b7bfd7 100644 --- a/packages/react/src/themes/teams/components/Chat/chatMessageStyles.ts +++ b/packages/react/src/themes/teams/components/Chat/chatMessageStyles.ts @@ -63,7 +63,7 @@ const chatMessageStyles: ComponentSlotStylesPrepared< opacity: 1, width: 'auto', - '[x-out-of-boundaries]': { + '[data-popper-reference-hidden]': { opacity: 0, }, }, @@ -112,7 +112,7 @@ const chatMessageStyles: ComponentSlotStylesPrepared< width: v.showActionMenu ? 'auto' : 0, }), - '[x-out-of-boundaries]': { + '[data-popper-reference-hidden]': { opacity: 0, }, }), diff --git a/packages/react/src/utils/positioner/Popper.tsx b/packages/react/src/utils/positioner/Popper.tsx index 2ae0f6db8f..c327173da7 100644 --- a/packages/react/src/utils/positioner/Popper.tsx +++ b/packages/react/src/utils/positioner/Popper.tsx @@ -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' @@ -28,34 +36,32 @@ function useDeepMemo(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. @@ -78,13 +84,11 @@ const Popper: React.FunctionComponent = props => { const proposedPlacement = getPlacement({ align, position, rtl }) - const popperRef = React.useRef() + const popperRef = React.useRef() const contentRef = React.useRef(null) - const latestPlacement = React.useRef(proposedPlacement) - const [computedPlacement, setComputedPlacement] = React.useState( - proposedPlacement, - ) + const latestPlacement = React.useRef(proposedPlacement) + const [computedPlacement, setComputedPlacement] = React.useState(proposedPlacement) const hasDocument = isBrowser() const hasScrollableElement = React.useMemo(() => { @@ -99,35 +103,38 @@ const Popper: React.FunctionComponent = 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 }, + }, + { name: 'flip', options: { padding: 0, flipVariations: true } }, + { name: 'preventOverflow', options: { padding: 0 } }, + + 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, @@ -137,25 +144,25 @@ const Popper: React.FunctionComponent = 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 } }, []) @@ -166,41 +173,50 @@ const Popper: React.FunctionComponent = props => { const reference = targetRef && isRefObject(targetRef) ? (targetRef as React.RefObject).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 }) => { + 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) @@ -213,7 +229,7 @@ const Popper: React.FunctionComponent = props => { proposedPlacement, targetRef, unstable_pinned, - ]) + ]) // TODO: use options instead of recreate useIsomorphicLayoutEffect(() => { createInstance() diff --git a/packages/react/src/utils/positioner/positioningHelper.ts b/packages/react/src/utils/positioner/positioningHelper.ts index c14a7e042a..56189a3d65 100644 --- a/packages/react/src/utils/positioner/positioningHelper.ts +++ b/packages/react/src/utils/positioner/positioningHelper.ts @@ -1,5 +1,4 @@ -import { Placement } from 'popper.js' - +import { Placement } from '@popperjs/core' import { Alignment, Position } from './types' enum PlacementParts { diff --git a/packages/react/src/utils/positioner/types.ts b/packages/react/src/utils/positioner/types.ts index 694f7e2f22..4580b442c8 100644 --- a/packages/react/src/utils/positioner/types.ts +++ b/packages/react/src/utils/positioner/types.ts @@ -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' @@ -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. @@ -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 | PopperJS.ReferenceObject + targetRef: React.RefObject | VirtualElement /** * Rtl attribute for the component. @@ -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. diff --git a/packages/react/test/__mocks__/popper.js.ts b/packages/react/test/__mocks__/popper.js.ts index deecc7935b..bde0a93d06 100644 --- a/packages/react/test/__mocks__/popper.js.ts +++ b/packages/react/test/__mocks__/popper.js.ts @@ -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 { diff --git a/packages/react/test/specs/utils/positioner/positioningHelper-test.ts b/packages/react/test/specs/utils/positioner/positioningHelper-test.ts index b5b1c6870e..ec11238702 100644 --- a/packages/react/test/specs/utils/positioner/positioningHelper-test.ts +++ b/packages/react/test/specs/utils/positioner/positioningHelper-test.ts @@ -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' diff --git a/yarn.lock b/yarn.lock index a2ec702472..b6dfa150b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3930,6 +3930,11 @@ universal-user-agent "^2.0.0" url-template "^2.0.8" +"@popperjs/core@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.0.6.tgz#5a39ac118811ca844155b0ad5190b8c24f35e533" + integrity sha512-zj7Gw8QC4jmR92eKUvtrZUEpl2ypRbq+qlE4pwf9n2hnUO9BOAcWUs4/Ht+gNIbFt98xtqhLvccdCfD469MzpQ== + "@reach/router@^1.2.1": version "1.2.1" resolved "https://registry.npmjs.org/@reach/router/-/router-1.2.1.tgz#34ae3541a5ac44fa7796e5506a5d7274a162be4e" @@ -16958,11 +16963,6 @@ popper.js@^1.14.4, popper.js@^1.14.7: resolved "https://registry.npmjs.org/popper.js/-/popper.js-1.16.0.tgz#2e1816bcbbaa518ea6c2e15a466f4cb9c6e2fbb3" integrity sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw== -popper.js@^1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2" - integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA== - portfinder@^1.0.13: version "1.0.25" resolved "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca"