Skip to content

Commit eb123de

Browse files
authored
chore: replace react-popper with @floating-ui/react (#2856)
1 parent 73ba880 commit eb123de

File tree

12 files changed

+263
-154
lines changed

12 files changed

+263
-154
lines changed

examples/vite/yarn.lock

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,42 @@
176176
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f"
177177
integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
178178

179+
"@floating-ui/core@^1.7.3":
180+
version "1.7.3"
181+
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.3.tgz#462d722f001e23e46d86fd2bd0d21b7693ccb8b7"
182+
integrity sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==
183+
dependencies:
184+
"@floating-ui/utils" "^0.2.10"
185+
186+
"@floating-ui/dom@^1.7.4":
187+
version "1.7.4"
188+
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.4.tgz#ee667549998745c9c3e3e84683b909c31d6c9a77"
189+
integrity sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==
190+
dependencies:
191+
"@floating-ui/core" "^1.7.3"
192+
"@floating-ui/utils" "^0.2.10"
193+
194+
"@floating-ui/react-dom@^2.1.6":
195+
version "2.1.6"
196+
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.6.tgz#189f681043c1400561f62972f461b93f01bf2231"
197+
integrity sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==
198+
dependencies:
199+
"@floating-ui/dom" "^1.7.4"
200+
201+
"@floating-ui/react@^0.27.2":
202+
version "0.27.16"
203+
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.27.16.tgz#6e485b5270b7a3296fdc4d0faf2ac9abf955a2f7"
204+
integrity sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==
205+
dependencies:
206+
"@floating-ui/react-dom" "^2.1.6"
207+
"@floating-ui/utils" "^0.2.10"
208+
tabbable "^6.0.0"
209+
210+
"@floating-ui/utils@^0.2.10":
211+
version "0.2.10"
212+
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c"
213+
integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==
214+
179215
"@humanwhocodes/config-array@^0.11.14":
180216
version "0.11.14"
181217
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"
@@ -216,11 +252,6 @@
216252
"@nodelib/fs.scandir" "2.1.5"
217253
fastq "^1.6.0"
218254

219-
"@popperjs/core@^2.11.5":
220-
version "2.11.8"
221-
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
222-
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
223-
224255
"@react-aria/focus@^3":
225256
version "3.16.2"
226257
resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.16.2.tgz#2285bc19e091233b4d52399c506ac8fa60345b44"
@@ -1409,10 +1440,10 @@ levn@^0.4.1:
14091440
prelude-ls "^1.2.1"
14101441
type-check "~0.4.0"
14111442

1412-
linkifyjs@^4.1.0:
1413-
version "4.1.3"
1414-
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.3.tgz#0edbc346428a7390a23ea2e5939f76112c9ae07f"
1415-
integrity sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==
1443+
linkifyjs@^4.3.2:
1444+
version "4.3.2"
1445+
resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.3.2.tgz#d97eb45419aabf97ceb4b05a7adeb7b8c8ade2b1"
1446+
integrity sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==
14161447

14171448
load-script@^1.0.0:
14181449
version "1.0.0"
@@ -1466,7 +1497,7 @@ longest-streak@^3.0.0:
14661497
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4"
14671498
integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==
14681499

1469-
loose-envify@^1.0.0, loose-envify@^1.4.0:
1500+
loose-envify@^1.4.0:
14701501
version "1.4.0"
14711502
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
14721503
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@@ -2175,14 +2206,6 @@ react-player@2.10.1:
21752206
prop-types "^15.7.2"
21762207
react-fast-compare "^3.0.1"
21772208

2178-
react-popper@^2.3.0:
2179-
version "2.3.0"
2180-
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.3.0.tgz#17891c620e1320dce318bad9fede46a5f71c70ba"
2181-
integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==
2182-
dependencies:
2183-
react-fast-compare "^3.0.1"
2184-
warning "^4.0.2"
2185-
21862209
react-textarea-autosize@^8.3.0:
21872210
version "8.5.3"
21882211
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz#d1e9fe760178413891484847d3378706052dd409"
@@ -2407,6 +2430,11 @@ supports-color@^7.1.0:
24072430
dependencies:
24082431
has-flag "^4.0.0"
24092432

2433+
tabbable@^6.0.0:
2434+
version "6.2.0"
2435+
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97"
2436+
integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==
2437+
24102438
text-table@^0.2.0:
24112439
version "0.2.0"
24122440
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@@ -2581,13 +2609,6 @@ vite@^6.3.5:
25812609
optionalDependencies:
25822610
fsevents "~2.3.3"
25832611

2584-
warning@^4.0.2:
2585-
version "4.0.3"
2586-
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
2587-
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
2588-
dependencies:
2589-
loose-envify "^1.0.0"
2590-
25912612
which@^2.0.1:
25922613
version "2.0.2"
25932614
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
],
103103
"dependencies": {
104104
"@braintree/sanitize-url": "^6.0.4",
105-
"@popperjs/core": "^2.11.5",
105+
"@floating-ui/react": "^0.27.2",
106106
"@react-aria/focus": "^3",
107107
"clsx": "^2.0.0",
108108
"dayjs": "^1.10.4",
@@ -122,7 +122,6 @@
122122
"react-image-gallery": "1.2.12",
123123
"react-markdown": "^9.0.3",
124124
"react-player": "2.10.1",
125-
"react-popper": "^2.3.0",
126125
"react-textarea-autosize": "^8.3.0",
127126
"react-virtuoso": "^2.16.5",
128127
"remark-gfm": "^4.0.1",

src/components/Dialog/ButtonWithSubmenu.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
33
import { useDialog, useDialogIsOpen } from './hooks';
44
import { useDialogAnchor } from './DialogAnchor';
55
import type { ComponentProps, ComponentType } from 'react';
6-
import type { Placement } from '@popperjs/core';
6+
import type { PopperLikePlacement } from './hooks';
77

88
type ButtonWithSubmenu = ComponentProps<'button'> & {
99
children: React.ReactNode;
10-
placement: Placement;
10+
placement: PopperLikePlacement;
1111
Submenu: ComponentType;
1212
submenuContainerProps?: ComponentProps<'div'>;
1313
};
@@ -26,7 +26,7 @@ export const ButtonWithSubmenu = ({
2626
const dialogId = useMemo(() => `submenu-${Math.random().toString(36).slice(2)}`, []);
2727
const dialog = useDialog({ id: dialogId });
2828
const dialogIsOpen = useDialogIsOpen(dialogId);
29-
const { attributes, setPopperElement, styles } = useDialogAnchor<HTMLDivElement>({
29+
const { setPopperElement, styles } = useDialogAnchor<HTMLDivElement>({
3030
open: dialogIsOpen,
3131
placement,
3232
referenceElement: buttonRef.current,
@@ -102,7 +102,6 @@ export const ButtonWithSubmenu = ({
102102
</button>
103103
{dialogIsOpen && (
104104
<div
105-
{...attributes.popper}
106105
onBlur={(event) => {
107106
const isBlurredDescendant =
108107
event.relatedTarget instanceof Node &&
@@ -125,7 +124,7 @@ export const ButtonWithSubmenu = ({
125124
setPopperElement(element);
126125
setDialogContainer(element);
127126
}}
128-
style={styles.popper}
127+
style={styles}
129128
tabIndex={-1}
130129
{...submenuContainerProps}
131130
>

src/components/Dialog/DialogAnchor.tsx

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import clsx from 'clsx';
2-
import type { Placement } from '@popperjs/core';
32
import type { ComponentProps, PropsWithChildren } from 'react';
43
import React, { useEffect, useState } from 'react';
54
import { FocusScope } from '@react-aria/focus';
6-
import { usePopper } from 'react-popper';
75
import { DialogPortalEntry } from './DialogPortal';
86
import { useDialog, useDialogIsOpen } from './hooks';
7+
import { usePopoverPosition } from './hooks/usePopoverPosition';
8+
import type { PopperLikePlacement } from './hooks';
99

1010
export interface DialogAnchorOptions {
1111
open: boolean;
12-
placement: Placement;
12+
placement: PopperLikePlacement;
1313
referenceElement: HTMLElement | null;
1414
allowFlip?: boolean;
1515
}
@@ -21,25 +21,20 @@ export function useDialogAnchor<T extends HTMLElement>({
2121
referenceElement,
2222
}: DialogAnchorOptions) {
2323
const [popperElement, setPopperElement] = useState<T | null>(null);
24-
const { attributes, styles, update } = usePopper(referenceElement, popperElement, {
25-
modifiers: [
26-
{
27-
enabled: !!allowFlip, // Prevent flipping
28-
name: 'flip',
29-
},
30-
{
31-
name: 'eventListeners',
32-
options: {
33-
// It's not safe to update popper position on resize and scroll, since popper's
34-
// reference element might not be visible at the time.
35-
resize: false,
36-
scroll: false,
37-
},
38-
},
39-
],
24+
const { refs, strategy, update, x, y } = usePopoverPosition({
25+
allowFlip,
26+
freeze: true,
4027
placement,
4128
});
4229

30+
useEffect(() => {
31+
refs.setReference(referenceElement);
32+
}, [referenceElement, refs]);
33+
34+
useEffect(() => {
35+
refs.setFloating(popperElement);
36+
}, [popperElement, refs]);
37+
4338
useEffect(() => {
4439
if (open && popperElement) {
4540
// Since the popper's reference element might not be (and usually is not) visible
@@ -54,9 +49,12 @@ export function useDialogAnchor<T extends HTMLElement>({
5449
}
5550

5651
return {
57-
attributes,
5852
setPopperElement,
59-
styles,
53+
styles: {
54+
left: x ?? 0,
55+
position: strategy,
56+
top: y ?? 0,
57+
} as React.CSSProperties,
6058
};
6159
}
6260

@@ -80,7 +78,7 @@ export const DialogAnchor = ({
8078
}: DialogAnchorProps) => {
8179
const dialog = useDialog({ id });
8280
const open = useDialogIsOpen(id);
83-
const { attributes, setPopperElement, styles } = useDialogAnchor<HTMLDivElement>({
81+
const { setPopperElement, styles } = useDialogAnchor<HTMLDivElement>({
8482
allowFlip,
8583
open,
8684
placement,
@@ -111,11 +109,10 @@ export const DialogAnchor = ({
111109
<FocusScope autoFocus={focus} contain={trapFocus} restoreFocus>
112110
<div
113111
{...restDivProps}
114-
{...attributes.popper}
115112
className={clsx('str-chat__dialog-contents', className)}
116113
data-testid='str-chat__dialog-contents'
117114
ref={setPopperElement}
118-
style={styles.popper}
115+
style={styles}
119116
tabIndex={typeof tabIndex !== 'undefined' ? tabIndex : 0}
120117
>
121118
{children}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './useDialog';
2+
export type { PopperLikePlacement } from './usePopoverPosition';
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import {
2+
autoPlacement,
3+
autoUpdate,
4+
flip as flipMw,
5+
offset as offsetMw,
6+
type Placement,
7+
shift as shiftMw,
8+
size as sizeMw,
9+
useFloating,
10+
} from '@floating-ui/react';
11+
import type { AutoPlacementOptions } from '@floating-ui/core';
12+
13+
const hasResizeObserver = typeof window !== 'undefined' && 'ResizeObserver' in window;
14+
15+
export type PopperLikePlacement = Placement | 'auto' | 'auto-start' | 'auto-end';
16+
17+
function autoMiddlewareFor(p: PopperLikePlacement) {
18+
if (!String(p).startsWith('auto')) return null;
19+
const alignment: AutoPlacementOptions['alignment'] =
20+
p === 'auto-start' ? 'start' : p === 'auto-end' ? 'end' : undefined;
21+
return autoPlacement({ alignment });
22+
}
23+
24+
type OffsetOpt =
25+
| number
26+
| { mainAxis?: number; crossAxis?: number; alignmentAxis?: number }
27+
| [crossAxis: number, mainAxis: number]; // keep your tuple compat
28+
29+
function toOffsetMw(opt?: OffsetOpt) {
30+
if (opt == null) return null;
31+
if (Array.isArray(opt)) {
32+
const [crossAxis, mainAxis] = opt;
33+
return offsetMw({ crossAxis, mainAxis });
34+
}
35+
if (typeof opt === 'number') return offsetMw(opt);
36+
return offsetMw(opt);
37+
}
38+
39+
export type UsePopoverParams = {
40+
placement?: PopperLikePlacement;
41+
/** Add flip() when placement is not 'auto*' */
42+
allowFlip?: boolean;
43+
/** Keep in viewport; default true to match common popper setups */
44+
allowShift?: boolean;
45+
/** The floating UI is fitted to the available space (by constraining its max size) instead of letting it overflow; default false */
46+
fitAvailableSpace?: boolean;
47+
/** Offset (number, object, or [crossAxis, mainAxis] tuple) */
48+
offset?: OffsetOpt;
49+
/**
50+
* Freeze behavior like Popper's eventListeners: { scroll:false, resize:false }.
51+
* If true → no autoUpdate (you can call `update()` manually).
52+
*/
53+
freeze?: boolean;
54+
/**
55+
* Fine-grained control of autoUpdate triggers (only if freeze=false).
56+
* Defaults match Popper's "disabled" example when all set to false.
57+
*/
58+
autoUpdateOptions?: Partial<Parameters<typeof autoUpdate>[3]>;
59+
};
60+
61+
export function usePopoverPosition({
62+
allowFlip = true,
63+
allowShift = true,
64+
autoUpdateOptions,
65+
fitAvailableSpace = false,
66+
freeze = false,
67+
offset,
68+
placement = 'bottom-start',
69+
}: UsePopoverParams) {
70+
const autoMw = autoMiddlewareFor(placement);
71+
const offsetMiddleware = toOffsetMw(offset);
72+
73+
const middleware = [
74+
// offset first (mirrors common Popper setups)
75+
...(offsetMiddleware ? [offsetMiddleware] : []),
76+
77+
// choose between autoPlacement (Popper's "auto*") OR flip()
78+
...(autoMw ? [autoMw] : allowFlip ? [flipMw()] : []),
79+
80+
// viewport collision adjustments
81+
...(allowShift ? [shiftMw({ padding: 8 })] : []),
82+
83+
// optional size constraining
84+
// eslint-disable-next-line @typescript-eslint/no-empty-function
85+
...(fitAvailableSpace ? [sizeMw({ apply: () => {} })] : []),
86+
];
87+
88+
// if placement is 'auto*', seed with any static placement; autoPlacement will pick the final one
89+
const seedPlacement: Placement = String(placement).startsWith('auto')
90+
? 'bottom'
91+
: (placement as Placement);
92+
93+
return useFloating({
94+
middleware,
95+
placement: seedPlacement,
96+
whileElementsMounted: freeze
97+
? undefined
98+
: (reference, floating, update) =>
99+
autoUpdate(reference, floating, update, {
100+
ancestorResize: true,
101+
ancestorScroll: true,
102+
animationFrame: false,
103+
elementResize: hasResizeObserver,
104+
...autoUpdateOptions,
105+
}),
106+
});
107+
}

src/components/Form/Dropdown.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useEffect } from 'react';
44
import React, { useState } from 'react';
55
import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog';
66
import { DialogManagerProvider, useTranslationContext } from '../../context';
7-
import type { Placement } from '@popperjs/core';
7+
import type { PopperLikePlacement } from '../Dialog';
88

99
type DropdownContextValue = {
1010
close(): void;
@@ -28,7 +28,7 @@ export const useDropdownContext = () => React.useContext(DropdownContext);
2828
export type DropdownProps = PropsWithChildren<{
2929
className?: string;
3030
openButtonProps?: React.HTMLAttributes<HTMLButtonElement>;
31-
placement?: Placement;
31+
placement?: PopperLikePlacement;
3232
}>;
3333

3434
export const Dropdown = (props: DropdownProps) => {

0 commit comments

Comments
 (0)