mirror of
https://github.com/suitenumerique/docs.git
synced 2026-04-25 17:15:01 +02:00
🐛(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:
@@ -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
|
||||
|
||||
|
||||
@@ -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,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 (
|
||||
<Box as="span" $position="relative">
|
||||
<Box
|
||||
as="span"
|
||||
className="inline-content"
|
||||
$background={colorsTokens['gray-100']}
|
||||
$color="var(--c--globals--colors--gray-700)"
|
||||
$direction="row"
|
||||
$radius="3px"
|
||||
$padding="1px"
|
||||
$display="inline-flex"
|
||||
tabIndex={-1} // Ensure the span is focusable
|
||||
<Popover
|
||||
position="bottom"
|
||||
opened={popoverOpened}
|
||||
withinPortal={true}
|
||||
hideDetached={false}
|
||||
>
|
||||
{' '}
|
||||
{trigger}
|
||||
<Box
|
||||
as="input"
|
||||
name="doc-search-input"
|
||||
$padding={{ left: '3px' }}
|
||||
$css={inputStyle}
|
||||
ref={inputRef}
|
||||
$display="inline-flex"
|
||||
onInput={(e) => {
|
||||
const value = (e.target as HTMLInputElement).value;
|
||||
setSearch(value);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
$minWidth={isDesktop ? '330px' : '220px'}
|
||||
$width="fit-content"
|
||||
$position="absolute"
|
||||
$css={css`
|
||||
top: 28px;
|
||||
z-index: 1000;
|
||||
|
||||
& .quick-search-container [cmdk-root] {
|
||||
border-radius: inherit;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<QuickSearch showInput={false}>
|
||||
<Card
|
||||
<Popover.Target>
|
||||
<Box
|
||||
as="span"
|
||||
className="inline-content"
|
||||
$background={colorsTokens['gray-100']}
|
||||
$color="var(--c--globals--colors--gray-700)"
|
||||
$direction="row"
|
||||
$radius="3px"
|
||||
$padding="1px"
|
||||
$display="inline-flex"
|
||||
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}
|
||||
$display="inline-flex"
|
||||
onInput={(e) => {
|
||||
const value = (e.target as HTMLInputElement).value;
|
||||
setSearch(value);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
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"
|
||||
$zIndex="10"
|
||||
$css={css`
|
||||
box-shadow: 0 0 3px 0px var(--c--globals--colors--gray-200);
|
||||
& > 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' }}
|
||||
>
|
||||
<DocSearchContent
|
||||
groupName={t('Select a document')}
|
||||
search={search}
|
||||
target={DocSearchTarget.CURRENT}
|
||||
parentPath={treeContext?.root?.path}
|
||||
onSelect={(doc) => {
|
||||
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 (
|
||||
<QuickSearchItemContent
|
||||
left={
|
||||
<Box
|
||||
$direction="row"
|
||||
$gap="0.2rem"
|
||||
$align="center"
|
||||
$padding={{ vertical: '0.5rem', horizontal: '0.2rem' }}
|
||||
$width="100%"
|
||||
>
|
||||
<Box
|
||||
$css={css`
|
||||
width: 24px;
|
||||
flex-shrink: 0;
|
||||
`}
|
||||
>
|
||||
{emoji ? (
|
||||
<Text $size="18px">{emoji}</Text>
|
||||
) : (
|
||||
<FoundPageIcon
|
||||
width="100%"
|
||||
style={{ maxHeight: '24px' }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Text
|
||||
$size="sm"
|
||||
$color="var(--c--globals--colors--gray-1000)"
|
||||
spellCheck="false"
|
||||
>
|
||||
{titleWithoutEmoji}
|
||||
</Text>
|
||||
</Box>
|
||||
<QuickSearch showInput={false}>
|
||||
<Card
|
||||
$css={css`
|
||||
box-shadow: 0 0 3px 0px var(--c--globals--colors--gray-200);
|
||||
& > div {
|
||||
margin-top: var(--c--globals--spacings--0);
|
||||
& [cmdk-group-heading] {
|
||||
padding: 0.4rem;
|
||||
margin: 0;
|
||||
}
|
||||
right={
|
||||
<Icon iconName="keyboard_return" spellCheck="false" />
|
||||
|
||||
& [cmdk-group-items] .ml-b {
|
||||
margin-left: 0rem;
|
||||
padding: 0.5rem;
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<QuickSearchGroup
|
||||
group={{
|
||||
groupName: '',
|
||||
elements: [],
|
||||
endActions: [
|
||||
{
|
||||
onSelect: createChildDoc,
|
||||
content: (
|
||||
<Box
|
||||
$css={css`
|
||||
border-top: 1px solid
|
||||
var(--c--globals--colors--gray-200);
|
||||
`}
|
||||
$width="100%"
|
||||
>
|
||||
<Box
|
||||
$direction="row"
|
||||
$gap="0.4rem"
|
||||
$align="center"
|
||||
$padding={{
|
||||
vertical: '0.5rem',
|
||||
horizontal: '0.3rem',
|
||||
}}
|
||||
$css={css`
|
||||
&:hover {
|
||||
background-color: var(
|
||||
--c--globals--colors--gray-100
|
||||
);
|
||||
}
|
||||
`}
|
||||
>
|
||||
<AddPageIcon />
|
||||
<Text
|
||||
$size="sm"
|
||||
$color="var(--c--globals--colors--gray-1000)"
|
||||
contentEditable={false}
|
||||
|
||||
& [cmdk-item] {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
& .--docs--doc-search-item > div {
|
||||
gap: 0.8rem;
|
||||
}
|
||||
}
|
||||
`}
|
||||
$margin={{ top: '0.5rem' }}
|
||||
>
|
||||
<DocSearchContent
|
||||
groupName={t('Select a document')}
|
||||
search={search}
|
||||
target={DocSearchTarget.CURRENT}
|
||||
parentPath={treeContext?.root?.path}
|
||||
onSelect={(doc) => {
|
||||
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 (
|
||||
<QuickSearchItemContent
|
||||
left={
|
||||
<Box
|
||||
$direction="row"
|
||||
$gap="0.2rem"
|
||||
$align="center"
|
||||
$padding={{
|
||||
vertical: '0.5rem',
|
||||
horizontal: '0.2rem',
|
||||
}}
|
||||
$width="100%"
|
||||
>
|
||||
{t('New sub-doc')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</QuickSearch>
|
||||
</Box>
|
||||
<Box
|
||||
$css={css`
|
||||
width: 24px;
|
||||
flex-shrink: 0;
|
||||
`}
|
||||
>
|
||||
{emoji ? (
|
||||
<Text $size="18px">{emoji}</Text>
|
||||
) : (
|
||||
<FoundPageIcon
|
||||
width="100%"
|
||||
style={{ maxHeight: '24px' }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Text
|
||||
$size="sm"
|
||||
$color="var(--c--globals--colors--gray-1000)"
|
||||
spellCheck="false"
|
||||
>
|
||||
{titleWithoutEmoji}
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
right={
|
||||
<Icon iconName="keyboard_return" spellCheck="false" />
|
||||
}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<QuickSearchGroup
|
||||
group={{
|
||||
groupName: '',
|
||||
elements: [],
|
||||
endActions: [
|
||||
{
|
||||
onSelect: createChildDoc,
|
||||
content: (
|
||||
<Box
|
||||
$css={css`
|
||||
border-top: 1px solid
|
||||
var(--c--globals--colors--gray-200);
|
||||
`}
|
||||
$width="100%"
|
||||
>
|
||||
<Box
|
||||
$direction="row"
|
||||
$gap="0.4rem"
|
||||
$align="center"
|
||||
$padding={{
|
||||
vertical: '0.5rem',
|
||||
horizontal: '0.3rem',
|
||||
}}
|
||||
$css={css`
|
||||
&:hover {
|
||||
background-color: var(
|
||||
--c--globals--colors--gray-100
|
||||
);
|
||||
}
|
||||
`}
|
||||
>
|
||||
<AddPageIcon />
|
||||
<Text
|
||||
$size="sm"
|
||||
$color="var(--c--globals--colors--gray-1000)"
|
||||
contentEditable={false}
|
||||
>
|
||||
{t('New sub-doc')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</QuickSearch>
|
||||
</Box>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user