From c5c323690bdb90a39a7ead28a302b224b1e06259 Mon Sep 17 00:00:00 2001 From: ktmouk Date: Thu, 12 Oct 2023 12:37:57 +0900 Subject: [PATCH] Add the keyboard operation on recent search items (#5987) Co-authored-by: Tom Moor --- app/scenes/Search/Search.tsx | 40 ++++++++---- .../Search/components/RecentSearches.tsx | 61 +++++++++++++------ shared/i18n/locales/en_US/translation.json | 2 +- 3 files changed, 72 insertions(+), 31 deletions(-) diff --git a/app/scenes/Search/Search.tsx b/app/scenes/Search/Search.tsx index f4f6ce267..addaae670 100644 --- a/app/scenes/Search/Search.tsx +++ b/app/scenes/Search/Search.tsx @@ -47,7 +47,8 @@ type Props = RouteComponentProps< @observer class Search extends React.Component { - compositeRef: HTMLDivElement | null | undefined; + resultListCompositeRef: HTMLDivElement | null | undefined; + recentSearchesCompositeRef: HTMLDivElement | null | undefined; searchInputRef: HTMLInputElement | null | undefined; lastQuery = ""; @@ -128,12 +129,8 @@ class Search extends React.Component { } } - if (this.compositeRef) { - const linkItems = this.compositeRef.querySelectorAll( - "[href]" - ) as NodeListOf; - linkItems[0]?.focus(); - } + const firstItem = this.firstResultItem ?? this.firstRecentSearchItem; + firstItem?.focus(); } }; @@ -178,6 +175,20 @@ class Search extends React.Component { this.handleFilterChange({ titleFilter: ev.target.checked }); }; + get firstResultItem() { + const linkItems = this.resultListCompositeRef?.querySelectorAll( + "[href]" + ) as NodeListOf; + return linkItems?.[0]; + } + + get firstRecentSearchItem() { + const linkItems = this.recentSearchesCompositeRef?.querySelectorAll( + "li > [href]" + ) as NodeListOf; + return linkItems?.[0]; + } + get includeArchived() { return this.params.get("includeArchived") === "true"; } @@ -292,8 +303,12 @@ class Search extends React.Component { }); }; - setCompositeRef = (ref: HTMLDivElement | null) => { - this.compositeRef = ref; + setResultListCompositeRef = (ref: HTMLDivElement | null) => { + this.resultListCompositeRef = ref; + }; + + setRecentSearchesCompositeRef = (ref: HTMLDivElement | null) => { + this.recentSearchesCompositeRef = ref; }; setSearchInputRef = (ref: HTMLInputElement | null) => { @@ -372,7 +387,10 @@ class Search extends React.Component { /> ) : ( - + )} {showEmpty && ( @@ -385,7 +403,7 @@ class Search extends React.Component { )} diff --git a/app/scenes/Search/components/RecentSearches.tsx b/app/scenes/Search/components/RecentSearches.tsx index cb8e8a9a5..5b0a4e81f 100644 --- a/app/scenes/Search/components/RecentSearches.tsx +++ b/app/scenes/Search/components/RecentSearches.tsx @@ -3,8 +3,10 @@ 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"; @@ -12,7 +14,15 @@ import useStores from "~/hooks/useStores"; import { hover } from "~/styles"; import { searchPath } from "~/utils/routeHelpers"; -function RecentSearches() { +type Props = { + /** Callback when the Escape key is pressed while navigating the list */ + onEscape?: (ev: React.KeyboardEvent) => void; +}; + +function RecentSearches( + { onEscape }: Props, + ref: React.RefObject +) { const { searches } = useStores(); const { t } = useTranslation(); const [isPreloaded] = React.useState(searches.recent.length > 0); @@ -25,24 +35,37 @@ function RecentSearches() { <> {t("Recent searches")} - {searches.recent.map((searchQuery) => ( - - - {searchQuery.query} - - { - ev.preventDefault(); - await searchQuery.delete(); - }} + + {(compositeProps) => + searches.recent.map((searchQuery) => ( + + - - - - - - ))} + {searchQuery.query} + + { + ev.preventDefault(); + await searchQuery.delete(); + }} + > + + + + + + )) + } + ) : null; @@ -104,4 +127,4 @@ const RecentSearch = styled(Link)` } `; -export default observer(RecentSearches); +export default observer(React.forwardRef(RecentSearches)); diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 397a79eb7..36a8e4285 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -706,6 +706,7 @@ "Past week": "Past week", "Past month": "Past month", "Past year": "Past year", + "Search Results": "Search Results", "Remove search": "Remove search", "Active documents": "Active documents", "Documents in collections you are able to access": "Documents in collections you are able to access", @@ -716,7 +717,6 @@ "We were unable to find the page you’re looking for.": "We were unable to find the page you’re looking for.", "Search titles only": "Search titles only", "No documents found for your search filters.": "No documents found for your search filters.", - "Search Results": "Search Results", "New token": "New token", "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the developer documentation.": "You can create an unlimited amount of personal tokens to authenticate\n with the API. Tokens have the same permissions as your user account.\n For more details see the developer documentation.", "Active": "Active",