UNPKG

3.9 kBTypeScriptView Raw
1import React, { memo, forwardRef, type CSSProperties } from "react";
2import { symToStr } from "tsafe/symToStr";
3import { assert } from "tsafe/assert";
4import { createComponentI18nApi } from "../i18n";
5import type { Equals } from "tsafe";
6import { cx } from "../tools/cx";
7import { fr } from "../fr";
8import { SearchButton } from "./SearchButton";
9import { useAnalyticsId } from "../tools/useAnalyticsId";
10import "../assets/search-bar.css";
11
12export type SearchBarProps = {
13 className?: string;
14 id?: string;
15 /** Default: "Rechercher" (or translation) */
16 label?: string;
17 /** Default: false */
18 big?: boolean;
19 classes?: Partial<Record<"root" | "label", string>>;
20 style?: CSSProperties;
21 renderInput?: (
22 /**
23 * id and name must be forwarded to the <input /> component
24 * the others params can, but it's not mandatory.
25 **/
26 params: {
27 id: string;
28 type: "search";
29 className: string;
30 placeholder: string;
31 }
32 ) => JSX.Element;
33 /** Default: false */
34 clearInputOnSearch?: boolean;
35 /** Default: false */
36 allowEmptySearch?: boolean;
37 onButtonClick?: (text: string) => void;
38};
39
40/**
41 * @see <https://components.react-dsfr.codegouv.studio/?path=/docs/components-input>
42 * */
43export const SearchBar = memo(
44 forwardRef<HTMLDivElement, SearchBarProps>((props, ref) => {
45 const {
46 className,
47 id: id_props,
48 label: label_props,
49 big = false,
50 classes = {},
51 style,
52 renderInput = ({ className, id, placeholder, type }) => (
53 <input className={className} id={id} placeholder={placeholder} type={type} />
54 ),
55 clearInputOnSearch = false,
56 allowEmptySearch = false,
57 onButtonClick,
58 ...rest
59 } = props;
60
61 assert<Equals<keyof typeof rest, never>>();
62
63 const { t } = useTranslation();
64
65 const label = label_props ?? t("label");
66
67 const id = useAnalyticsId({
68 "defaultIdPrefix": "fr-search-bar",
69 "explicitlyProvidedId": id_props
70 });
71
72 const inputId = `search-${id}-input`;
73
74 return (
75 <div
76 id={id}
77 className={cx(
78 fr.cx("fr-search-bar", big && "fr-search-bar--lg"),
79 classes.root,
80 className
81 )}
82 role="search"
83 ref={ref}
84 style={style}
85 {...rest}
86 >
87 <label className={cx(fr.cx("fr-label"), classes.label)} htmlFor={inputId}>
88 {label}
89 </label>
90 {/* NOTE: It is crucial that renderInput be called
91 one time and only one time in each render to allow useState to be used inline*/}
92 {renderInput({
93 "className": fr.cx("fr-input"),
94 "placeholder": label,
95 "type": "search",
96 "id": inputId
97 })}
98 <SearchButton
99 clearInputOnSearch={clearInputOnSearch}
100 id={`${id}-button`}
101 searchInputId={inputId}
102 allowEmptySearch={allowEmptySearch}
103 onClick={onButtonClick}
104 />
105 </div>
106 );
107 })
108);
109
110SearchBar.displayName = symToStr({ SearchBar });
111
112export default SearchBar;
113
114export const { useTranslation, addSearchBarTranslations } = createComponentI18nApi({
115 "componentName": symToStr({ SearchBar }),
116 "frMessages": {
117 /* spell-checker: disable */
118 "label": "Rechercher"
119 /* spell-checker: enable */
120 }
121});
122
123addSearchBarTranslations({
124 "lang": "en",
125 "messages": {
126 "label": "Search"
127 }
128});
129
\No newline at end of file