Skip to content

Commit 2a9c204

Browse files
committed
add highlights & improve a11y
1 parent ec4ba2a commit 2a9c204

File tree

3 files changed

+104
-35
lines changed

3 files changed

+104
-35
lines changed

clients/search-component/src/TrieveModal/index.tsx

+38-21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState } from "react";
1+
import React, { act, useEffect, useState } from "react";
22
import { SearchChunksReqPayload, TrieveSDK } from "trieve-ts-sdk";
33
import { Chunk } from "../utils/types";
44
import * as Dialog from "@radix-ui/react-dialog";
@@ -10,10 +10,15 @@ type Props = {
1010
onResultClick?: (chunk: Chunk) => void;
1111
showImages?: boolean;
1212
theme?: "light" | "dark";
13-
searchOptions?: Omit<SearchChunksReqPayload, "query">;
13+
searchOptions?: Omit<
14+
Omit<SearchChunksReqPayload, "query">,
15+
"highlight_options"
16+
>;
1417
placeholder?: string;
1518
};
1619

20+
export type ChunkWithHighlights = { chunk: Chunk; highlights: string[] };
21+
1722
export const TrieveModalSearch = ({
1823
placeholder = "Search...",
1924
onResultClick,
@@ -22,23 +27,23 @@ export const TrieveModalSearch = ({
2227
theme = "light",
2328
searchOptions = {
2429
search_type: "hybrid",
25-
highlight_options: {
26-
highlight_delimiters: ["?", ",", ".", "!", "↵"],
27-
highlight_max_length: 2,
28-
highlight_max_num: 2,
29-
highlight_strategy: "exactmatch",
30-
highlight_window: 100,
31-
},
3230
},
3331
}: Props) => {
3432
const [query, setQuery] = useState("");
35-
const [results, setResults] = useState<{ chunk: Chunk }[]>([]);
33+
const [results, setResults] = useState<ChunkWithHighlights[]>([]);
3634
const [open, setOpen] = useState(false);
3735

3836
const search = async () => {
3937
const results = await trieve.search({
4038
...searchOptions,
4139
query,
40+
highlight_options: {
41+
highlight_delimiters: ["?", ",", ".", "!", "↵"],
42+
highlight_max_length: 2,
43+
highlight_max_num: 1,
44+
highlight_strategy: "exactmatch",
45+
highlight_window: 10,
46+
},
4247
});
4348
const resultsWithHighlight = results.chunks.map((chunk) => {
4449
const c = chunk.chunk as unknown as Chunk;
@@ -47,18 +52,10 @@ export const TrieveModalSearch = ({
4752
chunk: {
4853
...chunk.chunk,
4954
highlight: highlightText(query, c.chunk_html),
50-
highlightTitle: highlightText(
51-
query,
52-
c.metadata?.title || c.metadata?.page_title || c.metadata?.name
53-
),
54-
highlightDescription: highlightText(
55-
query,
56-
c.metadata?.description || c.metadata?.page_description
57-
),
5855
},
5956
};
6057
});
61-
setResults(resultsWithHighlight as unknown as { chunk: Chunk }[]);
58+
setResults(resultsWithHighlight as unknown as ChunkWithHighlights[]);
6259
};
6360
useEffect(() => {
6461
if (query) {
@@ -70,6 +67,23 @@ export const TrieveModalSearch = ({
7067
if (e.code === "KeyK" && e.metaKey && !open) setOpen(true);
7168
};
7269

70+
const onUpOrDownClicked = (index: number, code: string) => {
71+
console.log("clicked");
72+
if (code === "ArrowDown") {
73+
document
74+
.getElementsByClassName("trieve-elements-search")[0]
75+
.getElementsByClassName("item")
76+
[index + 1]?.focus();
77+
}
78+
79+
if (code === "ArrowUp") {
80+
document
81+
.getElementsByClassName("trieve-elements-search")[0]
82+
.getElementsByClassName("item")
83+
[index - 1]?.focus();
84+
}
85+
};
86+
7387
useEffect(() => {
7488
document.addEventListener("keydown", checkForCMDK);
7589
return () => {
@@ -106,6 +120,7 @@ export const TrieveModalSearch = ({
106120
</button>
107121
</Dialog.Trigger>
108122
<Dialog.Portal>
123+
<Dialog.DialogTitle className="sr-only">Search</Dialog.DialogTitle>
109124
<Dialog.Overlay id="trieve-search-modal-overlay" />
110125
<Dialog.Content
111126
id="trieve-search-modal"
@@ -136,10 +151,12 @@ export const TrieveModalSearch = ({
136151
<kbd>ESC</kbd>
137152
</div>
138153
</div>
139-
<ul>
140-
{results.map((result) => (
154+
<ul className="trieve-elements-search">
155+
{results.map((result, index) => (
141156
<Item
157+
onUpOrDownClicked={onUpOrDownClicked}
142158
item={result}
159+
index={index}
143160
onResultClick={onResultClick}
144161
showImages={showImages}
145162
key={result.chunk.id}

clients/search-component/src/TrieveModal/item.tsx

+38-12
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,49 @@
1+
import { ChunkWithHighlights } from ".";
12
import { Chunk } from "../utils/types";
2-
import React from "react";
3+
import React, { useEffect, useRef } from "react";
34

45
type Props = {
5-
item: { chunk: Chunk };
6+
item: ChunkWithHighlights;
67
onResultClick?: (chunk: Chunk) => void;
78
showImages?: boolean;
9+
index: number;
10+
onUpOrDownClicked: (index: number, code: string) => void;
811
};
912

10-
export const Item = ({ item, onResultClick, showImages }: Props) => {
13+
export const Item = ({
14+
item,
15+
onResultClick,
16+
showImages,
17+
index,
18+
onUpOrDownClicked,
19+
}: Props) => {
1120
const Component = item.chunk.link ? "a" : "button";
21+
const itemRef = useRef<HTMLButtonElement | HTMLLinkElement | any>(null);
22+
const title =
23+
item.chunk.metadata?.title ||
24+
item.chunk.metadata?.page_title ||
25+
item.chunk.metadata?.name;
26+
27+
const checkForUpAndDown = (e: KeyboardEvent) => {
28+
if (
29+
(e.code === "ArrowDown" || e.code === "ArrowUp") &&
30+
itemRef.current === document.activeElement
31+
) {
32+
onUpOrDownClicked(index, e.code);
33+
}
34+
};
35+
36+
useEffect(() => {
37+
document.addEventListener("keydown", checkForUpAndDown);
38+
return () => {
39+
document.removeEventListener("keydown", checkForUpAndDown);
40+
};
41+
});
42+
1243
return (
1344
<li>
1445
<Component
46+
ref={itemRef}
1547
className="item"
1648
onClick={() => onResultClick && onResultClick(item.chunk)}
1749
{...(item.chunk.link ? { href: item.chunk.link } : {})}
@@ -22,18 +54,12 @@ export const Item = ({ item, onResultClick, showImages }: Props) => {
2254
item.chunk.image_urls[0] ? (
2355
<img src={item.chunk.image_urls[0]} />
2456
) : null}
25-
{item.chunk.highlightDescription || item.chunk.highlightTitle ? (
57+
{item.chunk.highlightDescription || title ? (
2658
<div>
27-
<h4
28-
dangerouslySetInnerHTML={{
29-
__html: item.chunk.highlightTitle || "",
30-
}}
31-
></h4>
59+
<h4>{title}</h4>
3260
<p
3361
className="description"
34-
dangerouslySetInnerHTML={{
35-
__html: item.chunk.highlightDescription || "",
36-
}}
62+
dangerouslySetInnerHTML={{ __html: item.highlights[0] }}
3763
></p>
3864
</div>
3965
) : (

clients/search-component/src/app.css

+28-2
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,17 @@
134134
.input-wrapper {
135135
@apply bg-zinc-900;
136136
}
137+
138+
.item {
139+
@apply hover:bg-zinc-800 border-zinc-700 focus:bg-zinc-800;
140+
141+
.description {
142+
@apply text-zinc-400;
143+
}
144+
}
137145
}
138146
#trieve-search-modal {
139-
@apply data-[state=open]:animate-contentShow fixed top-1/2 left-1/2 max-h-[80vh] sm:max-h-[540px] w-[90vw] max-w-[550px] -translate-x-1/2 -translate-y-1/2 rounded-lg bg-zinc-50 p-2 pt-0 shadow-2xl focus:outline-none;
147+
@apply data-[state=open]:animate-contentShow fixed top-1/2 left-1/2 max-h-[80vh] w-[90vw] sm:max-w-[550px] -translate-x-1/2 -translate-y-1/2 rounded-lg bg-zinc-50 p-2 pt-0 shadow-2xl focus:outline-none;
140148

141149
.kbd-wrapper {
142150
@apply absolute right-2 flex py-1.5 pr-1.5;
@@ -173,11 +181,29 @@
173181
}
174182

175183
.item {
176-
@apply select-none cursor-pointer p-4 text-left flex items-start gap-2 border-b border-zinc-200 w-full text-sm;
184+
@apply select-none cursor-pointer p-4 text-left flex items-start gap-2 border-b border-zinc-200 w-full text-sm pr-6 bg-zinc-50 outline-none;
185+
186+
&:hover,
187+
&:focus {
188+
@apply bg-zinc-100 outline-none ring-magenta-500 ring-1 ring-inset;
189+
}
190+
191+
&:hover .arrow-link,
192+
&:focus .arrow-link {
193+
@apply block;
194+
}
195+
196+
.arrow-link {
197+
@apply hidden w-4 h-4 text-zinc-600 dark:text-zinc-400 relative -right-2;
198+
}
177199

178200
> div {
179201
@apply flex items-center justify-between w-full;
180202
}
203+
204+
.description {
205+
@apply font-normal text-xs text-zinc-600;
206+
}
181207
}
182208
}
183209

0 commit comments

Comments
 (0)