From 2a51e79f147b0355531dae8a2d1c6c3cc1dfd74f Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Tue, 28 Jul 2020 15:09:30 +0200 Subject: [PATCH 01/10] feat(transfer): add onSourceEndReached & onPickedEndReached callbacks Fixes TECH-378 --- .../src/Transfer/EndIntersectionDetector.js | 30 ++ .../widgets/src/Transfer/OptionsContainer.js | 109 ++++++ .../widgets/src/Transfer/PickedOptions.js | 30 +- .../widgets/src/Transfer/SourceOptions.js | 30 +- packages/widgets/src/Transfer/Transfer.js | 97 ++--- .../widgets/src/Transfer/Transfer.stories.js | 348 ++++++++++-------- 6 files changed, 415 insertions(+), 229 deletions(-) create mode 100644 packages/widgets/src/Transfer/EndIntersectionDetector.js create mode 100644 packages/widgets/src/Transfer/OptionsContainer.js diff --git a/packages/widgets/src/Transfer/EndIntersectionDetector.js b/packages/widgets/src/Transfer/EndIntersectionDetector.js new file mode 100644 index 0000000000..e8bc9aeea6 --- /dev/null +++ b/packages/widgets/src/Transfer/EndIntersectionDetector.js @@ -0,0 +1,30 @@ +import { IntersectionDetector } from '@dhis2/ui-core' +import propTypes from '@dhis2/prop-types' +import React from 'react' + +export const EndIntersectionDetector = ({ rootRef, onEndReached }) => ( +
+ isIntersecting && onEndReached()} + /> + + +
+) + +EndIntersectionDetector.propTypes = { + rootRef: propTypes.shape({ + current: propTypes.instanceOf(HTMLElement), + }).isRequired, + onEndReached: propTypes.func.isRequired, +} diff --git a/packages/widgets/src/Transfer/OptionsContainer.js b/packages/widgets/src/Transfer/OptionsContainer.js new file mode 100644 index 0000000000..ee1c19bf47 --- /dev/null +++ b/packages/widgets/src/Transfer/OptionsContainer.js @@ -0,0 +1,109 @@ +import { spacers } from '@dhis2/ui-constants' +import React, { Fragment, useEffect, useRef, useState } from 'react' +import propTypes from '@dhis2/prop-types' + +import { EndIntersectionDetector } from './EndIntersectionDetector.js' + +export const OptionsContainer = ({ + dataTest, + emptyComponent, + onEndReached, + getOptionClickHandlers, + highlightedOptions, + renderOption, + options, + selectionHandler, + toggleHighlightedOption, +}) => { + const [remountCounter, setRemountCounter] = useState(0) + const [resizeObserver, setResizeObserver] = useState(null) + const optionsRef = useRef(null) + const wrapperRef = useRef(null) + + useEffect(() => { + if (onEndReached && wrapperRef.current) { + // The initial call is irrelevant as there has been + // no resize yet that we want to react to + let firstCall = false + + const observer = new ResizeObserver(() => { + if (!firstCall) { + const newCounter = remountCounter + 1 + setRemountCounter(newCounter) + firstCall = true + } + }) + + observer.observe(wrapperRef.current) + setResizeObserver(observer) + + return () => observer.disconnect() + } + }, [onEndReached, wrapperRef.current, setRemountCounter]) + + return ( +
+
+ {!options.length && emptyComponent} + {options.map(option => { + const highlighted = !!highlightedOptions.find( + highlightedSourceOption => + highlightedSourceOption === option.value + ) + + return ( + + {renderOption({ + ...option, + ...getOptionClickHandlers( + option, + selectionHandler, + toggleHighlightedOption + ), + highlighted, + selected: false, + })} + + ) + })} + + {onEndReached && resizeObserver && ( + + )} +
+ + +
+ ) +} + +OptionsContainer.propTypes = { + dataTest: propTypes.string.isRequired, + getOptionClickHandlers: propTypes.func.isRequired, + emptyComponent: propTypes.node, + highlightedOptions: propTypes.arrayOf(propTypes.string), + options: propTypes.arrayOf( + propTypes.shape({ + label: propTypes.string.isRequired, + value: propTypes.string.isRequired, + }) + ), + renderOption: propTypes.func, + selectionHandler: propTypes.func, + toggleHighlightedOption: propTypes.func, + onEndReached: propTypes.func, +} diff --git a/packages/widgets/src/Transfer/PickedOptions.js b/packages/widgets/src/Transfer/PickedOptions.js index 78a41a906d..8e585059b1 100644 --- a/packages/widgets/src/Transfer/PickedOptions.js +++ b/packages/widgets/src/Transfer/PickedOptions.js @@ -1,23 +1,39 @@ +import { spacers } from '@dhis2/ui-constants' import React from 'react' import propTypes from '@dhis2/prop-types' -import { spacers } from '@dhis2/ui-constants' +import { EndIntersectionDetector } from './EndIntersectionDetector.js' export const PickedOptions = ({ children, dataTest, selectedEmptyComponent, + pickedOptionsRef, + onPickedEndReached, }) => ( -
- {!React.Children.count(children) && selectedEmptyComponent} - {children} +
+
+ {!React.Children.count(children) && selectedEmptyComponent} + {children} + + {onPickedEndReached && ( + + )} +
) @@ -25,5 +41,9 @@ export const PickedOptions = ({ PickedOptions.propTypes = { children: propTypes.node.isRequired, dataTest: propTypes.string.isRequired, + pickedOptionsRef: propTypes.shape({ + current: propTypes.instanceOf(HTMLElement), + }), selectedEmptyComponent: propTypes.node, + onPickedEndReached: propTypes.func, } diff --git a/packages/widgets/src/Transfer/SourceOptions.js b/packages/widgets/src/Transfer/SourceOptions.js index bfdd24e7fd..38bb236299 100644 --- a/packages/widgets/src/Transfer/SourceOptions.js +++ b/packages/widgets/src/Transfer/SourceOptions.js @@ -1,23 +1,39 @@ +import { spacers } from '@dhis2/ui-constants' import React from 'react' import propTypes from '@dhis2/prop-types' -import { spacers } from '@dhis2/ui-constants' +import { EndIntersectionDetector } from './EndIntersectionDetector.js' export const SourceOptions = ({ children, dataTest, sourceEmptyPlaceholder, + sourceOptionsRef, + onSourceEndReached, }) => ( -
- {children} - {!React.Children.count(children) && sourceEmptyPlaceholder} +
+
+ {children} + {!React.Children.count(children) && sourceEmptyPlaceholder} + + {onSourceEndReached && ( + + )} +
) @@ -26,4 +42,8 @@ SourceOptions.propTypes = { dataTest: propTypes.string.isRequired, children: propTypes.node, sourceEmptyPlaceholder: propTypes.node, + sourceOptionsRef: propTypes.shape({ + current: propTypes.instanceOf(HTMLElement), + }), + onSourceEndReached: propTypes.func, } diff --git a/packages/widgets/src/Transfer/Transfer.js b/packages/widgets/src/Transfer/Transfer.js index c4467cfb1b..c58d1dcf1b 100644 --- a/packages/widgets/src/Transfer/Transfer.js +++ b/packages/widgets/src/Transfer/Transfer.js @@ -1,4 +1,4 @@ -import React, { Fragment, useState } from 'react' +import React, { useState } from 'react' import propTypes from '@dhis2/prop-types' import { Actions } from './Actions.js' @@ -9,14 +9,13 @@ import { Filter } from './Filter.js' import { LeftFooter } from './LeftFooter.js' import { LeftHeader } from './LeftHeader.js' import { LeftSide } from './LeftSide.js' -import { PickedOptions } from './PickedOptions.js' +import { OptionsContainer } from './OptionsContainer.js' import { RemoveAll } from './RemoveAll.js' import { RemoveIndividual } from './RemoveIndividual.js' import { ReorderingActions } from './ReorderingActions.js' import { RightHeader } from './RightHeader.js' import { RightFooter } from './RightFooter.js' import { RightSide } from './RightSide.js' -import { SourceOptions } from './SourceOptions.js' import { TransferOption } from './TransferOption.js' import { addAllSelectableSourceOptions, @@ -68,34 +67,36 @@ export const Transfer = ({ options, onChange, + addAllText, + addIndividualText, className, dataTest, disabled, - sourceEmptyPlaceholder, - selectedEmptyComponent, enableOrderChange, + filterCallback, filterLabel, filterPlaceholder, - filterCallback, filterable, height, + hideFilterInput, initialSearchTerm, - addAllText, - addIndividualText, - removeAllText, - removeIndividualText, leftFooter, leftHeader, maxSelections, optionsWidth, + removeAllText, + removeIndividualText, renderOption, - rightHeader, rightFooter, + rightHeader, searchTerm, selected, + selectedEmptyComponent, selectedWidth, - hideFilterInput, + sourceEmptyPlaceholder, onFilterChange, + onSourceEndReached, + onPickedEndReached, }) => { /* * Used in the "Filter" section and for @@ -202,32 +203,17 @@ export const Transfer = ({ )} - - {sourceOptions.map(option => { - const highlighted = !!highlightedSourceOptions.find( - highlightedSourceOption => - highlightedSourceOption === option.value - ) - - return ( - - {renderOption({ - ...option, - ...getOptionClickHandlers( - option, - selectSingleOption, - toggleHighlightedSourceOption - ), - highlighted, - selected: false, - })} - - ) - })} - + getOptionClickHandlers={getOptionClickHandlers} + emptyPlaceholder={sourceEmptyPlaceholder} + highlightedOptions={highlightedSourceOptions} + options={sourceOptions} + renderOption={renderOption} + selectionHandler={selectSingleOption} + toggleHighlightedOption={toggleHighlightedSourceOption} + onEndReached={onSourceEndReached} + /> {leftFooter && ( @@ -305,31 +291,18 @@ export const Transfer = ({ {rightHeader} )} - - {pickedOptions.map(option => { - const highlighted = !!highlightedPickedOptions.find( - value => option.value === value - ) - return ( - - {renderOption({ - ...option, - ...getOptionClickHandlers( - option, - deselectSingleOption, - toggleHighlightedPickedOption - ), - highlighted, - selected: true, - })} - - ) - })} - + {(rightFooter || enableOrderChange) && ( @@ -454,4 +427,6 @@ Transfer.propTypes = { selectedWidth: propTypes.string, sourceEmptyPlaceholder: propTypes.node, onFilterChange: propTypes.func, + onPickedEndReached: propTypes.func, + onSourceEndReached: propTypes.func, } diff --git a/packages/widgets/src/Transfer/Transfer.stories.js b/packages/widgets/src/Transfer/Transfer.stories.js index 69b9e8cc08..76adcb1ed6 100644 --- a/packages/widgets/src/Transfer/Transfer.stories.js +++ b/packages/widgets/src/Transfer/Transfer.stories.js @@ -1,25 +1,18 @@ /* eslint-disable react/prop-types */ import { SingleSelectOption, Tab, TabBar } from '@dhis2/ui-core' -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { SingleSelectField, Transfer, TransferOption } from '../index.js' -export default { title: 'Transfer' } +const statefulDecorator = ({ initialState = [] } = {}) => fn => + React.createElement(() => { + const [selected, setSelected] = useState(initialState) -const StatefulWrapper = ({ children, initialState }) => { - const [selected, setSelected] = useState(initialState) - - return React.Children.map(children, child => - React.cloneElement(child, { + return fn({ selected, - onChange: ({ selected }) => setSelected(selected), + onChange: payload => setSelected(payload.selected), }) - ) -} - -StatefulWrapper.defaultProps = { - initialState: [], -} + }) const options = [ { @@ -114,80 +107,76 @@ const options = [ }, ] -export const SingleSelection = () => ( - - console.log('Will be overriden')} - options={options} - /> - +export default { title: 'Transfer', decorators: [statefulDecorator()] } + +export const SingleSelection = ({ selected, onChange }) => ( + ) -export const Multiple = () => ( - - console.log('Will be overriden')} - options={options.slice(0, 3)} - /> - +export const Multiple = ({ selected, onChange }) => ( + ) -export const Header = () => ( - - console.log('Will be overriden')} - leftHeader={

Header on the left side

} - rightHeader={

Header on the right side

} - options={options} - /> -
+export const Header = ({ selected, onChange }) => ( + Header on the left side} + rightHeader={

Header on the right side

} + options={options} + /> ) -export const OptionsFooter = () => ( - - console.log('Will be overriden')} - leftFooter={ - - Reload list - - } - options={options} - /> - +export const OptionsFooter = ({ selected, onChange }) => ( + + Reload list + + } + options={options} + /> ) -export const Filtered = () => ( - - console.log('Will be overriden by StatefulWrapper')} - initialSearchTerm="ANC" - leftHeader={

Header on the left side

} - rightHeader={

Header on the right side

} - options={options} - /> -
+export const Filtered = ({ selected, onChange }) => ( + Header on the left side} + rightHeader={

Header on the right side

} + options={options} + /> ) -export const FilterPlaceholder = () => ( - - console.log('Will be overriden by StatefulWrapper')} - options={options} - filterLabel="Filter with placeholder" - filterPlaceholder="Search" - /> - +export const FilterPlaceholder = ({ selected, onChange }) => ( + ) const renderOption = ({ label, value, onClick, highlighted, selected }) => ( @@ -202,7 +191,7 @@ const renderOption = ({ label, value, onClick, highlighted, selected }) => (

) -export const CustomListOptions = () => ( +const RenderOptionCode = () => ( <> Custom option code: @@ -218,56 +207,64 @@ export const CustomListOptions = () => (

)`}
- value)} - > - - console.log('Will be overriden by StatefulWrapper') - } - renderOption={renderOption} - options={options} - /> - ) -export const IndividualCustomOption = () => ( - - console.log('Will be overriden')} - addAllText="Add all" - addIndividualText="Add individual" - removeAllText="Remove all" - removeIndividualText="Remove individual" - renderOption={args => { - if (args.option.value === options[0].value) { - return renderOption(args) - } +export const CustomListOptions = ({ selected, onChange }) => ( + <> + - return - }} + - + ) -export const CustomButtonText = () => ( - - console.log('Will be overriden')} - addAllText="Add all" - addIndividualText="Add individual" - removeAllText="Remove all" - removeIndividualText="Remove individual" - options={options} - /> - +CustomListOptions.story = { + decorators: [ + statefulDecorator({ + initialState: options.slice(0, 2).map(({ value }) => value), + }), + ], +} + +export const IndividualCustomOption = ({ selected, onChange }) => ( + { + if (option.value === options[0].value) { + return renderOption(option) + } + + return + }} + options={options} + /> +) + +export const CustomButtonText = ({ selected, onChange }) => ( + ) -export const SourceEmptyPlaceholder = () => ( +export const SourceEmptyPlaceholder = ({ onChange }) => ( console.log('Will be overriden')} + onChange={onChange} options={[]} sourceEmptyPlaceholder={

@@ -281,9 +278,10 @@ export const SourceEmptyPlaceholder = () => ( /> ) -export const PickedEmptyComponent = () => ( +export const PickedEmptyComponent = ({ selected, onChange }) => ( console.log('Will be overriden')} + selected={selected} + onChange={onChange} selectedEmptyComponent={

You have not selected anything yet @@ -294,49 +292,50 @@ export const PickedEmptyComponent = () => ( /> ) -export const Reordering = () => ( - value)} - > - null} - options={options.slice(0, 4)} - /> - +export const Reordering = ({ selected, onChange }) => ( + ) -export const IncreasedOptionsHeight = () => ( -

- - - console.log('Will be overriden by StatefulWrapper') - } - height="400px" - leftHeader={

Header on the left side

} - rightHeader={

Header on the right side

} - options={options} - /> -
-
-) +Reordering.story = { + decorators: [ + statefulDecorator({ + initialState: options.slice(0, 4).map(({ value }) => value), + }), + ], +} -export const DifferentWidths = () => ( - +export const IncreasedOptionsHeight = ({ selected, onChange }) => ( +
console.log('Will be overriden by StatefulWrapper')} - initialSearchTerm="Ba" + onChange={onChange} + selected={selected} + height="400px" leftHeader={

Header on the left side

} rightHeader={

Header on the right side

} - optionsWidth="500px" - selectedWidth="240px" options={options} /> - +
+) + +export const DifferentWidths = ({ selected, onChange }) => ( + Header on the left side} + rightHeader={

Header on the right side

} + optionsWidth="500px" + selectedWidth="240px" + options={options} + /> ) const createCustomFilteringInHeader = hideFilterInput => { @@ -443,10 +442,12 @@ const createCustomFilteringInHeader = hideFilterInput => { } // eslint-disable-next-line react/display-name - return () => ( - - - + return ({ selected, onChange }) => ( + ) } @@ -457,3 +458,34 @@ export const CustomFilteringWithFilterInput = createCustomFilteringInHeader( export const CustomFilteringWithoutFilterInput = createCustomFilteringInHeader( true ) + +const sliceLength = 6 + +export const InifiniteLoading = ({ selected, onChange }) => { + const [optionsLength, setOptionsLength] = useState(sliceLength) + const [optionsSlice, setOptionsSlice] = useState( + options.slice(0, optionsLength) + ) + + useEffect(() => { + if (sliceLength !== optionsLength) { + setOptionsSlice(options.slice(0, optionsLength)) + } + }, [optionsLength]) + + return ( + { + const newOptionsLength = Math.min( + optionsLength + sliceLength, + options.length + ) + + setOptionsLength(newOptionsLength) + }} + /> + ) +} From 479c091652c8378476d758683dddd68dedc9ce15 Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Tue, 28 Jul 2020 15:40:59 +0200 Subject: [PATCH 02/10] feat(transfer): allow filtering the picked options --- packages/widgets/src/Transfer/Transfer.js | 60 +++++++++++++++++-- .../widgets/src/Transfer/Transfer.stories.js | 20 +++++++ .../Transfer/removeIndividualPickedOptions.js | 24 +++++++- 3 files changed, 97 insertions(+), 7 deletions(-) diff --git a/packages/widgets/src/Transfer/Transfer.js b/packages/widgets/src/Transfer/Transfer.js index c58d1dcf1b..a52b976cf6 100644 --- a/packages/widgets/src/Transfer/Transfer.js +++ b/packages/widgets/src/Transfer/Transfer.js @@ -74,12 +74,18 @@ export const Transfer = ({ disabled, enableOrderChange, filterCallback, + filterCallbackPicked, filterLabel, + filterLabelPicked, filterPlaceholder, + filterPlaceholderPicked, filterable, + filterablePicked, height, hideFilterInput, + hideFilterInputPicked, initialSearchTerm, + initialSearchTermPicked, leftFooter, leftHeader, maxSelections, @@ -90,11 +96,13 @@ export const Transfer = ({ rightFooter, rightHeader, searchTerm, + searchTermPicked, selected, selectedEmptyComponent, selectedWidth, sourceEmptyPlaceholder, onFilterChange, + onFilterPickedChange, onSourceEndReached, onPickedEndReached, }) => { @@ -109,6 +117,16 @@ export const Transfer = ({ const actualFilter = onFilterChange ? searchTerm : internalFilter const actualFilterCallback = filterable ? filterCallback : identity + const [internalFilterPicked, setInternalFilterPicked] = useState( + initialSearchTermPicked + ) + const actualFilterPicked = onFilterPickedChange + ? searchTermPicked + : internalFilterPicked + const actualFilterPickedCallback = filterablePicked + ? filterCallbackPicked + : identity + /* * Extract the not-selected options. * Filters options if filterable is true. @@ -122,11 +140,14 @@ export const Transfer = ({ * Extract the selected options. Can't use `options.filter` * because we need to keep the order of `selected` */ - const pickedOptions = selected - .map(value => options.find(option => value === option.value)) - // filter -> in case a selected value has been provided - // that does not exist as option - .filter(identity) + const pickedOptions = actualFilterPickedCallback( + selected + .map(value => options.find(option => value === option.value)) + // filter -> in case a selected value has been provided + // that does not exist as option + .filter(identity), + actualFilterPicked + ) /* * These are all the highlighted options on the options side. @@ -276,6 +297,8 @@ export const Transfer = ({ disabled={isRemoveIndividualDisabled} onClick={() => removeIndividualPickedOptions({ + filterablePicked, + pickedOptions, highlightedPickedOptions, onChange, selected, @@ -286,9 +309,24 @@ export const Transfer = ({ - {rightHeader && ( + {(rightHeader || filterablePicked) && ( {rightHeader} + + {filterablePicked && !hideFilterInputPicked && ( + + setInternalFilterPicked(value) + } + /> + )} )} @@ -348,12 +386,14 @@ Transfer.defaultProps = { dataTest: 'dhis2-uicore-transfer', height: '240px', initialSearchTerm: '', + initialSearchTermPicked: '', maxSelections: Infinity, optionsWidth: '320px', renderOption: defaultRenderOption, selected: [], selectedWidth: '320px', filterCallback: defaultFilterCallback, + filterCallbackPicked: defaultFilterCallback, } /** @@ -406,12 +446,18 @@ Transfer.propTypes = { disabled: propTypes.bool, enableOrderChange: propTypes.bool, filterCallback: propTypes.func, + filterCallbackPicked: propTypes.func, filterLabel: propTypes.string, + filterLabelPicked: propTypes.string, filterPlaceholder: propTypes.string, + filterPlaceholderPicked: propTypes.string, filterable: propTypes.bool, + filterablePicked: propTypes.bool, height: propTypes.string, hideFilterInput: propTypes.bool, + hideFilterInputPicked: propTypes.bool, initialSearchTerm: propTypes.string, + initialSearchTermPicked: propTypes.string, leftFooter: propTypes.node, leftHeader: propTypes.node, maxSelections: propTypes.oneOf([1, Infinity]), @@ -422,11 +468,13 @@ Transfer.propTypes = { rightFooter: propTypes.node, rightHeader: propTypes.node, searchTerm: propTypes.string, + searchTermPicked: propTypes.string, selected: propTypes.arrayOf(propTypes.string), selectedEmptyComponent: propTypes.node, selectedWidth: propTypes.string, sourceEmptyPlaceholder: propTypes.node, onFilterChange: propTypes.func, + onFilterPickedChange: propTypes.func, onPickedEndReached: propTypes.func, onSourceEndReached: propTypes.func, } diff --git a/packages/widgets/src/Transfer/Transfer.stories.js b/packages/widgets/src/Transfer/Transfer.stories.js index 76adcb1ed6..2e091c29b9 100644 --- a/packages/widgets/src/Transfer/Transfer.stories.js +++ b/packages/widgets/src/Transfer/Transfer.stories.js @@ -168,6 +168,26 @@ export const Filtered = ({ selected, onChange }) => ( /> ) +export const FilteredPicked = ({ selected, onChange }) => ( + Header on the left side} + rightHeader={

Header on the right side

} + options={options} + /> +) + +FilteredPicked.story = { + decorators: [ + statefulDecorator({ + initialState: options.map(({ value }) => value), + }), + ], +} + export const FilterPlaceholder = ({ selected, onChange }) => ( { + /** + * Creates a subset of the highlighted options to reflect a changed + * filter value in case previously highlighted options are now + * hidden. + * + * This enables us to keep items highlighted while searching for + * a particular one. + * + * With this subset we only select the subset when the user + * clicks the "add individuals" button + */ + const filteredHighlightedPickedOptions = filterablePicked + ? highlightedPickedOptions.filter(value => + pickedOptions.find( + filteredOption => filteredOption.value === value + ) + ) + : highlightedPickedOptions + const newSelected = selected.filter( - selectedOption => !highlightedPickedOptions.includes(selectedOption) + selectedOption => + !filteredHighlightedPickedOptions.includes(selectedOption) ) setHighlightedPickedOptions([]) From ce6fa8465f5e7763a1a99846246f754d6ed5fb1e Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Tue, 28 Jul 2020 16:25:58 +0200 Subject: [PATCH 03/10] feat(transfer): add loading props Fixes TECH-379 --- .../widgets/src/Transfer/OptionsContainer.js | 102 ++++++++++++------ packages/widgets/src/Transfer/Transfer.js | 10 +- .../widgets/src/Transfer/Transfer.stories.js | 41 ++++++- 3 files changed, 116 insertions(+), 37 deletions(-) diff --git a/packages/widgets/src/Transfer/OptionsContainer.js b/packages/widgets/src/Transfer/OptionsContainer.js index ee1c19bf47..582cd2d844 100644 --- a/packages/widgets/src/Transfer/OptionsContainer.js +++ b/packages/widgets/src/Transfer/OptionsContainer.js @@ -1,3 +1,4 @@ +import { CircularLoader } from '@dhis2/ui-core' import { spacers } from '@dhis2/ui-constants' import React, { Fragment, useEffect, useRef, useState } from 'react' import propTypes from '@dhis2/prop-types' @@ -10,6 +11,7 @@ export const OptionsContainer = ({ onEndReached, getOptionClickHandlers, highlightedOptions, + loading, renderOption, options, selectionHandler, @@ -42,50 +44,81 @@ export const OptionsContainer = ({ }, [onEndReached, wrapperRef.current, setRemountCounter]) return ( -
-
- {!options.length && emptyComponent} - {options.map(option => { - const highlighted = !!highlightedOptions.find( - highlightedSourceOption => - highlightedSourceOption === option.value - ) - - return ( - - {renderOption({ - ...option, - ...getOptionClickHandlers( - option, - selectionHandler, - toggleHighlightedOption - ), - highlighted, - selected: false, - })} - - ) - })} - - {onEndReached && resizeObserver && ( - - )} +
+ {loading && ( +
+ +
+ )} + +
+
+ {!options.length && emptyComponent} + {options.map(option => { + const highlighted = !!highlightedOptions.find( + highlightedSourceOption => + highlightedSourceOption === option.value + ) + + return ( + + {renderOption({ + ...option, + ...getOptionClickHandlers( + option, + selectionHandler, + toggleHighlightedOption + ), + highlighted, + selected: false, + })} + + ) + })} + + {onEndReached && resizeObserver && ( + + )} +
) @@ -96,6 +129,7 @@ OptionsContainer.propTypes = { getOptionClickHandlers: propTypes.func.isRequired, emptyComponent: propTypes.node, highlightedOptions: propTypes.arrayOf(propTypes.string), + loading: propTypes.bool, options: propTypes.arrayOf( propTypes.shape({ label: propTypes.string.isRequired, diff --git a/packages/widgets/src/Transfer/Transfer.js b/packages/widgets/src/Transfer/Transfer.js index a52b976cf6..e9a909b9f0 100644 --- a/packages/widgets/src/Transfer/Transfer.js +++ b/packages/widgets/src/Transfer/Transfer.js @@ -88,6 +88,8 @@ export const Transfer = ({ initialSearchTermPicked, leftFooter, leftHeader, + loadingPicked, + loadingSource, maxSelections, optionsWidth, removeAllText, @@ -226,9 +228,10 @@ export const Transfer = ({ { + const [loading, setLoading] = useState(false) const [optionsLength, setOptionsLength] = useState(sliceLength) const [optionsSlice, setOptionsSlice] = useState( options.slice(0, optionsLength) @@ -489,16 +490,24 @@ export const InifiniteLoading = ({ selected, onChange }) => { useEffect(() => { if (sliceLength !== optionsLength) { - setOptionsSlice(options.slice(0, optionsLength)) + setTimeout(() => { + setOptionsSlice(options.slice(0, optionsLength)) + setLoading(false) + }, 1000) + + setLoading(true) } }, [optionsLength]) return ( { + if (loading) return + const newOptionsLength = Math.min( optionsLength + sliceLength, options.length @@ -509,3 +518,33 @@ export const InifiniteLoading = ({ selected, onChange }) => { /> ) } + +export const LoadingSource = ({ selected, onChange }) => ( + Left header} + leftFooter={

Left footer

} + /> +) + +export const LoadingPicked = ({ selected, onChange }) => ( + Right header} + rightFooter={

Right footer

} + /> +) + +LoadingPicked.story = { + decorators: [ + statefulDecorator({ + initialState: options.slice(0, 2).map(({ value }) => value), + }), + ], +} From 8067f651ecb732b1f9d08ee4be0dc99c3c7cac0d Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Wed, 29 Jul 2020 10:32:41 +0200 Subject: [PATCH 04/10] chore: fix broken cypress tests after refactoring --- packages/widgets/src/Transfer/Transfer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/widgets/src/Transfer/Transfer.js b/packages/widgets/src/Transfer/Transfer.js index e9a909b9f0..1f85cd02aa 100644 --- a/packages/widgets/src/Transfer/Transfer.js +++ b/packages/widgets/src/Transfer/Transfer.js @@ -228,7 +228,7 @@ export const Transfer = ({ Date: Wed, 29 Jul 2020 15:36:30 +0200 Subject: [PATCH 05/10] refactor(transfer): add data test to intersection detector --- packages/widgets/src/Transfer/EndIntersectionDetector.js | 9 +++++++-- packages/widgets/src/Transfer/OptionsContainer.js | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/widgets/src/Transfer/EndIntersectionDetector.js b/packages/widgets/src/Transfer/EndIntersectionDetector.js index e8bc9aeea6..609450c8db 100644 --- a/packages/widgets/src/Transfer/EndIntersectionDetector.js +++ b/packages/widgets/src/Transfer/EndIntersectionDetector.js @@ -2,8 +2,12 @@ import { IntersectionDetector } from '@dhis2/ui-core' import propTypes from '@dhis2/prop-types' import React from 'react' -export const EndIntersectionDetector = ({ rootRef, onEndReached }) => ( -
+export const EndIntersectionDetector = ({ + rootRef, + onEndReached, + dataTest, +}) => ( +
isIntersecting && onEndReached()} @@ -27,4 +31,5 @@ EndIntersectionDetector.propTypes = { current: propTypes.instanceOf(HTMLElement), }).isRequired, onEndReached: propTypes.func.isRequired, + dataTest: propTypes.string, } diff --git a/packages/widgets/src/Transfer/OptionsContainer.js b/packages/widgets/src/Transfer/OptionsContainer.js index 582cd2d844..cb1d0a86ff 100644 --- a/packages/widgets/src/Transfer/OptionsContainer.js +++ b/packages/widgets/src/Transfer/OptionsContainer.js @@ -78,6 +78,7 @@ export const OptionsContainer = ({ {onEndReached && resizeObserver && ( Date: Wed, 29 Jul 2020 15:37:23 +0200 Subject: [PATCH 06/10] test(transfer): cover end of list notification with cypress tests --- .../notify_at_end_of_list.stories.e2e.js | 71 ++++++++++++++++ .../features/notify_at_end_of_list.feature | 29 +++++++ .../features/notify_at_end_of_list/index.js | 84 +++++++++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 packages/widgets/src/Transfer/__e2e__/notify_at_end_of_list.stories.e2e.js create mode 100644 packages/widgets/src/Transfer/features/notify_at_end_of_list.feature create mode 100644 packages/widgets/src/Transfer/features/notify_at_end_of_list/index.js diff --git a/packages/widgets/src/Transfer/__e2e__/notify_at_end_of_list.stories.e2e.js b/packages/widgets/src/Transfer/__e2e__/notify_at_end_of_list.stories.e2e.js new file mode 100644 index 0000000000..b451b14f75 --- /dev/null +++ b/packages/widgets/src/Transfer/__e2e__/notify_at_end_of_list.stories.e2e.js @@ -0,0 +1,71 @@ +/* eslint-disable react/prop-types */ +import React from 'react' + +import { Transfer } from '../Transfer' +import { statefulDecorator } from './common/statefulDecorator' +import { options } from './common/options' + +export default { + title: 'Transfer End Of List', + decorators: [statefulDecorator()], +} + +window.onSourceEndReached = window.Cypress + ? window.Cypress.cy.stub() + : () => console.log('onSourceEndReached') + +window.onPickedEndReached = window.Cypress + ? window.Cypress.cy.stub() + : () => console.log('onPickedEndReached') + +export const FullSourceList = ({ selected, onChange }) => ( + +) + +export const FullPickedList = ({ selected, onChange }) => ( + +) + +FullPickedList.story = { + decorators: [ + statefulDecorator({ + initialState: options.map(({ value }) => value), + }), + ], +} + +export const PartialSourceList = ({ selected, onChange }) => ( + +) + +export const PartialPickedList = ({ selected, onChange }) => ( + +) + +PartialPickedList.story = { + decorators: [ + statefulDecorator({ + initialState: options.slice(0, 4).map(({ value }) => value), + }), + ], +} diff --git a/packages/widgets/src/Transfer/features/notify_at_end_of_list.feature b/packages/widgets/src/Transfer/features/notify_at_end_of_list.feature new file mode 100644 index 0000000000..4e3f5e1e9a --- /dev/null +++ b/packages/widgets/src/Transfer/features/notify_at_end_of_list.feature @@ -0,0 +1,29 @@ +Feature: The source and picked option lists notify the consumer when the end has been reached + + Scenario Outline: The list is displayed initially and the end is not visible + Given the Transfer has enough items to fill the list completely + Then the callback for reaching the end should not be called + + Examples: + | type | + | source | + | picked | + + Scenario Outline: The list is displayed initially and the end is visible + Given the Transfer does not have enough items to fill the list completely + Then the callback for reaching the end should be called + + Examples: + | type | + | source | + | picked | + + Scenario Outline: The user scrolls down the list to the end + Given the Transfer has enough items to fill the list completely + When the user scroll to the end of the list + Then the callback for reaching the end should be called + + Examples: + | type | + | source | + | picked | diff --git a/packages/widgets/src/Transfer/features/notify_at_end_of_list/index.js b/packages/widgets/src/Transfer/features/notify_at_end_of_list/index.js new file mode 100644 index 0000000000..371af7ead5 --- /dev/null +++ b/packages/widgets/src/Transfer/features/notify_at_end_of_list/index.js @@ -0,0 +1,84 @@ +import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps' + +Cypress.on('uncaught:exception', err => { + // This prevents a benign error: + // This error means that ResizeObserver was not able to deliver all + // observations within a single animation frame. It is benign (your site + // will not break). + // + // Source: https://stackoverflow.com/a/50387233/1319140 + if (err.match(/ResizeObserver loop limit exceeded/)) { + return false + } +}) + +Given( + 'the Transfer has enough items to fill the source list completely', + () => { + cy.visitStory('Transfer End Of List', 'Full Source List') + cy.wrap('source').as('listType') + } +) + +Given( + 'the Transfer has enough items to fill the picked list completely', + () => { + cy.visitStory('Transfer End Of List', 'Full Picked List') + cy.wrap('picked').as('listType') + } +) + +Given( + 'the Transfer does not have enough items to fill the source list completely', + () => { + cy.visitStory('Transfer End Of List', 'Partial Source List') + cy.wrap('source').as('listType') + } +) + +Given( + 'the Transfer does not have enough items to fill the picked list completely', + () => { + cy.visitStory('Transfer End Of List', 'Partial Picked List') + cy.wrap('picked').as('listType') + } +) + +When('the user scroll to the end of the list', () => { + cy.get('@listType').then(listType => { + const listSelector = + listType === 'source' + ? 'transfer-sourceoptions' + : 'transfer-pickedoptions' + + cy.get(`{${listSelector}-endintersectiondetector}`).scrollIntoView() + }) +}) + +Then('the callback for reaching the end should not be called', () => { + cy.all( + () => cy.window(), + () => cy.get('@listType') + ).should(([win, listType]) => { + const callback = + listType === 'source' + ? win.onSourceEndReached + : win.onPickedEndReached + + expect(callback).to.not.be.called + }) +}) + +Then('the callback for reaching the end should be called', () => { + cy.all( + () => cy.window(), + () => cy.get('@listType') + ).should(([win, listType]) => { + const callback = + listType === 'source' + ? win.onSourceEndReached + : win.onPickedEndReached + + expect(callback).to.be.calledOnce + }) +}) From aad40f037ae88491d8768073afb2391a881aeaf8 Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Wed, 29 Jul 2020 16:27:27 +0200 Subject: [PATCH 07/10] test(transfer): cover loading lists with cypress tests --- .../__e2e__/loading_lists.stories.e2e.js | 32 ++++++++++++++ .../Transfer/features/loading_lists.feature | 19 ++++++++ .../Transfer/features/loading_lists/index.js | 43 +++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 packages/widgets/src/Transfer/__e2e__/loading_lists.stories.e2e.js create mode 100644 packages/widgets/src/Transfer/features/loading_lists.feature create mode 100644 packages/widgets/src/Transfer/features/loading_lists/index.js diff --git a/packages/widgets/src/Transfer/__e2e__/loading_lists.stories.e2e.js b/packages/widgets/src/Transfer/__e2e__/loading_lists.stories.e2e.js new file mode 100644 index 0000000000..eb61014403 --- /dev/null +++ b/packages/widgets/src/Transfer/__e2e__/loading_lists.stories.e2e.js @@ -0,0 +1,32 @@ +import React from 'react' + +import { Transfer } from '../Transfer' +import { options } from './common/options' + +export default { title: 'Transfer Loading Lists' } + +export const LoadingSource = () => ( + null} + options={options} + /> +) + +export const LoadingPicked = () => ( + null} + options={options} + /> +) + +export const NotLoadingSource = () => ( + null} options={options} /> +) + +export const NotLoadingPicked = () => ( + null} options={options} /> +) diff --git a/packages/widgets/src/Transfer/features/loading_lists.feature b/packages/widgets/src/Transfer/features/loading_lists.feature new file mode 100644 index 0000000000..ef4fb9ba44 --- /dev/null +++ b/packages/widgets/src/Transfer/features/loading_lists.feature @@ -0,0 +1,19 @@ +Feature: The source and picked lists can have a loading state + + Scenario Outline: A list is loading + Given the list is loading + Then the loading indicator should be shown + + Examples: + | type | + | source | + | picked | + + Scenario Outline: A list is not loading + Given the list is not loading + Then the loading indicator should not be shown + + Examples: + | type | + | source | + | picked | diff --git a/packages/widgets/src/Transfer/features/loading_lists/index.js b/packages/widgets/src/Transfer/features/loading_lists/index.js new file mode 100644 index 0000000000..91155c1383 --- /dev/null +++ b/packages/widgets/src/Transfer/features/loading_lists/index.js @@ -0,0 +1,43 @@ +import { Given, Then } from 'cypress-cucumber-preprocessor/steps' + +Given('the source list is loading', () => { + cy.visitStory('Transfer Loading Lists', 'Loading Source') + cy.wrap('source').as('listType') +}) + +Given('the picked list is loading', () => { + cy.visitStory('Transfer Loading Lists', 'Loading Picked') + cy.wrap('picked').as('listType') +}) + +Given('the source list is not loading', () => { + cy.visitStory('Transfer Loading Lists', 'Not Loading Source') + cy.wrap('source').as('listType') +}) + +Given('the picked list is not loading', () => { + cy.visitStory('Transfer Loading Lists', 'Not Loading Picked') + cy.wrap('picked').as('listType') +}) + +Then('the loading indicator should be shown', () => { + cy.get('@listType').then(listType => { + const listSelector = + listType === 'source' + ? '{transfer-leftside}' + : '{transfer-rightside}' + + cy.get(`${listSelector} .loading`).should('exist') + }) +}) + +Then('the loading indicator should not be shown', () => { + cy.get('@listType').then(listType => { + const listSelector = + listType === 'source' + ? '{transfer-leftside}' + : '{transfer-rightside}' + + cy.get(`${listSelector} .loading`).should('not.exist') + }) +}) From 2da5fcd3846c6a3f7bd24b0f07a2677d06f280f0 Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Tue, 11 Aug 2020 12:52:06 +0200 Subject: [PATCH 08/10] refactor: extract logic for contstructing filtering variables --- packages/widgets/src/Transfer/Transfer.js | 83 +++++++++++-------- .../widgets/src/Transfer/Transfer/index.js | 1 + .../src/Transfer/Transfer/useFilter.js | 17 ++++ 3 files changed, 67 insertions(+), 34 deletions(-) create mode 100644 packages/widgets/src/Transfer/Transfer/useFilter.js diff --git a/packages/widgets/src/Transfer/Transfer.js b/packages/widgets/src/Transfer/Transfer.js index 1f85cd02aa..682d1beae8 100644 --- a/packages/widgets/src/Transfer/Transfer.js +++ b/packages/widgets/src/Transfer/Transfer.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React from 'react' import propTypes from '@dhis2/prop-types' import { Actions } from './Actions.js' @@ -29,6 +29,7 @@ import { moveHighlightedPickedOptionUp, removeAllPickedOptions, removeIndividualPickedOptions, + useFilter, useHighlightedOptions, } from './Transfer/index.js' @@ -108,28 +109,23 @@ export const Transfer = ({ onSourceEndReached, onPickedEndReached, }) => { - /* - * Used in the "Filter" section and for - * limiting the selectable source options - * - * Filter can be controlled & uncontrolled. - * Providing the "onFilterChange" callback will make it a controlled value - */ - const [internalFilter, setInternalFilter] = useState(initialSearchTerm) - const actualFilter = onFilterChange ? searchTerm : internalFilter - const actualFilterCallback = filterable ? filterCallback : identity - - const [internalFilterPicked, setInternalFilterPicked] = useState( - initialSearchTermPicked - ) - const actualFilterPicked = onFilterPickedChange - ? searchTermPicked - : internalFilterPicked - const actualFilterPickedCallback = filterablePicked - ? filterCallbackPicked - : identity + /* Source options search value: + * Depending on whether the onFilterChange callback has been provided + * either the internal or external search value is used */ + const { + filterValue: actualFilter, + filter: actualFilterCallback, + setInternalFilter, + } = useFilter({ + initialSearchTerm, + onFilterChange, + externalSearchTerm: searchTerm, + filterable, + filterCallback, + }) /* + * Actual source options: * Extract the not-selected options. * Filters options if filterable is true. */ @@ -139,6 +135,36 @@ export const Transfer = ({ ) /* + * Source options highlighting: + * These are all the highlighted options on the selected side. + */ + const { + highlightedOptions: highlightedPickedOptions, + setHighlightedOptions: setHighlightedPickedOptions, + toggleHighlightedOption: toggleHighlightedPickedOption, + } = useHighlightedOptions({ + options: pickedOptions, + disabled, + maxSelections, + }) + + /* Picked options search value: + * Depending on whether the onFilterPickedChange callback has been provided + * either the internal or external search value is used */ + const { + filterValue: actualFilterPicked, + filter: actualFilterPickedCallback, + setInternalFilter: setInternalFilterPicked, + } = useFilter({ + filterable: filterablePicked, + initialSearchTerm: initialSearchTermPicked, + onFilterChange: onFilterPickedChange, + externalSearchTerm: searchTermPicked, + filterCallback: filterCallbackPicked, + }) + + /* + * Actual picked options: * Extract the selected options. Can't use `options.filter` * because we need to keep the order of `selected` */ @@ -152,6 +178,7 @@ export const Transfer = ({ ) /* + * Picked options highlighting: * These are all the highlighted options on the options side. */ const { @@ -165,19 +192,7 @@ export const Transfer = ({ }) /* - * These are all the highlighted options on the selected side. - */ - const { - highlightedOptions: highlightedPickedOptions, - setHighlightedOptions: setHighlightedPickedOptions, - toggleHighlightedOption: toggleHighlightedPickedOption, - } = useHighlightedOptions({ - options: pickedOptions, - disabled, - maxSelections, - }) - - /* + * Source & Picked options: * These are the double click handlers for (de-)selection */ const { diff --git a/packages/widgets/src/Transfer/Transfer/index.js b/packages/widgets/src/Transfer/Transfer/index.js index 8e7e3662f8..6304b44698 100644 --- a/packages/widgets/src/Transfer/Transfer/index.js +++ b/packages/widgets/src/Transfer/Transfer/index.js @@ -9,4 +9,5 @@ export * from './moveHighlightedPickedOptionDown.js' export * from './moveHighlightedPickedOptionUp.js' export * from './removeAllPickedOptions.js' export * from './removeIndividualPickedOptions.js' +export * from './useFilter' export * from './useHighlightedOptions.js' diff --git a/packages/widgets/src/Transfer/Transfer/useFilter.js b/packages/widgets/src/Transfer/Transfer/useFilter.js new file mode 100644 index 0000000000..a0ab34a89c --- /dev/null +++ b/packages/widgets/src/Transfer/Transfer/useFilter.js @@ -0,0 +1,17 @@ +import { useState } from 'react' + +const identity = value => value + +export const useFilter = ({ + initialSearchTerm, + onFilterChange, + externalSearchTerm, + filterable, + filterCallback, +}) => { + const [internalFilter, setInternalFilter] = useState(initialSearchTerm) + const filterValue = onFilterChange ? externalSearchTerm : internalFilter + const filter = filterable ? filterCallback : identity + + return { filterValue, filter, setInternalFilter } +} From 74ff168c26a0ef64814cb0ed1220b08482bb6901 Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Tue, 11 Aug 2020 13:55:25 +0200 Subject: [PATCH 09/10] chore: fix broken cypress tests --- packages/widgets/src/Transfer/Transfer.js | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/widgets/src/Transfer/Transfer.js b/packages/widgets/src/Transfer/Transfer.js index 682d1beae8..88aaaae220 100644 --- a/packages/widgets/src/Transfer/Transfer.js +++ b/packages/widgets/src/Transfer/Transfer.js @@ -135,15 +135,15 @@ export const Transfer = ({ ) /* - * Source options highlighting: - * These are all the highlighted options on the selected side. + * Picked options highlighting: + * These are all the highlighted options on the options side. */ const { - highlightedOptions: highlightedPickedOptions, - setHighlightedOptions: setHighlightedPickedOptions, - toggleHighlightedOption: toggleHighlightedPickedOption, + highlightedOptions: highlightedSourceOptions, + setHighlightedOptions: setHighlightedSourceOptions, + toggleHighlightedOption: toggleHighlightedSourceOption, } = useHighlightedOptions({ - options: pickedOptions, + options: sourceOptions, disabled, maxSelections, }) @@ -178,15 +178,15 @@ export const Transfer = ({ ) /* - * Picked options highlighting: - * These are all the highlighted options on the options side. + * Source options highlighting: + * These are all the highlighted options on the selected side. */ const { - highlightedOptions: highlightedSourceOptions, - setHighlightedOptions: setHighlightedSourceOptions, - toggleHighlightedOption: toggleHighlightedSourceOption, + highlightedOptions: highlightedPickedOptions, + setHighlightedOptions: setHighlightedPickedOptions, + toggleHighlightedOption: toggleHighlightedPickedOption, } = useHighlightedOptions({ - options: sourceOptions, + options: pickedOptions, disabled, maxSelections, }) From 13a1804714cf464acdd439f00e4f1b8c122b18ba Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Fri, 14 Aug 2020 13:30:40 +0200 Subject: [PATCH 10/10] refactor: make prop names consistent --- packages/widgets/src/Transfer/Transfer.js | 30 +++++++++---------- .../__e2e__/loading_lists.stories.e2e.js | 7 +---- .../notify_at_end_of_list.stories.e2e.js | 16 +++++----- .../features/notify_at_end_of_list/index.js | 8 ++--- 4 files changed, 26 insertions(+), 35 deletions(-) diff --git a/packages/widgets/src/Transfer/Transfer.js b/packages/widgets/src/Transfer/Transfer.js index 88aaaae220..c706e9cfd0 100644 --- a/packages/widgets/src/Transfer/Transfer.js +++ b/packages/widgets/src/Transfer/Transfer.js @@ -90,7 +90,7 @@ export const Transfer = ({ leftFooter, leftHeader, loadingPicked, - loadingSource, + loading, maxSelections, optionsWidth, removeAllText, @@ -105,9 +105,9 @@ export const Transfer = ({ selectedWidth, sourceEmptyPlaceholder, onFilterChange, - onFilterPickedChange, - onSourceEndReached, - onPickedEndReached, + onFilterChangePicked, + onEndReached, + onEndReachedPicked, }) => { /* Source options search value: * Depending on whether the onFilterChange callback has been provided @@ -149,7 +149,7 @@ export const Transfer = ({ }) /* Picked options search value: - * Depending on whether the onFilterPickedChange callback has been provided + * Depending on whether the onFilterChangePicked callback has been provided * either the internal or external search value is used */ const { filterValue: actualFilterPicked, @@ -158,7 +158,7 @@ export const Transfer = ({ } = useFilter({ filterable: filterablePicked, initialSearchTerm: initialSearchTermPicked, - onFilterChange: onFilterPickedChange, + onFilterChange: onFilterChangePicked, externalSearchTerm: searchTermPicked, filterCallback: filterCallbackPicked, }) @@ -246,12 +246,12 @@ export const Transfer = ({ emptyComponent={sourceEmptyPlaceholder} getOptionClickHandlers={getOptionClickHandlers} highlightedOptions={highlightedSourceOptions} - loading={loadingSource} + loading={loading} options={sourceOptions} renderOption={renderOption} selectionHandler={selectSingleOption} toggleHighlightedOption={toggleHighlightedSourceOption} - onEndReached={onSourceEndReached} + onEndReached={onEndReached} /> {leftFooter && ( @@ -338,8 +338,8 @@ export const Transfer = ({ dataTest={`${dataTest}-filter`} filter={actualFilterPicked} onChange={ - onFilterPickedChange - ? onFilterPickedChange + onFilterChangePicked + ? onFilterChangePicked : ({ value }) => setInternalFilterPicked(value) } @@ -358,7 +358,7 @@ export const Transfer = ({ renderOption={renderOption} selectionHandler={deselectSingleOption} toggleHighlightedOption={toggleHighlightedPickedOption} - onEndReached={onPickedEndReached} + onEndReached={onEndReachedPicked} /> {(rightFooter || enableOrderChange) && ( @@ -479,8 +479,8 @@ Transfer.propTypes = { initialSearchTermPicked: propTypes.string, leftFooter: propTypes.node, leftHeader: propTypes.node, + loading: propTypes.bool, loadingPicked: propTypes.bool, - loadingSource: propTypes.bool, maxSelections: propTypes.oneOf([1, Infinity]), optionsWidth: propTypes.string, removeAllText: propTypes.string, @@ -494,8 +494,8 @@ Transfer.propTypes = { selectedEmptyComponent: propTypes.node, selectedWidth: propTypes.string, sourceEmptyPlaceholder: propTypes.node, + onEndReached: propTypes.func, + onEndReachedPicked: propTypes.func, onFilterChange: propTypes.func, - onFilterPickedChange: propTypes.func, - onPickedEndReached: propTypes.func, - onSourceEndReached: propTypes.func, + onFilterChangePicked: propTypes.func, } diff --git a/packages/widgets/src/Transfer/__e2e__/loading_lists.stories.e2e.js b/packages/widgets/src/Transfer/__e2e__/loading_lists.stories.e2e.js index eb61014403..8acccb4999 100644 --- a/packages/widgets/src/Transfer/__e2e__/loading_lists.stories.e2e.js +++ b/packages/widgets/src/Transfer/__e2e__/loading_lists.stories.e2e.js @@ -6,12 +6,7 @@ import { options } from './common/options' export default { title: 'Transfer Loading Lists' } export const LoadingSource = () => ( - null} - options={options} - /> + null} options={options} /> ) export const LoadingPicked = () => ( diff --git a/packages/widgets/src/Transfer/__e2e__/notify_at_end_of_list.stories.e2e.js b/packages/widgets/src/Transfer/__e2e__/notify_at_end_of_list.stories.e2e.js index b451b14f75..aa0f48ca8c 100644 --- a/packages/widgets/src/Transfer/__e2e__/notify_at_end_of_list.stories.e2e.js +++ b/packages/widgets/src/Transfer/__e2e__/notify_at_end_of_list.stories.e2e.js @@ -10,20 +10,20 @@ export default { decorators: [statefulDecorator()], } -window.onSourceEndReached = window.Cypress +window.onEndReached = window.Cypress ? window.Cypress.cy.stub() - : () => console.log('onSourceEndReached') + : () => console.log('onEndReached') -window.onPickedEndReached = window.Cypress +window.onEndReachedPicked = window.Cypress ? window.Cypress.cy.stub() - : () => console.log('onPickedEndReached') + : () => console.log('onEndReachedPicked') export const FullSourceList = ({ selected, onChange }) => ( ) @@ -32,7 +32,7 @@ export const FullPickedList = ({ selected, onChange }) => ( options={options} selected={selected} onChange={onChange} - onPickedEndReached={window.onPickedEndReached} + onEndReachedPicked={window.onEndReachedPicked} /> ) @@ -49,7 +49,7 @@ export const PartialSourceList = ({ selected, onChange }) => ( options={options.slice(0, 4)} selected={selected} onChange={onChange} - onSourceEndReached={window.onSourceEndReached} + onEndReached={window.onEndReached} /> ) @@ -58,7 +58,7 @@ export const PartialPickedList = ({ selected, onChange }) => ( options={options} selected={selected} onChange={onChange} - onPickedEndReached={window.onPickedEndReached} + onEndReachedPicked={window.onEndReachedPicked} /> ) diff --git a/packages/widgets/src/Transfer/features/notify_at_end_of_list/index.js b/packages/widgets/src/Transfer/features/notify_at_end_of_list/index.js index 371af7ead5..1e7715f249 100644 --- a/packages/widgets/src/Transfer/features/notify_at_end_of_list/index.js +++ b/packages/widgets/src/Transfer/features/notify_at_end_of_list/index.js @@ -61,9 +61,7 @@ Then('the callback for reaching the end should not be called', () => { () => cy.get('@listType') ).should(([win, listType]) => { const callback = - listType === 'source' - ? win.onSourceEndReached - : win.onPickedEndReached + listType === 'source' ? win.onEndReached : win.onEndReachedPicked expect(callback).to.not.be.called }) @@ -75,9 +73,7 @@ Then('the callback for reaching the end should be called', () => { () => cy.get('@listType') ).should(([win, listType]) => { const callback = - listType === 'source' - ? win.onSourceEndReached - : win.onPickedEndReached + listType === 'source' ? win.onEndReached : win.onEndReachedPicked expect(callback).to.be.calledOnce })