diff --git a/apps/web/src/components/pure/quick-search-modal/Footer.tsx b/apps/web/src/components/pure/quick-search-modal/Footer.tsx index fdfd1fe2d2..b9905995a4 100644 --- a/apps/web/src/components/pure/quick-search-modal/Footer.tsx +++ b/apps/web/src/components/pure/quick-search-modal/Footer.tsx @@ -1,7 +1,7 @@ import { useTranslation } from '@affine/i18n'; import type { PageBlockModel } from '@blocksuite/blocks'; import { PlusIcon } from '@blocksuite/icons'; -import { assertEquals, assertExists, nanoid } from '@blocksuite/store'; +import { assertEquals, nanoid } from '@blocksuite/store'; import { Command } from 'cmdk'; import { NextRouter } from 'next/router'; import React from 'react'; @@ -49,8 +49,9 @@ export const Footer: React.FC = ({ const block = newPage.getBlockByFlavour( 'affine:page' )[0] as PageBlockModel; - assertExists(block); - block.title.insert(query, 0); + if (block) { + block.title.insert(query, 0); + } } }} > diff --git a/apps/web/src/components/pure/quick-search-modal/Input.tsx b/apps/web/src/components/pure/quick-search-modal/Input.tsx deleted file mode 100644 index 0e80359baf..0000000000 --- a/apps/web/src/components/pure/quick-search-modal/Input.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useTranslation } from '@affine/i18n'; -import { SearchIcon } from '@blocksuite/icons'; -import { Command } from 'cmdk'; -import React, { useEffect, useRef, useState } from 'react'; - -import { StyledInputContent, StyledLabel } from './style'; -export const Input = (props: { - open: boolean; - query: string; - setQuery: (query: string) => void; - isPublic: boolean; - publishWorkspaceName: string | undefined; -}) => { - const { open, query, setQuery, isPublic, publishWorkspaceName } = props; - const [isComposition, setIsComposition] = useState(false); - const [inputValue, setInputValue] = useState(''); - const inputRef = useRef(null); - const { t } = useTranslation(); - useEffect(() => { - if (open) { - const inputElement = inputRef.current; - return inputElement?.focus(); - } - }, [open]); - useEffect(() => { - const inputElement = inputRef.current; - if (!open) { - return; - } - const handleFocus = () => { - inputElement?.focus(); - }; - inputElement?.addEventListener('blur', handleFocus, true); - return () => inputElement?.removeEventListener('blur', handleFocus, true); - }, [inputRef, open]); - useEffect(() => { - setInputValue(query); - }, [query]); - return ( - - - - - { - setIsComposition(true); - }} - onCompositionEnd={e => { - setQuery(e.data); - setIsComposition(false); - }} - onValueChange={str => { - setInputValue(str); - if (!isComposition) { - setQuery(str); - } - }} - onKeyDown={(e: React.KeyboardEvent) => { - if (e.key === 'a' && e.metaKey) { - e.stopPropagation(); - inputRef.current?.select(); - return; - } - if (isComposition) { - if ( - e.key === 'ArrowDown' || - e.key === 'ArrowUp' || - e.key === 'Enter' - ) { - e.stopPropagation(); - } - } - }} - placeholder={ - isPublic - ? t('Quick search placeholder2', { - workspace: publishWorkspaceName, - }) - : t('Quick search placeholder') - } - /> - - ); -}; diff --git a/apps/web/src/components/pure/quick-search-modal/Results.tsx b/apps/web/src/components/pure/quick-search-modal/Results.tsx index 36ade0806c..d2c6057c8f 100644 --- a/apps/web/src/components/pure/quick-search-modal/Results.tsx +++ b/apps/web/src/components/pure/quick-search-modal/Results.tsx @@ -4,7 +4,7 @@ import { EdgelessIcon, PageIcon } from '@blocksuite/icons'; import { assertExists } from '@blocksuite/store'; import { Command } from 'cmdk'; import { NextRouter } from 'next/router'; -import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import React, { Dispatch, SetStateAction, useEffect } from 'react'; import { useRecentlyViewed } from '../../../hooks/affine/use-recent-views'; import { useBlockSuiteWorkspaceHelper } from '../../../hooks/use-blocksuite-workspace-helper'; @@ -18,14 +18,12 @@ import { StyledListItem, StyledNotFound } from './style'; export type ResultsProps = { blockSuiteWorkspace: BlockSuiteWorkspace; query: string; - loading: boolean; onClose: () => void; setShowCreatePage: Dispatch>; router: NextRouter; }; export const Results: React.FC = ({ query, - loading, blockSuiteWorkspace, setShowCreatePage, router, @@ -35,15 +33,12 @@ export const Results: React.FC = ({ const pageList = usePageMeta(blockSuiteWorkspace); assertExists(blockSuiteWorkspace.id); const List = useSwitchToConfig(blockSuiteWorkspace.id); - const [results, setResults] = useState(new Map()); + const recentlyViewed = useRecentlyViewed(); const { t } = useTranslation(); const { jumpToPage } = useRouterHelper(router); - useEffect(() => { - setResults(blockSuiteWorkspace.search(query)); - //Save the Map obtained from the search as state - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [query, setResults]); + const results = blockSuiteWorkspace.search(query); + const pageIds = [...results.values()]; const resultsPageMeta = pageList.filter( @@ -61,93 +56,88 @@ export const Results: React.FC = ({ setShowCreatePage(!resultsPageMeta.length); //Determine whether to display the ‘+ New page’ }, [resultsPageMeta.length, setShowCreatePage]); - return loading ? null : ( - <> - {query ? ( - resultsPageMeta.length ? ( - - {resultsPageMeta.map(result => { + if (!query) { + return ( + <> + {recentlyViewedItem.length > 0 && ( + + {recentlyViewedItem.map(recent => { + const page = pageList.find(page => recent.id === page.id); + assertExists(page); return ( { onClose(); - assertExists(blockSuiteWorkspace.id); - jumpToPage(blockSuiteWorkspace.id, result.id); + jumpToPage(blockSuiteWorkspace.id, page.id); }} - value={result.id} > - {result.mode === 'edgeless' ? ( + {recent.mode === 'edgeless' ? ( ) : ( )} - {result.title} + {page.title || UNTITLED_WORKSPACE_NAME} ); })} - ) : ( - - {t('Find 0 result')} - - - ) - ) : ( -
- {recentlyViewedItem.length > 0 && ( - - {recentlyViewedItem.map(recent => { - const page = pageList.find(page => recent.id === page.id); - assertExists(page); - return ( - { - onClose(); - jumpToPage(blockSuiteWorkspace.id, page.id); - }} - > - - {recent.mode === 'edgeless' ? ( - - ) : ( - - )} - {page.title || UNTITLED_WORKSPACE_NAME} - - - ); - })} - - )} - - - {List.map(link => { - return ( - { - onClose(); - router.push(link.href); - }} - > - - - {link.title} - - - ); - })} - -
- )} - + )} + + {List.map(link => { + return ( + { + onClose(); + router.push(link.href); + }} + > + + + {link.title} + + + ); + })} + + + ); + } + if (!resultsPageMeta.length) { + return ( + + {t('Find 0 result')} + + + ); + } + return ( + + {resultsPageMeta.map(result => { + return ( + { + onClose(); + assertExists(blockSuiteWorkspace.id); + jumpToPage(blockSuiteWorkspace.id, result.id); + }} + value={result.id} + > + + {result.mode === 'edgeless' ? : } + {result.title} + + + ); + })} + ); }; diff --git a/apps/web/src/components/pure/quick-search-modal/SearchInput.tsx b/apps/web/src/components/pure/quick-search-modal/SearchInput.tsx new file mode 100644 index 0000000000..d36ee7305a --- /dev/null +++ b/apps/web/src/components/pure/quick-search-modal/SearchInput.tsx @@ -0,0 +1,33 @@ +import { SearchIcon } from '@blocksuite/icons'; +import { Command } from 'cmdk'; +import { forwardRef } from 'react'; + +import { StyledInputContent, StyledLabel } from './style'; + +export const SearchInput = forwardRef< + HTMLInputElement, + Omit< + React.InputHTMLAttributes, + 'value' | 'onChange' | 'type' + > & { + /** + * Optional controlled state for the value of the search input. + */ + value?: string; + /** + * Event handler called when the search value changes. + */ + onValueChange?: (search: string) => void; + } & React.RefAttributes +>((props, ref) => { + return ( + + + + + + + ); +}); + +SearchInput.displayName = 'SearchInput'; diff --git a/apps/web/src/components/pure/quick-search-modal/index.tsx b/apps/web/src/components/pure/quick-search-modal/index.tsx index 75499df27c..0eeb1c9123 100644 --- a/apps/web/src/components/pure/quick-search-modal/index.tsx +++ b/apps/web/src/components/pure/quick-search-modal/index.tsx @@ -1,20 +1,22 @@ import { Modal, ModalWrapper } from '@affine/component'; import { getEnvironment } from '@affine/env'; +import { useTranslation } from '@affine/i18n'; import { Command } from 'cmdk'; import { NextRouter } from 'next/router'; import React, { useCallback, useEffect, useMemo, + useRef, useState, useTransition, } from 'react'; import { BlockSuiteWorkspace } from '../../../shared'; import { Footer } from './Footer'; -import { Input } from './Input'; import { PublishedResults } from './PublishedResults'; import { Results } from './Results'; +import { SearchInput } from './SearchInput'; import { StyledContent, StyledModalDivider, @@ -41,6 +43,8 @@ export const QuickSearchModal: React.FC = ({ router, blockSuiteWorkspace, }) => { + const { t } = useTranslation(); + const inputRef = useRef(null); const [loading, startTransition] = useTransition(); const [query, _setQuery] = useState(''); const setQuery = useCallback((query: string) => { @@ -58,9 +62,8 @@ export const QuickSearchModal: React.FC = ({ return isPublicWorkspace && query.length === 0; }, [isPublicWorkspace, query.length]); const handleClose = useCallback(() => { - setQuery(''); setOpen(false); - }, [setOpen, setQuery]); + }, [setOpen]); // Add ‘⌘+K’ shortcut keys as switches useEffect(() => { const keydown = (e: KeyboardEvent) => { @@ -80,7 +83,15 @@ export const QuickSearchModal: React.FC = ({ return () => document.removeEventListener('keydown', keydown, { capture: true }); }, [open, router, setOpen, setQuery]); - + useEffect(() => { + if (open) { + // Waiting for DOM rendering + requestAnimationFrame(() => { + const inputElement = inputRef.current; + inputElement?.focus(); + }); + } + }, [open]); return ( = ({ }} > - { + setQuery(value); + }} + onKeyDown={e => { + // Avoid triggering the cmdk onSelect event when the input method is in use + if (e.nativeEvent.isComposing) { + e.stopPropagation(); + return; + } + }} + placeholder={ + isPublicWorkspace + ? t('Quick search placeholder2', { + workspace: publishWorkspaceName, + }) + : t('Quick search placeholder') + } /> {isMac() ? '⌘ + K' : 'Ctrl + K'} @@ -130,7 +154,6 @@ export const QuickSearchModal: React.FC = ({ {!isPublicWorkspace ? ( { width: '100%', overflow: 'auto', marginBottom: '10px', - ...displayFlex('center', 'flex-start'), + ...displayFlex('flex-start', 'flex-start'), + flexDirection: 'column', color: theme.colors.textColor, transition: 'all 0.15s', letterSpacing: '0.06em', @@ -67,7 +68,7 @@ export const StyledInputContent = styled('div')(({ theme }) => { fontSize: theme.font.base, ...displayFlex('space-between', 'center'), letterSpacing: '0.06em', - + color: theme.colors.textColor, '::placeholder': { color: theme.colors.placeHolderColor, }, @@ -114,6 +115,7 @@ export const StyledModalFooter = styled('div')(({ theme }) => { lineHeight: '22px', marginBottom: '8px', textAlign: 'center', + color: theme.colors.textColor, ...displayFlex('center', 'center'), '[aria-selected="true"]': { transition: 'background .15s, color .15s',