Skip to content

Commit

Permalink
Add server side data example
Browse files Browse the repository at this point in the history
  • Loading branch information
joshwooding committed Dec 13, 2023
1 parent de82254 commit 88a4d9f
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 11 deletions.
13 changes: 11 additions & 2 deletions packages/lab/src/combo-box-next/ComboBoxNext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import comboBoxCss from "./ComboBoxNext.css";
import { clsx } from "clsx";
import { flip, limitShift, offset, shift, size } from "@floating-ui/react";
import { ChevronDownIcon, ChevronUpIcon } from "@salt-ds/icons";
import { useComboBoxNext } from "./useComboBoxNext";

export interface ComboBoxNextProps
extends InputProps,
Expand Down Expand Up @@ -63,6 +64,8 @@ export const ComboBoxNext = forwardRef<HTMLDivElement, ComboBoxNextProps>(
onKeyDown,
onFocus,
onBlur,
value,
defaultValue,
...rest
} = props;

Expand All @@ -82,14 +85,16 @@ export const ComboBoxNext = forwardRef<HTMLDivElement, ComboBoxNextProps>(
const disabled = Boolean(disabledProp) || formFieldDisabled;
const readOnly = Boolean(readOnlyProp) || formFieldReadOnly;

const listControl = useListControl({
const listControl = useComboBoxNext({
open,
defaultOpen,
onOpenChange,
multiselect,
defaultSelected,
selected,
onSelectionChange,
value,
defaultValue,
});

const {
Expand All @@ -109,6 +114,8 @@ export const ComboBoxNext = forwardRef<HTMLDivElement, ComboBoxNextProps>(
focusedState,
setFocusedState,
listRef,
valueState,
setValueState,
} = listControl;

const { Component: FloatingComponent } = useFloatingComponent();
Expand Down Expand Up @@ -255,14 +262,15 @@ export const ComboBoxNext = forwardRef<HTMLDivElement, ComboBoxNextProps>(
clear(event);
}

setValueState(event.target.value);

// Wait for the filter to happen
queueMicrotask(() => {
const newOption = getOptionAtIndex(0);
if (newOption) {
setActive(newOption);
}
});

onChange?.(event);
};

Expand Down Expand Up @@ -353,6 +361,7 @@ export const ComboBoxNext = forwardRef<HTMLDivElement, ComboBoxNextProps>(
aria-activedescendant={activeState?.id}
variant={variant}
inputRef={inputRef}
value={valueState}
{...rest}
ref={handleRef}
/>
Expand Down
63 changes: 63 additions & 0 deletions packages/lab/src/combo-box-next/useComboBoxNext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
useListControl,
ListControlProps,
} from "../list-control/ListControlState";

import { SyntheticEvent } from "react";
import { OptionValue } from "../list-control/ListControlContext";

export function useComboBoxNext(props: ListControlProps) {
const {
open,
defaultOpen,
onOpenChange,
multiselect,
defaultSelected,
selected,
onSelectionChange,
defaultValue,
value,
} = props;

const listControl = useListControl({
open,
defaultOpen,
onOpenChange,
multiselect,
defaultSelected,
selected,
onSelectionChange,
defaultValue,
value,
});

const { selectedState, getOptionsMatching, setValueState, setSelectedState } =
listControl;

const select = (event: SyntheticEvent, option: OptionValue) => {
const { disabled, value } = option;

if (disabled) {
return;
}

let newSelected = [value];

if (multiselect) {
if (selectedState.includes(value)) {
newSelected = selectedState.filter((item) => item !== value);
} else {
newSelected = selectedState.concat([value]);
}
}

setSelectedState(newSelected);
const newValue = getOptionsMatching((option) =>
newSelected.includes(option.value)
).map((option) => option.text);
setValueState(multiselect ? "" : newValue[0]);
onSelectionChange?.(event, newSelected);
};

return { ...listControl, select };
}
6 changes: 4 additions & 2 deletions packages/lab/src/list-control/ListControlState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ export interface ListControlProps {
/**
* The default value.
*/
defaultValue?: string;
defaultValue?: string | readonly string[] | number | undefined;
/**
* The value. The component will be controlled if this prop is provided.
*/
value?: string;
value?: string | readonly string[] | number | undefined;
/**
* If true, multiple options can be selected.
*/
Expand Down Expand Up @@ -256,9 +256,11 @@ export function useListControl(props: ListControlProps) {
activeState,
setActive,
selectedState,
setSelectedState,
select,
clear,
valueState,
setValueState,
focusVisibleState,
setFocusVisibleState,
focusedState,
Expand Down
10 changes: 10 additions & 0 deletions site/docs/components/combo-box/examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -146,5 +146,15 @@ Combo Box can show validation states (warning, success, error) with the prop `va

Due to Combo Box’s flexible API, you can customize the filtering logic to suit your needs. In this example it displays the entire list of options when the combo box opens due to a mouse click.

</LivePreview>

<LivePreview componentName="combo-box" exampleName="ServerSideData" >

### Server side data

Combo Box can be used to display data from a server. In this example, the data is fetched and filtered on the server side.

Note that the data fetching in this example has been made intentionally show to highlight the loading states.

</LivePreview>
</LivePreviewControls>
52 changes: 52 additions & 0 deletions site/public/example-data/states.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
[
"Alabama",
"Alaska",
"Arizona",
"Arkansas",
"California",
"Colorado",
"Connecticut",
"Delaware",
"Florida",
"Georgia",
"Hawaii",
"Idaho",
"Illinois",
"Indiana",
"Iowa",
"Kansas",
"Kentucky",
"Louisiana",
"Maine",
"Maryland",
"Massachusetts",
"Michigan",
"Minnesota",
"Mississippi",
"Missouri",
"Montana",
"Nebraska",
"Nevada",
"New Hampshire",
"New Jersey",
"New Mexico",
"New York",
"North Carolina",
"North Dakota",
"Ohio",
"Oklahoma",
"Oregon",
"Pennsylvania",
"Rhode Island",
"South Carolina",
"South Dakota",
"Tennessee",
"Texas",
"Utah",
"Vermont",
"Virginia",
"Washington",
"West Virginia",
"Wisconsin",
"Wyoming"
]
10 changes: 3 additions & 7 deletions site/src/examples/combo-box/ComplexOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ const customMatchPattern = (

function OptionWithCountrySymbol({ value }: { value: LargeCity }) {
return (
<Option value={value.name} key={value.countryCode}>
<LazyCountrySymbol code={value.countryCode} />
<Option value={value.name}>
<LazyCountrySymbol aria-hidden code={value.countryCode} />
{value.name}
</Option>
);
Expand All @@ -32,11 +32,7 @@ export const ComplexOptions = (): ReactElement => {

return (
<Suspense fallback={null}>
<ComboBoxNext
style={{ width: "266px" }}
onChange={handleChange}
value={filter}
>
<ComboBoxNext style={{ width: "266px" }} onChange={handleChange}>
{largestCities
.filter((value) => customMatchPattern(value, filter))
.map((value) => (
Expand Down
66 changes: 66 additions & 0 deletions site/src/examples/combo-box/ServerSideData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ChangeEvent, ReactElement, SyntheticEvent, useState } from "react";
import { ComboBoxNext, Option } from "@salt-ds/lab";
import useSWR from "swr";
import { Spinner } from "@salt-ds/core";

const fetcher = async (url: string, filter: string) => {
// Sleep for 1 second to highlight to loading state
await new Promise((resolve) => setTimeout(resolve, 1000));

const rawData = await fetch(url);
const data = (await rawData.json()) as string[];
return data.filter((state) =>
state.toLowerCase().includes(filter.trim().toLowerCase())
);
};

export const ServerSideData = (): ReactElement => {
const [value, setValue] = useState("");
const { data, isLoading } = useSWR<string[]>(
`/example-data/states.json?s=${value}`,
(url: string) => fetcher(url, value),
{
fallbackData: [],
}
);

const loading = isLoading;

const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setValue(value);
};

const handleSelectionChange = (
event: SyntheticEvent,
newSelected: string[]
) => {
if (newSelected.length === 1) {
setValue(newSelected[0]);
} else {
setValue("");
}
};

return (
<ComboBoxNext
onChange={handleChange}
onSelectionChange={handleSelectionChange}
value={value}
style={{ width: "266px" }}
endAdornment={loading && <Spinner size="small" />}
>
{!loading ? (
data?.map((color) => (
<Option value={color} key={color}>
{color}
</Option>
))
) : (
<Option value="loading" key="loading">
Loading
</Option>
)}
</ComboBoxNext>
);
};
1 change: 1 addition & 0 deletions site/src/examples/combo-box/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from "./Grouped";
export * from "./EmptyMessage";
export * from "./Validation";
export * from "./CustomFiltering";
export * from "./ServerSideData";

0 comments on commit 88a4d9f

Please sign in to comment.