@cn-npm/search-autocomplete 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.babelrc +3 -0
  2. package/.storybook/main.js +26 -0
  3. package/.storybook/preview.js +24 -0
  4. package/README.md +69 -0
  5. package/dist/App.d.ts +4 -0
  6. package/dist/bundle.js +406 -0
  7. package/dist/components/ErrorBoundary/ErrorBoundary.d.ts +10 -0
  8. package/dist/components/Loader/Loader.d.ts +6 -0
  9. package/dist/components/SearchAutocomplete/SearchAutocomplete.d.ts +7 -0
  10. package/dist/components/SearchAutocompleteSection/SearchAutocompleteSection.d.ts +9 -0
  11. package/dist/components/SearchAutocompleteTag/SearchAutocompleteTag.d.ts +10 -0
  12. package/dist/components/index.d.ts +3 -0
  13. package/dist/index.d.ts +1 -0
  14. package/dist/reportWebVitals.d.ts +3 -0
  15. package/dist/search-result.d.ts +42 -0
  16. package/dist/setupTests.d.ts +1 -0
  17. package/generate-react-cli.json +15 -0
  18. package/package.json +110 -0
  19. package/postcss.config.js +6 -0
  20. package/public/favicon.ico +0 -0
  21. package/public/index.html +43 -0
  22. package/public/logo192.png +0 -0
  23. package/public/logo512.png +0 -0
  24. package/public/manifest.json +25 -0
  25. package/public/robots.txt +3 -0
  26. package/rollup.config.js +35 -0
  27. package/src/App.scss +0 -0
  28. package/src/App.tsx +13 -0
  29. package/src/assets/img/circle-close.svg +12 -0
  30. package/src/assets/img/search.svg +18 -0
  31. package/src/components/ErrorBoundary/ErrorBoundary.module.scss +1 -0
  32. package/src/components/ErrorBoundary/ErrorBoundary.tsx +33 -0
  33. package/src/components/Loader/Loader.module.scss +21 -0
  34. package/src/components/Loader/Loader.stories.tsx +12 -0
  35. package/src/components/Loader/Loader.test.tsx +12 -0
  36. package/src/components/Loader/Loader.tsx +18 -0
  37. package/src/components/SearchAutocomplete/SearchAutocomplete.module.scss +3 -0
  38. package/src/components/SearchAutocomplete/SearchAutocomplete.stories.tsx +41 -0
  39. package/src/components/SearchAutocomplete/SearchAutocomplete.test.tsx +86 -0
  40. package/src/components/SearchAutocomplete/SearchAutocomplete.tsx +310 -0
  41. package/src/components/SearchAutocompleteSection/SearchAutocompleteSection.module.scss +64 -0
  42. package/src/components/SearchAutocompleteSection/SearchAutocompleteSection.test.tsx +54 -0
  43. package/src/components/SearchAutocompleteSection/SearchAutocompleteSection.tsx +74 -0
  44. package/src/components/SearchAutocompleteTag/SearchAutocompleteTag.module.scss +6 -0
  45. package/src/components/SearchAutocompleteTag/SearchAutocompleteTag.test.tsx +52 -0
  46. package/src/components/SearchAutocompleteTag/SearchAutocompleteTag.tsx +67 -0
  47. package/src/components/index.tsx +4 -0
  48. package/src/index.css +320 -0
  49. package/src/index.tsx +19 -0
  50. package/src/react-app-env.d.ts +1 -0
  51. package/src/reportWebVitals.ts +15 -0
  52. package/src/search-result.ts +49 -0
  53. package/src/setupTests.ts +5 -0
  54. package/src/stories/Introduction.stories.mdx +211 -0
  55. package/src/stories/assets/code-brackets.svg +1 -0
  56. package/src/stories/assets/colors.svg +1 -0
  57. package/src/stories/assets/comments.svg +1 -0
  58. package/src/stories/assets/direction.svg +1 -0
  59. package/src/stories/assets/flow.svg +1 -0
  60. package/src/stories/assets/plugin.svg +1 -0
  61. package/src/stories/assets/repo.svg +1 -0
  62. package/src/stories/assets/stackalt.svg +1 -0
  63. package/tailwind.config.js +93 -0
  64. package/tsconfig.json +22 -0
@@ -0,0 +1,310 @@
1
+ import React, { FC, useState } from "react";
2
+ import styles from "./SearchAutocomplete.module.scss";
3
+ import { DebounceInput } from "react-debounce-input";
4
+ import SearchAutocompleteSection from "../SearchAutocompleteSection/SearchAutocompleteSection";
5
+ import classNames from "classnames";
6
+ import CSSTransition from "react-transition-group/CSSTransition";
7
+ import axios from "axios";
8
+ import { SearchResponse } from "../../search-result";
9
+ import Loader from "../Loader/Loader";
10
+ import { ReactComponent as SearchIcon } from "../../assets/img/search.svg";
11
+ import { ReactComponent as CircleCloseIcon } from "../../assets/img/circle-close.svg";
12
+ import ErrorBoundary from "../ErrorBoundary/ErrorBoundary";
13
+
14
+ interface SearchAutocompleteProps {
15
+ label?: string;
16
+ open?: boolean;
17
+ }
18
+
19
+ const SearchAutocomplete: FC<SearchAutocompleteProps> = (props) => {
20
+ // Set state variables
21
+ const [open, setOpen] = useState(props.open || false);
22
+ const [label, setLabel] = useState(
23
+ props.label || "Search by Charity or Cause"
24
+ );
25
+ const [loading, setLoading] = useState<boolean>(false);
26
+ const [searchTerm, setSearchTerm] = useState("");
27
+ const [searchData, setSearchData] = useState<SearchResponse>({
28
+ data: {
29
+ searchAutocomplete: [],
30
+ },
31
+ });
32
+ const [hasError, setHasError] = useState<boolean>(false);
33
+ const [selectedTag, setSelectedTag] = useState<number>(0);
34
+
35
+ // On selected tag change
36
+ React.useEffect(() => {
37
+ const tags = document.querySelectorAll(
38
+ ".autocomplete-tag, .autocomplete-custom-search"
39
+ );
40
+
41
+ tags.forEach((x, i) => {
42
+ x.classList.remove(
43
+ "autocomplete-selected",
44
+ "!bg-grey-100",
45
+ "!ring-blue-600"
46
+ );
47
+ if (i + 1 === selectedTag) {
48
+ x.classList.add(
49
+ "autocomplete-selected",
50
+ tags[i].classList.contains("autocomplete-tag")
51
+ ? "!bg-grey-100"
52
+ : "none",
53
+ "!ring-blue-600"
54
+ );
55
+ x.scrollIntoView({
56
+ behavior: "smooth",
57
+ });
58
+ }
59
+ });
60
+ }, [selectedTag]);
61
+
62
+ // On open props change
63
+ React.useEffect(() => {
64
+ setOpen(props.open || false);
65
+ }, [props.open]);
66
+
67
+ // On open change
68
+ React.useEffect(() => {
69
+ if (!open) {
70
+ setSelectedTag(0);
71
+ }
72
+ }, [open]);
73
+
74
+ // On label change
75
+ React.useEffect(() => {
76
+ setLabel(props.label || "Search by Charity or Cause");
77
+ }, [props.label]);
78
+
79
+ // On search term change
80
+ React.useEffect(() => {
81
+ setLoading(true);
82
+
83
+ setSelectedTag(0);
84
+
85
+ // Trigger custom event for analytics
86
+ if (searchTerm) {
87
+ // Create custom event
88
+ var event = new CustomEvent("autocomplete-search", {
89
+ detail: {
90
+ name: searchTerm,
91
+ },
92
+ });
93
+
94
+ // Trigger custom event
95
+ document.dispatchEvent(event);
96
+ }
97
+
98
+ const controller = new AbortController();
99
+
100
+ // Get results from API
101
+ axios({
102
+ signal: controller.signal,
103
+ url: `https://graph.charitynavigator.org/graphql`,
104
+ params: {
105
+ user_key: "67a233b3ae2f5690bce775c1760925f2",
106
+ },
107
+ method: "post",
108
+ data: {
109
+ query: `
110
+ query SearchQuery {
111
+ searchAutocomplete(term: "${searchTerm}") {
112
+ title
113
+ results {
114
+ ein
115
+ title
116
+ url
117
+ }
118
+ }
119
+ }
120
+ `,
121
+ },
122
+ })
123
+ .then((res) => {
124
+ const data: SearchResponse = res.data;
125
+ setSearchData(data);
126
+ setHasError(false);
127
+ setLoading(false);
128
+ })
129
+ .catch((e: any) => {
130
+ setSearchData({} as SearchResponse);
131
+ setHasError(true);
132
+ setLoading(false);
133
+ });
134
+
135
+ return () => {
136
+ controller.abort();
137
+ };
138
+ }, [searchTerm]);
139
+
140
+ // Clear search
141
+ const clearSearch = (e: any) => {
142
+ e.preventDefault();
143
+ setSearchTerm("");
144
+ };
145
+
146
+ // Handle key events
147
+ const keyDownHandler = (e: any) => {
148
+ const tags = document.querySelectorAll(
149
+ ".autocomplete-tag, .autocomplete-custom-search"
150
+ );
151
+
152
+ switch (e.key) {
153
+ case "ArrowUp": {
154
+ setSelectedTag(Math.max(0, selectedTag - 1));
155
+ break;
156
+ }
157
+ case "ArrowDown": {
158
+ setOpen(true);
159
+ setSelectedTag(Math.min(tags.length, selectedTag + 1));
160
+ break;
161
+ }
162
+ case "ArrowLeft": {
163
+ if (selectedTag > 0) {
164
+ setSelectedTag(Math.max(0, selectedTag - 1));
165
+ }
166
+ break;
167
+ }
168
+ case "ArrowRight": {
169
+ setOpen(true);
170
+ if (selectedTag > 0) {
171
+ setSelectedTag(Math.min(tags.length, selectedTag + 1));
172
+ }
173
+ break;
174
+ }
175
+ case "Tab": {
176
+ break;
177
+ }
178
+ case "Enter": {
179
+ if (selectedTag > 0) {
180
+ e.preventDefault();
181
+ (tags[selectedTag - 1] as any).click();
182
+ }
183
+ break;
184
+ }
185
+ case "Space": {
186
+ break;
187
+ }
188
+ case "Escape": {
189
+ setOpen(false);
190
+ setSelectedTag(0);
191
+ break;
192
+ }
193
+ case "Home": {
194
+ setSelectedTag(1);
195
+ break;
196
+ }
197
+ case "End": {
198
+ setSelectedTag(tags.length);
199
+ break;
200
+ }
201
+ }
202
+ };
203
+
204
+ return (
205
+ <div
206
+ className={classNames(styles.SearchAutocomplete, "autocomplete")}
207
+ data-testid="SearchAutocomplete"
208
+ >
209
+ <form
210
+ action={`/search?q=${searchTerm}`}
211
+ className="flex font-sofia-pro"
212
+ autoComplete="off"
213
+ >
214
+ <label htmlFor="search" className="hidden">
215
+ {label}
216
+ </label>
217
+ <div className="relative flex flex-grow">
218
+ <DebounceInput
219
+ type="text"
220
+ name="q"
221
+ id="search"
222
+ placeholder={label}
223
+ className="flex-auto pr-0 items-stretch flex-grow text-night-sky-800 ring-0 py-0 shadow-none focus:ring-0 bg-white transition-colors border-opacity-100 hover:bg-grey-100 hover:border-grey-500 hover:border-opacity-50 focus:bg-grey-100 focus:border-blue-400 inline-block sm:text-sm border-grey-500 rounded-md rounded-tr-none rounded-br-none placeholder:text-gray-600"
224
+ debounceTimeout={300}
225
+ minLength={2}
226
+ value={searchTerm}
227
+ onChange={(e) => setSearchTerm(e.target.value)}
228
+ onClick={() => setOpen(true)}
229
+ onFocus={() => setOpen(true)}
230
+ onBlur={() => setOpen(false)}
231
+ onKeyDown={(e) => keyDownHandler(e)}
232
+ />
233
+ {searchTerm !== "" && (
234
+ <button
235
+ type="button"
236
+ className="absolute right-3 top-[50%] -translate-y-[50%] text-night-sky-800 hover:text-blue-500 transition-colors !bg-[transparent] cursor-pointer"
237
+ onClick={clearSearch}
238
+ >
239
+ <CircleCloseIcon className="fill-night-sky-800 hover:fill-blue-500 h-4 w-4" />
240
+ </button>
241
+ )}
242
+ </div>
243
+ <button
244
+ type="submit"
245
+ className="inline-flex transition-colors items-center justify-center px-4 h-[38px] font-medium rounded-md shadow-sm border border-blue-500 text-white bg-blue-500 hover:bg-blue-600 hover:border-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:text-sm rounded-tl-none rounded-bl-none"
246
+ >
247
+ <SearchIcon className="fill-white h-4 w-4" />
248
+ </button>
249
+ </form>
250
+ <div className="relative">
251
+ <CSSTransition
252
+ mountOnEnter
253
+ unmountOnExit
254
+ in={open}
255
+ timeout={300}
256
+ classNames="fade-slide"
257
+ data-testid="searchResults"
258
+ >
259
+ <div
260
+ className={classNames(
261
+ "search-results-container absolute top-0 left-1/2 -translate-x-1/2 mt-2 bg-white py-5 rounded-[1.25rem] z-50 shadow-lg border border-[#a6abbd] border-opacity-60 w-full tablet:w-[70vw] min-h-[8rem] overflow-hidden"
262
+ )}
263
+ >
264
+ <ErrorBoundary>
265
+ {loading && (
266
+ <Loader
267
+ data-testid="Loader"
268
+ className="absolute top-5 right-5"
269
+ />
270
+ )}
271
+ <div
272
+ className="px-6 h-full mb-12 max-h-[70vh] overflow-y-auto"
273
+ data-testid="Sections"
274
+ >
275
+ {hasError && (
276
+ <p>
277
+ Sorry, there was a problem. Please try your search again.
278
+ </p>
279
+ )}
280
+ {searchData.data &&
281
+ searchData.data.searchAutocomplete &&
282
+ searchData.data.searchAutocomplete!.map((section) => (
283
+ <SearchAutocompleteSection
284
+ key={section.title}
285
+ searchTerm={searchTerm}
286
+ title={section.title}
287
+ results={section.results}
288
+ />
289
+ ))}
290
+ </div>
291
+ </ErrorBoundary>
292
+ <div className="border-t border-[#a6abbd] border-opacity-60 flex items-center absolute bottom-0 left-0 w-full bg-white h-16">
293
+ <a
294
+ href={`/search?q=${searchTerm}`}
295
+ className="px-3 py-1 ml-6 flex items-center space-x-3 ring ring-1 ring-offset-2 ring-transparent transition-colors text-blue-500 hover:text-blue-600 rounded-lg autocomplete-custom-search"
296
+ >
297
+ <SearchIcon className="fill-blue-500 hover:fill-blue-600 h-4 w-4" />
298
+ <span className="font-semibold text-[1.125rem] ">
299
+ {searchTerm === "" ? "Custom Search" : "View All Results"}
300
+ </span>
301
+ </a>
302
+ </div>
303
+ </div>
304
+ </CSSTransition>
305
+ </div>
306
+ </div>
307
+ );
308
+ };
309
+
310
+ export default SearchAutocomplete;
@@ -0,0 +1,64 @@
1
+ .SearchAutocompleteSection {
2
+ @apply font-sofia-pro mb-6;
3
+ }
4
+
5
+ // Hide results over 5 on mobile
6
+ :global(.autocomplete-section a:nth-child(6)) {
7
+ @apply hidden tablet:inline-block;
8
+ }
9
+
10
+ :global(.autocomplete-section a:nth-child(7)) {
11
+ @apply hidden tablet:inline-block;
12
+ }
13
+
14
+ :global(.autocomplete-section a:nth-child(8)) {
15
+ @apply hidden tablet:inline-block;
16
+ }
17
+
18
+ :global(.autocomplete-section a:nth-child(9)) {
19
+ @apply hidden tablet:inline-block;
20
+ }
21
+
22
+ :global(.autocomplete-section a:nth-child(10)) {
23
+ @apply hidden tablet:inline-block;
24
+ }
25
+
26
+ :global(.autocomplete-section a:nth-child(11)) {
27
+ @apply hidden tablet:inline-block;
28
+ }
29
+
30
+ :global(.autocomplete-section a:nth-child(12)) {
31
+ @apply hidden tablet:inline-block;
32
+ }
33
+
34
+ :global(.autocomplete-section a:nth-child(13)) {
35
+ @apply hidden tablet:inline-block;
36
+ }
37
+
38
+ :global(.autocomplete-section a:nth-child(14)) {
39
+ @apply hidden tablet:inline-block;
40
+ }
41
+
42
+ :global(.autocomplete-section a:nth-child(15)) {
43
+ @apply hidden tablet:inline-block;
44
+ }
45
+
46
+ :global(.autocomplete-section a:nth-child(16)) {
47
+ @apply hidden tablet:inline-block;
48
+ }
49
+
50
+ :global(.autocomplete-section a:nth-child(17)) {
51
+ @apply hidden tablet:inline-block;
52
+ }
53
+
54
+ :global(.autocomplete-section a:nth-child(18)) {
55
+ @apply hidden tablet:inline-block;
56
+ }
57
+
58
+ :global(.autocomplete-section a:nth-child(19)) {
59
+ @apply hidden tablet:inline-block;
60
+ }
61
+
62
+ :global(.autocomplete-section a:nth-child(20)) {
63
+ @apply hidden tablet:inline-block;
64
+ }
@@ -0,0 +1,54 @@
1
+ import React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import "@testing-library/jest-dom/extend-expect";
4
+ import SearchAutocompleteSection, {
5
+ SearchAutocompleteSectionProps,
6
+ } from "./SearchAutocompleteSection";
7
+
8
+ // Search autocomplete section test block
9
+ describe("<SearchAutocompleteSection />", () => {
10
+ // Set component props
11
+ const props: SearchAutocompleteSectionProps = {
12
+ searchTerm: "Test",
13
+ title: "test",
14
+ results: [
15
+ {
16
+ title: "Tag 1",
17
+ url: "http://yahoo.com",
18
+ },
19
+ {
20
+ title: "Tag 2",
21
+ url: "http://google.com",
22
+ },
23
+ ],
24
+ };
25
+
26
+ // Component should mount
27
+ test("it should mount", async () => {
28
+ await render(<SearchAutocompleteSection {...props} />);
29
+ const searchAutocompleteSection = screen.getByTestId(
30
+ "searchAutocompleteSection"
31
+ );
32
+ expect(searchAutocompleteSection).toBeInTheDocument();
33
+ });
34
+
35
+ // Component should have correct title
36
+ test("it should have correct title", async () => {
37
+ await render(<SearchAutocompleteSection {...props} />);
38
+ const searchResultTitle = screen.getByTestId("searchResultTitle");
39
+ expect(searchResultTitle.textContent).toBe("test");
40
+ });
41
+
42
+ // Component should have a tag
43
+ test("it should have a tag", async () => {
44
+ await render(<SearchAutocompleteSection {...props} />);
45
+ const searchTag = screen.getByTestId("searchTag");
46
+ expect(searchTag).toBeInTheDocument();
47
+ });
48
+
49
+ // Component should have correct tags
50
+ test("it should have correct tags", async () => {
51
+ await render(<SearchAutocompleteSection {...props} />);
52
+ expect(screen.getAllByRole("link").length).toBe(2);
53
+ });
54
+ });
@@ -0,0 +1,74 @@
1
+ import classNames from "classnames";
2
+ import React, { FC } from "react";
3
+ import { Result } from "../../search-result";
4
+ import SearchAutocompleteTag from "../SearchAutocompleteTag/SearchAutocompleteTag";
5
+ import styles from "./SearchAutocompleteSection.module.scss";
6
+
7
+ export interface SearchAutocompleteSectionProps {
8
+ searchTerm: string;
9
+ title?: string;
10
+ results?: Result[];
11
+ }
12
+
13
+ // Custom limits based on section
14
+ const resultsLimit = (title: string) => {
15
+ switch (title.toLowerCase()) {
16
+ case "where to give now": {
17
+ return 3;
18
+ }
19
+ case "organizations": {
20
+ return 10;
21
+ }
22
+ case "causes": {
23
+ return 5;
24
+ }
25
+ default: {
26
+ return 20;
27
+ }
28
+ }
29
+ };
30
+
31
+ const SearchAutocompleteSection: FC<SearchAutocompleteSectionProps> = (
32
+ props
33
+ ) => (
34
+ <div
35
+ className={classNames(
36
+ styles.SearchAutocompleteSection,
37
+ "autocomplete-section"
38
+ )}
39
+ data-testid="searchAutocompleteSection"
40
+ >
41
+ <h2
42
+ className="text-night-sky-800 mb-2 text-base"
43
+ data-testid="searchResultTitle"
44
+ >
45
+ {props.title}
46
+ </h2>
47
+ <div className="flow-root">
48
+ <div
49
+ className="-m-1 flex flex-wrap autocomplete-section"
50
+ data-testid="searchTag"
51
+ >
52
+ {(props.results == null || props.results.length === 0) && (
53
+ <p className="pl-3">No results found</p>
54
+ )}
55
+ {props.results &&
56
+ props.results!.length > 0 &&
57
+ props.results
58
+ ?.slice(0, resultsLimit(props.title || ""))
59
+ .map((results) => (
60
+ <SearchAutocompleteTag
61
+ key={results.url}
62
+ section={props.title || ""}
63
+ searchTerm={props.searchTerm}
64
+ ein={results.ein}
65
+ name={results.title}
66
+ url={results.url}
67
+ />
68
+ ))}
69
+ </div>
70
+ </div>
71
+ </div>
72
+ );
73
+
74
+ export default SearchAutocompleteSection;
@@ -0,0 +1,6 @@
1
+ .SearchAutocompleteTag {
2
+ @apply inline-block font-sofia-pro font-normal text-base leading-6 tracking-normal text-left transition-colors text-night-sky-800 bg-grey-50 hover:bg-grey-100 rounded-lg pt-2 pb-2 pl-3 pr-3 m-1 ring ring-1 ring-offset-2 ring-transparent;
3
+ }
4
+ :global(.highlighted-text) {
5
+ @apply font-semibold;
6
+ }
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+ import "@testing-library/jest-dom/extend-expect";
4
+ import SearchAutocompleteTag, {
5
+ SearchAutocompleteTagProps,
6
+ } from "./SearchAutocompleteTag";
7
+
8
+ // Search autocomplete tag test block
9
+ describe("<SearchAutocompleteTag />", () => {
10
+ // Set component props
11
+ const props: SearchAutocompleteTagProps = {
12
+ section: "Test",
13
+ searchTerm: "Tes",
14
+ name: "Test",
15
+ url: "http://google.com",
16
+ };
17
+
18
+ // Component should mount
19
+ test("it should mount", async () => {
20
+ await render(<SearchAutocompleteTag {...props} />);
21
+ const searchAutocompleteTag = screen.getByTestId("searchAutocompleteTag");
22
+ expect(searchAutocompleteTag).toBeInTheDocument();
23
+ });
24
+
25
+ // Component should have correct name
26
+ test("it should have correct name", async () => {
27
+ await render(<SearchAutocompleteTag {...props} />);
28
+ const searchAutocompleteTag = screen.getByTestId("searchAutocompleteTag");
29
+ expect(searchAutocompleteTag.textContent).toBe("Test");
30
+ });
31
+
32
+ // Component should have correct url
33
+ test("it should have correct url", async () => {
34
+ await render(<SearchAutocompleteTag {...props} />);
35
+ const searchAutocompleteTag = screen.getByTestId("searchAutocompleteTag");
36
+ expect(searchAutocompleteTag.getAttribute("href")).toBe(
37
+ "http://google.com"
38
+ );
39
+ });
40
+
41
+ // Component should have correct highlighted text
42
+ test("it should have correct highlighted text", async () => {
43
+ await render(<SearchAutocompleteTag {...props} />);
44
+ const highlightedText = screen.getByText("Tes");
45
+ expect(highlightedText).toHaveClass("highlighted-text");
46
+ });
47
+
48
+ // Component should trigger tracking event
49
+ test("it should trigger tracking event", async () => {
50
+ // TODO: Write test for tracking event
51
+ });
52
+ });
@@ -0,0 +1,67 @@
1
+ import React, { FC } from "react";
2
+ import styles from "./SearchAutocompleteTag.module.scss";
3
+ import Highlighter from "react-highlight-words";
4
+ import classNames from "classnames";
5
+
6
+ export interface SearchAutocompleteTagProps {
7
+ section: string;
8
+ searchTerm: string;
9
+ ein?: string;
10
+ name: string;
11
+ url?: string;
12
+ }
13
+
14
+ const Highlight = ({ children, highlightIndex }: any) => (
15
+ <span className="highlighted-text">{children}</span>
16
+ );
17
+
18
+ const SearchAutocompleteTag: FC<SearchAutocompleteTagProps> = (props) => {
19
+ // Custom event trigger
20
+ const triggerCustomEvent = (e: any) => {
21
+ e.preventDefault();
22
+
23
+ // Create custom event
24
+ var event = new CustomEvent("autocomplete-term-clicked", {
25
+ detail: {
26
+ name: props.name,
27
+ searchTerm: props.searchTerm,
28
+ section: props.section,
29
+ url: props.url,
30
+ },
31
+ });
32
+
33
+ // Trigger custom event
34
+ document.dispatchEvent(event);
35
+
36
+ if (props.url) {
37
+ window.location.href = props.url;
38
+ }
39
+ };
40
+
41
+ return (
42
+ <a
43
+ key={props.ein}
44
+ href={props.url}
45
+ className={classNames(
46
+ styles.SearchAutocompleteTag,
47
+ "autocomplete-tag"
48
+ // ? 'bg-grey-100' : ''
49
+ )}
50
+ data-section={props.section}
51
+ data-testid="searchAutocompleteTag"
52
+ onClick={(e) => triggerCustomEvent(e)}
53
+ >
54
+ {props.searchTerm === "" ? (
55
+ <span className="highlighted-text">{props.name}</span>
56
+ ) : (
57
+ <Highlighter
58
+ highlightTag={Highlight}
59
+ searchWords={[props.searchTerm]}
60
+ textToHighlight={props.name}
61
+ />
62
+ )}
63
+ </a>
64
+ );
65
+ };
66
+
67
+ export default SearchAutocompleteTag;
@@ -0,0 +1,4 @@
1
+ import SearchAutocomplete from "./SearchAutocomplete/SearchAutocomplete";
2
+ import "../index.css";
3
+
4
+ export { SearchAutocomplete };