From cb56192391ea61d6fca5a9293bd09bb75e46cffc Mon Sep 17 00:00:00 2001 From: Brandon Dow Date: Wed, 21 Aug 2024 16:37:35 -0400 Subject: [PATCH] fix: TreeSelectField and treeFilter fixes 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 --- src/components/Filters/Filters.stories.tsx | 1 + .../TreeSelectField/TreeSelectField.test.tsx | 34 ++++++++++++++++++- .../TreeSelectField/TreeSelectField.tsx | 19 ++++++++--- src/inputs/TreeSelectField/utils.ts | 2 +- src/inputs/internal/ComboBoxBase.tsx | 4 +-- 5 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/components/Filters/Filters.stories.tsx b/src/components/Filters/Filters.stories.tsx index 32dc9284c..c169bb07e 100644 --- a/src/components/Filters/Filters.stories.tsx +++ b/src/components/Filters/Filters.stories.tsx @@ -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 })), })), })); diff --git a/src/inputs/TreeSelectField/TreeSelectField.test.tsx b/src/inputs/TreeSelectField/TreeSelectField.test.tsx index aaaf16f91..032501c59 100644 --- a/src/inputs/TreeSelectField/TreeSelectField.test.tsx +++ b/src/inputs/TreeSelectField/TreeSelectField.test.tsx @@ -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, () => { @@ -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( + 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( + o.id} + getOptionLabel={(o) => o.name} + />, + ); + + // Then the values are reset + expect(getSelected(r.favoriteLeague)).toEqual(undefined); + }); }); function getNestedOptions(): NestedOption[] { diff --git a/src/inputs/TreeSelectField/TreeSelectField.tsx b/src/inputs/TreeSelectField/TreeSelectField.tsx index e111d808e..87ac348ff 100644 --- a/src/inputs/TreeSelectField/TreeSelectField.tsx +++ b/src/inputs/TreeSelectField/TreeSelectField.tsx @@ -94,8 +94,16 @@ export function TreeSelectField( } = props; const [collapsedKeys, setCollapsedKeys] = useState( - 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>( () => ({ 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 @@ -248,8 +256,8 @@ function TreeSelectFieldBase(props: TreeSelectFieldProps(props: TreeSelectFieldProps 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 diff --git a/src/inputs/TreeSelectField/utils.ts b/src/inputs/TreeSelectField/utils.ts index f0a1844a4..5849031e8 100644 --- a/src/inputs/TreeSelectField/utils.ts +++ b/src/inputs/TreeSelectField/utils.ts @@ -3,7 +3,7 @@ import { Key } from "react"; import { Value } from "src/inputs/Value"; type FoundOption = { option: NestedOption; parents: NestedOption[] }; -export type NestedOption = O & { children?: NestedOption[] }; +export type NestedOption = O & { children?: NestedOption[]; defaultCollapsed?: boolean }; export type NestedOptionsOrLoad = | NestedOption[] | { current: NestedOption[]; load: () => Promise<{ options: NestedOption[] }> }; diff --git a/src/inputs/internal/ComboBoxBase.tsx b/src/inputs/internal/ComboBoxBase.tsx index e2650be30..caa19899b 100644 --- a/src/inputs/internal/ComboBoxBase.tsx +++ b/src/inputs/internal/ComboBoxBase.tsx @@ -344,7 +344,7 @@ export function ComboBoxBase(props: ComboBoxBaseProps) ...positionProps.style, width: comboBoxRef?.current?.clientWidth, // Ensures the menu never gets too small. - minWidth: 200, + minWidth: 320, }; const fieldMaxWidth = getFieldWidth(fullWidth); @@ -378,7 +378,7 @@ export function ComboBoxBase(props: ComboBoxBaseProps) positionProps={positionProps} onClose={() => state.close()} isOpen={state.isOpen} - minWidth={200} + minWidth={320} >