@tensor_1/react 2.24.0 → 2.25.0
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.
- package/README.md +17 -0
- package/dist/components/Card.d.ts +8 -1
- package/dist/components/Card.js +8 -2
- package/dist/components/EditorialHome.d.ts +9 -2
- package/dist/components/EditorialHome.js +8 -2
- package/dist/components/Field.d.ts +9 -1
- package/dist/components/Field.js +8 -2
- package/dist/components/InfoTip.d.ts +29 -0
- package/dist/components/InfoTip.js +185 -0
- package/dist/components/PageHeader.d.ts +9 -2
- package/dist/components/PageHeader.js +8 -2
- package/dist/components/SearchSuggest.d.ts +87 -0
- package/dist/components/SearchSuggest.js +299 -0
- package/dist/index.cjs +1783 -1203
- package/dist/index.d.cts +163 -8
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/info-tip-model.d.ts +10 -0
- package/dist/info-tip-model.js +8 -0
- package/dist/search-suggest-model.d.ts +20 -0
- package/dist/search-suggest-model.js +74 -0
- package/package.json +1 -1
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { cx } from "../cx.js";
|
|
5
|
+
import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
|
|
6
|
+
import { searchSegments } from "../search-suggest-model.js";
|
|
7
|
+
import { Spinner } from "./Spinner.js";
|
|
8
|
+
const SearchIcon = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
9
|
+
/* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "7" }),
|
|
10
|
+
/* @__PURE__ */ jsx("path", { d: "m20 20-3.5-3.5" })
|
|
11
|
+
] });
|
|
12
|
+
const ClearIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) });
|
|
13
|
+
function Emphasised({ text, query }) {
|
|
14
|
+
return /* @__PURE__ */ jsx(Fragment, { children: searchSegments(text, query).map(
|
|
15
|
+
(segment, index) => segment.match ? /* @__PURE__ */ jsx("mark", { className: "uix-search-suggest__match", children: segment.text }, index) : /* @__PURE__ */ jsx("span", { children: segment.text }, index)
|
|
16
|
+
) });
|
|
17
|
+
}
|
|
18
|
+
const keepFocus = (event) => event.preventDefault();
|
|
19
|
+
const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
|
|
20
|
+
function SearchSuggest({
|
|
21
|
+
value,
|
|
22
|
+
onValueChange,
|
|
23
|
+
options,
|
|
24
|
+
onSelect,
|
|
25
|
+
label,
|
|
26
|
+
placeholder,
|
|
27
|
+
highlight,
|
|
28
|
+
heading,
|
|
29
|
+
headingAction,
|
|
30
|
+
footer,
|
|
31
|
+
loading = false,
|
|
32
|
+
loadingLabel,
|
|
33
|
+
empty,
|
|
34
|
+
error,
|
|
35
|
+
shortcutHint,
|
|
36
|
+
clearLabel = "Clear search",
|
|
37
|
+
status,
|
|
38
|
+
size = "md",
|
|
39
|
+
open: openProp,
|
|
40
|
+
strategy = "absolute",
|
|
41
|
+
onOpenChange,
|
|
42
|
+
inputRef,
|
|
43
|
+
autoFocus,
|
|
44
|
+
name,
|
|
45
|
+
className,
|
|
46
|
+
onBlur,
|
|
47
|
+
...props
|
|
48
|
+
}) {
|
|
49
|
+
const id = useId();
|
|
50
|
+
const listboxId = `${id}-listbox`;
|
|
51
|
+
const headingId = `${id}-heading`;
|
|
52
|
+
const rootRef = useRef(null);
|
|
53
|
+
const fieldRef = useRef(null);
|
|
54
|
+
useImperativeHandle(inputRef, () => fieldRef.current, []);
|
|
55
|
+
const [openState, setOpenState] = useState(false);
|
|
56
|
+
const open = openProp ?? openState;
|
|
57
|
+
const setOpen = (next) => {
|
|
58
|
+
if (openProp === void 0) setOpenState(next);
|
|
59
|
+
if (next !== open) onOpenChange?.(next);
|
|
60
|
+
};
|
|
61
|
+
const [focused, setFocused] = useState(false);
|
|
62
|
+
const [activeState, setActive] = useState(-1);
|
|
63
|
+
const itemCount = options.length + (footer ? 1 : 0);
|
|
64
|
+
const active = activeState < itemCount ? activeState : -1;
|
|
65
|
+
const optionKey = options.map((option) => option.id).join("|");
|
|
66
|
+
useEffect(() => setActive(-1), [optionKey]);
|
|
67
|
+
const hasText = value.trim().length > 0;
|
|
68
|
+
const showEmpty = hasText && !loading && !error && options.length === 0 && empty != null;
|
|
69
|
+
const showLoading = loading && options.length === 0;
|
|
70
|
+
const visible = open && (itemCount > 0 || showLoading || showEmpty || error != null);
|
|
71
|
+
const optionId = (index) => `${id}-option-${index}`;
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!visible || active < 0) return;
|
|
74
|
+
const node = document.getElementById(optionId(active));
|
|
75
|
+
node?.scrollIntoView?.({ block: "nearest" });
|
|
76
|
+
}, [active, visible]);
|
|
77
|
+
const choose = (index) => {
|
|
78
|
+
const option = options[index];
|
|
79
|
+
if (option) onSelect(option.id, option);
|
|
80
|
+
else if (footer && index === options.length) footer.onSelect();
|
|
81
|
+
else return;
|
|
82
|
+
setOpen(false);
|
|
83
|
+
setActive(-1);
|
|
84
|
+
};
|
|
85
|
+
const handleKeyDown = (event) => {
|
|
86
|
+
if (event.nativeEvent.isComposing || event.keyCode === 229) return;
|
|
87
|
+
switch (event.key) {
|
|
88
|
+
case "ArrowDown":
|
|
89
|
+
case "ArrowUp": {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
if (!itemCount) {
|
|
92
|
+
setOpen(true);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const down = event.key === "ArrowDown";
|
|
96
|
+
if (!visible) {
|
|
97
|
+
setOpen(true);
|
|
98
|
+
setActive(down ? 0 : itemCount - 1);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
setActive((stored) => {
|
|
102
|
+
const current = stored < itemCount ? stored : -1;
|
|
103
|
+
if (current < 0) return down ? 0 : itemCount - 1;
|
|
104
|
+
return (current + (down ? 1 : -1) + itemCount) % itemCount;
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
case "Home":
|
|
109
|
+
case "End":
|
|
110
|
+
if (visible && active >= 0 && itemCount) {
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
setActive(event.key === "Home" ? 0 : itemCount - 1);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
case "Enter":
|
|
116
|
+
if (visible && active >= 0) {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
choose(active);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (visible && hasText && options.length) {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
choose(0);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
case "Escape":
|
|
127
|
+
if (visible) {
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
event.stopPropagation();
|
|
130
|
+
setOpen(false);
|
|
131
|
+
setActive(-1);
|
|
132
|
+
} else if (value) {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
event.stopPropagation();
|
|
135
|
+
onValueChange("");
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
default:
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const handleBlur = (event) => {
|
|
142
|
+
onBlur?.(event);
|
|
143
|
+
if (!rootRef.current?.contains(event.relatedTarget)) {
|
|
144
|
+
setFocused(false);
|
|
145
|
+
setOpen(false);
|
|
146
|
+
setActive(-1);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const clear = () => {
|
|
150
|
+
onValueChange("");
|
|
151
|
+
setActive(-1);
|
|
152
|
+
fieldRef.current?.focus();
|
|
153
|
+
};
|
|
154
|
+
const activeId = visible && active >= 0 ? optionId(active) : void 0;
|
|
155
|
+
const fieldBoxRef = useRef(null);
|
|
156
|
+
const popupRef = useRef(null);
|
|
157
|
+
const floating = strategy === "fixed";
|
|
158
|
+
useIsomorphicLayoutEffect(() => {
|
|
159
|
+
const popup = popupRef.current;
|
|
160
|
+
const box = fieldBoxRef.current;
|
|
161
|
+
if (!floating || !visible || !popup || !box) return;
|
|
162
|
+
const size2 = () => {
|
|
163
|
+
popup.style.width = `${box.offsetWidth}px`;
|
|
164
|
+
};
|
|
165
|
+
size2();
|
|
166
|
+
window.addEventListener("resize", size2);
|
|
167
|
+
return () => window.removeEventListener("resize", size2);
|
|
168
|
+
}, [floating, visible]);
|
|
169
|
+
useAnchoredPosition(floating ? fieldBoxRef : null, popupRef, { open: floating && visible, placement: "bottom-start", offset: 4 });
|
|
170
|
+
return /* @__PURE__ */ jsxs(
|
|
171
|
+
"div",
|
|
172
|
+
{
|
|
173
|
+
ref: rootRef,
|
|
174
|
+
className: cx("uix-search-suggest", size === "lg" && "uix-search-suggest--lg", className),
|
|
175
|
+
"data-open": visible || void 0,
|
|
176
|
+
onBlur: handleBlur,
|
|
177
|
+
...props,
|
|
178
|
+
children: [
|
|
179
|
+
/* @__PURE__ */ jsxs("div", { ref: fieldBoxRef, className: "uix-search-suggest__field", children: [
|
|
180
|
+
/* @__PURE__ */ jsx("span", { className: "uix-search-suggest__lead", "aria-hidden": "true", children: /* @__PURE__ */ jsx(SearchIcon, {}) }),
|
|
181
|
+
/* @__PURE__ */ jsx(
|
|
182
|
+
"input",
|
|
183
|
+
{
|
|
184
|
+
ref: fieldRef,
|
|
185
|
+
className: "uix-input uix-search-suggest__input",
|
|
186
|
+
type: "text",
|
|
187
|
+
role: "combobox",
|
|
188
|
+
"aria-label": label,
|
|
189
|
+
"aria-autocomplete": "list",
|
|
190
|
+
"aria-expanded": visible,
|
|
191
|
+
"aria-controls": listboxId,
|
|
192
|
+
"aria-activedescendant": activeId,
|
|
193
|
+
"aria-busy": loading || void 0,
|
|
194
|
+
autoComplete: "off",
|
|
195
|
+
spellCheck: false,
|
|
196
|
+
enterKeyHint: "search",
|
|
197
|
+
name,
|
|
198
|
+
value,
|
|
199
|
+
placeholder,
|
|
200
|
+
autoFocus,
|
|
201
|
+
onChange: (event) => {
|
|
202
|
+
onValueChange(event.target.value);
|
|
203
|
+
setActive(-1);
|
|
204
|
+
setOpen(true);
|
|
205
|
+
},
|
|
206
|
+
onFocus: () => {
|
|
207
|
+
setFocused(true);
|
|
208
|
+
setOpen(true);
|
|
209
|
+
},
|
|
210
|
+
onClick: () => setOpen(true),
|
|
211
|
+
onKeyDown: handleKeyDown
|
|
212
|
+
}
|
|
213
|
+
),
|
|
214
|
+
/* @__PURE__ */ jsxs("span", { className: "uix-search-suggest__trail", children: [
|
|
215
|
+
loading && /* @__PURE__ */ jsx(Spinner, { className: "uix-search-suggest__spinner", label: loadingLabel }),
|
|
216
|
+
!loading && value && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-search-suggest__clear", "aria-label": clearLabel, onMouseDown: keepFocus, onClick: clear, children: /* @__PURE__ */ jsx(ClearIcon, {}) }),
|
|
217
|
+
!loading && !value && !focused && shortcutHint && /* @__PURE__ */ jsx("kbd", { className: "uix-kbd", "aria-hidden": "true", children: shortcutHint })
|
|
218
|
+
] })
|
|
219
|
+
] }),
|
|
220
|
+
/* @__PURE__ */ jsxs("div", { ref: popupRef, className: "uix-search-suggest__popup", "data-strategy": floating ? "fixed" : void 0, hidden: !visible, children: [
|
|
221
|
+
heading && options.length > 0 && /* @__PURE__ */ jsxs("div", { className: "uix-search-suggest__header", children: [
|
|
222
|
+
/* @__PURE__ */ jsx("span", { className: "uix-search-suggest__heading", id: headingId, children: heading }),
|
|
223
|
+
headingAction && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-search-suggest__header-action", onMouseDown: keepFocus, onClick: headingAction.onSelect, children: headingAction.label })
|
|
224
|
+
] }),
|
|
225
|
+
/* @__PURE__ */ jsxs(
|
|
226
|
+
"ul",
|
|
227
|
+
{
|
|
228
|
+
id: listboxId,
|
|
229
|
+
role: "listbox",
|
|
230
|
+
className: "uix-search-suggest__list",
|
|
231
|
+
"aria-label": heading && options.length > 0 ? void 0 : label,
|
|
232
|
+
"aria-labelledby": heading && options.length > 0 ? headingId : void 0,
|
|
233
|
+
"aria-busy": loading || void 0,
|
|
234
|
+
children: [
|
|
235
|
+
options.map((option, index) => {
|
|
236
|
+
const crumbs = option.meta ?? [];
|
|
237
|
+
const descId = option.description ? `${optionId(index)}-desc` : void 0;
|
|
238
|
+
return /* @__PURE__ */ jsxs(
|
|
239
|
+
"li",
|
|
240
|
+
{
|
|
241
|
+
id: optionId(index),
|
|
242
|
+
role: "option",
|
|
243
|
+
"aria-selected": index === active,
|
|
244
|
+
"aria-label": crumbs.length ? `${option.title}, ${crumbs.join(" \u203A ")}` : option.title,
|
|
245
|
+
"aria-describedby": descId,
|
|
246
|
+
className: "uix-search-suggest__option",
|
|
247
|
+
"data-active": index === active || void 0,
|
|
248
|
+
onMouseDown: keepFocus,
|
|
249
|
+
onMouseMove: () => {
|
|
250
|
+
if (active !== index) setActive(index);
|
|
251
|
+
},
|
|
252
|
+
onClick: () => choose(index),
|
|
253
|
+
children: [
|
|
254
|
+
option.icon != null && /* @__PURE__ */ jsx("span", { className: "uix-search-suggest__icon", "aria-hidden": "true", children: option.icon }),
|
|
255
|
+
/* @__PURE__ */ jsxs("span", { className: "uix-search-suggest__body", children: [
|
|
256
|
+
/* @__PURE__ */ jsx("span", { className: "uix-search-suggest__title", children: /* @__PURE__ */ jsx(Emphasised, { text: option.title, query: highlight ?? value }) }),
|
|
257
|
+
crumbs.length > 0 && /* @__PURE__ */ jsx("span", { className: "uix-search-suggest__meta", title: crumbs.join(" \u203A "), children: crumbs.map((crumb, crumbIndex) => /* @__PURE__ */ jsx("span", { className: "uix-search-suggest__crumb", children: crumb }, crumbIndex)) }),
|
|
258
|
+
option.description && /* @__PURE__ */ jsx("span", { id: descId, className: "uix-search-suggest__desc", children: /* @__PURE__ */ jsx(Emphasised, { text: option.description, query: highlight ?? value }) })
|
|
259
|
+
] })
|
|
260
|
+
]
|
|
261
|
+
},
|
|
262
|
+
option.id
|
|
263
|
+
);
|
|
264
|
+
}),
|
|
265
|
+
footer && /* @__PURE__ */ jsx(
|
|
266
|
+
"li",
|
|
267
|
+
{
|
|
268
|
+
id: optionId(options.length),
|
|
269
|
+
role: "option",
|
|
270
|
+
"aria-selected": active === options.length,
|
|
271
|
+
className: "uix-search-suggest__footer",
|
|
272
|
+
"data-active": active === options.length || void 0,
|
|
273
|
+
onMouseDown: keepFocus,
|
|
274
|
+
onMouseMove: () => {
|
|
275
|
+
if (active !== options.length) setActive(options.length);
|
|
276
|
+
},
|
|
277
|
+
onClick: () => choose(options.length),
|
|
278
|
+
children: footer.label
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
),
|
|
284
|
+
showLoading && // The field's spinner is the announced status; this row is the visual echo in the list.
|
|
285
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-search-suggest__state", "aria-hidden": "true", children: [
|
|
286
|
+
/* @__PURE__ */ jsx("span", { className: "uix-spinner" }),
|
|
287
|
+
loadingLabel && /* @__PURE__ */ jsx("span", { children: loadingLabel })
|
|
288
|
+
] }),
|
|
289
|
+
error != null && options.length === 0 && !loading && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__state", children: error }),
|
|
290
|
+
showEmpty && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__state", children: empty })
|
|
291
|
+
] }),
|
|
292
|
+
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", "aria-live": "polite", children: status ?? "" })
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
export {
|
|
298
|
+
SearchSuggest
|
|
299
|
+
};
|