diff --git a/CHANGELOG.md b/CHANGELOG.md
index eb44ceb77..5659a3883 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased]
+### Added
+
+- Support for lazy loading select options with pagination.
+
## [9.128.1] - 2020-08-28
### Fixed
diff --git a/react/components/EXPERIMENTAL_Select/README.md b/react/components/EXPERIMENTAL_Select/README.md
index 3bad6c3ba..21c85c6d2 100755
--- a/react/components/EXPERIMENTAL_Select/README.md
+++ b/react/components/EXPERIMENTAL_Select/README.md
@@ -393,3 +393,49 @@ class SelectWithModalExample extends React.Component {
;
```
+
+Paginated
+
+```js
+const options = []
+for (let i = 0; i < 1000; ++i) {
+ options.push({
+ value: i + 1,
+ label: `Option ${i + 1}`,
+ })
+}
+
+const sleep = ms =>
+ new Promise(resolve => {
+ setTimeout(() => {
+ resolve()
+ }, ms)
+ })
+
+;
+
+
+
+```
diff --git a/react/components/EXPERIMENTAL_Select/index.js b/react/components/EXPERIMENTAL_Select/index.js
index 0d71b50fa..fabd03ec7 100755
--- a/react/components/EXPERIMENTAL_Select/index.js
+++ b/react/components/EXPERIMENTAL_Select/index.js
@@ -1,9 +1,10 @@
+import React, { useMemo } from 'react'
import classNames from 'classnames'
import PropTypes from 'prop-types'
-import React, { Component } from 'react'
import uuid from 'uuid/v4'
import ReactSelect from 'react-select'
import CreatableSelect from 'react-select/lib/Creatable'
+import { useAsyncPaginate, useComponents } from 'react-select-async-paginate'
import ClearIndicator from './ClearIndicator'
import COLORS from './colors'
@@ -23,191 +24,190 @@ const getOptionValue = option => {
return JSON.stringify(option.value)
}
-class Select extends Component {
- constructor(props) {
- super(props)
+const Select = ({
+ autoFocus,
+ clearable,
+ components,
+ creatable,
+ defaultMenuIsOpen,
+ defaultValue,
+ disabled,
+ errorMessage,
+ formatCreateLabel,
+ forwardedRef,
+ label,
+ loadOptions,
+ loading,
+ menuPosition,
+ multi,
+ noOptionsMessage,
+ onChange,
+ onSearchInputChange,
+ options,
+ paginated,
+ placeholder,
+ size,
+ value,
+ valuesMaxHeight,
+}) => {
+ const inputId = useMemo(() => `react-select-input-${uuid()}`, [])
- this.inputId = `react-select-input-${uuid()}`
- }
+ const paginatedProps = useAsyncPaginate({ loadOptions })
+ const paginatedComponents = useComponents(components)
- render() {
- const {
- forwardedRef,
- autoFocus,
- creatable,
- defaultValue,
- disabled,
- errorMessage,
- formatCreateLabel,
- label,
- loading,
- multi,
- noOptionsMessage,
- onChange,
- onSearchInputChange,
- options,
- placeholder,
- size,
- value,
- valuesMaxHeight,
- clearable,
- defaultMenuIsOpen,
- components,
- menuPosition,
- } = this.props
+ const reactSelectComponentProps = {
+ menuPosition,
+ defaultMenuIsOpen,
+ ref: forwardedRef,
+ autoFocus,
+ className: `pointer bw1 ${getFontClassNameFromSize(size)}`,
+ errorMessage,
+ size,
+ components: {
+ ClearIndicator,
+ Control: ControlComponent,
+ DropdownIndicator: DropdownIndicatorComponent,
+ IndicatorSeparator: () => null,
+ MultiValueRemove,
+ Placeholder,
+ Option,
+ ...(paginated ? paginatedComponents : components),
+ },
+ defaultValue,
+ formatCreateLabel,
+ getOptionValue,
+ isClearable: clearable,
+ isDisabled: disabled,
+ isLoading: loading,
+ isMulti: multi,
+ noOptionsMessage,
+ inputId: inputId,
+ onInputChange: (value, { action }) => {
+ if (
+ action === 'input-change' &&
+ typeof onSearchInputChange === 'function'
+ ) {
+ onSearchInputChange(value)
+ }
+ },
+ onChange,
+ options,
+ placeholder,
+ styles: {
+ control: (style, state) => {
+ const { isFocused } = state
- const reactSelectComponentProps = {
- menuPosition,
- defaultMenuIsOpen,
- ref: forwardedRef,
- autoFocus,
- className: `pointer bw1 ${getFontClassNameFromSize(size)}`,
- errorMessage,
- size,
- components: {
- ClearIndicator,
- Control: ControlComponent,
- DropdownIndicator: DropdownIndicatorComponent,
- IndicatorSeparator: () => null,
- MultiValueRemove,
- Placeholder,
- Option,
- ...components,
- },
- defaultValue,
- formatCreateLabel,
- getOptionValue,
- isClearable: clearable,
- isDisabled: disabled,
- isLoading: loading,
- isMulti: multi,
- noOptionsMessage,
- inputId: this.inputId,
- onInputChange: (value, { action }) => {
- if (
- action === 'input-change' &&
- typeof onSearchInputChange === 'function'
- ) {
- onSearchInputChange(value)
- }
- },
- onChange,
- options,
- placeholder,
- styles: {
- control: (style, state) => {
- const { isFocused } = state
-
- return {
- ...style,
- '&:hover': {
- borderColor: errorMessage
- ? COLORS.red
- : isFocused
- ? COLORS['muted-2']
- : COLORS['muted-3'],
- },
- boxShadow: 'none',
+ return {
+ ...style,
+ '&:hover': {
borderColor: errorMessage
? COLORS.red
: isFocused
? COLORS['muted-2']
- : COLORS['muted-4'],
- borderWidth: '.125rem',
- }
- },
- menu: style => ({ ...style, marginTop: 0 }),
- multiValue: (style, state) => ({
- ...style,
- backgroundColor: state.isDisabled
- ? COLORS['muted-4']
- : COLORS.aliceBlue,
- ':hover': {
- transition: '.15s ease-in-out',
- backgroundColor: COLORS['hover-action-secondary'],
- },
- borderRadius: 100,
- padding: getTagPaddingFromSize(size),
- position: 'relative',
- }),
- multiValueLabel: (style, state) => ({
- ...style,
- padding: '0.125rem',
- paddingRight: 0,
- fontWeight: 500,
- fontSize: size === 'large' ? '100%' : style.fontSize,
- color: state.isDisabled ? COLORS.gray : COLORS['c-on-base'],
- }),
- multiValueRemove: (style, state) => ({
- ...style,
- color: state.isDisabled ? COLORS.gray : COLORS['muted-1'],
- ':hover': {
- backgroundColor: 'transparent',
- color: COLORS.blue,
+ : COLORS['muted-3'],
},
- }),
- option: (style, state) => ({
- ...style,
- cursor: 'pointer',
- backgroundColor: state.isFocused
- ? COLORS['hover-action-secondary']
- : 'transparent',
- color: COLORS['c-muted-1'],
- }),
- valueContainer: (style, state) => ({
- ...style,
- cursor: 'pointer',
- paddingLeft: state.isMulti && state.hasValue ? '.25rem' : '1rem',
- paddingRight: '.25rem',
- backgroundColor: state.isDisabled
- ? COLORS.lightGray
- : style.backgroundColor,
- maxHeight: `${valuesMaxHeight}px`,
- overflowY: 'auto',
- }),
+ boxShadow: 'none',
+ borderColor: errorMessage
+ ? COLORS.red
+ : isFocused
+ ? COLORS['muted-2']
+ : COLORS['muted-4'],
+ borderWidth: '.125rem',
+ }
},
- theme: theme => ({
- ...theme,
- spacing: {
- ...theme.spacing,
- controlHeight: getControlHeightFromSize(size),
+ menu: style => ({ ...style, marginTop: 0 }),
+ multiValue: (style, state) => ({
+ ...style,
+ backgroundColor: state.isDisabled
+ ? COLORS['muted-4']
+ : COLORS.aliceBlue,
+ ':hover': {
+ transition: '.15s ease-in-out',
+ backgroundColor: COLORS['hover-action-secondary'],
+ },
+ borderRadius: 100,
+ padding: getTagPaddingFromSize(size),
+ position: 'relative',
+ }),
+ multiValueLabel: (style, state) => ({
+ ...style,
+ padding: '0.125rem',
+ paddingRight: 0,
+ fontWeight: 500,
+ fontSize: size === 'large' ? '100%' : style.fontSize,
+ color: state.isDisabled ? COLORS.gray : COLORS['c-on-base'],
+ }),
+ multiValueRemove: (style, state) => ({
+ ...style,
+ color: state.isDisabled ? COLORS.gray : COLORS['muted-1'],
+ ':hover': {
+ backgroundColor: 'transparent',
+ color: COLORS.blue,
},
}),
- value,
- }
+ option: (style, state) => ({
+ ...style,
+ cursor: 'pointer',
+ backgroundColor: state.isFocused
+ ? COLORS['hover-action-secondary']
+ : 'transparent',
+ color: COLORS['c-muted-1'],
+ }),
+ valueContainer: (style, state) => ({
+ ...style,
+ cursor: 'pointer',
+ paddingLeft: state.isMulti && state.hasValue ? '.25rem' : '1rem',
+ paddingRight: '.25rem',
+ backgroundColor: state.isDisabled
+ ? COLORS.lightGray
+ : style.backgroundColor,
+ maxHeight: `${valuesMaxHeight}px`,
+ overflowY: 'auto',
+ }),
+ },
+ theme: theme => ({
+ ...theme,
+ spacing: {
+ ...theme.spacing,
+ controlHeight: getControlHeightFromSize(size),
+ },
+ }),
+ value,
+ ...(paginated ? paginatedProps : {}),
+ }
- return (
-
- {label && (
-
- )}
+ return (
+
+ {label && (
+
+ )}
- {creatable ? (
-
- ) : (
-
- )}
+ {creatable ? (
+
+ ) : (
+
+ )}
- {errorMessage && (
- {errorMessage}
- )}
-
- )
- }
+ {errorMessage && (
+
{errorMessage}
+ )}
+
+ )
}
Select.defaultProps = {
+ clearable: true,
+ defaultMenuIsOpen: false,
multi: true,
+ paginated: false,
placeholder: 'Select...',
size: 'regular',
- clearable: true,
- defaultMenuIsOpen: false,
}
const OptionShape = PropTypes.shape({
@@ -251,6 +251,8 @@ Select.propTypes = {
formatCreateLabel: PropTypes.func,
/** Label text. */
label: PropTypes.string,
+ /** Function that deals with how the options are loaded if using pagination. */
+ loadOptions: PropTypes.func,
/** Is the select in a state of loading (async). */
loading: PropTypes.bool,
/** Text to display when loading options */
@@ -269,6 +271,8 @@ Select.propTypes = {
placeholder: PropTypes.string,
/** Select size */
size: PropTypes.oneOf(['small', 'regular', 'large']),
+ /** Flag for informing wheter pagination should be used or not. */
+ paginated: PropTypes.bool,
/** Value of the select. */
value: PropTypes.oneOfType([OptionShape, OptionsShape]),
/** Max height (in _px_) of the selected values container */
diff --git a/react/package.json b/react/package.json
index d255a5622..d79732726 100644
--- a/react/package.json
+++ b/react/package.json
@@ -10,6 +10,7 @@
"react-overlays": "^1.1.2",
"react-responsive-modal": "^3.1.0",
"react-select": "^2.1.2",
+ "react-select-async-paginate": "^0.4.0",
"react-sticky": "^6.0.3",
"react-virtualized": "^9.19.1",
"use-media": "^1.4.0",
diff --git a/react/yarn.lock b/react/yarn.lock
index 5da9d520e..4cfbe9dd8 100644
--- a/react/yarn.lock
+++ b/react/yarn.lock
@@ -48,6 +48,13 @@
dependencies:
regenerator-runtime "^0.13.2"
+"@babel/runtime@^7.10.5":
+ version "7.11.2"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
+ integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/types@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c"
@@ -118,6 +125,11 @@
"@types/istanbul-reports" "^1.1.1"
"@types/yargs" "^13.0.0"
+"@seznam/compose-react-refs@^1.0.4":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@seznam/compose-react-refs/-/compose-react-refs-1.0.4.tgz#9dd29c8c503b85955b4478bf115caa608b4e87ab"
+ integrity sha512-TwrojUAFVSd+HPAdnul0o65X8mIam+dJOxcWI6LhHAUIpVRk2cJp2dyWXWl6sJvZTY9ODSJpOibt7JKSNUjVfQ==
+
"@types/classnames@^2.2.9":
version "2.2.9"
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.9.tgz#d868b6febb02666330410fe7f58f3c4b8258be7b"
@@ -1626,6 +1638,11 @@ react-input-autosize@^2.2.1:
dependencies:
prop-types "^15.5.8"
+react-is-mounted-hook@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/react-is-mounted-hook/-/react-is-mounted-hook-1.0.3.tgz#297e3ccc9f3c5eb2e3a867576e1c9e5c59c3d39e"
+ integrity sha512-YCCYcTVYMPfTi6WhWIwM9EYBcpHoivjjkE90O5ScsE9wXSbeXGZvLDMGt4mdSNcWshhc8JD0AzgBmsleCSdSFA==
+
react-is@^16.3.2, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.9.0:
version "16.12.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
@@ -1717,6 +1734,16 @@ react-responsive-modal@^3.1.0:
react-minimalist-portal "^2.3.1"
react-transition-group "^2.4.0"
+react-select-async-paginate@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/react-select-async-paginate/-/react-select-async-paginate-0.4.0.tgz#0c2adfe1425eedd16bc4893228c9ba43aa6cb62a"
+ integrity sha512-57rjnsNy/doJ3RBgHLL4re4dsUeZnTm1EVD2LBsq6PL6JkvGqimnuKHCKBIqlpuuzRcxDY4Ffc7xjoQ4ZlOL1A==
+ dependencies:
+ "@babel/runtime" "^7.10.5"
+ "@seznam/compose-react-refs" "^1.0.4"
+ react-is-mounted-hook "^1.0.3"
+ sleep-promise "^8.0.1"
+
react-select@^2.1.2:
version "2.4.4"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-2.4.4.tgz#ba72468ef1060c7d46fbb862b0748f96491f1f73"
@@ -1854,6 +1881,11 @@ regenerator-runtime@^0.13.2:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==
+regenerator-runtime@^0.13.4:
+ version "0.13.7"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
+ integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
+
regex-not@^1.0.0, regex-not@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
@@ -1944,6 +1976,11 @@ slash@^2.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
+sleep-promise@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/sleep-promise/-/sleep-promise-8.0.1.tgz#8d795a27ea23953df6b52b91081e5e22665993c5"
+ integrity sha1-jXlaJ+ojlT32tSuRCB5eImZZk8U=
+
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"