UNPKG

5.2 kBTypeScriptView Raw
1"use client";
2
3import React, { useEffect, useState, useReducer } from "react";
4import { useTranslation } from "./SearchBar";
5import { fr } from "../fr";
6import { assert } from "tsafe/assert";
7import { is } from "tsafe/is";
8import { useConstCallback } from "../tools/powerhooks/useConstCallback";
9import { observeInputValue } from "../tools/observeInputValue";
10import { id } from "tsafe/id";
11
12export type SearchButtonProps = {
13 id: string;
14 searchInputId: string;
15 clearInputOnSearch: boolean;
16 allowEmptySearch: boolean;
17 onClick: ((text: string) => void) | undefined;
18};
19
20export function SearchButton(props: SearchButtonProps) {
21 const {
22 searchInputId,
23 clearInputOnSearch,
24 allowEmptySearch,
25 onClick: onClick_props,
26 id: id_props
27 } = props;
28
29 const { t } = useTranslation();
30
31 const [, forceUpdate] = useReducer(x => x + 1, 0);
32
33 const [{ focusInputElement, getInputValue, resetInputValue, getIsInputFocused }, setInputApi] =
34 useState(() => ({
35 "getInputValue": id<() => string>(() => ""),
36 "resetInputValue": id<() => void>(() => {
37 /* do nothing */
38 }),
39 "focusInputElement": id<() => void>(() => {
40 /* do nothing */
41 }),
42 "getIsInputFocused": id<() => boolean>(() => false)
43 }));
44
45 const onClick = useConstCallback(() => {
46 const inputValue = getInputValue();
47
48 if (!allowEmptySearch && inputValue === "") {
49 focusInputElement();
50 return;
51 }
52
53 onClick_props?.(inputValue);
54 if (clearInputOnSearch) {
55 resetInputValue();
56 }
57 });
58
59 const isControlledByUser = onClick_props === undefined;
60
61 useEffect(() => {
62 const inputElement = document.getElementById(searchInputId);
63
64 assert(inputElement !== null, `${searchInputId} should be mounted`);
65 assert(
66 "value" in inputElement && typeof inputElement.value === "string",
67 `${searchInputId} is not an HTML input`
68 );
69
70 assert(is<HTMLInputElement>(inputElement));
71
72 setInputApi({
73 "focusInputElement": () => inputElement.focus(),
74 "getInputValue": () => inputElement.value,
75 "resetInputValue": () => (inputElement.value = ""),
76 "getIsInputFocused": () => document.activeElement === inputElement
77 });
78
79 const cleanups: (() => void)[] = [];
80
81 {
82 const { cleanup } = observeInputValue({
83 inputElement,
84 "callback": () => forceUpdate()
85 });
86
87 cleanups.push(cleanup);
88 }
89
90 if (isControlledByUser) {
91 inputElement.addEventListener(
92 "focus",
93 (() => {
94 const callback = () => forceUpdate();
95
96 cleanups.push(() => inputElement.removeEventListener("focus", callback));
97
98 return callback;
99 })()
100 );
101
102 inputElement.addEventListener(
103 "blur",
104 (() => {
105 const callback = () => forceUpdate();
106
107 cleanups.push(() => inputElement.removeEventListener("blur", callback));
108
109 return callback;
110 })()
111 );
112 }
113
114 if (!isControlledByUser) {
115 inputElement.addEventListener(
116 "keydown",
117 (() => {
118 const callback = (event: KeyboardEvent): void => {
119 if (event.key !== "Enter") {
120 return;
121 }
122
123 onClick();
124 inputElement.blur();
125 };
126
127 cleanups.push(() => inputElement.removeEventListener("keydown", callback));
128
129 return callback;
130 })()
131 );
132
133 inputElement.addEventListener(
134 "keydown",
135 (() => {
136 const callback = (event: KeyboardEvent) => {
137 if (event.key !== "Escape") {
138 return;
139 }
140
141 inputElement.blur();
142 };
143
144 cleanups.push(() => inputElement.removeEventListener("keydown", callback));
145
146 return callback;
147 })()
148 );
149 }
150
151 return () => {
152 cleanups.forEach(cleanup => cleanup());
153 };
154 }, [searchInputId, isControlledByUser]);
155
156 if (onClick_props === undefined && (getIsInputFocused() || getInputValue() !== "")) {
157 return null;
158 }
159
160 return (
161 <button
162 id={id_props}
163 className={fr.cx("fr-btn")}
164 title={t("label")}
165 onClick={onClick}
166 style={
167 onClick_props !== undefined
168 ? undefined
169 : {
170 "position": "absolute",
171 "right": 0
172 }
173 }
174 >
175 {t("label")}
176 </button>
177 );
178}
179
\No newline at end of file