Skip to content

Commit

Permalink
fix: TreeSelectField and treeFilter fixes
Browse files Browse the repository at this point in the history
Updates/fixes include:
1. TreeSelectField can reset values when outside component sets values to undefined
2. Can set individual groups to be default collapsed
3. Increase min-width of ListBox from 200 to 320px wide per Design feedback
  • Loading branch information
Brandon Dow committed Aug 21, 2024
1 parent dd4be0f commit cb56192
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 8 deletions.
1 change: 1 addition & 0 deletions src/components/Filters/Filters.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ function TestFilterPage({ vertical = false, numberOfInlineFilters = 4 }) {
children: cohorts.map(({ id, name, projects }) => ({
id,
name,
defaultCollapsed: true,
children: projects.map(({ id, name }) => ({ id, name })),
})),
}));
Expand Down
34 changes: 33 additions & 1 deletion src/inputs/TreeSelectField/TreeSelectField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TreeSelectField } from "src/inputs";
import { NestedOption } from "src/inputs/TreeSelectField/utils";
import { HasIdAndName } from "src/types";
import { noop } from "src/utils";
import { blur, click, focus, render, wait } from "src/utils/rtl";
import { blur, click, focus, getSelected, render, wait } from "src/utils/rtl";
import { useState } from "react";

describe(TreeSelectField, () => {
Expand Down Expand Up @@ -682,6 +682,38 @@ describe(TreeSelectField, () => {
expect(r.selectedOptionsCount).toHaveTextContent("1");
expect(r.favoriteLeague_unfocusedPlaceholderContainer).toHaveTextContent("Baseball");
});

it("can reset values to undefined", async () => {
// Given a TreeSelectField with values set
const r = await render(
<TreeSelectField
onSelect={noop}
values={["nba", "mlb"]}
options={getNestedOptions()}
label="Favorite League"
getOptionValue={(o) => o.id}
getOptionLabel={(o) => o.name}
/>,
);
// Then the options are initially selected
expect(getSelected(r.favoriteLeague)).toEqual(["MLB", "NBA"]);

// When we re-render with values set to undefined (simulating an outside component's "clear" action, i.e. Filters)
r.rerender(
<TreeSelectField
chipDisplay="all"
onSelect={noop}
values={undefined}
options={getNestedOptions()}
label="Favorite League"
getOptionValue={(o) => o.id}
getOptionLabel={(o) => o.name}
/>,
);

// Then the values are reset
expect(getSelected(r.favoriteLeague)).toEqual(undefined);
});
});

function getNestedOptions(): NestedOption<HasIdAndName>[] {
Expand Down
19 changes: 15 additions & 4 deletions src/inputs/TreeSelectField/TreeSelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,16 @@ export function TreeSelectField<O, V extends Value>(
} = props;

const [collapsedKeys, setCollapsedKeys] = useState<Key[]>(
Array.isArray(options) && defaultCollapsed ? options.map((o) => getOptionValue(o)) : [],
!Array.isArray(options)
? []
: defaultCollapsed
? options.map((o) => getOptionValue(o))
: options
.flatMap(flattenOptions)
.filter((o) => o.defaultCollapsed)
.map((o) => getOptionValue(o)),
);

const contextValue = useMemo<CollapsedChildrenState<O, V>>(
() => ({ collapsedKeys, setCollapsedKeys, getOptionValue }),
// TODO: validate this eslint-disable. It was automatically ignored as part of https://app.shortcut.com/homebound-team/story/40033/enable-react-hooks-exhaustive-deps-for-react-projects
Expand Down Expand Up @@ -248,8 +256,8 @@ function TreeSelectFieldBase<O, V extends Value>(props: TreeSelectFieldProps<O,
getOptionLabel,
isReadOnly,
nothingSelectedText,
collapsedKeys,
getOptionValue,
collapsedKeys,
]);

// Initialize the TreeFieldState
Expand All @@ -260,9 +268,12 @@ function TreeSelectFieldBase<O, V extends Value>(props: TreeSelectFieldProps<O,
// if the values does not match the values in the fieldState, then update the fieldState
const selectedKeys = fieldState.selectedOptions.map((o) => valueToKey(getOptionValue(o)));
if (
values &&
(values.length !== selectedKeys.length || !values.every((v) => selectedKeys.includes(valueToKey(v))))
// If the values were cleared
(values === undefined && selectedKeys.length !== 0) ||
// Or values were set, but they don't match the selected keys
(values && (values.length !== selectedKeys.length || !values.every((v) => selectedKeys.includes(valueToKey(v)))))
) {
// Then reinitialize
setFieldState(initTreeFieldState());
}
// eslint-disable-next-line react-hooks/exhaustive-deps
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 @@ -3,7 +3,7 @@ import { Key } from "react";
import { Value } from "src/inputs/Value";

type FoundOption<O> = { option: NestedOption<O>; parents: NestedOption<O>[] };
export type NestedOption<O> = O & { children?: NestedOption<O>[] };
export type NestedOption<O> = O & { children?: NestedOption<O>[]; defaultCollapsed?: boolean };
export type NestedOptionsOrLoad<O> =
| NestedOption<O>[]
| { current: NestedOption<O>[]; load: () => Promise<{ options: NestedOption<O>[] }> };
Expand Down
4 changes: 2 additions & 2 deletions src/inputs/internal/ComboBoxBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ export function ComboBoxBase<O, V extends Value>(props: ComboBoxBaseProps<O, V>)
...positionProps.style,
width: comboBoxRef?.current?.clientWidth,
// Ensures the menu never gets too small.
minWidth: 200,
minWidth: 320,
};

const fieldMaxWidth = getFieldWidth(fullWidth);
Expand Down Expand Up @@ -378,7 +378,7 @@ export function ComboBoxBase<O, V extends Value>(props: ComboBoxBaseProps<O, V>)
positionProps={positionProps}
onClose={() => state.close()}
isOpen={state.isOpen}
minWidth={200}
minWidth={320}
>
<ListBox
{...listBoxProps}
Expand Down

0 comments on commit cb56192

Please sign in to comment.