mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
Migrate SearchPopover to Radix (#9844)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user