@vectara/vectara-ui 0.0.4 → 1.0.2

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.
@@ -43,7 +43,7 @@ import { VuiOptionsButton } from "./optionsButton/OptionsButton";
43
43
  import { VuiOptionsList } from "./optionsList/OptionsList";
44
44
  import { VuiOptionsListItem } from "./optionsList/OptionsListItem";
45
45
  import { OptionListItem } from "./optionsList/types";
46
- import { VuiPopover } from "./popover/Popover";
46
+ import { VuiPopover, AnchorSide } from "./popover/Popover";
47
47
  import { VuiPortal } from "./portal/Portal";
48
48
  import { PROGRESS_BAR_COLOR, VuiProgressBar } from "./progressBar/ProgressBar";
49
49
  import { VuiPrompt } from "./prompt/Prompt";
@@ -71,5 +71,5 @@ import { TEXT_COLOR, TEXT_SIZE, TITLE_SIZE } from "./typography/types";
71
71
  import { VuiTitle } from "./typography/Title";
72
72
  import { VuiToggle } from "./toggle/Toggle";
73
73
  import { VuiTopicButton } from "./topicButton/TopicButton";
74
- export type { AppContentPadding, ButtonColor, CalloutColor, ChatLanguage, ChatStyle, ChatTurn, CodeLanguage, InfoTableColumnAlign, InfoTableRow, InfoTableRowType, LinkProps, MenuItem, Notification, OptionListItem, RadioButtonConfig, SearchResult, Sections, SectionItem, TabSize, Tree, TreeItem };
74
+ export type { AnchorSide, AppContentPadding, ButtonColor, CalloutColor, ChatLanguage, ChatStyle, ChatTurn, CodeLanguage, InfoTableColumnAlign, InfoTableRow, InfoTableRowType, LinkProps, MenuItem, Notification, OptionListItem, RadioButtonConfig, SearchResult, Sections, SectionItem, TabSize, Tree, TreeItem };
75
75
  export { BADGE_COLOR, BUTTON_COLOR, BUTTON_SIZE, CALLOUT_COLOR, CALLOUT_SIZE, ICON_COLOR, ICON_SIZE, PROGRESS_BAR_COLOR, SPACER_SIZE, SPINNER_COLOR, SPINNER_SIZE, TAB_SIZE, TEXT_COLOR, TEXT_SIZE, TITLE_SIZE, VuiAccordion, VuiAccountMenu, VuiAppContent, VuiAppHeader, VuiAppLayout, VuiAppSideNav, VuiBadge, VuiButtonPrimary, VuiButtonSecondary, VuiButtonTertiary, VuiIconButton, VuiCallout, VuiCard, VuiChat, VuiCheckbox, VuiCode, VuiContextProvider, VuiCopyButton, VuiDrawer, VuiFlexContainer, VuiFlexItem, VuiFormGroup, VuiGrid, VuiHorizontalRule, VuiIcon, VuiInfoTable, VuiLabel, VuiLink, VuiLinkInternal, VuiList, VuiMenu, VuiMenuItem, VuiModal, VuiNotifications, VuiNumberInput, VuiOptionsButton, VuiOptionsList, VuiOptionsListItem, VuiPasswordInput, VuiPopover, VuiPortal, VuiProgressBar, VuiPrompt, VuiRadioButton, VuiScreenBlock, VuiSearchInput, VuiSearchResult, VuiSearchSelect, VuiSelect, VuiSetting, VuiSpacer, VuiSpinner, VuiStatList, VuiStatus, VuiSummary, VuiSummaryCitation, VuiSuperRadioGroup, VuiTable, VuiTab, VuiTabbedRoutes, VuiTabs, VuiText, VuiTextArea, VuiTextColor, VuiTextInput, VuiTitle, VuiToggle, VuiTopicButton };
@@ -5,9 +5,11 @@ export type Props<T> = {
5
5
  options: OptionListItem<T>[];
6
6
  onSelectOption?: (value: T) => void;
7
7
  selected?: T | T[];
8
+ onScrollToBottom?: () => void;
8
9
  isSelectable?: boolean;
9
10
  isScrollable?: boolean;
10
11
  size?: (typeof SIZE)[number];
12
+ isLoading?: boolean;
11
13
  };
12
- export declare const VuiOptionsList: <T extends unknown = unknown>({ className, options, onSelectOption, selected, isSelectable, isScrollable, size, ...rest }: Props<T>) => import("react/jsx-runtime").JSX.Element;
14
+ export declare const VuiOptionsList: <T extends unknown = unknown>({ className, options, onSelectOption, selected, onScrollToBottom, isSelectable, isScrollable, size, isLoading, ...rest }: Props<T>) => import("react/jsx-runtime").JSX.Element;
13
15
  export {};
@@ -9,23 +9,50 @@ var __rest = (this && this.__rest) || function (s, e) {
9
9
  }
10
10
  return t;
11
11
  };
12
- import { jsx as _jsx } from "react/jsx-runtime";
12
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
13
13
  import classNames from "classnames";
14
14
  import { VuiOptionsListItem } from "./OptionsListItem";
15
+ import { useEffect, useRef } from "react";
16
+ import { VuiFlexContainer } from "../flex/FlexContainer";
17
+ import { VuiFlexItem } from "../flex/FlexItem";
18
+ import { VuiSpinner } from "../spinner/Spinner";
19
+ import { VuiText } from "../typography/Text";
20
+ import { VuiSpacer } from "../spacer/Spacer";
15
21
  const SIZE = ["s", "m", "l"];
16
22
  // https://github.com/typescript-eslint/typescript-eslint/issues/4062
17
23
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
18
24
  export const VuiOptionsList = (_a) => {
19
- var { className, options, onSelectOption, selected, isSelectable = false, isScrollable = false, size = "s" } = _a, rest = __rest(_a, ["className", "options", "onSelectOption", "selected", "isSelectable", "isScrollable", "size"]);
25
+ var { className, options, onSelectOption, selected, onScrollToBottom, isSelectable = false, isScrollable = false, size = "s", isLoading } = _a, rest = __rest(_a, ["className", "options", "onSelectOption", "selected", "onScrollToBottom", "isSelectable", "isScrollable", "size", "isLoading"]);
26
+ const isScrolledToBottomRef = useRef(false);
27
+ const scrollableContainerRef = useRef(null);
28
+ useEffect(() => {
29
+ const scrollableContainer = scrollableContainerRef.current;
30
+ const onScroll = () => {
31
+ const newIsScrolledToBottom = scrollableContainerRef.current
32
+ ? Math.abs(scrollableContainerRef.current.scrollHeight -
33
+ scrollableContainerRef.current.clientHeight -
34
+ scrollableContainerRef.current.scrollTop) < 10
35
+ : true;
36
+ // Only dispatch onScrollToBottom once the threshold is crossed.
37
+ if (!isScrolledToBottomRef.current && newIsScrolledToBottom) {
38
+ onScrollToBottom === null || onScrollToBottom === void 0 ? void 0 : onScrollToBottom();
39
+ }
40
+ isScrolledToBottomRef.current = newIsScrolledToBottom;
41
+ };
42
+ scrollableContainer === null || scrollableContainer === void 0 ? void 0 : scrollableContainer.addEventListener("scroll", onScroll);
43
+ return () => {
44
+ scrollableContainer === null || scrollableContainer === void 0 ? void 0 : scrollableContainer.removeEventListener("scroll", onScroll);
45
+ };
46
+ }, []);
20
47
  const classes = classNames("vuiOptionsList", `vuiOptionsList--${size}`, {
21
48
  "vuiOptionsList--scrollable": isScrollable
22
49
  }, className);
23
- return (_jsx("div", Object.assign({ className: classes }, rest, { children: options.map((_a) => {
24
- var { value, label, onClick } = _a, rest = __rest(_a, ["value", "label", "onClick"]);
25
- const isSelected = Array.isArray(selected) ? selected.includes(value) : value === selected;
26
- return (_jsx(VuiOptionsListItem, Object.assign({ value: value, label: label, onClick: () => {
27
- onClick === null || onClick === void 0 ? void 0 : onClick(value);
28
- onSelectOption === null || onSelectOption === void 0 ? void 0 : onSelectOption(value);
29
- }, isSelectable: isSelectable, isSelected: isSelected }, rest), label));
30
- }) })));
50
+ return (_jsxs("div", Object.assign({ className: classes }, rest, { ref: scrollableContainerRef }, { children: [options.map((_a) => {
51
+ var { value, label, onClick } = _a, rest = __rest(_a, ["value", "label", "onClick"]);
52
+ const isSelected = Array.isArray(selected) ? selected.includes(value) : value === selected;
53
+ return (_jsx(VuiOptionsListItem, Object.assign({ value: value, label: label, onClick: () => {
54
+ onClick === null || onClick === void 0 ? void 0 : onClick(value);
55
+ onSelectOption === null || onSelectOption === void 0 ? void 0 : onSelectOption(value);
56
+ }, isSelectable: isSelectable, isSelected: isSelected }, rest), label));
57
+ }), isLoading && (_jsxs(_Fragment, { children: [_jsx(VuiSpacer, { size: "xxs" }), _jsxs(VuiFlexContainer, Object.assign({ alignItems: "center", justifyContent: "center", spacing: "xs" }, { children: [_jsx(VuiFlexItem, Object.assign({ grow: false }, { children: _jsx(VuiSpinner, { size: "xs" }) })), _jsx(VuiFlexItem, Object.assign({ grow: false }, { children: _jsx(VuiText, { children: _jsx("p", { children: "Loading options\u2026" }) }) }))] }))] }))] })));
31
58
  };
@@ -31,7 +31,7 @@ export const VuiOptionsListItem = (_a) => {
31
31
  var { value, label, icon, color = "neutral", href, target, onClick, isSelectable, isSelected, testId } = _a, rest = __rest(_a, ["value", "label", "icon", "color", "href", "target", "onClick", "isSelectable", "isSelected", "testId"]);
32
32
  const { createLink } = useVuiContext();
33
33
  const labelContent = icon ? (_jsxs(VuiFlexContainer, Object.assign({ alignItems: "center", spacing: "xs" }, { children: [_jsx(VuiFlexItem, Object.assign({ grow: false, shrink: false }, { children: colorIcon(icon, color) })), _jsx(VuiFlexItem, Object.assign({ grow: 1 }, { children: label }))] }))) : (label);
34
- const content = (_jsxs(VuiFlexContainer, Object.assign({ alignItems: "center", spacing: "xs" }, { children: [isSelectable && (_jsx(VuiFlexItem, Object.assign({ grow: false }, { children: _jsx(VuiIcon, Object.assign({ className: isSelected ? "" : "vuiOptionsListItem__selected--unselected", color: "accent", size: "s" }, { children: _jsx(BiCheck, {}) })) }))), _jsx(VuiFlexItem, Object.assign({ grow: false }, { children: labelContent }))] })));
34
+ const content = (_jsxs(VuiFlexContainer, Object.assign({ alignItems: "center", spacing: "xs" }, { children: [isSelectable && (_jsx(VuiFlexItem, Object.assign({ grow: false }, { children: _jsx(VuiIcon, Object.assign({ className: isSelected ? "" : "vuiOptionsListItem__selected--unselected", color: "subdued", size: "s" }, { children: _jsx(BiCheck, {}) })) }))), _jsx(VuiFlexItem, Object.assign({ grow: false }, { children: labelContent }))] })));
35
35
  const classes = classNames("vuiOptionsListItem", `vuiOptionsListItem--${color}`);
36
36
  if (href) {
37
37
  return createLink(Object.assign({ className: classes, href,
@@ -79,6 +79,10 @@ $color: (
79
79
  &:hover {
80
80
  color: #{map.get($colorValue, "hover-color")};
81
81
  background-color: #{map.get($colorValue, "selected-color")};
82
+
83
+ .vuiIcon__inner {
84
+ color: #{map.get($colorValue, "hover-color")};
85
+ }
82
86
  }
83
87
  }
84
88
  }
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ export type AnchorSide = "left" | "right";
2
3
  export type Props = {
3
4
  button: React.ReactElement;
4
5
  children?: React.ReactNode;
@@ -7,5 +8,6 @@ export type Props = {
7
8
  isOpen: boolean;
8
9
  setIsOpen: (isOpen: boolean) => void;
9
10
  padding?: boolean;
11
+ anchorSide?: AnchorSide;
10
12
  };
11
- export declare const VuiPopover: ({ button: originalButton, children, className, header, isOpen, setIsOpen, padding, ...rest }: Props) => import("react/jsx-runtime").JSX.Element;
13
+ export declare const VuiPopover: ({ button: originalButton, children, className, header, isOpen, setIsOpen, padding, anchorSide, ...rest }: Props) => import("react/jsx-runtime").JSX.Element;
@@ -14,17 +14,20 @@ import { cloneElement, useEffect, useRef, useState } from "react";
14
14
  import classNames from "classnames";
15
15
  import { VuiPortal } from "../portal/Portal";
16
16
  import { FocusOn } from "react-focus-on";
17
- const getPosition = (button) => {
17
+ const calculatePopoverPosition = (button, anchorSide) => {
18
18
  if (!button)
19
19
  return undefined;
20
- const { bottom, right } = button.getBoundingClientRect();
21
- return {
22
- top: bottom + 2 + document.documentElement.scrollTop,
23
- right: window.innerWidth - right
24
- };
20
+ const buttonRect = button.getBoundingClientRect();
21
+ const top = buttonRect.bottom + 2 + document.documentElement.scrollTop;
22
+ const left = buttonRect.left;
23
+ if (anchorSide === "left") {
24
+ return { top: `${top}px`, left: `${left}px` };
25
+ }
26
+ const right = window.innerWidth - buttonRect.right;
27
+ return { top: `${top}px`, right: `${right}px` };
25
28
  };
26
29
  export const VuiPopover = (_a) => {
27
- var { button: originalButton, children, className, header, isOpen, setIsOpen, padding } = _a, rest = __rest(_a, ["button", "children", "className", "header", "isOpen", "setIsOpen", "padding"]);
30
+ var { button: originalButton, children, className, header, isOpen, setIsOpen, padding, anchorSide = "right" } = _a, rest = __rest(_a, ["button", "children", "className", "header", "isOpen", "setIsOpen", "padding", "anchorSide"]);
28
31
  const returnFocusElRef = useRef(null);
29
32
  const buttonRef = useRef(null);
30
33
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -73,7 +76,7 @@ export const VuiPopover = (_a) => {
73
76
  // Always keep menu position up to date. If we tried to cache this inside
74
77
  // a useEffect based on isOpen then there'd be a flicker if the width
75
78
  // of the button changes.
76
- const position = getPosition(buttonRef.current);
79
+ const position = calculatePopoverPosition(buttonRef.current, anchorSide);
77
80
  const classes = classNames("vuiPopover", className);
78
81
  const contentClasses = classNames("vuiPopoverContent", {
79
82
  "vuiPopoverContent--padding": padding
@@ -87,5 +90,5 @@ export const VuiPopover = (_a) => {
87
90
  // Enable scrolling of the page.
88
91
  scrollLock: false,
89
92
  // Enable scrolling of the page.
90
- preventScrollOnFocus: false }, { children: _jsxs("div", Object.assign({ className: classes, style: { top: `${position.top}px`, right: `${position.right}px` } }, rest, { children: [header && typeof header === "string" ? _jsx("div", Object.assign({ className: "vuiPopoverTitle" }, { children: header })) : header, children && _jsx("div", Object.assign({ className: contentClasses }, { children: children }))] })) }))) })] }));
93
+ preventScrollOnFocus: false }, { children: _jsxs("div", Object.assign({ className: classes, style: position }, rest, { children: [header && typeof header === "string" ? _jsx("div", Object.assign({ className: "vuiPopoverTitle" }, { children: header })) : header, children && _jsx("div", Object.assign({ className: contentClasses }, { children: children }))] })) }))) })] }));
91
94
  };
@@ -3,9 +3,16 @@ import { Props as PopoverProps } from "../popover/Popover";
3
3
  type Props<T> = Pick<PopoverProps, "isOpen" | "setIsOpen"> & Pick<OptionsListProps<T>, "options"> & {
4
4
  children: PopoverProps["button"];
5
5
  title?: string;
6
- selected: T[];
6
+ searchValue: string;
7
+ setSearchValue: (searchValue: string) => void;
8
+ asyncSearch?: {
9
+ isSearching?: boolean;
10
+ onSearchChange?: (searchValue: string) => void;
11
+ onLazyLoad: () => void;
12
+ };
13
+ selectedOptions: T[];
7
14
  onSelect: (selected: T[]) => void;
8
15
  isMultiSelect?: boolean;
9
16
  };
10
- export declare const VuiSearchSelect: <T extends unknown = unknown>({ children, title, isOpen, setIsOpen, options, onSelect, isMultiSelect, selected }: Props<T>) => import("react/jsx-runtime").JSX.Element;
17
+ export declare const VuiSearchSelect: <T extends unknown = unknown>({ children, title, isOpen, setIsOpen, options, searchValue, setSearchValue, asyncSearch, selectedOptions, onSelect, isMultiSelect }: Props<T>) => import("react/jsx-runtime").JSX.Element;
11
18
  export {};
@@ -4,75 +4,75 @@ import { VuiOptionsList } from "../optionsList/OptionsList";
4
4
  import { VuiPopover } from "../popover/Popover";
5
5
  import { VuiTextInput } from "../form";
6
6
  import { VuiSpacer } from "../spacer/Spacer";
7
+ import { sortSelectedOptions } from "./sortSelectedOptions";
7
8
  // https://github.com/typescript-eslint/typescript-eslint/issues/4062
8
9
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
9
- export const VuiSearchSelect = ({ children, title, isOpen, setIsOpen, options, onSelect, isMultiSelect = true, selected = [] }) => {
10
- const [searchValue, setSearchValue] = useState("");
11
- const [selectedOptions, setSelectedOptions] = useState();
10
+ export const VuiSearchSelect = ({ children, title, isOpen, setIsOpen, options, searchValue, setSearchValue, asyncSearch, selectedOptions, onSelect, isMultiSelect = true }) => {
12
11
  const [orderedOptions, setOrderedOptions] = useState([]);
12
+ // Async-only. Cache all options in case they get selected.
13
+ const [optionsCache, setOptionsCache] = useState({});
14
+ const addOptionsToCache = (optionsToAdd) => {
15
+ setOptionsCache((prev) => {
16
+ const newOptionsCache = Object.assign({}, prev);
17
+ optionsToAdd.forEach((option) => {
18
+ newOptionsCache[option.value] = option;
19
+ });
20
+ return newOptionsCache;
21
+ });
22
+ };
13
23
  useEffect(() => {
14
24
  // When the popover is opened, initialize the selected options,
15
25
  // and sort the options so the selected ones are on top.
16
26
  if (isOpen) {
17
- const selectedOptionsCopy = selected.concat();
18
- setSelectedOptions(selectedOptionsCopy);
19
- const sortedOptions = options.concat().sort((first, second) => {
20
- const isFirstSelected = selectedOptionsCopy.includes(first.value);
21
- const isSecondSelected = selectedOptionsCopy.includes(second.value);
22
- if (isFirstSelected && !isSecondSelected) {
23
- return -1;
24
- }
25
- if (!isFirstSelected && isSecondSelected) {
26
- return 1;
27
- }
28
- return 0;
29
- });
30
- setOrderedOptions(sortedOptions);
31
- }
32
- }, [isOpen]);
33
- const updateOpen = () => {
34
- if (isOpen) {
35
- // When the popover is closed, notify the consumer of the
36
- // selected options.
37
- onSelect(selectedOptions !== null && selectedOptions !== void 0 ? selectedOptions : []);
38
- // Signal the popover to be closed. We don't depend on the
39
- // original isOpen because it will cause a flicker when the
40
- // options are sorted.
41
- setSelectedOptions(undefined);
27
+ if (asyncSearch)
28
+ addOptionsToCache(options);
29
+ const isAsyncSearchInactive = asyncSearch && !searchValue;
30
+ const newOrderedOptions = sortSelectedOptions(selectedOptions, options, isAsyncSearchInactive ? optionsCache : undefined);
31
+ setOrderedOptions(newOrderedOptions);
42
32
  }
43
- setIsOpen(!isOpen);
44
- };
33
+ }, [isOpen, options]);
45
34
  const onSelectOption = (value) => {
46
35
  if (isMultiSelect) {
47
- setSelectedOptions((prev) => {
48
- if (!prev)
49
- return [];
50
- const updated = prev.concat();
51
- const index = prev.findIndex((item) => item === value);
52
- if (index !== -1) {
53
- updated.splice(index, 1);
54
- return updated;
55
- }
56
- updated.push(value);
57
- return updated;
58
- });
59
- return;
36
+ const updatedSelectedOptions = selectedOptions.concat();
37
+ const index = selectedOptions.findIndex((item) => item === value);
38
+ if (index === -1) {
39
+ // Select item.
40
+ updatedSelectedOptions.push(value);
41
+ }
42
+ else {
43
+ // Delect item.
44
+ updatedSelectedOptions.splice(index, 1);
45
+ }
46
+ onSelect(updatedSelectedOptions);
47
+ }
48
+ else {
49
+ if (selectedOptions[0] === value) {
50
+ // If the user clicks on the selected option, deselect it.
51
+ onSelect([]);
52
+ return;
53
+ }
54
+ onSelect([value]);
60
55
  }
61
- // If the user can only select one option at a time,
62
- // close the search select as soon as they make a choice.
63
- onSelect([value]);
64
- // Signal the popover to be closed. We don't depend on the
65
- // original isOpen because it will cause a flicker when the
66
- // options are sorted.
67
- setSelectedOptions(undefined);
68
- setIsOpen(false);
69
56
  };
70
- const visibleOptions = orderedOptions.filter((option) => {
71
- if (!searchValue.trim())
72
- return true;
73
- if (option.label.toLowerCase().includes(searchValue.toLowerCase()))
74
- return true;
75
- return false;
76
- });
77
- return (_jsxs(VuiPopover, Object.assign({ isOpen: selectedOptions !== undefined, setIsOpen: updateOpen, button: children, header: title }, { children: [_jsxs("div", Object.assign({ className: "vuiSearchSelect__search" }, { children: [_jsx(VuiTextInput, { placeholder: "Search", value: searchValue, onChange: (event) => setSearchValue(event.target.value) }), _jsx(VuiSpacer, { size: "xxs" })] })), _jsx(VuiOptionsList, { isSelectable: true, isScrollable: true, onSelectOption: onSelectOption, selected: selectedOptions, options: visibleOptions })] })));
57
+ // If onSearchChange is provided, we don't filter the options here because
58
+ // we assume the consumer has already filtered them.
59
+ const visibleOptions = (asyncSearch === null || asyncSearch === void 0 ? void 0 : asyncSearch.onSearchChange)
60
+ ? orderedOptions
61
+ : orderedOptions.filter((option) => {
62
+ if (!searchValue.trim())
63
+ return true;
64
+ if (option.label.toLowerCase().includes(searchValue.toLowerCase()))
65
+ return true;
66
+ return false;
67
+ });
68
+ return (_jsxs(VuiPopover, Object.assign({ isOpen: isOpen, setIsOpen: (isOpen) => {
69
+ setIsOpen(isOpen);
70
+ if (!isOpen)
71
+ setSearchValue("");
72
+ }, button: children, header: title }, { children: [_jsxs("div", Object.assign({ className: "vuiSearchSelect__search" }, { children: [_jsx(VuiTextInput, { placeholder: "Search", value: searchValue, onChange: (event) => {
73
+ var _a;
74
+ const { value } = event.target;
75
+ (_a = asyncSearch === null || asyncSearch === void 0 ? void 0 : asyncSearch.onSearchChange) === null || _a === void 0 ? void 0 : _a.call(asyncSearch, value);
76
+ setSearchValue(value);
77
+ } }), _jsx(VuiSpacer, { size: "xxs" })] })), _jsx(VuiOptionsList, { isSelectable: true, isScrollable: true, onScrollToBottom: asyncSearch === null || asyncSearch === void 0 ? void 0 : asyncSearch.onLazyLoad, onSelectOption: onSelectOption, selected: selectedOptions, options: visibleOptions, isLoading: asyncSearch === null || asyncSearch === void 0 ? void 0 : asyncSearch.isSearching })] })));
78
78
  };
@@ -0,0 +1,14 @@
1
+ import { OptionListItem } from "../optionsList/types";
2
+ /**
3
+ * The desired UX to is sort selected options to the top of the list.
4
+ *
5
+ * When there is an active async search, the list of options has already
6
+ * been filtered, so we only want to show selected options that are in
7
+ * the list of options. In this case, cache will NOT be provided.
8
+ *
9
+ * When the async search is not active, the list of options will consist
10
+ * of the first few pages and might not contain any "searched" options
11
+ * that have been selected. We need to show all selected options so we
12
+ * use the cache to fill in any holes. In this case, cache will be provided.
13
+ */
14
+ export declare const sortSelectedOptions: <T>(selectedOptions: T[], options: OptionListItem<T>[], cache?: Record<string, OptionListItem<T>> | undefined) => OptionListItem<T>[];
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The desired UX to is sort selected options to the top of the list.
3
+ *
4
+ * When there is an active async search, the list of options has already
5
+ * been filtered, so we only want to show selected options that are in
6
+ * the list of options. In this case, cache will NOT be provided.
7
+ *
8
+ * When the async search is not active, the list of options will consist
9
+ * of the first few pages and might not contain any "searched" options
10
+ * that have been selected. We need to show all selected options so we
11
+ * use the cache to fill in any holes. In this case, cache will be provided.
12
+ */
13
+ export const sortSelectedOptions = (selectedOptions, options, cache) => {
14
+ const comprehensiveOptions = options.concat();
15
+ // Presence of cache indicates the async search is not active, and we
16
+ // need to fill in any missing selected options from the cache.
17
+ if (cache) {
18
+ selectedOptions.forEach((selectedOption) => {
19
+ if (!options.find(({ value }) => value === selectedOption)) {
20
+ const cachedOption = cache[selectedOption];
21
+ if (cachedOption)
22
+ comprehensiveOptions.push(cachedOption);
23
+ }
24
+ });
25
+ }
26
+ const sortedOptions = comprehensiveOptions.sort((first, second) => {
27
+ const isFirstSelected = selectedOptions.includes(first.value);
28
+ const isSecondSelected = selectedOptions.includes(second.value);
29
+ if (isFirstSelected && !isSecondSelected) {
30
+ return -1;
31
+ }
32
+ if (!isFirstSelected && isSecondSelected) {
33
+ return 1;
34
+ }
35
+ return 0;
36
+ });
37
+ return sortedOptions;
38
+ };
@@ -0,0 +1,56 @@
1
+ import { sortSelectedOptions } from "./sortSelectedOptions";
2
+ describe("sortSelectedOptions", () => {
3
+ describe("sync search use case", () => {
4
+ test("returns the options as-is when nothing has been selected", () => {
5
+ const selectedOptions = [];
6
+ const options = [
7
+ { value: "1", label: "One" },
8
+ { value: "2", label: "Two" }
9
+ ];
10
+ expect(sortSelectedOptions(selectedOptions, options)).toEqual(options);
11
+ });
12
+ test("returns the selected options at the beginning of the list", () => {
13
+ const selectedOptions = ["3", "2"];
14
+ const options = [
15
+ { value: "1", label: "One" },
16
+ { value: "2", label: "Two" },
17
+ { value: "3", label: "Three" }
18
+ ];
19
+ expect(sortSelectedOptions(selectedOptions, options)).toEqual([
20
+ { value: "2", label: "Two" },
21
+ { value: "3", label: "Three" },
22
+ { value: "1", label: "One" }
23
+ ]);
24
+ });
25
+ });
26
+ describe("async search use case", () => {
27
+ test("ignores missing selected options when cache isn't provided", () => {
28
+ const selectedOptions = ["4", "3", "2"];
29
+ const options = [
30
+ { value: "1", label: "One" },
31
+ { value: "2", label: "Two" },
32
+ { value: "3", label: "Three" }
33
+ ];
34
+ expect(sortSelectedOptions(selectedOptions, options)).toEqual([
35
+ { value: "2", label: "Two" },
36
+ { value: "3", label: "Three" },
37
+ { value: "1", label: "One" }
38
+ ]);
39
+ });
40
+ test("fill in missing selected options when cache is provided", () => {
41
+ const selectedOptions = ["4", "3", "2"];
42
+ const options = [
43
+ { value: "1", label: "One" },
44
+ { value: "2", label: "Two" },
45
+ { value: "3", label: "Three" }
46
+ ];
47
+ const cache = { 4: { value: "4", label: "Four" } };
48
+ expect(sortSelectedOptions(selectedOptions, options, cache)).toEqual([
49
+ { value: "2", label: "Two" },
50
+ { value: "3", label: "Three" },
51
+ { value: "4", label: "Four" },
52
+ { value: "1", label: "One" }
53
+ ]);
54
+ });
55
+ });
56
+ });
@@ -2234,6 +2234,9 @@ fieldset {
2234
2234
  color: #551edf;
2235
2235
  background-color: #eadfff;
2236
2236
  }
2237
+ .vuiOptionsListItem--accent:hover .vuiIcon__inner {
2238
+ color: #551edf;
2239
+ }
2237
2240
 
2238
2241
  .vuiOptionsListItem--primary {
2239
2242
  color: #264cd6;
@@ -2242,6 +2245,9 @@ fieldset {
2242
2245
  color: #264cd6;
2243
2246
  background-color: #edf5ff;
2244
2247
  }
2248
+ .vuiOptionsListItem--primary:hover .vuiIcon__inner {
2249
+ color: #264cd6;
2250
+ }
2245
2251
 
2246
2252
  .vuiOptionsListItem--success {
2247
2253
  color: #04821f;
@@ -2250,6 +2256,9 @@ fieldset {
2250
2256
  color: #04821f;
2251
2257
  background-color: #e9f2e9;
2252
2258
  }
2259
+ .vuiOptionsListItem--success:hover .vuiIcon__inner {
2260
+ color: #04821f;
2261
+ }
2253
2262
 
2254
2263
  .vuiOptionsListItem--danger {
2255
2264
  color: #c41535;
@@ -2258,6 +2267,9 @@ fieldset {
2258
2267
  color: #c41535;
2259
2268
  background-color: #fae9eb;
2260
2269
  }
2270
+ .vuiOptionsListItem--danger:hover .vuiIcon__inner {
2271
+ color: #c41535;
2272
+ }
2261
2273
 
2262
2274
  .vuiOptionsListItem--warning {
2263
2275
  color: #965a15;
@@ -2266,6 +2278,9 @@ fieldset {
2266
2278
  color: #965a15;
2267
2279
  background-color: #f4eee8;
2268
2280
  }
2281
+ .vuiOptionsListItem--warning:hover .vuiIcon__inner {
2282
+ color: #965a15;
2283
+ }
2269
2284
 
2270
2285
  .vuiOptionsListItem--neutral {
2271
2286
  color: #2c313a;
@@ -2274,6 +2289,9 @@ fieldset {
2274
2289
  color: #264cd6;
2275
2290
  background-color: #edf5ff;
2276
2291
  }
2292
+ .vuiOptionsListItem--neutral:hover .vuiIcon__inner {
2293
+ color: #264cd6;
2294
+ }
2277
2295
 
2278
2296
  .vuiPopover {
2279
2297
  position: absolute;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectara/vectara-ui",
3
- "version": "0.0.4",
3
+ "version": "1.0.2",
4
4
  "homepage": "https://vectara.github.io/vectara-ui/",
5
5
  "description": "Vectara's design system, codified as a React and Sass component library",
6
6
  "author": "Vectara",