diff --git a/packages/react-core/src/components/DualListSelector/DualListSelectorPane.tsx b/packages/react-core/src/components/DualListSelector/DualListSelectorPane.tsx index a633181e03f..178de4d296b 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelectorPane.tsx +++ b/packages/react-core/src/components/DualListSelector/DualListSelectorPane.tsx @@ -13,7 +13,7 @@ import { SearchInput } from '../SearchInput'; * such as sorting, can also be passed into this sub-component. */ -export interface DualListSelectorPaneProps { +export interface DualListSelectorPaneProps extends Omit, 'title'> { /** Additional classes applied to the dual list selector pane. */ className?: string; /** A dual list selector list or dual list selector tree to be rendered in the pane. */ @@ -65,6 +65,8 @@ export interface DualListSelectorPaneProps { searchInputAriaLabel?: string; /** @hide Callback for updating the filtered options in DualListSelector. To be used when isSearchable is true. */ onFilterUpdate?: (newFilteredOptions: React.ReactNode[], paneType: string, isSearchReset: boolean) => void; + /** Minimum height of the list of options rendered in the pane. **/ + listMinHeight?: string; } export const DualListSelectorPane: React.FunctionComponent = ({ @@ -87,6 +89,7 @@ export const DualListSelectorPane: React.FunctionComponent { const [input, setInput] = React.useState(''); @@ -198,12 +201,21 @@ export const DualListSelectorPane: React.FunctionComponent {children} )} {isTree && ( - + {options.length > 0 ? ( { } }; - // builds a search input - used in each dual list selector pane - const buildSearchInput = (isAvailable: boolean) => { - const onChange = (value: string) => { - isAvailable ? setAvailableFilter(value) : setChosenFilter(value); - const toFilter = isAvailable ? [...availableOptions] : [...chosenOptions]; - toFilter.forEach(option => { - option.isVisible = value === '' || option.text.toLowerCase().includes(value.toLowerCase()); - }); - }; - - return ( - onChange('')} - /> - ); + const onFilterChange = (value: string, isAvailable: boolean) => { + isAvailable ? setAvailableFilter(value) : setChosenFilter(value); + const toFilter = isAvailable ? [...availableOptions] : [...chosenOptions]; + toFilter.forEach(option => { + option.isVisible = value === '' || option.text.toLowerCase().includes(value.toLowerCase()); + }); }; + // builds a search input - used in each dual list selector pane + const buildSearchInput = (isAvailable: boolean) => ( + onFilterChange(value, isAvailable)} + onClear={() => onFilterChange('', isAvailable)} + /> + ); + // builds a sort control - passed to both dual list selector panes const buildSort = (isAvailable: boolean) => { const onSort = () => { @@ -132,6 +137,21 @@ export const DualListSelectorComposable: React.FunctionComponent = () => { ); }; + const buildEmptyState = (isAvailable: boolean) => ( + + + + No results found + + No results match the filter criteria. Clear all filters and try again. + + + + + ); + return ( { } options selected`} searchInput={buildSearchInput(true)} actions={[buildSort(true)]} + listMinHeight="300px" > - - {availableOptions.map((option, index) => - option.isVisible ? ( - onOptionSelect(e, index, false)} - > - {option.text} - - ) : null - )} - + {availableFilter !== '' && + availableOptions.filter(option => option.isVisible).length === 0 && + buildEmptyState(true)} + {availableOptions.filter(option => option.isVisible).length > 0 && ( + + {availableOptions.map((option, index) => + option.isVisible ? ( + onOptionSelect(e, index, false)} + > + {option.text} + + ) : null + )} + + )} { searchInput={buildSearchInput(false)} actions={[buildSort(false)]} isChosen + listMinHeight="300px" > - - {chosenOptions.map((option, index) => - option.isVisible ? ( - onOptionSelect(e, index, true)} - > - {option.text} - - ) : null - )} - + {chosenFilter !== '' && chosenOptions.filter(option => option.isVisible).length === 0 && buildEmptyState(false)} + {chosenOptions.filter(option => option.isVisible).length > 0 && ( + + {chosenOptions.map((option, index) => + option.isVisible ? ( + onOptionSelect(e, index, true)} + > + {option.text} + + ) : null + )} + + )} ); diff --git a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposableTree.tsx b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposableTree.tsx index c88ae3a46ee..e711d7baa99 100644 --- a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposableTree.tsx +++ b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposableTree.tsx @@ -7,12 +7,20 @@ import { DualListSelectorControl, DualListSelectorTree, DualListSelectorTreeItemData, - SearchInput + SearchInput, + Title, + Button, + EmptyState, + EmptyStateVariant, + EmptyStateIcon, + EmptyStateBody, + EmptyStatePrimary } from '@patternfly/react-core'; import AngleDoubleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-left-icon'; import AngleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-left-icon'; import AngleDoubleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-right-icon'; import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-icon'; +import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; interface FoodNode { id: string; @@ -235,19 +243,37 @@ export const DualListSelectorComposableTree: React.FunctionComponent - - onOptionCheck(e, isChecked, itemData, isChosen)} - /> - + {filterApplied && options.length === 0 && ( + + + + No results found + + No results match the filter criteria. Clear all filters and try again. + + + + + )} + {options.length > 0 && ( + + onOptionCheck(e, isChecked, itemData, isChosen)} + /> + + )} ); };