Skip to content

Commit

Permalink
feat(floating-menu): replace popperjs with floatingui (#2097)
Browse files Browse the repository at this point in the history
  • Loading branch information
shlroland committed Jul 3, 2023
1 parent 2d3fdc5 commit f1f9925
Show file tree
Hide file tree
Showing 8 changed files with 479 additions and 636 deletions.
6 changes: 6 additions & 0 deletions .changeset/thin-cars-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@remirror/react-components': patch
'@remirror/react': patch
---

Replace [Popper](https://popper.js.org/) with [Floating UI](https://floating-ui.com/).
3 changes: 2 additions & 1 deletion packages/remirror__react-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
"@emotion/styled": "^11.11.0",
"@lingui/core": "^4.2.0",
"@mui/material": "^5.13.2",
"@popperjs/core": "^2.11.8",
"@remirror/core": "^2.0.17",
"@remirror/extension-blockquote": "^2.0.14",
"@remirror/extension-bold": "^2.0.13",
Expand Down Expand Up @@ -81,6 +80,7 @@
"react-color": "^2.19.3"
},
"devDependencies": {
"@floating-ui/react": "^0.24.3",
"@remirror/pm": "^2.0.6",
"@remirror/react": "^2.0.29",
"@types/react": "^18.2.0",
Expand All @@ -89,6 +89,7 @@
"react-dom": "^18.2.0"
},
"peerDependencies": {
"@floating-ui/react": "^0.24.3",
"@remirror/pm": "^2.0.5",
"@types/react": "^16.14.0 || ^17 || ^18",
"@types/react-dom": "^16.9.0 || ^17 || ^18",
Expand Down
100 changes: 80 additions & 20 deletions packages/remirror__react-components/src/floating-menu.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
import { Placement } from '@popperjs/core';
import {
Alignment,
autoPlacement,
autoUpdate,
flip,
FloatingPortal,
Middleware,
offset,
Placement as FloatingUIPlacement,
Strategy,
useFloating,
} from '@floating-ui/react';
import React, {
FC,
MouseEventHandler,
PropsWithChildren,
ReactChild,
ReactNode,
Ref,
useCallback,
useMemo,
} from 'react';
import { createPortal } from 'react-dom';
import { cx } from '@remirror/core';
import { cx, isObject } from '@remirror/core';
import type { PositionerParam } from '@remirror/extension-positioner';
import { getPositioner } from '@remirror/extension-positioner';
import { useHelpers } from '@remirror/react-core';
import { useEditorFocus, UseEditorFocusProps, usePositioner } from '@remirror/react-hooks';
import { ComponentsTheme, ExtensionPositionerTheme } from '@remirror/theme';

import { composeRefs } from './commonjs-packages/seznam-compose-react-refs';
import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
import { usePopper } from './use-popper';

interface BaseFloatingPositioner extends UseEditorFocusProps {
/**
Expand Down Expand Up @@ -47,8 +56,13 @@ interface BaseFloatingPositioner extends UseEditorFocusProps {

/**
* Where to place the popover relative to the positioner.
* @remarks
* The floating-ui library has removed the auto- prefixed placement attribute types.
* The type declaration you see here is for compatibility with Popper.js.
*
* https://floating-ui.com/docs/autoPlacement#conflict-with-flip
*/
placement?: Placement;
placement?: FloatingUIPlacement | 'auto' | `auto-${Alignment}`;

/**
* When `true` the child component is rendered outside the `ProseMirror`
Expand All @@ -64,6 +78,27 @@ interface BaseFloatingPositioner extends UseEditorFocusProps {
* @defaultValue false
*/
renderOutsideEditor?: boolean;

/**
* Array of middleware objects to modify the positioning or provide data for
* rendering.
*/
middleware?: Array<Middleware | null | undefined | false>;

/**
* The strategy to use when positioning the floating element.
*/
strategy?: Strategy;

/**
* Portals the floating element into a given container element — by default,
* outside of the app root and into the body.
* @see https://floating-ui.com/docs/FloatingPortal
* @defaultValue false
* @remarks This is conflict to renderOutsideEditor, and renderOutsideEditor has high priority.
* And this property will cause the loss of the css variable if you use remirror's internal style
*/
useFloatingPortal?: boolean | Parameters<typeof FloatingPortal>[0];
}

interface FloatingWrapperProps extends BaseFloatingPositioner {
Expand Down Expand Up @@ -105,6 +140,9 @@ export const FloatingWrapper: FC<PropsWithChildren<FloatingWrapperProps>> = (
floatingLabel,
hideWhenInvisible = true,
renderOutsideEditor = false,
middleware: propsMiddleware,
strategy,
useFloatingPortal,
} = props;

const [isFocused] = useEditorFocus({ blurOnInactive, ignoredElements });
Expand All @@ -124,41 +162,57 @@ export const FloatingWrapper: FC<PropsWithChildren<FloatingWrapperProps>> = (

const shouldShow = (hideWhenInvisible ? visible : true) && active;
const position = useMemoizedPosition({ height, left, top, width });
const { popperRef, referenceRef, popoverStyles, update } = usePopper({
placement,
visible,

const _placement = isFloatingUIPlacement(placement) ? placement : undefined;

const middleware = useMemo(() => {
if (propsMiddleware) {
return propsMiddleware;
}

return [
_placement ? flip({ padding: 8 }) : autoPlacement({ padding: 8 }),
offset({ mainAxis: 12 }),
];
}, [_placement, propsMiddleware]);

const { refs, floatingStyles } = useFloating({
placement: _placement,
open: visible,
whileElementsMounted: autoUpdate,
strategy,
middleware,
});

const handleMouseDown: MouseEventHandler<HTMLDivElement> = useCallback(
(e) => {
if (renderOutsideEditor) {
if (renderOutsideEditor || useFloatingPortal) {
// Prevent blur events from being triggered
e.preventDefault();
}
},
[renderOutsideEditor],
[renderOutsideEditor, useFloatingPortal],
);

let floatingElement = (
<div
aria-label={floatingLabel}
ref={popperRef as any}
style={popoverStyles}
ref={refs.setFloating}
style={floatingStyles}
className={cx(ComponentsTheme.FLOATING_POPOVER, containerClass)}
onMouseDown={handleMouseDown}
>
{shouldShow && children}
</div>
);

if (!renderOutsideEditor) {
if (!renderOutsideEditor && !useFloatingPortal) {
floatingElement = <PositionerPortal>{floatingElement}</PositionerPortal>;
} else if (useFloatingPortal) {
const props = isObject(useFloatingPortal) ? useFloatingPortal : {};
floatingElement = <FloatingPortal {...props}>{floatingElement}</FloatingPortal>;
}

useIsomorphicLayoutEffect(() => {
update();
}, [shouldShow, update, height, left, top, width]);

return (
<>
<PositionerPortal>
Expand All @@ -170,7 +224,7 @@ export const FloatingWrapper: FC<PropsWithChildren<FloatingWrapperProps>> = (
width: position.width,
height: position.height,
}}
ref={composeRefs(ref, referenceRef) as Ref<any>}
ref={composeRefs(ref, refs.setReference) as Ref<any>}
/>
</PositionerPortal>
{floatingElement}
Expand All @@ -179,7 +233,7 @@ export const FloatingWrapper: FC<PropsWithChildren<FloatingWrapperProps>> = (
};

export interface PositionerComponentProps {
children: ReactChild;
children: ReactNode;
}

/**
Expand All @@ -192,6 +246,12 @@ export const PositionerPortal: FC<PositionerComponentProps> = (props) => {
return createPortal(<>{props.children}</>, container);
};

function isFloatingUIPlacement(
placement: BaseFloatingPositioner['placement'],
): placement is FloatingUIPlacement {
return !!placement?.startsWith('auto');
}

// interface FloatingActionsMenuProps extends Partial<FloatingWrapperProps> {
// actions: MenuActionItemUnion[];
// }
Expand Down

1 comment on commit f1f9925

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

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

🎉 Published on https://remirror.io as production
🚀 Deployed on https://64a2a0ef04804b71a50f86bb--remirror.netlify.app

Please sign in to comment.