@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.
- package/lib/components/index.d.ts +2 -2
- package/lib/components/optionsList/OptionsList.d.ts +3 -1
- package/lib/components/optionsList/OptionsList.js +37 -10
- package/lib/components/optionsList/OptionsListItem.js +1 -1
- package/lib/components/optionsList/_index.scss +4 -0
- package/lib/components/popover/Popover.d.ts +3 -1
- package/lib/components/popover/Popover.js +12 -9
- package/lib/components/searchSelect/SearchSelect.d.ts +9 -2
- package/lib/components/searchSelect/SearchSelect.js +59 -59
- package/lib/components/searchSelect/sortSelectedOptions.d.ts +14 -0
- package/lib/components/searchSelect/sortSelectedOptions.js +38 -0
- package/lib/components/searchSelect/sortSelectedOptions.test.d.ts +1 -0
- package/lib/components/searchSelect/sortSelectedOptions.test.js +56 -0
- package/lib/styles/index.css +18 -0
- package/package.json +1 -1
|
@@ -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 (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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: "
|
|
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,
|
|
@@ -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
|
|
17
|
+
const calculatePopoverPosition = (button, anchorSide) => {
|
|
18
18
|
if (!button)
|
|
19
19
|
return undefined;
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
44
|
-
};
|
|
33
|
+
}, [isOpen, options]);
|
|
45
34
|
const onSelectOption = (value) => {
|
|
46
35
|
if (isMultiSelect) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
});
|
package/lib/styles/index.css
CHANGED
|
@@ -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