@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.
@@ -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
- return (_jsx("form", Object.assign({ onSubmit: onSubmit }, { children: _jsxs("div", Object.assign({ className: classes }, { children: [_jsx("input", Object.assign({ className: "vuiSearchInput__input", type: "text", autoComplete: "off", autoCapitalize: "off", spellCheck: "false", autoFocus: autoFocus, placeholder: placeholder, value: value, onChange: onChange }, rest)), isClearable && value && (_jsx(VuiIconButton, { "aria-label": "Clear input", className: "vuiSearchInput__clearButton", icon: _jsx(VuiIcon, { children: _jsx(BiX, {}) }), onClick: (e) => {
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,4 @@
1
+ export type SearchSuggestion = {
2
+ label: string;
3
+ href: string;
4
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectara/vectara-ui",
3
- "version": "9.9.0",
3
+ "version": "9.10.0",
4
4
  "homepage": "./",
5
5
  "description": "Vectara's design system, codified as a React and Sass component library",
6
6
  "author": "Vectara",