Skip to content

Commit

Permalink
chore: migrate vui-source changes to 1.0.0 (#167)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrderyk authored May 20, 2024
1 parent e3b4b37 commit ec42352
Show file tree
Hide file tree
Showing 11 changed files with 417 additions and 68 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vectara/vectara-ui",
"version": "0.0.4",
"version": "1.0.1",
"homepage": "https://vectara.github.io/vectara-ui/",
"description": "Vectara's design system, codified as a React and Sass component library",
"author": "Vectara",
Expand Down
5 changes: 4 additions & 1 deletion src/docs/pages/modal/PrimaryModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const options = [
export const PrimaryModal = () => {
const [isOpen, setIsOpen] = useState(false);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [searchValue, setSearchValue] = useState<string>("");

return (
<>
Expand All @@ -37,8 +38,10 @@ export const PrimaryModal = () => {
title="Select all that apply"
isOpen={isPopoverOpen}
setIsOpen={setIsPopoverOpen}
searchValue={searchValue}
setSearchValue={setSearchValue}
onSelect={() => undefined}
selected={[]}
selectedOptions={[]}
options={options}
>
<VuiButtonSecondary color="neutral" size="s">
Expand Down
153 changes: 153 additions & 0 deletions src/docs/pages/searchSelect/Async.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { useEffect, useRef, useState } from "react";
import { VuiButtonSecondary, VuiSearchSelect } from "../../../lib";

type Option = { value: string; label: string };

// Simulate a few pages worth of responses.
const optionsResponses = [
[
{ value: "a1", label: "A1" },
{ value: "b1", label: "B1" },
{ value: "c1", label: "C1" },
{ value: "d1", label: "D1" },
{ value: "e1", label: "E1" },
{ value: "f1", label: "F1" },
{ value: "g1", label: "G1" },
{ value: "h1", label: "H1" },
{ value: "i1", label: "I1" },
{ value: "j1", label: "J1" },
{ value: "k1", label: "K1" },
{ value: "l1", label: "L1" },
{ value: "m1", label: "M1" }
],
[
{ value: "n1", label: "N1" },
{ value: "o1", label: "O1" },
{ value: "p1", label: "P1" },
{ value: "q1", label: "Q1" },
{ value: "r1", label: "R1" },
{ value: "s1", label: "S1" },
{ value: "t1", label: "T1" },
{ value: "u1", label: "U1" },
{ value: "v1", label: "V1" },
{ value: "w1", label: "W1" },
{ value: "x1", label: "X1" },
{ value: "y1", label: "Y1" },
{ value: "z1", label: "Z1" }
],
[
{ value: "a2", label: "A2" },
{ value: "b2", label: "B2" },
{ value: "c2", label: "C2" },
{ value: "d2", label: "D2" },
{ value: "e2", label: "E2" },
{ value: "f2", label: "F2" },
{ value: "g2", label: "G2" },
{ value: "h2", label: "H2" },
{ value: "i2", label: "I2" },
{ value: "j2", label: "J2" },
{ value: "k2", label: "K2" },
{ value: "l2", label: "L2" },
{ value: "m2", label: "M2" }
],
[
{ value: "n2", label: "N2" },
{ value: "o2", label: "O2" },
{ value: "p2", label: "P2" },
{ value: "q2", label: "Q2" },
{ value: "r2", label: "R2" },
{ value: "s2", label: "S2" },
{ value: "t2", label: "T2" },
{ value: "u2", label: "U2" },
{ value: "v2", label: "V2" },
{ value: "w2", label: "W2" },
{ value: "x2", label: "X2" },
{ value: "y2", label: "Y2" },
{ value: "z2", label: "Z2" }
]
];

export const Async = () => {
const pageRef = useRef(0);

const [isOpen, setIsOpen] = useState(false);
const [searchValue, setSearchValue] = useState<string>("");
const [selectedOptions, setSelectedOptions] = useState(["a1", "b1"]);

// List of options that grows as we retrieve more pages.
const [lazyLoadedOptionsList, setLazyLoadedOptionsList] = useState<Array<Option>>([]);
// List of options limited to those that match a search query.
const [searchedOptionsList, setSearchedOptionsList] = useState<Array<Option>>([]);
const [isSearching, setIsSearching] = useState(false);

const fetchPage = async (newPage: number) => {
setIsSearching(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
setLazyLoadedOptionsList((prev) => [...prev, ...optionsResponses[newPage]]);
setIsSearching(false);
};

useEffect(() => {
fetchPage(pageRef.current);
}, []);

useEffect(() => {
const search = async () => {
setIsSearching(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
const response = optionsResponses
.flat()
.filter((option) => option.label.toLowerCase().includes(searchValue.toLowerCase()));
setSearchedOptionsList(response);
setIsSearching(false);
};

if (searchValue) {
search();
} else {
setSearchedOptionsList([]);
}
}, [searchValue]);

const asyncSearch = {
isSearching,
onSearchChange: (searchValue: string) => {
setSearchValue(searchValue);
},
onLazyLoad: () => {
if (pageRef.current < optionsResponses.length - 1) {
pageRef.current++;
fetchPage(pageRef.current);
}
}
};

// If there's a search, show the searched list otherwise show the lazy-loaded list.
const options = searchValue ? searchedOptionsList : lazyLoadedOptionsList;

const title =
!lazyLoadedOptionsList.length && isSearching
? "Loading"
: selectedOptions.length
? `Selected: ${selectedOptions.length === 1 ? "1 item" : `${selectedOptions.length} items`}`
: "Select all that apply";

return (
<VuiSearchSelect
isOpen={isOpen}
setIsOpen={setIsOpen}
searchValue={searchValue}
setSearchValue={setSearchValue}
onSelect={(value: string[]) => {
setSelectedOptions(value);
}}
options={options}
selectedOptions={selectedOptions}
asyncSearch={asyncSearch}
>
<VuiButtonSecondary color="neutral" size="m">
{title}
</VuiButtonSecondary>
</VuiSearchSelect>
);
};
5 changes: 4 additions & 1 deletion src/docs/pages/searchSelect/SearchSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,20 @@ const options = [

export const SearchSelect = () => {
const [isOpen, setIsOpen] = useState(false);
const [searchValue, setSearchValue] = useState<string>("");
const [selectedOptions, setSelectedOptions] = useState(["a", "b"]);

return (
<VuiSearchSelect
title="Select all that apply"
isOpen={isOpen}
setIsOpen={setIsOpen}
searchValue={searchValue}
setSearchValue={setSearchValue}
onSelect={(value: string[]) => {
setSelectedOptions(value);
}}
selected={selectedOptions}
selectedOptions={selectedOptions}
options={options}
>
<VuiButtonSecondary color="neutral" size="s">
Expand Down
7 changes: 7 additions & 0 deletions src/docs/pages/searchSelect/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { SearchSelect } from "./SearchSelect";
import { Async } from "./Async";

const SearchSelectSource = require("!!raw-loader!./SearchSelect");
const AsyncSource = require("!!raw-loader!./Async");

export const searchSelect = {
name: "Search Select",
Expand All @@ -8,6 +11,10 @@ export const searchSelect = {
{
component: <SearchSelect />,
source: SearchSelectSource.default.toString()
},
{
component: <Async />,
source: AsyncSource.default.toString()
}
]
};
57 changes: 56 additions & 1 deletion src/lib/components/optionsList/OptionsList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import classNames from "classnames";
import { VuiOptionsListItem } from "./OptionsListItem";
import { OptionListItem } from "./types";
import { useEffect, useRef } from "react";
import { VuiFlexContainer } from "../flex/FlexContainer";
import { VuiFlexItem } from "../flex/FlexItem";
import { VuiSpinner } from "../spinner/Spinner";
import { VuiText } from "../typography/Text";
import { VuiSpacer } from "../spacer/Spacer";

const SIZE = ["s", "m", "l"] as const;

Expand All @@ -9,9 +15,11 @@ export type Props<T> = {
options: OptionListItem<T>[];
onSelectOption?: (value: T) => void;
selected?: T | T[];
onScrollToBottom?: () => void;
isSelectable?: boolean;
isScrollable?: boolean;
size?: (typeof SIZE)[number];
isLoading?: boolean;
};

// https://github.com/typescript-eslint/typescript-eslint/issues/4062
Expand All @@ -21,11 +29,42 @@ export const VuiOptionsList = <T extends unknown = unknown>({
options,
onSelectOption,
selected,
onScrollToBottom,
isSelectable = false,
isScrollable = false,
size = "s",
isLoading,
...rest
}: Props<T>) => {
const isScrolledToBottomRef = useRef(false);
const scrollableContainerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const scrollableContainer = scrollableContainerRef.current;
const onScroll = () => {
const newIsScrolledToBottom = scrollableContainerRef.current
? Math.abs(
scrollableContainerRef.current.scrollHeight -
scrollableContainerRef.current.clientHeight -
scrollableContainerRef.current.scrollTop
) < 10
: true;

// Only dispatch onScrollToBottom once the threshold is crossed.
if (!isScrolledToBottomRef.current && newIsScrolledToBottom) {
onScrollToBottom?.();
}

isScrolledToBottomRef.current = newIsScrolledToBottom;
};

scrollableContainer?.addEventListener("scroll", onScroll);

return () => {
scrollableContainer?.removeEventListener("scroll", onScroll);
};
}, []);

const classes = classNames(
"vuiOptionsList",
`vuiOptionsList--${size}`,
Expand All @@ -36,7 +75,7 @@ export const VuiOptionsList = <T extends unknown = unknown>({
);

return (
<div className={classes} {...rest}>
<div className={classes} {...rest} ref={scrollableContainerRef}>
{options.map(({ value, label, onClick, ...rest }) => {
const isSelected = Array.isArray(selected) ? selected.includes(value) : value === selected;
return (
Expand All @@ -54,6 +93,22 @@ export const VuiOptionsList = <T extends unknown = unknown>({
/>
);
})}
{isLoading && (
<>
<VuiSpacer size="xxs" />
<VuiFlexContainer alignItems="center" justifyContent="center" spacing="xs">
<VuiFlexItem grow={false}>
<VuiSpinner size="xs" />
</VuiFlexItem>

<VuiFlexItem grow={false}>
<VuiText>
<p>Loading options…</p>
</VuiText>
</VuiFlexItem>
</VuiFlexContainer>
</>
)}
</div>
);
};
2 changes: 1 addition & 1 deletion src/lib/components/optionsList/OptionsListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const VuiOptionsListItem = <T extends unknown = unknown>({
<VuiFlexContainer alignItems="center" spacing="xs">
{isSelectable && (
<VuiFlexItem grow={false}>
<VuiIcon className={isSelected ? "" : "vuiOptionsListItem__selected--unselected"} color="accent" size="s">
<VuiIcon className={isSelected ? "" : "vuiOptionsListItem__selected--unselected"} color="subdued" size="s">
<BiCheck />
</VuiIcon>
</VuiFlexItem>
Expand Down
4 changes: 4 additions & 0 deletions src/lib/components/optionsList/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ $color: (
&:hover {
color: #{map.get($colorValue, "hover-color")};
background-color: #{map.get($colorValue, "selected-color")};

.vuiIcon__inner {
color: #{map.get($colorValue, "hover-color")};
}
}
}
}
Loading

0 comments on commit ec42352

Please sign in to comment.