fix(electron): optimize find in page in electron (#9900)

fix AF-2168
When using find in page (cmd+f) in electron, the popup should not prevent the user from interacting with the main content.
Also fixed some minor ui issues
This commit is contained in:
pengx17
2025-01-27 07:19:12 +00:00
parent e3fac97b9b
commit ffbec1633e
3 changed files with 106 additions and 121 deletions

View File

@@ -25,32 +25,20 @@ const contentHide = keyframes({
}, },
}); });
export const modalOverlay = style({ export const anchor = style({
position: 'fixed', position: 'fixed',
inset: 0,
backgroundColor: 'transparent',
zIndex: cssVar('zIndexModal'),
});
export const modalContentWrapper = style({
position: 'fixed',
inset: 0,
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'flex-end',
zIndex: cssVar('zIndexModal'),
right: '28px', right: '28px',
top: '80px', top: '80px',
zIndex: cssVar('zIndexModal'),
}); });
export const modalContent = style({ export const contentContainer = style({
width: 400, width: 400,
height: 48, height: 48,
backgroundColor: cssVar('backgroundOverlayPanelColor'), backgroundColor: cssVar('backgroundOverlayPanelColor'),
borderRadius: '8px', borderRadius: '8px',
boxShadow: cssVar('shadow3'), boxShadow: cssVar('shadow3'),
minHeight: 48, minHeight: 48,
// :focus-visible will set outline
outline: 'none', outline: 'none',
display: 'flex', display: 'flex',
gap: 8, gap: 8,
@@ -116,6 +104,7 @@ export const input = style({
height: '100%', height: '100%',
width: '100%', width: '100%',
color: 'transparent', color: 'transparent',
fontSize: '15px',
}); });
export const inputHack = style([ export const inputHack = style([
@@ -134,19 +123,22 @@ export const count = style({
userSelect: 'none', userSelect: 'none',
}); });
export const arrowButtonContainer = style({
border: '1px solid',
borderColor: cssVarV2('layer/insideBorder/border'),
flexShrink: 0,
borderRadius: '4px',
overflow: 'hidden',
});
export const arrowButton = style({ export const arrowButton = style({
width: 32, width: 32,
height: 32, height: 32,
flexShrink: 0, borderRadius: 0,
border: '1px solid',
borderColor: cssVarV2('layer/insideBorder/border'),
selectors: { selectors: {
'&.backward': { '&:first-child': {
borderRadius: '4px 0 0 4px', borderRight: '1px solid',
}, borderColor: cssVarV2('layer/insideBorder/border'),
'&.forward': {
borderLeft: 'none',
borderRadius: '0 4px 4px 0',
}, },
}, },
}); });

View File

@@ -6,7 +6,7 @@ import {
CloseIcon, CloseIcon,
SearchIcon, SearchIcon,
} from '@blocksuite/icons/rc'; } from '@blocksuite/icons/rc';
import * as Dialog from '@radix-ui/react-dialog'; import * as Popover from '@radix-ui/react-popover';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { assignInlineVars } from '@vanilla-extract/dynamic'; import { assignInlineVars } from '@vanilla-extract/dynamic';
import clsx from 'clsx'; import clsx from 'clsx';
@@ -21,7 +21,7 @@ import {
} from 'react'; } from 'react';
import { useTransition } from 'react-transition-state'; import { useTransition } from 'react-transition-state';
import * as styles from './find-in-page-modal.css'; import * as styles from './find-in-page-popup.css';
const animationTimeout = 120; const animationTimeout = 120;
@@ -36,8 +36,10 @@ const drawText = (
} }
const dpr = window.devicePixelRatio || 1; const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.getBoundingClientRect().width * dpr; // the container will be animated,
canvas.height = canvas.getBoundingClientRect().height * dpr; // so we need to use clientWidth and clientHeight instead of getBoundingClientRect
canvas.width = canvas.clientWidth * dpr;
canvas.height = canvas.clientHeight * dpr;
const rootStyles = getComputedStyle(document.documentElement); const rootStyles = getComputedStyle(document.documentElement);
const textColor = rootStyles const textColor = rootStyles
@@ -46,15 +48,13 @@ const drawText = (
ctx.scale(dpr, dpr); ctx.scale(dpr, dpr);
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
const offsetX = -scrollLeft;
ctx.fillStyle = textColor; ctx.fillStyle = textColor;
ctx.font = '15px Inter'; ctx.font = '15px Inter';
ctx.letterSpacing = '0.01em';
const offsetX = -scrollLeft; // Offset based on scrollLeft
ctx.fillText(text, offsetX, 22);
ctx.textAlign = 'left'; ctx.textAlign = 'left';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'alphabetic';
ctx.fillText(text, offsetX, 22);
}; };
const CanvasText = ({ const CanvasText = ({
@@ -80,7 +80,7 @@ const CanvasText = ({
return <canvas className={className} ref={ref} />; return <canvas className={className} ref={ref} />;
}; };
export const FindInPageModal = () => { export const FindInPagePopup = () => {
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const findInPage = useService(FindInPageService).findInPage; const findInPage = useService(FindInPageService).findInPage;
@@ -123,22 +123,6 @@ export const FindInPageModal = () => {
setActive(false); setActive(false);
}, []); }, []);
useEffect(() => {
if (visible) {
setValue(findInPage.searchText$.value || '');
const onEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
findInPage.onChangeVisible(false);
}
};
window.addEventListener('keydown', onEsc);
return () => {
window.removeEventListener('keydown', onEsc);
};
}
return () => {};
}, [findInPage, findInPage.searchText$.value, visible]);
useEffect(() => { useEffect(() => {
const unsub = findInPage.isSearching$.subscribe(() => { const unsub = findInPage.isSearching$.subscribe(() => {
inputRef.current?.focus(); inputRef.current?.focus();
@@ -169,6 +153,7 @@ export const FindInPageModal = () => {
}, },
[findInPage] [findInPage]
); );
const handleDone = useCallback(() => { const handleDone = useCallback(() => {
onChangeVisible(false); onChangeVisible(false);
}, [onChangeVisible]); }, [onChangeVisible]);
@@ -207,79 +192,87 @@ export const FindInPageModal = () => {
); );
return ( return (
<Dialog.Root open={status !== 'exited'}> <Popover.Root open={status !== 'exited'} onOpenChange={onChangeVisible}>
<Dialog.Portal> <Popover.Anchor className={styles.anchor} data-find-in-page-anchor />
<Dialog.Overlay className={styles.modalOverlay} /> <Popover.Portal>
<div className={styles.modalContentWrapper}> <Popover.Content
<Dialog.Content style={assignInlineVars({
style={assignInlineVars({ [styles.animationTimeout]: `${animationTimeout}ms`,
[styles.animationTimeout]: `${animationTimeout}ms`, })}
className={styles.contentContainer}
data-state={status}
sideOffset={5}
side="left"
onFocusOutside={e => {
// do not close the popup when focus outside (like focus in the editor)
e.preventDefault();
}}
onPointerDownOutside={e => {
// do not close the popup when clicking outside (like clicking at the sidebar)
e.preventDefault();
}}
>
<div
className={clsx(styles.inputContainer, {
active: active || isSearching,
})} })}
className={styles.modalContent}
data-state={status}
> >
<div <SearchIcon className={styles.searchIcon} />
className={clsx(styles.inputContainer, { <div className={styles.inputMain}>
active: active || isSearching, <RowInput
})} type="text"
> autoFocus
<SearchIcon className={styles.searchIcon} /> value={value}
<div className={styles.inputMain}> ref={inputRef}
<RowInput style={{
type="text" visibility: isSearching ? 'hidden' : 'visible',
autoFocus }}
value={value} onBlur={handleBlur}
ref={inputRef} onFocus={handleFocus}
style={{ className={styles.input}
visibility: isSearching ? 'hidden' : 'visible', onKeyDown={handleKeydown}
}} onChange={handleValueChange}
onBlur={handleBlur} onScroll={handleScroll}
onFocus={handleFocus} onCompositionStart={handleCompositionStart}
className={styles.input} onCompositionEnd={handleCompositionEnd}
onKeyDown={handleKeydown}
onChange={handleValueChange}
onScroll={handleScroll}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
/>
<CanvasText
className={styles.inputHack}
text={value}
scrollLeft={scrollLeft}
/>
</div>
<div className={styles.count}>
{value.length > 0 && result && result.matches !== 0 ? (
<>
<span>{result?.activeMatchOrdinal || 0}</span>
<span>/</span>
<span>{result?.matches || 0}</span>
</>
) : value.length ? (
<span>No matches</span>
) : null}
</div>
</div>
<div>
<IconButton
size="24"
className={clsx(styles.arrowButton, 'backward')}
onClick={handleBackWard}
icon={<ArrowUpSmallIcon />}
/> />
<IconButton <CanvasText
size="24" className={styles.inputHack}
className={clsx(styles.arrowButton, 'forward')} text={value}
onClick={handleForward} scrollLeft={scrollLeft}
icon={<ArrowDownSmallIcon />}
/> />
</div> </div>
<div className={styles.count}>
{value.length > 0 && result && result.matches !== 0 ? (
<>
<span>{result?.activeMatchOrdinal || 0}</span>
<span>/</span>
<span>{result?.matches || 0}</span>
</>
) : value.length ? (
<span>No matches</span>
) : null}
</div>
</div>
<IconButton onClick={handleDone} icon={<CloseIcon />} /> <div className={styles.arrowButtonContainer}>
</Dialog.Content> <IconButton
</div> size="24"
</Dialog.Portal> className={styles.arrowButton}
</Dialog.Root> onClick={handleBackWard}
icon={<ArrowUpSmallIcon />}
/>
<IconButton
size="24"
className={styles.arrowButton}
onClick={handleForward}
icon={<ArrowDownSmallIcon />}
/>
</div>
<IconButton onClick={handleDone} icon={<CloseIcon />} />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
); );
}; };

View File

@@ -6,7 +6,7 @@ import { Outlet } from 'react-router-dom';
import { GlobalDialogs } from '../../dialogs'; import { GlobalDialogs } from '../../dialogs';
import { CustomThemeModifier } from './custom-theme'; import { CustomThemeModifier } from './custom-theme';
import { FindInPageModal } from './find-in-page/find-in-page-modal'; import { FindInPagePopup } from './find-in-page/find-in-page-popup';
export const RootWrapper = () => { export const RootWrapper = () => {
const defaultServerService = useService(DefaultServerService); const defaultServerService = useService(DefaultServerService);
@@ -30,7 +30,7 @@ export const RootWrapper = () => {
<NotificationCenter /> <NotificationCenter />
<Outlet /> <Outlet />
<CustomThemeModifier /> <CustomThemeModifier />
{BUILD_CONFIG.isElectron && <FindInPageModal />} {BUILD_CONFIG.isElectron && <FindInPagePopup />}
</FrameworkScope> </FrameworkScope>
); );
}; };