From b3dd8f2e39192bd552d49d4ca5d5271a6a4424f3 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Tue, 14 Apr 2026 18:19:27 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(frontend)=20fix=20interlinking=20m?= =?UTF-8?q?odal=20clipping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Depend the parent block, the modal search may be clipped by the parent block. We now use the portal to render the modal search, which will not be affected by the parent block's clipping. --- CHANGELOG.md | 1 + .../Interlinking/SearchPage.tsx | 436 ++++++++++-------- 2 files changed, 238 insertions(+), 199 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9f3f0f33..dd95d760b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to - 🚸(frontend) redirect on current url tab after 401 #2197 - 🐛(frontend) abort check media status unmount #2194 - ✨(backend) order pinned documents by last updated at #2028 +- 🐛(frontend) fix interlinking modal clipping #2213 - 🛂(frontend) fix cannot manage member on small screen #2226 - 🐛(backend) load jwks url when OIDC_RS_PRIVATE_KEY_STR is set diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/SearchPage.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/SearchPage.tsx index 06de7ee26..655d3bfa8 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/SearchPage.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/custom-inline-content/Interlinking/SearchPage.tsx @@ -4,8 +4,9 @@ import { } from '@blocknote/core'; import { useBlockNoteEditor } from '@blocknote/react'; import { useTreeContext } from '@gouvfr-lasuite/ui-kit'; +import { Popover } from '@mantine/core'; import type { KeyboardEvent } from 'react'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useId, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; @@ -90,18 +91,27 @@ export const SearchPage = ({ const { untitledDocument } = useTrans(); const isEditable = editor.isEditable; const treeContext = useTreeContext(); + const modalRef = useRef(null); + const dropdownId = useId(); + const [popoverOpened, setPopoverOpened] = useState(false); + /** * createReactInlineContentSpec add automatically the focus after * the inline content, so we need to set the focus on the input * after the component is mounted. + * We also defer opening the popover to after mount so that + * floating-ui attaches scroll/resize listeners correctly. */ useEffect(() => { - setTimeout(() => { + const timeoutId = setTimeout(() => { if (inputRef.current) { inputRef.current.focus(); } + setPopoverOpened(true); }, 100); - }, [inputRef]); + + return () => clearTimeout(timeoutId); + }, []); const closeSearch = (insertContent: string) => { if (!isEditable) { @@ -131,9 +141,7 @@ export const SearchPage = ({ closeSearch(''); } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { // Allow arrow keys to be handled by the command menu for navigation - const commandList = e.currentTarget - .closest('.inline-content') - ?.nextElementSibling?.querySelector('[cmdk-list]'); + const commandList = modalRef.current?.querySelector('[cmdk-list]'); // Create a synthetic keyboard event for the command menu const syntheticEvent = new KeyboardEvent('keydown', { @@ -145,11 +153,9 @@ export const SearchPage = ({ e.preventDefault(); } else if (e.key === 'Enter') { // Handle Enter key to select the currently highlighted item - const selectedItem = e.currentTarget - .closest('.inline-content') - ?.nextElementSibling?.querySelector( - '[cmdk-item][data-selected="true"]', - ) as HTMLElement; + const selectedItem = modalRef.current?.querySelector( + '[cmdk-item][data-selected="true"]', + ) as HTMLElement; selectedItem?.click(); e.preventDefault(); @@ -158,204 +164,236 @@ export const SearchPage = ({ return ( - - {' '} - {trigger} - { - const value = (e.target as HTMLInputElement).value; - setSearch(value); - }} - onKeyDown={handleKeyDown} - autoComplete="off" - /> - - - - + + {' '} + + { + const value = (e.target as HTMLInputElement).value; + setSearch(value); + }} + onKeyDown={handleKeyDown} + autoComplete="off" + /> + + + + div { - margin-top: var(--c--globals--spacings--0); - & [cmdk-group-heading] { - padding: 0.4rem; - margin: 0; - } + position: relative; - & [cmdk-group-items] .ml-b { - margin-left: 0rem; - padding: 0.5rem; - font-size: 14px; - display: block; - } + .mantine-Popover-dropdown[data-position='bottom'] & { + top: -10px; + } + .mantine-Popover-dropdown[data-position='top'] & { + top: 10px; + } - & [cmdk-item] { - border-radius: 0; - } - - & .--docs--doc-search-item > div { - gap: 0.8rem; - } + & .quick-search-container [cmdk-root] { + border-radius: inherit; } `} - $margin={{ top: '0.5rem' }} > - { - if (!isEditable) { - return; - } - - updateInlineContent({ - type: 'interlinkingSearchInline', - props: { - disabled: true, - trigger, - }, - }); - - contentRef(null); - - editor.insertInlineContent([ - { - type: 'interlinkingLinkInline', - props: { - docId: doc.id, - title: doc.title || untitledDocument, - }, - }, - ]); - - editor.focus(); - }} - renderSearchElement={(doc) => { - const { emoji, titleWithoutEmoji } = getEmojiAndTitle( - doc.title || untitledDocument, - ); - - return ( - - - {emoji ? ( - {emoji} - ) : ( - - )} - - - - {titleWithoutEmoji} - - + + div { + margin-top: var(--c--globals--spacings--0); + & [cmdk-group-heading] { + padding: 0.4rem; + margin: 0; } - right={ - + + & [cmdk-group-items] .ml-b { + margin-left: 0rem; + padding: 0.5rem; + font-size: 14px; + display: block; } - /> - ); - }} - /> - - - - div { + gap: 0.8rem; + } + } + `} + $margin={{ top: '0.5rem' }} + > + { + if (!isEditable) { + return; + } + + updateInlineContent({ + type: 'interlinkingSearchInline', + props: { + disabled: true, + trigger, + }, + }); + + contentRef(null); + + editor.insertInlineContent([ + { + type: 'interlinkingLinkInline', + props: { + docId: doc.id, + title: doc.title || untitledDocument, + }, + }, + ]); + + editor.focus(); + }} + renderSearchElement={(doc) => { + const { emoji, titleWithoutEmoji } = getEmojiAndTitle( + doc.title || untitledDocument, + ); + + return ( + - {t('New sub-doc')} - - - - ), - }, - ], - }} - /> - - - + + {emoji ? ( + {emoji} + ) : ( + + )} + + + + {titleWithoutEmoji} + + + } + right={ + + } + /> + ); + }} + /> + + + + + {t('New sub-doc')} + + + + ), + }, + ], + }} + /> + + + + + ); };