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"