diff --git a/.changeset/brown-terms-hear.md b/.changeset/brown-terms-hear.md
deleted file mode 100644
index 76f174b24f..0000000000
--- a/.changeset/brown-terms-hear.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'react-select': patch
----
-
-Convert class components that don't have to be class components to function components to reduce bundle size
diff --git a/.changeset/chilly-eagles-arrive.md b/.changeset/chilly-eagles-arrive.md
new file mode 100644
index 0000000000..bc90ac2277
--- /dev/null
+++ b/.changeset/chilly-eagles-arrive.md
@@ -0,0 +1,5 @@
+---
+"react-select": patch
+---
+
+Fix repository field
diff --git a/.changeset/flat-kings-applaud.md b/.changeset/flat-kings-applaud.md
new file mode 100644
index 0000000000..0e805a6a21
--- /dev/null
+++ b/.changeset/flat-kings-applaud.md
@@ -0,0 +1,5 @@
+---
+'react-select': patch
+---
+
+Improve performance of option filtering when ignoreAccents is enabled (the default)
diff --git a/.changeset/nine-goats-care.md b/.changeset/nine-goats-care.md
deleted file mode 100644
index ead4342084..0000000000
--- a/.changeset/nine-goats-care.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"react-select": minor
----
-
-Add `isLoading` prop support to the AsyncSelect component (see #3690)
diff --git a/.changeset/old-tips-leave.md b/.changeset/old-tips-leave.md
deleted file mode 100644
index 815e39d0c9..0000000000
--- a/.changeset/old-tips-leave.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"react-select": patch
----
-
-Allow the input component to be a `textarea` element
diff --git a/.changeset/real-suns-rescue.md b/.changeset/real-suns-rescue.md
deleted file mode 100644
index 123f906beb..0000000000
--- a/.changeset/real-suns-rescue.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'react-select': patch
----
-
-Add aria attributes to dummy input
diff --git a/.changeset/silent-jokes-camp.md b/.changeset/silent-jokes-camp.md
deleted file mode 100644
index 40acfb6cf9..0000000000
--- a/.changeset/silent-jokes-camp.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"react-select": patch
----
-
-Fix Flow issues. Refer to the linked PR for more details on the specific issues.
diff --git a/.changeset/sixty-forks-fry.md b/.changeset/sixty-forks-fry.md
new file mode 100644
index 0000000000..ff60f26ea5
--- /dev/null
+++ b/.changeset/sixty-forks-fry.md
@@ -0,0 +1,5 @@
+---
+"react-select": patch
+---
+
+Fix react-select ignoring HTML5 "form" attribute
diff --git a/.changeset/soft-lemons-invent.md b/.changeset/soft-lemons-invent.md
deleted file mode 100644
index eabbcc00b0..0000000000
--- a/.changeset/soft-lemons-invent.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'react-select': patch
----
-
-Update react-transition-group to ^4.3.0
diff --git a/.changeset/spicy-ads-poke.md b/.changeset/spicy-ads-poke.md
deleted file mode 100644
index 8e99e82e3e..0000000000
--- a/.changeset/spicy-ads-poke.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'react-select': patch
----
-
-Enable Babel loose mode to improve bundle size
diff --git a/.circleci/config.yml b/.circleci/config.yml
index bc9814e5e3..0391cb806c 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -2,7 +2,7 @@ version: 2
docker_defaults: &docker_defaults
docker:
- - image: cypress/base:8
+ - image: cypress/base:13.6.0
environment:
TERM: xterm
working_directory: ~/project/repo
@@ -17,7 +17,7 @@ install_steps: &install_steps
- restore_cache:
name: Restore node_modules cache
keys:
- - dependency-cache-{{ .Branch }}-{{ checksum "package.json" }}
+ - dependency-cache-{{ .Branch }}-{{ checksum "yarn.lock" }}
- dependency-cache-{{ .Branch }}-
- dependency-cache-
- run:
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
index 20856b2f33..1a70f05a29 100644
--- a/.github/ISSUE_TEMPLATE.md
+++ b/.github/ISSUE_TEMPLATE.md
@@ -3,13 +3,21 @@
Before creating an issue...
# Are you asking a question?
-Please don't file GitHub issues to ask questions. Use Stack Overflow with a [#react-select](http://stackoverflow.com/questions/tagged/react-select) tag
+Please don't file GitHub issues to ask questions. Use Stack Overflow with a [#react-select](http://stackoverflow.com/questions/tagged/react-select) tag.
# Are you reporting a bug or runtime error?
Please include a test case that demonstrates the issue you're reporting!
+
This is very helpful to maintainers in order to help us see the issue you're seeing.
+Please note we are currently only directing our efforts towards the current major (v3) version and beyond.
+
+We understand this might be inconvenient but it is in the best interest of supporting the broader community and to sustain the `react-select` project going forward.
+
+To report bugs against react-select v3 please fork the following code-sandbox:
+https://codesandbox.io/s/react-select-v3-sandbox-jgdch
+
To report bugs against react-select v2 please fork the following code-sandbox:
https://codesandbox.io/s/k5rvn9z3lv
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e68285cd43..520150afcf 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -12,6 +12,9 @@ jobs:
steps:
- name: Checkout Repo
uses: actions/checkout@master
+ with:
+ # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
+ fetch-depth: 0
- name: Setup Node.js 10.x
uses: actions/setup-node@master
diff --git a/.nvmrc b/.nvmrc
index f599e28b8a..48082f72f0 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-10
+12
diff --git a/babel.config.js b/babel.config.js
index af7941a6f4..629bd0c472 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -2,10 +2,7 @@ module.exports = {
plugins: [
'emotion',
['@babel/plugin-proposal-class-properties', { loose: true }],
+ '@babel/plugin-transform-runtime',
],
- presets: [
- ['@babel/preset-env', { loose: true }],
- '@babel/preset-react',
- '@babel/preset-flow',
- ],
+ presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-flow'],
};
diff --git a/docs/examples/StyleCompositionExample.js b/docs/examples/StyleCompositionExample.js
index a1c9b3a43e..9ea96092cb 100644
--- a/docs/examples/StyleCompositionExample.js
+++ b/docs/examples/StyleCompositionExample.js
@@ -1,5 +1,5 @@
-import React from 'react';
-import { css } from 'emotion';
+/** @jsx jsx */
+import { jsx } from '@emotion/core';
import Select from 'react-select';
import { colourOptions } from '../data';
@@ -18,8 +18,8 @@ const Option = (props: OptionProps) => {
return (
diff --git a/docs/pages/home/index.js b/docs/pages/home/index.js
index 72a5ce0442..a1032c1a26 100644
--- a/docs/pages/home/index.js
+++ b/docs/pages/home/index.js
@@ -136,7 +136,7 @@ export default function Home() {
Use the Async component to load options from a remote source as the user types.
~~~jsx
- import Async from 'react-select/async';
+ import AsyncSelect from 'react-select/async';
~~~
${(
diff --git a/enzymeAdapter.setup.js b/enzymeAdapter.setup.js
deleted file mode 100644
index fc7b0dce1f..0000000000
--- a/enzymeAdapter.setup.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import Enzyme from 'enzyme';
-import Adapter from 'enzyme-adapter-react-16';
-
-Enzyme.configure({ adapter: new Adapter() });
diff --git a/package.json b/package.json
index 8c2574bb0a..385649a779 100644
--- a/package.json
+++ b/package.json
@@ -15,9 +15,9 @@
"@atlaskit/modal-dialog": "^4.0.3",
"@atlaskit/spinner": "^5.0.0",
"@atlaskit/tooltip": "^9.1.4",
- "@babel/cli": "^7.0.0",
"@babel/core": "^7.2.2",
"@babel/plugin-proposal-class-properties": "^7.2.3",
+ "@babel/plugin-transform-runtime": "^7.8.3",
"@babel/polyfill": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
@@ -28,13 +28,17 @@
"@emotion/cache": "^10.0.9",
"@emotion/core": "^10.0.9",
"@emotion/css": "^10.0.9",
+ "@preconstruct/cli": "^1.0.0",
+ "@testing-library/dom": "7.0.4",
+ "@testing-library/jest-dom": "5.1.1",
+ "@testing-library/react": "10.0.1",
+ "@testing-library/user-event": "^10.0.0",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^9.0.0",
"babel-jest": "^23.6.0",
"babel-loader": "^8.0.0",
"babel-plugin-emotion": "^10.0.9",
"bolt-check": "^0.3.0",
- "bundlesize": "^0.17.0",
"chroma-js": "^1.3.6",
"chrono-node": "^1.3.5",
"codesandboxer": "^0.1.1",
@@ -55,21 +59,20 @@
"flow-bin": "^0.91.0",
"gh-pages": "^1.1.0",
"html-webpack-plugin": "^3.2.0",
- "jest": "^24.8.0",
+ "jest": "^25.1.0",
"jest-emotion": "^10.0.11",
"jest-in-case": "^1.0.2",
"memoize-one": "^5.0.0",
"moment": "^2.20.1",
"node-fetch": "^2.5.0",
- "@preconstruct/cli": "^1.0.0",
"pretty-proptypes": "^0.5.0",
"prop-types": "^15.6.0",
"raf": "^3.4.0",
"raf-schd": "^2.1.0",
"raw-loader": "^2.0.0",
- "react": "^16.8.0",
+ "react": "^16.13.0",
"react-codesandboxer": "^2.0.1",
- "react-dom": "^16.8.0",
+ "react-dom": "^16.13.0",
"react-helmet": "^5.2.0",
"react-input-autosize": "^2.2.2",
"react-markings": "^1.3.0",
@@ -96,8 +99,8 @@
"e2e": "concurrently --kill-others --success=first --names 'SERVER,E2E' 'yarn start --progress=false --no-info' 'yarn test:cypress'",
"test:cypress": "cypress run --spec ./cypress/integration/select_spec.js",
"test:cypress-watch": "node ./node_modules/.bin/cypress open",
- "precommit": "flow check && lint-staged",
- "postinstall": "preconstruct dev && bolt-check",
+ "precommit": "flow check",
+ "postinstall": "preconstruct dev",
"changeset": "changeset",
"version-packages": "changeset version",
"release": "yarn build && changeset publish"
@@ -121,8 +124,8 @@
"./node_modules"
],
"testRegex": "src/*(/(__tests?__/)([^_].*/)*?[^_][^/]*?\\.(test|spec)?\\.(js?))$",
- "setupFiles": [
- "./enzymeAdapter.setup.js"
+ "setupFilesAfterEnv": [
+ "./test-setup.js"
],
"snapshotSerializers": [
"jest-emotion"
diff --git a/packages/react-select/CHANGELOG.md b/packages/react-select/CHANGELOG.md
index 85614d4847..25f99b951a 100644
--- a/packages/react-select/CHANGELOG.md
+++ b/packages/react-select/CHANGELOG.md
@@ -1,5 +1,21 @@
# react-select
+## 3.1.0
+
+### Minor Changes
+
+- [4cf6c43c](https://github.com/JedWatson/react-select/commit/4cf6c43cc17a01b043fb60b33cad355d433fdf8c) [#3690](https://github.com/JedWatson/react-select/pull/3690) Thanks [@JedWatson](https://github.com/JedWatson)! - Add `isLoading` prop support to the AsyncSelect component (see #3690)
+
+### Patch Changes
+
+- [83b48de4](https://github.com/JedWatson/react-select/commit/83b48de4a18263b361744fc5e89d9b9845b26e4f) [#3868](https://github.com/JedWatson/react-select/pull/3868) Thanks [@Tirzono](https://github.com/Tirzono)! - Fix for not focusing the selected value when the menu opens
+- [563b046a](https://github.com/JedWatson/react-select/commit/563b046a57a94c47950e62cedc4ce1c489f19f91) [#3794](https://github.com/JedWatson/react-select/pull/3794) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Convert class components that don't have to be class components to function components to reduce bundle size
+- [c7e9c697](https://github.com/JedWatson/react-select/commit/c7e9c697dada15ce3ff9a767bf914ad890080433) [#3682](https://github.com/JedWatson/react-select/pull/3682) Thanks [@JedWatson](https://github.com/JedWatson)! - Allow the input component to be a `textarea` element
+- [3c7de0de](https://github.com/JedWatson/react-select/commit/3c7de0de52826fe74d303a01475c43fe88256156) [#3090](https://github.com/JedWatson/react-select/pull/3090) Thanks [@akiselev](https://github.com/akiselev)! - Add aria attributes to dummy input
+- [d2a820ef](https://github.com/JedWatson/react-select/commit/d2a820efc70835adf864169eebc76947783a15e2) [#3537](https://github.com/JedWatson/react-select/pull/3537) Thanks [@jdelStrother](https://github.com/jdelStrother)! - Fix Flow issues. Refer to the linked PR for more details on the specific issues.
+- [fc52085b](https://github.com/JedWatson/react-select/commit/fc52085b969b1b6f53adf29d52469db9560b828c) [#3662](https://github.com/JedWatson/react-select/pull/3662) Thanks [@eemeli](https://github.com/eemeli)! - Update react-transition-group to ^4.3.0
+- [edb18dd3](https://github.com/JedWatson/react-select/commit/edb18dd3d65b8fbc342bde9e805c5e3293ab6e37) [#3797](https://github.com/JedWatson/react-select/pull/3797) Thanks [@mitchellhamilton](https://github.com/mitchellhamilton)! - Enable Babel loose mode to improve bundle size
+
## 3.0.8
### Patch Changes
diff --git a/packages/react-select/package.json b/packages/react-select/package.json
index 19b96d05fd..cf5a2dc59f 100644
--- a/packages/react-select/package.json
+++ b/packages/react-select/package.json
@@ -1,16 +1,13 @@
{
"name": "react-select",
- "version": "3.0.8",
+ "version": "3.1.0",
"description": "A Select control built with and for ReactJS",
"main": "dist/react-select.cjs.js",
"module": "dist/react-select.esm.js",
"sideEffects": false,
"author": "Jed Watson",
"license": "MIT",
- "repository": {
- "type": "git",
- "url": "https://github.com/JedWatson/react-select.git"
- },
+ "repository": "https://github.com/JedWatson/react-select/tree/master/packages/react-select",
"dependencies": {
"@babel/runtime": "^7.4.4",
"@emotion/cache": "^10.0.9",
@@ -26,8 +23,8 @@
"enzyme": "^3.8.0",
"enzyme-to-json": "^3.3.0",
"jest-in-case": "^1.0.2",
- "react": "^16.8.0",
- "react-dom": "^16.8.0"
+ "react": "^16.13.0",
+ "react-dom": "^16.13.0"
},
"peerDependencies": {
"react": "^16.8.0",
diff --git a/packages/react-select/src/Async.js b/packages/react-select/src/Async.js
index 99cf20c533..26952e2336 100644
--- a/packages/react-select/src/Async.js
+++ b/packages/react-select/src/Async.js
@@ -25,9 +25,6 @@ export type AsyncProps = {
/* Function that returns a promise, which is the set of options to be used
once the promise resolves. */
loadOptions: (string, (OptionsType) => void) => Promise<*> | void,
- /* If cacheOptions is truthy, then the loaded data will be cached. The cache
- will remain until `cacheOptions` changes value. */
- cacheOptions: any,
/* Same behaviour as for Select */
onInputChange?: (string, InputActionMeta) => void,
/* Same behaviour as for Select */
diff --git a/packages/react-select/src/Select.js b/packages/react-select/src/Select.js
index c8d59039d0..4f3ec87b8a 100644
--- a/packages/react-select/src/Select.js
+++ b/packages/react-select/src/Select.js
@@ -243,6 +243,8 @@ export type Props = {
tabSelectsValue: boolean,
/* The value of the select; reflected by the selected option */
value: ValueType,
+ /* Sets the form attribute on the input */
+ form?: string,
};
export const defaultProps = {
@@ -367,6 +369,17 @@ export default class Select extends Component
{
'react-select-' + (this.props.instanceId || ++instanceId);
const selectValue = cleanValue(value);
+
+ this.buildMenuOptions = memoizeOne(
+ this.buildMenuOptions,
+ (newArgs: any, lastArgs: any) => {
+ const [newProps, newSelectValue] = (newArgs: [Props, OptionsType]);
+ const [lastProps, lastSelectValue] = (lastArgs: [Props, OptionsType]);
+
+ return isEqual(newSelectValue, lastSelectValue)
+ && isEqual(newProps.inputValue, lastProps.inputValue)
+ && isEqual(newProps.options, lastProps.options);
+ }).bind(this);
const menuOptions = props.menuIsOpen
? this.buildMenuOptions(props, selectValue)
: { render: [], focusable: [] };
@@ -434,8 +447,8 @@ export default class Select extends Component {
this.scrollToFocusedOptionOnUpdate
) {
scrollIntoView(this.menuListRef, this.focusedOptionRef);
+ this.scrollToFocusedOptionOnUpdate = false;
}
- this.scrollToFocusedOptionOnUpdate = false;
}
componentWillUnmount() {
this.stopListeningComposition();
@@ -483,7 +496,8 @@ export default class Select extends Component {
blur = this.blurInput;
openMenu(focusOption: 'first' | 'last') {
- const { menuOptions, selectValue, isFocused } = this.state;
+ const { selectValue, isFocused } = this.state;
+ const menuOptions = this.buildMenuOptions(this.props, selectValue);
const { isMulti } = this.props;
let openAtIndex =
focusOption === 'first' ? 0 : menuOptions.focusable.length - 1;
@@ -499,13 +513,14 @@ export default class Select extends Component {
this.scrollToFocusedOptionOnUpdate = !(isFocused && this.menuListRef);
this.inputIsHiddenAfterUpdate = false;
- this.onMenuOpen();
this.setState({
+ menuOptions,
focusedValue: null,
focusedOption: menuOptions.focusable[openAtIndex],
+ }, () => {
+ this.onMenuOpen();
+ this.announceAriaLiveContext({ event: 'menu' });
});
-
- this.announceAriaLiveContext({ event: 'menu' });
}
focusValue(direction: 'previous' | 'next') {
const { isMulti, isSearchable } = this.props;
@@ -1278,7 +1293,7 @@ export default class Select extends Component {
// Menu Options
// ==============================
- buildMenuOptions(props: Props, selectValue: OptionsType): MenuOptions {
+ buildMenuOptions = (props: Props, selectValue: OptionsType): MenuOptions => {
const { inputValue = '', options } = props;
const toOption = (option, id) => {
@@ -1397,6 +1412,7 @@ export default class Select extends Component {
inputId,
inputValue,
tabIndex,
+ form,
} = this.props;
const { Input } = this.components;
const { inputIsHidden } = this.state;
@@ -1422,6 +1438,7 @@ export default class Select extends Component {
readOnly
disabled={isDisabled}
tabIndex={tabIndex}
+ form={form}
value=""
{...ariaAttributes}
/>
@@ -1447,6 +1464,7 @@ export default class Select extends Component {
selectProps={selectProps}
spellCheck="false"
tabIndex={tabIndex}
+ form={form}
theme={theme}
type="text"
value={inputValue}
diff --git a/packages/react-select/src/__tests__/Async.test.js b/packages/react-select/src/__tests__/Async.test.js
index e1f6902770..0c52811f68 100644
--- a/packages/react-select/src/__tests__/Async.test.js
+++ b/packages/react-select/src/__tests__/Async.test.js
@@ -1,16 +1,14 @@
import React from 'react';
-import { mount } from 'enzyme';
-import toJson from 'enzyme-to-json';
import cases from 'jest-in-case';
+import { render, fireEvent, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import Async from '../Async';
import { OPTIONS } from './constants';
-import { components } from '../components';
-const { Option } = components;
test('defaults - snapshot', () => {
- const tree = mount();
- expect(toJson(tree)).toMatchSnapshot();
+ const { container } = render();
+ expect(container).toMatchSnapshot();
});
/**
@@ -20,9 +18,16 @@ test('defaults - snapshot', () => {
*/
cases(
'load option prop with defaultOptions true',
- ({ props, expectOptionLength }) => {
- const asyncSelectWrapper = mount();
- expect(asyncSelectWrapper.find(Option).length).toBe(expectOptionLength);
+ async ({ props, expectOptionLength }) => {
+ const { container } = render(
+
+ );
+
+ await waitFor(() => {
+ expect(container.querySelectorAll('.react-select__option').length).toBe(
+ expectOptionLength
+ );
+ });
},
{
'with callback > should resolve options': {
@@ -33,7 +38,6 @@ cases(
expectOptionLength: 1,
},
'with promise > should resolve options': {
- skip: true,
props: {
defaultOptions: true,
loadOptions: () => Promise.resolve([OPTIONS[0]]),
@@ -44,13 +48,15 @@ cases(
);
test('load options prop with defaultOptions true and inputValue prop', () => {
- const loadOptionsSpy = jest.fn((value) => value);
+ const loadOptionsSpy = jest.fn(value => value);
const searchString = 'hello world';
- mount();
+ />
+ );
expect(loadOptionsSpy).toHaveReturnedWith(searchString);
});
@@ -61,18 +67,21 @@ test('load options prop with defaultOptions true and inputValue prop', () => {
*/
cases(
'load options props with no default options',
- ({ props, expectloadOptionsLength }) => {
- let asyncSelectWrapper = mount(
-
- );
- let inputValueWrapper = asyncSelectWrapper.find(
- 'div.react-select__input input'
- );
- asyncSelectWrapper.setProps({ inputValue: 'a' });
- inputValueWrapper.simulate('change', { currentTarget: { value: 'a' } });
- expect(asyncSelectWrapper.find(Option).length).toBe(
- expectloadOptionsLength
+ async ({ props, expectloadOptionsLength }) => {
+ let { container } = render(
+
);
+ let input = container.querySelector('div.react-select__input input');
+ userEvent.type(input, 'a');
+ await waitFor(() => {
+ expect(container.querySelectorAll('.react-select__option').length).toBe(
+ expectloadOptionsLength
+ );
+ });
},
{
'with callback > should resolve the options': {
@@ -82,7 +91,6 @@ cases(
expectloadOptionsLength: 17,
},
'with promise > should resolve the options': {
- skip: true,
props: {
loadOptions: () => Promise.resolve(OPTIONS),
},
@@ -91,43 +99,74 @@ cases(
}
);
-/**
- * Need to update props to trigger on change in input
- * when updating props renders the component therefore options cache is lost thus loadOptions is called again
- */
-test.skip('to not call loadOptions again for same value when cacheOptions is true', () => {
- let loadOptionsSpy = jest.fn();
- let asyncSelectWrapper = mount(
-
- );
- let inputValueWrapper = asyncSelectWrapper.find(
- 'div.react-select__input input'
+test('to not call loadOptions again for same value when cacheOptions is true', () => {
+ let loadOptionsSpy = jest.fn((_, callback) => callback([]));
+ let { container } = render(
+
);
+ let input = container.querySelector('div.react-select__input input');
- asyncSelectWrapper.setProps({ inputValue: 'a' });
- inputValueWrapper.simulate('change', { currentTarget: { value: 'a' } });
- expect(loadOptionsSpy).toHaveBeenCalledTimes(1);
-
- asyncSelectWrapper.setProps({ inputValue: 'b' });
- inputValueWrapper.simulate('change', {
- target: { value: 'b' },
- currentTarget: { value: 'b' },
+ fireEvent.input(input, {
+ target: {
+ value: 'foo',
+ },
+ bubbles: true,
+ cancelable: true,
+ });
+ fireEvent.input(input, {
+ target: {
+ value: 'bar',
+ },
+ bubbles: true,
+ cancelable: true,
+ });
+ fireEvent.input(input, {
+ target: {
+ value: 'foo',
+ },
+ bubbles: true,
+ cancelable: true,
});
- expect(loadOptionsSpy).toHaveBeenCalledTimes(2);
-
- asyncSelectWrapper.setProps({ inputValue: 'b' });
- inputValueWrapper.simulate('change', { currentTarget: { value: 'b' } });
expect(loadOptionsSpy).toHaveBeenCalledTimes(2);
});
-test('to create new cache for each instance', () => {
- const asyncSelectWrapper = mount();
- const instanceOne = asyncSelectWrapper.instance();
+test('to create new cache for each instance', async () => {
+ let loadOptionsOne = jest.fn();
+ let { container: containerOne } = render(
+
+ );
+ userEvent.type(
+ containerOne.querySelector('div.react-select__input input'),
+ 'a'
+ );
- const asyncSelectTwoWrapper = mount();
- const instanceTwo = asyncSelectTwoWrapper.instance();
+ let loadOptionsTwo = jest.fn();
+ let { container: containerTwo } = render(
+
+ );
- expect(instanceOne.optionsCache).not.toBe(instanceTwo.optionsCache);
+ userEvent.type(
+ containerTwo.querySelector('div.react-select__input input'),
+ 'a'
+ );
+
+ expect(loadOptionsOne).toHaveBeenCalled();
+ expect(loadOptionsTwo).toHaveBeenCalled();
});
test('in case of callbacks display the most recently-requested loaded options (if results are returned out of order)', () => {
@@ -135,31 +174,43 @@ test('in case of callbacks display the most recently-requested loaded options (i
const loadOptions = (inputValue, callback) => {
callbacks.push(callback);
};
- let asyncSelectWrapper = mount(
-
- );
- let inputValueWrapper = asyncSelectWrapper.find(
- 'div.react-select__input input'
+ let { container } = render(
+
);
- asyncSelectWrapper.setProps({ inputValue: 'foo' });
- inputValueWrapper.simulate('change', { currentTarget: { value: 'foo' } });
- asyncSelectWrapper.setProps({ inputValue: 'bar' });
- inputValueWrapper.simulate('change', { currentTarget: { value: 'bar' } });
- expect(asyncSelectWrapper.find(Option).exists()).toBeFalsy();
+
+ let input = container.querySelector('div.react-select__input input');
+ fireEvent.input(input, {
+ target: {
+ value: 'foo',
+ },
+ bubbles: true,
+ cancelable: true,
+ });
+ fireEvent.input(input, {
+ target: {
+ value: 'bar',
+ },
+ bubbles: true,
+ cancelable: true,
+ });
+ expect(container.querySelector('.react-select__option')).toBeFalsy();
callbacks[1]([{ value: 'bar', label: 'bar' }]);
callbacks[0]([{ value: 'foo', label: 'foo' }]);
- asyncSelectWrapper.update();
- expect(asyncSelectWrapper.find(Option).text()).toBe('bar');
+ expect(container.querySelector('.react-select__option').textContent).toBe(
+ 'bar'
+ );
});
-/**
- * This throws a jsdom exception
- */
+// QUESTION: we currently do not do this, do we want to?
test.skip('in case of callbacks should handle an error by setting options to an empty array', () => {
const loadOptions = (inputValue, callback) => {
callback(new Error('error'));
};
- let asyncSelectWrapper = mount(
+ let { container } = render(
);
- let inputValueWrapper = asyncSelectWrapper.find(
- 'div.react-select__input input'
- );
- asyncSelectWrapper.setProps({ inputValue: 'foo' });
- inputValueWrapper.simulate('change', { currentTarget: { value: 'foo' } });
- asyncSelectWrapper.update();
- expect(asyncSelectWrapper.find(Option).length).toBe(1);
+ let input = container.querySelector('div.react-select__input input');
+ fireEvent.input(input, {
+ target: {
+ value: 'foo',
+ },
+ bubbles: true,
+ cancelable: true,
+ });
+ expect(container.querySelectorAll('.react-select__option').length).toBe(0);
});
diff --git a/packages/react-select/src/__tests__/AsyncCreatable.test.js b/packages/react-select/src/__tests__/AsyncCreatable.test.js
index 53e9746fc4..8457ff54ee 100644
--- a/packages/react-select/src/__tests__/AsyncCreatable.test.js
+++ b/packages/react-select/src/__tests__/AsyncCreatable.test.js
@@ -1,71 +1,68 @@
import React from 'react';
-import { mount } from 'enzyme';
-import toJson from 'enzyme-to-json';
import AsyncCreatable from '../AsyncCreatable';
-import Select from '../Select';
-import { components } from '../components';
import { OPTIONS } from './constants';
-const { Menu, Option } = components;
+import { render, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
test('defaults - snapshot', () => {
- const tree = mount();
- expect(toJson(tree)).toMatchSnapshot();
+ const { container } = render();
+ expect(container).toMatchSnapshot();
});
test('creates an inner Select', () => {
- const asyncCreatableWrapper = mount(
-
+ const { container } = render(
+
);
- expect(asyncCreatableWrapper.find(Select).exists()).toBeTruthy();
+ expect(container.querySelector('.react-select')).toBeInTheDocument();
});
test('render decorated select with props passed', () => {
- const asyncCreatableWrapper = mount();
- expect(asyncCreatableWrapper.find(Select).props().className).toBe('foo');
+ const { container } = render(
+
+ );
+ expect(container.querySelector('.foo')).toBeInTheDocument();
});
test('to show the create option in menu', () => {
- let asyncCreatableWrapper = mount(
-
+ let { container, rerender } = render(
+
+ );
+ let input = container.querySelector('div.react-select__input input');
+ rerender(
+
);
- let inputValueWrapper = asyncCreatableWrapper.find(
- 'div.react-select__input input'
+ userEvent.type(input, 'a');
+ expect(container.querySelector('.react-select__option').textContent).toBe(
+ 'Create "a"'
);
- asyncCreatableWrapper.setProps({ inputValue: 'a' });
- inputValueWrapper.simulate('change', { currentTarget: { value: 'a' } });
- expect(
- asyncCreatableWrapper
- .find(Option)
- .last()
- .text()
- ).toBe('Create "a"');
});
-test('to show loading and then create option in menu', () => {
- jest.useFakeTimers();
+test('to show loading and then create option in menu', async () => {
let loadOptionsSpy = jest.fn((inputValue, callback) =>
setTimeout(() => callback(OPTIONS), 200)
);
- let asyncCreatableWrapper = mount(
-
- );
- let inputValueWrapper = asyncCreatableWrapper.find(
- 'div.react-select__input input'
+ let { container } = render(
+
);
- asyncCreatableWrapper.setProps({ inputValue: 'a' });
- inputValueWrapper.simulate('change', { currentTarget: { value: 'a' } });
+ let input = container.querySelector('div.react-select__input input');
+ userEvent.type(input, 'a');
// to show a loading message while loading options
- expect(asyncCreatableWrapper.find(Menu).text()).toBe('Loading...');
- jest.runAllTimers();
- asyncCreatableWrapper.update();
-
- // show create options once options are loaded
- expect(
- asyncCreatableWrapper
- .find(Option)
- .last()
- .text()
- ).toBe('Create "a"');
+ expect(container.querySelector('.react-select__menu').textContent).toBe(
+ 'Loading...'
+ );
+ await waitFor(() => {
+ // show create options once options are loaded
+ let options = container.querySelectorAll('.react-select__option');
+ expect(options[options.length - 1].textContent).toBe('Create "a"');
+ });
});
diff --git a/packages/react-select/src/__tests__/Creatable.test.js b/packages/react-select/src/__tests__/Creatable.test.js
index 6c9700ad35..ce41c860f2 100644
--- a/packages/react-select/src/__tests__/Creatable.test.js
+++ b/packages/react-select/src/__tests__/Creatable.test.js
@@ -1,25 +1,35 @@
import React from 'react';
-import { shallow, mount } from 'enzyme';
-import toJson from 'enzyme-to-json';
+import { render, fireEvent } from '@testing-library/react';
import cases from 'jest-in-case';
import Creatable from '../Creatable';
import { OPTIONS } from './constants';
-import { components } from '../components';
-const { Menu, NoOptionsMessage } = components;
+
+const BASIC_PROPS = {
+ className: 'react-select',
+ classNamePrefix: 'react-select',
+ onChange: jest.fn(),
+ onInputChange: jest.fn(),
+ onMenuClose: jest.fn(),
+ onMenuOpen: jest.fn(),
+ name: 'test-input-name',
+ options: OPTIONS,
+};
test('defaults - snapshot', () => {
- const tree = shallow();
- expect(toJson(tree)).toMatchSnapshot();
+ const { container } = render();
+ expect(container).toMatchSnapshot();
});
-cases('filtered option is an exact match for an existing option',
- ({ props = { options: OPTIONS } }) => {
- const creatableSelectWrapper = mount();
- creatableSelectWrapper.setProps({ inputValue: 'one' });
- expect(creatableSelectWrapper.find(Menu).text()).not.toEqual(
- expect.stringContaining('create')
- );
+cases(
+ 'filtered option is an exact match for an existing option',
+ ({ props }) => {
+ props = { ...BASIC_PROPS, ...props };
+ const { container, rerender } = render();
+ rerender();
+ expect(
+ container.querySelector('.react-select__menu').textContent
+ ).not.toEqual(expect.stringContaining('create'));
},
{
'single select > should not show "create..." prompt"': {},
@@ -32,17 +42,28 @@ cases('filtered option is an exact match for an existing option',
}
);
-cases('filterOptions returns invalid value ( null )',
- ({ props = { option: OPTIONS } }) => {
+cases(
+ 'filterOptions returns invalid value ( null )',
+ ({ props }) => {
+ props = { ...BASIC_PROPS, ...props };
let filterOptionSpy = jest.fn().mockReturnValue(null);
- const creatableSelectWrapper = mount(
+
+ const { container, rerender } = render(
);
- creatableSelectWrapper.setProps({ inputValue: 'one' });
- expect(creatableSelectWrapper.find(NoOptionsMessage).exists()).toBeTruthy();
- expect(creatableSelectWrapper.find(Menu).text()).not.toEqual(
- expect.stringContaining('create')
+ rerender(
+
);
+
+ expect(
+ container.querySelector('.react-select__menu-notice--no-options')
+ .textContent
+ ).toEqual(expect.stringContaining('No options'));
},
{
'single select > should not show "create..." prompt"': {},
@@ -55,11 +76,17 @@ cases('filterOptions returns invalid value ( null )',
}
);
-cases('inputValue does not match any option after filter',
- ({ props = { options: OPTIONS } }) => {
- const creatableSelectWrapper = mount();
- creatableSelectWrapper.setProps({ inputValue: 'option not is list' });
- expect(creatableSelectWrapper.find(Menu).text()).toBe(
+cases(
+ 'inputValue does not match any option after filter',
+ ({ props }) => {
+ props = { ...BASIC_PROPS, ...props };
+
+ const { container, rerender } = render();
+ rerender(
+
+ );
+
+ expect(container.querySelector('.react-select__menu').textContent).toBe(
'Create "option not is list"'
);
},
@@ -74,24 +101,48 @@ cases('inputValue does not match any option after filter',
}
);
-cases('isValidNewOption() prop',
- ({ props = { options: OPTIONS } }) => {
+cases(
+ 'isValidNewOption() prop',
+ ({ props }) => {
+ props = { ...BASIC_PROPS, ...props };
let isValidNewOption = jest.fn(options => options === 'new Option');
- const creatableSelectWrapper = mount(
-
+
+ const { container, rerender } = render(
+
);
- creatableSelectWrapper.setProps({ inputValue: 'new Option' });
- expect(creatableSelectWrapper.find(Menu).text()).toEqual(
- expect.stringContaining('Create "new Option"')
+ rerender(
+
);
- expect(creatableSelectWrapper.find(NoOptionsMessage).exists()).toBeFalsy();
- creatableSelectWrapper.setProps({ inputValue: 'invalid new Option' });
- expect(creatableSelectWrapper.find(Menu).text()).not.toEqual(
- expect.stringContaining('Create "invalid new Option"')
+ expect(container.querySelector('.react-select__menu').textContent).toEqual(
+ 'Create "new Option"'
+ );
+
+ expect(
+ container.querySelector('.react-select__menu-notice--no-options')
+ ).toBeFalsy();
+
+ rerender(
+
);
- expect(creatableSelectWrapper.find(NoOptionsMessage).exists()).toBeTruthy();
+ expect(
+ container.querySelector('.react-select__menu').textContent
+ ).not.toEqual('Create "invalid new Option"');
+
+ expect(
+ container.querySelector('.react-select__menu-notice--no-options')
+ ).toBeTruthy();
},
{
'single select > should show "create..." prompt only if isValidNewOption returns thruthy value': {},
@@ -104,18 +155,17 @@ cases('isValidNewOption() prop',
}
);
-cases('close by hitting escape with search text present',
- ({ props = { options: OPTIONS } }) => {
- const creatableSelectWrapper = mount();
- creatableSelectWrapper.setState({ inputValue: 'new Option' });
- creatableSelectWrapper.update();
- creatableSelectWrapper.simulate('keyDown', { keyCode: 27, key: 'Escape' });
- creatableSelectWrapper.update();
- expect(creatableSelectWrapper.state('inputValue')).toBe('');
+cases(
+ 'close by hitting escape with search text present',
+ ({ props }) => {
+ props = { ...BASIC_PROPS, ...props };
+ const { container, rerender } = render();
+ rerender();
+ fireEvent.keyDown(container, { keyCode: 27, key: 'Escape' });
+ expect(container.querySelector('input').textContent).toEqual('');
},
{
- 'single select > should remove the search text': {
- },
+ 'single select > should remove the search text': {},
'multi select > should remove the search text': {
props: {
isMulti: true,
@@ -126,31 +176,40 @@ cases('close by hitting escape with search text present',
);
test('should remove the new option after closing on blur', () => {
- const creatableSelectWrapper = mount(
+ const { container, rerender } = render(
);
- creatableSelectWrapper.setState({ inputValue: 'new Option' });
- creatableSelectWrapper.find('Control input').simulate('blur');
- expect(creatableSelectWrapper.state('inputValue')).toBe('');
+ rerender();
+ fireEvent.blur(container);
+ expect(container.querySelector('input').textContent).toEqual('');
});
-cases('getNewOptionData() prop',
- ({ props = { options: OPTIONS } }) => {
+cases(
+ 'getNewOptionData() prop',
+ ({ props }) => {
+ props = { ...BASIC_PROPS, ...props };
let getNewOptionDataSpy = jest.fn(label => ({
label: `custom text ${label}`,
value: label,
}));
- const creatableSelectWrapper = mount(
-
+ const { container, rerender } = render(
+
+ );
+ rerender(
+
);
- creatableSelectWrapper.setState({ inputValue: 'new Option' });
- expect(creatableSelectWrapper.find(Menu).text()).toEqual(
- expect.stringContaining('custom text new Option')
+
+ expect(container.querySelector('.react-select__menu').textContent).toEqual(
+ 'custom text new Option'
);
},
{
- 'single select > should create option as per label returned from getNewOptionData': {
- },
+ 'single select > should create option as per label returned from getNewOptionData': {},
'multi select > should create option as per label returned from getNewOptionData': {
props: {
isMulti: true,
@@ -160,19 +219,29 @@ cases('getNewOptionData() prop',
}
);
-cases('formatCreateLabel() prop',
+cases(
+ 'formatCreateLabel() prop',
({ props = { options: OPTIONS } }) => {
+ props = { ...BASIC_PROPS, ...props };
let formatCreateLabelSpy = jest.fn(label => `custom label "${label}"`);
- const creatableSelectWrapper = mount(
+ const { container, rerender } = render(
+ );
+
+ rerender(
+
);
- creatableSelectWrapper.setState({ inputValue: 'new Option' });
- expect(creatableSelectWrapper.find(Menu).text()).toEqual(
- expect.stringContaining('custom label "new Option"')
+ expect(container.querySelector('.react-select__menu').textContent).toEqual(
+ 'custom label "new Option"'
);
},
{
diff --git a/packages/react-select/src/__tests__/Select.test.js b/packages/react-select/src/__tests__/Select.test.js
index e61e5967d7..b0ab8d52c6 100644
--- a/packages/react-select/src/__tests__/Select.test.js
+++ b/packages/react-select/src/__tests__/Select.test.js
@@ -1,32 +1,20 @@
import React from 'react';
-import { shallow, mount } from 'enzyme';
-import toJson from 'enzyme-to-json';
+import { render, fireEvent } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import cases from 'jest-in-case';
import {
OPTIONS,
+ OPTIONS_ACCENTED,
OPTIONS_NUMBER_VALUE,
OPTIONS_BOOLEAN_VALUE,
- OPTIONS_DISABLED
+ OPTIONS_DISABLED,
} from './constants';
import Select from '../Select';
-import { components } from '../components';
-
-const {
- ClearIndicator,
- Control,
- DropdownIndicator,
- GroupHeading,
- IndicatorsContainer,
- Input,
- Menu,
- MultiValue,
- NoOptionsMessage,
- Option,
- Placeholder,
- ValueContainer,
- SingleValue,
-} = components;
+
+import { matchers } from 'jest-emotion';
+
+expect.extend(matchers);
const BASIC_PROPS = {
className: 'react-select',
@@ -40,71 +28,68 @@ const BASIC_PROPS = {
};
test('snapshot - defaults', () => {
- const tree = shallow();
- expect(toJson(tree)).toMatchSnapshot();
+ const { container } = render();
+ expect(container).toMatchSnapshot();
});
test('instanceId prop > to have instanceId as id prefix for the select components', () => {
- let selectWrapper = mount(
+ let { container } = render(
);
- expect(selectWrapper.find(Input).props().id).toContain('custom-id');
- selectWrapper.find('div.react-select__option').forEach(opt => {
- expect(opt.props().id).toContain('custom-id');
+ expect(container.querySelector('input').id).toContain('custom-id');
+ container.querySelectorAll('div.react-select__option').forEach(opt => {
+ expect(opt.id).toContain('custom-id');
});
});
test('hidden input field is not present if name is not passes', () => {
- let selectWrapper = mount();
- expect(selectWrapper.find('input[type="hidden"]').exists()).toBeFalsy();
+ let { container } = render();
+ expect(container.querySelector('input[type="hidden"]')).toBeNull();
});
test('hidden input field is present if name passes', () => {
- let selectWrapper = mount(
+ let { container } = render(
);
- expect(selectWrapper.find('input[type="hidden"]').exists()).toBeTruthy();
+ expect(container.querySelector('input[type="hidden"]')).toBeTruthy();
});
test('single select > passing multiple values > should select the first value', () => {
const props = { ...BASIC_PROPS, value: [OPTIONS[0], OPTIONS[4]] };
- let selectWrapper = mount();
- expect(selectWrapper.find(Control).text()).toBe('0');
+ let { container } = render();
+
+ expect(container.querySelector('.react-select__control').textContent).toBe(
+ '0'
+ );
});
-test('isRtl boolean props is passed down to the control component', () => {
- let selectWrapper = mount(
+test('isRtl boolean prop sets direction: rtl on container', () => {
+ let { container } = render(
);
- expect(selectWrapper.props().isRtl).toBe(true);
+ expect(container.firstChild).toHaveStyleRule('direction', 'rtl');
});
test('isOptionSelected() prop > single select > mark value as isSelected if isOptionSelected returns true for the option', () => {
// Select all but option with label '1'
let isOptionSelected = jest.fn(option => option.label !== '1');
- let selectWrapper = mount(
+ let { container } = render(
);
+ let options = container.querySelectorAll('.react-select__option');
+
// Option label 0 to be selected
- expect(
- selectWrapper
- .find(Option)
- .at(0)
- .props().isSelected
- ).toBe(true);
+ expect(options[0].classList).toContain('react-select__option--is-selected');
// Option label 1 to be not selected
- expect(
- selectWrapper
- .find(Option)
- .at(1)
- .props().isSelected
- ).toBe(false);
+ expect(options[1].classList).not.toContain(
+ 'react-select__option--is-selected'
+ );
});
test('isOptionSelected() prop > multi select > to not show the selected options in Menu for multiSelect', () => {
// Select all but option with label '1'
let isOptionSelected = jest.fn(option => option.label !== '1');
- let selectWrapper = mount(
+ let { container } = render(
+
`;
diff --git a/packages/react-select/src/__tests__/__snapshots__/AsyncCreatable.test.js.snap b/packages/react-select/src/__tests__/__snapshots__/AsyncCreatable.test.js.snap
index 70236039c0..23814a323b 100644
--- a/packages/react-select/src/__tests__/__snapshots__/AsyncCreatable.test.js.snap
+++ b/packages/react-select/src/__tests__/__snapshots__/AsyncCreatable.test.js.snap
@@ -136,1035 +136,71 @@ exports[`defaults - snapshot 1`] = `
stroke-width: 0;
}
-