diff --git a/packages/react-core/src/components/DualListSelector/DualListSelector.tsx b/packages/react-core/src/components/DualListSelector/DualListSelector.tsx index 99c65d6868c..a8930861cb2 100644 --- a/packages/react-core/src/components/DualListSelector/DualListSelector.tsx +++ b/packages/react-core/src/components/DualListSelector/DualListSelector.tsx @@ -190,10 +190,22 @@ export class DualListSelector extends React.Component { + if (key[0] === '_') { + return undefined; + } + return value; + }; + componentDidUpdate() { if ( - JSON.stringify(this.props.availableOptions) !== JSON.stringify(this.state.availableOptions) || - JSON.stringify(this.props.chosenOptions) !== JSON.stringify(this.state.chosenOptions) + 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], diff --git a/packages/react-core/src/components/DualListSelector/examples/DualListSelector.md b/packages/react-core/src/components/DualListSelector/examples/DualListSelector.md index 5fd83020a46..bf9efe384f6 100644 --- a/packages/react-core/src/components/DualListSelector/examples/DualListSelector.md +++ b/packages/react-core/src/components/DualListSelector/examples/DualListSelector.md @@ -2,14 +2,15 @@ id: Dual list selector section: components cssPrefix: 'pf-c-dual-list-selector' -propComponents: [ - 'DualListSelector', - 'DualListSelectorPane', - 'DualListSelectorControl', - 'DualListSelectorControlsWrapper', - 'DualListSelectorTree', - 'DualListSelectorTreeItemData' -] +propComponents: + [ + 'DualListSelector', + 'DualListSelectorPane', + 'DualListSelectorControl', + 'DualListSelectorControlsWrapper', + 'DualListSelectorTree', + 'DualListSelectorTreeItemData', + ] beta: true --- @@ -23,806 +24,76 @@ import PficonSortCommonAscIcon from '@patternfly/react-icons/dist/esm/icons/pfic ### Basic -```js -import React from 'react'; -import { DualListSelector } from '@patternfly/react-core'; - -class BasicDualListSelector extends React.Component { - constructor(props) { - super(props); - this.state = { - availableOptions: ['Option 1', 'Option 2', 'Option 3', 'Option 4'], - chosenOptions: [] - }; - - this.onListChange = (newAvailableOptions, newChosenOptions) => { - this.setState({ - availableOptions: newAvailableOptions.sort(), - chosenOptions: newChosenOptions.sort() - }); - }; - } - - render() { - return ( - - ); - } -} +```ts file="./DualListSelectorBasic.tsx" ``` ### Basic with tooltips -```js -import React from 'react'; -import { DualListSelector } from '@patternfly/react-core'; - -class BasicDualListSelector extends React.Component { - constructor(props) { - super(props); - this.state = { - availableOptions: ['Option 1', 'Option 2', 'Option 3', 'Option 4'], - chosenOptions: [] - }; - - this.onListChange = (newAvailableOptions, newChosenOptions) => { - this.setState({ - availableOptions: newAvailableOptions.sort(), - chosenOptions: newChosenOptions.sort() - }); - }; - } - - render() { - return ( - - ); - } -} +```ts file="./DualListSelectorBasicTooltips.tsx" ``` ### Basic with search -```js -import React from 'react'; -import { DualListSelector } from '@patternfly/react-core'; - -class BasicDualListSelectorWithSearch extends React.Component { - constructor(props) { - super(props); - this.state = { - availableOptions: ['Option 1', 'Option 2', 'Option 3', 'Option 4'], - chosenOptions: [] - }; - - this.onListChange = (newAvailableOptions, newChosenOptions) => { - this.setState({ - availableOptions: newAvailableOptions.sort(), - chosenOptions: newChosenOptions.sort() - }); - }; - } - - render() { - return ( - - ); - } -} +```ts file="./DualListSelectorBasicSearch.tsx" ``` ### Using more complex options with actions -```js -import React from 'react'; -import { Button, ButtonVariant, Checkbox, Dropdown, DropdownItem, DualListSelector, KebabToggle } from '@patternfly/react-core'; -import PficonSortCommonAscIcon from '@patternfly/react-icons/dist/esm/icons/pficon-sort-common-asc-icon'; - -class ComplexDualListSelector extends React.Component { - constructor(props) { - super(props); - this.state = { - availableOptions: [Option 1, Option 3, Option 4, Option 2], - chosenOptions: [], - isAvailableKebabOpen: false, - isChosenKebabOpen: false, - isDisabled: false - }; - - this.onSort = panel => { - if (panel === 'available') { - this.setState(prevState => { - const available = prevState.availableOptions.sort((a, b) => { - let returnValue = 0; - if (a.props.children > b.props.children) returnValue = 1; - if (a.props.children < b.props.children) returnValue = -1; - return returnValue; - }); - return { - availableOptions: available - }; - }); - } - - if (panel === 'chosen') { - this.setState(prevState => { - const chosen = prevState.chosenOptions.sort((a, b) => { - let returnValue = 0; - if (a.props.children > b.props.children) returnValue = 1; - if (a.props.children < b.props.children) returnValue = -1; - return returnValue; - }); - return { - chosenOptions: chosen - }; - }); - } - }; - - this.onListChange = (newAvailableOptions, newChosenOptions) => { - this.setState({ - availableOptions: newAvailableOptions, - chosenOptions: newChosenOptions - }); - }; - - this.onToggle = (isOpen, pane) => { - this.setState(prevState => { - return { - isAvailableKebabOpen: pane === 'available' ? isOpen : prevState.isAvailableKebabOpen, - isChosenKebabOpen: pane === 'chosen' ? isOpen : prevState.isChosenKebabOpen - }; - }); - }; - - this.filterOption = (option, input) => { - return option.props.children.includes(input); - }; - } - - render() { - const dropdownItems = [ - Link, - - Action - , - - Second Action - - ]; - - const availableOptionsActions = [ - , - this.onToggle(isOpen, 'available')} id="toggle-id-1" />} - isOpen={this.state.isAvailableKebabOpen} - isPlain - dropdownItems={dropdownItems} - key="availableDropdown" - /> - ]; - - const chosenOptionsActions = [ - , - this.onToggle(isOpen, 'chosen')} id="toggle-id-2" />} - isOpen={this.state.isChosenKebabOpen} - isPlain - dropdownItems={dropdownItems} - key="chosenDropdown" - /> - ]; - - return ( - - - - this.setState({ - isDisabled: !this.state.isDisabled - }) - } - /> - - ); - } -} +```ts file="./DualListSelectorComplexOptionsActions.tsx" ``` -### Expandable options - -```js -import React from 'react'; -import { DualListSelector } from '@patternfly/react-core'; +### With tree -class TreeDualListSelector extends React.Component { - constructor(props) { - super(props); - this.state = { - chosenOptions: [ - { - id: 'CF1', - text: 'Chosen Folder 1', - isChecked: false, - checkProps: { 'aria-label': 'Chosen Folder 1' }, - hasBadge: true, - badgeProps: { isRead: true }, - children: [ - { id: 'CO1', text: 'Chosen Option 1', isChecked: false, checkProps: { 'aria-label': 'Chosen Option 1' } }, - { - id: 'CF1A', - text: 'Chosen Folder 1A', - isChecked: false, - checkProps: { 'aria-label': 'Chosen Folder 1A' }, - children: [ - { - id: 'CO2', - text: 'Chosen Option 2', - isChecked: false, - checkProps: { 'aria-label': 'Chosen Option 2' } - }, - { - id: 'CO3', - text: 'Chosen Option 3', - isChecked: false, - checkProps: { 'aria-label': 'Chosen Option 3' } - } - ] - }, - { id: 'CO4', text: 'Chosen Option 4', isChecked: false, checkProps: { 'aria-label': 'Chosen Option 4' } } - ] - } - ], - availableOptions: [ - { - id: 'F1', - text: 'Folder 1', - isChecked: false, - checkProps: { 'aria-label': 'Folder 1' }, - hasBadge: true, - badgeProps: { isRead: true }, - children: [ - { id: 'O1', text: 'Option 1', isChecked: false, checkProps: { 'aria-label': 'Option 1' } }, - { - id: 'F1A', - text: 'Folder 1A', - isChecked: false, - checkProps: { 'aria-label': 'Folder 1A' }, - children: [ - { id: 'O2', text: 'Option 2', isChecked: false, checkProps: { 'aria-label': 'Option 2' } }, - { id: 'O3', text: 'Option 3', isChecked: false, checkProps: { 'aria-label': 'Option 3' } } - ] - }, - { id: 'O4', text: 'Option 4', isChecked: false, checkProps: { 'aria-label': 'Option 4' } } - ] - }, - { id: 'O5', text: 'Option 5', isChecked: false, checkProps: { 'aria-label': 'Option 5' } }, - { - id: 'F2', - text: 'Folder 2', - isChecked: false, - checkProps: { 'aria-label': 'Folder 2' }, - children: [ - { id: 'O6', text: 'Option 6', isChecked: false, checkProps: { 'aria-label': 'Option 6' } }, - { id: 'O7', text: 'Option 5', isChecked: false, checkProps: { 'aria-label': 'Option 5 duplicate' } } - ] - } - ] - }; - - this.onListChange = (newAvailableOptions, newChosenOptions) => { - this.setState({ - availableOptions: newAvailableOptions, - chosenOptions: newChosenOptions - }); - }; - } - - render() { - return ( - - ); - } -} +```ts file="./DualListSelectorTree.tsx" ``` ### Composable dual list selector -For more flexibility, a Dual list selector can be built using sub components. When doing so, the intended component -relationships are arranged as follows: - -```js noLive -import React from 'react'; -import { DualListSelector, DualListSelectorPane, DualListSelectorList, DualListSelectorListItem, DualListSelectorControlsWrapper, DualListSelectorControl } from '@patternfly/react-core'; +For more flexibility, a dual list selector can be built using sub components. When doing so, the intended component relationships are arranged as follows: +```noLive - - + - + - {/* The standard Dual list selector has 4 controls */} + /* The standard Dual list selector has 4 controls */ - + - + - ``` -Note: Keyboard accessibility and screen reader accessibility for the `DragDrop` component are still in development. - -```js -import React from 'react'; -import { - Button, - ButtonVariant, - DualListSelector, - DualListSelectorPane, - DualListSelectorList, - DualListSelectorListItem, - DualListSelectorControlsWrapper, - DualListSelectorControl, - SearchInput -} 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'; - -const ComposableDualListSelector = () => { - const [availableOptions, setAvailableOptions] = React.useState([ - { text: 'Apple', selected: false, isVisible: true }, - { text: 'Banana', selected: false, isVisible: true }, - { text: 'Pineapple', selected: false, isVisible: true }, - { text: 'Orange', selected: false, isVisible: true }, - { text: 'Grape', selected: false, isVisible: true }, - { text: 'Peach', selected: false, isVisible: true }, - { text: 'Strawberry', 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) => { - 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) => { - if (fromAvailable) { - setChosenOptions([...availableOptions.filter(x => x.isVisible), ...chosenOptions]); - setAvailableOptions([...availableOptions.filter(x => !x.isVisible)]); - } else { - setAvailableOptions([...chosenOptions.filter(x => x.isVisible), ...availableOptions]); - setChosenOptions([...chosenOptions.filter(x => !x.isVisible)]); - } - }; - - // callback when option is selected - const onOptionSelect = (event, index, isChosen) => { - 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); - } - }; - - // builds a search input - used in each dual list selector pane - const buildSearchInput = (isAvailable) => { - const onChange = (value) => { - isAvailable ? setAvailableFilter(value) : setChosenFilter(value); - const toFilter = isAvailable ? [...availableOptions] : [...chosenOptions]; - toFilter.forEach((option) => { - option.isVisible = value === '' || option.text.toLowerCase().includes(value.toLowerCase()); - }) - }; - - return ( - onChange('')} - /> - ); - }; - - // builds a sort control - passed to both dual list selector panes - const buildSort = (isAvailable) => { - 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); - } - }; - - return ( - - ); - }; - - return ( - - x.selected && x.isVisible).length} of ${availableOptions.filter(x => x.isVisible).length} options selected`} - searchInput={buildSearchInput(true)} - actions={[buildSort(true)]} - > - - {availableOptions.map((option, index) => { - return 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" - > - - - - x.selected && x.isVisible).length} of ${chosenOptions.filter(x => x.isVisible).length} options selected`} - searchInput={buildSearchInput(false)} - actions={[buildSort(false)]} - isChosen - > - - {chosenOptions.map((option, index) => { - return option.isVisible ? ( - onOptionSelect(e, index, true)} - > - {option.text} - - ) : null; - })} - - - - ); -} +```ts file="./DualListSelectorComposable.tsx" ``` -### Reordering lists using drag and drop +### Composable dual list selector with 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: -To make a pane able to be reordered: - - wrap the `DualListSelectorPane` in a `DragDrop` component - - wrap the `DualListSelectorList` in a `Droppable` component - - wrap the `DualListSelectorListItem` components in a `Draggable` component - - define an `onDrop` callback which reorders the sortable options. - - The `onDrop` function provides the starting location and destination location for a dragged item. It should return - true to enable the 'drop' animation in the new location and false to enable the 'drop' animation back to the item's +- wrap the `DualListSelectorPane` in a `DragDrop` component +- wrap the `DualListSelectorList` in a `Droppable` component +- wrap the `DualListSelectorListItem` components in a `Draggable` component +- define an `onDrop` callback which reorders the sortable options. + - The `onDrop` function provides the starting location and destination location for a dragged item. It should return + true to enable the 'drop' animation in the new location and false to enable the 'drop' animation back to the item's old position. - - define an `onDrag` callback which ensures that the drag event will not cross hairs with the `onOptionSelect` click + - 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. -```js -import React from 'react'; -import { - DragDrop, - Droppable, - Draggable, - 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'; - -const ComposableDualListSelector = () => { - const [ignoreNextOptionSelect, setIgnoreNextOptionSelect] = React.useState(false); - const [availableOptions, setAvailableOptions] = React.useState([ - { text: 'Apple', selected: false, isVisible: true }, - { text: 'Banana', selected: false, isVisible: true }, - { text: 'Pineapple', selected: false, isVisible: true } - ]); - const [chosenOptions, setChosenOptions] = React.useState([ - { text: 'Orange', selected: false, isVisible: true }, - { text: 'Grape', selected: false, isVisible: true }, - { text: 'Peach', selected: false, isVisible: true }, - { text: 'Strawberry', selected: false, isVisible: true } - ]); - - const moveSelected = (fromAvailable) => { - 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]); - } - }; - - const moveAll = (fromAvailable) => { - if (fromAvailable) { - setChosenOptions([...availableOptions.filter(x => x.isVisible), ...chosenOptions]); - setAvailableOptions([...availableOptions.filter(x => !x.isVisible)]); - } else { - setAvailableOptions([...chosenOptions.filter(x => x.isVisible), ...availableOptions]); - setChosenOptions([...chosenOptions.filter(x => !x.isVisible)]); - } - }; - - const onOptionSelect = (event, index, isChosen) => { - if (ignoreNextOptionSelect) { - setIgnoreNextOptionSelect(false); - return; - } - 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 onDrop = (source, dest) => { - if (dest){ - const newList = [...chosenOptions]; - const [removed] = newList.splice(source.index, 1); - newList.splice(dest.index, 0, removed); - setChosenOptions(newList); - return true; - } - return false; - }; +Note: Keyboard accessibility and screen reader accessibility for the `DragDrop` component are still in development. - return ( - - x.selected && x.isVisible).length} of ${availableOptions.filter(x => x.isVisible).length} options selected`} - > - - {availableOptions.map((option, index) => { - return 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); return true; }} onDrop={onDrop}> - x.selected && x.isVisible).length} of ${chosenOptions.filter(x => x.isVisible).length} options selected`} - isChosen - > - - - {chosenOptions.map((option, index) => { - return option.isVisible ? ( - - onOptionSelect(e, index, true)} - isDraggable - > - {option.text} - - - ) : null; - })} - - - - - - ); -} +```ts file="DualListSelectorComposableDragDrop.tsx" ``` -### Composable dual list selector tree -```ts file="ComposableTree.tsx" -``` +### Composable dual list selector with tree +```ts file="DualListSelectorComposableTree.tsx" +``` diff --git a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasic.tsx b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasic.tsx new file mode 100644 index 00000000000..6795babc22e --- /dev/null +++ b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasic.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { DualListSelector } from '@patternfly/react-core'; + +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 = (newAvailableOptions: React.ReactNode[], newChosenOptions: React.ReactNode[]) => { + setAvailableOptions(newAvailableOptions.sort()); + setChosenOptions(newChosenOptions.sort()); + }; + + return ( + + ); +}; diff --git a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasicSearch.tsx b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasicSearch.tsx new file mode 100644 index 00000000000..c84f84c710d --- /dev/null +++ b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasicSearch.tsx @@ -0,0 +1,27 @@ +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' + ]); + const [chosenOptions, setChosenOptions] = React.useState([]); + + const onListChange = (newAvailableOptions: React.ReactNode[], newChosenOptions: React.ReactNode[]) => { + setAvailableOptions(newAvailableOptions.sort()); + setChosenOptions(newChosenOptions.sort()); + }; + + return ( + + ); +}; diff --git a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasicTooltips.tsx b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasicTooltips.tsx new file mode 100644 index 00000000000..e202656a956 --- /dev/null +++ b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorBasicTooltips.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { DualListSelector } from '@patternfly/react-core'; + +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 = (newAvailableOptions: React.ReactNode[], newChosenOptions: React.ReactNode[]) => { + setAvailableOptions(newAvailableOptions.sort()); + setChosenOptions(newChosenOptions.sort()); + }; + + return ( + + ); +}; diff --git a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComplexOptionsActions.tsx b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComplexOptionsActions.tsx new file mode 100644 index 00000000000..10b0b6a18e8 --- /dev/null +++ b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComplexOptionsActions.tsx @@ -0,0 +1,146 @@ +import React from 'react'; +import { + Button, + ButtonVariant, + Checkbox, + Dropdown, + DropdownItem, + DualListSelector, + KebabToggle +} from '@patternfly/react-core'; +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 [isAvailableKebabOpen, setIsAvailableKebabOpen] = React.useState(false); + const [isChosenKebabOpen, setIsChosenKebabOpen] = React.useState(false); + 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 onToggle = (isOpen: boolean, pane: string) => { + if (pane === 'available') { + setIsAvailableKebabOpen(isOpen); + } else { + setIsChosenKebabOpen(isOpen); + } + }; + + const filterOption = (option: React.ReactNode, input: string) => + (option as React.ReactElement).props.children.includes(input); + + const dropdownItems = [ + Link, + + Action + , + + Second Action + + ]; + + const availableOptionsActions = [ + , + onToggle(isOpen, 'available')} + id="complex-available-toggle" + /> + } + isOpen={isAvailableKebabOpen} + isPlain + dropdownItems={dropdownItems} + key="availableDropdown" + /> + ]; + + const chosenOptionsActions = [ + , + onToggle(isOpen, 'chosen')} + id="complex-chosen-toggle" + /> + } + isOpen={isChosenKebabOpen} + isPlain + dropdownItems={dropdownItems} + key="chosenDropdown" + /> + ]; + + return ( + + + setIsDisabled(!isDisabled)} + /> + + ); +}; diff --git a/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposable.tsx b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposable.tsx new file mode 100644 index 00000000000..59d7de18c57 --- /dev/null +++ b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposable.tsx @@ -0,0 +1,214 @@ +import React from 'react'; +import { + Button, + ButtonVariant, + DualListSelector, + DualListSelectorPane, + DualListSelectorList, + DualListSelectorListItem, + DualListSelectorControlsWrapper, + DualListSelectorControl, + SearchInput +} 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'; + +export const DualListSelectorComposable: React.FunctionComponent = () => { + const [availableOptions, setAvailableOptions] = React.useState([ + { text: 'Apple', selected: false, isVisible: true }, + { text: 'Banana', selected: false, isVisible: true }, + { text: 'Pineapple', selected: false, isVisible: true }, + { text: 'Orange', selected: false, isVisible: true }, + { text: 'Grape', selected: false, isVisible: true }, + { text: 'Peach', selected: false, isVisible: true }, + { text: 'Strawberry', 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); + } + }; + + // builds a search input - used in each dual list selector pane + const buildSearchInput = (isAvailable: boolean) => { + const onChange = (value: string) => { + isAvailable ? setAvailableFilter(value) : setChosenFilter(value); + const toFilter = isAvailable ? [...availableOptions] : [...chosenOptions]; + toFilter.forEach(option => { + option.isVisible = value === '' || option.text.toLowerCase().includes(value.toLowerCase()); + }); + }; + + return ( + onChange('')} + /> + ); + }; + + // 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); + } + }; + + return ( + + ); + }; + + return ( + + option.selected && option.isVisible).length} of ${ + availableOptions.filter(option => option.isVisible).length + } options selected`} + searchInput={buildSearchInput(true)} + actions={[buildSort(true)]} + > + + {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 + } options selected`} + searchInput={buildSearchInput(false)} + actions={[buildSort(false)]} + isChosen + > + + {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/components/DualListSelector/examples/DualListSelectorComposableDragDrop.tsx new file mode 100644 index 00000000000..6d251291dbf --- /dev/null +++ b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposableDragDrop.tsx @@ -0,0 +1,190 @@ +import React from 'react'; +import { + DragDrop, + Droppable, + Draggable, + 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 SourceType { + droppableId: string; + index: number; +} + +interface DestinationType extends SourceType {} + +export const DualListSelectorComposableDragDrop: React.FunctionComponent = () => { + const [ignoreNextOptionSelect, setIgnoreNextOptionSelect] = React.useState(false); + const [availableOptions, setAvailableOptions] = React.useState([ + { text: 'Apple', selected: false, isVisible: true }, + { text: 'Banana', selected: false, isVisible: true }, + { text: 'Pineapple', selected: false, isVisible: true } + ]); + const [chosenOptions, setChosenOptions] = React.useState([ + { text: 'Orange', selected: false, isVisible: true }, + { text: 'Grape', selected: false, isVisible: true }, + { text: 'Peach', selected: false, isVisible: true }, + { text: 'Strawberry', selected: false, isVisible: true } + ]); + + 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]); + } + }; + + 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 onOptionSelect = ( + event: React.MouseEvent | React.ChangeEvent | React.KeyboardEvent, + index: number, + isChosen: boolean + ) => { + if (ignoreNextOptionSelect) { + setIgnoreNextOptionSelect(false); + return; + } + 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 onDrop = (source: SourceType, dest: DestinationType) => { + if (dest) { + const newList = [...chosenOptions]; + const [removed] = newList.splice(source.index, 1); + newList.splice(dest.index, 0, removed); + setChosenOptions(newList); + return true; + } + return false; + }; + + 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); + return true; + }} + onDrop={onDrop} + > + 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)} + isDraggable + > + {option.text} + + + ) : null + )} + + + + + + ); +}; diff --git a/packages/react-core/src/components/DualListSelector/examples/ComposableTree.tsx b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposableTree.tsx similarity index 98% rename from packages/react-core/src/components/DualListSelector/examples/ComposableTree.tsx rename to packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposableTree.tsx index 1622059291b..45248c2c1bd 100644 --- a/packages/react-core/src/components/DualListSelector/examples/ComposableTree.tsx +++ b/packages/react-core/src/components/DualListSelector/examples/DualListSelectorComposableTree.tsx @@ -24,7 +24,7 @@ interface ExampleProps { data: FoodNode[]; } -export const ComposableDualListSelectorTree: React.FunctionComponent = ({ data }: ExampleProps) => { +export const DualListSelectorComposableTree: React.FunctionComponent = ({ data }: ExampleProps) => { const [checkedLeafIds, setCheckedLeafIds] = React.useState([]); const [chosenLeafIds, setChosenLeafIds] = React.useState(['beans', 'beef', 'chicken', 'tofu']); const [chosenFilter, setChosenFilter] = React.useState(''); @@ -290,8 +290,8 @@ export const ComposableDualListSelectorTree: React.FunctionComponent ( - ( + { + const [availableOptions, setAvailableOptions] = React.useState([ + { + id: 'F1', + text: 'Folder 1', + isChecked: false, + checkProps: { 'aria-label': 'Folder 1' }, + hasBadge: true, + badgeProps: { isRead: true }, + children: [ + { id: 'O1', text: 'Option 1', isChecked: false, checkProps: { 'aria-label': 'Option 1' } }, + { + id: 'F1A', + text: 'Folder 1A', + isChecked: false, + checkProps: { 'aria-label': 'Folder 1A' }, + children: [ + { id: 'O2', text: 'Option 2', isChecked: false, checkProps: { 'aria-label': 'Option 2' } }, + { id: 'O3', text: 'Option 3', isChecked: false, checkProps: { 'aria-label': 'Option 3' } } + ] + }, + { id: 'O4', text: 'Option 4', isChecked: false, checkProps: { 'aria-label': 'Option 4' } } + ] + }, + { id: 'O5', text: 'Option 5', isChecked: false, checkProps: { 'aria-label': 'Option 5' } }, + { + id: 'F2', + text: 'Folder 2', + isChecked: false, + checkProps: { 'aria-label': 'Folder 2' }, + children: [ + { id: 'O6', text: 'Option 6', isChecked: false, checkProps: { 'aria-label': 'Option 6' } }, + { id: 'O7', text: 'Option 5', isChecked: false, checkProps: { 'aria-label': 'Option 5 duplicate' } } + ] + } + ]); + + const [chosenOptions, setChosenOptions] = React.useState([ + { + id: 'CF1', + text: 'Chosen Folder 1', + isChecked: false, + checkProps: { 'aria-label': 'Chosen Folder 1' }, + hasBadge: true, + badgeProps: { isRead: true }, + children: [ + { id: 'CO1', text: 'Chosen Option 1', isChecked: false, checkProps: { 'aria-label': 'Chosen Option 1' } }, + { + id: 'CF1A', + text: 'Chosen Folder 1A', + isChecked: false, + checkProps: { 'aria-label': 'Chosen Folder 1A' }, + children: [ + { + id: 'CO2', + text: 'Chosen Option 2', + isChecked: false, + checkProps: { 'aria-label': 'Chosen Option 2' } + }, + { + id: 'CO3', + text: 'Chosen Option 3', + isChecked: false, + checkProps: { 'aria-label': 'Chosen Option 3' } + } + ] + }, + { id: 'CO4', text: 'Chosen Option 4', isChecked: false, checkProps: { 'aria-label': 'Chosen Option 4' } } + ] + } + ]); + + const onListChange = (newAvailableOptions: React.ReactNode[], newChosenOptions: React.ReactNode[]) => { + setAvailableOptions(newAvailableOptions.sort()); + setChosenOptions(newChosenOptions.sort()); + }; + + return ( + + ); +};