diff --git a/apps/client/src/components/ui/emoji-picker.tsx b/apps/client/src/components/ui/emoji-picker.tsx index 804d1b0f4..8f87735d6 100644 --- a/apps/client/src/components/ui/emoji-picker.tsx +++ b/apps/client/src/components/ui/emoji-picker.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useState } from "react"; +import React, { ReactNode, useEffect, useState } from "react"; import { ActionIcon, Popover, @@ -7,9 +7,24 @@ import { } from "@mantine/core"; import { useClickOutside, useDisclosure, useWindowEvent } from "@mantine/hooks"; import { Suspense } from "react"; -const Picker = React.lazy(() => import("@emoji-mart/react")); import { useTranslation } from "react-i18next"; +// Load the picker module AND the emoji data in parallel inside the lazy +// resolution, then bind the data into the component. React.lazy only finishes +// suspending once both are in memory, so the Suspense boundary hides the +// Remove button until the Picker can render with real content. +const Picker = React.lazy(async () => { + const [pickerModule, dataModule] = await Promise.all([ + import("@slidoapp/emoji-mart-react"), + import("@slidoapp/emoji-mart-data"), + ]); + const PickerComp = pickerModule.default; + const data = dataModule.default; + return { + default: (props: any) => , + }; +}); + export interface EmojiPickerInterface { onEmojiSelect: (emoji: any) => void; icon: ReactNode; @@ -50,6 +65,38 @@ function EmojiPicker({ } }); + // emoji-mart's built-in autoFocus calls .focus() without preventScroll, which + // makes the browser scroll every scrollable ancestor of the search input to + // bring it on screen — including the page editor's scroll container, so the + // page jumps to the top whenever the picker is opened from a scrolled-down + // position. The search input lives inside the custom + // element's shadow root, so we poll for it after the dropdown mounts and + // focus it ourselves with preventScroll. + useEffect(() => { + if (!opened || !dropdown) return; + let cancelled = false; + let rafId = 0; + const tryFocus = (attempts: number) => { + if (cancelled) return; + const pickerEl = dropdown.querySelector("em-emoji-picker"); + const input = pickerEl?.shadowRoot?.querySelector( + 'input[type="search"]', + ); + if (input) { + input.focus({ preventScroll: true }); + return; + } + if (attempts < 60) { + rafId = requestAnimationFrame(() => tryFocus(attempts + 1)); + } + }; + rafId = requestAnimationFrame(() => tryFocus(0)); + return () => { + cancelled = true; + cancelAnimationFrame(rafId); + }; + }, [opened, dropdown]); + const handleEmojiSelect = (emoji) => { onEmojiSelect(emoji); handlers.close(); @@ -85,7 +132,6 @@ function EmojiPicker({ (await import("@emoji-mart/data")).default} onEmojiSelect={handleEmojiSelect} perLine={8} skinTonePosition="search"