@vectara/vectara-ui 9.9.0 → 9.10.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/lib/components/index.d.ts +2 -1
- package/lib/components/searchInput/SearchInput.d.ts +3 -1
- package/lib/components/searchInput/SearchInput.js +114 -3
- package/lib/components/searchInput/SearchInputSuggestions.d.ts +10 -0
- package/lib/components/searchInput/SearchInputSuggestions.js +4 -0
- package/lib/components/searchInput/_index.scss +49 -0
- package/lib/components/searchInput/types.d.ts +4 -0
- package/lib/components/searchInput/types.js +1 -0
- package/lib/styles/index.css +43 -0
- package/package.json +1 -1
|
@@ -58,6 +58,7 @@ import { PROGRESS_BAR_COLOR, VuiProgressBar } from "./progressBar/ProgressBar";
|
|
|
58
58
|
import { VuiPrompt } from "./prompt/Prompt";
|
|
59
59
|
import { VuiScreenBlock } from "./screenBlock/ScreenBlock";
|
|
60
60
|
import { VuiSearchInput } from "./searchInput/SearchInput";
|
|
61
|
+
import { SearchSuggestion } from "./searchInput/types";
|
|
61
62
|
import { SearchResult, VuiSearchResult } from "./searchResult/SearchResult";
|
|
62
63
|
import { VuiSearchSelect } from "./searchSelect/SearchSelect";
|
|
63
64
|
import { VuiSetting } from "./setting/Setting";
|
|
@@ -84,5 +85,5 @@ import { VuiToggle } from "./toggle/Toggle";
|
|
|
84
85
|
import { VuiTooltip } from "./tooltip/Tooltip";
|
|
85
86
|
import { VuiTopicButton } from "./topicButton/TopicButton";
|
|
86
87
|
import { copyToClipboard } from "../utils/copyToClipboard";
|
|
87
|
-
export type { AnchorSide, AppContentPadding, ButtonColor, CalloutColor, ChatLanguage, ChatStyle, ChatTurn, CodeLanguage, InfoListType, InfoTableColumnAlign, InfoTableRow, InfoTableRowType, LinkProps, MenuItem, OptionListItem, Pagination, RadioButtonConfig, SearchResult, Sections, SectionItem, Stat, StepStatus, StepSize, TabSize, Tree, TreeItem, VuiStepProps };
|
|
88
|
+
export type { AnchorSide, AppContentPadding, ButtonColor, CalloutColor, ChatLanguage, ChatStyle, ChatTurn, CodeLanguage, InfoListType, InfoTableColumnAlign, InfoTableRow, InfoTableRowType, LinkProps, MenuItem, OptionListItem, Pagination, RadioButtonConfig, SearchResult, SearchSuggestion, Sections, SectionItem, Stat, StepStatus, StepSize, TabSize, Tree, TreeItem, VuiStepProps };
|
|
88
89
|
export { BADGE_COLOR, BUTTON_COLOR, BUTTON_SIZE, CALLOUT_COLOR, CALLOUT_SIZE, ICON_COLOR, ICON_SIZE, ICON_TYPE, PROGRESS_BAR_COLOR, SPACER_SIZE, SPINNER_COLOR, SPINNER_SIZE, TAB_SIZE, TEXT_COLOR, TEXT_SIZE, TITLE_SIZE, addNotification, copyToClipboard, VuiAccordion, VuiAccountButton, VuiAppContent, VuiAppHeader, VuiAppLayout, VuiAppSideNav, VuiAppSideNavLink, VuiBadge, VuiButtonPrimary, VuiButtonSecondary, VuiButtonTertiary, VuiIconButton, VuiCallout, VuiCard, VuiChat, VuiCheckbox, VuiCode, VuiContextProvider, VuiCopyButton, VuiDatePicker, VuiDateRangePicker, VuiDrawer, VuiErrorBoundary, VuiFlexContainer, VuiFlexItem, VuiFormGroup, VuiGrid, VuiHorizontalRule, VuiIcon, VuiInfoList, VuiInfoMenu, VuiInfoTable, VuiInProgress, VuiItemsInput, VuiLabel, VuiLink, VuiLinkInternal, VuiList, VuiMenu, VuiMenuItem, VuiModal, VuiNotifications, VuiNumberInput, VuiOptionsButton, VuiOptionsList, VuiOptionsListItem, VuiPagination, VuiPanel, VuiPasswordInput, VuiPopover, VuiPortal, VuiProgressBar, VuiPrompt, VuiRadioButton, VuiScreenBlock, VuiSearchInput, VuiSearchResult, VuiSearchSelect, VuiSelect, VuiSetting, VuiSpacer, VuiSpinner, VuiStatList, VuiStatus, VuiSteps, VuiSummary, VuiSummaryCitation, VuiSuperRadioGroup, VuiTable, VuiTab, VuiTabbedRoutes, VuiTabs, VuiText, VuiTextArea, VuiTextColor, VuiTextInput, VuiTimeline, VuiTimelineItem, VuiTitle, VuiToggle, VuiTooltip, VuiTopicButton };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ChangeEventHandler, FormEventHandler } from "react";
|
|
2
|
+
import { SearchSuggestion } from "./types";
|
|
2
3
|
declare const SIZE: readonly ["m", "l"];
|
|
3
4
|
type Props = {
|
|
4
5
|
className?: string;
|
|
@@ -8,6 +9,7 @@ type Props = {
|
|
|
8
9
|
placeholder?: string;
|
|
9
10
|
autoFocus?: boolean;
|
|
10
11
|
onSubmit?: FormEventHandler;
|
|
12
|
+
suggestions?: SearchSuggestion[];
|
|
11
13
|
};
|
|
12
14
|
type ClearableProps = {
|
|
13
15
|
isClearable?: true;
|
|
@@ -16,5 +18,5 @@ type ClearableProps = {
|
|
|
16
18
|
isClearable?: false;
|
|
17
19
|
onClear?: never;
|
|
18
20
|
};
|
|
19
|
-
export declare const VuiSearchInput: ({ className, size, value, onChange, placeholder, autoFocus, onSubmit, isClearable, onClear, ...rest }: Props & ClearableProps) => import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export declare const VuiSearchInput: ({ className, size, value, onChange, placeholder, autoFocus, onSubmit, isClearable, onClear, suggestions, ...rest }: Props & ClearableProps) => import("react/jsx-runtime").JSX.Element;
|
|
20
22
|
export {};
|
|
@@ -10,16 +10,127 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
10
10
|
return t;
|
|
11
11
|
};
|
|
12
12
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
|
+
import { useRef, useState, useEffect, useMemo } from "react";
|
|
13
14
|
import classNames from "classnames";
|
|
14
15
|
import { VuiIconButton } from "../button/IconButton";
|
|
15
16
|
import { BiX } from "react-icons/bi";
|
|
16
17
|
import { VuiIcon } from "../icon/Icon";
|
|
18
|
+
import { VuiSearchInputSuggestions } from "./SearchInputSuggestions";
|
|
19
|
+
import { createId } from "../../utils/createId";
|
|
17
20
|
const SIZE = ["m", "l"];
|
|
18
21
|
export const VuiSearchInput = (_a) => {
|
|
19
|
-
var { className, size = "m", value, onChange, placeholder, autoFocus, onSubmit, isClearable, onClear } = _a, rest = __rest(_a, ["className", "size", "value", "onChange", "placeholder", "autoFocus", "onSubmit", "isClearable", "onClear"]);
|
|
22
|
+
var { className, size = "m", value, onChange, placeholder, autoFocus, onSubmit, isClearable, onClear, suggestions } = _a, rest = __rest(_a, ["className", "size", "value", "onChange", "placeholder", "autoFocus", "onSubmit", "isClearable", "onClear", "suggestions"]);
|
|
20
23
|
const classes = classNames("vuiSearchInput", `vuiSearchInput--${size}`, className);
|
|
21
|
-
|
|
24
|
+
const inputRef = useRef(null);
|
|
25
|
+
const containerRef = useRef(null);
|
|
26
|
+
const [areSuggestionsVisible, setAreSuggestionsVisible] = useState(true);
|
|
27
|
+
const suggestionRefs = useRef([]);
|
|
28
|
+
const suppressNextFocus = useRef(false);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const handleClickOutside = (event) => {
|
|
31
|
+
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
|
32
|
+
setAreSuggestionsVisible(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
36
|
+
return () => {
|
|
37
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
38
|
+
};
|
|
39
|
+
}, []);
|
|
40
|
+
const handleInputFocus = () => {
|
|
41
|
+
// Don't show suggestions if focus was triggered by Escape key
|
|
42
|
+
if (suppressNextFocus.current) {
|
|
43
|
+
suppressNextFocus.current = false;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Show suggestions when input receives focus (if suggestions exist).
|
|
47
|
+
if (suggestions && suggestions.length > 0) {
|
|
48
|
+
setAreSuggestionsVisible(true);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const handleInputKeyDown = (e) => {
|
|
52
|
+
var _a, _b;
|
|
53
|
+
switch (e.key) {
|
|
54
|
+
case "ArrowDown": {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
// Show suggestions if hidden, or move to first suggestion.
|
|
57
|
+
if (suggestions && suggestions.length > 0) {
|
|
58
|
+
if (!areSuggestionsVisible) {
|
|
59
|
+
setAreSuggestionsVisible(true);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
(_a = suggestionRefs.current[0]) === null || _a === void 0 ? void 0 : _a.focus();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case "Escape": {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
setAreSuggestionsVisible(false);
|
|
70
|
+
suppressNextFocus.current = true;
|
|
71
|
+
(_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.focus();
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "Tab": {
|
|
75
|
+
// Hide suggestions and allow default tab behavior.
|
|
76
|
+
setAreSuggestionsVisible(false);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const handleSuggestionKeyDown = (e, index) => {
|
|
82
|
+
var _a, _b, _c, _d, _e;
|
|
83
|
+
switch (e.key) {
|
|
84
|
+
case "ArrowDown": {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
// Move to next suggestion, or wrap to first.
|
|
87
|
+
const nextIndex = index + 1;
|
|
88
|
+
if (nextIndex < suggestionRefs.current.length) {
|
|
89
|
+
(_a = suggestionRefs.current[nextIndex]) === null || _a === void 0 ? void 0 : _a.focus();
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
(_b = suggestionRefs.current[0]) === null || _b === void 0 ? void 0 : _b.focus();
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case "ArrowUp": {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
if (index === 0) {
|
|
99
|
+
// Move back to input.
|
|
100
|
+
(_c = inputRef.current) === null || _c === void 0 ? void 0 : _c.focus();
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Move to previous suggestion.
|
|
104
|
+
(_d = suggestionRefs.current[index - 1]) === null || _d === void 0 ? void 0 : _d.focus();
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case "Escape": {
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
setAreSuggestionsVisible(false);
|
|
111
|
+
suppressNextFocus.current = true;
|
|
112
|
+
(_e = inputRef.current) === null || _e === void 0 ? void 0 : _e.focus();
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case "Tab": {
|
|
116
|
+
// Hide suggestions and allow default tab behavior.
|
|
117
|
+
setAreSuggestionsVisible(false);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
// Reset suggestions visibility when suggestions change
|
|
123
|
+
const prevSuggestionsRef = useRef(suggestions);
|
|
124
|
+
if (prevSuggestionsRef.current !== suggestions) {
|
|
125
|
+
prevSuggestionsRef.current = suggestions;
|
|
126
|
+
if (suggestions && suggestions.length > 0) {
|
|
127
|
+
setAreSuggestionsVisible(true);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const hasSuggestions = suggestions && suggestions.length > 0 && areSuggestionsVisible;
|
|
131
|
+
const controlsId = useMemo(() => `searchSuggestions-${createId()}`, []);
|
|
132
|
+
return (_jsx("form", Object.assign({ onSubmit: onSubmit }, { children: _jsxs("div", Object.assign({ ref: containerRef, className: classes }, { children: [_jsx("input", Object.assign({ ref: inputRef, className: "vuiSearchInput__input", type: "text", autoComplete: "off", autoCapitalize: "off", spellCheck: "false", autoFocus: autoFocus, placeholder: placeholder, value: value, onChange: onChange, onFocus: handleInputFocus, onKeyDown: handleInputKeyDown, "aria-autocomplete": "list", "aria-controls": hasSuggestions ? controlsId : undefined }, rest)), isClearable && value && (_jsx(VuiIconButton, { "aria-label": "Clear input", className: "vuiSearchInput__clearButton", icon: _jsx(VuiIcon, { children: _jsx(BiX, {}) }), onClick: (e) => {
|
|
22
133
|
e.preventDefault();
|
|
23
134
|
onClear();
|
|
24
|
-
} }))] })) })));
|
|
135
|
+
} })), hasSuggestions && (_jsx(VuiSearchInputSuggestions, { id: controlsId, suggestions: suggestions, onSuggestionKeyDown: handleSuggestionKeyDown, suggestionRefs: suggestionRefs }))] })) })));
|
|
25
136
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { KeyboardEvent, MutableRefObject } from "react";
|
|
2
|
+
import { SearchSuggestion } from "./types";
|
|
3
|
+
type Props = {
|
|
4
|
+
id: string;
|
|
5
|
+
suggestions: SearchSuggestion[];
|
|
6
|
+
onSuggestionKeyDown: (e: KeyboardEvent<HTMLAnchorElement>, index: number) => void;
|
|
7
|
+
suggestionRefs: MutableRefObject<(HTMLAnchorElement | null)[]>;
|
|
8
|
+
};
|
|
9
|
+
export declare const VuiSearchInputSuggestions: ({ id, suggestions, onSuggestionKeyDown, suggestionRefs }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export const VuiSearchInputSuggestions = ({ id, suggestions, onSuggestionKeyDown, suggestionRefs }) => {
|
|
3
|
+
return (_jsx("div", Object.assign({ id: id, className: "vuiSearchInputSuggestions", role: "listbox" }, { children: _jsx("div", Object.assign({ className: "vuiSearchInputSuggestions__suggestionsList" }, { children: suggestions.map((suggestion, index) => (_jsx("a", Object.assign({ href: suggestion.href, className: "vuiSearchInputSuggestions__suggestion", ref: (el) => (suggestionRefs.current[index] = el), onKeyDown: (e) => onSuggestionKeyDown(e, index), role: "option", "aria-selected": "false" }, { children: suggestion.label }), index))) })) })));
|
|
4
|
+
};
|
|
@@ -61,3 +61,52 @@
|
|
|
61
61
|
color: $colorAccent;
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
.vuiSearchInputSuggestions {
|
|
66
|
+
position: absolute;
|
|
67
|
+
top: 100%;
|
|
68
|
+
left: 0;
|
|
69
|
+
right: 0;
|
|
70
|
+
z-index: 1;
|
|
71
|
+
|
|
72
|
+
// If this is at the bottom of the page we need to add some space
|
|
73
|
+
// at the bottom so the user can see the menu completely.
|
|
74
|
+
&:after {
|
|
75
|
+
content: "";
|
|
76
|
+
position: absolute;
|
|
77
|
+
left: 0;
|
|
78
|
+
right: 0;
|
|
79
|
+
height: $sizeXl;
|
|
80
|
+
pointer-events: none;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.vuiSearchInputSuggestions__suggestionsList {
|
|
85
|
+
background-color: $colorEmptyShade;
|
|
86
|
+
border: 1px solid $colorMediumShade;
|
|
87
|
+
border-top: none;
|
|
88
|
+
border-radius: 0 0 $sizeM $sizeM;
|
|
89
|
+
box-shadow: $shadowLargeEnd;
|
|
90
|
+
max-height: 220px;
|
|
91
|
+
overflow-y: auto;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.vuiSearchInputSuggestions__suggestion {
|
|
95
|
+
display: block;
|
|
96
|
+
background-color: $colorEmptyShade;
|
|
97
|
+
text-decoration: none;
|
|
98
|
+
padding: ($sizeXxs + 1px) $sizeS;
|
|
99
|
+
font-size: $fontSizeStandard;
|
|
100
|
+
color: $colorText;
|
|
101
|
+
|
|
102
|
+
&:focus-visible,
|
|
103
|
+
&:hover {
|
|
104
|
+
text-decoration: underline;
|
|
105
|
+
color: $colorPrimary;
|
|
106
|
+
background-color: $colorPrimaryLighterShade;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
&:focus-visible {
|
|
110
|
+
outline: none;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/styles/index.css
CHANGED
|
@@ -3688,6 +3688,49 @@ h2.react-datepicker__current-month {
|
|
|
3688
3688
|
color: #5f30c3;
|
|
3689
3689
|
}
|
|
3690
3690
|
|
|
3691
|
+
.vuiSearchInputSuggestions {
|
|
3692
|
+
position: absolute;
|
|
3693
|
+
top: 100%;
|
|
3694
|
+
left: 0;
|
|
3695
|
+
right: 0;
|
|
3696
|
+
z-index: 1;
|
|
3697
|
+
}
|
|
3698
|
+
.vuiSearchInputSuggestions:after {
|
|
3699
|
+
content: "";
|
|
3700
|
+
position: absolute;
|
|
3701
|
+
left: 0;
|
|
3702
|
+
right: 0;
|
|
3703
|
+
height: 32px;
|
|
3704
|
+
pointer-events: none;
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
.vuiSearchInputSuggestions__suggestionsList {
|
|
3708
|
+
background-color: #ffffff;
|
|
3709
|
+
border: 1px solid #cbd1de;
|
|
3710
|
+
border-top: none;
|
|
3711
|
+
border-radius: 0 0 16px 16px;
|
|
3712
|
+
box-shadow: rgba(50, 50, 93, 0.25) 0px 6px 12px -2px, rgba(0, 0, 0, 0.3) 0px 3px 7px -3px;
|
|
3713
|
+
max-height: 220px;
|
|
3714
|
+
overflow-y: auto;
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
.vuiSearchInputSuggestions__suggestion {
|
|
3718
|
+
display: block;
|
|
3719
|
+
background-color: #ffffff;
|
|
3720
|
+
text-decoration: none;
|
|
3721
|
+
padding: 5px 12px;
|
|
3722
|
+
font-size: 14px;
|
|
3723
|
+
color: #1c1d22;
|
|
3724
|
+
}
|
|
3725
|
+
.vuiSearchInputSuggestions__suggestion:focus-visible, .vuiSearchInputSuggestions__suggestion:hover {
|
|
3726
|
+
text-decoration: underline;
|
|
3727
|
+
color: #045dda;
|
|
3728
|
+
background-color: #f1f7ff;
|
|
3729
|
+
}
|
|
3730
|
+
.vuiSearchInputSuggestions__suggestion:focus-visible {
|
|
3731
|
+
outline: none;
|
|
3732
|
+
}
|
|
3733
|
+
|
|
3691
3734
|
.vuiSearchResult {
|
|
3692
3735
|
position: relative;
|
|
3693
3736
|
}
|