🐛(frontend) fix emojipicker closing

In the tree view, if the emoji picker is opened
near the bottom of the viewport, it would
trigger an overflow that rerendered the treeview
and closed the picker immediately.
The root problem is the treeview that rerender
because of not stable props.
To fix this, we change 2 things:
- we use "fixed" position for the emoji picker
  so it won't affect the document flow
- we adjust the position calculation logic, if
  the picker does not have enough space below,
  we position it above the icon instead.
This commit is contained in:
Anthony LC
2026-01-16 14:37:12 +01:00
parent 21f5feab3e
commit 75f71368f4
4 changed files with 32 additions and 61 deletions

View File

@@ -14,6 +14,7 @@ and this project adheres to
- ✅(e2e) fix e2e test for other browsers #1799
- 🐛(frontend) add fallback for unsupported Blocknote languages #1810
- 🐛(frontend) fix emojipicker closing in tree #1808
### Changed

View File

@@ -4,7 +4,6 @@ import {
createDoc,
expectLoginPage,
keyCloakSignIn,
randomName,
updateDocTitle,
verifyDocName,
} from './utils-common';
@@ -20,50 +19,6 @@ test.describe('Doc Tree', () => {
await page.goto('/');
});
test('create new sub pages', async ({ page, browserName }) => {
const [titleParent] = await createDoc(
page,
'doc-tree-content',
browserName,
1,
);
await verifyDocName(page, titleParent);
const addButton = page.getByTestId('new-doc-button');
const docTree = page.getByTestId('doc-tree');
await expect(addButton).toBeVisible();
// Wait for and intercept the POST request to create a new page
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes('/documents/') &&
response.url().includes('/children/') &&
response.request().method() === 'POST',
);
await clickOnAddRootSubPage(page);
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
const subPageJson = await response.json();
await expect(docTree).toBeVisible();
const subPageItem = docTree
.getByTestId(`doc-sub-page-item-${subPageJson.id}`)
.first();
await expect(subPageItem).toBeVisible();
await subPageItem.click();
await verifyDocName(page, '');
const input = page.getByRole('textbox', { name: 'Document title' });
await input.click();
const [randomDocName] = randomName('doc-tree-test', browserName, 1);
await input.fill(randomDocName);
await input.press('Enter');
await expect(subPageItem.getByText(randomDocName)).toBeVisible();
await page.reload();
await expect(subPageItem.getByText(randomDocName)).toBeVisible();
});
test('check the reorder of sub pages', async ({ page, browserName }) => {
await createDoc(page, 'doc-tree-content', browserName, 1);
const addButton = page.getByTestId('new-doc-button');

View File

@@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next';
import { Box } from '@/components';
export const PICKER_HEIGHT = 500;
interface EmojiPickerProps {
emojiData: EmojiMartData;
onClickOutside: () => void;
@@ -27,12 +29,7 @@ export const EmojiPicker = ({
};
const pickerContent = (
<Box
$position="absolute"
$zIndex={1000}
$margin="2rem 0 0 0"
onKeyDownCapture={handleKeyDown}
>
<Box $position="absolute" $zIndex={1000} onKeyDownCapture={handleKeyDown}>
<Picker
data={emojiData}
locale={i18n.resolvedLanguage}

View File

@@ -1,11 +1,14 @@
import { MouseEvent, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import {
Box,
BoxButton,
BoxButtonType,
EmojiPicker,
PICKER_HEIGHT,
Text,
TextType,
emojidata,
@@ -66,9 +69,24 @@ export const DocIcon = ({
if (!openEmojiPicker && iconRef.current) {
const rect = iconRef.current.getBoundingClientRect();
const pickerHeight = PICKER_HEIGHT;
const spaceBelow = window.innerHeight - rect.bottom;
const spaceAbove = rect.top;
// Position picker above if not enough space below and enough space above
const shouldPositionAbove =
spaceBelow < pickerHeight && spaceAbove >= pickerHeight;
// Offset to align the picker properly
const ROW_OFFSET_TOP = 55;
const ROW_OFFSET_BOTTOM = 10;
setPickerPosition({
top: rect.bottom + window.scrollY + 8,
left: rect.left + window.scrollX,
top: shouldPositionAbove
? rect.top - pickerHeight + ROW_OFFSET_TOP
: rect.bottom + ROW_OFFSET_BOTTOM,
left: rect.left,
});
}
@@ -120,13 +138,13 @@ export const DocIcon = ({
</BoxButton>
{openEmojiPicker &&
createPortal(
<div
style={{
position: 'absolute',
top: pickerPosition.top,
left: pickerPosition.left,
zIndex: 1000,
}}
<Box
$position="fixed"
$css={css`
top: ${pickerPosition.top}px;
left: ${pickerPosition.left}px;
z-index: 1000;
`}
>
<EmojiPicker
emojiData={emojidata}
@@ -134,7 +152,7 @@ export const DocIcon = ({
onClickOutside={handleClickOutside}
withOverlay={true}
/>
</div>,
</Box>,
document.body,
)}
</>