Skip to content

Commit

Permalink
feat: Allow lazy-loaded SelectFields to change their options.
Browse files Browse the repository at this point in the history
I.e. if a variable in the GQL query changed, after we've done the
initial load.
  • Loading branch information
stephenh committed Oct 10, 2023
1 parent 520794f commit 9919970
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 99 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, initial: maybeOptions.initial };

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]],
initial: 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)!],
initial: 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)!],
initial: 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]],
initial: 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={{
initial: 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={{
initial: 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],
initial: 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],
initial: 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
Loading

0 comments on commit 9919970

Please sign in to comment.