Migrate SearchPopover to Radix (#9844)

This commit is contained in:
Hemachandar
2025-08-06 03:12:06 +05:30
committed by GitHub
parent 8ad097aef2
commit 9bb802c35c
2 changed files with 61 additions and 66 deletions
+58 -65
View File
@@ -2,15 +2,17 @@ import debounce from "lodash/debounce";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { usePopoverState, PopoverDisclosure } from "reakit/Popover";
import styled from "styled-components";
import { depths } from "@shared/styles";
import Empty from "~/components/Empty";
import { Outline } from "~/components/Input";
import InputSearch from "~/components/InputSearch";
import Placeholder from "~/components/List/Placeholder";
import PaginatedList from "~/components/PaginatedList";
import Popover from "~/components/Popover";
import {
Popover,
PopoverAnchor,
PopoverContent,
} from "~/components/primitives/Popover";
import { id as bodyContentId } from "~/components/SkipNavContent";
import useKeyDown from "~/hooks/useKeyDown";
import useStores from "~/hooks/useStores";
@@ -27,14 +29,8 @@ function SearchPopover({ shareId, className }: Props) {
const { documents } = useStores();
const focusRef = React.useRef<HTMLElement | null>(null);
const popover = usePopoverState({
placement: "bottom-start",
unstable_offset: [-24, 0],
modal: true,
});
const [open, setOpen] = React.useState(false);
const [query, setQuery] = React.useState("");
const { show, hide } = popover;
const [searchResults, setSearchResults] = React.useState<
SearchResult[] | undefined
@@ -48,9 +44,9 @@ function SearchPopover({ shareId, className }: Props) {
if (searchResults) {
setCachedQuery(query);
setCachedSearchResults(searchResults);
show();
setOpen(true);
}
}, [searchResults, query, show]);
}, [searchResults, query]);
const performSearch = React.useCallback(
async ({ query: searchQuery, ...options }) => {
@@ -76,25 +72,14 @@ function SearchPopover({ shareId, className }: Props) {
() =>
debounce(async (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setQuery(value.trim());
// covers edge case: user manually dismisses popover then
// quickly edits input resulting in no change in query
// the useEffect that normally shows the popover will miss it
if (value === cachedQuery) {
popover.show();
}
if (!value.length) {
popover.hide();
}
const trimmedValue = value.trim();
setQuery(trimmedValue);
setOpen(!!trimmedValue);
}, 300),
[popover, cachedQuery]
[cachedQuery]
);
const searchInputRef =
popover.unstable_referenceRef as React.RefObject<HTMLInputElement>;
const searchInputRef = React.useRef<HTMLInputElement>(null);
const firstSearchItem = React.useRef<HTMLAnchorElement>(null);
const handleEscapeList = React.useCallback(
@@ -114,24 +99,29 @@ function SearchPopover({ shareId, className }: Props) {
if (ev.key === "Enter") {
if (searchResults) {
popover.show();
setOpen(true);
}
}
if (ev.key === "ArrowDown" && !ev.shiftKey) {
if (ev.currentTarget.value.length) {
if (
ev.currentTarget.value.length === ev.currentTarget.selectionStart
) {
popover.show();
const atEnd =
ev.currentTarget.value.length === ev.currentTarget.selectionStart;
if (atEnd) {
setOpen(true);
}
if (open || atEnd) {
ev.preventDefault();
firstSearchItem.current?.focus();
}
firstSearchItem.current?.focus();
}
}
if (ev.key === "ArrowUp") {
if (popover.visible) {
popover.hide();
if (open) {
setOpen(false);
if (!ev.shiftKey) {
ev.preventDefault();
}
@@ -147,22 +137,22 @@ function SearchPopover({ shareId, className }: Props) {
}
if (ev.key === "Escape") {
if (popover.visible) {
popover.hide();
if (open) {
setOpen(false);
ev.preventDefault();
}
}
},
[popover, searchResults]
[open, searchResults]
);
const handleSearchItemClick = React.useCallback(() => {
hide();
setOpen(false);
if (searchInputRef.current) {
searchInputRef.current.value = "";
focusRef.current = document.getElementById(bodyContentId);
}
}, [searchInputRef, hide]);
}, [searchInputRef]);
useKeyDown("/", (ev) => {
if (
@@ -175,30 +165,33 @@ function SearchPopover({ shareId, className }: Props) {
});
return (
<>
<PopoverDisclosure {...popover}>
{(props) => (
// props assumes the disclosure is a button, but we want a type-ahead
// so we take the aria props, and ref and ignore the event handlers
<StyledInputSearch
aria-controls={props["aria-controls"]}
aria-expanded={props["aria-expanded"]}
aria-haspopup={props["aria-haspopup"]}
ref={props.ref}
onChange={handleSearchInputChange}
onFocus={handleSearchInputFocus}
onKeyDown={handleKeyDown}
className={className}
/>
)}
</PopoverDisclosure>
<Popover
{...popover}
<Popover open={open} onOpenChange={setOpen} modal={true}>
<PopoverAnchor>
<StyledInputSearch
aria-controls="search-results"
aria-expanded={open}
aria-haspopup="listbox"
ref={searchInputRef}
onChange={handleSearchInputChange}
onFocus={handleSearchInputFocus}
onKeyDown={handleKeyDown}
className={className}
/>
</PopoverAnchor>
<PopoverContent
id="search-results"
aria-label={t("Results")}
unstable_autoFocusOnShow={false}
unstable_finalFocusRef={focusRef}
style={{ zIndex: depths.sidebar + 1 }}
side="bottom"
align="start"
shrink
onEscapeKeyDown={handleEscapeList}
onOpenAutoFocus={(e) => e.preventDefault()}
onInteractOutside={(event) => {
const target = event.target as Element | null;
if (target === searchInputRef.current) {
event.preventDefault();
}
}}
>
<PaginatedList<SearchResult>
options={{ query, snippetMinWords: 10, snippetMaxWords: 11 }}
@@ -221,8 +214,8 @@ function SearchPopover({ shareId, className }: Props) {
/>
)}
/>
</Popover>
</>
</PopoverContent>
</Popover>
);
}
+3 -1
View File
@@ -7,6 +7,8 @@ import { fadeAndScaleIn } from "~/styles/animations";
const Popover = PopoverPrimitive.Root;
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverTrigger = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Trigger>
@@ -116,4 +118,4 @@ const StyledContent = styled(PopoverPrimitive.Content)<StyledContentProps>`
}
`;
export { Popover, PopoverTrigger, PopoverContent };
export { Popover, PopoverAnchor, PopoverTrigger, PopoverContent };