diff --git a/packages/react-core/single-packages.config.json b/packages/react-core/single-packages.config.json index b018731de2d..da7e08727fc 100644 --- a/packages/react-core/single-packages.config.json +++ b/packages/react-core/single-packages.config.json @@ -1,5 +1,5 @@ { "packageName": "@patternfly/react-core", "moduleGlob": ["/dist/esm/helpers/**/*.js", "/dist/esm/styles/**/index.js", "/dist/esm/*/*/**/index.js"], - "exclude": ["/dist/esm/helpers/Popper/thirdparty"] + "exclude": ["/dist/esm/helpers/Popper/thirdparty", "/dist/esm/next"] } diff --git a/packages/react-core/src/components/DualListSelector/DualListSelector.tsx b/packages/react-core/src/components/DualListSelector/DualListSelector.tsx index 61dd0ee72be..2663f3fc74e 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelector.tsx +++ b/packages/react-core/src/components/DualListSelector/DualListSelector.tsx @@ -1,23 +1,7 @@ import * as React from 'react'; import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector'; import { css } from '@patternfly/react-styles'; -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 { DualListSelectorPane } from './DualListSelectorPane'; import { GenerateId, PickOptional } from '../../helpers'; -import { DualListSelectorTreeItemData } from './DualListSelectorTree'; -import { - flattenTree, - flattenTreeWithFolders, - filterFolders, - filterTreeItems, - filterTreeItemsWithoutFolders, - filterRestTreeItems -} from './treeUtils'; -import { DualListSelectorControlsWrapper } from './DualListSelectorControlsWrapper'; -import { DualListSelectorControl } from './DualListSelectorControl'; import { DualListSelectorContext } from './DualListSelectorContext'; /** Acts as a container for all other DualListSelector sub-components when using a @@ -27,763 +11,34 @@ import { DualListSelectorContext } from './DualListSelectorContext'; export interface DualListSelectorProps { /** Additional classes applied to the dual list selector. */ className?: string; - /** Id of the dual list selector. */ + /** ID of the dual list selector. */ id?: string; - /** Flag indicating if the dual list selector uses trees instead of simple lists */ + /** Flag indicating if the dual list selector uses trees instead of simple lists. */ isTree?: boolean; - /** Flag indicating if the dual list selector is in a disabled state */ - isDisabled?: boolean; - /** Content to be rendered in the dual list selector. Panes & controls will not be built dynamically when children are provided. */ + /** Content to be rendered in the dual list selector. */ children?: React.ReactNode; - /** Title applied to the dynamically built available options pane. */ - availableOptionsTitle?: string; - /** Options to display in the dynamically built available options pane. When using trees, the options should be in the DualListSelectorTreeItemData[] format. */ - availableOptions?: React.ReactNode[] | DualListSelectorTreeItemData[]; - /** Status message to display above the dynamically built available options pane. */ - availableOptionsStatus?: string; - /** Actions to be displayed above the dynamically built available options pane. */ - availableOptionsActions?: React.ReactNode[]; - /** Title applied to the dynamically built chosen options pane. */ - chosenOptionsTitle?: string; - /** Options to display in the dynamically built chosen options pane. When using trees, the options should be in the DualListSelectorTreeItemData[] format. */ - chosenOptions?: React.ReactNode[] | DualListSelectorTreeItemData[]; - /** Status message to display above the dynamically built chosen options pane.*/ - chosenOptionsStatus?: string; - /** Actions to be displayed above the dynamically built chosen options pane. */ - chosenOptionsActions?: React.ReactNode[]; - /** Accessible label for the dynamically built controls between the two panes. */ - controlsAriaLabel?: string; - /** Optional callback for the dynamically built add selected button */ - addSelected?: (newAvailableOptions: React.ReactNode[], newChosenOptions: React.ReactNode[]) => void; - /** Accessible label for the dynamically built add selected button */ - addSelectedAriaLabel?: string; - /** Tooltip content for the dynamically built add selected button */ - addSelectedTooltip?: React.ReactNode; - /** Additonal tooltip properties for the dynamically built add selected tooltip */ - addSelectedTooltipProps?: any; - /** Callback fired every time dynamically built options are chosen or removed */ - onListChange?: ( - event: React.MouseEvent, - newAvailableOptions: React.ReactNode[], - newChosenOptions: React.ReactNode[] - ) => void; - /** Optional callback for the dynamically built add all button */ - addAll?: (newAvailableOptions: React.ReactNode[], newChosenOptions: React.ReactNode[]) => void; - /** Accessible label for the dynamically built add all button */ - addAllAriaLabel?: string; - /** Tooltip content for the dynamically built add all button */ - addAllTooltip?: React.ReactNode; - /** Additonal tooltip properties for the dynamically built add all tooltip */ - addAllTooltipProps?: any; - /** Optional callback for the dynamically built remove selected button */ - removeSelected?: (newAvailableOptions: React.ReactNode[], newChosenOptions: React.ReactNode[]) => void; - /** Accessible label for the dynamically built remove selected button */ - removeSelectedAriaLabel?: string; - /** Tooltip content for the dynamically built remove selected button */ - removeSelectedTooltip?: React.ReactNode; - /** Additonal tooltip properties for the dynamically built remove selected tooltip */ - removeSelectedTooltipProps?: any; - /** Optional callback for the dynamically built remove all button */ - removeAll?: (newAvailableOptions: React.ReactNode[], newChosenOptions: React.ReactNode[]) => void; - /** Accessible label for the dynamically built remove all button */ - removeAllAriaLabel?: string; - /** Tooltip content for the dynamically built remove all button */ - removeAllTooltip?: React.ReactNode; - /** Additonal tooltip properties for the dynamically built remove all tooltip */ - removeAllTooltipProps?: any; - /** Optional callback fired when a dynamically built option is selected */ - onOptionSelect?: ( - event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, - index: number, - isChosen: boolean, - id: string, - itemData: any, - parentData: any - ) => void; - /** Optional callback fired when a dynamically built option is checked */ - onOptionCheck?: ( - event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, - checked: boolean, - checkedId: string, - newCheckedItems: string[] - ) => void; - /** Flag indicating a search bar should be included above both the dynamically built available and chosen panes. */ - isSearchable?: boolean; - /** Accessible label for the search input on the dynamically built available options pane. */ - availableOptionsSearchAriaLabel?: string; - /** A callback for when the search input value for the dynamically built available options changes. */ - onAvailableOptionsSearchInputChanged?: (event: React.FormEvent, value: string) => void; - /** Accessible label for the search input on the dynamically built chosen options pane. */ - chosenOptionsSearchAriaLabel?: string; - /** A callback for when the search input value for the dynamically built chosen options changes. */ - onChosenOptionsSearchInputChanged?: (event: React.FormEvent, value: string) => void; - /** Optional filter function for custom filtering based on search string. Used with a dynamically built search input. */ - filterOption?: (option: React.ReactNode, input: string) => boolean; } -interface DualListSelectorState { - availableOptions: React.ReactNode[]; - availableOptionsSelected: number[]; - availableFilteredOptions: React.ReactNode[]; - chosenOptions: React.ReactNode[]; - chosenOptionsSelected: number[]; - chosenFilteredOptions: React.ReactNode[]; - availableTreeFilteredOptions: string[]; - availableTreeOptionsChecked: string[]; - chosenTreeOptionsChecked: string[]; - chosenTreeFilteredOptions: string[]; -} - -class DualListSelector extends React.Component { +class DualListSelector extends React.Component { static displayName = 'DualListSelector'; - private addAllButtonRef = React.createRef(); - private addSelectedButtonRef = React.createRef(); - private removeSelectedButtonRef = React.createRef(); - private removeAllButtonRef = React.createRef(); static defaultProps: PickOptional = { children: '', - availableOptions: [], - availableOptionsTitle: 'Available options', - availableOptionsSearchAriaLabel: 'Available search input', - chosenOptions: [], - chosenOptionsTitle: 'Chosen options', - chosenOptionsSearchAriaLabel: 'Chosen search input', - controlsAriaLabel: 'Selector controls', - addAllAriaLabel: 'Add all', - addSelectedAriaLabel: 'Add selected', - removeSelectedAriaLabel: 'Remove selected', - removeAllAriaLabel: 'Remove all', - isTree: false, - isDisabled: false + isTree: false }; - // If the DualListSelector uses trees, concat the two initial arrays and merge duplicate folder IDs - private createMergedCopy() { - const copyOfAvailable = JSON.parse(JSON.stringify(this.props.availableOptions)); - const copyOfChosen = JSON.parse(JSON.stringify(this.props.chosenOptions)); - - return this.props.isTree - ? Object.values( - (copyOfAvailable as DualListSelectorTreeItemData[]) - .concat(copyOfChosen as DualListSelectorTreeItemData[]) - .reduce((mapObj: any, item: DualListSelectorTreeItemData) => { - const key = item.id; - if (mapObj[key]) { - // If map already has an item ID, add the dupe ID's children to the existing map - mapObj[key].children.push(...item.children); - } else { - // Else clone the item data - mapObj[key] = { ...item }; - } - return mapObj; - }, {}) - ) - : null; - } - constructor(props: DualListSelectorProps) { super(props); - this.state = { - availableOptions: [...this.props.availableOptions] as React.ReactNode[], - availableOptionsSelected: [], - availableFilteredOptions: null, - availableTreeFilteredOptions: null, - chosenOptions: [...this.props.chosenOptions] as React.ReactNode[], - chosenOptionsSelected: [], - chosenFilteredOptions: null, - chosenTreeFilteredOptions: null, - availableTreeOptionsChecked: [], - chosenTreeOptionsChecked: [] - }; - } - - /** In dev environment, prevents circular structure during JSON stringification when - * options passed in to the dual list selector include HTML elements. - */ - replacer = (key: string, value: any) => { - if (key[0] === '_') { - return undefined; - } - return value; - }; - - componentDidUpdate() { - if ( - JSON.stringify(this.props.availableOptions, this.replacer) !== - JSON.stringify(this.state.availableOptions, this.replacer) || - JSON.stringify(this.props.chosenOptions, this.replacer) !== - JSON.stringify(this.state.chosenOptions, this.replacer) - ) { - this.setState({ - availableOptions: [...this.props.availableOptions] as React.ReactNode[], - chosenOptions: [...this.props.chosenOptions] as React.ReactNode[] - }); - } } - onFilterUpdate = (newFilteredOptions: React.ReactNode[], paneType: string, isSearchReset: boolean) => { - const { isTree } = this.props; - if (paneType === 'available') { - if (isSearchReset) { - this.setState({ - availableFilteredOptions: null, - availableTreeFilteredOptions: null - }); - return; - } - if (isTree) { - this.setState({ - availableTreeFilteredOptions: flattenTreeWithFolders( - newFilteredOptions as unknown as DualListSelectorTreeItemData[] - ) - }); - } else { - this.setState({ - availableFilteredOptions: newFilteredOptions as React.ReactNode[] - }); - } - } else if (paneType === 'chosen') { - if (isSearchReset) { - this.setState({ - chosenFilteredOptions: null, - chosenTreeFilteredOptions: null - }); - return; - } - if (isTree) { - this.setState({ - chosenTreeFilteredOptions: flattenTreeWithFolders( - newFilteredOptions as unknown as DualListSelectorTreeItemData[] - ) - }); - } else { - this.setState({ - chosenFilteredOptions: newFilteredOptions as React.ReactNode[] - }); - } - } - }; - - addAllVisible = (event: React.MouseEvent) => { - this.setState((prevState) => { - const itemsToRemove = [] as React.ReactNode[]; - const newAvailable = [] as React.ReactNode[]; - const movedOptions = prevState.availableFilteredOptions || prevState.availableOptions; - prevState.availableOptions.forEach((value) => { - if (movedOptions.indexOf(value) !== -1) { - itemsToRemove.push(value); - } else { - newAvailable.push(value); - } - }); - - const newChosen = [...prevState.chosenOptions, ...itemsToRemove]; - this.props.addAll && this.props.addAll(newAvailable, newChosen); - this.props.onListChange && this.props.onListChange(event, newAvailable, newChosen); - - return { - chosenOptions: newChosen, - chosenFilteredOptions: newChosen, - availableOptions: newAvailable, - availableFilteredOptions: newAvailable, - chosenOptionsSelected: [], - availableOptionsSelected: [] - }; - }); - }; - - addAllTreeVisible = (event: React.MouseEvent) => { - this.setState((prevState) => { - const movedOptions = - prevState.availableTreeFilteredOptions || - flattenTreeWithFolders(prevState.availableOptions as unknown as DualListSelectorTreeItemData[]); - const newAvailable = prevState.availableOptions - .map((opt) => Object.assign({}, opt)) - .filter((item) => - filterRestTreeItems(item as unknown as DualListSelectorTreeItemData, movedOptions) - ) as React.ReactNode[]; - - const currChosen = flattenTree(prevState.chosenOptions as unknown as DualListSelectorTreeItemData[]); - const nextChosenOptions = currChosen.concat(movedOptions); - const newChosen = this.createMergedCopy() - .map((opt) => Object.assign({}, opt)) - .filter((item) => - filterTreeItemsWithoutFolders(item as DualListSelectorTreeItemData, nextChosenOptions) - ) as React.ReactNode[]; - - this.props.addAll && this.props.addAll(newAvailable, newChosen); - this.props.onListChange && this.props.onListChange(event, newAvailable, newChosen); - - return { - chosenOptions: newChosen, - chosenFilteredOptions: newChosen, - availableOptions: newAvailable, - availableFilteredOptions: newAvailable, - availableTreeOptionsChecked: [], - chosenTreeOptionsChecked: [] - }; - }); - }; - - addSelected = (event: React.MouseEvent) => { - this.setState((prevState) => { - const itemsToRemove = [] as React.ReactNode[]; - const newAvailable = [] as React.ReactNode[]; - prevState.availableOptions.forEach((value, index) => { - if (prevState.availableOptionsSelected.indexOf(index) !== -1) { - itemsToRemove.push(value); - } else { - newAvailable.push(value); - } - }); - - const newChosen = [...prevState.chosenOptions, ...itemsToRemove]; - this.props.addSelected && this.props.addSelected(newAvailable, newChosen); - this.props.onListChange && this.props.onListChange(event, newAvailable, newChosen); - - return { - chosenOptionsSelected: [], - availableOptionsSelected: [], - chosenOptions: newChosen, - chosenFilteredOptions: newChosen, - availableOptions: newAvailable, - availableFilteredOptions: newAvailable - }; - }); - }; - - addTreeSelected = (event: React.MouseEvent) => { - this.setState((prevState) => { - // Remove selected available nodes from current available nodes - const newAvailable = prevState.availableOptions - .map((opt) => Object.assign({}, opt)) - .filter((item) => - filterRestTreeItems(item as unknown as DualListSelectorTreeItemData, prevState.availableTreeOptionsChecked) - ); - - // Get next chosen options from current + new nodes and remap from base - const currChosen = flattenTree(prevState.chosenOptions as unknown as DualListSelectorTreeItemData[]); - const nextChosenOptions = currChosen.concat(prevState.availableTreeOptionsChecked); - const newChosen = this.createMergedCopy() - .map((opt) => Object.assign({}, opt)) - .filter((item) => - filterTreeItemsWithoutFolders(item as DualListSelectorTreeItemData, nextChosenOptions) - ) as React.ReactNode[]; - - this.props.addSelected && this.props.addSelected(newAvailable, newChosen); - this.props.onListChange && this.props.onListChange(event, newAvailable, newChosen); - - return { - availableTreeOptionsChecked: [], - chosenTreeOptionsChecked: [], - availableOptions: newAvailable, - chosenOptions: newChosen - }; - }); - }; - - removeAllVisible = (event: React.MouseEvent) => { - this.setState((prevState) => { - const itemsToRemove = [] as React.ReactNode[]; - const newChosen = [] as React.ReactNode[]; - const movedOptions = prevState.chosenFilteredOptions || prevState.chosenOptions; - prevState.chosenOptions.forEach((value) => { - if (movedOptions.indexOf(value) !== -1) { - itemsToRemove.push(value); - } else { - newChosen.push(value); - } - }); - - const newAvailable = [...prevState.availableOptions, ...itemsToRemove]; - this.props.removeAll && this.props.removeAll(newAvailable, newChosen); - this.props.onListChange && this.props.onListChange(event, newAvailable, newChosen); - - return { - chosenOptions: newChosen, - chosenFilteredOptions: newChosen, - availableOptions: newAvailable, - availableFilteredOptions: newAvailable, - chosenOptionsSelected: [], - availableOptionsSelected: [] - }; - }); - }; - - removeAllTreeVisible = (event: React.MouseEvent) => { - this.setState((prevState) => { - const movedOptions = - prevState.chosenTreeFilteredOptions || - flattenTreeWithFolders(prevState.chosenOptions as unknown as DualListSelectorTreeItemData[]); - - const newChosen = prevState.chosenOptions - .map((opt) => Object.assign({}, opt)) - .filter((item) => filterRestTreeItems(item as unknown as DualListSelectorTreeItemData, movedOptions)); - const currAvailable = flattenTree(prevState.availableOptions as unknown as DualListSelectorTreeItemData[]); - const nextAvailableOptions = currAvailable.concat(movedOptions); - const newAvailable = this.createMergedCopy() - .map((opt) => Object.assign({}, opt)) - .filter((item) => - filterTreeItemsWithoutFolders(item as DualListSelectorTreeItemData, nextAvailableOptions) - ) as React.ReactNode[]; - - this.props.removeAll && this.props.removeAll(newAvailable, newChosen); - this.props.onListChange && this.props.onListChange(event, newAvailable, newChosen); - - return { - chosenOptions: newChosen, - availableOptions: newAvailable, - availableTreeOptionsChecked: [], - chosenTreeOptionsChecked: [] - }; - }); - }; - - removeSelected = (event: React.MouseEvent) => { - this.setState((prevState) => { - const itemsToRemove = [] as React.ReactNode[]; - const newChosen = [] as React.ReactNode[]; - prevState.chosenOptions.forEach((value, index) => { - if (prevState.chosenOptionsSelected.indexOf(index) !== -1) { - itemsToRemove.push(value); - } else { - newChosen.push(value); - } - }); - - const newAvailable = [...prevState.availableOptions, ...itemsToRemove]; - this.props.removeSelected && this.props.removeSelected(newAvailable, newChosen); - this.props.onListChange && this.props.onListChange(event, newAvailable, newChosen); - - return { - chosenOptionsSelected: [], - availableOptionsSelected: [], - chosenOptions: newChosen, - chosenFilteredOptions: newChosen, - availableOptions: newAvailable, - availableFilteredOptions: newAvailable - }; - }); - }; - - removeTreeSelected = (event: React.MouseEvent) => { - this.setState((prevState) => { - // Remove selected chosen nodes from current chosen nodes - const newChosen = prevState.chosenOptions - .map((opt) => Object.assign({}, opt)) - .filter((item) => - filterRestTreeItems(item as unknown as DualListSelectorTreeItemData, prevState.chosenTreeOptionsChecked) - ); - - // Get next chosen options from current and remap from base - const currAvailable = flattenTree(prevState.availableOptions as unknown as DualListSelectorTreeItemData[]); - const nextAvailableOptions = currAvailable.concat(prevState.chosenTreeOptionsChecked); - const newAvailable = this.createMergedCopy() - .map((opt) => Object.assign({}, opt)) - .filter((item) => - filterTreeItemsWithoutFolders(item as DualListSelectorTreeItemData, nextAvailableOptions) - ) as React.ReactNode[]; - - this.props.removeSelected && this.props.removeSelected(newAvailable, newChosen); - this.props.onListChange && this.props.onListChange(event, newAvailable, newChosen); - - return { - availableTreeOptionsChecked: [], - chosenTreeOptionsChecked: [], - availableOptions: newAvailable, - chosenOptions: newChosen - }; - }); - }; - - onOptionSelect = ( - e: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, - index: number, - isChosen: boolean, - /* eslint-disable @typescript-eslint/no-unused-vars */ - id?: string, - itemData?: any, - parentData?: any - /* eslint-enable @typescript-eslint/no-unused-vars */ - ) => { - this.setState((prevState) => { - const originalArray = isChosen ? prevState.chosenOptionsSelected : prevState.availableOptionsSelected; - - let updatedArray = null; - if (originalArray.indexOf(index) !== -1) { - updatedArray = originalArray.filter((value) => value !== index); - } else { - updatedArray = [...originalArray, index]; - } - - return { - chosenOptionsSelected: isChosen ? updatedArray : prevState.chosenOptionsSelected, - availableOptionsSelected: isChosen ? prevState.availableOptionsSelected : updatedArray - }; - }); - - this.props.onOptionSelect && this.props.onOptionSelect(e, index, isChosen, id, itemData, parentData); - }; - - isChecked = (treeItem: DualListSelectorTreeItemData, isChosen: boolean) => - isChosen - ? this.state.chosenTreeOptionsChecked.includes(treeItem.id) - : this.state.availableTreeOptionsChecked.includes(treeItem.id); - areAllDescendantsChecked = (treeItem: DualListSelectorTreeItemData, isChosen: boolean): boolean => - treeItem.children - ? treeItem.children.every((child) => this.areAllDescendantsChecked(child, isChosen)) - : this.isChecked(treeItem, isChosen); - areSomeDescendantsChecked = (treeItem: DualListSelectorTreeItemData, isChosen: boolean): boolean => - treeItem.children - ? treeItem.children.some((child) => this.areSomeDescendantsChecked(child, isChosen)) - : this.isChecked(treeItem, isChosen); - - mapChecked = (item: DualListSelectorTreeItemData, isChosen: boolean): DualListSelectorTreeItemData => { - const hasCheck = this.areAllDescendantsChecked(item, isChosen); - item.isChecked = false; - - if (hasCheck) { - item.isChecked = true; - } else { - const hasPartialCheck = this.areSomeDescendantsChecked(item, isChosen); - if (hasPartialCheck) { - item.isChecked = null; - } - } - - if (item.children) { - return { - ...item, - children: item.children.map((child) => this.mapChecked(child, isChosen)) - }; - } - return item; - }; - - onTreeOptionCheck = ( - evt: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, - isChecked: boolean, - itemData: DualListSelectorTreeItemData, - isChosen: boolean - ) => { - const { availableOptions, availableTreeFilteredOptions, chosenOptions, chosenTreeFilteredOptions } = this.state; - let panelOptions; - if (isChosen) { - if (chosenTreeFilteredOptions) { - panelOptions = chosenOptions - .map((opt) => Object.assign({}, opt)) - .filter((item) => - filterTreeItemsWithoutFolders(item as unknown as DualListSelectorTreeItemData, chosenTreeFilteredOptions) - ); - } else { - panelOptions = chosenOptions; - } - } else { - if (availableTreeFilteredOptions) { - panelOptions = availableOptions - .map((opt) => Object.assign({}, opt)) - .filter((item) => - filterTreeItemsWithoutFolders(item as unknown as DualListSelectorTreeItemData, availableTreeFilteredOptions) - ); - } else { - panelOptions = availableOptions; - } - } - const checkedOptionTree = panelOptions - .map((opt) => Object.assign({}, opt)) - .filter((item) => filterTreeItems(item as unknown as DualListSelectorTreeItemData, [itemData.id])); - const flatTree = flattenTreeWithFolders(checkedOptionTree as unknown as DualListSelectorTreeItemData[]); - - const prevChecked = isChosen ? this.state.chosenTreeOptionsChecked : this.state.availableTreeOptionsChecked; - let updatedChecked = [] as string[]; - if (isChecked) { - updatedChecked = prevChecked.concat(flatTree.filter((id) => !prevChecked.includes(id))); - } else { - updatedChecked = prevChecked.filter((id) => !flatTree.includes(id)); - } - - this.setState( - (prevState) => ({ - availableTreeOptionsChecked: isChosen ? prevState.availableTreeOptionsChecked : updatedChecked, - chosenTreeOptionsChecked: isChosen ? updatedChecked : prevState.chosenTreeOptionsChecked - }), - () => { - this.props.onOptionCheck && this.props.onOptionCheck(evt, isChecked, itemData.id, updatedChecked); - } - ); - }; - render() { - const { - availableOptionsTitle, - availableOptionsActions, - availableOptionsSearchAriaLabel, - className, - children, - chosenOptionsTitle, - chosenOptionsActions, - chosenOptionsSearchAriaLabel, - filterOption, - isSearchable, - chosenOptionsStatus, - availableOptionsStatus, - controlsAriaLabel, - addAllAriaLabel, - addSelectedAriaLabel, - removeSelectedAriaLabel, - removeAllAriaLabel, - /* eslint-disable @typescript-eslint/no-unused-vars */ - availableOptions: consumerPassedAvailableOptions, - chosenOptions: consumerPassedChosenOptions, - removeSelected, - addAll, - removeAll, - addSelected, - onListChange, - onAvailableOptionsSearchInputChanged, - onChosenOptionsSearchInputChanged, - onOptionSelect, - onOptionCheck, - id, - isTree, - isDisabled, - addAllTooltip, - addAllTooltipProps, - addSelectedTooltip, - addSelectedTooltipProps, - removeAllTooltip, - removeAllTooltipProps, - removeSelectedTooltip, - removeSelectedTooltipProps, - ...props - } = this.props; - const { - availableOptions, - chosenOptions, - chosenOptionsSelected, - availableOptionsSelected, - chosenTreeOptionsChecked, - availableTreeOptionsChecked - } = this.state; - const availableOptionsStatusToDisplay = - availableOptionsStatus || - (isTree - ? `${ - filterFolders(availableOptions as unknown as DualListSelectorTreeItemData[], availableTreeOptionsChecked) - .length - } of ${flattenTree(availableOptions as unknown as DualListSelectorTreeItemData[]).length} items selected` - : `${availableOptionsSelected.length} of ${availableOptions.length} items selected`); - const chosenOptionsStatusToDisplay = - chosenOptionsStatus || - (isTree - ? `${ - filterFolders(chosenOptions as unknown as DualListSelectorTreeItemData[], chosenTreeOptionsChecked).length - } of ${flattenTree(chosenOptions as unknown as DualListSelectorTreeItemData[]).length} items selected` - : `${chosenOptionsSelected.length} of ${chosenOptions.length} items selected`); - - const available = ( - isTree - ? availableOptions.map((item) => this.mapChecked(item as unknown as DualListSelectorTreeItemData, false)) - : availableOptions - ) as React.ReactNode[]; - const chosen = ( - isTree - ? chosenOptions.map((item) => this.mapChecked(item as unknown as DualListSelectorTreeItemData, true)) - : chosenOptions - ) as React.ReactNode[]; + const { className, children, id, isTree, ...props } = this.props; return ( {(randomId) => (
- {children === '' ? ( - <> - this.onTreeOptionCheck(e, isChecked, itemData, false)} - actions={availableOptionsActions} - id={`${id || randomId}-available-pane`} - isDisabled={isDisabled} - /> - - - - - - - - - - - - - - - this.onTreeOptionCheck(e, isChecked, itemData, true)} - actions={chosenOptionsActions} - id={`${id || randomId}-chosen-pane`} - isDisabled={isDisabled} - /> - - ) : ( - children - )} + {children}
)}
diff --git a/packages/react-core/src/components/DualListSelector/DualListSelectorContext.ts b/packages/react-core/src/components/DualListSelector/DualListSelectorContext.ts index 27b1ed1eff2..3eccc9a0dc6 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelectorContext.ts +++ b/packages/react-core/src/components/DualListSelector/DualListSelectorContext.ts @@ -12,7 +12,6 @@ export const DualListSelectorListContext = React.createContext<{ displayOption?: (option: React.ReactNode) => boolean; selectedOptions?: string[] | number[]; id?: string; - onOptionSelect?: (e: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, index: number, id: string) => void; options?: React.ReactNode[]; isDisabled?: boolean; }>({}); diff --git a/packages/react-core/src/components/DualListSelector/DualListSelectorControl.tsx b/packages/react-core/src/components/DualListSelector/DualListSelectorControl.tsx index 266098bdb85..8ce13d6f23c 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelectorControl.tsx +++ b/packages/react-core/src/components/DualListSelector/DualListSelectorControl.tsx @@ -29,7 +29,7 @@ export interface DualListSelectorControlProps extends Omit = ({ innerRef, - children = null, + children, className, 'aria-label': ariaLabel, isDisabled = true, diff --git a/packages/react-core/src/components/DualListSelector/DualListSelectorControlsWrapper.tsx b/packages/react-core/src/components/DualListSelector/DualListSelectorControlsWrapper.tsx index 835879f250a..ee36d662a0c 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelectorControlsWrapper.tsx +++ b/packages/react-core/src/components/DualListSelector/DualListSelectorControlsWrapper.tsx @@ -6,7 +6,7 @@ import { handleArrows } from '../../helpers'; /** Acts as the container for the DualListSelectorControl sub-components. */ export interface DualListSelectorControlsWrapperProps extends React.HTMLProps { - /** Anything that can be rendered inside of the wrapper. */ + /** Content to be rendered inside of the controls wrapper. */ children?: React.ReactNode; /** Additional classes added to the wrapper. */ className?: string; @@ -60,6 +60,7 @@ export const DualListSelectorControlsWrapperBase: React.FunctionComponent { window.removeEventListener('keydown', handleKeys); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [wrapperRef.current]); return ( diff --git a/packages/react-core/src/components/DualListSelector/DualListSelectorList.tsx b/packages/react-core/src/components/DualListSelector/DualListSelectorList.tsx index cc0224eef1f..13d00467e6b 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelectorList.tsx +++ b/packages/react-core/src/components/DualListSelector/DualListSelectorList.tsx @@ -7,7 +7,7 @@ import { DualListSelectorListContext } from './DualListSelectorContext'; /** Acts as the container for DualListSelectorListItem sub-components. */ export interface DualListSelectorListProps extends React.HTMLProps { - /** Content rendered inside the dual list selector list */ + /** Content rendered inside the dual list selector list. */ children?: React.ReactNode; } @@ -15,24 +15,8 @@ export const DualListSelectorList: React.FunctionComponent { - const { - setFocusedOption, - isTree, - ariaLabelledBy, - focusedOption, - displayOption, - selectedOptions, - id, - onOptionSelect, - options, - isDisabled - } = React.useContext(DualListSelectorListContext); - - // only called when options are passed via options prop - const onOptionClick = (e: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, index: number, id: string) => { - setFocusedOption(id); - onOptionSelect(e, index, id); - }; + const { isTree, ariaLabelledBy, focusedOption, displayOption, selectedOptions, id, options, isDisabled } = + React.useContext(DualListSelectorListContext); const hasOptions = () => options.length !== 0 || (children !== undefined && (children as React.ReactNode[]).length !== 0); @@ -58,7 +42,6 @@ export const DualListSelectorList: React.FunctionComponent onOptionClick(e, index, id)} orderIndex={index} isDisabled={isDisabled} > diff --git a/packages/react-core/src/components/DualListSelector/DualListSelectorListItem.tsx b/packages/react-core/src/components/DualListSelector/DualListSelectorListItem.tsx index e2c3e8433a6..5ed946e2a9b 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelectorListItem.tsx +++ b/packages/react-core/src/components/DualListSelector/DualListSelectorListItem.tsx @@ -17,7 +17,7 @@ export interface DualListSelectorListItemProps extends React.HTMLProps void; /** ID of the option. */ id?: string; @@ -25,11 +25,11 @@ export interface DualListSelectorListItemProps extends React.HTMLProps; - /** Flag indicating this item is draggable for reordring */ + /** Flag indicating this item is draggable for reordering. */ isDraggable?: boolean; - /** Accessible label for the draggable button on draggable list items */ + /** Accessible label for the draggable button on draggable list items. */ draggableButtonAriaLabel?: string; - /** Flag indicating if the dual list selector is in a disabled state */ + /** Flag indicating if the dual list selector is in a disabled state. */ isDisabled?: boolean; } diff --git a/packages/react-core/src/components/DualListSelector/DualListSelectorListWrapper.tsx b/packages/react-core/src/components/DualListSelector/DualListSelectorListWrapper.tsx index 642b53b0ed0..a284f4f02f6 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelectorListWrapper.tsx +++ b/packages/react-core/src/components/DualListSelector/DualListSelectorListWrapper.tsx @@ -8,11 +8,11 @@ import { DualListSelectorContext, DualListSelectorListContext } from './DualList export interface DualListSelectorListWrapperProps extends React.HTMLProps { /** Additional classes applied to the dual list selector. */ className?: string; - /** Anything that can be rendered inside of the list */ + /** Anything that can be rendered inside of the list. */ children?: React.ReactNode; - /** Id of the dual list selector list */ + /** ID of the dual list selector list. */ id?: string; - /** Accessibly label for the list */ + /** Accessibly label for the list. */ 'aria-labelledby': string; /** @hide forwarded ref */ innerRef?: React.RefObject; @@ -20,9 +20,7 @@ export interface DualListSelectorListWrapperProps extends React.HTMLProps void; - /** @hide Function to determine if an option should be displayed depending on a dynamically built filter value */ + /** @hide Function to determine if an option should be displayed depending on a custom filter value. */ displayOption?: (option: React.ReactNode) => boolean; /** Flag indicating whether the component is disabled. */ isDisabled?: boolean; @@ -35,7 +33,6 @@ export const DualListSelectorListWrapperBase: React.FunctionComponent { if ( !menuRef.current || @@ -94,6 +90,7 @@ export const DualListSelectorListWrapperBase: React.FunctionComponent { window.removeEventListener('keydown', handleKeys); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [menuRef.current]); return ( @@ -108,7 +105,6 @@ export const DualListSelectorListWrapperBase: React.FunctionComponent diff --git a/packages/react-core/src/components/DualListSelector/DualListSelectorPane.tsx b/packages/react-core/src/components/DualListSelector/DualListSelectorPane.tsx index b8160db1cd9..8011cee0a5d 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelectorPane.tsx +++ b/packages/react-core/src/components/DualListSelector/DualListSelectorPane.tsx @@ -1,11 +1,9 @@ import * as React from 'react'; import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector'; import { css } from '@patternfly/react-styles'; -import { DualListSelectorTree, DualListSelectorTreeItemData } from './DualListSelectorTree'; import { getUniqueId } from '../../helpers'; import { DualListSelectorListWrapper } from './DualListSelectorListWrapper'; -import { DualListSelectorContext, DualListSelectorPaneContext } from './DualListSelectorContext'; -import { DualListSelectorList } from './DualListSelectorList'; +import { DualListSelectorPaneContext } from './DualListSelectorContext'; import { SearchInput } from '../SearchInput'; import cssMenuMinHeight from '@patternfly/react-tokens/dist/esm/c_dual_list_selector__menu_MinHeight'; @@ -29,43 +27,12 @@ export interface DualListSelectorPaneProps extends Omit void; - /** @hide Callback for when a tree option is checked. Optionally used only when options prop is provided. */ - onOptionCheck?: ( - event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, - isChecked: boolean, - itemData: DualListSelectorTreeItemData - ) => void; - /** @hide Flag indicating a dynamically built search bar should be included above the pane. */ - isSearchable?: boolean; /** Flag indicating whether the component is disabled. */ isDisabled?: boolean; /** Callback for search input. To be used when isSearchable is true. */ onSearch?: (event: React.ChangeEvent) => void; - /** @hide A callback for when the search input value for changes. To be used when isSearchable is true. */ - onSearchInputChanged?: (event: React.FormEvent, value: string) => void; - /** @hide Callback for search input clear button */ - onSearchInputClear?: (event: React.SyntheticEvent) => void; - /** @hide Filter function for custom filtering based on search string. To be used when isSearchable is true. */ - filterOption?: (option: React.ReactNode, input: string) => boolean; - /** @hide Accessible label for the search input. To be used when isSearchable is true. */ - 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; } @@ -77,169 +44,51 @@ export const DualListSelectorPane: React.FunctionComponent { - const [input, setInput] = React.useState(''); - const { isTree } = React.useContext(DualListSelectorContext); - - // only called when search input is dynamically built - const onChange = (e: React.FormEvent, newValue: string) => { - let filtered: React.ReactNode[]; - if (isTree) { - filtered = options - .map((opt) => Object.assign({}, opt)) - .filter((item) => filterInput(item as unknown as DualListSelectorTreeItemData, newValue)); - } else { - filtered = options.filter((option) => { - if (displayOption(option)) { - return option; - } - }); - } - onFilterUpdate(filtered, isChosen ? 'chosen' : 'available', newValue === ''); - - if (onSearchInputChanged) { - onSearchInputChanged(e, newValue); - } - setInput(newValue); - }; - - // only called when options are passed via options prop and isTree === true - const filterInput = (item: DualListSelectorTreeItemData, input: string): boolean => { - if (filterOption) { - return filterOption(item as unknown as React.ReactNode, input); - } else { - if (item.text.toLowerCase().includes(input.toLowerCase()) || input === '') { - return true; - } - } - if (item.children) { - return ( - (item.children = item.children - .map((opt) => Object.assign({}, opt)) - .filter((child) => filterInput(child, input))).length > 0 - ); - } - }; - - // only called when options are passed via options prop and isTree === false - const displayOption = (option: React.ReactNode) => { - if (filterOption) { - return filterOption(option, input); - } else { - return option.toString().toLowerCase().includes(input.toLowerCase()); - } - }; - - return ( -
- {title && ( -
-
-
{title}
-
-
- )} - {(actions || searchInput || isSearchable) && ( -
- {(isSearchable || searchInput) && ( -
- {searchInput ? ( - searchInput - ) : ( - onChange(e as React.FormEvent, '') - } - isDisabled={isDisabled} - aria-label={searchInputAriaLabel} - /> - )} -
- )} - {actions &&
{actions}
} +}: DualListSelectorPaneProps) => ( +
+ {title && ( +
+
+
{title}
- )} - {status && ( -
-
- {status} +
+ )} + {(actions || searchInput) && ( +
+ {searchInput && ( +
+ {searchInput ? searchInput : }
-
- )} - - {!isTree && ( - onOptionSelect(e, index, isChosen, id)} - displayOption={displayOption} - id={`${id}-list`} - isDisabled={isDisabled} - {...(listMinHeight && { - style: { [cssMenuMinHeight.name]: listMinHeight } as React.CSSProperties - })} - > - {children} - )} - {isTree && ( - - {options.length > 0 ? ( - - Object.assign({}, opt)) - .filter((item) => - filterInput(item as unknown as DualListSelectorTreeItemData, input) - ) as unknown as DualListSelectorTreeItemData[]) - : (options as unknown as DualListSelectorTreeItemData[]) - } - onOptionCheck={onOptionCheck} - id={`${id}-tree`} - isDisabled={isDisabled} - /> - - ) : ( - children - )} - - )} - -
- ); -}; + {actions &&
{actions}
} +
+ )} + {status && ( +
+
+ {status} +
+
+ )} + + + {children} + + +
+); DualListSelectorPane.displayName = 'DualListSelectorPane'; diff --git a/packages/react-core/src/components/DualListSelector/DualListSelectorTree.tsx b/packages/react-core/src/components/DualListSelector/DualListSelectorTree.tsx index a151d625050..8fb28a289b9 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelectorTree.tsx +++ b/packages/react-core/src/components/DualListSelector/DualListSelectorTree.tsx @@ -10,26 +10,26 @@ export interface DualListSelectorTreeItemData { className?: string; /** Flag indicating this option is expanded by default. */ defaultExpanded?: boolean; - /** Flag indicating this option has a badge */ + /** Flag indicating this option has a badge. */ hasBadge?: boolean; - /** Callback fired when an option is checked */ + /** Callback fired when an option is checked. */ onOptionCheck?: ( event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, isChecked: boolean, isChosen: boolean, itemData: DualListSelectorTreeItemData ) => void; - /** ID of the option */ + /** ID of the option. */ id: string; - /** Text of the option */ + /** Text of the option. */ text: string; - /** Parent id of an option */ + /** Parent ID of an option. */ parentId?: string; - /** Checked state of the option */ + /** Checked state of the option. */ isChecked: boolean; - /** Additional properties to pass to the option checkbox */ + /** Additional properties to pass to the option checkbox. */ checkProps?: any; - /** Additional properties to pass to the option badge */ + /** Additional properties to pass to the option badge. */ badgeProps?: any; /** Flag indicating whether the component is disabled. */ isDisabled?: boolean; @@ -40,19 +40,19 @@ export interface DualListSelectorTreeItemData { */ export interface DualListSelectorTreeProps extends Omit, 'data'> { - /** Data of the tree view */ + /** Data of the tree view. */ data: DualListSelectorTreeItemData[] | (() => DualListSelectorTreeItemData[]); - /** ID of the tree view */ + /** ID of the tree view. */ id?: string; - /** @hide Flag indicating if the list is nested */ + /** @hide Flag indicating if the list is nested. */ isNested?: boolean; - /** Flag indicating if all options should have badges */ + /** Flag indicating if all options should have badges. */ hasBadges?: boolean; - /** Sets the default expanded behavior */ + /** Sets the default expanded behavior. */ defaultAllExpanded?: boolean; - /** Flag indicating if the dual list selector tree is in the disabled state */ + /** Flag indicating if the dual list selector tree is in the disabled state. */ isDisabled?: boolean; - /** Callback fired when an option is checked */ + /** Callback fired when an option is checked. */ onOptionCheck?: ( event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, isChecked: boolean, diff --git a/packages/react-core/src/components/DualListSelector/DualListSelectorTreeItem.tsx b/packages/react-core/src/components/DualListSelector/DualListSelectorTreeItem.tsx index 921902decfa..bfa4c0f158d 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelectorTreeItem.tsx +++ b/packages/react-core/src/components/DualListSelector/DualListSelectorTreeItem.tsx @@ -14,25 +14,25 @@ export interface DualListSelectorTreeItemProps extends React.HTMLProps | React.KeyboardEvent, isChecked: boolean, itemData: DualListSelectorTreeItemData ) => void; - /** ID of the option */ + /** ID of the option. */ id: string; - /** Text of the option */ + /** Text of the option. */ text: string; /** Flag indicating if this open is checked. */ isChecked?: boolean; - /** Additional properties to pass to the option checkbox */ + /** Additional properties to pass to the option checkbox. */ checkProps?: any; - /** Additional properties to pass to the option badge */ + /** Additional properties to pass to the option badge. */ badgeProps?: any; - /** Raw data of the option */ + /** Raw data of the option. */ itemData?: DualListSelectorTreeItemData; /** Flag indicating whether the component is disabled. */ isDisabled?: boolean; diff --git a/packages/react-core/src/components/DualListSelector/__tests__/DualListSelector.test.tsx b/packages/react-core/src/components/DualListSelector/__tests__/DualListSelector.test.tsx index b5baebcb692..373aa0e993d 100644 --- a/packages/react-core/src/components/DualListSelector/__tests__/DualListSelector.test.tsx +++ b/packages/react-core/src/components/DualListSelector/__tests__/DualListSelector.test.tsx @@ -1,65 +1,26 @@ import { render } from '@testing-library/react'; -import { DualListSelector } from '../../DualListSelector'; +import { DualListSelectorPane } from '../DualListSelectorPane'; +import { SearchInput } from '../../SearchInput'; import React from 'react'; describe('DualListSelector', () => { test('basic', () => { - const { asFragment } = render(); + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); test('with search inputs', () => { - const { asFragment } = render( - - ); + const { asFragment } = render(} />); expect(asFragment()).toMatchSnapshot(); }); test('with custom status', () => { - const { asFragment } = render( - - ); + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); test('basic with disabled controls', () => { - const { asFragment } = render( - - ); - expect(asFragment()).toMatchSnapshot(); - }); - - test('with tree', () => { - const { asFragment } = render( - - ); - expect(asFragment()).toMatchSnapshot(); - }); - - test('with actions', () => { - const { asFragment } = render( - TestNode1]} - chosenOptionsActions={[TestNode2]} - id="fourthTest" - /> - ); + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); }); }); diff --git a/packages/react-core/src/components/DualListSelector/__tests__/__snapshots__/DualListSelector.test.tsx.snap b/packages/react-core/src/components/DualListSelector/__tests__/__snapshots__/DualListSelector.test.tsx.snap index 7c28575d55c..63dab5eee9e 100644 --- a/packages/react-core/src/components/DualListSelector/__tests__/__snapshots__/DualListSelector.test.tsx.snap +++ b/packages/react-core/src/components/DualListSelector/__tests__/__snapshots__/DualListSelector.test.tsx.snap @@ -3,255 +3,15 @@ exports[`DualListSelector basic 1`] = `
-
-
-
- Available options -
-
-
-
-
- 0 of 2 items selected -
-
-
-
    -
  • -
    - - - - Option 1 - - - -
    -
  • -
  • -
    - - - - Option 2 - - - -
    -
  • -
-
-
-
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
- Chosen options -
-
-
-
-
- 0 of 0 items selected -
-
-
-
    -
+
@@ -260,591 +20,15 @@ exports[`DualListSelector basic 1`] = ` exports[`DualListSelector basic with disabled controls 1`] = `
-
-
-
- Available options -
-
-
-
-
- 0 of 2 items selected -
-
-
-
    -
  • -
    - - - - Option 1 - - - -
    -
  • -
  • -
    - - - - Option 2 - - - -
    -
  • -
-
-
-
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
- Chosen options -
-
-
-
-
- 0 of 0 items selected -
-
-
-
    -
-
-
-
-`; - -exports[`DualListSelector with actions 1`] = ` - -
-
-
-
-
- Available options -
-
-
-
-
- - TestNode1 - -
-
-
-
- 0 of 2 items selected -
-
-
-
    -
  • -
    - - - - Option 1 - - - -
    -
  • -
  • -
    - - - - Option 2 - - - -
    -
  • -
-
-
-
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
- Chosen options -
-
-
-
-
- - TestNode2 - -
-
-
-
- 0 of 2 items selected -
-
-
-
    -
  • -
    - - - - Option 3 - - - -
    -
  • -
  • -
    - - - - Option 4 - - - -
    -
  • -
-
+
@@ -853,255 +37,25 @@ exports[`DualListSelector with actions 1`] = ` exports[`DualListSelector with custom status 1`] = `
-
-
- Available options -
-
-
-
-
- Test status1 -
-
-
-
    -
  • -
    - - - - Option 1 - - - -
    -
  • -
  • -
    - - - - Option 2 - - - -
    -
  • -
+ Test status1
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
- Chosen options -
-
-
-
-
- Test status2 -
-
-
-
    -
+
@@ -1110,675 +64,58 @@ exports[`DualListSelector with custom status 1`] = ` exports[`DualListSelector with search inputs 1`] = `
-
-
- Available options -
-
-
-
-
- - - - + class="pf-v6-c-text-input-group__icon" + > + -
+ +
-
-
- 0 of 2 items selected -
-
-
-
    -
  • -
    - - - - Option 1 - - - -
    -
  • -
  • -
    - - - - Option 2 - - - -
    -
  • -
-
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
- Chosen options -
-
-
-
-
-
-
- - - - - - -
-
-
-
-
-
- 0 of 0 items selected -
-
-
-
    -
-
-
-
-`; - -exports[`DualListSelector with tree 1`] = ` - -
-
-
-
-
- Available options -
-
-
-
-
- Test status1 -
-
-
-
    -
  • -
    -
    - -
    - - - -
    - - - - - Opt1 - -
    -
    -
    -
      -
    • -
      -
      - - - - - - Opt3 - - -
      -
      -
    • -
    -
  • -
  • -
    -
    - - - - - - Opt2 - - -
    -
    -
  • -
-
-
-
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
- Chosen options -
-
-
-
-
- Test status2 -
-
-
-
    -
+
diff --git a/packages/react-core/src/components/DualListSelector/examples/DualListSelector.md b/packages/react-core/src/components/DualListSelector/examples/DualListSelector.md index 5dd542efcc3..d2ea380783d 100644 --- a/packages/react-core/src/components/DualListSelector/examples/DualListSelector.md +++ b/packages/react-core/src/components/DualListSelector/examples/DualListSelector.md @@ -13,6 +13,7 @@ propComponents: 'DualListSelectorTree', 'DualListSelectorTreeItemData' ] +beta: true --- import AngleDoubleLeftIcon from '@patternfly/react-icons/dist/esm/icons/angle-double-left-icon'; @@ -26,6 +27,28 @@ import { DragDrop, Draggable, Droppable } from '@patternfly/react-core/deprecate ## Examples +The dual list selector is built in a composable manner to make customization easier. The standard sub-component relationships are arranged as follows: + +```noLive + + + + + + + + + /* A standard Dual list selector has 4 controls */ + + + + + + + + +``` + ### Basic ```ts file="./DualListSelectorBasic.tsx" @@ -52,43 +75,11 @@ import { DragDrop, Draggable, Droppable } from '@patternfly/react-core/deprecate ### With tree -```ts file="./DualListSelectorTreeExample.tsx" - -``` - -## Composable structure - -The dual list selector can also be built in a composable manner to make customization easier. The standard sub-component relationships are arranged as follows: - -```noLive - - - - - - - - - /* A standard Dual list selector has 4 controls */ - - - - - - - - -``` - -### Composable dual list selector - -```ts file="./DualListSelectorComposable.tsx" +```ts file="DualListSelectorTree.tsx" ``` -### Composable with drag and drop - -Note: There is a new recommended drag and drop implementation with full keyboard functionality, which replaces this implementation. To adhere to our new recommendations, refer to the [drag and drop demos](/components/drag-and-drop/react-demos). +### Drag and drop This example only allows reordering the contents of the "chosen" pane with drag and drop. To make a pane able to be reordered: @@ -102,14 +93,8 @@ This example only allows reordering the contents of the "chosen" pane with drag - define an `onDrag` callback which ensures that the drag event will not cross hairs with the `onOptionSelect` click event set on the option. Note: the `ignoreNextOptionSelect` state value is used to prevent selection while dragging. -Keyboard and screen reader accessibility for the `` component is still in development. - -```ts isDeprecated file="DualListSelectorComposableDragDrop.tsx" - -``` - -### Composable with tree +Note: Keyboard accessibility and screen reader accessibility for the `DragDrop` component are still in development. -```ts file="DualListSelectorComposableTree.tsx" +```ts file="DualListSelectorDragDrop.tsx" ``` diff --git a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasic.tsx b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasic.tsx index 216e34d14cc..53e287e5d1d 100644 --- a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasic.tsx +++ b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasic.tsx @@ -1,30 +1,157 @@ import React from 'react'; -import { DualListSelector } from '@patternfly/react-core'; +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 DualListSelectorBasic: React.FunctionComponent = () => { - const [availableOptions, setAvailableOptions] = React.useState([ - 'Option 1', - 'Option 2', - 'Option 3', - 'Option 4' + 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 [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)]); + } + }; - const onListChange = ( - event: React.MouseEvent, - newAvailableOptions: React.ReactNode[], - newChosenOptions: React.ReactNode[] + // callback when option is selected + const onOptionSelect = ( + event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, + index: number, + isChosen: boolean ) => { - setAvailableOptions(newAvailableOptions.sort()); - setChosenOptions(newChosenOptions.sort()); + 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" + > + + + moveAll(true)} + aria-label="Add all" + > + + + moveAll(false)} + aria-label="Remove all" + > + + + moveSelected(false)} + isDisabled={!chosenOptions.some((option) => option.selected)} + aria-label="Remove selected" + > + + + + 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-core/src/components/DualListSelector/examples/DualListSelectorBasicSearch.tsx b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasicSearch.tsx index c6625a02931..3e467cd034f 100644 --- a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasicSearch.tsx +++ b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasicSearch.tsx @@ -1,31 +1,211 @@ import React from 'react'; -import { DualListSelector } from '@patternfly/react-core'; - -export const DualListSelectorBasicSearch: React.FunctionComponent = () => { - const [availableOptions, setAvailableOptions] = React.useState([ - 'Option 1', - 'Option 2', - 'Option 3', - 'Option 4' +import { + Button, + DualListSelector, + DualListSelectorPane, + DualListSelectorList, + DualListSelectorListItem, + DualListSelectorControlsWrapper, + DualListSelectorControl, + SearchInput, + 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 Option { + text: string; + selected: boolean; + isVisible: boolean; +} + +export const DualListSelectorSearch: 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 onListChange = ( - event: React.MouseEvent, - newAvailableOptions: React.ReactNode[], - newChosenOptions: React.ReactNode[] + const [chosenOptions, setChosenOptions] = React.useState([]); + const [availableFilter, setAvailableFilter] = React.useState(''); + const [chosenFilter, setChosenFilter] = 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 ) => { - setAvailableOptions(newAvailableOptions.sort()); - setChosenOptions(newChosenOptions.sort()); + 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 ( - { + 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)} /> ); + + const buildEmptyState = (isAvailable: boolean) => ( + + No results match the filter criteria. Clear all filters and try again. + + + + + + + ); + + return ( + + option.selected && option.isVisible).length} of ${ + availableOptions.filter((option) => option.isVisible).length + } options selected`} + searchInput={buildSearchInput(true)} + listMinHeight="300px" + > + {availableFilter !== '' && + availableOptions.filter((option) => option.isVisible).length === 0 && + buildEmptyState(true)} + + + {availableOptions.map((option, index) => + option.isVisible ? ( + onOptionSelect(e, index, false)} + > + {option.text} + + ) : null + )} + + + + option.selected)} + onClick={() => moveSelected(true)} + aria-label="Add selected" + > + + + moveAll(true)} + aria-label="Add all" + > + + + moveAll(false)} + aria-label="Remove all" + > + + + moveSelected(false)} + isDisabled={!chosenOptions.some((option) => option.selected)} + aria-label="Remove selected" + > + + + + option.selected && option.isVisible).length} of ${ + chosenOptions.filter((option) => option.isVisible).length + } options selected`} + searchInput={buildSearchInput(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)} + > + {option.text} + + ) : null + )} + + )} + + + ); }; diff --git a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasicTooltips.tsx b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasicTooltips.tsx index ac0e267134d..717c5860bd7 100644 --- a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasicTooltips.tsx +++ b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasicTooltips.tsx @@ -1,38 +1,165 @@ import React from 'react'; -import { DualListSelector } from '@patternfly/react-core'; +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'; -export const DualListSelectorBasicTooltips: React.FunctionComponent = () => { - const [availableOptions, setAvailableOptions] = React.useState([ - 'Option 1', - 'Option 2', - 'Option 3', - 'Option 4' +interface Option { + text: string; + selected: boolean; + isVisible: boolean; +} + +export const DualListSelectorBasic: 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 [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)]); + } + }; - const onListChange = ( - event: React.MouseEvent, - newAvailableOptions: React.ReactNode[], - newChosenOptions: React.ReactNode[] + // callback when option is selected + const onOptionSelect = ( + event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, + index: number, + isChosen: boolean ) => { - setAvailableOptions(newAvailableOptions.sort()); - setChosenOptions(newChosenOptions.sort()); + 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' }} + > + + + moveAll(true)} + aria-label="Add all" + tooltipContent="Add all" + tooltipProps={{ position: 'right', 'aria-live': 'off' }} + > + + + moveAll(false)} + aria-label="Remove all" + tooltipContent="Remove all" + tooltipProps={{ position: 'left', 'aria-live': 'off' }} + > + + + moveSelected(false)} + isDisabled={!chosenOptions.some((option) => option.selected)} + aria-label="Remove selected" + tooltipContent="Remove selected" + tooltipProps={{ position: 'bottom', 'aria-live': 'off' }} + > + + + + 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-core/src/components/DualListSelector/examples/DualListSelectorComplexOptionsActions.tsx b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComplexOptionsActions.tsx index 8ef67fb8ae6..9ccc57b142f 100644 --- a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComplexOptionsActions.tsx +++ b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComplexOptionsActions.tsx @@ -3,157 +3,334 @@ import { Button, ButtonVariant, Checkbox, - DualListSelector, 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 SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; -export const DualListSelectorComplexOptionsActions: React.FunctionComponent = () => { - const [availableOptions, setAvailableOptions] = React.useState([ - Option 1, - Option 3, - Option 4, - Option 2 +interface Option { + text: string; + selected: boolean; + isVisible: boolean; +} + +export const DualListSelectorComplexOptionsActionsNext: 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 [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 [isDisabled, setIsDisabled] = React.useState(false); - const onListChange = (newAvailableOptions: React.ReactNode[], newChosenOptions: React.ReactNode[]) => { - setAvailableOptions(newAvailableOptions); - setChosenOptions(newChosenOptions); - }; - - const onSort = (pane: string) => { - const toSort = pane === 'available' ? [...availableOptions] : [...chosenOptions]; - (toSort as React.ReactElement[]).sort((a, b) => { - if (a.props.children > b.props.children) { - return 1; + // 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 (a.props.children < b.props.children) { - return -1; - } - return 0; - }); + } + if (fromAvailable) { + setAvailableOptions([...sourceOptions]); + setChosenOptions([...destinationOptions]); + } else { + setChosenOptions([...sourceOptions]); + setAvailableOptions([...destinationOptions]); + } + }; - if (pane === 'available') { - setAvailableOptions(toSort); + // 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 { - setChosenOptions(toSort); + setAvailableOptions([...chosenOptions.filter((option) => option.isVisible), ...availableOptions]); + setChosenOptions([...chosenOptions.filter((option) => !option.isVisible)]); } }; - const onToggle = (pane: string) => { - if (pane === 'available') { - setIsAvailableKebabOpen(!isAvailableKebabOpen); + // 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 { - setIsChosenKebabOpen(!isChosenKebabOpen); + const newAvailable = [...availableOptions]; + newAvailable[index].selected = !availableOptions[index].selected; + setAvailableOptions(newAvailable); } }; - const filterOption = (option: React.ReactNode, input: string) => - (option as React.ReactElement).props.children.includes(input); + 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()); + }); + }; - const availableOptionsActions = [ - , - ) => ( - onToggle('available')} - variant="plain" - id="complex-available-toggle" - aria-label="Complex actions example available kebab toggle" - > - - )} - isOpen={isAvailableKebabOpen} - onOpenChange={(isOpen: boolean) => setIsAvailableKebabOpen(isOpen)} - onSelect={() => setIsAvailableKebabOpen(false)} - key="availableDropdown" - > - - Action - {/* Prevent default onClick functionality for example purposes */} - event.preventDefault()}> - Link - - - - ]; + /> + ); - const chosenOptionsActions = [ - , - ) => ( - onToggle('chosen')} - id="complex-chosen-toggle" - aria-label="Complex actions example chosen kebab toggle" - > - - )} - isOpen={isChosenKebabOpen} - onOpenChange={(isOpen) => setIsChosenKebabOpen(isOpen)} - onSelect={() => setIsChosenKebabOpen(false)} - key="chosenDropdown" - > - - Action - {/* Prevent default onClick functionality for example purposes */} - event.preventDefault()}> - Link - - - - ]; + // builds a sort control - passed to both dual list selector panes + const buildSort = (isAvailable: boolean) => { + const onSort = () => { + const toSort = isAvailable ? [...availableOptions] : [...chosenOptions]; + toSort.sort((a, b) => { + if (a.text > b.text) { + return 1; + } + if (a.text < b.text) { + return -1; + } + return 0; + }); + if (isAvailable) { + setAvailableOptions(toSort); + } else { + setChosenOptions(toSort); + } + }; + + const onToggle = (pane: string) => { + if (pane === 'available') { + setIsAvailableKebabOpen(!isAvailableKebabOpen); + } else { + setIsChosenKebabOpen(!isChosenKebabOpen); + } + }; + + return isAvailable + ? [ + , + ) => ( + onToggle('available')} + variant="plain" + id="complex-available-toggle" + aria-label="Complex actions example available kebab toggle" + > + + )} + isOpen={isAvailableKebabOpen} + onOpenChange={(isOpen: boolean) => setIsAvailableKebabOpen(isOpen)} + onSelect={() => setIsAvailableKebabOpen(false)} + key="availableDropdown" + > + + Available Action + event.preventDefault()}> + Available Link + + + + ] + : [ + , + ) => ( + onToggle('chosen')} + variant="plain" + id="complex-chosen-toggle" + aria-label="Complex actions example chosen kebab toggle" + > + + )} + isOpen={isChosenKebabOpen} + onOpenChange={(isOpen: boolean) => setIsChosenKebabOpen(isOpen)} + onSelect={() => setIsChosenKebabOpen(false)} + key="chosenDropdown" + > + + Chosen Action + event.preventDefault()}> + Chosen Link + + + + ]; + }; + + const buildEmptyState = (isAvailable: boolean) => ( + + No results match the filter criteria. Clear all filters and try again. + + + + + + + ); 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" + > + + + moveAll(true)} + aria-label="Add all" + > + + + moveAll(false)} + aria-label="Remove all" + > + + + moveSelected(false)} + isDisabled={!chosenOptions.some((option) => option.selected) || isDisabled} + aria-label="Remove selected" + > + + + + 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 + )} + + )} + +
void; + /** Accessible label for the dynamically built add selected button */ + addSelectedAriaLabel?: string; + /** Tooltip content for the dynamically built add selected button */ + addSelectedTooltip?: React.ReactNode; + /** Additonal tooltip properties for the dynamically built add selected tooltip */ + addSelectedTooltipProps?: any; + /** Callback fired every time dynamically built options are chosen or removed */ + onListChange?: ( + event: React.MouseEvent, + newAvailableOptions: React.ReactNode[], + newChosenOptions: React.ReactNode[] + ) => void; + /** Optional callback for the dynamically built add all button */ + addAll?: (newAvailableOptions: React.ReactNode[], newChosenOptions: React.ReactNode[]) => void; + /** Accessible label for the dynamically built add all button */ + addAllAriaLabel?: string; + /** Tooltip content for the dynamically built add all button */ + addAllTooltip?: React.ReactNode; + /** Additonal tooltip properties for the dynamically built add all tooltip */ + addAllTooltipProps?: any; + /** Optional callback for the dynamically built remove selected button */ + removeSelected?: (newAvailableOptions: React.ReactNode[], newChosenOptions: React.ReactNode[]) => void; + /** Accessible label for the dynamically built remove selected button */ + removeSelectedAriaLabel?: string; + /** Tooltip content for the dynamically built remove selected button */ + removeSelectedTooltip?: React.ReactNode; + /** Additonal tooltip properties for the dynamically built remove selected tooltip */ + removeSelectedTooltipProps?: any; + /** Optional callback for the dynamically built remove all button */ + removeAll?: (newAvailableOptions: React.ReactNode[], newChosenOptions: React.ReactNode[]) => void; + /** Accessible label for the dynamically built remove all button */ + removeAllAriaLabel?: string; + /** Tooltip content for the dynamically built remove all button */ + removeAllTooltip?: React.ReactNode; + /** Additonal tooltip properties for the dynamically built remove all tooltip */ + removeAllTooltipProps?: any; + /** Optional callback fired when a dynamically built option is selected */ + onOptionSelect?: ( + event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, + index: number, + isChosen: boolean, + id: string, + itemData: any, + parentData: any + ) => void; + /** Optional callback fired when a dynamically built option is checked */ + onOptionCheck?: ( + event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, + checked: boolean, + checkedId: string, + newCheckedItems: string[] + ) => void; + /** Flag indicating a search bar should be included above both the dynamically built available and chosen panes. */ + isSearchable?: boolean; + /** Accessible label for the search input on the dynamically built available options pane. */ + availableOptionsSearchAriaLabel?: string; + /** A callback for when the search input value for the dynamically built available options changes. */ + onAvailableOptionsSearchInputChanged?: (event: React.FormEvent, value: string) => void; + /** Accessible label for the search input on the dynamically built chosen options pane. */ + chosenOptionsSearchAriaLabel?: string; + /** A callback for when the search input value for the dynamically built chosen options changes. */ + onChosenOptionsSearchInputChanged?: (event: React.FormEvent, value: string) => void; + /** Optional filter function for custom filtering based on search string. Used with a dynamically built search input. */ + filterOption?: (option: React.ReactNode, input: string) => boolean; +} + +interface DualListSelectorState { + availableOptions: React.ReactNode[]; + availableOptionsSelected: number[]; + availableFilteredOptions: React.ReactNode[]; + chosenOptions: React.ReactNode[]; + chosenOptionsSelected: number[]; + chosenFilteredOptions: React.ReactNode[]; + availableTreeFilteredOptions: string[]; + availableTreeOptionsChecked: string[]; + chosenTreeOptionsChecked: string[]; + chosenTreeFilteredOptions: string[]; +} + +class DualListSelector extends React.Component { + static displayName = 'DualListSelector'; + private addAllButtonRef = React.createRef(); + private addSelectedButtonRef = React.createRef(); + private removeSelectedButtonRef = React.createRef(); + private removeAllButtonRef = React.createRef(); + static defaultProps: PickOptional = { + children: '', + availableOptions: [], + availableOptionsTitle: 'Available options', + availableOptionsSearchAriaLabel: 'Available search input', + chosenOptions: [], + chosenOptionsTitle: 'Chosen options', + chosenOptionsSearchAriaLabel: 'Chosen search input', + controlsAriaLabel: 'Selector controls', + addAllAriaLabel: 'Add all', + addSelectedAriaLabel: 'Add selected', + removeSelectedAriaLabel: 'Remove selected', + removeAllAriaLabel: 'Remove all', + isTree: false, + isDisabled: false + }; + + // If the DualListSelector uses trees, concat the two initial arrays and merge duplicate folder IDs + private createMergedCopy() { + const copyOfAvailable = JSON.parse(JSON.stringify(this.props.availableOptions)); + const copyOfChosen = JSON.parse(JSON.stringify(this.props.chosenOptions)); + + return this.props.isTree + ? Object.values( + (copyOfAvailable as DualListSelectorTreeItemData[]) + .concat(copyOfChosen as DualListSelectorTreeItemData[]) + .reduce((mapObj: any, item: DualListSelectorTreeItemData) => { + const key = item.id; + if (mapObj[key]) { + // If map already has an item ID, add the dupe ID's children to the existing map + mapObj[key].children.push(...item.children); + } else { + // Else clone the item data + mapObj[key] = { ...item }; + } + return mapObj; + }, {}) + ) + : null; + } + + constructor(props: DualListSelectorProps) { + super(props); + this.state = { + availableOptions: [...this.props.availableOptions] as React.ReactNode[], + availableOptionsSelected: [], + availableFilteredOptions: null, + availableTreeFilteredOptions: null, + chosenOptions: [...this.props.chosenOptions] as React.ReactNode[], + chosenOptionsSelected: [], + chosenFilteredOptions: null, + chosenTreeFilteredOptions: null, + availableTreeOptionsChecked: [], + chosenTreeOptionsChecked: [] + }; + } + + /** In dev environment, prevents circular structure during JSON stringification when + * options passed in to the dual list selector include HTML elements. + */ + replacer = (key: string, value: any) => { + if (key[0] === '_') { + return undefined; + } + return value; + }; + + componentDidUpdate() { + if ( + JSON.stringify(this.props.availableOptions, this.replacer) !== + JSON.stringify(this.state.availableOptions, this.replacer) || + JSON.stringify(this.props.chosenOptions, this.replacer) !== + JSON.stringify(this.state.chosenOptions, this.replacer) + ) { + this.setState({ + availableOptions: [...this.props.availableOptions] as React.ReactNode[], + chosenOptions: [...this.props.chosenOptions] as React.ReactNode[] + }); + } + } + + onFilterUpdate = (newFilteredOptions: React.ReactNode[], paneType: string, isSearchReset: boolean) => { + const { isTree } = this.props; + if (paneType === 'available') { + if (isSearchReset) { + this.setState({ + availableFilteredOptions: null, + availableTreeFilteredOptions: null + }); + return; + } + if (isTree) { + this.setState({ + availableTreeFilteredOptions: flattenTreeWithFolders( + newFilteredOptions as unknown as DualListSelectorTreeItemData[] + ) + }); + } else { + this.setState({ + availableFilteredOptions: newFilteredOptions as React.ReactNode[] + }); + } + } else if (paneType === 'chosen') { + if (isSearchReset) { + this.setState({ + chosenFilteredOptions: null, + chosenTreeFilteredOptions: null + }); + return; + } + if (isTree) { + this.setState({ + chosenTreeFilteredOptions: flattenTreeWithFolders( + newFilteredOptions as unknown as DualListSelectorTreeItemData[] + ) + }); + } else { + this.setState({ + chosenFilteredOptions: newFilteredOptions as React.ReactNode[] + }); + } + } + }; + + addAllVisible = (event: React.MouseEvent) => { + this.setState((prevState) => { + const itemsToRemove = [] as React.ReactNode[]; + const newAvailable = [] as React.ReactNode[]; + const movedOptions = prevState.availableFilteredOptions || prevState.availableOptions; + prevState.availableOptions.forEach((value) => { + if (movedOptions.indexOf(value) !== -1) { + itemsToRemove.push(value); + } else { + newAvailable.push(value); + } + }); + + const newChosen = [...prevState.chosenOptions, ...itemsToRemove]; + this.props.addAll && this.props.addAll(newAvailable, newChosen); + this.props.onListChange && this.props.onListChange(event, newAvailable, newChosen); + + return { + chosenOptions: newChosen, + chosenFilteredOptions: newChosen, + availableOptions: newAvailable, + availableFilteredOptions: newAvailable, + chosenOptionsSelected: [], + availableOptionsSelected: [] + }; + }); + }; + + addAllTreeVisible = (event: React.MouseEvent) => { + this.setState((prevState) => { + const movedOptions = + prevState.availableTreeFilteredOptions || + flattenTreeWithFolders(prevState.availableOptions as unknown as DualListSelectorTreeItemData[]); + const newAvailable = prevState.availableOptions + .map((opt) => Object.assign({}, opt)) + .filter((item) => + filterRestTreeItems(item as unknown as DualListSelectorTreeItemData, movedOptions) + ) as React.ReactNode[]; + + const currChosen = flattenTree(prevState.chosenOptions as unknown as DualListSelectorTreeItemData[]); + const nextChosenOptions = currChosen.concat(movedOptions); + const newChosen = this.createMergedCopy() + .map((opt) => Object.assign({}, opt)) + .filter((item) => + filterTreeItemsWithoutFolders(item as DualListSelectorTreeItemData, nextChosenOptions) + ) as React.ReactNode[]; + + this.props.addAll && this.props.addAll(newAvailable, newChosen); + this.props.onListChange && this.props.onListChange(event, newAvailable, newChosen); + + return { + chosenOptions: newChosen, + chosenFilteredOptions: newChosen, + availableOptions: newAvailable, + availableFilteredOptions: newAvailable, + availableTreeOptionsChecked: [], + chosenTreeOptionsChecked: [] + }; + }); + }; + + addSelected = (event: React.MouseEvent) => { + this.setState((prevState) => { + const itemsToRemove = [] as React.ReactNode[]; + const newAvailable = [] as React.ReactNode[]; + prevState.availableOptions.forEach((value, index) => { + if (prevState.availableOptionsSelected.indexOf(index) !== -1) { + itemsToRemove.push(value); + } else { + newAvailable.push(value); + } + }); + + const newChosen = [...prevState.chosenOptions, ...itemsToRemove]; + this.props.addSelected && this.props.addSelected(newAvailable, newChosen); + this.props.onListChange && this.props.onListChange(event, newAvailable, newChosen); + + return { + chosenOptionsSelected: [], + availableOptionsSelected: [], + chosenOptions: newChosen, + chosenFilteredOptions: newChosen, + availableOptions: newAvailable, + availableFilteredOptions: newAvailable + }; + }); + }; + + addTreeSelected = (event: React.MouseEvent) => { + this.setState((prevState) => { + // Remove selected available nodes from current available nodes + const newAvailable = prevState.availableOptions + .map((opt) => Object.assign({}, opt)) + .filter((item) => + filterRestTreeItems(item as unknown as DualListSelectorTreeItemData, prevState.availableTreeOptionsChecked) + ); + + // Get next chosen options from current + new nodes and remap from base + const currChosen = flattenTree(prevState.chosenOptions as unknown as DualListSelectorTreeItemData[]); + const nextChosenOptions = currChosen.concat(prevState.availableTreeOptionsChecked); + const newChosen = this.createMergedCopy() + .map((opt) => Object.assign({}, opt)) + .filter((item) => + filterTreeItemsWithoutFolders(item as DualListSelectorTreeItemData, nextChosenOptions) + ) as React.ReactNode[]; + + this.props.addSelected && this.props.addSelected(newAvailable, newChosen); + this.props.onListChange && this.props.onListChange(event, newAvailable, newChosen); + + return { + availableTreeOptionsChecked: [], + chosenTreeOptionsChecked: [], + availableOptions: newAvailable, + chosenOptions: newChosen + }; + }); + }; + + removeAllVisible = (event: React.MouseEvent) => { + this.setState((prevState) => { + const itemsToRemove = [] as React.ReactNode[]; + const newChosen = [] as React.ReactNode[]; + const movedOptions = prevState.chosenFilteredOptions || prevState.chosenOptions; + prevState.chosenOptions.forEach((value) => { + if (movedOptions.indexOf(value) !== -1) { + itemsToRemove.push(value); + } else { + newChosen.push(value); + } + }); + + const newAvailable = [...prevState.availableOptions, ...itemsToRemove]; + this.props.removeAll && this.props.removeAll(newAvailable, newChosen); + this.props.onListChange && this.props.onListChange(event, newAvailable, newChosen); + + return { + chosenOptions: newChosen, + chosenFilteredOptions: newChosen, + availableOptions: newAvailable, + availableFilteredOptions: newAvailable, + chosenOptionsSelected: [], + availableOptionsSelected: [] + }; + }); + }; + + removeAllTreeVisible = (event: React.MouseEvent) => { + this.setState((prevState) => { + const movedOptions = + prevState.chosenTreeFilteredOptions || + flattenTreeWithFolders(prevState.chosenOptions as unknown as DualListSelectorTreeItemData[]); + + const newChosen = prevState.chosenOptions + .map((opt) => Object.assign({}, opt)) + .filter((item) => filterRestTreeItems(item as unknown as DualListSelectorTreeItemData, movedOptions)); + const currAvailable = flattenTree(prevState.availableOptions as unknown as DualListSelectorTreeItemData[]); + const nextAvailableOptions = currAvailable.concat(movedOptions); + const newAvailable = this.createMergedCopy() + .map((opt) => Object.assign({}, opt)) + .filter((item) => + filterTreeItemsWithoutFolders(item as DualListSelectorTreeItemData, nextAvailableOptions) + ) as React.ReactNode[]; + + this.props.removeAll && this.props.removeAll(newAvailable, newChosen); + this.props.onListChange && this.props.onListChange(event, newAvailable, newChosen); + + return { + chosenOptions: newChosen, + availableOptions: newAvailable, + availableTreeOptionsChecked: [], + chosenTreeOptionsChecked: [] + }; + }); + }; + + removeSelected = (event: React.MouseEvent) => { + this.setState((prevState) => { + const itemsToRemove = [] as React.ReactNode[]; + const newChosen = [] as React.ReactNode[]; + prevState.chosenOptions.forEach((value, index) => { + if (prevState.chosenOptionsSelected.indexOf(index) !== -1) { + itemsToRemove.push(value); + } else { + newChosen.push(value); + } + }); + + const newAvailable = [...prevState.availableOptions, ...itemsToRemove]; + this.props.removeSelected && this.props.removeSelected(newAvailable, newChosen); + this.props.onListChange && this.props.onListChange(event, newAvailable, newChosen); + + return { + chosenOptionsSelected: [], + availableOptionsSelected: [], + chosenOptions: newChosen, + chosenFilteredOptions: newChosen, + availableOptions: newAvailable, + availableFilteredOptions: newAvailable + }; + }); + }; + + removeTreeSelected = (event: React.MouseEvent) => { + this.setState((prevState) => { + // Remove selected chosen nodes from current chosen nodes + const newChosen = prevState.chosenOptions + .map((opt) => Object.assign({}, opt)) + .filter((item) => + filterRestTreeItems(item as unknown as DualListSelectorTreeItemData, prevState.chosenTreeOptionsChecked) + ); + + // Get next chosen options from current and remap from base + const currAvailable = flattenTree(prevState.availableOptions as unknown as DualListSelectorTreeItemData[]); + const nextAvailableOptions = currAvailable.concat(prevState.chosenTreeOptionsChecked); + const newAvailable = this.createMergedCopy() + .map((opt) => Object.assign({}, opt)) + .filter((item) => + filterTreeItemsWithoutFolders(item as DualListSelectorTreeItemData, nextAvailableOptions) + ) as React.ReactNode[]; + + this.props.removeSelected && this.props.removeSelected(newAvailable, newChosen); + this.props.onListChange && this.props.onListChange(event, newAvailable, newChosen); + + return { + availableTreeOptionsChecked: [], + chosenTreeOptionsChecked: [], + availableOptions: newAvailable, + chosenOptions: newChosen + }; + }); + }; + + onOptionSelect = ( + e: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, + index: number, + isChosen: boolean, + /* eslint-disable @typescript-eslint/no-unused-vars */ + id?: string, + itemData?: any, + parentData?: any + /* eslint-enable @typescript-eslint/no-unused-vars */ + ) => { + this.setState((prevState) => { + const originalArray = isChosen ? prevState.chosenOptionsSelected : prevState.availableOptionsSelected; + + let updatedArray = null; + if (originalArray.indexOf(index) !== -1) { + updatedArray = originalArray.filter((value) => value !== index); + } else { + updatedArray = [...originalArray, index]; + } + + return { + chosenOptionsSelected: isChosen ? updatedArray : prevState.chosenOptionsSelected, + availableOptionsSelected: isChosen ? prevState.availableOptionsSelected : updatedArray + }; + }); + + this.props.onOptionSelect && this.props.onOptionSelect(e, index, isChosen, id, itemData, parentData); + }; + + isChecked = (treeItem: DualListSelectorTreeItemData, isChosen: boolean) => + isChosen + ? this.state.chosenTreeOptionsChecked.includes(treeItem.id) + : this.state.availableTreeOptionsChecked.includes(treeItem.id); + areAllDescendantsChecked = (treeItem: DualListSelectorTreeItemData, isChosen: boolean): boolean => + treeItem.children + ? treeItem.children.every((child) => this.areAllDescendantsChecked(child, isChosen)) + : this.isChecked(treeItem, isChosen); + areSomeDescendantsChecked = (treeItem: DualListSelectorTreeItemData, isChosen: boolean): boolean => + treeItem.children + ? treeItem.children.some((child) => this.areSomeDescendantsChecked(child, isChosen)) + : this.isChecked(treeItem, isChosen); + + mapChecked = (item: DualListSelectorTreeItemData, isChosen: boolean): DualListSelectorTreeItemData => { + const hasCheck = this.areAllDescendantsChecked(item, isChosen); + item.isChecked = false; + + if (hasCheck) { + item.isChecked = true; + } else { + const hasPartialCheck = this.areSomeDescendantsChecked(item, isChosen); + if (hasPartialCheck) { + item.isChecked = null; + } + } + + if (item.children) { + return { + ...item, + children: item.children.map((child) => this.mapChecked(child, isChosen)) + }; + } + return item; + }; + + onTreeOptionCheck = ( + evt: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, + isChecked: boolean, + itemData: DualListSelectorTreeItemData, + isChosen: boolean + ) => { + const { availableOptions, availableTreeFilteredOptions, chosenOptions, chosenTreeFilteredOptions } = this.state; + let panelOptions; + if (isChosen) { + if (chosenTreeFilteredOptions) { + panelOptions = chosenOptions + .map((opt) => Object.assign({}, opt)) + .filter((item) => + filterTreeItemsWithoutFolders(item as unknown as DualListSelectorTreeItemData, chosenTreeFilteredOptions) + ); + } else { + panelOptions = chosenOptions; + } + } else { + if (availableTreeFilteredOptions) { + panelOptions = availableOptions + .map((opt) => Object.assign({}, opt)) + .filter((item) => + filterTreeItemsWithoutFolders(item as unknown as DualListSelectorTreeItemData, availableTreeFilteredOptions) + ); + } else { + panelOptions = availableOptions; + } + } + const checkedOptionTree = panelOptions + .map((opt) => Object.assign({}, opt)) + .filter((item) => filterTreeItems(item as unknown as DualListSelectorTreeItemData, [itemData.id])); + const flatTree = flattenTreeWithFolders(checkedOptionTree as unknown as DualListSelectorTreeItemData[]); + + const prevChecked = isChosen ? this.state.chosenTreeOptionsChecked : this.state.availableTreeOptionsChecked; + let updatedChecked = [] as string[]; + if (isChecked) { + updatedChecked = prevChecked.concat(flatTree.filter((id) => !prevChecked.includes(id))); + } else { + updatedChecked = prevChecked.filter((id) => !flatTree.includes(id)); + } + + this.setState( + (prevState) => ({ + availableTreeOptionsChecked: isChosen ? prevState.availableTreeOptionsChecked : updatedChecked, + chosenTreeOptionsChecked: isChosen ? updatedChecked : prevState.chosenTreeOptionsChecked + }), + () => { + this.props.onOptionCheck && this.props.onOptionCheck(evt, isChecked, itemData.id, updatedChecked); + } + ); + }; + + render() { + const { + availableOptionsTitle, + availableOptionsActions, + availableOptionsSearchAriaLabel, + className, + children, + chosenOptionsTitle, + chosenOptionsActions, + chosenOptionsSearchAriaLabel, + filterOption, + isSearchable, + chosenOptionsStatus, + availableOptionsStatus, + controlsAriaLabel, + addAllAriaLabel, + addSelectedAriaLabel, + removeSelectedAriaLabel, + removeAllAriaLabel, + /* eslint-disable @typescript-eslint/no-unused-vars */ + availableOptions: consumerPassedAvailableOptions, + chosenOptions: consumerPassedChosenOptions, + removeSelected, + addAll, + removeAll, + addSelected, + onListChange, + onAvailableOptionsSearchInputChanged, + onChosenOptionsSearchInputChanged, + onOptionSelect, + onOptionCheck, + id, + isTree, + isDisabled, + addAllTooltip, + addAllTooltipProps, + addSelectedTooltip, + addSelectedTooltipProps, + removeAllTooltip, + removeAllTooltipProps, + removeSelectedTooltip, + removeSelectedTooltipProps, + ...props + } = this.props; + const { + availableOptions, + chosenOptions, + chosenOptionsSelected, + availableOptionsSelected, + chosenTreeOptionsChecked, + availableTreeOptionsChecked + } = this.state; + const availableOptionsStatusToDisplay = + availableOptionsStatus || + (isTree + ? `${ + filterFolders(availableOptions as unknown as DualListSelectorTreeItemData[], availableTreeOptionsChecked) + .length + } of ${flattenTree(availableOptions as unknown as DualListSelectorTreeItemData[]).length} items selected` + : `${availableOptionsSelected.length} of ${availableOptions.length} items selected`); + const chosenOptionsStatusToDisplay = + chosenOptionsStatus || + (isTree + ? `${ + filterFolders(chosenOptions as unknown as DualListSelectorTreeItemData[], chosenTreeOptionsChecked).length + } of ${flattenTree(chosenOptions as unknown as DualListSelectorTreeItemData[]).length} items selected` + : `${chosenOptionsSelected.length} of ${chosenOptions.length} items selected`); + + const available = ( + isTree + ? availableOptions.map((item) => this.mapChecked(item as unknown as DualListSelectorTreeItemData, false)) + : availableOptions + ) as React.ReactNode[]; + const chosen = ( + isTree + ? chosenOptions.map((item) => this.mapChecked(item as unknown as DualListSelectorTreeItemData, true)) + : chosenOptions + ) as React.ReactNode[]; + + return ( + + + {(randomId) => ( +
+ {children === '' ? ( + <> + this.onTreeOptionCheck(e, isChecked, itemData, false)} + actions={availableOptionsActions} + id={`${id || randomId}-available-pane`} + isDisabled={isDisabled} + /> + + + + + + + + + + + + + + + this.onTreeOptionCheck(e, isChecked, itemData, true)} + actions={chosenOptionsActions} + id={`${id || randomId}-chosen-pane`} + isDisabled={isDisabled} + /> + + ) : ( + children + )} +
+ )} +
+
+ ); + } +} + +export { DualListSelector }; diff --git a/packages/react-core/src/next/components/DualListSelector/DualListSelectorContext.ts b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorContext.ts similarity index 83% rename from packages/react-core/src/next/components/DualListSelector/DualListSelectorContext.ts rename to packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorContext.ts index 3eccc9a0dc6..27b1ed1eff2 100644 --- a/packages/react-core/src/next/components/DualListSelector/DualListSelectorContext.ts +++ b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorContext.ts @@ -12,6 +12,7 @@ export const DualListSelectorListContext = React.createContext<{ displayOption?: (option: React.ReactNode) => boolean; selectedOptions?: string[] | number[]; id?: string; + onOptionSelect?: (e: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, index: number, id: string) => void; options?: React.ReactNode[]; isDisabled?: boolean; }>({}); diff --git a/packages/react-core/src/next/components/DualListSelector/DualListSelectorControl.tsx b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorControl.tsx similarity index 99% rename from packages/react-core/src/next/components/DualListSelector/DualListSelectorControl.tsx rename to packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorControl.tsx index e874c25ad92..9a6750e458e 100644 --- a/packages/react-core/src/next/components/DualListSelector/DualListSelectorControl.tsx +++ b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorControl.tsx @@ -29,7 +29,7 @@ export interface DualListSelectorControlProps extends Omit = ({ innerRef, - children, + children = null, className, 'aria-label': ariaLabel, isDisabled = true, diff --git a/packages/react-core/src/next/components/DualListSelector/DualListSelectorControlsWrapper.tsx b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorControlsWrapper.tsx similarity index 95% rename from packages/react-core/src/next/components/DualListSelector/DualListSelectorControlsWrapper.tsx rename to packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorControlsWrapper.tsx index 66d69b0373a..2d5c9665fd8 100644 --- a/packages/react-core/src/next/components/DualListSelector/DualListSelectorControlsWrapper.tsx +++ b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorControlsWrapper.tsx @@ -6,7 +6,7 @@ import { handleArrows } from '../../../helpers'; /** Acts as the container for the DualListSelectorControl sub-components. */ export interface DualListSelectorControlsWrapperProps extends React.HTMLProps { - /** Content to be rendered inside of the controls wrapper. */ + /** Anything that can be rendered inside of the wrapper. */ children?: React.ReactNode; /** Additional classes added to the wrapper. */ className?: string; @@ -60,7 +60,6 @@ export const DualListSelectorControlsWrapperBase: React.FunctionComponent { window.removeEventListener('keydown', handleKeys); }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [wrapperRef.current]); return ( diff --git a/packages/react-core/src/next/components/DualListSelector/DualListSelectorList.tsx b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorList.tsx similarity index 74% rename from packages/react-core/src/next/components/DualListSelector/DualListSelectorList.tsx rename to packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorList.tsx index 13d00467e6b..cc0224eef1f 100644 --- a/packages/react-core/src/next/components/DualListSelector/DualListSelectorList.tsx +++ b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorList.tsx @@ -7,7 +7,7 @@ import { DualListSelectorListContext } from './DualListSelectorContext'; /** Acts as the container for DualListSelectorListItem sub-components. */ export interface DualListSelectorListProps extends React.HTMLProps { - /** Content rendered inside the dual list selector list. */ + /** Content rendered inside the dual list selector list */ children?: React.ReactNode; } @@ -15,8 +15,24 @@ export const DualListSelectorList: React.FunctionComponent { - const { isTree, ariaLabelledBy, focusedOption, displayOption, selectedOptions, id, options, isDisabled } = - React.useContext(DualListSelectorListContext); + const { + setFocusedOption, + isTree, + ariaLabelledBy, + focusedOption, + displayOption, + selectedOptions, + id, + onOptionSelect, + options, + isDisabled + } = React.useContext(DualListSelectorListContext); + + // only called when options are passed via options prop + const onOptionClick = (e: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, index: number, id: string) => { + setFocusedOption(id); + onOptionSelect(e, index, id); + }; const hasOptions = () => options.length !== 0 || (children !== undefined && (children as React.ReactNode[]).length !== 0); @@ -42,6 +58,7 @@ export const DualListSelectorList: React.FunctionComponent onOptionClick(e, index, id)} orderIndex={index} isDisabled={isDisabled} > diff --git a/packages/react-core/src/next/components/DualListSelector/DualListSelectorListItem.tsx b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorListItem.tsx similarity index 96% rename from packages/react-core/src/next/components/DualListSelector/DualListSelectorListItem.tsx rename to packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorListItem.tsx index 8e25b149aa3..d14ec10e06f 100644 --- a/packages/react-core/src/next/components/DualListSelector/DualListSelectorListItem.tsx +++ b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorListItem.tsx @@ -17,7 +17,7 @@ export interface DualListSelectorListItemProps extends React.HTMLProps void; /** ID of the option. */ id?: string; @@ -25,11 +25,11 @@ export interface DualListSelectorListItemProps extends React.HTMLProps; - /** Flag indicating this item is draggable for reordering. */ + /** Flag indicating this item is draggable for reordring */ isDraggable?: boolean; - /** Accessible label for the draggable button on draggable list items. */ + /** Accessible label for the draggable button on draggable list items */ draggableButtonAriaLabel?: string; - /** Flag indicating if the dual list selector is in a disabled state. */ + /** Flag indicating if the dual list selector is in a disabled state */ isDisabled?: boolean; } diff --git a/packages/react-core/src/next/components/DualListSelector/DualListSelectorListWrapper.tsx b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorListWrapper.tsx similarity index 86% rename from packages/react-core/src/next/components/DualListSelector/DualListSelectorListWrapper.tsx rename to packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorListWrapper.tsx index 9f856424850..e50b504b66f 100644 --- a/packages/react-core/src/next/components/DualListSelector/DualListSelectorListWrapper.tsx +++ b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorListWrapper.tsx @@ -8,11 +8,11 @@ import { DualListSelectorContext, DualListSelectorListContext } from './DualList export interface DualListSelectorListWrapperProps extends React.HTMLProps { /** Additional classes applied to the dual list selector. */ className?: string; - /** Anything that can be rendered inside of the list. */ + /** Anything that can be rendered inside of the list */ children?: React.ReactNode; - /** ID of the dual list selector list. */ + /** Id of the dual list selector list */ id?: string; - /** Accessibly label for the list. */ + /** Accessibly label for the list */ 'aria-labelledby': string; /** @hide forwarded ref */ innerRef?: React.RefObject; @@ -20,7 +20,9 @@ export interface DualListSelectorListWrapperProps extends React.HTMLProps void; + /** @hide Function to determine if an option should be displayed depending on a dynamically built filter value */ displayOption?: (option: React.ReactNode) => boolean; /** Flag indicating whether the component is disabled. */ isDisabled?: boolean; @@ -33,6 +35,7 @@ export const DualListSelectorListWrapperBase: React.FunctionComponent { if ( !menuRef.current || @@ -90,7 +94,6 @@ export const DualListSelectorListWrapperBase: React.FunctionComponent { window.removeEventListener('keydown', handleKeys); }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [menuRef.current]); return ( @@ -105,6 +108,7 @@ export const DualListSelectorListWrapperBase: React.FunctionComponent diff --git a/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorPane.tsx b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorPane.tsx new file mode 100644 index 00000000000..e9a0e2999e7 --- /dev/null +++ b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorPane.tsx @@ -0,0 +1,245 @@ +import * as React from 'react'; +import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector'; +import { css } from '@patternfly/react-styles'; +import { DualListSelectorTree, DualListSelectorTreeItemData } from './DualListSelectorTree'; +import { getUniqueId } from '../../../helpers'; +import { DualListSelectorListWrapper } from './DualListSelectorListWrapper'; +import { DualListSelectorContext, DualListSelectorPaneContext } from './DualListSelectorContext'; +import { DualListSelectorList } from './DualListSelectorList'; +import { SearchInput } from '../../../components/SearchInput'; +import cssMenuMinHeight from '@patternfly/react-tokens/dist/esm/c_dual_list_selector__menu_MinHeight'; + +/** Acts as the container for a list of options that are either available or chosen, + * depending on the pane type (available or chosen). A search input and other actions, + * such as sorting, can also be passed into this sub-component. + */ + +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. */ + children?: React.ReactNode; + /** Flag indicating if this pane is the chosen pane. */ + isChosen?: boolean; + /** Status to display above the pane. */ + status?: string; + /** Title of the pane. */ + title?: React.ReactNode; + /** A search input placed above the list at the top of the pane, before actions. */ + searchInput?: React.ReactNode; + /** Actions to place above the pane. */ + actions?: React.ReactNode[]; + /** Id of the pane. */ + id?: string; + /** @hide Options to list in the pane. */ + options?: React.ReactNode[]; + /** @hide Options currently selected in the pane. */ + selectedOptions?: string[] | number[]; + /** @hide Callback for when an option is selected. Optionally used only when options prop is provided. */ + onOptionSelect?: ( + event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, + index: number, + isChosen: boolean, + id?: string, + itemData?: any, + parentData?: any + ) => void; + /** @hide Callback for when a tree option is checked. Optionally used only when options prop is provided. */ + onOptionCheck?: ( + event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, + isChecked: boolean, + itemData: DualListSelectorTreeItemData + ) => void; + /** @hide Flag indicating a dynamically built search bar should be included above the pane. */ + isSearchable?: boolean; + /** Flag indicating whether the component is disabled. */ + isDisabled?: boolean; + /** Callback for search input. To be used when isSearchable is true. */ + onSearch?: (event: React.ChangeEvent) => void; + /** @hide A callback for when the search input value for changes. To be used when isSearchable is true. */ + onSearchInputChanged?: (event: React.FormEvent, value: string) => void; + /** @hide Callback for search input clear button */ + onSearchInputClear?: (event: React.SyntheticEvent) => void; + /** @hide Filter function for custom filtering based on search string. To be used when isSearchable is true. */ + filterOption?: (option: React.ReactNode, input: string) => boolean; + /** @hide Accessible label for the search input. To be used when isSearchable is true. */ + 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 = ({ + isChosen = false, + className = '', + status = '', + actions, + searchInput, + children, + onOptionSelect, + onOptionCheck, + title = '', + options = [], + selectedOptions = [], + isSearchable = false, + searchInputAriaLabel = '', + onFilterUpdate, + onSearchInputChanged, + onSearchInputClear, + filterOption, + id = getUniqueId('dual-list-selector-pane'), + isDisabled = false, + listMinHeight, + ...props +}: DualListSelectorPaneProps) => { + const [input, setInput] = React.useState(''); + const { isTree } = React.useContext(DualListSelectorContext); + + // only called when search input is dynamically built + const onChange = (e: React.FormEvent, newValue: string) => { + let filtered: React.ReactNode[]; + if (isTree) { + filtered = options + .map((opt) => Object.assign({}, opt)) + .filter((item) => filterInput(item as unknown as DualListSelectorTreeItemData, newValue)); + } else { + filtered = options.filter((option) => { + if (displayOption(option)) { + return option; + } + }); + } + onFilterUpdate(filtered, isChosen ? 'chosen' : 'available', newValue === ''); + + if (onSearchInputChanged) { + onSearchInputChanged(e, newValue); + } + setInput(newValue); + }; + + // only called when options are passed via options prop and isTree === true + const filterInput = (item: DualListSelectorTreeItemData, input: string): boolean => { + if (filterOption) { + return filterOption(item as unknown as React.ReactNode, input); + } else { + if (item.text.toLowerCase().includes(input.toLowerCase()) || input === '') { + return true; + } + } + if (item.children) { + return ( + (item.children = item.children + .map((opt) => Object.assign({}, opt)) + .filter((child) => filterInput(child, input))).length > 0 + ); + } + }; + + // only called when options are passed via options prop and isTree === false + const displayOption = (option: React.ReactNode) => { + if (filterOption) { + return filterOption(option, input); + } else { + return option.toString().toLowerCase().includes(input.toLowerCase()); + } + }; + + return ( +
+ {title && ( +
+
+
{title}
+
+
+ )} + {(actions || searchInput || isSearchable) && ( +
+ {(isSearchable || searchInput) && ( +
+ {searchInput ? ( + searchInput + ) : ( + onChange(e as React.FormEvent, '') + } + isDisabled={isDisabled} + aria-label={searchInputAriaLabel} + /> + )} +
+ )} + {actions &&
{actions}
} +
+ )} + {status && ( +
+
+ {status} +
+
+ )} + + {!isTree && ( + onOptionSelect(e, index, isChosen, id)} + displayOption={displayOption} + id={`${id}-list`} + isDisabled={isDisabled} + {...(listMinHeight && { + style: { [cssMenuMinHeight.name]: listMinHeight } as React.CSSProperties + })} + > + {children} + + )} + {isTree && ( + + {options.length > 0 ? ( + + Object.assign({}, opt)) + .filter((item) => + filterInput(item as unknown as DualListSelectorTreeItemData, input) + ) as unknown as DualListSelectorTreeItemData[]) + : (options as unknown as DualListSelectorTreeItemData[]) + } + onOptionCheck={onOptionCheck} + id={`${id}-tree`} + isDisabled={isDisabled} + /> + + ) : ( + children + )} + + )} + +
+ ); +}; +DualListSelectorPane.displayName = 'DualListSelectorPane'; diff --git a/packages/react-core/src/next/components/DualListSelector/DualListSelectorTree.tsx b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorTree.tsx similarity index 82% rename from packages/react-core/src/next/components/DualListSelector/DualListSelectorTree.tsx rename to packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorTree.tsx index 8fb28a289b9..a151d625050 100644 --- a/packages/react-core/src/next/components/DualListSelector/DualListSelectorTree.tsx +++ b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorTree.tsx @@ -10,26 +10,26 @@ export interface DualListSelectorTreeItemData { className?: string; /** Flag indicating this option is expanded by default. */ defaultExpanded?: boolean; - /** Flag indicating this option has a badge. */ + /** Flag indicating this option has a badge */ hasBadge?: boolean; - /** Callback fired when an option is checked. */ + /** Callback fired when an option is checked */ onOptionCheck?: ( event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, isChecked: boolean, isChosen: boolean, itemData: DualListSelectorTreeItemData ) => void; - /** ID of the option. */ + /** ID of the option */ id: string; - /** Text of the option. */ + /** Text of the option */ text: string; - /** Parent ID of an option. */ + /** Parent id of an option */ parentId?: string; - /** Checked state of the option. */ + /** Checked state of the option */ isChecked: boolean; - /** Additional properties to pass to the option checkbox. */ + /** Additional properties to pass to the option checkbox */ checkProps?: any; - /** Additional properties to pass to the option badge. */ + /** Additional properties to pass to the option badge */ badgeProps?: any; /** Flag indicating whether the component is disabled. */ isDisabled?: boolean; @@ -40,19 +40,19 @@ export interface DualListSelectorTreeItemData { */ export interface DualListSelectorTreeProps extends Omit, 'data'> { - /** Data of the tree view. */ + /** Data of the tree view */ data: DualListSelectorTreeItemData[] | (() => DualListSelectorTreeItemData[]); - /** ID of the tree view. */ + /** ID of the tree view */ id?: string; - /** @hide Flag indicating if the list is nested. */ + /** @hide Flag indicating if the list is nested */ isNested?: boolean; - /** Flag indicating if all options should have badges. */ + /** Flag indicating if all options should have badges */ hasBadges?: boolean; - /** Sets the default expanded behavior. */ + /** Sets the default expanded behavior */ defaultAllExpanded?: boolean; - /** Flag indicating if the dual list selector tree is in the disabled state. */ + /** Flag indicating if the dual list selector tree is in the disabled state */ isDisabled?: boolean; - /** Callback fired when an option is checked. */ + /** Callback fired when an option is checked */ onOptionCheck?: ( event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, isChecked: boolean, diff --git a/packages/react-core/src/next/components/DualListSelector/DualListSelectorTreeItem.tsx b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorTreeItem.tsx similarity index 95% rename from packages/react-core/src/next/components/DualListSelector/DualListSelectorTreeItem.tsx rename to packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorTreeItem.tsx index bfefccc81f8..7b450b1aa74 100644 --- a/packages/react-core/src/next/components/DualListSelector/DualListSelectorTreeItem.tsx +++ b/packages/react-core/src/deprecated/components/DualListSelector/DualListSelectorTreeItem.tsx @@ -14,25 +14,25 @@ export interface DualListSelectorTreeItemProps extends React.HTMLProps | React.KeyboardEvent, isChecked: boolean, itemData: DualListSelectorTreeItemData ) => void; - /** ID of the option. */ + /** ID of the option */ id: string; - /** Text of the option. */ + /** Text of the option */ text: string; /** Flag indicating if this open is checked. */ isChecked?: boolean; - /** Additional properties to pass to the option checkbox. */ + /** Additional properties to pass to the option checkbox */ checkProps?: any; - /** Additional properties to pass to the option badge. */ + /** Additional properties to pass to the option badge */ badgeProps?: any; - /** Raw data of the option. */ + /** Raw data of the option */ itemData?: DualListSelectorTreeItemData; /** Flag indicating whether the component is disabled. */ isDisabled?: boolean; diff --git a/packages/react-core/src/deprecated/components/DualListSelector/__tests__/DualListSelector.test.tsx b/packages/react-core/src/deprecated/components/DualListSelector/__tests__/DualListSelector.test.tsx new file mode 100644 index 00000000000..0bf84200bf7 --- /dev/null +++ b/packages/react-core/src/deprecated/components/DualListSelector/__tests__/DualListSelector.test.tsx @@ -0,0 +1,65 @@ +import { render } from '@testing-library/react'; +import { DualListSelector } from '../DualListSelector'; +import React from 'react'; + +describe('DualListSelector', () => { + test('basic', () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); + + test('with search inputs', () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot(); + }); + + test('with custom status', () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot(); + }); + + test('basic with disabled controls', () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot(); + }); + + test('with tree', () => { + const { asFragment } = render( + + ); + expect(asFragment()).toMatchSnapshot(); + }); + + test('with actions', () => { + const { asFragment } = render( + TestNode1]} + chosenOptionsActions={[TestNode2]} + id="fourthTest" + /> + ); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/packages/react-core/src/deprecated/components/DualListSelector/__tests__/__snapshots__/DualListSelector.test.tsx.snap b/packages/react-core/src/deprecated/components/DualListSelector/__tests__/__snapshots__/DualListSelector.test.tsx.snap new file mode 100644 index 00000000000..7c28575d55c --- /dev/null +++ b/packages/react-core/src/deprecated/components/DualListSelector/__tests__/__snapshots__/DualListSelector.test.tsx.snap @@ -0,0 +1,1785 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DualListSelector basic 1`] = ` + +
+
+
+
+
+ Available options +
+
+
+
+
+ 0 of 2 items selected +
+
+
+
    +
  • +
    + + + + Option 1 + + + +
    +
  • +
  • +
    + + + + Option 2 + + + +
    +
  • +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ Chosen options +
+
+
+
+
+ 0 of 0 items selected +
+
+
+
    +
+
+
+
+`; + +exports[`DualListSelector basic with disabled controls 1`] = ` + +
+
+
+
+
+ Available options +
+
+
+
+
+ 0 of 2 items selected +
+
+
+
    +
  • +
    + + + + Option 1 + + + +
    +
  • +
  • +
    + + + + Option 2 + + + +
    +
  • +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ Chosen options +
+
+
+
+
+ 0 of 0 items selected +
+
+
+
    +
+
+
+
+`; + +exports[`DualListSelector with actions 1`] = ` + +
+
+
+
+
+ Available options +
+
+
+
+
+ + TestNode1 + +
+
+
+
+ 0 of 2 items selected +
+
+
+
    +
  • +
    + + + + Option 1 + + + +
    +
  • +
  • +
    + + + + Option 2 + + + +
    +
  • +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ Chosen options +
+
+
+
+
+ + TestNode2 + +
+
+
+
+ 0 of 2 items selected +
+
+
+
    +
  • +
    + + + + Option 3 + + + +
    +
  • +
  • +
    + + + + Option 4 + + + +
    +
  • +
+
+
+
+
+`; + +exports[`DualListSelector with custom status 1`] = ` + +
+
+
+
+
+ Available options +
+
+
+
+
+ Test status1 +
+
+
+
    +
  • +
    + + + + Option 1 + + + +
    +
  • +
  • +
    + + + + Option 2 + + + +
    +
  • +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ Chosen options +
+
+
+
+
+ Test status2 +
+
+
+
    +
+
+
+
+`; + +exports[`DualListSelector with search inputs 1`] = ` + +
+
+
+
+
+ Available options +
+
+
+
+
+
+
+ + + + + + +
+
+
+
+
+
+ 0 of 2 items selected +
+
+
+
    +
  • +
    + + + + Option 1 + + + +
    +
  • +
  • +
    + + + + Option 2 + + + +
    +
  • +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ Chosen options +
+
+
+
+
+
+
+ + + + + + +
+
+
+
+
+
+ 0 of 0 items selected +
+
+
+
    +
+
+
+
+`; + +exports[`DualListSelector with tree 1`] = ` + +
+
+
+
+
+ Available options +
+
+
+
+
+ Test status1 +
+
+
+
    +
  • +
    +
    + +
    + + + +
    + + + + + Opt1 + +
    +
    +
    +
      +
    • +
      +
      + + + + + + Opt3 + + +
      +
      +
    • +
    +
  • +
  • +
    +
    + + + + + + Opt2 + + +
    +
    +
  • +
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ Chosen options +
+
+
+
+
+ Test status2 +
+
+
+
    +
+
+
+
+`; diff --git a/packages/react-core/src/next/components/DualListSelector/examples/DualListSelector.md b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelector.md similarity index 67% rename from packages/react-core/src/next/components/DualListSelector/examples/DualListSelector.md rename to packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelector.md index d5f6b8e7647..9bf078d52ee 100644 --- a/packages/react-core/src/next/components/DualListSelector/examples/DualListSelector.md +++ b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelector.md @@ -13,9 +13,22 @@ propComponents: 'DualListSelectorTree', 'DualListSelectorTreeItemData' ] -beta: true +deprecated: true --- +import { +DragDrop, +Draggable, +Droppable, +DualListSelector as DLSDeprecated, +DualListSelectorPane as DLSPaneDeprecated, +DualListSelectorList as DLSListDeprecated, +DualListSelectorListItem as DLSListItemDeprecated, +DualListSelectorControlsWrapper as DLSControlsWrapperDeprecated, +DualListSelectorControl as DLSControlDeprecated, +DualListSelectorTree as DLSTreeDeprecated, +DualListSelectorTreeItemData as DLSTreeItemDataDeprecated, +} from '@patternfly/react-core/deprecated'; 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'; @@ -23,32 +36,9 @@ import AngleRightIcon from '@patternfly/react-icons/dist/esm/icons/angle-right-i import PficonSortCommonAscIcon from '@patternfly/react-icons/dist/esm/icons/pficon-sort-common-asc-icon'; import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; -import { DragDrop, Draggable, Droppable } from '@patternfly/react-core/deprecated'; ## Examples -The dual list selector is built in a composable manner to make customization easier. The standard sub-component relationships are arranged as follows: - -```noLive - - - - - - - - - /* A standard Dual list selector has 4 controls */ - - - - - - - - -``` - ### Basic ```ts file="./DualListSelectorBasic.tsx" @@ -75,11 +65,43 @@ The dual list selector is built in a composable manner to make customization eas ### With tree -```ts file="DualListSelectorTree.tsx" +```ts file="./DualListSelectorTreeExample.tsx" + +``` + +## Composable structure + +The dual list selector can also be built in a composable manner to make customization easier. The standard sub-component relationships are arranged as follows: + +```noLive + + + + + + + + + /* A standard Dual list selector has 4 controls */ + + + + + + + + +``` + +### Composable dual list selector + +```ts file="./DualListSelectorComposable.tsx" ``` -### Drag and drop +### Composable with drag and drop + +Note: There is a new recommended drag and drop implementation with full keyboard functionality, which replaces this implementation. To adhere to our new recommendations, refer to the [drag and drop demos](/components/drag-and-drop/react-demos). This example only allows reordering the contents of the "chosen" pane with drag and drop. To make a pane able to be reordered: @@ -93,8 +115,14 @@ This example only allows reordering the contents of the "chosen" pane with drag - define an `onDrag` callback which ensures that the drag event will not cross hairs with the `onOptionSelect` click event set on the option. Note: the `ignoreNextOptionSelect` state value is used to prevent selection while dragging. -Note: Keyboard accessibility and screen reader accessibility for the `DragDrop` component are still in development. +Keyboard and screen reader accessibility for the `` component is still in development. + +```ts isDeprecated file="DualListSelectorComposableDragDrop.tsx" + +``` + +### Composable with tree -```ts file="DualListSelectorDragDrop.tsx" +```ts file="DualListSelectorComposableTree.tsx" ``` diff --git a/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorBasic.tsx b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorBasic.tsx new file mode 100644 index 00000000000..c253a7d0a1d --- /dev/null +++ b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorBasic.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { DualListSelector as DLSDeprecated } from '@patternfly/react-core/deprecated'; + +export const DualListSelectorBasic: React.FunctionComponent = () => { + const [availableOptions, setAvailableOptions] = React.useState([ + 'Option 1', + 'Option 2', + 'Option 3', + 'Option 4' + ]); + const [chosenOptions, setChosenOptions] = React.useState([]); + + const onListChange = ( + event: React.MouseEvent, + newAvailableOptions: React.ReactNode[], + newChosenOptions: React.ReactNode[] + ) => { + setAvailableOptions(newAvailableOptions.sort()); + setChosenOptions(newChosenOptions.sort()); + }; + + return ( + + ); +}; diff --git a/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorBasicSearch.tsx b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorBasicSearch.tsx new file mode 100644 index 00000000000..1ae0e33ef71 --- /dev/null +++ b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorBasicSearch.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { DualListSelector as DLSDeprecated } from '@patternfly/react-core/deprecated'; + +export const DualListSelectorBasicSearch: React.FunctionComponent = () => { + const [availableOptions, setAvailableOptions] = React.useState([ + 'Option 1', + 'Option 2', + 'Option 3', + 'Option 4' + ]); + const [chosenOptions, setChosenOptions] = React.useState([]); + + const onListChange = ( + event: React.MouseEvent, + newAvailableOptions: React.ReactNode[], + newChosenOptions: React.ReactNode[] + ) => { + setAvailableOptions(newAvailableOptions.sort()); + setChosenOptions(newChosenOptions.sort()); + }; + + return ( + + ); +}; diff --git a/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorBasicTooltips.tsx b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorBasicTooltips.tsx new file mode 100644 index 00000000000..6bf32db13bf --- /dev/null +++ b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorBasicTooltips.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { DualListSelector as DLSDeprecated } from '@patternfly/react-core/deprecated'; + +export const DualListSelectorBasicTooltips: React.FunctionComponent = () => { + const [availableOptions, setAvailableOptions] = React.useState([ + 'Option 1', + 'Option 2', + 'Option 3', + 'Option 4' + ]); + const [chosenOptions, setChosenOptions] = React.useState([]); + + const onListChange = ( + event: React.MouseEvent, + newAvailableOptions: React.ReactNode[], + newChosenOptions: React.ReactNode[] + ) => { + setAvailableOptions(newAvailableOptions.sort()); + setChosenOptions(newChosenOptions.sort()); + }; + + return ( + + ); +}; diff --git a/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorComplexOptionsActions.tsx b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorComplexOptionsActions.tsx new file mode 100644 index 00000000000..67082c0d55d --- /dev/null +++ b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorComplexOptionsActions.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { Button, ButtonVariant, Checkbox } from '@patternfly/react-core'; +import { DualListSelector as DLSDeprecated } from '@patternfly/react-core/deprecated'; +import PficonSortCommonAscIcon from '@patternfly/react-icons/dist/esm/icons/pficon-sort-common-asc-icon'; + +export const DualListSelectorComplexOptionsActions: React.FunctionComponent = () => { + const [availableOptions, setAvailableOptions] = React.useState([ + Option 1, + Option 3, + Option 4, + Option 2 + ]); + const [chosenOptions, setChosenOptions] = React.useState([]); + const [isDisabled, setIsDisabled] = React.useState(false); + + const onListChange = (newAvailableOptions: React.ReactNode[], newChosenOptions: React.ReactNode[]) => { + setAvailableOptions(newAvailableOptions); + setChosenOptions(newChosenOptions); + }; + + const onSort = (pane: string) => { + const toSort = pane === 'available' ? [...availableOptions] : [...chosenOptions]; + (toSort as React.ReactElement[]).sort((a, b) => { + if (a.props.children > b.props.children) { + return 1; + } + if (a.props.children < b.props.children) { + return -1; + } + return 0; + }); + + if (pane === 'available') { + setAvailableOptions(toSort); + } else { + setChosenOptions(toSort); + } + }; + + const filterOption = (option: React.ReactNode, input: string) => + (option as React.ReactElement).props.children.includes(input); + + const availableOptionsActions = [ + + ]; + + const chosenOptionsActions = [ + + ]; + + return ( + + + setIsDisabled(!isDisabled)} + /> + + ); +}; diff --git a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposable.tsx b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorComposable.tsx similarity index 88% rename from packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposable.tsx rename to packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorComposable.tsx index 53870b2bbef..87534c8db49 100644 --- a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposable.tsx +++ b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorComposable.tsx @@ -2,12 +2,6 @@ import React from 'react'; import { Button, ButtonVariant, - DualListSelector, - DualListSelectorPane, - DualListSelectorList, - DualListSelectorListItem, - DualListSelectorControlsWrapper, - DualListSelectorControl, SearchInput, EmptyState, EmptyStateVariant, @@ -15,6 +9,14 @@ import { EmptyStateBody, EmptyStateActions } from '@patternfly/react-core'; +import { + DualListSelector as DLSDeprecated, + DualListSelectorPane as DLSPaneDeprecated, + DualListSelectorList as DLSListDeprecated, + DualListSelectorListItem as DLSListItemDeprecated, + DualListSelectorControlsWrapper as DLSControlsWrapperDeprecated, + DualListSelectorControl as DLSControlDeprecated +} from '@patternfly/react-core/deprecated'; 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'; @@ -150,8 +152,8 @@ export const DualListSelectorComposable: React.FunctionComponent = () => { ); return ( - - + option.selected && option.isVisible).length} of ${ availableOptions.filter((option) => option.isVisible).length @@ -164,57 +166,57 @@ export const DualListSelectorComposable: React.FunctionComponent = () => { 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 )} - + )} - - - + + option.selected)} onClick={() => moveSelected(true)} aria-label="Add selected" tooltipContent="Add selected" > - - + moveAll(true)} aria-label="Add all" tooltipContent="Add all" > - - + moveAll(false)} aria-label="Remove all" tooltipContent="Remove all" > - - + moveSelected(false)} isDisabled={!chosenOptions.some((option) => option.selected)} aria-label="Remove selected" tooltipContent="Remove selected" > - - - + + option.selected && option.isVisible).length} of ${ chosenOptions.filter((option) => option.isVisible).length @@ -228,22 +230,22 @@ export const DualListSelectorComposable: React.FunctionComponent = () => { 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/DualListSelectorComposableDragDrop.tsx b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorComposableDragDrop.tsx similarity index 82% rename from packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposableDragDrop.tsx rename to packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorComposableDragDrop.tsx index ecc0a13c9d1..b7d00dd4702 100644 --- a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposableDragDrop.tsx +++ b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorComposableDragDrop.tsx @@ -1,13 +1,16 @@ import React from 'react'; import { - DualListSelector, - DualListSelectorPane, - DualListSelectorList, - DualListSelectorListItem, - DualListSelectorControlsWrapper, - DualListSelectorControl -} from '@patternfly/react-core'; -import { DragDrop, Draggable, Droppable, DraggableItemPosition } from '@patternfly/react-core/deprecated'; + DragDrop, + Draggable, + Droppable, + DraggableItemPosition, + DualListSelector as DLSDeprecated, + DualListSelectorPane as DLSPaneDeprecated, + DualListSelectorList as DLSListDeprecated, + DualListSelectorListItem as DLSListItemDeprecated, + DualListSelectorControlsWrapper as DLSControlsWrapperDeprecated, + DualListSelectorControl as DLSControlDeprecated +} from '@patternfly/react-core/deprecated'; 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'; @@ -90,58 +93,58 @@ export const DualListSelectorComposableDragDrop: React.FunctionComponent = () => }; 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" > - - + moveAll(true)} aria-label="Add all" > - - + moveAll(false)} aria-label="Remove all" > - - + moveSelected(false)} isDisabled={!chosenOptions.some((option) => option.selected)} aria-label="Remove selected" > - - + + { setIgnoreNextOptionSelect(true); @@ -149,7 +152,7 @@ export const DualListSelectorComposableDragDrop: React.FunctionComponent = () => }} onDrop={onDrop} > - option.selected && option.isVisible).length} of ${ chosenOptions.filter((option) => option.isVisible).length @@ -157,25 +160,25 @@ export const DualListSelectorComposableDragDrop: React.FunctionComponent = () => isChosen > - + {chosenOptions.map((option, index) => option.isVisible ? ( - onOptionSelect(e, index, true)} isDraggable > {option.text} - + ) : null )} - + - + - + ); }; diff --git a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposableTree.tsx b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorComposableTree.tsx similarity index 91% rename from packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposableTree.tsx rename to packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorComposableTree.tsx index 1adeda4b975..238ce17db1d 100644 --- a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposableTree.tsx +++ b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorComposableTree.tsx @@ -1,12 +1,5 @@ import React from 'react'; import { - DualListSelector, - DualListSelectorPane, - DualListSelectorList, - DualListSelectorControlsWrapper, - DualListSelectorControl, - DualListSelectorTree, - DualListSelectorTreeItemData, SearchInput, Button, EmptyState, @@ -15,6 +8,15 @@ import { EmptyStateBody, EmptyStateActions } from '@patternfly/react-core'; +import { + DualListSelector as DLSDeprecated, + DualListSelectorPane as DLSPaneDeprecated, + DualListSelectorList as DLSListDeprecated, + DualListSelectorControlsWrapper as DLSControlsWrapperDeprecated, + DualListSelectorControl as DLSControlDeprecated, + DualListSelectorTree as DLSTreeDeprecated, + DualListSelectorTreeItemData as DLSTreeItemDataDeprecated +} from '@patternfly/react-core/deprecated'; 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'; @@ -144,7 +146,7 @@ export const DualListSelectorComposableTree: React.FunctionComponent | React.KeyboardEvent, isChecked: boolean, - node: DualListSelectorTreeItemData, + node: DLSTreeItemDataDeprecated, isChosen: boolean ) => { const nodeIdsToCheck = memoizedLeavesById[node.id].filter((id) => @@ -181,7 +183,7 @@ export const DualListSelectorComposableTree: React.FunctionComponent { + ): DLSTreeItemDataDeprecated[] => { if (!node) { return []; } @@ -240,7 +242,7 @@ export const DualListSelectorComposableTree: React.FunctionComponent { - const options: DualListSelectorTreeItemData[] = buildOptions(isChosen, data, false); + const options: DLSTreeItemDataDeprecated[] = 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) @@ -248,7 +250,7 @@ export const DualListSelectorComposableTree: React.FunctionComponent )} {options.length > 0 && ( - - + onOptionCheck(e, isChecked, itemData, isChosen)} /> - + )} - + ); }; return ( - + {buildPane(false)} - - + !chosenLeafIds.includes(x)).length} onClick={() => moveChecked(true)} aria-label="Add selected" > - - + moveAll(true)} aria-label="Add all" > - - + moveAll(false)} aria-label="Remove all" > - - + moveChecked(false)} isDisabled={!checkedLeafIds.filter((x) => !!chosenLeafIds.includes(x)).length} aria-label="Remove selected" > - - + + {buildPane(true)} - + ); }; diff --git a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorTreeExample.tsx b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorTreeExample.tsx similarity index 87% rename from packages/react-core/src/components/DualListSelector/examples/DualListSelectorTreeExample.tsx rename to packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorTreeExample.tsx index 6bcb06ac573..587dcbe3a41 100644 --- a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorTreeExample.tsx +++ b/packages/react-core/src/deprecated/components/DualListSelector/examples/DualListSelectorTreeExample.tsx @@ -1,8 +1,11 @@ import React from 'react'; -import { DualListSelector, DualListSelectorTreeItemData } from '@patternfly/react-core'; +import { + DualListSelector as DLSDeprecated, + DualListSelectorTreeItemData as DLSTreeItemDataDeprecated +} from '@patternfly/react-core/deprecated'; export const DualListSelectorTreeExample: React.FunctionComponent = () => { - const [availableOptions, setAvailableOptions] = React.useState([ + const [availableOptions, setAvailableOptions] = React.useState([ { id: 'F1', text: 'Folder 1', @@ -38,7 +41,7 @@ export const DualListSelectorTreeExample: React.FunctionComponent = () => { } ]); - const [chosenOptions, setChosenOptions] = React.useState([ + const [chosenOptions, setChosenOptions] = React.useState([ { id: 'CF1', text: 'Chosen Folder 1', @@ -75,15 +78,15 @@ export const DualListSelectorTreeExample: React.FunctionComponent = () => { const onListChange = ( event: React.MouseEvent, - newAvailableOptions: DualListSelectorTreeItemData[], - newChosenOptions: DualListSelectorTreeItemData[] + newAvailableOptions: DLSTreeItemDataDeprecated[], + newChosenOptions: DLSTreeItemDataDeprecated[] ) => { setAvailableOptions(newAvailableOptions.sort()); setChosenOptions(newChosenOptions.sort()); }; return ( - { - static displayName = 'DualListSelector'; - static defaultProps: PickOptional = { - children: '', - isTree: false - }; - - constructor(props: DualListSelectorProps) { - super(props); - } - - render() { - const { className, children, id, isTree, ...props } = this.props; - - return ( - - - {(randomId) => ( -
- {children} -
- )} -
-
- ); - } -} - -export { DualListSelector }; diff --git a/packages/react-core/src/next/components/DualListSelector/DualListSelectorPane.tsx b/packages/react-core/src/next/components/DualListSelector/DualListSelectorPane.tsx deleted file mode 100644 index 3ab0db8da61..00000000000 --- a/packages/react-core/src/next/components/DualListSelector/DualListSelectorPane.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import * as React from 'react'; -import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector'; -import { css } from '@patternfly/react-styles'; -import { getUniqueId } from '../../../helpers'; -import { DualListSelectorListWrapper } from './DualListSelectorListWrapper'; -import { DualListSelectorPaneContext } from './DualListSelectorContext'; -import { SearchInput } from '../../../components/SearchInput'; -import cssMenuMinHeight from '@patternfly/react-tokens/dist/esm/c_dual_list_selector__menu_MinHeight'; - -/** Acts as the container for a list of options that are either available or chosen, - * depending on the pane type (available or chosen). A search input and other actions, - * such as sorting, can also be passed into this sub-component. - */ - -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. */ - children?: React.ReactNode; - /** Flag indicating if this pane is the chosen pane. */ - isChosen?: boolean; - /** Status to display above the pane. */ - status?: string; - /** Title of the pane. */ - title?: React.ReactNode; - /** A search input placed above the list at the top of the pane, before actions. */ - searchInput?: React.ReactNode; - /** Actions to place above the pane. */ - actions?: React.ReactNode[]; - /** ID of the pane. */ - id?: string; - /** Flag indicating whether the component is disabled. */ - isDisabled?: boolean; - /** Callback for search input. To be used when isSearchable is true. */ - onSearch?: (event: React.ChangeEvent) => void; - /** Minimum height of the list of options rendered in the pane. **/ - listMinHeight?: string; -} - -export const DualListSelectorPane: React.FunctionComponent = ({ - isChosen = false, - className = '', - status = '', - actions, - searchInput, - children, - title = '', - id = getUniqueId('dual-list-selector-pane'), - isDisabled = false, - listMinHeight, - ...props -}: DualListSelectorPaneProps) => ( -
- {title && ( -
-
-
{title}
-
-
- )} - {(actions || searchInput) && ( -
- {searchInput && ( -
- {searchInput ? searchInput : } -
- )} - {actions &&
{actions}
} -
- )} - {status && ( -
-
- {status} -
-
- )} - - - {children} - - -
-); -DualListSelectorPane.displayName = 'DualListSelectorPane'; diff --git a/packages/react-core/src/next/components/DualListSelector/__tests__/DualListSelector.test.tsx b/packages/react-core/src/next/components/DualListSelector/__tests__/DualListSelector.test.tsx deleted file mode 100644 index c9d3fb38e8d..00000000000 --- a/packages/react-core/src/next/components/DualListSelector/__tests__/DualListSelector.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { render } from '@testing-library/react'; -import { DualListSelectorPane } from '../../DualListSelector'; -import { SearchInput } from '../../../../components/SearchInput'; -import React from 'react'; - -describe('DualListSelector', () => { - test('basic', () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); - - test('with search inputs', () => { - const { asFragment } = render(} />); - expect(asFragment()).toMatchSnapshot(); - }); - - test('with custom status', () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); - - test('basic with disabled controls', () => { - const { asFragment } = render(); - expect(asFragment()).toMatchSnapshot(); - }); -}); diff --git a/packages/react-core/src/next/components/DualListSelector/__tests__/__snapshots__/DualListSelector.test.tsx.snap b/packages/react-core/src/next/components/DualListSelector/__tests__/__snapshots__/DualListSelector.test.tsx.snap deleted file mode 100644 index 63dab5eee9e..00000000000 --- a/packages/react-core/src/next/components/DualListSelector/__tests__/__snapshots__/DualListSelector.test.tsx.snap +++ /dev/null @@ -1,122 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`DualListSelector basic 1`] = ` - -
-
-
    -
-
-
-`; - -exports[`DualListSelector basic with disabled controls 1`] = ` - -
-
-
    -
-
-
-`; - -exports[`DualListSelector with custom status 1`] = ` - -
-
-
- Test status1 -
-
-
-
    -
-
-
-`; - -exports[`DualListSelector with search inputs 1`] = ` - -
-
-
-
-
- - - - - - -
-
-
-
-
-
    -
-
-
-`; diff --git a/packages/react-core/src/next/components/DualListSelector/examples/DualListSelectorBasic.tsx b/packages/react-core/src/next/components/DualListSelector/examples/DualListSelectorBasic.tsx deleted file mode 100644 index 53e287e5d1d..00000000000 --- a/packages/react-core/src/next/components/DualListSelector/examples/DualListSelectorBasic.tsx +++ /dev/null @@ -1,157 +0,0 @@ -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 DualListSelectorBasic: 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" - > - - - moveAll(true)} - aria-label="Add all" - > - - - moveAll(false)} - aria-label="Remove all" - > - - - moveSelected(false)} - isDisabled={!chosenOptions.some((option) => option.selected)} - aria-label="Remove selected" - > - - - - 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-core/src/next/components/DualListSelector/examples/DualListSelectorBasicSearch.tsx b/packages/react-core/src/next/components/DualListSelector/examples/DualListSelectorBasicSearch.tsx deleted file mode 100644 index 3e467cd034f..00000000000 --- a/packages/react-core/src/next/components/DualListSelector/examples/DualListSelectorBasicSearch.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import React from 'react'; -import { - Button, - DualListSelector, - DualListSelectorPane, - DualListSelectorList, - DualListSelectorListItem, - DualListSelectorControlsWrapper, - DualListSelectorControl, - SearchInput, - 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 Option { - text: string; - selected: boolean; - isVisible: boolean; -} - -export const DualListSelectorSearch: 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 [availableFilter, setAvailableFilter] = React.useState(''); - const [chosenFilter, setChosenFilter] = 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); - } - }; - - 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)} - /> - ); - - const buildEmptyState = (isAvailable: boolean) => ( - - No results match the filter criteria. Clear all filters and try again. - - - - - - - ); - - return ( - - option.selected && option.isVisible).length} of ${ - availableOptions.filter((option) => option.isVisible).length - } options selected`} - searchInput={buildSearchInput(true)} - listMinHeight="300px" - > - {availableFilter !== '' && - availableOptions.filter((option) => option.isVisible).length === 0 && - buildEmptyState(true)} - - - {availableOptions.map((option, index) => - option.isVisible ? ( - onOptionSelect(e, index, false)} - > - {option.text} - - ) : null - )} - - - - option.selected)} - onClick={() => moveSelected(true)} - aria-label="Add selected" - > - - - moveAll(true)} - aria-label="Add all" - > - - - moveAll(false)} - aria-label="Remove all" - > - - - moveSelected(false)} - isDisabled={!chosenOptions.some((option) => option.selected)} - aria-label="Remove selected" - > - - - - option.selected && option.isVisible).length} of ${ - chosenOptions.filter((option) => option.isVisible).length - } options selected`} - searchInput={buildSearchInput(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)} - > - {option.text} - - ) : null - )} - - )} - - - ); -}; diff --git a/packages/react-core/src/next/components/DualListSelector/examples/DualListSelectorBasicTooltips.tsx b/packages/react-core/src/next/components/DualListSelector/examples/DualListSelectorBasicTooltips.tsx deleted file mode 100644 index 717c5860bd7..00000000000 --- a/packages/react-core/src/next/components/DualListSelector/examples/DualListSelectorBasicTooltips.tsx +++ /dev/null @@ -1,165 +0,0 @@ -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 DualListSelectorBasic: 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' }} - > - - - moveAll(true)} - aria-label="Add all" - tooltipContent="Add all" - tooltipProps={{ position: 'right', 'aria-live': 'off' }} - > - - - moveAll(false)} - aria-label="Remove all" - tooltipContent="Remove all" - tooltipProps={{ position: 'left', 'aria-live': 'off' }} - > - - - moveSelected(false)} - isDisabled={!chosenOptions.some((option) => option.selected)} - aria-label="Remove selected" - tooltipContent="Remove selected" - tooltipProps={{ position: 'bottom', 'aria-live': 'off' }} - > - - - - 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-core/src/next/components/DualListSelector/examples/DualListSelectorComplexOptionsActions.tsx b/packages/react-core/src/next/components/DualListSelector/examples/DualListSelectorComplexOptionsActions.tsx deleted file mode 100644 index 9ccc57b142f..00000000000 --- a/packages/react-core/src/next/components/DualListSelector/examples/DualListSelectorComplexOptionsActions.tsx +++ /dev/null @@ -1,344 +0,0 @@ -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 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; -} - -export const DualListSelectorComplexOptionsActionsNext: 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 [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 onSort = () => { - const toSort = isAvailable ? [...availableOptions] : [...chosenOptions]; - toSort.sort((a, b) => { - if (a.text > b.text) { - return 1; - } - if (a.text < b.text) { - return -1; - } - return 0; - }); - if (isAvailable) { - setAvailableOptions(toSort); - } else { - setChosenOptions(toSort); - } - }; - - const onToggle = (pane: string) => { - if (pane === 'available') { - setIsAvailableKebabOpen(!isAvailableKebabOpen); - } else { - setIsChosenKebabOpen(!isChosenKebabOpen); - } - }; - - return isAvailable - ? [ - , - ) => ( - onToggle('available')} - variant="plain" - id="complex-available-toggle" - aria-label="Complex actions example available kebab toggle" - > - - )} - isOpen={isAvailableKebabOpen} - onOpenChange={(isOpen: boolean) => setIsAvailableKebabOpen(isOpen)} - onSelect={() => setIsAvailableKebabOpen(false)} - key="availableDropdown" - > - - Available Action - event.preventDefault()}> - Available Link - - - - ] - : [ - , - ) => ( - onToggle('chosen')} - variant="plain" - id="complex-chosen-toggle" - aria-label="Complex actions example chosen kebab toggle" - > - - )} - isOpen={isChosenKebabOpen} - onOpenChange={(isOpen: boolean) => setIsChosenKebabOpen(isOpen)} - onSelect={() => setIsChosenKebabOpen(false)} - key="chosenDropdown" - > - - Chosen Action - event.preventDefault()}> - Chosen Link - - - - ]; - }; - - const buildEmptyState = (isAvailable: boolean) => ( - - No results match the filter criteria. Clear all filters and try again. - - - - - - - ); - - 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" - > - - - moveAll(true)} - aria-label="Add all" - > - - - moveAll(false)} - aria-label="Remove all" - > - - - moveSelected(false)} - isDisabled={!chosenOptions.some((option) => option.selected) || isDisabled} - aria-label="Remove selected" - > - - - - 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-core/src/next/components/index.ts b/packages/react-core/src/next/components/index.ts index 00d4164b54b..e69de29bb2d 100644 --- a/packages/react-core/src/next/components/index.ts +++ b/packages/react-core/src/next/components/index.ts @@ -1 +0,0 @@ -export * from './DualListSelector'; diff --git a/packages/react-core/src/next/index.ts b/packages/react-core/src/next/index.ts index 07635cbbc8e..e69de29bb2d 100644 --- a/packages/react-core/src/next/index.ts +++ b/packages/react-core/src/next/index.ts @@ -1 +0,0 @@ -export * from './components'; diff --git a/packages/react-drag-drop/src/components/DragDrop/DraggableDualListSelectorListItem.tsx b/packages/react-drag-drop/src/components/DragDrop/DraggableDualListSelectorListItem.tsx index 7e92d6c481b..eef43cf82c3 100644 --- a/packages/react-drag-drop/src/components/DragDrop/DraggableDualListSelectorListItem.tsx +++ b/packages/react-drag-drop/src/components/DragDrop/DraggableDualListSelectorListItem.tsx @@ -5,7 +5,7 @@ import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector'; import dragStyles from '@patternfly/react-styles/css/components/DragDrop/drag-drop'; import { DragButton } from './DragButton'; -import { DualListSelectorListContext } from '@patternfly/react-core/dist/esm/components/DualListSelector'; +import { DualListSelectorListContext as DLSListContextDeprecated } from '@patternfly/react-core/dist/esm/deprecated/components/DualListSelector'; export interface DraggableDualListSelectorListItemProps extends React.HTMLProps { /** Content rendered inside DragDrop */ @@ -42,7 +42,7 @@ export const DraggableDualListSelectorListItem: React.FunctionComponent false }); - const { setFocusedOption } = React.useContext(DualListSelectorListContext); + const { setFocusedOption } = React.useContext(DLSListContextDeprecated); const style = { transform: CSS.Transform.toString(transform), diff --git a/packages/react-drag-drop/src/next/components/DragDrop/DraggableDualListSelectorListItem.tsx b/packages/react-drag-drop/src/next/components/DragDrop/DraggableDualListSelectorListItem.tsx index 7e92d6c481b..86415f89e7b 100644 --- a/packages/react-drag-drop/src/next/components/DragDrop/DraggableDualListSelectorListItem.tsx +++ b/packages/react-drag-drop/src/next/components/DragDrop/DraggableDualListSelectorListItem.tsx @@ -5,7 +5,7 @@ import { css } from '@patternfly/react-styles'; import styles from '@patternfly/react-styles/css/components/DualListSelector/dual-list-selector'; import dragStyles from '@patternfly/react-styles/css/components/DragDrop/drag-drop'; import { DragButton } from './DragButton'; -import { DualListSelectorListContext } from '@patternfly/react-core/dist/esm/components/DualListSelector'; +import { DualListSelectorListContext } from '@patternfly/react-core/dist/esm/deprecated/components/DualListSelector'; export interface DraggableDualListSelectorListItemProps extends React.HTMLProps { /** Content rendered inside DragDrop */ diff --git a/packages/react-integration/cypress/integration/duallistselectorbasic.spec.ts b/packages/react-integration/cypress/integration/duallistselectordeprecatedbasic.spec.ts similarity index 97% rename from packages/react-integration/cypress/integration/duallistselectorbasic.spec.ts rename to packages/react-integration/cypress/integration/duallistselectordeprecatedbasic.spec.ts index ed82efb243d..03e59da2fa1 100644 --- a/packages/react-integration/cypress/integration/duallistselectorbasic.spec.ts +++ b/packages/react-integration/cypress/integration/duallistselectordeprecatedbasic.spec.ts @@ -1,6 +1,6 @@ -describe('Dual List Selector BasicDemo Test', () => { +describe('Dual List Selector deprecated BasicDemo Test', () => { it('Navigate to demo section', () => { - cy.visit('http://localhost:3000/dual-list-selector-basic-demo-nav-link'); + cy.visit('http://localhost:3000/dual-list-selector-deprecated-basic-demo-nav-link'); }); it('Verify existence', () => { diff --git a/packages/react-integration/cypress/integration/duallistselectortree.spec.ts b/packages/react-integration/cypress/integration/duallistselectordeprecatedtree.spec.ts similarity index 96% rename from packages/react-integration/cypress/integration/duallistselectortree.spec.ts rename to packages/react-integration/cypress/integration/duallistselectordeprecatedtree.spec.ts index 2e9753cc48f..81fd0af28de 100644 --- a/packages/react-integration/cypress/integration/duallistselectortree.spec.ts +++ b/packages/react-integration/cypress/integration/duallistselectordeprecatedtree.spec.ts @@ -1,6 +1,6 @@ -describe('Dual List Selector TreeDemo Test', () => { +describe('Dual List Selector deprecated TreeDemo Test', () => { it('Navigate to demo section', () => { - cy.visit('http://localhost:3000/dual-list-selector-tree-demo-nav-link'); + cy.visit('http://localhost:3000/dual-list-selector-deprecated-tree-demo-nav-link'); }); it('Verify existence', () => { diff --git a/packages/react-integration/cypress/integration/duallistselectorwithactions.spec.ts b/packages/react-integration/cypress/integration/duallistselectordeprecatedwithactions.spec.ts similarity index 97% rename from packages/react-integration/cypress/integration/duallistselectorwithactions.spec.ts rename to packages/react-integration/cypress/integration/duallistselectordeprecatedwithactions.spec.ts index 7fa6dae3e33..a1a0143bfb5 100644 --- a/packages/react-integration/cypress/integration/duallistselectorwithactions.spec.ts +++ b/packages/react-integration/cypress/integration/duallistselectordeprecatedwithactions.spec.ts @@ -1,6 +1,6 @@ -describe('Dual List Selector With Actions Demo Test', () => { +describe('Dual List Selector deprecated With Actions Demo Test', () => { it('Navigate to demo section', () => { - cy.visit('http://localhost:3000/dual-list-selector-with-actions-demo-nav-link'); + cy.visit('http://localhost:3000/dual-list-selector-deprecated-with-actions-demo-nav-link'); }); it('Verify existence', () => { diff --git a/packages/react-integration/demo-app-ts/src/Demos.ts b/packages/react-integration/demo-app-ts/src/Demos.ts index 3222738b3c3..da3c73365c1 100644 --- a/packages/react-integration/demo-app-ts/src/Demos.ts +++ b/packages/react-integration/demo-app-ts/src/Demos.ts @@ -143,19 +143,19 @@ export const Demos: DemoInterface[] = [ componentType: Examples.DropdownDemo }, { - id: 'dual-list-selector-basic-demo', - name: 'DualListSelector basic Demo', - componentType: Examples.DualListSelectorBasicDemo + id: 'dual-list-selector-deprecated-basic-demo', + name: 'DualListSelector deprecated basic Demo', + componentType: Examples.DualListSelectorDeprecatedBasicDemo }, { - id: 'dual-list-selector-tree-demo', - name: 'DualListSelector Tree Demo', - componentType: Examples.DualListSelectorTreeDemo + id: 'dual-list-selector-deprecated-tree-demo', + name: 'DualListSelector deprecated Tree Demo', + componentType: Examples.DualListSelectorDeprecatedTreeDemo }, { - id: 'dual-list-selector-with-actions-demo', - name: 'DualListSelector with actions Demo', - componentType: Examples.DualListSelectorWithActionsDemo + id: 'dual-list-selector-deprecated-with-actions-demo', + name: 'DualListSelector deprecated with actions Demo', + componentType: Examples.DualListSelectorDeprecatedWithActionsDemo }, { id: 'expandable-section-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/DualListSelectorDeprecatedDemo/DualListSelectorDeprecatedBasicDemo.tsx similarity index 75% rename from packages/react-integration/demo-app-ts/src/components/demos/DualListSelectorDemo/DualListSelectorBasicDemo.tsx rename to packages/react-integration/demo-app-ts/src/components/demos/DualListSelectorDeprecatedDemo/DualListSelectorDeprecatedBasicDemo.tsx index a3398273f5c..75d52e31a8b 100644 --- a/packages/react-integration/demo-app-ts/src/components/demos/DualListSelectorDemo/DualListSelectorBasicDemo.tsx +++ b/packages/react-integration/demo-app-ts/src/components/demos/DualListSelectorDeprecatedDemo/DualListSelectorDeprecatedBasicDemo.tsx @@ -1,11 +1,14 @@ import React, { Component } from 'react'; -import { DualListSelector, DualListSelectorProps } from '@patternfly/react-core'; +import { + DualListSelector as DLSDeprecated, + DualListSelectorProps as DLSPropsDeprecated +} from '@patternfly/react-core/deprecated'; interface DualListSelectorState { availableOptions: React.ReactNode[]; chosenOptions: React.ReactNode[]; } -export class DualListSelectorBasicDemo extends Component { +class DualListSelectorDeprecatedBasicDemo extends Component { static displayName = 'DualListSelectorDemo'; onListChange: ( event: React.MouseEvent, @@ -13,7 +16,7 @@ export class DualListSelectorBasicDemo extends Component void; - constructor(props: DualListSelectorProps) { + constructor(props: DLSPropsDeprecated) { super(props); this.state = { availableOptions: ['Option 1', 'Option 2', 'Option 3', 'Option 4'], @@ -30,7 +33,7 @@ export class DualListSelectorBasicDemo extends Component { +class DualListSelectorDeprecatedTreeDemo extends Component { static displayName = 'DualListSelectorTreeDemo'; onListChange: ( event: React.MouseEvent, - newAvailableOptions: DualListSelectorTreeItemData[], - newChosenOptions: DualListSelectorTreeItemData[] + newAvailableOptions: DLSTreeItemDataDeprecated[], + newChosenOptions: DLSTreeItemDataDeprecated[] ) => void; - constructor(props: DualListSelectorProps) { + constructor(props: DLSPropsDeprecated) { super(props); this.state = { availableOptions: [ @@ -50,7 +54,7 @@ export class DualListSelectorTreeDemo extends Component { +class DualListSelectorDeprecatedWithActionsDemo extends React.Component { static displayName = 'DualListSelectorDemo'; onSort: (panel: string) => void; onListChange: ( @@ -34,7 +29,7 @@ class DualListSelectorWithActionsDemo extends React.Component boolean; onOptionSelect: (event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent) => void; - constructor(props: DualListSelectorProps) { + constructor(props: DLSPropsDeprecated) { super(props); this.state = { availableOptions: [ @@ -182,7 +177,7 @@ class DualListSelectorWithActionsDemo extends React.Component