diff --git a/packages/react-integration/demo-app-ts/src/Demos.ts b/packages/react-integration/demo-app-ts/src/Demos.ts index da3c73365c1..19dc20b683f 100644 --- a/packages/react-integration/demo-app-ts/src/Demos.ts +++ b/packages/react-integration/demo-app-ts/src/Demos.ts @@ -142,6 +142,21 @@ export const Demos: DemoInterface[] = [ name: 'Dropdown Demo', componentType: Examples.DropdownDemo }, + { + id: 'dual-list-selector-basic-demo', + name: 'DualListSelector basic Demo', + componentType: Examples.DualListSelectorBasicDemo + }, + { + id: 'dual-list-selector-tree-demo', + name: 'DualListSelector Tree Demo', + componentType: Examples.DualListSelectorTreeDemo + }, + { + id: 'dual-list-selector-with-actions-demo', + name: 'DualListSelector with actions Demo', + componentType: Examples.DualListSelectorWithActionsDemo + }, { id: 'dual-list-selector-deprecated-basic-demo', name: 'DualListSelector deprecated basic Demo', diff --git a/packages/react-integration/demo-app-ts/src/components/demos/DualListSelectorDemo/DualListSelectorBasicDemo.tsx b/packages/react-integration/demo-app-ts/src/components/demos/DualListSelectorDemo/DualListSelectorBasicDemo.tsx new file mode 100644 index 00000000000..784c4c7c96d --- /dev/null +++ b/packages/react-integration/demo-app-ts/src/components/demos/DualListSelectorDemo/DualListSelectorBasicDemo.tsx @@ -0,0 +1,161 @@ +import React from 'react'; +import { + DualListSelector, + DualListSelectorPane, + DualListSelectorList, + DualListSelectorListItem, + DualListSelectorControlsWrapper, + DualListSelectorControl +} 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'; + +interface Option { + text: string; + selected: boolean; + isVisible: boolean; +} + +export const DualListSelectorBasicDemo: React.FunctionComponent = () => { + const [availableOptions, setAvailableOptions] = React.useState([ + { text: 'Option 1', selected: false, isVisible: true }, + { text: 'Option 2', selected: false, isVisible: true }, + { text: 'Option 3', selected: false, isVisible: true }, + { text: 'Option 4', selected: false, isVisible: true } + ]); + const [chosenOptions, setChosenOptions] = React.useState([]); + + // callback for moving selected options between lists + const moveSelected = (fromAvailable: boolean) => { + const sourceOptions = fromAvailable ? availableOptions : chosenOptions; + const destinationOptions = fromAvailable ? chosenOptions : availableOptions; + for (let i = 0; i < sourceOptions.length; i++) { + const option = sourceOptions[i]; + if (option.selected && option.isVisible) { + sourceOptions.splice(i, 1); + destinationOptions.push(option); + option.selected = false; + i--; + } + } + if (fromAvailable) { + setAvailableOptions([...sourceOptions]); + setChosenOptions([...destinationOptions]); + } else { + setChosenOptions([...sourceOptions]); + setAvailableOptions([...destinationOptions]); + } + }; + + // callback for moving all options between lists + const moveAll = (fromAvailable: boolean) => { + if (fromAvailable) { + setChosenOptions([...availableOptions.filter((option) => option.isVisible), ...chosenOptions]); + setAvailableOptions([...availableOptions.filter((option) => !option.isVisible)]); + } else { + setAvailableOptions([...chosenOptions.filter((option) => option.isVisible), ...availableOptions]); + setChosenOptions([...chosenOptions.filter((option) => !option.isVisible)]); + } + }; + + // callback when option is selected + const onOptionSelect = ( + _event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, + index: number, + isChosen: boolean + ) => { + if (isChosen) { + const newChosen = [...chosenOptions]; + newChosen[index].selected = !chosenOptions[index].selected; + setChosenOptions(newChosen); + } else { + const newAvailable = [...availableOptions]; + newAvailable[index].selected = !availableOptions[index].selected; + setAvailableOptions(newAvailable); + } + }; + + return ( + + option.selected && option.isVisible).length} of ${ + availableOptions.filter((option) => option.isVisible).length + } options selected`} + > + + {availableOptions.map((option, index) => + option.isVisible ? ( + onOptionSelect(e, index, false)} + > + {option.text} + + ) : null + )} + + + + option.selected)} + onClick={() => moveSelected(true)} + aria-label="Add selected" + tooltipContent="Add selected" + tooltipProps={{ position: 'top', 'aria-live': 'off' }} + icon={} + /> + moveAll(true)} + aria-label="Add all" + tooltipContent="Add all" + tooltipProps={{ position: 'right', 'aria-live': 'off' }} + icon={} + /> + moveAll(false)} + aria-label="Remove all" + tooltipContent="Remove all" + tooltipProps={{ position: 'left', 'aria-live': 'off' }} + icon={} + /> + moveSelected(false)} + isDisabled={!chosenOptions.some((option) => option.selected)} + aria-label="Remove selected" + tooltipContent="Remove selected" + tooltipProps={{ position: 'bottom', 'aria-live': 'off' }} + icon={} + /> + + option.selected && option.isVisible).length} of ${ + chosenOptions.filter((option) => option.isVisible).length + } options selected`} + isChosen + > + + {chosenOptions.map((option, index) => + option.isVisible ? ( + onOptionSelect(e, index, true)} + > + {option.text} + + ) : null + )} + + + + ); +}; diff --git a/packages/react-integration/demo-app-ts/src/components/demos/DualListSelectorDemo/DualListSelectorTreeDemo.tsx b/packages/react-integration/demo-app-ts/src/components/demos/DualListSelectorDemo/DualListSelectorTreeDemo.tsx new file mode 100644 index 00000000000..cecf9debcac --- /dev/null +++ b/packages/react-integration/demo-app-ts/src/components/demos/DualListSelectorDemo/DualListSelectorTreeDemo.tsx @@ -0,0 +1,370 @@ +import React from 'react'; +import { + DualListSelector, + DualListSelectorPane, + DualListSelectorList, + DualListSelectorControlsWrapper, + DualListSelectorControl, + DualListSelectorTree, + DualListSelectorTreeItemData, + SearchInput, + Button, + EmptyState, + EmptyStateVariant, + EmptyStateFooter, + EmptyStateBody, + EmptyStateActions +} 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; + text: string; + children?: FoodNode[]; +} + +interface ExampleProps { + data: FoodNode[]; +} + +const DualListSelectorComposableTree: React.FunctionComponent = ({ data }: ExampleProps) => { + const [checkedLeafIds, setCheckedLeafIds] = React.useState([]); + const [chosenLeafIds, setChosenLeafIds] = React.useState(['beans', 'beef', 'chicken', 'tofu']); + const [chosenFilter, setChosenFilter] = React.useState(''); + const [availableFilter, setAvailableFilter] = React.useState(''); + let hiddenChosen: string[] = []; + let hiddenAvailable: string[] = []; + + // helper function to build memoized lists + const buildTextById = (node: FoodNode): { [key: string]: string } => { + let textById: { [key: string]: string } = {}; + if (!node) { + return textById; + } + textById[node.id] = node.text; + if (node.children) { + node.children.forEach((child) => { + textById = { ...textById, ...buildTextById(child) }; + }); + } + return textById; + }; + + // helper function to build memoized lists + const getDescendantLeafIds = (node: FoodNode): string[] => { + if (!node.children || !node.children.length) { + return [node.id]; + } else { + let childrenIds: string[] = []; + node.children.forEach((child) => { + childrenIds = [...childrenIds, ...getDescendantLeafIds(child)]; + }); + return childrenIds; + } + }; + + // helper function to build memoized lists + const getLeavesById = (node: FoodNode): { [key: string]: string[] } => { + let leavesById: { [key: string]: string[] } = {}; + if (!node.children || !node.children.length) { + leavesById[node.id] = [node.id]; + } else { + node.children.forEach((child) => { + leavesById[node.id] = getDescendantLeafIds(node); + leavesById = { ...leavesById, ...getLeavesById(child) }; + }); + } + return leavesById; + }; + + // Builds a map of child leaf nodes by node id - memoized so that it only rebuilds the list if the data changes. + const { memoizedLeavesById, memoizedAllLeaves, memoizedNodeText } = React.useMemo(() => { + let leavesById: { [key: string]: string[] } = {}; + let allLeaves: string[] = []; + let nodeTexts: { [key: string]: string } = {}; + data.forEach((foodNode) => { + nodeTexts = { ...nodeTexts, ...buildTextById(foodNode) }; + leavesById = { ...leavesById, ...getLeavesById(foodNode) }; + allLeaves = [...allLeaves, ...getDescendantLeafIds(foodNode)]; + }); + return { + memoizedLeavesById: leavesById, + memoizedAllLeaves: allLeaves, + memoizedNodeText: nodeTexts + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data]); + + const moveChecked = (toChosen: boolean) => { + setChosenLeafIds( + (prevChosenIds) => + toChosen + ? [...prevChosenIds, ...checkedLeafIds] // add checked ids to chosen list + : [...prevChosenIds.filter((x) => !checkedLeafIds.includes(x))] // remove checked ids from chosen list + ); + + // uncheck checked ids that just moved + setCheckedLeafIds((prevChecked) => + toChosen + ? [...prevChecked.filter((x) => chosenLeafIds.includes(x))] + : [...prevChecked.filter((x) => !chosenLeafIds.includes(x))] + ); + }; + + const moveAll = (toChosen: boolean) => { + if (toChosen) { + setChosenLeafIds(memoizedAllLeaves); + } else { + setChosenLeafIds([]); + } + }; + + const areAllDescendantsSelected = (node: FoodNode, isChosen: boolean) => + memoizedLeavesById[node.id].every( + (id) => checkedLeafIds.includes(id) && (isChosen ? chosenLeafIds.includes(id) : !chosenLeafIds.includes(id)) + ); + const areSomeDescendantsSelected = (node: FoodNode, isChosen: boolean) => + memoizedLeavesById[node.id].some( + (id) => checkedLeafIds.includes(id) && (isChosen ? chosenLeafIds.includes(id) : !chosenLeafIds.includes(id)) + ); + + const isNodeChecked = (node: FoodNode, isChosen: boolean) => { + if (areAllDescendantsSelected(node, isChosen)) { + return true; + } + if (areSomeDescendantsSelected(node, isChosen)) { + return false; + } + return false; + }; + + const onOptionCheck = ( + event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, + isChecked: boolean, + node: DualListSelectorTreeItemData, + isChosen: boolean + ) => { + const nodeIdsToCheck = memoizedLeavesById[node.id].filter((id) => + isChosen + ? chosenLeafIds.includes(id) && !hiddenChosen.includes(id) + : !chosenLeafIds.includes(id) && !hiddenAvailable.includes(id) + ); + if (isChosen) { + hiddenChosen = []; + } else { + hiddenAvailable = []; + } + setCheckedLeafIds((prevChecked) => { + const otherCheckedNodeNames = prevChecked.filter((id) => !nodeIdsToCheck.includes(id)); + return !isChecked ? otherCheckedNodeNames : [...otherCheckedNodeNames, ...nodeIdsToCheck]; + }); + }; + + // builds a search input - used in each dual list selector pane + const buildSearchInput = (isChosen: boolean) => { + const onChange = (value: string) => (isChosen ? setChosenFilter(value) : setAvailableFilter(value)); + + return ( + onChange(value)} + onClear={() => onChange('')} + /> + ); + }; + + // Builds the DualListSelectorTreeItems from the FoodNodes + const buildOptions = ( + isChosen: boolean, + [node, ...remainingNodes]: FoodNode[], + hasParentMatch: boolean + ): DualListSelectorTreeItemData[] => { + if (!node) { + return []; + } + + const isChecked = isNodeChecked(node, isChosen); + + const filterValue = (isChosen ? chosenFilter : availableFilter).toLowerCase().trim(); + const descendentLeafIds = memoizedLeavesById[node.id]; + const descendentsOnThisPane = isChosen + ? descendentLeafIds.filter((id) => chosenLeafIds.includes(id)) + : descendentLeafIds.filter((id) => !chosenLeafIds.includes(id)); + + const hasMatchingChildren = + filterValue && descendentsOnThisPane.some((id) => memoizedNodeText[id].toLowerCase().includes(filterValue)); + const isFilterMatch = + filterValue && node.text.toLowerCase().includes(filterValue) && descendentsOnThisPane.length > 0; + + // A node is displayed if either of the following is true: + // - There is no filter value and this node or its descendents belong on this pane + // - There is a filter value and this node or one of this node's descendents or ancestors match on this pane + const isDisplayed = + (!filterValue && descendentsOnThisPane.length > 0) || + hasMatchingChildren || + (hasParentMatch && descendentsOnThisPane.length > 0) || + isFilterMatch; + + if (!isDisplayed) { + if (isChosen) { + hiddenChosen.push(node.id); + } else { + hiddenAvailable.push(node.id); + } + } + + return [ + ...(isDisplayed + ? [ + { + id: node.id, + text: node.text, + isChecked, + checkProps: { 'aria-label': `Select ${node.text}` }, + hasBadge: node.children && node.children.length > 0, + badgeProps: { isRead: true }, + defaultExpanded: isChosen ? !!chosenFilter : !!availableFilter, + children: node.children + ? buildOptions(isChosen, node.children, isFilterMatch || hasParentMatch) + : undefined + } + ] + : []), + ...(!isDisplayed && node.children && node.children.length + ? buildOptions(isChosen, node.children, hasParentMatch) + : []), + ...(remainingNodes ? buildOptions(isChosen, remainingNodes, hasParentMatch) : []) + ]; + }; + + const buildPane = (isChosen: boolean): React.ReactNode => { + const options: DualListSelectorTreeItemData[] = buildOptions(isChosen, data, false); + const numOptions = isChosen ? chosenLeafIds.length : memoizedAllLeaves.length - chosenLeafIds.length; + const numSelected = checkedLeafIds.filter((id) => + isChosen ? chosenLeafIds.includes(id) : !chosenLeafIds.includes(id) + ).length; + const status = `${numSelected} of ${numOptions} options selected`; + const filterApplied = isChosen ? chosenFilter !== '' : availableFilter !== ''; + return ( + + {filterApplied && options.length === 0 && ( + + No results match the filter criteria. Clear all filters and try again. + + + + + + + )} + {options.length > 0 && ( + + onOptionCheck(e, isChecked, itemData, isChosen)} + /> + + )} + + ); + }; + + return ( + + {buildPane(false)} + + !chosenLeafIds.includes(x)).length} + onClick={() => moveChecked(true)} + aria-label="Add selected" + icon={} + /> + moveAll(true)} + aria-label="Add all" + icon={} + /> + moveAll(false)} + aria-label="Remove all" + icon={} + /> + moveChecked(false)} + isDisabled={!checkedLeafIds.filter((x) => !!chosenLeafIds.includes(x)).length} + aria-label="Remove selected" + icon={} + /> + + {buildPane(true)} + + ); +}; + +export const DualListSelectorTreeDemo: React.FunctionComponent = () => ( + +); diff --git a/packages/react-integration/demo-app-ts/src/components/demos/DualListSelectorDemo/DualListSelectorWithActionsDemo.tsx b/packages/react-integration/demo-app-ts/src/components/demos/DualListSelectorDemo/DualListSelectorWithActionsDemo.tsx new file mode 100644 index 00000000000..ee331f50dc3 --- /dev/null +++ b/packages/react-integration/demo-app-ts/src/components/demos/DualListSelectorDemo/DualListSelectorWithActionsDemo.tsx @@ -0,0 +1,362 @@ +import React from 'react'; +import { + Button, + ButtonVariant, + Checkbox, + Dropdown, + DropdownList, + DropdownItem, + DualListSelector, + DualListSelectorPane, + DualListSelectorList, + DualListSelectorListItem, + DualListSelectorControlsWrapper, + DualListSelectorControl, + SearchInput, + EmptyState, + EmptyStateVariant, + EmptyStateFooter, + EmptyStateBody, + EmptyStateActions, + MenuToggle, + MenuToggleElement +} 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 PficonSortCommonAscIcon from '@patternfly/react-icons/dist/esm/icons/pficon-sort-common-asc-icon'; +import PficonSortCommonDescIcon from '@patternfly/react-icons/dist/esm/icons/pficon-sort-common-desc-icon'; +import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; +import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; + +interface Option { + text: string; + selected: boolean; + isVisible: boolean; +} + +type SortDirection = 'asc' | 'desc'; + +export const DualListSelectorWithActionsDemo: React.FunctionComponent = () => { + const [availableOptions, setAvailableOptions] = React.useState([ + { text: 'Option 1', selected: false, isVisible: true }, + { text: 'Option 2', selected: false, isVisible: true }, + { text: 'Option 3', selected: false, isVisible: true }, + { text: 'Option 4', selected: false, isVisible: true } + ]); + + const [chosenOptions, setChosenOptions] = React.useState([]); + const [isAvailableKebabOpen, setIsAvailableKebabOpen] = React.useState(false); + const [isChosenKebabOpen, setIsChosenKebabOpen] = React.useState(false); + const [availableFilter, setAvailableFilter] = React.useState(''); + const [chosenFilter, setChosenFilter] = React.useState(''); + const [availableSortDirection, setAvailableSortDirection] = React.useState('desc'); + const [chosenSortDirection, setChosenSortDirection] = React.useState('desc'); + const [isDisabled, setIsDisabled] = React.useState(false); + + // callback for moving selected options between lists + const moveSelected = (fromAvailable: boolean) => { + const sourceOptions = fromAvailable ? availableOptions : chosenOptions; + const destinationOptions = fromAvailable ? chosenOptions : availableOptions; + for (let i = 0; i < sourceOptions.length; i++) { + const option = sourceOptions[i]; + if (option.selected && option.isVisible) { + sourceOptions.splice(i, 1); + destinationOptions.push(option); + option.selected = false; + i--; + } + } + if (fromAvailable) { + setAvailableOptions([...sourceOptions]); + setChosenOptions([...destinationOptions]); + } else { + setChosenOptions([...sourceOptions]); + setAvailableOptions([...destinationOptions]); + } + }; + + // callback for moving all options between lists + const moveAll = (fromAvailable: boolean) => { + if (fromAvailable) { + setChosenOptions([...availableOptions.filter((option) => option.isVisible), ...chosenOptions]); + setAvailableOptions([...availableOptions.filter((option) => !option.isVisible)]); + } else { + setAvailableOptions([...chosenOptions.filter((option) => option.isVisible), ...availableOptions]); + setChosenOptions([...chosenOptions.filter((option) => !option.isVisible)]); + } + }; + + // callback when option is selected + const onOptionSelect = ( + event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, + index: number, + isChosen: boolean + ) => { + if (isChosen) { + const newChosen = [...chosenOptions]; + newChosen[index].selected = !chosenOptions[index].selected; + setChosenOptions(newChosen); + } else { + const newAvailable = [...availableOptions]; + newAvailable[index].selected = !availableOptions[index].selected; + setAvailableOptions(newAvailable); + } + }; + + 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)} + isDisabled={isDisabled} + /> + ); + + // builds a sort control - passed to both dual list selector panes + const buildSort = (isAvailable: boolean) => { + const sortHelper = { + getOppositeDirection(direction: SortDirection) { + return direction === 'asc' ? 'desc' : 'asc'; + }, + getFullDirectionName(direction: SortDirection) { + return direction === 'asc' ? 'ascending' : 'descending'; + }, + getIcon(direction: SortDirection) { + return direction === 'asc' ? : ; + } + }; + + const onSort = () => { + const toSort = isAvailable ? [...availableOptions] : [...chosenOptions]; + + const sortDirection = isAvailable ? availableSortDirection : chosenSortDirection; + const sortCoefficient = sortDirection === 'asc' ? 1 : -1; + + toSort.sort((a, b) => { + if (a.text > b.text) { + return sortCoefficient; + } + if (a.text < b.text) { + return sortCoefficient * -1; + } + return 0; + }); + + if (isAvailable) { + setAvailableOptions(toSort); + setAvailableSortDirection(sortHelper.getOppositeDirection(availableSortDirection)); + } else { + setChosenOptions(toSort); + setChosenSortDirection(sortHelper.getOppositeDirection(chosenSortDirection)); + } + }; + + const onToggle = (pane: string) => { + if (pane === 'available') { + setIsAvailableKebabOpen(!isAvailableKebabOpen); + } else { + setIsChosenKebabOpen(!isChosenKebabOpen); + } + }; + + return isAvailable + ? [ + + + + + ); + + return ( + + + option.selected && option.isVisible).length} of ${ + availableOptions.filter((option) => option.isVisible).length + } options selected`} + searchInput={buildSearchInput(true)} + actions={[buildSort(true)]} + listMinHeight="300px" + isDisabled={isDisabled} + > + {availableFilter !== '' && + availableOptions.filter((option) => option.isVisible).length === 0 && + buildEmptyState(true)} + + + {availableOptions.map((option, index) => + option.isVisible ? ( + onOptionSelect(e, index, false)} + isDisabled={isDisabled} + > + {option.text} + + ) : null + )} + + + + option.selected) || isDisabled} + onClick={() => moveSelected(true)} + aria-label="Add selected" + icon={} + /> + moveAll(true)} + aria-label="Add all" + icon={} + /> + moveAll(false)} + aria-label="Remove all" + icon={} + /> + moveSelected(false)} + isDisabled={!chosenOptions.some((option) => option.selected) || isDisabled} + aria-label="Remove selected" + icon={} + /> + + option.selected && option.isVisible).length} of ${ + chosenOptions.filter((option) => option.isVisible).length + } options selected`} + searchInput={buildSearchInput(false)} + actions={[buildSort(false)]} + listMinHeight="300px" + isChosen + > + {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)} + isDisabled={isDisabled} + > + {option.text} + + ) : null + )} + + )} + + + setIsDisabled(!isDisabled)} + /> + + ); +}; diff --git a/packages/react-integration/demo-app-ts/src/components/demos/index.ts b/packages/react-integration/demo-app-ts/src/components/demos/index.ts index b4927f913b3..1e16d6877e4 100644 --- a/packages/react-integration/demo-app-ts/src/components/demos/index.ts +++ b/packages/react-integration/demo-app-ts/src/components/demos/index.ts @@ -24,6 +24,9 @@ export * from './DescriptionListDemo/DescriptionListBreakpointsDemo'; export * from './DrawerDemo/DrawerDemo'; export * from './DrawerDemo/DrawerResizeDemo'; export * from './DropdownDemo/DropdownDemo'; +export * from './DualListSelectorDemo/DualListSelectorBasicDemo'; +export * from './DualListSelectorDemo/DualListSelectorTreeDemo'; +export * from './DualListSelectorDemo/DualListSelectorWithActionsDemo'; export * from './DualListSelectorDeprecatedDemo/DualListSelectorDeprecatedBasicDemo'; export * from './DualListSelectorDeprecatedDemo/DualListSelectorDeprecatedTreeDemo'; export * from './DualListSelectorDeprecatedDemo/DualListSelectorDeprecatedWithActionsDemo';