Replace reakit/Composite with react-roving-tabindex (#6985)

* fix: replace reakit composite with react-roving-tabindex

* fix: touch points

* fix: focus stuck at first list item

* fix: document history navigation

* fix: remove ununsed ListItem components

* fix: keyboard navigation in recent search list

* fix: updated lib
This commit is contained in:
Apoorv Mishra
2024-06-13 18:45:44 +05:30
committed by GitHub
parent 20b1766e8d
commit 23c8adc5d1
19 changed files with 219 additions and 329 deletions

View File

@@ -53,8 +53,8 @@ function Search(props: Props) {
// refs
const searchInputRef = React.useRef<HTMLInputElement | null>(null);
const resultListCompositeRef = React.useRef<HTMLDivElement | null>(null);
const recentSearchesCompositeRef = React.useRef<HTMLDivElement | null>(null);
const resultListRef = React.useRef<HTMLDivElement | null>(null);
const recentSearchesRef = React.useRef<HTMLDivElement | null>(null);
// filters
const query = decodeURIComponentSafe(routeMatch.params.term ?? "");
@@ -178,19 +178,9 @@ function Search(props: Props) {
}
}
const firstResultItem = (
resultListCompositeRef.current?.querySelectorAll(
"[href]"
) as NodeListOf<HTMLAnchorElement>
)?.[0];
const firstItem = (resultListRef.current?.firstElementChild ??
recentSearchesRef.current?.firstElementChild) as HTMLAnchorElement;
const firstRecentSearchItem = (
recentSearchesCompositeRef.current?.querySelectorAll(
"li > [href]"
) as NodeListOf<HTMLAnchorElement>
)?.[0];
const firstItem = firstResultItem ?? firstRecentSearchItem;
firstItem?.focus();
}
};
@@ -277,11 +267,11 @@ function Search(props: Props) {
)}
<ResultList column>
<StyledArrowKeyNavigation
ref={resultListCompositeRef}
ref={resultListRef}
onEscape={handleEscape}
aria-label={t("Search Results")}
>
{(compositeProps) =>
{() =>
data?.length
? data.map((result) => (
<DocumentListItem
@@ -291,7 +281,6 @@ function Search(props: Props) {
context={result.context}
showCollection
showTemplate
{...compositeProps}
/>
))
: null
@@ -305,10 +294,7 @@ function Search(props: Props) {
</ResultList>
</>
) : documentId || collectionId ? null : (
<RecentSearches
ref={recentSearchesCompositeRef}
onEscape={handleEscape}
/>
<RecentSearches ref={recentSearchesRef} onEscape={handleEscape} />
)}
</ResultsWrapper>
</Scene>

View File

@@ -0,0 +1,92 @@
import {
useFocusEffect,
useRovingTabIndex,
} from "@getoutline/react-roving-tabindex";
import { CloseIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { s } from "@shared/styles";
import type SearchQuery from "~/models/SearchQuery";
import NudeButton from "~/components/NudeButton";
import Tooltip from "~/components/Tooltip";
import { hover } from "~/styles";
import { searchPath } from "~/utils/routeHelpers";
type Props = {
searchQuery: SearchQuery;
};
function RecentSearchListItem({ searchQuery }: Props) {
const { t } = useTranslation();
const ref = React.useRef<HTMLAnchorElement>(null);
const { focused, ...rovingTabIndex } = useRovingTabIndex(ref, false);
useFocusEffect(focused, ref);
return (
<RecentSearch
to={searchPath(searchQuery.query)}
ref={ref}
{...rovingTabIndex}
>
{searchQuery.query}
<Tooltip content={t("Remove search")} delay={150}>
<RemoveButton
aria-label={t("Remove search")}
onClick={async (ev) => {
ev.preventDefault();
await searchQuery.delete();
}}
>
<CloseIcon />
</RemoveButton>
</Tooltip>
</RecentSearch>
);
}
const RemoveButton = styled(NudeButton)`
opacity: 0;
color: ${s("textTertiary")};
&:hover {
color: ${s("text")};
}
`;
const RecentSearch = styled(Link)`
display: flex;
justify-content: space-between;
color: ${s("textSecondary")};
cursor: var(--pointer);
padding: 1px 4px;
border-radius: 4px;
position: relative;
font-size: 14px;
&:before {
content: "·";
color: ${s("textTertiary")};
position: absolute;
left: -8px;
}
&:focus-visible {
outline: none;
}
&:focus,
&:${hover} {
color: ${s("text")};
background: ${s("secondaryBackground")};
${RemoveButton} {
opacity: 1;
}
}
`;
export default RecentSearchListItem;

View File

@@ -1,18 +1,12 @@
import { observer } from "mobx-react";
import { CloseIcon } from "outline-icons";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { CompositeItem } from "reakit/Composite";
import styled from "styled-components";
import { s } from "@shared/styles";
import ArrowKeyNavigation from "~/components/ArrowKeyNavigation";
import Fade from "~/components/Fade";
import NudeButton from "~/components/NudeButton";
import Tooltip from "~/components/Tooltip";
import useStores from "~/hooks/useStores";
import { hover } from "~/styles";
import { searchPath } from "~/utils/routeHelpers";
import RecentSearchListItem from "./RecentSearchListItem";
type Props = {
/** Callback when the Escape key is pressed while navigating the list */
@@ -36,39 +30,20 @@ function RecentSearches(
const content = searches.recent.length ? (
<>
<Heading>{t("Recent searches")}</Heading>
<List>
<ArrowKeyNavigation
ref={ref}
onEscape={onEscape}
aria-label={t("Search Results")}
>
{(compositeProps) =>
searches.recent.map((searchQuery) => (
<ListItem key={searchQuery.id}>
<CompositeItem
as={RecentSearch}
to={searchPath(searchQuery.query)}
role="menuitem"
{...compositeProps}
>
{searchQuery.query}
<Tooltip content={t("Remove search")} delay={150}>
<RemoveButton
aria-label={t("Remove search")}
onClick={async (ev) => {
ev.preventDefault();
await searchQuery.delete();
}}
>
<CloseIcon />
</RemoveButton>
</Tooltip>
</CompositeItem>
</ListItem>
))
}
</ArrowKeyNavigation>
</List>
<StyledArrowKeyNavigation
ref={ref}
onEscape={onEscape}
aria-label={t("Recent searches")}
>
{() =>
searches.recent.map((searchQuery) => (
<RecentSearchListItem
key={searchQuery.id}
searchQuery={searchQuery}
/>
))
}
</StyledArrowKeyNavigation>
</>
) : null;
@@ -83,55 +58,9 @@ const Heading = styled.h2`
margin-bottom: 0;
`;
const List = styled.ol`
const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
padding: 0;
margin-top: 8px;
`;
const ListItem = styled.li`
font-size: 14px;
padding: 0;
list-style: none;
position: relative;
&:before {
content: "·";
color: ${s("textTertiary")};
position: absolute;
left: -8px;
}
`;
const RemoveButton = styled(NudeButton)`
opacity: 0;
color: ${s("textTertiary")};
&:hover {
color: ${s("text")};
}
`;
const RecentSearch = styled(Link)`
display: flex;
justify-content: space-between;
color: ${s("textSecondary")};
cursor: var(--pointer);
padding: 1px 4px;
border-radius: 4px;
&:focus-visible {
outline: none;
}
&:focus,
&: ${hover} {
color: ${s("text")};
background: ${s("secondaryBackground")};
${RemoveButton} {
opacity: 1;
}
}
`;
export default observer(React.forwardRef(RecentSearches));