🐛(frontend) fix interlinking modal clipping

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.
This commit is contained in:
Anthony LC
2026-04-14 18:19:27 +02:00
parent 203b3edcae
commit b3dd8f2e39
2 changed files with 238 additions and 199 deletions

View File

@@ -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

View File

@@ -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<Doc>();
const modalRef = useRef<HTMLDivElement>(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,9 +153,7 @@ 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(
const selectedItem = modalRef.current?.querySelector(
'[cmdk-item][data-selected="true"]',
) as HTMLElement;
@@ -158,6 +164,13 @@ export const SearchPage = ({
return (
<Box as="span" $position="relative">
<Popover
position="bottom"
opened={popoverOpened}
withinPortal={true}
hideDetached={false}
>
<Popover.Target>
<Box
as="span"
className="inline-content"
@@ -170,10 +183,18 @@ export const SearchPage = ({
tabIndex={-1} // Ensure the span is focusable
>
{' '}
<Box as="span" aria-hidden="true">
{trigger}
</Box>
<Box
as="input"
name="doc-search-input"
role="combobox"
aria-label={t('Search for a document')}
aria-expanded={popoverOpened}
aria-haspopup="listbox"
aria-autocomplete="list"
aria-controls={dropdownId}
$padding={{ left: '3px' }}
$css={inputStyle}
ref={inputRef}
@@ -186,13 +207,25 @@ export const SearchPage = ({
autoComplete="off"
/>
</Box>
</Popover.Target>
<Popover.Dropdown>
<Box
ref={modalRef}
id={dropdownId}
role="listbox"
aria-label={t('Search results')}
$minWidth={isDesktop ? '330px' : '220px'}
$width="fit-content"
$position="absolute"
$zIndex="10"
$css={css`
top: 28px;
z-index: 1000;
position: relative;
.mantine-Popover-dropdown[data-position='bottom'] & {
top: -10px;
}
.mantine-Popover-dropdown[data-position='top'] & {
top: 10px;
}
& .quick-search-container [cmdk-root] {
border-radius: inherit;
@@ -272,7 +305,10 @@ export const SearchPage = ({
$direction="row"
$gap="0.2rem"
$align="center"
$padding={{ vertical: '0.5rem', horizontal: '0.2rem' }}
$padding={{
vertical: '0.5rem',
horizontal: '0.2rem',
}}
$width="100%"
>
<Box
@@ -356,6 +392,8 @@ export const SearchPage = ({
</Card>
</QuickSearch>
</Box>
</Popover.Dropdown>
</Popover>
</Box>
);
};