From 8ee7298d9b19584f9d5b60766aeada56534ee1a2 Mon Sep 17 00:00:00 2001 From: Andrew Durber Date: Sat, 5 Oct 2019 17:33:09 +0100 Subject: [PATCH] feat: migrate to typescript BREAKING CHANGE: Large re-write to TypeScript --- .gitignore | 3 +- index.d.ts | 422 -- package.json | 94 +- rollup.config.js | 30 - src/Button.js | 38 - src/Button.tsx | 38 + src/{Filter.js => Filter.tsx} | 73 +- src/{Option.js => Option.tsx} | 155 +- src/{Picky.js => Picky.tsx} | 415 +- src/{Placeholder.js => Placeholder.tsx} | 203 +- src/{SelectAll.js => SelectAll.tsx} | 30 +- .../__tests__/Picky.test.tsx | 92 +- .../__tests__/Placeholder.test.tsx | 271 +- .../__tests__/utils.test.tsx | 20 +- src/index.js | 48 - src/index.ts | 3 + src/lib/debounce.js | 11 - src/lib/debounce.ts | 13 + src/lib/{format.js => format.ts} | 22 +- src/lib/{includes.js => includes.ts} | 8 +- src/lib/utils.js | 58 - src/lib/utils.ts | 96 + src/types.ts | 169 + tests/__fixtures__/countries.js | 536 --- tsconfig.json | 30 + tsdx.config.js | 11 + yarn.lock | 4022 ++++++++++++----- 27 files changed, 3967 insertions(+), 2944 deletions(-) delete mode 100644 index.d.ts delete mode 100644 rollup.config.js delete mode 100644 src/Button.js create mode 100644 src/Button.tsx rename src/{Filter.js => Filter.tsx} (60%) rename src/{Option.js => Option.tsx} (62%) rename src/{Picky.js => Picky.tsx} (57%) rename src/{Placeholder.js => Placeholder.tsx} (71%) rename src/{SelectAll.js => SelectAll.tsx} (69%) rename tests/Picky.test.js => src/__tests__/Picky.test.tsx (95%) rename tests/Placeholder.test.js => src/__tests__/Placeholder.test.tsx (94%) rename tests/utils.test.js => src/__tests__/utils.test.tsx (90%) delete mode 100644 src/index.js create mode 100644 src/index.ts delete mode 100644 src/lib/debounce.js create mode 100644 src/lib/debounce.ts rename src/lib/{format.js => format.ts} (52%) rename src/lib/{includes.js => includes.ts} (70%) delete mode 100644 src/lib/utils.js create mode 100644 src/lib/utils.ts create mode 100644 src/types.ts delete mode 100644 tests/__fixtures__/countries.js create mode 100644 tsconfig.json create mode 100644 tsdx.config.js diff --git a/.gitignore b/.gitignore index bd4ea6e..5163ebf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ coverage/ node_modules/ .rpt2_cache/ *.log -dist/ \ No newline at end of file +dist/ +.rts2_* \ No newline at end of file diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index d1e17ab..0000000 --- a/index.d.ts +++ /dev/null @@ -1,422 +0,0 @@ -declare module 'react-picky' { - import * as React from 'react'; - export type PickyValue = any[] | string | number | Object; - - export type PickyTabIndex = string | number; - - type SelectAllMode = 'default' | 'filtered'; - /** - * Properties returned from render - * - * @export - * @interface RenderProps - */ - export interface RenderProps { - /** - * Index of the item - * - * @type {number} - * @memberof RenderProps - */ - index: number; - - /** - * Option to render - * - * @type {*} - * @memberof RenderProps - */ - item: any; - - /** - * Is option selected - * - * @type {boolean} - * @memberof RenderProps - */ - isSelected: boolean; - - /** - * Callback to select an option - * - * @memberof RenderProps - */ - selectValue: (item: any) => void; - - /** - * Used to determine the label of an option if options supplied are objects - * - * @type {string} - * @memberof RenderProps - */ - labelKey?: string; - - /** - * Used to determine the value of an option if options supplied are objects - * - * @type {string} - * @memberof RenderProps - */ - valueKey?: string; - - /** - * True if Picky allows multiple selection - * - * @type {boolean} - * @memberof RenderProps - */ - multiple?: boolean; - } - - /** - * Props provided to {renderSelectAll} - * - * @export - * @interface RenderSelectAllProps - */ - export interface RenderSelectAllProps { - /** - * True if the current options have been filtered. - * - * @type {boolean} - * @memberof RenderSelectAllProps - */ - filtered: boolean; - - /** - * True of all items are selected - * - * @type {boolean} - * @memberof RenderSelectAllProps - */ - allSelected: boolean; - - /** - * Used to trigger a select all - * - * @memberof RenderSelectAllProps - */ - toggleSelectAll: () => void; - - /** - * Tab index - * - * @type {PickyTabIndex} - * @memberof RenderSelectAllProps - */ - tabIndex: PickyTabIndex; - - /** - * True if supplied to Picky component - * - * @type {boolean} - * @memberof RenderSelectAllProps - */ - multiple: boolean; - } - - /** - * Properties returned from renderList - * - * @export - * @interface RenderListProps - */ - export interface RenderListProps { - /** - * Array of options to render - * - * @type {any[]} - * @memberof RenderListProps - */ - items: any[]; - - /** - * Current selected value(s) - * - * @type {PickyValue} - * @memberof RenderListProps - */ - selected: PickyValue; - - /** - * True if Picky allows multiple selection - * - * @type {boolean} - * @memberof RenderListProps - */ - multiple?: boolean; - - /** - * Utility function for determining whether an option is selected or not - * - * @memberof RenderListProps - */ - getIsSelected: (item: any) => boolean; - - /** - * Callback to select an option - * - * @memberof RenderListProps - */ - selectValue: (item: any) => void; - } - - /** - * All supported props in React Picky - * - * @export - * @interface PickyProps - */ - export interface PickyProps { - /** - * The ID for the component, used for accessibility - * - * @type {string} - * @memberof PickyProps - */ - id: string; - /** - * Default placeholder text - * - * @type {string} - * @memberof PickyProps - */ - placeholder?: string; - - /** - * The value of the Picky. - * Picky is a controlled component so use this in conjunction with onChange and update the value accordingly - * - * @type {PickyValue} - * @memberof PickyProps - */ - value?: PickyValue; - - /** - * The number of items to be displayed before the placeholder turns to "5 selected" - * - * @type {number} [3] - * @memberof PickyProps - */ - numberDisplayed?: number; - - /** - * True if multiple options can be selected - * - * @type {boolean} - * @memberof PickyProps - */ - multiple?: boolean; - - /** - * Options for the Picky component either [1, 2, 3] or [{label: "1", value: 1}] in conjunction with valueKey and labelKey props - * - * @type {any[]} [[]] - * @memberof PickyProps - */ - options: any[]; - - /** - * Called when the selected value changes, use this to re-set the value prop - * - * @memberof PickyProps - */ - onChange: (value: PickyValue) => any; - - /** - * Used to control whether the Picky is open by default - * - * @type {boolean} - * @memberof PickyProps - */ - open?: boolean; - - /** - * True if you want a select all option at the top of the dropdown. - * Won't appear if multiple is false - * - * @type {boolean} - * @memberof PickyProps - */ - includeSelectAll?: boolean; - - /** - * True if you want a filter input at the top of the dropdown, used to filter items. - * - * @type {boolean} - * @memberof PickyProps - */ - includeFilter?: boolean; - - /** - * Used to debounce onFilterChange events. Set value to zero to disable debounce. Duration is in milliseconds. - * - * @type {number} [300] - * @memberof PickyProps - */ - filterDebounce?: number; - - /** - * The max height of the dropdown, height is in px. - * - * @type {number} [300] - * @memberof PickyProps - */ - dropdownHeight?: number; - - /** - * Callback when options have been filtered. - * - * @memberof PickyProps - */ - onFiltered?: (filteredOptions: any[]) => any; - - /** - * Called when dropdown is opened - * - * @memberof PickyProps - */ - onOpen?: () => any; - - /** - * Called when dropdown is closed - * - * @memberof PickyProps - */ - onClose?: () => any; - - /** - * Indicates which key is the value in an object. Used when supplied options are objects. - * - * @type {string} - * @memberof PickyProps - */ - valueKey?: string; - /** - * Indicates which key is the label in an object. Used when supplied options are objects. - * - * @type {string} - * @memberof PickyProps - */ - labelKey?: string; - - /** - * Render prop for individual options - * - * @memberof PickyProps - */ - render?: (props: RenderProps) => any; - - /** - * Tab index for accessibility - * - * @type {PickyTabIndex} [0] - * @memberof PickyProps - */ - tabIndex?: PickyTabIndex; - - /** - * True if the dropdown should be permanently open. - * - * @type {boolean} - * @memberof PickyProps - */ - keepOpen?: boolean; - - /** - * The placeholder when the number of items are higher than {numberDisplayed} and all aren't selected. - * Default "%s selected" where %s is the number of items selected. - * - * @type {string} ["%s selected"] - * @memberof PickyProps - */ - manySelectedPlaceholder?: string; - - /** - * Default "%s selected" where %s is the number of items selected. This gets used when all options are selected. - * - * @type {string} ["%s selected"] - * @memberof PickyProps - */ - allSelectedPlaceholder?: string; - - /** - * Default select all text - * - * @type {string} ["Select all"] - * @memberof PickyProps - */ - selectAllText?: string; - - /** - * Render prop for rendering a custom select all component - * - * @memberof PickyProps - */ - renderSelectAll?: (props: RenderSelectAllProps) => any; - - /** - * If set to true, will focus the filter by default when opened. - * - * @type {boolean} - * @memberof PickyProps - */ - defaultFocusFilter?: boolean; - - /** - * Used to supply a class to the root picky component. Helps when using Picky with a CSS-in-JS library like styled-components - * - * @type {string} - * @memberof PickyProps - */ - className?: string; - - /** - * Render prop for whole list, you can use this to add virtualization/windowing if necessary. - * - * @memberof PickyProps - */ - renderList?: (props: RenderListProps) => any; - - /** - * Override the placeholder of the filter - * - * @type {string} - * @memberof PickyProps - */ - filterPlaceholder?: string; - /** - * Will provide the input value of filter to the picky dropdown, so that if we have a larger list of options then we can only supply the matching options based on this value. - */ - getFilterValue?: (term: string) => any; - /** - * If true options will be returned when they match case, defaults to false - */ - caseSensitiveFilter?: boolean; - - /** - * Pass additional props the the button component - * - * @type {React.DetailedHTMLProps, HTMLButtonElement>} - * @memberof PickyProps - */ - buttonProps?: React.DetailedHTMLProps< - React.ButtonHTMLAttributes, - HTMLButtonElement - >; - - /** - * True if you want a disabled Picky - */ - disabled?: boolean; - - /** - * Allows for additional functionalty with select all and filtering, see the docs. - */ - selectAllMode?: SelectAllMode; - } - - export default class Picky extends React.PureComponent { - render(): JSX.Element; - } -} diff --git a/package.json b/package.json index 20bcd41..3872782 100644 --- a/package.json +++ b/package.json @@ -2,98 +2,72 @@ "name": "react-picky", "version": "4.6.0", "main": "dist/index.js", - "typings": "./index.d.ts", + "module": "dist/picky.esm.js", + "typings": "dist/index.d.ts", "license": "MIT", "files": [ "/dist", "./index.d.ts" ], "scripts": { - "start": "yarn --cwd example start", "prepare-example": "yarn --cwd example install", - "build": "cross-env NODE_ENV=production rollup -c", - "prettier": "prettier --write {src,tests}{/**/,/}*.js", "dev": "rollup -c -w", - "dev:production": "cross-env NODE_ENV=production rollup -c -w", - "pretest": "npm run build", - "lint": "eslint src/**/*.js", - "test": "cross-env NODE_ENV=test jest", - "test:watch": "cross-env NODE_ENV=test jest --watch", + "start": "concurrently \"tsdx watch\" \"yarn start:example\"", + "start:example": "yarn --cwd example start", + "build": "tsdx build", + "test": "tsdx test --env=jsdom", + "lint": "tsdx lint", "semantic-release": "semantic-release" }, "dependencies": { "recompose": "^0.30.0" }, "devDependencies": { - "@babel/core": "^7.4.3", - "@babel/plugin-proposal-object-rest-spread": "^7.4.3", - "@babel/preset-env": "^7.4.3", + "@babel/core": "^7.6.2", "@babel/preset-react": "^7.0.0", "@commitlint/cli": "^8.2.0", "@commitlint/config-conventional": "^8.2.0", "@semantic-release/changelog": "^3.0.4", "@semantic-release/git": "^7.0.16", - "babel-core": "^7.0.0-bridge.0", - "babel-eslint": "^10.0.3", - "babel-jest": "^24.7.1", - "babel-plugin-react-remove-properties": "^0.3.0", - "babel-plugin-transform-react-remove-prop-types": "^0.4.24", + "@testing-library/react": "^9.3.0", + "@types/enzyme": "^3.10.3", + "@types/jest": "^24.0.18", + "@types/react": "^16.9.5", + "@types/react-dom": "^16.9.1", + "@types/recompose": "^0.30.7", + "concurrently": "^4.1.2", "cross-env": "^5.2.0", "cz-conventional-changelog": "^2.1.0", "enzyme": "^3.9.0", - "enzyme-adapter-react-16": "^1.12.1", - "eslint": "^6.5.1", - "eslint-config-prettier": "^6.3.0", - "eslint-config-react-app": "^5.0.2", - "eslint-plugin-import": "^2.18.2", - "eslint-plugin-jsx-a11y": "^6.2.3", - "eslint-plugin-react": "^7.16.0", - "husky": "^3.0.0", - "jest": "^24.7.1", - "prettier": "^1.17.0", + "enzyme-adapter-react-16": "^1.14.0", + "husky": "^3.0.8", + "identity-obj-proxy": "^3.0.0", + "prettier": "^1.18.2", "prettier-eslint": "^9.0.0", "pretty-quick": "^1.11.1", - "react": "^16.8.6", - "react-dom": "^16.8.6", - "react-testing-library": "^6.1.2", - "rollup": "^1.10.0", - "rollup-plugin-babel": "^4.3.2", - "rollup-plugin-commonjs": "^9.3.4", + "react": "^16.10.2", + "react-dom": "^16.10.2", "rollup-plugin-css-only": "^1.0.0", - "rollup-plugin-filesize": "^6.1.1", - "rollup-plugin-jsx": "^1.0.3", - "rollup-plugin-node-resolve": "^4.2.3", - "rollup-plugin-uglify": "^6.0.2", - "semantic-release": "^15.13.3" + "rollup-plugin-filesize": "^6.2.0", + "semantic-release": "^15.13.24", + "tsdx": "^0.9.3", + "tslib": "^1.10.0", + "typescript": "^3.6.3" + }, + "peerDependencies": { + "prop-types": "15.7.2", + "react": "> 16.3.0", + "react-dom": "> 16.3.0" }, "jest": { - "collectCoverage": false, - "modulePaths": [ - "./src" - ], - "moduleFileExtensions": [ - "js", - "jsx" - ], "setupFiles": [ "./tests/helpers/setup.js" ], - "collectCoverageFrom": [ - "src/**/*.js", - "!src/index.js", - "!tests/**" - ], - "transform": { - "^.+\\.jsx?$": "babel-jest", - "^.+\\.css$": "/config/jest/cssTransform.js", - "^(?!.*\\.(js|jsx|css|json)$)": "/config/jest/fileTransform.js" + "moduleNameMapper": { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", + "\\.(css|less)$": "identity-obj-proxy" } }, - "peerDependencies": { - "prop-types": "> 15.6.0", - "react": "> 16.3.0", - "react-dom": "> 16.3.0" - }, "repository": { "type": "git", "url": "https://github.com/Aidurber/react-picky.git" diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index cbe5656..0000000 --- a/rollup.config.js +++ /dev/null @@ -1,30 +0,0 @@ -import babel from 'rollup-plugin-babel'; -import { uglify } from 'rollup-plugin-uglify'; -import filesize from 'rollup-plugin-filesize'; -import css from 'rollup-plugin-css-only'; -import resolve from 'rollup-plugin-node-resolve'; -import commonjs from 'rollup-plugin-commonjs'; - -const isProduction = process.env.NODE_ENV === 'production'; - -export default { - input: 'src/index.js', - output: { - file: 'dist/index.js', - format: 'cjs', - }, - external: ['react', 'prop-types', 'react-dom', 'lodash.isequal'], - plugins: [ - css({ output: 'dist/picky.css' }), - resolve(), - babel({ - exclude: 'node_modules/**', - externalHelpers: true, - }), - commonjs({ - ignore: ['node_modules/prop-types'], - }), - isProduction && uglify(), - isProduction && filesize(), - ], -}; diff --git a/src/Button.js b/src/Button.js deleted file mode 100644 index 1d93ed7..0000000 --- a/src/Button.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { onlyUpdateForKeys } from 'recompose'; -function Button({ id, disabled, onClick, children, className, ...rest }) { - const buttonId = `${id}__button`; - const classes = [ - 'picky__input', - disabled ? 'picky__input--disabled' : '', - className - ].join(' '); - - return ( - - ); -} - -Button.propTypes = { - id: PropTypes.string.isRequired, - disabled: PropTypes.bool, - onClick: PropTypes.func.isRequired, - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node, - ]).isRequired, - className:PropTypes.string -}; -Button.displayName = 'Button'; -export default onlyUpdateForKeys(['disabled', 'children'])(Button); diff --git a/src/Button.tsx b/src/Button.tsx new file mode 100644 index 0000000..f3a521a --- /dev/null +++ b/src/Button.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; + +type ButtonProps = {} & React.DetailedHTMLProps< + React.ButtonHTMLAttributes, + HTMLButtonElement +>; +const Button: React.FC = ({ + id, + disabled, + onClick, + children, + className, + ...rest +}) => { + const buttonId = `${id}__button`; + const classes = [ + 'picky__input', + disabled ? 'picky__input--disabled' : '', + className, + ].join(' '); + + return ( + + ); +}; + +Button.displayName = 'Picky(Button)'; +export default Button; diff --git a/src/Filter.js b/src/Filter.tsx similarity index 60% rename from src/Filter.js rename to src/Filter.tsx index 16abf1d..e7f0674 100644 --- a/src/Filter.js +++ b/src/Filter.tsx @@ -1,36 +1,37 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -class Filter extends Component { - shouldComponentUpdate(nextProps) { - return ( - this.props.placeholder !== nextProps.placeholder || - this.props.tabIndex !== nextProps.tabIndex - ); - } - render() { - return ( -
- (this.filterInput = input)} - className="picky__filter__input" - data-testid="picky__filter__input" - placeholder={this.props.placeholder} - tabIndex={this.props.tabIndex} - aria-label="filter options" - onChange={e => this.props.onFilterChange(e.target.value)} - /> -
- ); - } -} -Filter.defaultProps = { - placeholder: 'Filter...', -}; -Filter.propTypes = { - onFilterChange: PropTypes.func.isRequired, - tabIndex: PropTypes.number, - placeholder: PropTypes.string, -}; - -export default Filter; +import * as React from 'react'; +export type FilterProps = { + onFilterChange(term: string): void; + tabIndex: number | undefined; + placeholder?: string; +}; +class Filter extends React.Component { + filterInput: HTMLInputElement | null = null; + static displayName = 'Picky(Filter)'; + static defaultProps = { + placeholder: 'Filter...', + }; + shouldComponentUpdate(nextProps: FilterProps) { + return ( + this.props.placeholder !== nextProps.placeholder || + this.props.tabIndex !== nextProps.tabIndex + ); + } + render() { + return ( +
+ (this.filterInput = input)} + className="picky__filter__input" + data-testid="picky__filter__input" + placeholder={this.props.placeholder} + tabIndex={this.props.tabIndex} + aria-label="filter options" + onChange={e => this.props.onFilterChange(e.target.value)} + /> +
+ ); + } +} + +export default Filter; diff --git a/src/Option.js b/src/Option.tsx similarity index 62% rename from src/Option.js rename to src/Option.tsx index 9d1965a..6403e7f 100644 --- a/src/Option.js +++ b/src/Option.tsx @@ -1,77 +1,78 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { onlyUpdateForKeys } from 'recompose'; -import { isDataObject } from './lib/utils'; -const Option = props => { - const { - id, - item, - isSelected, - labelKey, - valueKey, - selectValue, - style, - multiple, - tabIndex, - disabled, - } = props; - const cssClass = isSelected ? 'option selected' : 'option'; - const body = isDataObject(item, labelKey, valueKey) ? item[labelKey] : item; - const inputType = multiple ? 'checkbox' : 'radio'; - const select = () => !disabled && selectValue(item); - - return ( -
{ - e.preventDefault(); - if (!disabled) { - selectValue(item); - } - }} - > - - {body} -
- ); -}; - -Option.propTypes = { - isSelected: PropTypes.bool, - valueKey: PropTypes.string, - labelKey: PropTypes.string, - id: PropTypes.string, - item: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - PropTypes.object, - ]).isRequired, - style: PropTypes.object, - selectValue: PropTypes.func.isRequired, - multiple: PropTypes.bool, - tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - disabled: PropTypes.bool, -}; -export default onlyUpdateForKeys([ - 'multiple', - 'isSelected', - 'id', - 'item', - 'tabIndex', -])(Option); +import * as React from 'react'; +import { onlyUpdateForKeys } from 'recompose'; +import { isDataObject } from './lib/utils'; +import { OptionType, ComplexOptionType } from './types'; + +type OptionProps = { + isSelected: boolean; + valueKey?: string; + labelKey?: string; + id: string; + item: OptionType; + style?: React.CSSProperties; + selectValue(option: OptionType): void; + multiple: boolean; + tabIndex: number | undefined; + disabled: boolean; +}; +const Option: React.FC = props => { + const { + id, + item, + isSelected, + labelKey, + valueKey, + selectValue, + style, + multiple, + tabIndex, + disabled, + } = props; + const cssClass = isSelected ? 'option selected' : 'option'; + const body = isDataObject(item, labelKey, valueKey) + ? (item as ComplexOptionType)[labelKey!] + : item; + const inputType = multiple ? 'checkbox' : 'radio'; + const select = () => !disabled && selectValue(item); + + return ( +
{ + e.preventDefault(); + if (!disabled) { + selectValue(item); + } + }} + > + + {body} +
+ ); +}; + +Option.displayName = 'Picky(Option)'; + +export default onlyUpdateForKeys([ + 'multiple', + 'isSelected', + 'id', + 'item', + 'tabIndex', +])(Option); diff --git a/src/Picky.js b/src/Picky.tsx similarity index 57% rename from src/Picky.js rename to src/Picky.tsx index 2f042e2..c2c2507 100644 --- a/src/Picky.js +++ b/src/Picky.tsx @@ -1,7 +1,6 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import debounce from './lib/debounce'; -import includes from './lib/includes'; +import * as React from 'react'; +import { debounce } from './lib/debounce'; +import { includes } from './lib/includes'; import { isDataObject, hasItem, @@ -16,9 +15,288 @@ import Option from './Option'; import './Picky.css'; import SelectAll from './SelectAll'; import Button from './Button'; +import { + RenderListProps, + SelectAllMode, + RenderSelectAllProps, + RenderProps, + OptionsType, + OptionType, + ComplexOptionType, +} from './types'; + +type PickyState = { + selectedValue: OptionsType | OptionType | null; + open?: boolean; + filtered?: boolean; + filteredOptions: OptionsType; + allSelected: boolean; +}; + +type PickyProps = { + /** + * The ID for the component, used for accessibility + * + * @type {string} + * @memberof PickyProps + */ + id: string; + /** + * Default placeholder text + * + * @type {string} + * @memberof PickyProps + */ + placeholder?: string; + + /** + * The value of the Picky. + * Picky is a controlled component so use this in conjunction with onChange and update the value accordingly + * + * @type {PickyValue} + * @memberof PickyProps + */ + value?: OptionsType | OptionType; + + /** + * The number of items to be displayed before the placeholder turns to "5 selected" + * + * @type {number} [3] + * @memberof PickyProps + */ + numberDisplayed?: number; + + /** + * True if multiple options can be selected + * + * @type {boolean} + * @memberof PickyProps + */ + multiple?: boolean; + + /** + * Options for the Picky component either [1, 2, 3] or [{label: "1", value: 1}] in conjunction with valueKey and labelKey props + * + * @type {any[]} [[]] + * @memberof PickyProps + */ + options: any[]; + + /** + * Called when the selected value changes, use this to re-set the value prop + * + * @memberof PickyProps + */ + onChange: (value: OptionsType | OptionType) => any; + + /** + * Used to control whether the Picky is open by default + * + * @type {boolean} + * @memberof PickyProps + */ + open?: boolean; -class Picky extends React.PureComponent { - constructor(props) { + /** + * True if you want a select all option at the top of the dropdown. + * Won't appear if multiple is false + * + * @type {boolean} + * @memberof PickyProps + */ + includeSelectAll?: boolean; + + /** + * True if you want a filter input at the top of the dropdown, used to filter items. + * + * @type {boolean} + * @memberof PickyProps + */ + includeFilter?: boolean; + + /** + * Used to debounce onFilterChange events. Set value to zero to disable debounce. Duration is in milliseconds. + * + * @type {number} [300] + * @memberof PickyProps + */ + filterDebounce?: number; + + /** + * The max height of the dropdown, height is in px. + * + * @type {number} [300] + * @memberof PickyProps + */ + dropdownHeight?: number; + + /** + * Callback when options have been filtered. + * + * @memberof PickyProps + */ + onFiltered?: (filteredOptions: any[]) => any; + + /** + * Called when dropdown is opened + * + * @memberof PickyProps + */ + onOpen?: () => any; + + /** + * Called when dropdown is closed + * + * @memberof PickyProps + */ + onClose?: () => any; + + /** + * Indicates which key is the value in an object. Used when supplied options are objects. + * + * @type {string} + * @memberof PickyProps + */ + valueKey?: string; + /** + * Indicates which key is the label in an object. Used when supplied options are objects. + * + * @type {string} + * @memberof PickyProps + */ + labelKey?: string; + + /** + * Render prop for individual options + * + * @memberof PickyProps + */ + render?: (props: RenderProps) => any; + + /** + * Tab index for accessibility + * + * @type {PickyTabIndex} [0] + * @memberof PickyProps + */ + tabIndex?: number | undefined; + + /** + * True if the dropdown should be permanently open. + * + * @type {boolean} + * @memberof PickyProps + */ + keepOpen?: boolean; + + /** + * The placeholder when the number of items are higher than {numberDisplayed} and all aren't selected. + * Default "%s selected" where %s is the number of items selected. + * + * @type {string} ["%s selected"] + * @memberof PickyProps + */ + manySelectedPlaceholder?: string; + + /** + * Default "%s selected" where %s is the number of items selected. This gets used when all options are selected. + * + * @type {string} ["%s selected"] + * @memberof PickyProps + */ + allSelectedPlaceholder?: string; + + /** + * Default select all text + * + * @type {string} ["Select all"] + * @memberof PickyProps + */ + selectAllText?: string; + + /** + * Render prop for rendering a custom select all component + * + * @memberof PickyProps + */ + renderSelectAll?: (props: RenderSelectAllProps) => any; + + /** + * If set to true, will focus the filter by default when opened. + * + * @type {boolean} + * @memberof PickyProps + */ + defaultFocusFilter?: boolean; + + /** + * Used to supply a class to the root picky component. Helps when using Picky with a CSS-in-JS library like styled-components + * + * @type {string} + * @memberof PickyProps + */ + className?: string; + + /** + * Render prop for whole list, you can use this to add virtualization/windowing if necessary. + * + * @memberof PickyProps + */ + renderList?: (props: RenderListProps) => any; + + /** + * Override the placeholder of the filter + * + * @type {string} + * @memberof PickyProps + */ + filterPlaceholder?: string; + /** + * Will provide the input value of filter to the picky dropdown, so that if we have a larger list of options then we can only supply the matching options based on this value. + */ + getFilterValue?: (term: string) => any; + /** + * If true options will be returned when they match case, defaults to false + */ + caseSensitiveFilter?: boolean; + + /** + * Pass additional props the the button component + * + * @type {React.DetailedHTMLProps, HTMLButtonElement>} + * @memberof PickyProps + */ + buttonProps?: React.DetailedHTMLProps< + React.ButtonHTMLAttributes, + HTMLButtonElement + >; + + /** + * True if you want a disabled Picky + */ + disabled?: boolean; + + /** + * Allows for additional functionalty with select all and filtering, see the docs. + */ + selectAllMode?: SelectAllMode; +}; + +class Picky extends React.PureComponent { + static defaultProps = { + numberDisplayed: 3, + options: [], + filterDebounce: 150, + dropdownHeight: 300, + onChange: () => {}, + tabIndex: 0, + keepOpen: true, + selectAllText: 'Select all', + selectAllMode: 'default', + }; + node: HTMLDivElement | null = null; + filter: Filter | null = null; + constructor(props: PickyProps) { super(props); this.state = { selectedValue: props.value || (props.multiple ? [] : null), @@ -44,33 +322,33 @@ class Picky extends React.PureComponent { } componentDidMount() { - this.focusFilterInput(this.state.open); + this.focusFilterInput(!!this.state.open); } componentWillUnmount() { document.removeEventListener('click', this.handleOutsideClick, false); } - UNSAFE_componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps: PickyProps) { if ( this.props.options !== nextProps.options || this.props.value !== nextProps.value ) { let valuesEqual = Array.isArray(nextProps.value) - ? arraysEqual(nextProps.value, this.props.value) + ? arraysEqual(nextProps.value, this.props.value as OptionsType) : nextProps.value === this.props.value; let optsEqual = arraysEqual(nextProps.options, this.props.options); this.setState({ allSelected: !(valuesEqual && optsEqual) - ? this.allSelected(nextProps.value, nextProps.options) + ? this.allSelected(nextProps.value as OptionsType, nextProps.options) : this.allSelected(), }); } } - selectValue(val) { + selectValue(val: string | number) { const valueLookup = this.props.value; if (this.props.multiple && Array.isArray(valueLookup)) { const itemIndex = hasItemIndex( @@ -80,14 +358,14 @@ class Picky extends React.PureComponent { this.props.labelKey ); - let selectedValue = []; + let selectedValue: OptionsType = []; if (itemIndex > -1) { selectedValue = [ ...valueLookup.slice(0, itemIndex), ...valueLookup.slice(itemIndex + 1), ]; } else { - selectedValue = [...this.props.value, val]; + selectedValue = [...(this.props.value as OptionsType), val]; } this.setState( { @@ -108,9 +386,9 @@ class Picky extends React.PureComponent { * @returns * @memberof Picky */ - getValue(option) { + getValue(option: OptionType) { return typeof this.props.valueKey !== 'undefined' - ? option[this.props.valueKey] + ? (option as ComplexOptionType)[this.props.valueKey] : option; } /** @@ -119,7 +397,7 @@ class Picky extends React.PureComponent { * @returns {Boolean} * @memberof Picky */ - allSelected(overrideSelected, overrideOptions) { + allSelected(overrideSelected?: any[], overrideOptions?: any[]) { const { value, options } = this.props; const selectedValue = overrideSelected || value; const selectedOptions = overrideOptions || options; @@ -162,13 +440,13 @@ class Picky extends React.PureComponent { ); } - isItemSelected(item) { + isItemSelected(item: OptionType): boolean { return hasItem( this.props.value, item, this.props.valueKey, this.props.labelKey - ); + ) as boolean; } renderOptions() { @@ -223,9 +501,9 @@ class Picky extends React.PureComponent { selectValue={this.selectValue} labelKey={labelKey} valueKey={valueKey} - multiple={multiple} + multiple={Boolean(multiple)} tabIndex={tabIndex} - disabled={disabled} + disabled={Boolean(disabled)} id={this.props.id + '-option-' + index} /> ); @@ -239,7 +517,7 @@ class Picky extends React.PureComponent { * @returns * @memberof Picky */ - onFilterChange(term) { + onFilterChange(term: string) { /** * getFilterValue function will provide the input value of filter to the picky dropdown, so that if we have a larger list of options then we can only supply the matching options based on this value */ @@ -260,7 +538,7 @@ class Picky extends React.PureComponent { const filteredOptions = this.props.options.filter(option => { if (isObject) { return includes( - option[this.props.labelKey], + option[this.props.labelKey!], term, this.props.caseSensitiveFilter ); @@ -286,7 +564,7 @@ class Picky extends React.PureComponent { * @returns * @memberof Picky */ - handleOutsideClick(e) { + handleOutsideClick(e: any) { // If keep open then don't toggle dropdown // If radio and not keepOpen then auto close it on selecting a value // If radio and click to the filter input then don't toggle dropdown @@ -304,7 +582,7 @@ class Picky extends React.PureComponent { this.toggleDropDown(); } - focusFilterInput(isOpen) { + focusFilterInput(isOpen: boolean) { if (isOpen && this.props.defaultFocusFilter) { if (this.filter && this.filter.filterInput) { this.filter.filterInput.focus(); @@ -334,7 +612,7 @@ class Picky extends React.PureComponent { }; }, () => { - const isOpen = this.state.open; + const isOpen = !!this.state.open; // Prop callbacks this.focusFilterInput(isOpen); if (isOpen && this.props.onOpen) { @@ -348,19 +626,20 @@ class Picky extends React.PureComponent { get filterDebounce() { const { filterDebounce } = this.props; - return filterDebounce > 0 - ? debounce(this.onFilterChange, filterDebounce) + const amount = filterDebounce || 0; + return (amount || 0) > 0 + ? debounce(this.onFilterChange, amount) : this.onFilterChange; } - get showSelectAll() { + get showSelectAll(): boolean { const { renderSelectAll, multiple, includeSelectAll } = this.props; - return ( + return Boolean( !renderSelectAll && - includeSelectAll && - multiple && - ((this.props.selectAllMode === 'default' && !this.state.filtered) || - this.props.selectAllMode === 'filtered') + includeSelectAll && + multiple && + ((this.props.selectAllMode === 'default' && !this.state.filtered) || + this.props.selectAllMode === 'filtered') ); } render() { @@ -386,7 +665,10 @@ class Picky extends React.PureComponent { ariaOwns += this.props.id + '-list'; } const buttonId = `${this.props.id}__button`; - const dropdownStyle = { maxHeight: dropdownHeight, overflowY: 'scroll' }; + const dropdownStyle: React.CSSProperties = { + maxHeight: dropdownHeight, + overflowY: 'scroll', + }; return (
{ @@ -413,8 +695,8 @@ class Picky extends React.PureComponent { manySelectedPlaceholder={this.props.manySelectedPlaceholder} allSelectedPlaceholder={this.props.allSelectedPlaceholder} value={value} - multiple={multiple} - numberDisplayed={numberDisplayed} + multiple={Boolean(multiple)} + numberDisplayed={numberDisplayed!} valueKey={valueKey} labelKey={labelKey} data-testid="placeholder-component" @@ -429,6 +711,7 @@ class Picky extends React.PureComponent { > {includeFilter && ( (this.filter = filter)} placeholder={filterPlaceholder} onFilterChange={this.filterDebounce} @@ -436,18 +719,18 @@ class Picky extends React.PureComponent { )} {renderSelectAll ? ( renderSelectAll({ - filtered: this.state.filtered, + filtered: Boolean(this.state.filtered), allSelected: this.state.allSelected, toggleSelectAll: this.toggleSelectAll, tabIndex, - multiple, - disabled, + multiple: Boolean(multiple), + disabled: Boolean(disabled), }) ) : ( {}, - tabIndex: 0, - keepOpen: true, - selectAllText: 'Select all', - selectAllMode: 'default', -}; -Picky.propTypes = { - id: PropTypes.string.isRequired, - placeholder: PropTypes.string, - value: PropTypes.oneOfType([ - PropTypes.array, - PropTypes.string, - PropTypes.number, - PropTypes.object, - ]), - numberDisplayed: PropTypes.number, - multiple: PropTypes.bool, - options: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, - open: PropTypes.bool, - includeSelectAll: PropTypes.bool, - includeFilter: PropTypes.bool, - filterDebounce: PropTypes.number, - dropdownHeight: PropTypes.number, - onFiltered: PropTypes.func, - onOpen: PropTypes.func, - onClose: PropTypes.func, - valueKey: PropTypes.string, - labelKey: PropTypes.string, - render: PropTypes.func, - tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - keepOpen: PropTypes.bool, - manySelectedPlaceholder: PropTypes.string, - allSelectedPlaceholder: PropTypes.string, - selectAllText: PropTypes.string, - renderSelectAll: PropTypes.func, - defaultFocusFilter: PropTypes.bool, - className: PropTypes.string, - renderList: PropTypes.func, - filterPlaceholder: PropTypes.string, - disabled: PropTypes.bool, - getFilterValue: PropTypes.func, - caseSensitiveFilter: PropTypes.bool, - buttonProps: PropTypes.object, - selectAllMode: PropTypes.oneOf(['default', 'filtered']), -}; - export default Picky; diff --git a/src/Placeholder.js b/src/Placeholder.tsx similarity index 71% rename from src/Placeholder.js rename to src/Placeholder.tsx index e11b54c..7da3ca9 100644 --- a/src/Placeholder.js +++ b/src/Placeholder.tsx @@ -1,101 +1,102 @@ -// NEEDS REFACTOR -import React from 'react'; -import PropTypes from 'prop-types'; -import format from './lib/format'; -import { isDataObject } from './lib/utils'; -import includes from './lib/includes'; -import { onlyUpdateForKeys } from 'recompose'; -const isEmptyValue = value => - value === null || - value === undefined || - (Array.isArray(value) && !value.length); - -const Placeholder = ({ - placeholder, - value, - numberDisplayed, - multiple, - valueKey, - labelKey, - manySelectedPlaceholder, - allSelectedPlaceholder, - allSelected, -}) => { - let message = ''; - if (isEmptyValue(value)) { - message = placeholder; - } else { - if (Array.isArray(value) && multiple) { - // If type is array and values length less than number displayed - // join the values - if (value.length <= numberDisplayed) { - message = value - .map(opt => { - if (isDataObject(opt, valueKey, labelKey)) { - return opt[labelKey]; - } - return opt; - }) - .join(', '); - } else { - // if many selected and not all selected then use the placeholder - if (manySelectedPlaceholder && !allSelected) { - // if it doesn't include the sprintf token then just use the placeholder - message = includes(manySelectedPlaceholder, '%s') - ? format(manySelectedPlaceholder, value.length) - : manySelectedPlaceholder; - //If all selected and there is an allselectedplaceholder use that - } else if (allSelected && allSelectedPlaceholder) { - // if it doesn't include the sprintf token then just use the placeholder - message = includes(allSelectedPlaceholder, '%s') - ? format(allSelectedPlaceholder, value.length) - : allSelectedPlaceholder; - } - } - } else { - let tempValue = Array.isArray(value) ? value[0] : value; - if (isDataObject(tempValue, valueKey, labelKey)) { - message = tempValue[labelKey]; - } else { - message = tempValue; - } - } - } - - return ( - - {message} - - ); -}; - -Placeholder.defaultProps = { - placeholder: 'None selected', - allSelectedPlaceholder: '%s selected', - manySelectedPlaceholder: '%s selected', - allSelected: false, -}; -Placeholder.propTypes = { - placeholder: PropTypes.string, - value: PropTypes.oneOfType([ - PropTypes.array, - PropTypes.string, - PropTypes.number, - PropTypes.object, - ]), - numberDisplayed: PropTypes.number, - multiple: PropTypes.bool, - valueKey: PropTypes.string, - labelKey: PropTypes.string, - manySelectedPlaceholder: PropTypes.string, - allSelectedPlaceholder: PropTypes.string, - allSelected: PropTypes.bool, -}; - -export default onlyUpdateForKeys([ - 'multiple', - 'value', - 'numberDisplayed', - 'allSelected', - 'allSelectedPlaceholder', -])(Placeholder); +// NEEDS REFACTOR +import * as React from 'react'; +import { onlyUpdateForKeys } from 'recompose'; +import { format } from './lib/format'; +import { isDataObject } from './lib/utils'; +import { includes } from './lib/includes'; +import { + OptionsType, + OptionType, + ComplexOptionType, + SimpleOptionType, +} from './types'; + +const isEmptyValue = (value: any) => + value === null || + value === undefined || + (Array.isArray(value) && !value.length); + +type PlaceholderProps = { + placeholder?: string; + value: OptionsType | OptionType | undefined; + numberDisplayed: number; + multiple: boolean; + valueKey?: string; + labelKey?: string; + manySelectedPlaceholder?: string; + allSelectedPlaceholder?: string; + allSelected: boolean; +}; +const Placeholder: React.FC = ({ + placeholder, + value, + numberDisplayed, + multiple, + valueKey, + labelKey, + manySelectedPlaceholder, + allSelectedPlaceholder, + allSelected, +}) => { + let message: string = ''; + if (isEmptyValue(value)) { + message = placeholder || ''; + } else { + if (Array.isArray(value) && multiple) { + // If type is array and values length less than number displayed + // join the values + if (value.length <= numberDisplayed) { + message = value + .map(opt => { + if (isDataObject(opt, valueKey, labelKey)) { + return (opt as ComplexOptionType)[labelKey!]; + } + return opt; + }) + .join(', '); + } else { + // if many selected and not all selected then use the placeholder + if (manySelectedPlaceholder && !allSelected) { + // if it doesn't include the sprintf token then just use the placeholder + message = includes(manySelectedPlaceholder, '%s') + ? format(manySelectedPlaceholder, value.length) + : manySelectedPlaceholder; + //If all selected and there is an allselectedplaceholder use that + } else if (allSelected && allSelectedPlaceholder) { + // if it doesn't include the sprintf token then just use the placeholder + message = includes(allSelectedPlaceholder, '%s') + ? format(allSelectedPlaceholder, value.length) + : allSelectedPlaceholder; + } + } + } else { + let tempValue = Array.isArray(value) ? value[0] : value; + if (isDataObject(tempValue, valueKey, labelKey)) { + message = (tempValue as ComplexOptionType)[labelKey!]; + } else { + message = String(tempValue as SimpleOptionType); + } + } + } + + return ( + + {message} + + ); +}; + +Placeholder.defaultProps = { + placeholder: 'None selected', + allSelectedPlaceholder: '%s selected', + manySelectedPlaceholder: '%s selected', + allSelected: false, +}; +Placeholder.displayName = 'Picky(Placeholder)'; +export default onlyUpdateForKeys([ + 'multiple', + 'value', + 'numberDisplayed', + 'allSelected', + 'allSelectedPlaceholder', +])(Placeholder); diff --git a/src/SelectAll.js b/src/SelectAll.tsx similarity index 69% rename from src/SelectAll.js rename to src/SelectAll.tsx index 13727c0..a93ec37 100644 --- a/src/SelectAll.js +++ b/src/SelectAll.tsx @@ -1,7 +1,16 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import * as React from 'react'; import { onlyUpdateForKeys } from 'recompose'; -function SelectAll({ + +type SelectAllProps = { + tabIndex: number | undefined; + disabled: boolean; + allSelected: boolean; + id: string; + selectAllText?: string; + toggleSelectAll(): void; + visible: boolean; +}; +const SelectAll: React.FC = ({ tabIndex, disabled, allSelected, @@ -9,7 +18,7 @@ function SelectAll({ selectAllText, toggleSelectAll, visible, -}) { +}) => { if (!visible) { return null; } @@ -23,7 +32,6 @@ function SelectAll({ aria-selected={allSelected} className={allSelected ? 'option selected' : 'option'} onClick={toggleSelectAll} - disabled={disabled} onKeyPress={toggleSelectAll} > {selectAllText}
); -} - -SelectAll.propTypes = { - tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - disabled: PropTypes.bool, - allSelected: PropTypes.bool, - id: PropTypes.string.isRequired, - selectAllText: PropTypes.string, - toggleSelectAll: PropTypes.func.isRequired, - visible: PropTypes.bool, }; +SelectAll.displayName = 'Picky(SelectAll)'; + export default onlyUpdateForKeys([ 'tabIndex', 'disabled', diff --git a/tests/Picky.test.js b/src/__tests__/Picky.test.tsx similarity index 95% rename from tests/Picky.test.js rename to src/__tests__/Picky.test.tsx index fef9a15..7aeb19c 100644 --- a/tests/Picky.test.js +++ b/src/__tests__/Picky.test.tsx @@ -1,13 +1,19 @@ import React from 'react'; import { mount } from 'enzyme'; -import { render as rtlRender, fireEvent, cleanup } from 'react-testing-library'; - -import Placeholder from '../src/Placeholder'; -import Picky from '../src/Picky'; -import Filter from '../src/Filter'; -import Option from '../src/Option'; - +//@ts-ignore +import { + render as rtlRender, + fireEvent, + cleanup, +} from '@testing-library/react'; + +import Placeholder from '../Placeholder'; +import Picky from '../Picky'; +import Filter from '../Filter'; +import Option from '../Option'; +//@ts-ignore const sel = id => `[data-testid="${id}"]`; +//@ts-ignore const selSelected = id => `[data-testid="${id}"][data-selected="selected"]`; const corePickyProps = { @@ -15,7 +21,7 @@ const corePickyProps = { }; describe('Picky', () => { - afterEach(cleanup); // <-- add this + afterEach(cleanup); it('should have Placeholder component', () => { const wrapper = mount(); @@ -196,23 +202,13 @@ describe('Picky', () => { describe('Plain Dropdown drawer', () => { it('should open if prop open is true', () => { const wrapper = mount( - + ); expect(wrapper.find(sel('dropdown'))).toHaveLength(1); }); it('should not be open if prop open is false', () => { const wrapper = mount( - + ); expect(wrapper.find(sel('dropdown'))).toHaveLength(0); }); @@ -225,12 +221,7 @@ describe('Picky', () => { }); it('should open on click of button (open by prop)', () => { const wrapper = mount( - + ); expect(wrapper.find(sel('dropdown'))).toHaveLength(1); wrapper.find(sel('picky-input')).simulate('click'); @@ -243,7 +234,6 @@ describe('Picky', () => { value={[1, 2, 3]} options={[1, 2, 3, 4, 5]} open={true} - virtual={false} /> ); expect(wrapper.find(sel('option'))).toHaveLength(5); @@ -256,7 +246,6 @@ describe('Picky', () => { value={[1, 2, 3]} options={[1, 2, 3, 4, 5]} open={true} - virtual={false} /> ); const selected = wrapper.find(selSelected('option')); @@ -273,7 +262,6 @@ describe('Picky', () => { options={[1, 2, 3, 4, 5]} open={true} multiple - virtual={false} /> ); @@ -289,7 +277,6 @@ describe('Picky', () => { options={[1, 2, 3, 4, 5]} open={true} multiple={false} - virtual={false} /> ); expect(wrapper.find(sel('picky_placeholder')).text()).toEqual('1'); @@ -302,7 +289,6 @@ describe('Picky', () => { options={[1, 2, 3, 4, 5]} open={true} multiple={true} - virtual={false} /> ); expect(wrapper.find(sel('picky_placeholder')).text()).toEqual('1, 2, 3'); @@ -315,7 +301,6 @@ describe('Picky', () => { options={[1, 2, 3, 4, 5]} open={true} multiple={true} - virtual={false} /> ); expect(nextWrapper.find(sel('picky_placeholder')).text()).toEqual( @@ -486,7 +471,7 @@ describe('Picky', () => { const wrapper = mount( { it('should call onFilterChange prop when text has changed', () => { const onChange = jest.fn(); - const wrapper = mount(); + const wrapper = mount(); const event = { target: { value: '123' } }; wrapper.find(sel('picky__filter__input')).simulate('change', event); expect(onChange).toHaveBeenCalledWith('123'); @@ -774,17 +759,10 @@ describe('Picky', () => { }); }); - xit('should close when clicked outside of component', () => { - //Can't figure out how to test this - // const wrapper = mount(); - // wrapper.mount(); - // expect(wrapper.find(sel('dropdown'))).toHaveLength(1); - // wrapper.instance().handleOutsideClick({ target: '#root' }); - // expect(wrapper.find(sel('dropdown'))).toHaveLength(0); - }); it('should select option on keyPress', () => { const keyPressMock = jest.fn(); const wrapper = mount( + //@ts-ignore