From cb5e243622c05874869f86effa49cc6aa94eeeb9 Mon Sep 17 00:00:00 2001 From: martincupela Date: Fri, 10 Oct 2025 13:32:30 +0200 Subject: [PATCH 1/2] chore: replace react-popper with "@floating-ui/react --- examples/vite/yarn.lock | 71 +++++++----- package.json | 3 +- src/components/Dialog/ButtonWithSubmenu.tsx | 9 +- src/components/Dialog/DialogAnchor.tsx | 45 ++++---- src/components/Dialog/hooks/index.ts | 1 + .../Dialog/hooks/usePopoverPosition.ts | 105 ++++++++++++++++++ src/components/Form/Dropdown.tsx | 4 +- src/components/MessageActions/hooks/index.ts | 1 - .../hooks/useMessageActionsBoxPopper.ts | 49 -------- src/components/Tooltip/Tooltip.tsx | 37 +++--- src/plugins/Emojis/EmojiPicker.tsx | 29 +++-- yarn.lock | 61 ++++++---- 12 files changed, 261 insertions(+), 154 deletions(-) create mode 100644 src/components/Dialog/hooks/usePopoverPosition.ts delete mode 100644 src/components/MessageActions/hooks/index.ts delete mode 100644 src/components/MessageActions/hooks/useMessageActionsBoxPopper.ts diff --git a/examples/vite/yarn.lock b/examples/vite/yarn.lock index 18dc155e2c..3d248e981e 100644 --- a/examples/vite/yarn.lock +++ b/examples/vite/yarn.lock @@ -176,6 +176,42 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@floating-ui/core@^1.7.3": + version "1.7.3" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.3.tgz#462d722f001e23e46d86fd2bd0d21b7693ccb8b7" + integrity sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w== + dependencies: + "@floating-ui/utils" "^0.2.10" + +"@floating-ui/dom@^1.7.4": + version "1.7.4" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.4.tgz#ee667549998745c9c3e3e84683b909c31d6c9a77" + integrity sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA== + dependencies: + "@floating-ui/core" "^1.7.3" + "@floating-ui/utils" "^0.2.10" + +"@floating-ui/react-dom@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.6.tgz#189f681043c1400561f62972f461b93f01bf2231" + integrity sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw== + dependencies: + "@floating-ui/dom" "^1.7.4" + +"@floating-ui/react@^0.27.2": + version "0.27.16" + resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.27.16.tgz#6e485b5270b7a3296fdc4d0faf2ac9abf955a2f7" + integrity sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g== + dependencies: + "@floating-ui/react-dom" "^2.1.6" + "@floating-ui/utils" "^0.2.10" + tabbable "^6.0.0" + +"@floating-ui/utils@^0.2.10": + version "0.2.10" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c" + integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ== + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -216,11 +252,6 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@popperjs/core@^2.11.5": - version "2.11.8" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" - integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== - "@react-aria/focus@^3": version "3.16.2" resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.16.2.tgz#2285bc19e091233b4d52399c506ac8fa60345b44" @@ -1409,10 +1440,10 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -linkifyjs@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.1.3.tgz#0edbc346428a7390a23ea2e5939f76112c9ae07f" - integrity sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg== +linkifyjs@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-4.3.2.tgz#d97eb45419aabf97ceb4b05a7adeb7b8c8ade2b1" + integrity sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA== load-script@^1.0.0: version "1.0.0" @@ -1466,7 +1497,7 @@ longest-streak@^3.0.0: resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== -loose-envify@^1.0.0, loose-envify@^1.4.0: +loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -2175,14 +2206,6 @@ react-player@2.10.1: prop-types "^15.7.2" react-fast-compare "^3.0.1" -react-popper@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.3.0.tgz#17891c620e1320dce318bad9fede46a5f71c70ba" - integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q== - dependencies: - react-fast-compare "^3.0.1" - warning "^4.0.2" - react-textarea-autosize@^8.3.0: version "8.5.3" 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: dependencies: has-flag "^4.0.0" +tabbable@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" + integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -2581,13 +2609,6 @@ vite@^6.3.5: optionalDependencies: fsevents "~2.3.3" -warning@^4.0.2: - version "4.0.3" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" - integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== - dependencies: - loose-envify "^1.0.0" - which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" diff --git a/package.json b/package.json index 238610d9c7..ca919807bb 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ ], "dependencies": { "@braintree/sanitize-url": "^6.0.4", - "@popperjs/core": "^2.11.5", + "@floating-ui/react": "^0.27.2", "@react-aria/focus": "^3", "clsx": "^2.0.0", "dayjs": "^1.10.4", @@ -122,7 +122,6 @@ "react-image-gallery": "1.2.12", "react-markdown": "^9.0.3", "react-player": "2.10.1", - "react-popper": "^2.3.0", "react-textarea-autosize": "^8.3.0", "react-virtuoso": "^2.16.5", "remark-gfm": "^4.0.1", diff --git a/src/components/Dialog/ButtonWithSubmenu.tsx b/src/components/Dialog/ButtonWithSubmenu.tsx index c8f2587a3d..070b2eed96 100644 --- a/src/components/Dialog/ButtonWithSubmenu.tsx +++ b/src/components/Dialog/ButtonWithSubmenu.tsx @@ -3,11 +3,11 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useDialog, useDialogIsOpen } from './hooks'; import { useDialogAnchor } from './DialogAnchor'; import type { ComponentProps, ComponentType } from 'react'; -import type { Placement } from '@popperjs/core'; +import type { PopperLikePlacement } from './hooks'; type ButtonWithSubmenu = ComponentProps<'button'> & { children: React.ReactNode; - placement: Placement; + placement: PopperLikePlacement; Submenu: ComponentType; submenuContainerProps?: ComponentProps<'div'>; }; @@ -26,7 +26,7 @@ export const ButtonWithSubmenu = ({ const dialogId = useMemo(() => `submenu-${Math.random().toString(36).slice(2)}`, []); const dialog = useDialog({ id: dialogId }); const dialogIsOpen = useDialogIsOpen(dialogId); - const { attributes, setPopperElement, styles } = useDialogAnchor({ + const { setPopperElement, styles } = useDialogAnchor({ open: dialogIsOpen, placement, referenceElement: buttonRef.current, @@ -102,7 +102,6 @@ export const ButtonWithSubmenu = ({ {dialogIsOpen && (
{ const isBlurredDescendant = event.relatedTarget instanceof Node && @@ -125,7 +124,7 @@ export const ButtonWithSubmenu = ({ setPopperElement(element); setDialogContainer(element); }} - style={styles.popper} + style={styles} tabIndex={-1} {...submenuContainerProps} > diff --git a/src/components/Dialog/DialogAnchor.tsx b/src/components/Dialog/DialogAnchor.tsx index 5442256e61..9ee3621834 100644 --- a/src/components/Dialog/DialogAnchor.tsx +++ b/src/components/Dialog/DialogAnchor.tsx @@ -1,15 +1,15 @@ import clsx from 'clsx'; -import type { Placement } from '@popperjs/core'; import type { ComponentProps, PropsWithChildren } from 'react'; import React, { useEffect, useState } from 'react'; import { FocusScope } from '@react-aria/focus'; -import { usePopper } from 'react-popper'; import { DialogPortalEntry } from './DialogPortal'; import { useDialog, useDialogIsOpen } from './hooks'; +import { usePopoverPosition } from './hooks/usePopoverPosition'; +import type { PopperLikePlacement } from './hooks'; export interface DialogAnchorOptions { open: boolean; - placement: Placement; + placement: PopperLikePlacement; referenceElement: HTMLElement | null; allowFlip?: boolean; } @@ -21,25 +21,20 @@ export function useDialogAnchor({ referenceElement, }: DialogAnchorOptions) { const [popperElement, setPopperElement] = useState(null); - const { attributes, styles, update } = usePopper(referenceElement, popperElement, { - modifiers: [ - { - enabled: !!allowFlip, // Prevent flipping - name: 'flip', - }, - { - name: 'eventListeners', - options: { - // It's not safe to update popper position on resize and scroll, since popper's - // reference element might not be visible at the time. - resize: false, - scroll: false, - }, - }, - ], + const { refs, strategy, update, x, y } = usePopoverPosition({ + allowFlip, + freeze: true, placement, }); + useEffect(() => { + refs.setReference(referenceElement); + }, [referenceElement, refs]); + + useEffect(() => { + refs.setFloating(popperElement); + }, [popperElement, refs]); + useEffect(() => { if (open && popperElement) { // Since the popper's reference element might not be (and usually is not) visible @@ -54,9 +49,12 @@ export function useDialogAnchor({ } return { - attributes, setPopperElement, - styles, + styles: { + left: x ?? 0, + position: strategy, + top: y ?? 0, + } as React.CSSProperties, }; } @@ -80,7 +78,7 @@ export const DialogAnchor = ({ }: DialogAnchorProps) => { const dialog = useDialog({ id }); const open = useDialogIsOpen(id); - const { attributes, setPopperElement, styles } = useDialogAnchor({ + const { setPopperElement, styles } = useDialogAnchor({ allowFlip, open, placement, @@ -111,11 +109,10 @@ export const DialogAnchor = ({
{children} diff --git a/src/components/Dialog/hooks/index.ts b/src/components/Dialog/hooks/index.ts index 9d08c250c7..504515d252 100644 --- a/src/components/Dialog/hooks/index.ts +++ b/src/components/Dialog/hooks/index.ts @@ -1 +1,2 @@ export * from './useDialog'; +export type { PopperLikePlacement } from './usePopoverPosition'; diff --git a/src/components/Dialog/hooks/usePopoverPosition.ts b/src/components/Dialog/hooks/usePopoverPosition.ts new file mode 100644 index 0000000000..035a03440f --- /dev/null +++ b/src/components/Dialog/hooks/usePopoverPosition.ts @@ -0,0 +1,105 @@ +import { + autoPlacement, + autoUpdate, + flip as flipMw, + offset as offsetMw, + type Placement, + shift as shiftMw, + size as sizeMw, + useFloating, +} from '@floating-ui/react'; +import type { AutoPlacementOptions } from '@floating-ui/core'; + +export type PopperLikePlacement = Placement | 'auto' | 'auto-start' | 'auto-end'; + +function autoMiddlewareFor(p: PopperLikePlacement) { + if (!String(p).startsWith('auto')) return null; + const alignment: AutoPlacementOptions['alignment'] = + p === 'auto-start' ? 'start' : p === 'auto-end' ? 'end' : undefined; + return autoPlacement({ alignment }); +} + +type OffsetOpt = + | number + | { mainAxis?: number; crossAxis?: number; alignmentAxis?: number } + | [crossAxis: number, mainAxis: number]; // keep your tuple compat + +function toOffsetMw(opt?: OffsetOpt) { + if (opt == null) return null; + if (Array.isArray(opt)) { + const [crossAxis, mainAxis] = opt; + return offsetMw({ crossAxis, mainAxis }); + } + if (typeof opt === 'number') return offsetMw(opt); + return offsetMw(opt); +} + +export type UsePopoverParams = { + placement?: PopperLikePlacement; + /** Add flip() when placement is not 'auto*' */ + allowFlip?: boolean; + /** Keep in viewport; default true to match common popper setups */ + allowShift?: boolean; + /** The floating UI is fitted to the available space (by constraining its max size) instead of letting it overflow; default false */ + fitAvailableSpace?: boolean; + /** Offset (number, object, or [crossAxis, mainAxis] tuple) */ + offset?: OffsetOpt; + /** + * Freeze behavior like Popper's eventListeners: { scroll:false, resize:false }. + * If true → no autoUpdate (you can call `update()` manually). + */ + freeze?: boolean; + /** + * Fine-grained control of autoUpdate triggers (only if freeze=false). + * Defaults match Popper's "disabled" example when all set to false. + */ + autoUpdateOptions?: Partial[3]>; +}; + +export function usePopoverPosition({ + allowFlip = true, + allowShift = true, + autoUpdateOptions, + fitAvailableSpace = false, + freeze = false, + offset, + placement = 'bottom-start', +}: UsePopoverParams) { + const autoMw = autoMiddlewareFor(placement); + const offsetMiddleware = toOffsetMw(offset); + + const middleware = [ + // offset first (mirrors common Popper setups) + ...(offsetMiddleware ? [offsetMiddleware] : []), + + // choose between autoPlacement (Popper's "auto*") OR flip() + ...(autoMw ? [autoMw] : allowFlip ? [flipMw()] : []), + + // viewport collision adjustments + ...(allowShift ? [shiftMw({ padding: 8 })] : []), + + // optional size constraining + // eslint-disable-next-line @typescript-eslint/no-empty-function + ...(fitAvailableSpace ? [sizeMw({ apply: () => {} })] : []), + ]; + + // if placement is 'auto*', seed with any static placement; autoPlacement will pick the final one + const seedPlacement: Placement = String(placement).startsWith('auto') + ? 'bottom' + : (placement as Placement); + + return useFloating({ + middleware, + placement: seedPlacement, + whileElementsMounted: freeze + ? undefined + : (reference, floating, update) => + autoUpdate(reference, floating, update, { + ancestorResize: true, + ancestorScroll: true, + animationFrame: false, + elementResize: true, + ...autoUpdateOptions, + }), + }); +} diff --git a/src/components/Form/Dropdown.tsx b/src/components/Form/Dropdown.tsx index 749549103c..4edfd05426 100644 --- a/src/components/Form/Dropdown.tsx +++ b/src/components/Form/Dropdown.tsx @@ -4,7 +4,7 @@ import { useEffect } from 'react'; import React, { useState } from 'react'; import { DialogAnchor, useDialog, useDialogIsOpen } from '../Dialog'; import { DialogManagerProvider, useTranslationContext } from '../../context'; -import type { Placement } from '@popperjs/core'; +import type { PopperLikePlacement } from '../Dialog'; type DropdownContextValue = { close(): void; @@ -28,7 +28,7 @@ export const useDropdownContext = () => React.useContext(DropdownContext); export type DropdownProps = PropsWithChildren<{ className?: string; openButtonProps?: React.HTMLAttributes; - placement?: Placement; + placement?: PopperLikePlacement; }>; export const Dropdown = (props: DropdownProps) => { diff --git a/src/components/MessageActions/hooks/index.ts b/src/components/MessageActions/hooks/index.ts deleted file mode 100644 index 03da993e73..0000000000 --- a/src/components/MessageActions/hooks/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useMessageActionsBoxPopper'; diff --git a/src/components/MessageActions/hooks/useMessageActionsBoxPopper.ts b/src/components/MessageActions/hooks/useMessageActionsBoxPopper.ts deleted file mode 100644 index 755d14fb44..0000000000 --- a/src/components/MessageActions/hooks/useMessageActionsBoxPopper.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { Placement } from '@popperjs/core'; -import { useEffect, useRef } from 'react'; -import { usePopper } from 'react-popper'; - -export interface MessageActionsBoxPopperOptions { - open: boolean; - placement: Placement; - referenceElement: HTMLElement | null; -} - -export function useMessageActionsBoxPopper({ - open, - placement, - referenceElement, -}: MessageActionsBoxPopperOptions) { - const popperElementRef = useRef(null); - const { attributes, styles, update } = usePopper( - referenceElement, - popperElementRef.current, - { - modifiers: [ - { - name: 'eventListeners', - options: { - // It's not safe to update popper position on resize and scroll, since popper's - // reference element might not be visible at the time. - resize: false, - scroll: false, - }, - }, - ], - placement, - }, - ); - - useEffect(() => { - if (open) { - // Since the popper's reference element might not be (and usually is not) visible - // all the time, it's safer to force popper update before showing it. - update?.(); - } - }, [open, update]); - - return { - attributes, - popperElementRef, - styles, - }; -} diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx index da5ac0bba5..79860a585a 100644 --- a/src/components/Tooltip/Tooltip.tsx +++ b/src/components/Tooltip/Tooltip.tsx @@ -1,7 +1,7 @@ import type { ComponentProps } from 'react'; -import React, { useState } from 'react'; -import type { PopperProps } from 'react-popper'; -import { usePopper } from 'react-popper'; +import React, { useEffect, useState } from 'react'; +import type { PopperLikePlacement } from '../Dialog'; +import { usePopoverPosition } from '../Dialog/hooks/usePopoverPosition'; export const Tooltip = ({ children, ...rest }: ComponentProps<'div'>) => (
@@ -15,7 +15,7 @@ export type PopperTooltipProps = React.PropsWithChildren< /** Popper's modifier (offset) property - [xAxis offset, yAxis offset], default [0, 10] */ offset?: [number, number]; /** Popper's placement property defining default position of the tooltip, default 'top' */ - placement?: PopperProps['placement']; + placement?: PopperLikePlacement; /** Tells component whether to render its contents */ visible?: boolean; }>; @@ -28,26 +28,33 @@ export const PopperTooltip = ({ visible = false, }: PopperTooltipProps) => { const [popperElement, setPopperElement] = useState(null); - const { attributes, styles } = usePopper(referenceElement, popperElement, { - modifiers: [ - { - name: 'offset', - options: { - offset, - }, - }, - ], + const { + placement: resolvedPlacement, + refs, + strategy, + x, + y, + } = usePopoverPosition({ + offset, placement, }); + useEffect(() => { + refs.setReference(referenceElement); + }, [referenceElement, refs]); + + useEffect(() => { + refs.setFloating(popperElement); + }, [popperElement, refs]); + if (!visible) return null; return (
{children}
diff --git a/src/plugins/Emojis/EmojiPicker.tsx b/src/plugins/Emojis/EmojiPicker.tsx index e0da708005..34139d21d0 100644 --- a/src/plugins/Emojis/EmojiPicker.tsx +++ b/src/plugins/Emojis/EmojiPicker.tsx @@ -1,12 +1,11 @@ import React, { useEffect, useState } from 'react'; -import { usePopper } from 'react-popper'; import Picker from '@emoji-mart/react'; -import type { Options } from '@popperjs/core'; - import { EmojiPickerIcon } from './icons'; import { useMessageInputContext, useTranslationContext } from '../../context'; import { useMessageComposer } from '../../components'; +import type { PopperLikePlacement } from '../../components'; +import { usePopoverPosition } from '../../components/Dialog/hooks/usePopoverPosition'; const isShadowRoot = (node: Node): node is ShadowRoot => !!(node as ShadowRoot).host; @@ -22,10 +21,13 @@ export type EmojiPickerProps = { */ pickerProps?: Partial<{ theme: 'auto' | 'light' | 'dark' } & Record>; /** - * [React Popper options](https://popper.js.org/docs/v2/constructors/#options) to be - * passed down to the [react-popper `usePopper`](https://popper.js.org/react-popper/v2/hook/) hook + * Floating UI placement (default: 'top-end') for the picker popover + */ + placement?: PopperLikePlacement; + /** + * Deprecated: Popper options, use `placement` instead. */ - popperOptions?: Partial; + popperOptions?: Partial<{ placement: PopperLikePlacement }>; }; const classNames: EmojiPickerProps = { @@ -43,11 +45,17 @@ export const EmojiPicker = (props: EmojiPickerProps) => { null, ); const [popperElement, setPopperElement] = useState(null); - const { attributes, styles } = usePopper(referenceElement, popperElement, { - placement: 'top-end', - ...props.popperOptions, + const { refs, strategy, x, y } = usePopoverPosition({ + placement: props.placement ?? 'top-end', }); + useEffect(() => { + refs.setReference(referenceElement); + }, [referenceElement, refs]); + useEffect(() => { + refs.setFloating(popperElement); + }, [popperElement, refs]); + const { buttonClassName, pickerContainerClassName, wrapperClassName } = classNames; const { ButtonIconComponent = EmojiPickerIcon } = props; @@ -79,9 +87,8 @@ export const EmojiPicker = (props: EmojiPickerProps) => { {displayPicker && (
(await import('@emoji-mart/data')).default} diff --git a/yarn.lock b/yarn.lock index 710da01126..56747be99b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1600,6 +1600,42 @@ "@eslint/core" "^0.10.0" levn "^0.4.1" +"@floating-ui/core@^1.7.3": + version "1.7.3" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.3.tgz#462d722f001e23e46d86fd2bd0d21b7693ccb8b7" + integrity sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w== + dependencies: + "@floating-ui/utils" "^0.2.10" + +"@floating-ui/dom@^1.7.4": + version "1.7.4" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.4.tgz#ee667549998745c9c3e3e84683b909c31d6c9a77" + integrity sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA== + dependencies: + "@floating-ui/core" "^1.7.3" + "@floating-ui/utils" "^0.2.10" + +"@floating-ui/react-dom@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.6.tgz#189f681043c1400561f62972f461b93f01bf2231" + integrity sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw== + dependencies: + "@floating-ui/dom" "^1.7.4" + +"@floating-ui/react@^0.27.2": + version "0.27.16" + resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.27.16.tgz#6e485b5270b7a3296fdc4d0faf2ac9abf955a2f7" + integrity sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g== + dependencies: + "@floating-ui/react-dom" "^2.1.6" + "@floating-ui/utils" "^0.2.10" + tabbable "^6.0.0" + +"@floating-ui/utils@^0.2.10": + version "0.2.10" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c" + integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ== + "@gulpjs/to-absolute-glob@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz#1fc2460d3953e1d9b9f2dfdb4bcc99da4710c021" @@ -2301,11 +2337,6 @@ "@pnpm/network.ca-file" "^1.0.1" config-chain "^1.1.11" -"@popperjs/core@^2.11.5": - version "2.11.5" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64" - integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw== - "@react-aria/focus@^3": version "3.16.2" resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.16.2.tgz#2285bc19e091233b4d52399c506ac8fa60345b44" @@ -10961,14 +10992,6 @@ react-player@2.10.1: prop-types "^15.7.2" react-fast-compare "^3.0.1" -react-popper@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.3.0.tgz#17891c620e1320dce318bad9fede46a5f71c70ba" - integrity sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q== - dependencies: - react-fast-compare "^3.0.1" - warning "^4.0.2" - react-refresh@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.12.0.tgz#28ac0a2c30ef2bb3433d5fd0621e69a6d774c3a4" @@ -12376,6 +12399,11 @@ symlink-or-copy@^1.1.8, symlink-or-copy@^1.2.0, symlink-or-copy@^1.3.1: resolved "https://registry.yarnpkg.com/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz#9506dd64d8e98fa21dcbf4018d1eab23e77f71fe" integrity sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA== +tabbable@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.2.0.tgz#732fb62bc0175cfcec257330be187dcfba1f3b97" + integrity sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew== + tar@^6.1.11: version "6.1.13" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" @@ -13267,13 +13295,6 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" -warning@^4.0.2: - version "4.0.3" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" - integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== - dependencies: - loose-envify "^1.0.0" - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" From 8a8bd38302bafa051510bbf3c977df90f569aa18 Mon Sep 17 00:00:00 2001 From: martincupela Date: Fri, 10 Oct 2025 13:41:18 +0200 Subject: [PATCH 2/2] test: account for non-existent ResizeObserver --- src/components/Dialog/hooks/usePopoverPosition.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Dialog/hooks/usePopoverPosition.ts b/src/components/Dialog/hooks/usePopoverPosition.ts index 035a03440f..4e274a44c9 100644 --- a/src/components/Dialog/hooks/usePopoverPosition.ts +++ b/src/components/Dialog/hooks/usePopoverPosition.ts @@ -10,6 +10,8 @@ import { } from '@floating-ui/react'; import type { AutoPlacementOptions } from '@floating-ui/core'; +const hasResizeObserver = typeof window !== 'undefined' && 'ResizeObserver' in window; + export type PopperLikePlacement = Placement | 'auto' | 'auto-start' | 'auto-end'; function autoMiddlewareFor(p: PopperLikePlacement) { @@ -98,7 +100,7 @@ export function usePopoverPosition({ ancestorResize: true, ancestorScroll: true, animationFrame: false, - elementResize: true, + elementResize: hasResizeObserver, ...autoUpdateOptions, }), });