Skip to content

Commit

Permalink
feat: Allow lazy-loaded SelectFields to change their options. (#962)
Browse files Browse the repository at this point in the history
* feat: Allow lazy-loaded SelectFields to change their options.

I.e. if a variable in the GQL query changed, after we've done the
initial load.

* Allow initial to be undefined.

* Add asArray, allow options to be undefined.

* Rename initial to current.

* Fix eslint.
  • Loading branch information
stephenh authored Oct 10, 2023
1 parent 520794f commit e919851
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 105 deletions.
2 changes: 1 addition & 1 deletion src/components/Filters/SingleFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class SingleFilter<O, V extends Key> extends BaseFilter<V, SingleFilterProps<O,

const options = Array.isArray(maybeOptions)
? [allOption as O, ...maybeOptions]
: { ...maybeOptions, initial: [allOption as O, ...maybeOptions.initial] };
: { ...maybeOptions, current: maybeOptions.current };

return (
<SelectField<O, V>
Expand Down
42 changes: 21 additions & 21 deletions src/inputs/SelectField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ Contrast.args = { compact: true, contrast: true };
const loadTestOptions: TestOption[] = zeroTo(1000).map((i) => ({ id: String(i), name: `Project ${i}` }));

export function PerfTest() {
const [loaded, setLoaded] = useState<TestOption[]>([]);
const [selectedValue, setSelectedValue] = useState<string | undefined>(loadTestOptions[2].id);
return (
<SelectField
Expand All @@ -232,13 +233,12 @@ export function PerfTest() {
onSelect={setSelectedValue}
errorMsg={selectedValue !== undefined ? "" : "Select an option. Plus more error text to force it to wrap."}
options={{
initial: [loadTestOptions[2]],
current: loadTestOptions[2],
load: async () => {
return new Promise((resolve) => {
// @ts-ignore - believes `options` should be of type `never[]`
setTimeout(() => resolve({ options: loadTestOptions }), 1500);
});
await sleep(1500);
setLoaded(loadTestOptions);
},
options: loaded,
}}
onBlur={action("onBlur")}
onFocus={action("onFocus")}
Expand All @@ -248,6 +248,7 @@ export function PerfTest() {
PerfTest.parameters = { chromatic: { disableSnapshot: true } };

export function LazyLoadStateFields() {
const [loaded, setLoaded] = useState<TestOption[]>([]);
const [selectedValue, setSelectedValue] = useState<string | undefined>(loadTestOptions[2].id);
return (
<>
Expand All @@ -257,13 +258,12 @@ export function LazyLoadStateFields() {
onSelect={setSelectedValue}
unsetLabel={"-"}
options={{
initial: [loadTestOptions.find((o) => o.id === selectedValue)!],
current: loadTestOptions.find((o) => o.id === selectedValue)!,
load: async () => {
return new Promise((resolve) => {
// @ts-ignore - believes `options` should be of type `never[]`
setTimeout(() => resolve({ options: loadTestOptions }), 1500);
});
await sleep(1500);
setLoaded(loadTestOptions);
},
options: loaded,
}}
/>
<SelectField
Expand All @@ -272,13 +272,12 @@ export function LazyLoadStateFields() {
onSelect={setSelectedValue}
unsetLabel={"-"}
options={{
initial: [loadTestOptions.find((o) => o.id === selectedValue)!],
current: loadTestOptions.find((o) => o.id === selectedValue)!,
load: async () => {
return new Promise((resolve) => {
// @ts-ignore - believes `options` should be of type `never[]`
setTimeout(() => resolve({ options: loadTestOptions }), 1500);
});
await sleep(1500);
setLoaded(loadTestOptions);
},
options: loaded,
}}
/>
</>
Expand All @@ -287,21 +286,20 @@ export function LazyLoadStateFields() {
LazyLoadStateFields.parameters = { chromatic: { disableSnapshot: true } };

export function LoadingState() {
const [loaded, setLoaded] = useState<TestOption[]>([]);
const [selectedValue, setSelectedValue] = useState<string | undefined>(loadTestOptions[2].id);

return (
<SelectField
label="Project"
value={selectedValue}
onSelect={setSelectedValue}
options={{
initial: [loadTestOptions[2]],
current: loadTestOptions[2],
load: async () => {
return new Promise((resolve) => {
// @ts-ignore - believes `options` should be of type `never[]`
setTimeout(() => resolve({ options: loadTestOptions }), 5000);
});
await sleep(5000);
setLoaded(loadTestOptions);
},
options: loadTestOptions,
}}
/>
);
Expand Down Expand Up @@ -392,3 +390,5 @@ function TestSelectField<T extends object, V extends Value>(
</div>
);
}

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
82 changes: 50 additions & 32 deletions src/inputs/SelectField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,16 +181,25 @@ describe("SelectFieldTest", () => {

it("can load options via options prop callback", async () => {
// Given a Select Field with options that are loaded via a callback
const r = await render(
<TestSelectField
label="Age"
value="1"
options={{ initial: [options[0]], load: async () => ({ options }) }}
getOptionLabel={(o) => o.name}
getOptionValue={(o) => o.id}
data-testid="age"
/>,
);
function Test() {
const [loaded, setLoaded] = useState<HasIdAndName[]>([]);
return (
<SelectField
label="Age"
value="1"
options={{
current: options[0],
load: async () => setLoaded(options),
options: loaded,
}}
onSelect={() => {}}
getOptionLabel={(o) => o.name}
getOptionValue={(o) => o.id}
data-testid="age"
/>
);
}
const r = await render(<Test />);
// When opening the menu
click(r.age);
// Then expect to see the initial option and loading state
Expand Down Expand Up @@ -305,17 +314,25 @@ describe("SelectFieldTest", () => {
it("can define and select 'unsetLabel' when options are lazily loaded", async () => {
const onSelect = jest.fn();
// Given a Select Field with options that are loaded lazily
const r = await render(
<TestSelectField
label="Age"
value="1"
unsetLabel="None"
options={{ initial: [labelValueOptions[0]], load: async () => ({ options: labelValueOptions }) }}
getOptionLabel={(o) => o.label}
getOptionValue={(o) => o.value}
onSelect={onSelect}
/>,
);
function Test() {
const [loaded, setLoaded] = useState<HasLabelAndValue[]>([]);
return (
<TestSelectField
label="Age"
value="1"
unsetLabel="None"
options={{
current: labelValueOptions[0],
load: async () => setLoaded(labelValueOptions),
options: loaded,
}}
getOptionLabel={(o) => o.label}
getOptionValue={(o) => o.value}
onSelect={onSelect}
/>
);
}
const r = await render(<Test />);
// When we click the field to open the menu
await clickAndWait(r.age);
// The 'unset' option is in the menu and we select it
Expand Down Expand Up @@ -442,11 +459,12 @@ describe("SelectFieldTest", () => {
);
}

function TestMultipleSelectField<O, V extends Value>(
function TestMultipleSelectField<O extends HasIdAndName, V extends Value>(
props: Optional<SelectFieldProps<O, V>, "onSelect">,
): JSX.Element {
const [selected, setSelected] = useState<V | undefined>(props.value);
const init = options.find((o) => o.id === selected) as O;
const [loaded, setLoaded] = useState<O[]>([]);
return (
<>
<SelectField<O, V>
Expand All @@ -455,13 +473,12 @@ describe("SelectFieldTest", () => {
onSelect={setSelected}
unsetLabel={"-"}
options={{
initial: [init],
current: init,
load: async () => {
return new Promise((resolve) => {
// @ts-ignore - believes `options` should be of type `never[]`
setTimeout(() => resolve({ options }), 1500);
});
await sleep(1500);
setLoaded(props.options as O[]);
},
options: loaded,
}}
/>
<SelectField<O, V>
Expand All @@ -470,16 +487,17 @@ describe("SelectFieldTest", () => {
onSelect={setSelected}
unsetLabel={"-"}
options={{
initial: [init],
current: init,
load: async () => {
return new Promise((resolve) => {
// @ts-ignore - believes `options` should be of type `never[]`
setTimeout(() => resolve({ options }), 1500);
});
await sleep(1500);
setLoaded(props.options as O[]);
},
options: loaded,
}}
/>
</>
);
}
});

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
5 changes: 3 additions & 2 deletions src/inputs/SelectField.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useMemo } from "react";
import { Value } from "src/inputs";
import { ComboBoxBase, ComboBoxBaseProps, unsetOption } from "src/inputs/internal/ComboBoxBase";
import { HasIdAndName, Optional } from "src/types";
Expand Down Expand Up @@ -35,14 +36,14 @@ export function SelectField<O, V extends Value>(
value,
...otherProps
} = props;

const values = useMemo(() => [value], [value]);
return (
<ComboBoxBase
{...otherProps}
options={options}
getOptionLabel={getOptionLabel}
getOptionValue={getOptionValue}
values={[value]}
values={values}
onSelect={(values, options) => {
// If the user used `unsetLabel`, then values will be `[undefined]` and options `[unsetOption]`
if (values.length > 0 && options.length > 0) {
Expand Down
2 changes: 1 addition & 1 deletion src/inputs/TreeSelectField/TreeSelectField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export function AsyncOptions() {
<TestTreeSelectField
values={[]}
options={{
initial: initialOption,
current: initialOption,
load: async () => {
return new Promise((resolve) => {
// @ts-ignore - believes `options` should be of type `never[]`
Expand Down
2 changes: 1 addition & 1 deletion src/inputs/TreeSelectField/TreeSelectField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ describe(TreeSelectField, () => {
const r = await render(
<TreeSelectField
onSelect={noop}
options={{ initial: initialOption, load: async () => ({ options }) }}
options={{ current: initialOption, load: async () => ({ options }) }}
label="Favorite League"
values={[]}
getOptionValue={(o) => o.id}
Expand Down
2 changes: 1 addition & 1 deletion src/inputs/TreeSelectField/TreeSelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ function TreeSelectFieldBase<O, V extends Value>(props: TreeSelectFieldProps<O,

const isDisabled = !!disabled;
const isReadOnly = !!readOnly;
const initialOptions = Array.isArray(options) ? options : options.initial;
const initialOptions = Array.isArray(options) ? options : options.current;
const { contains } = useFilter({ sensitivity: "base" });

const { collapsedKeys } = useTreeSelectFieldProvider();
Expand Down
2 changes: 1 addition & 1 deletion src/inputs/TreeSelectField/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type FoundOption<O> = { option: NestedOption<O>; parents: NestedOption<O>[] };
export type NestedOption<O> = O & { children?: NestedOption<O>[] };
export type NestedOptionsOrLoad<O> =
| NestedOption<O>[]
| { initial: NestedOption<O>[]; load: () => Promise<{ options: NestedOption<O>[] }> };
| { current: NestedOption<O>[]; load: () => Promise<{ options: NestedOption<O>[] }> };
export type LeveledOption<O> = [NestedOption<O>, number];

export type TreeFieldState<O> = {
Expand Down
Loading

0 comments on commit e919851

Please sign in to comment.