Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: migrate from Popper to Floating UI #233

Merged
merged 8 commits into from
Nov 7, 2023
46 changes: 45 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
"styled-components": "^5.3.3"
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"@floating-ui/dom": "^1.5.3",
"darkreader": "4.9.58",
"polished": "4.2.2",
"react-datepicker": "^4.16.0",
Expand Down
2 changes: 1 addition & 1 deletion src/components/basic/TextWithTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import React from 'react';

import { Placement } from '@popperjs/core/lib/enums';
import { Placement } from '@floating-ui/dom';

import { Text, TextProps } from './Text';
import { Tooltip } from '../display/Tooltip';
Expand Down
53 changes: 14 additions & 39 deletions src/components/display/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,15 @@ import React, {
HTMLAttributes
} from 'react';

import {
createPopper,
Instance,
OptionsGeneric,
StrictModifiers,
VirtualElement
} from '@popperjs/core';
import { flip, limitShift, Placement, shift, VirtualElement } from '@floating-ui/dom';
import { find, some } from 'lodash';
import styled, { css, DefaultTheme, SimpleInterpolation, ThemeContext } from 'styled-components';

import { Tooltip } from './Tooltip';
import { useCombinedRefs } from '../../hooks/useCombinedRefs';
import { useKeyboard, getKeyboardPreset, KeyboardPreset } from '../../hooks/useKeyboard';
import { pseudoClasses } from '../../theme/theme-utils';
import { setupFloating } from '../../utils/floating-ui';
import { Icon } from '../basic/Icon';
import { Text } from '../basic/Text';
import { Container } from '../layout/Container';
Expand Down Expand Up @@ -263,7 +258,7 @@ const PopperList = styled.div<{
triggerRef: React.RefObject<HTMLElement>;
open: boolean;
}>`
position: absolute;
position: fixed;
display: none;
visibility: hidden;
pointer-events: none;
Expand Down Expand Up @@ -350,22 +345,7 @@ interface DropdownProps extends Omit<HTMLAttributes<HTMLDivElement>, 'contextMen
/** trigger ref that can be used instead of lost children ref caused by cloneElement */
triggerRef?: React.Ref<HTMLElement>;
/** Placement of the dropdown */
placement?:
| 'auto'
| 'auto-start'
| 'auto-end'
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'right'
| 'right-start'
| 'right-end'
| 'left'
| 'left-start'
| 'left-end';
placement?: Placement;
/** Flag to disable the Portal implementation */
disablePortal?: boolean;
/** Whether the Component is visible or not */
Expand Down Expand Up @@ -489,8 +469,7 @@ const Dropdown = React.forwardRef<HTMLDivElement, DropdownProps>(function Dropdo
bottom: e.clientY,
left: e.clientX,
x: e.clientX,
y: e.clientY,
toJSON: (): string => 'TODO' // TODO: check what this should return
y: e.clientY
})
};
setPosition(virtualElement);
Expand Down Expand Up @@ -572,24 +551,20 @@ const Dropdown = React.forwardRef<HTMLDivElement, DropdownProps>(function Dropdo
useKeyboard(dropdownRef, escapeEvent);

useLayoutEffect(() => {
let popperInstance: Instance | undefined;
let cleanup: ReturnType<typeof setupFloating>;
if (open) {
const popperOptions: OptionsGeneric<StrictModifiers> = {
placement,
modifiers: [],
strategy: 'fixed'
};

const popperReference = contextMenu ? position : innerTriggerRef.current;
if (popperReference && dropdownRef.current) {
popperInstance = createPopper<StrictModifiers>(
popperReference,
dropdownRef.current,
popperOptions
);
cleanup = setupFloating(popperReference, dropdownRef.current, {
placement,
middleware: [flip(), shift({ limiter: limitShift() })],
strategy: 'fixed'
});
}
}
return (): void => popperInstance && popperInstance.destroy();
return (): void => {
cleanup?.();
};
}, [open, placement, contextMenu, position, dropdownRef, innerTriggerRef]);

useEffect(() => {
Expand Down
5 changes: 2 additions & 3 deletions src/components/display/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import React, { useEffect, useState, useCallback, useMemo, useContext } from 'react';

import { VirtualElement } from '@popperjs/core';
import { VirtualElement } from '@floating-ui/dom';
import { debounce } from 'lodash';
import styled, { ThemeContext } from 'styled-components';

Expand Down Expand Up @@ -61,8 +61,7 @@ const Popover = React.forwardRef<HTMLDivElement, PopoverProps>(function PopoverF
bottom: clientY,
left: clientX,
x: clientX,
y: clientY,
toJSON: (): unknown => undefined
y: clientY
});
setInnerOpen(true);
onMouseMove.cancel();
Expand Down
55 changes: 17 additions & 38 deletions src/components/display/Popper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,17 @@ import React, {
HTMLAttributes
} from 'react';

import {
createPopper,
OptionsGeneric,
Placement,
StrictModifiers,
VirtualElement
} from '@popperjs/core';
import { flip, Placement, VirtualElement, offset, shift, limitShift } from '@floating-ui/dom';
import styled, { css, SimpleInterpolation, ThemeContext } from 'styled-components';

import { useCombinedRefs } from '../../hooks/useCombinedRefs';
import { KeyboardPreset, useKeyboard } from '../../hooks/useKeyboard';
import { setupFloating } from '../../utils/floating-ui';
import { Portal } from '../utilities/Portal';

const PopperContainer = styled.div<{ open: boolean }>`
display: none;
position: absolute;
${({ open }): SimpleInterpolation =>
open &&
css`
Expand Down Expand Up @@ -122,19 +118,8 @@ const Popper = React.forwardRef<HTMLDivElement, PopperProps>(function PopperFn(
useKeyboard(popperRef, escapeEvent);

useLayoutEffect(() => {
let cleanup: ReturnType<typeof setupFloating>;
if (open) {
const popperOptions: Pick<OptionsGeneric<StrictModifiers>, 'placement' | 'modifiers'> = {
placement,
modifiers: [
{
name: 'offset',
options: {
offset: [0, 8]
}
}
]
};

const anchorElement = anchorEl.current;
if (anchorElement) {
const virtualEl = virtualElement && {
Expand All @@ -146,31 +131,25 @@ const Popper = React.forwardRef<HTMLDivElement, PopperProps>(function PopperFn(
bottom: virtualElement.y,
left: virtualElement.x,
y: virtualElement.y,
x: virtualElement.x,
toJSON: (): unknown => undefined
x: virtualElement.x
})
};
if (!virtualEl) {
popperOptions.modifiers.push({
name: 'flip',
options: {
fallbackPlacements: ['bottom']
}

if (popperRef.current) {
cleanup = setupFloating(virtualEl || anchorElement, popperRef.current, {
placement,
middleware: [
offset(8),
!virtualEl && flip({ fallbackPlacements: ['bottom'] }),
rodleyorosa marked this conversation as resolved.
Show resolved Hide resolved
shift({ limiter: limitShift() })
]
});
}
const popperInstance =
popperRef.current &&
createPopper<StrictModifiers>(
virtualEl || anchorElement,
popperRef.current,
popperOptions
);
return (): void => {
popperInstance && popperInstance.destroy();
};
}
}
return (): void => undefined;
return (): void => {
cleanup?.();
};
}, [open, placement, anchorEl, virtualElement, popperRef]);

useEffect(() => {
Expand Down
29 changes: 9 additions & 20 deletions src/components/display/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import React, {
createRef
} from 'react';

import { createPopper, Instance, Placement } from '@popperjs/core';
import { flip, Placement, offset, shift, limitShift } from '@floating-ui/dom';
import { rgba } from 'polished';
import styled, { css, SimpleInterpolation } from 'styled-components';

import { useCombinedRefs } from '../../hooks/useCombinedRefs';
import { setupFloating } from '../../utils/floating-ui';
import { Text, TextProps } from '../basic/Text';
import { Portal } from '../utilities/Portal';

Expand All @@ -42,7 +43,7 @@ const TooltipWrapper = React.forwardRef<HTMLDivElement, TooltipWrapperProps>(
);
const TooltipWrapperWithCss = styled(TooltipWrapper)<{ $maxWidth: string }>`
display: none;
position: fixed;
position: absolute;
top: -1000px;
left: -1000px;
z-index: 5000;
Expand Down Expand Up @@ -101,7 +102,6 @@ const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(function TooltipF
ref
) {
const [open, setOpen] = useState(false);
const popperInstanceRef = useRef<Instance>();
const combinedTriggerRef = useCombinedRefs<HTMLElement>(triggerRef);
const tooltipRef = useCombinedRefs<HTMLDivElement>(ref);
const timeoutRef = useRef<null | ReturnType<typeof setTimeout>>(null);
Expand All @@ -128,27 +128,16 @@ const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(function TooltipF
}, []);

useLayoutEffect(() => {
let cleanup: ReturnType<typeof setupFloating>;
if (open && !disabled && combinedTriggerRef.current && tooltipRef.current) {
popperInstanceRef.current = createPopper(combinedTriggerRef.current, tooltipRef.current, {
cleanup = setupFloating(combinedTriggerRef.current, tooltipRef.current, {
placement,
modifiers: [
{
name: 'offset',
options: {
offset: [0, 8]
}
},
{
name: 'flip',
options: {
fallbackPlacements
}
}
]
middleware: [offset(8), flip({ fallbackPlacements }), shift({ limiter: limitShift() })]
});
} else if (popperInstanceRef.current) {
popperInstanceRef.current.destroy();
}
return (): void => {
cleanup?.();
};
}, [disabled, fallbackPlacements, open, placement, tooltipRef, combinedTriggerRef]);

useEffect(() => {
Expand Down
26 changes: 26 additions & 0 deletions src/utils/floating-ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2023 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
autoUpdate,
computePosition,
ComputePositionConfig,
ReferenceElement
} from '@floating-ui/dom';

export function setupFloating(
reference: ReferenceElement,
floating: HTMLElement,
options?: Partial<ComputePositionConfig>
): ReturnType<typeof autoUpdate> {
return autoUpdate(reference, floating, () => {
computePosition(reference, floating, options).then(({ x, y }) => {
Object.assign(floating.style, {
left: `${x}px`,
top: `${y}px`
});
});
});
}