Repository: downshift-js/downshift
Branch: master
Commit: f1862ed0633a
Files: 231
Total size: 1.1 MB
Directory structure:
gitextract_fp6r61rc/
├── .all-contributorsrc
├── .flowconfig
├── .gitattributes
├── .github/
│ ├── ISSUE_TEMPLATE.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ └── validate.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── babel.config.js
├── cypress/
│ ├── .eslintrc
│ ├── e2e/
│ │ ├── combobox.cy.js
│ │ ├── useCombobox.cy.js
│ │ ├── useMultipleCombobox.cy.js
│ │ ├── useMultipleSelect.cy.js
│ │ ├── useSelect.cy.js
│ │ └── useTagGroup.cy.js
│ ├── fixtures/
│ │ └── example.json
│ ├── plugins/
│ │ └── index.js
│ └── support/
│ └── e2e.js
├── cypress.config.js
├── docusaurus/
│ ├── pages/
│ │ ├── combobox.js
│ │ ├── index.js
│ │ ├── useCombobox.js
│ │ ├── useMultipleCombobox.js
│ │ ├── useMultipleSelect.js
│ │ ├── useSelect.js
│ │ ├── useTagGroup.css
│ │ ├── useTagGroup.tsx
│ │ ├── useTagGroupCombobox.css
│ │ └── useTagGroupCombobox.tsx
│ ├── plugins/
│ │ └── webpack5polyfills.js
│ ├── tsconfig.json
│ └── utils.ts
├── docusaurus.config.js
├── flow-typed/
│ └── npm/
│ └── downshift_v2.x.x.js.flow
├── jest.config.js
├── netlify.toml
├── other/
│ ├── MAINTAINING.md
│ ├── TYPESCRIPT_USAGE.md
│ ├── USERS.md
│ ├── manual-releases.md
│ ├── misc-tests/
│ │ ├── __tests__/
│ │ │ ├── build.js
│ │ │ └── preact.js
│ │ └── jest.config.js
│ ├── react-native/
│ │ ├── .babelrc
│ │ ├── __tests__/
│ │ │ ├── __snapshots__/
│ │ │ │ └── render-tests.js.snap
│ │ │ ├── onBlur-tests.js
│ │ │ ├── onChange-tests.js
│ │ │ └── render-tests.js
│ │ └── jest.config.js
│ └── ssr/
│ ├── __tests__/
│ │ └── index.js
│ └── jest.config.js
├── package.json
├── prettier.config.js
├── rollup.config.js
├── src/
│ ├── __mocks__/
│ │ ├── set-a11y-status.js
│ │ └── utils.js
│ ├── __tests__/
│ │ ├── .eslintrc
│ │ ├── __snapshots__/
│ │ │ ├── downshift.aria.js.snap
│ │ │ ├── downshift.get-item-props.js.snap
│ │ │ ├── downshift.get-menu-props.js.snap
│ │ │ ├── downshift.get-root-props.js.snap
│ │ │ ├── downshift.misc.js.snap
│ │ │ └── set-a11y-status.js.snap
│ │ ├── downshift.aria.js
│ │ ├── downshift.focus-restoration.js
│ │ ├── downshift.get-button-props.js
│ │ ├── downshift.get-input-props.js
│ │ ├── downshift.get-item-props.js
│ │ ├── downshift.get-label-props.js
│ │ ├── downshift.get-menu-props.js
│ │ ├── downshift.get-root-props.js
│ │ ├── downshift.lifecycle.js
│ │ ├── downshift.misc-with-utils-mocked.js
│ │ ├── downshift.misc.js
│ │ ├── downshift.props.js
│ │ ├── portal-support.js
│ │ ├── set-a11y-status.js
│ │ ├── utils.call-all-event-handlers.js
│ │ ├── utils.get-a11y-status-message.js
│ │ ├── utils.get-highlighted-index.js
│ │ ├── utils.handle-refs.js
│ │ ├── utils.pick-state.js
│ │ ├── utils.reset-id-counter.js
│ │ ├── utils.reset-id-counter.r18.js
│ │ └── utils.scroll-into-view.js
│ ├── downshift.js
│ ├── hooks/
│ │ ├── MIGRATION_V7.md
│ │ ├── MIGRATION_V8.md
│ │ ├── MIGRATION_V9.md
│ │ ├── README.md
│ │ ├── __tests__/
│ │ │ ├── __snapshots__/
│ │ │ │ └── utils.test.js.snap
│ │ │ └── utils.test.js
│ │ ├── index.ts
│ │ ├── reducer.js
│ │ ├── testUtils.js
│ │ ├── useCombobox/
│ │ │ ├── README.md
│ │ │ ├── __tests__/
│ │ │ │ ├── __snapshots__/
│ │ │ │ │ └── getInputProps.test.js.snap
│ │ │ │ ├── getInputProps.test.js
│ │ │ │ ├── getItemProps.test.js
│ │ │ │ ├── getLabelProps.test.js
│ │ │ │ ├── getMenuProps.test.js
│ │ │ │ ├── getToggleButtonProps.test.js
│ │ │ │ ├── memo.test.js
│ │ │ │ ├── props.test.js
│ │ │ │ ├── returnProps.test.js
│ │ │ │ └── utils.test.js
│ │ │ ├── index.js
│ │ │ ├── reducer.js
│ │ │ ├── stateChangeTypes.js
│ │ │ ├── testUtils.js
│ │ │ └── utils.js
│ │ ├── useMultipleSelection/
│ │ │ ├── MIGRATION_GUIDE.md
│ │ │ ├── README.md
│ │ │ ├── __tests__/
│ │ │ │ ├── getDropdownProps.test.js
│ │ │ │ ├── getSelectedItemProps.test.js
│ │ │ │ ├── memo.test.js
│ │ │ │ ├── props.test.js
│ │ │ │ ├── returnProps.test.js
│ │ │ │ └── utils.test.js
│ │ │ ├── index.js
│ │ │ ├── reducer.js
│ │ │ ├── stateChangeTypes.js
│ │ │ ├── testUtils.js
│ │ │ └── utils.js
│ │ ├── useSelect/
│ │ │ ├── README.md
│ │ │ ├── __tests__/
│ │ │ │ ├── __snapshots__/
│ │ │ │ │ └── getToggleButtonProps.test.js.snap
│ │ │ │ ├── getItemProps.test.js
│ │ │ │ ├── getLabelProps.test.js
│ │ │ │ ├── getMenuProps.test.js
│ │ │ │ ├── getToggleButtonProps.test.js
│ │ │ │ ├── memo.test.js
│ │ │ │ ├── props.test.js
│ │ │ │ ├── returnProps.test.js
│ │ │ │ └── utils.test.ts
│ │ │ ├── index.js
│ │ │ ├── reducer.js
│ │ │ ├── stateChangeTypes.js
│ │ │ ├── testUtils.js
│ │ │ └── utils/
│ │ │ ├── defaultProps.ts
│ │ │ ├── getItemIndexByCharacterKey.ts
│ │ │ ├── index.ts
│ │ │ └── propTypes.ts
│ │ ├── useTagGroup/
│ │ │ ├── README.md
│ │ │ ├── __tests__/
│ │ │ │ ├── getTagGroupProps.test.ts
│ │ │ │ ├── getTagProps.test.ts
│ │ │ │ ├── getTagRemoveProps.test.ts
│ │ │ │ ├── props.test.ts
│ │ │ │ ├── reducer.test.ts
│ │ │ │ ├── returnProps.test.ts
│ │ │ │ └── utils/
│ │ │ │ ├── defaultIds.ts
│ │ │ │ ├── defaultProps.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── renderTagGroup.tsx
│ │ │ │ └── renderUseTagGroup.ts
│ │ │ ├── index.ts
│ │ │ ├── index.types.ts
│ │ │ ├── reducer.ts
│ │ │ ├── stateChangeTypes.ts
│ │ │ └── utils/
│ │ │ ├── __tests__/
│ │ │ │ ├── useAccessibleDescription.test.ts
│ │ │ │ ├── useElementIds.legacy.test.ts
│ │ │ │ └── useElementIds.r18.test.ts
│ │ │ ├── getInitialState.ts
│ │ │ ├── getMergedProps.ts
│ │ │ ├── index.ts
│ │ │ ├── isStateEqual.ts
│ │ │ ├── useAccessibleDescription.ts
│ │ │ ├── useElementIds.ts
│ │ │ └── useRovingTagFocus.ts
│ │ ├── utils-ts/
│ │ │ ├── __tests__/
│ │ │ │ └── getItemAndIndex.test.ts
│ │ │ ├── callOnChangeProps.ts
│ │ │ ├── capitalizeString.ts
│ │ │ ├── getDefaultValue.ts
│ │ │ ├── getInitialValue.ts
│ │ │ ├── getItemAndIndex.ts
│ │ │ ├── index.ts
│ │ │ ├── propTypes.ts
│ │ │ ├── stateReducer.ts
│ │ │ ├── useA11yMessageStatus.ts
│ │ │ ├── useControlledReducer.ts
│ │ │ ├── useEnhancedReducer.ts
│ │ │ └── useIsInitialMount.ts
│ │ ├── utils.dropdown/
│ │ │ ├── __tests__/
│ │ │ │ ├── useElementIds.legacy.test.ts
│ │ │ │ └── useElementIds.r18.test.ts
│ │ │ ├── defaultProps.ts
│ │ │ ├── defaultStateValues.ts
│ │ │ ├── index.ts
│ │ │ ├── propTypes.ts
│ │ │ └── useElementIds.ts
│ │ └── utils.js
│ ├── index.ts
│ ├── is.macro.d.ts
│ ├── is.macro.js
│ ├── productionEnum.macro.d.ts
│ ├── productionEnum.macro.js
│ ├── stateChangeTypes.js
│ ├── utils-ts/
│ │ ├── __tests__/
│ │ │ ├── getState.test.ts
│ │ │ └── handleRefs.test.ts
│ │ ├── callAllEventHandlers.ts
│ │ ├── debounce.ts
│ │ ├── generateId.ts
│ │ ├── getState.ts
│ │ ├── handleRefs.ts
│ │ ├── index.ts
│ │ ├── noop.ts
│ │ ├── scrollIntoView.ts
│ │ ├── setA11yStatus.ts
│ │ ├── useLatestRef.ts
│ │ └── validatePropTypes.ts
│ └── utils.js
├── test/
│ ├── basic.test.js
│ ├── basic.test.tsx
│ ├── custom.test.js
│ ├── custom.test.tsx
│ ├── downshift.test.tsx
│ ├── setup.ts
│ ├── tsconfig.json
│ ├── useCombobox.test.tsx
│ ├── useMultipleSelect.test.tsx
│ └── useSelect.test.tsx
├── tsconfig.json
├── tsconfig.preact.json
└── typings/
├── index.d.ts
└── index.legacy.d.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .all-contributorsrc
================================================
{
"projectName": "downshift",
"projectOwner": "downshift-js",
"repoType": "github",
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"contributors": [
{
"login": "kentcdodds",
"name": "Kent C. Dodds",
"avatar_url": "https://avatars.githubusercontent.com/u/1500684?v=3",
"profile": "https://kentcdodds.com",
"contributions": [
"code",
"doc",
"infra",
"test",
"review",
"blog",
"bug",
"example",
"ideas",
"talk"
]
},
{
"login": "ryanflorence",
"name": "Ryan Florence",
"avatar_url": "https://avatars0.githubusercontent.com/u/100200?v=4",
"profile": "http://twitter.com/ryanflorence",
"contributions": [
"ideas"
]
},
{
"login": "jaredly",
"name": "Jared Forsyth",
"avatar_url": "https://avatars3.githubusercontent.com/u/112170?v=4",
"profile": "http://jaredforsyth.com",
"contributions": [
"ideas",
"doc"
]
},
{
"login": "jtmthf",
"name": "Jack Moore",
"avatar_url": "https://avatars1.githubusercontent.com/u/8162598?v=4",
"profile": "https://github.com/jtmthf",
"contributions": [
"example"
]
},
{
"login": "souporserious",
"name": "Travis Arnold",
"avatar_url": "https://avatars1.githubusercontent.com/u/2762082?v=4",
"profile": "https://souporserious.com/",
"contributions": [
"code",
"doc"
]
},
{
"login": "marcysutton",
"name": "Marcy Sutton",
"avatar_url": "https://avatars0.githubusercontent.com/u/1045233?v=4",
"profile": "http://marcysutton.com",
"contributions": [
"bug",
"ideas"
]
},
{
"login": "tizmagik",
"name": "Jeremy Gayed",
"avatar_url": "https://avatars2.githubusercontent.com/u/244704?v=4",
"profile": "http://www.jeremygayed.com",
"contributions": [
"example"
]
},
{
"login": "Haroenv",
"name": "Haroen Viaene",
"avatar_url": "https://avatars3.githubusercontent.com/u/6270048?v=4",
"profile": "https://haroen.me",
"contributions": [
"example"
]
},
{
"login": "rezof",
"name": "monssef",
"avatar_url": "https://avatars2.githubusercontent.com/u/15073300?v=4",
"profile": "https://github.com/rezof",
"contributions": [
"example"
]
},
{
"login": "FezVrasta",
"name": "Federico Zivolo",
"avatar_url": "https://avatars2.githubusercontent.com/u/5382443?v=4",
"profile": "https://fezvrasta.github.io",
"contributions": [
"doc"
]
},
{
"login": "divyenduz",
"name": "Divyendu Singh",
"avatar_url": "https://avatars3.githubusercontent.com/u/746482?v=4",
"profile": "https://divyendusingh.com",
"contributions": [
"example",
"code",
"doc",
"test"
]
},
{
"login": "salmanmanekia",
"name": "Muhammad Salman",
"avatar_url": "https://avatars1.githubusercontent.com/u/841955?v=4",
"profile": "https://github.com/salmanmanekia",
"contributions": [
"code"
]
},
{
"login": "psicotropicos",
"name": "João Alberto",
"avatar_url": "https://avatars3.githubusercontent.com/u/10820159?v=4",
"profile": "https://twitter.com/psicotropidev",
"contributions": [
"code"
]
},
{
"login": "bernard-lin",
"name": "Bernard Lin",
"avatar_url": "https://avatars0.githubusercontent.com/u/16327281?v=4",
"profile": "https://github.com/bernard-lin",
"contributions": [
"code",
"doc"
]
},
{
"login": "geoffdavis92",
"name": "Geoff Davis",
"avatar_url": "https://avatars1.githubusercontent.com/u/7330124?v=4",
"profile": "https://geoffdavis.info",
"contributions": [
"example"
]
},
{
"login": "reznord",
"name": "Anup",
"avatar_url": "https://avatars0.githubusercontent.com/u/3415488?v=4",
"profile": "https://github.com/reznord",
"contributions": [
"doc"
]
},
{
"login": "ferdinandsalis",
"name": "Ferdinand Salis",
"avatar_url": "https://avatars0.githubusercontent.com/u/340520?v=4",
"profile": "http://ferdinandsalis.com",
"contributions": [
"bug",
"code"
]
},
{
"login": "tkh44",
"name": "Kye Hohenberger",
"avatar_url": "https://avatars2.githubusercontent.com/u/662750?v=4",
"profile": "https://github.com/tkh44",
"contributions": [
"bug"
]
},
{
"login": "jgoux",
"name": "Julien Goux",
"avatar_url": "https://avatars0.githubusercontent.com/u/1443499?v=4",
"profile": "https://github.com/jgoux",
"contributions": [
"bug",
"code",
"test"
]
},
{
"login": "jseminck",
"name": "Joachim Seminck",
"avatar_url": "https://avatars2.githubusercontent.com/u/9586897?v=4",
"profile": "https://github.com/jseminck",
"contributions": [
"code"
]
},
{
"login": "the-simian",
"name": "Jesse Harlin",
"avatar_url": "https://avatars3.githubusercontent.com/u/954596?v=4",
"profile": "http://jesseharlin.net/",
"contributions": [
"bug",
"example"
]
},
{
"login": "pbomb",
"name": "Matt Parrish",
"avatar_url": "https://avatars0.githubusercontent.com/u/1402095?v=4",
"profile": "https://github.com/pbomb",
"contributions": [
"tool",
"review"
]
},
{
"login": "thomhos",
"name": "thom",
"avatar_url": "https://avatars1.githubusercontent.com/u/11661846?v=4",
"profile": "http://thom.kr",
"contributions": [
"code"
]
},
{
"login": "vutran",
"name": "Vu Tran",
"avatar_url": "https://avatars2.githubusercontent.com/u/1088312?v=4",
"profile": "http://twitter.com/tranvu",
"contributions": [
"code"
]
},
{
"login": "codiemullins",
"name": "Codie Mullins",
"avatar_url": "https://avatars1.githubusercontent.com/u/74193?v=4",
"profile": "https://github.com/codiemullins",
"contributions": [
"code",
"example"
]
},
{
"login": "morajabi",
"name": "Mohammad Rajabifard",
"avatar_url": "https://avatars3.githubusercontent.com/u/12202757?v=4",
"profile": "https://morajabi.me",
"contributions": [
"doc",
"ideas"
]
},
{
"login": "tansongyang",
"name": "Frank Tan",
"avatar_url": "https://avatars3.githubusercontent.com/u/9488719?v=4",
"profile": "https://github.com/tansongyang",
"contributions": [
"code"
]
},
{
"login": "srph",
"name": "Kier Borromeo",
"avatar_url": "https://avatars3.githubusercontent.com/u/5093058?v=4",
"profile": "https://kierb.com",
"contributions": [
"example"
]
},
{
"login": "paul-veevers",
"name": "Paul Veevers",
"avatar_url": "https://avatars1.githubusercontent.com/u/8969456?v=4",
"profile": "https://github.com/paul-veevers",
"contributions": [
"code"
]
},
{
"login": "Ronolibert",
"name": "Ron Cruz",
"avatar_url": "https://avatars2.githubusercontent.com/u/13622298?v=4",
"profile": "https://github.com/Ronolibert",
"contributions": [
"doc"
]
},
{
"login": "rickMcGavin",
"name": "Rick McGavin",
"avatar_url": "https://avatars1.githubusercontent.com/u/13605633?v=4",
"profile": "http://rickmcgavin.github.io",
"contributions": [
"doc"
]
},
{
"login": "vejersele",
"name": "Jelle Versele",
"avatar_url": "https://avatars0.githubusercontent.com/u/869669?v=4",
"profile": "http://twitter.com/vejersele",
"contributions": [
"example"
]
},
{
"login": "brentertz",
"name": "Brent Ertz",
"avatar_url": "https://avatars1.githubusercontent.com/u/202773?v=4",
"profile": "https://github.com/brentertz",
"contributions": [
"ideas"
]
},
{
"login": "Dajust",
"name": "Justice Mba ",
"avatar_url": "https://avatars3.githubusercontent.com/u/8015514?v=4",
"profile": "https://github.com/Dajust",
"contributions": [
"code",
"doc",
"ideas"
]
},
{
"login": "ellismarkf",
"name": "Mark Ellis",
"avatar_url": "https://avatars2.githubusercontent.com/u/3925281?v=4",
"profile": "http://mfellis.com",
"contributions": [
"ideas"
]
},
{
"login": "usandfriends",
"name": "us͡an̸df͘rien͜ds͠",
"avatar_url": "https://avatars1.githubusercontent.com/u/3241922?v=4",
"profile": "http://ronak.io/",
"contributions": [
"bug",
"code",
"test"
]
},
{
"login": "robin-drexler",
"name": "Robin Drexler",
"avatar_url": "https://avatars0.githubusercontent.com/u/474248?v=4",
"profile": "https://www.robin-drexler.com/",
"contributions": [
"bug",
"code"
]
},
{
"login": "arturoromeroslc",
"name": "Arturo Romero",
"avatar_url": "https://avatars0.githubusercontent.com/u/7406639?v=4",
"profile": "http://arturoromero.info/",
"contributions": [
"example"
]
},
{
"login": "yp",
"name": "yp",
"avatar_url": "https://avatars1.githubusercontent.com/u/275483?v=4",
"profile": "http://algolab.eu/pirola",
"contributions": [
"bug",
"code",
"test"
]
},
{
"login": "ifyoumakeit",
"name": "Dave Garwacke",
"avatar_url": "https://avatars0.githubusercontent.com/u/3998604?v=4",
"profile": "http://www.warbyparker.com",
"contributions": [
"doc"
]
},
{
"login": "Drapegnik",
"name": "Ivan Pazhitnykh",
"avatar_url": "https://avatars3.githubusercontent.com/u/11758660?v=4",
"profile": "http://linkedin.com/in/drapegnik",
"contributions": [
"code",
"test"
]
},
{
"login": "Rendez",
"name": "Luis Merino",
"avatar_url": "https://avatars0.githubusercontent.com/u/61776?v=4",
"profile": "https://github.com/Rendez",
"contributions": [
"doc"
]
},
{
"login": "arahansen",
"name": "Andrew Hansen",
"avatar_url": "https://avatars0.githubusercontent.com/u/8746094?v=4",
"profile": "http://twitter.com/arahansen",
"contributions": [
"code",
"test",
"ideas"
]
},
{
"login": "Jwhiles",
"name": "John Whiles",
"avatar_url": "https://avatars3.githubusercontent.com/u/20307225?v=4",
"profile": "http://www.johnwhiles.com",
"contributions": [
"code"
]
},
{
"login": "wKovacs64",
"name": "Justin Hall",
"avatar_url": "https://avatars1.githubusercontent.com/u/1288694?v=4",
"profile": "https://github.com/wKovacs64",
"contributions": [
"infra"
]
},
{
"login": "petetnt",
"name": "Pete Nykänen",
"avatar_url": "https://avatars2.githubusercontent.com/u/7641760?v=4",
"profile": "https://twitter.com/pete_tnt",
"contributions": [
"review"
]
},
{
"login": "jaredpalmer",
"name": "Jared Palmer",
"avatar_url": "https://avatars2.githubusercontent.com/u/4060187?v=4",
"profile": "http://jaredpalmer.com",
"contributions": [
"code"
]
},
{
"login": "philipyoungg",
"name": "Philip Young",
"avatar_url": "https://avatars3.githubusercontent.com/u/11477718?v=4",
"profile": "http://www.philipyoungg.com",
"contributions": [
"code",
"test",
"ideas"
]
},
{
"login": "alexandernanberg",
"name": "Alexander Nanberg",
"avatar_url": "https://avatars3.githubusercontent.com/u/8997319?v=4",
"profile": "https://alexandernanberg.com",
"contributions": [
"doc",
"code"
]
},
{
"login": "httpete-ire",
"name": "Pete Redmond",
"avatar_url": "https://avatars2.githubusercontent.com/u/1556430?v=4",
"profile": "https://httpete.com",
"contributions": [
"bug"
]
},
{
"login": "Zashy",
"name": "Nick Lavin",
"avatar_url": "https://avatars2.githubusercontent.com/u/1706342?v=4",
"profile": "https://github.com/Zashy",
"contributions": [
"bug",
"code",
"test"
]
},
{
"login": "jlongster",
"name": "James Long",
"avatar_url": "https://avatars2.githubusercontent.com/u/17031?v=4",
"profile": "http://jlongster.com",
"contributions": [
"bug",
"code"
]
},
{
"login": "cycomachead",
"name": "Michael Ball",
"avatar_url": "https://avatars0.githubusercontent.com/u/1505907?v=4",
"profile": "http://michaelball.co",
"contributions": [
"bug",
"code",
"test"
]
},
{
"login": "Julienng",
"name": "CAVALEIRO Julien",
"avatar_url": "https://avatars0.githubusercontent.com/u/8990614?v=4",
"profile": "https://github.com/Julienng",
"contributions": [
"example"
]
},
{
"login": "kimgronqvist",
"name": "Kim Grönqvist",
"avatar_url": "https://avatars1.githubusercontent.com/u/3421067?v=4",
"profile": "http://www.kimgronqvist.se",
"contributions": [
"code",
"test"
]
},
{
"login": "tiansijie",
"name": "Sijie",
"avatar_url": "https://avatars2.githubusercontent.com/u/3675602?v=4",
"profile": "http://sijietian.com",
"contributions": [
"bug",
"code"
]
},
{
"login": "donysukardi",
"name": "Dony Sukardi",
"avatar_url": "https://avatars0.githubusercontent.com/u/410792?v=4",
"profile": "http://dsds.io",
"contributions": [
"example",
"question",
"code",
"test"
]
},
{
"login": "dmmulroy",
"name": "Dillon Mulroy",
"avatar_url": "https://avatars1.githubusercontent.com/u/2755722?v=4",
"profile": "https://dillonmulroy.com",
"contributions": [
"doc"
]
},
{
"login": "curtiswilkinson",
"name": "Curtis Tate Wilkinson",
"avatar_url": "https://avatars3.githubusercontent.com/u/12440573?v=4",
"profile": "https://twitter.com/curtytate",
"contributions": [
"code"
]
},
{
"login": "brikou",
"name": "Brice BERNARD",
"avatar_url": "https://avatars3.githubusercontent.com/u/383212?v=4",
"profile": "https://github.com/brikou",
"contributions": [
"bug",
"code"
]
},
{
"login": "xutopia",
"name": "Tony Xu",
"avatar_url": "https://avatars3.githubusercontent.com/u/14304503?v=4",
"profile": "https://github.com/xutopia",
"contributions": [
"code"
]
},
{
"login": "newyork-anthonyng",
"name": "Anthony Ng",
"avatar_url": "https://avatars1.githubusercontent.com/u/14035529?v=4",
"profile": "http://anthonyng.me",
"contributions": [
"doc"
]
},
{
"login": "notruth",
"name": "S S",
"avatar_url": "https://avatars2.githubusercontent.com/u/11996139?v=4",
"profile": "https://github.com/notruth",
"contributions": [
"question",
"code",
"doc",
"ideas",
"test"
]
},
{
"login": "austintackaberry",
"name": "Austin Tackaberry",
"avatar_url": "https://avatars0.githubusercontent.com/u/29493001?v=4",
"profile": "http://austintackaberry.co",
"contributions": [
"question",
"code",
"doc",
"bug",
"example",
"ideas",
"review",
"test"
]
},
{
"login": "jduthon",
"name": "Jean Duthon",
"avatar_url": "https://avatars3.githubusercontent.com/u/4168055?v=4",
"profile": "https://github.com/jduthon",
"contributions": [
"bug",
"code"
]
},
{
"login": "Antontelesh",
"name": "Anton Telesh",
"avatar_url": "https://avatars3.githubusercontent.com/u/3889580?v=4",
"profile": "http://antontelesh.github.io",
"contributions": [
"bug",
"code"
]
},
{
"login": "ericedem",
"name": "Eric Edem",
"avatar_url": "https://avatars3.githubusercontent.com/u/1060669?v=4",
"profile": "https://github.com/ericedem",
"contributions": [
"code",
"doc",
"ideas",
"test"
]
},
{
"login": "indiesquidge",
"name": "Austin Wood",
"avatar_url": "https://avatars3.githubusercontent.com/u/3409645?v=4",
"profile": "https://github.com/indiesquidge",
"contributions": [
"question",
"doc",
"review"
]
},
{
"login": "mmmurray",
"name": "Mark Murray",
"avatar_url": "https://avatars3.githubusercontent.com/u/14275790?v=4",
"profile": "https://github.com/mmmurray",
"contributions": [
"infra"
]
},
{
"login": "gsimone",
"name": "Gianmarco",
"avatar_url": "https://avatars0.githubusercontent.com/u/1862172?v=4",
"profile": "https://github.com/gsimone",
"contributions": [
"bug",
"code"
]
},
{
"login": "pastr",
"name": "Emmanuel Pastor",
"avatar_url": "https://avatars2.githubusercontent.com/u/6838136?v=4",
"profile": "https://github.com/pastr",
"contributions": [
"example"
]
},
{
"login": "dalehurwitz",
"name": "dalehurwitz",
"avatar_url": "https://avatars2.githubusercontent.com/u/10345034?v=4",
"profile": "https://github.com/dalehurwitz",
"contributions": [
"code"
]
},
{
"login": "blobor",
"name": "Bogdan Lobor",
"avatar_url": "https://avatars1.githubusercontent.com/u/4813007?v=4",
"profile": "https://github.com/blobor",
"contributions": [
"bug",
"code"
]
},
{
"login": "infiniteluke",
"name": "Luke Herrington",
"avatar_url": "https://avatars0.githubusercontent.com/u/1127238?v=4",
"profile": "https://github.com/infiniteluke",
"contributions": [
"example"
]
},
{
"login": "drobannx",
"name": "Brandon Clemons",
"avatar_url": "https://avatars2.githubusercontent.com/u/6361167?v=4",
"profile": "https://github.com/drobannx",
"contributions": [
"code"
]
},
{
"login": "aMollusk",
"name": "Kieran",
"avatar_url": "https://avatars0.githubusercontent.com/u/10591587?v=4",
"profile": "https://github.com/aMollusk",
"contributions": [
"code"
]
},
{
"login": "Brushedoctopus",
"name": "Brushedoctopus",
"avatar_url": "https://avatars3.githubusercontent.com/u/11570627?v=4",
"profile": "https://github.com/Brushedoctopus",
"contributions": [
"bug",
"code"
]
},
{
"login": "cameronprattedwards",
"name": "Cameron Edwards",
"avatar_url": "https://avatars3.githubusercontent.com/u/5456216?v=4",
"profile": "http://cameronpedwards.com",
"contributions": [
"code",
"test"
]
},
{
"login": "stereobooster",
"name": "stereobooster",
"avatar_url": "https://avatars2.githubusercontent.com/u/179534?v=4",
"profile": "https://github.com/stereobooster",
"contributions": [
"code",
"test"
]
},
{
"login": "1Copenut",
"name": "Trevor Pierce",
"avatar_url": "https://avatars0.githubusercontent.com/u/934879?v=4",
"profile": "https://github.com/1Copenut",
"contributions": [
"review"
]
},
{
"login": "franklixuefei",
"name": "Xuefei Li",
"avatar_url": "https://avatars1.githubusercontent.com/u/1334982?v=4",
"profile": "http://xuefei-frank.com",
"contributions": [
"code"
]
},
{
"login": "alfredringstad",
"name": "Alfred Ringstad",
"avatar_url": "https://avatars0.githubusercontent.com/u/7252803?v=4",
"profile": "https://hyperlab.se",
"contributions": [
"code"
]
},
{
"login": "dovidweisz",
"name": "D[oa]vid Weisz",
"avatar_url": "https://avatars0.githubusercontent.com/u/6895497?v=4",
"profile": "https://github.com/dovidweisz",
"contributions": [
"code"
]
},
{
"login": "RoystonS",
"name": "Royston Shufflebotham",
"avatar_url": "https://avatars0.githubusercontent.com/u/19773?v=4",
"profile": "https://github.com/RoystonS",
"contributions": [
"bug",
"code"
]
},
{
"login": "MichaelDeBoey",
"name": "Michaël De Boey",
"avatar_url": "https://avatars3.githubusercontent.com/u/6643991?v=4",
"profile": "http://michaeldeboey.be",
"contributions": [
"code"
]
},
{
"login": "EricHenry",
"name": "Henry",
"avatar_url": "https://avatars3.githubusercontent.com/u/4412771?v=4",
"profile": "https://github.com/EricHenry",
"contributions": [
"code"
]
},
{
"login": "green-arrow",
"name": "Andrew Walton",
"avatar_url": "https://avatars3.githubusercontent.com/u/2180127?v=4",
"profile": "http://www.greenarrow.me",
"contributions": [
"bug",
"code",
"test"
]
},
{
"login": "arthurdenner",
"name": "Arthur Denner",
"avatar_url": "https://avatars0.githubusercontent.com/u/13774309?v=4",
"profile": "https://github.com/arthurdenner",
"contributions": [
"code"
]
},
{
"login": "stipsan",
"name": "Cody Olsen",
"avatar_url": "https://avatars2.githubusercontent.com/u/81981?v=4",
"profile": "http://twitter.com/stipsan",
"contributions": [
"code"
]
},
{
"login": "TLadd",
"name": "Thomas Ladd",
"avatar_url": "https://avatars0.githubusercontent.com/u/5084492?v=4",
"profile": "https://github.com/TLadd",
"contributions": [
"code"
]
},
{
"login": "lixualinta",
"name": "lixualinta",
"avatar_url": "https://avatars3.githubusercontent.com/u/34634369?v=4",
"profile": "https://github.com/lixualinta",
"contributions": [
"code"
]
},
{
"login": "JCofman",
"name": "Jacob Cofman",
"avatar_url": "https://avatars2.githubusercontent.com/u/2118956?v=4",
"profile": "https://twitter.com/JCofman",
"contributions": [
"code"
]
},
{
"login": "jf248",
"name": "Joshua Freedman",
"avatar_url": "https://avatars3.githubusercontent.com/u/19275184?v=4",
"profile": "https://github.com/jf248",
"contributions": [
"code"
]
},
{
"login": "AmyScript",
"name": "Amy",
"avatar_url": "https://avatars1.githubusercontent.com/u/24494020?v=4",
"profile": "https://github.com/AmyScript",
"contributions": [
"example"
]
},
{
"login": "roginfarrer",
"name": "Rogin Farrer",
"avatar_url": "https://avatars1.githubusercontent.com/u/9063669?v=4",
"profile": "http://twitter.com/roginfarrer",
"contributions": [
"code"
]
},
{
"login": "rifler",
"name": "Dmitrii Kanatnikov",
"avatar_url": "https://avatars3.githubusercontent.com/u/871583",
"profile": "https://github.com/rifler",
"contributions": [
"code"
]
},
{
"login": "dallonf",
"name": "Dallon Feldner",
"avatar_url": "https://avatars2.githubusercontent.com/u/346300?v=4",
"profile": "https://github.com/dallonf",
"contributions": [
"bug",
"code"
]
},
{
"login": "samuelfullerthomas",
"name": "Samuel Fuller Thomas",
"avatar_url": "https://avatars2.githubusercontent.com/u/10165959?v=4",
"profile": "https://samuelfullerthomas.com",
"contributions": [
"code"
]
},
{
"login": "audiolion",
"name": "Ryan Castner",
"avatar_url": "https://avatars1.githubusercontent.com/u/2430381?v=4",
"profile": "http://audiolion.github.io",
"contributions": [
"code"
]
},
{
"login": "silviuavram",
"name": "Silviu Alexandru Avram",
"avatar_url": "https://avatars2.githubusercontent.com/u/11275392?v=4",
"profile": "https://github.com/silviuavram",
"contributions": [
"bug",
"code",
"test"
]
},
{
"login": "akronb",
"name": "Anton Volkov",
"avatar_url": "https://avatars1.githubusercontent.com/u/15676655?v=4",
"profile": "https://github.com/akronb",
"contributions": [
"code",
"test"
]
},
{
"login": "keeganstreet",
"name": "Keegan Street",
"avatar_url": "https://avatars3.githubusercontent.com/u/513363?v=4",
"profile": "http://keegan.st",
"contributions": [
"bug",
"code"
]
},
{
"login": "mdugue",
"name": "Manuel Dugué",
"avatar_url": "https://avatars1.githubusercontent.com/u/894149?v=4",
"profile": "http://manueldugue.de",
"contributions": [
"code"
]
},
{
"login": "mkaradeniz",
"name": "Max Karadeniz",
"avatar_url": "https://avatars2.githubusercontent.com/u/12477983?v=4",
"profile": "https://github.com/mkaradeniz",
"contributions": [
"code"
]
},
{
"login": "GonchuB",
"name": "Gonzalo Beviglia",
"avatar_url": "https://avatars3.githubusercontent.com/u/857221?v=4",
"profile": "https://medium.com/@gonchub",
"contributions": [
"bug",
"code",
"review"
]
},
{
"login": "kilrain",
"name": "Brian Kilrain",
"avatar_url": "https://avatars2.githubusercontent.com/u/47700687?v=4",
"profile": "https://github.com/kilrain",
"contributions": [
"bug",
"code",
"test",
"doc"
]
},
{
"login": "rincedd",
"name": "Gerd Zschaler",
"avatar_url": "https://avatars0.githubusercontent.com/u/321265?v=4",
"profile": "http://www.gzschaler.de",
"contributions": [
"code",
"bug"
]
},
{
"login": "gaskar",
"name": "Karen Gasparyan",
"avatar_url": "https://avatars1.githubusercontent.com/u/491166?v=4",
"profile": "https://github.com/gaskar",
"contributions": [
"code"
]
},
{
"login": "kserjey",
"name": "Sergey Korchinskiy",
"avatar_url": "https://avatars1.githubusercontent.com/u/19753880?v=4",
"profile": "https://github.com/kserjey",
"contributions": [
"bug",
"code",
"test"
]
},
{
"login": "edygar",
"name": "Edygar Oliveira",
"avatar_url": "https://avatars.githubusercontent.com/u/566280?v=3",
"profile": "https://github.com/edygar",
"contributions": [
"code",
"bug"
]
},
{
"login": "epeicher",
"name": "epeicher",
"avatar_url": "https://avatars1.githubusercontent.com/u/3519124?v=4",
"profile": "https://github.com/epeicher",
"contributions": [
"bug"
]
},
{
"login": "francoischalifour",
"name": "François Chalifour",
"avatar_url": "https://avatars3.githubusercontent.com/u/6137112?v=4",
"profile": "https://francoischalifour.com",
"contributions": [
"code",
"test",
"platform"
]
},
{
"login": "maxmalov",
"name": "Maxim Malov",
"avatar_url": "https://avatars2.githubusercontent.com/u/284129?v=4",
"profile": "https://github.com/maxmalov",
"contributions": [
"bug",
"code"
]
},
{
"login": "epiqueras",
"name": "Enrique Piqueras",
"avatar_url": "https://avatars2.githubusercontent.com/u/19157096?v=4",
"profile": "https://epiqueras.github.io",
"contributions": [
"ideas"
]
},
{
"login": "layershifter",
"name": "Oleksandr Fediashov",
"avatar_url": "https://avatars0.githubusercontent.com/u/14183168?v=4",
"profile": "https://twitter.com/layershifter",
"contributions": [
"code",
"infra",
"ideas"
]
},
{
"login": "saitonakamura",
"name": "Mikhail Bashurov",
"avatar_url": "https://avatars1.githubusercontent.com/u/1552189?v=4",
"profile": "https://github.com/saitonakamura",
"contributions": [
"code",
"bug"
]
},
{
"login": "jgodi",
"name": "Joshua Godi",
"avatar_url": "https://avatars1.githubusercontent.com/u/870799?v=4",
"profile": "http://www.joshuagodi.com",
"contributions": [
"code"
]
},
{
"login": "lukyth",
"name": "Kanitkorn Sujautra",
"avatar_url": "https://avatars3.githubusercontent.com/u/7040242?v=4",
"profile": "https://github.com/lukyth",
"contributions": [
"bug",
"code"
]
},
{
"login": "jorgemoya",
"name": "Jorge Moya",
"avatar_url": "https://avatars3.githubusercontent.com/u/196129?v=4",
"profile": "https://github.com/jorgemoya",
"contributions": [
"code",
"bug"
]
},
{
"login": "KubaJastrz",
"name": "Jakub Jastrzębski",
"avatar_url": "https://avatars0.githubusercontent.com/u/6443113?v=4",
"profile": "https://kubajastrz.com",
"contributions": [
"code"
]
},
{
"login": "mufasa71",
"name": "Shukhrat Mukimov",
"avatar_url": "https://avatars1.githubusercontent.com/u/626420?v=4",
"profile": "https://github.com/mufasa71",
"contributions": [
"code"
]
},
{
"login": "jhonnymoreira",
"name": "Jhonny Moreira",
"avatar_url": "https://avatars0.githubusercontent.com/u/2177742?v=4",
"profile": "http://jhonnymoreira.dev",
"contributions": [
"code"
]
},
{
"login": "stefanprobst",
"name": "stefanprobst",
"avatar_url": "https://avatars0.githubusercontent.com/u/20753323?v=4",
"profile": "https://github.com/stefanprobst",
"contributions": [
"code",
"test"
]
},
{
"login": "louisaspicer",
"name": "Louisa Spicer",
"avatar_url": "https://avatars1.githubusercontent.com/u/20270031?v=4",
"profile": "https://github.com/louisaspicer",
"contributions": [
"code",
"bug"
]
},
{
"login": "neet",
"name": "Ryō Igarashi",
"avatar_url": "https://avatars2.githubusercontent.com/u/19276905?v=4",
"profile": "https://neet.love",
"contributions": [
"bug",
"code"
]
},
{
"login": "rlue",
"name": "Ryan Lue",
"avatar_url": "https://avatars2.githubusercontent.com/u/12194123?v=4",
"profile": "http://ryanlue.com/",
"contributions": [
"doc"
]
},
{
"login": "mattleonowicz",
"name": "Mateusz Leonowicz",
"avatar_url": "https://avatars3.githubusercontent.com/u/9438872?v=4",
"profile": "https://github.com/mattleonowicz",
"contributions": [
"code"
]
},
{
"login": "atomicpages",
"name": "Dennis Thompson",
"avatar_url": "https://avatars2.githubusercontent.com/u/1824291?v=4",
"profile": "https://github.com/atomicpages",
"contributions": [
"test"
]
},
{
"login": "mayicodefuture",
"name": "Maksym Boytsov",
"avatar_url": "https://avatars1.githubusercontent.com/u/32408893?v=4",
"profile": "https://mayicodefuture.live",
"contributions": [
"code"
]
},
{
"login": "IwalkAlone",
"name": "Sergey Skrynnikov",
"avatar_url": "https://avatars1.githubusercontent.com/u/5685800?v=4",
"profile": "http://dataart.com",
"contributions": [
"code",
"test"
]
},
{
"login": "vvo",
"name": "Vincent Voyer",
"avatar_url": "https://avatars0.githubusercontent.com/u/123822?v=4",
"profile": "https://www.linkedin.com/in/vvoyer",
"contributions": [
"doc"
]
},
{
"login": "limejoe",
"name": "limejoe",
"avatar_url": "https://avatars2.githubusercontent.com/u/7977551?v=4",
"profile": "https://github.com/limejoe",
"contributions": [
"code",
"bug"
]
},
{
"login": "k88manish",
"name": "Manish Kumar",
"avatar_url": "https://avatars2.githubusercontent.com/u/19614770?v=4",
"profile": "https://github.com/k88manish",
"contributions": [
"code"
]
},
{
"login": "fcrezza",
"name": "Anang Fachreza",
"avatar_url": "https://avatars2.githubusercontent.com/u/48123020?v=4",
"profile": "https://github.com/fcrezza",
"contributions": [
"doc",
"example"
]
},
{
"login": "ndeom",
"name": "Nick Deom",
"avatar_url": "https://avatars2.githubusercontent.com/u/56491159?v=4",
"profile": "http://nickdeom.com",
"contributions": [
"code",
"bug"
]
},
{
"login": "clementgarbay",
"name": "Clément Garbay",
"avatar_url": "https://avatars3.githubusercontent.com/u/12433625?v=4",
"profile": "https://github.com/clementgarbay",
"contributions": [
"code"
]
},
{
"login": "KaiminHuang",
"name": "Kaimin Huang",
"avatar_url": "https://avatars.githubusercontent.com/u/5600404?v=4",
"profile": "https://github.com/KaiminHuang",
"contributions": [
"code",
"bug"
]
},
{
"login": "davewelling",
"name": "David Welling",
"avatar_url": "https://avatars.githubusercontent.com/u/1242456?v=4",
"profile": "http://theredcircuit.com",
"contributions": [
"code",
"bug",
"ideas",
"research"
]
},
{
"login": "chandrasekhar1996",
"name": "chandrasekhar1996",
"avatar_url": "https://avatars.githubusercontent.com/u/33996892?v=4",
"profile": "https://github.com/chandrasekhar1996",
"contributions": [
"bug",
"code"
]
},
{
"login": "drewbrend",
"name": "Brendan Drew",
"avatar_url": "https://avatars.githubusercontent.com/u/5375799?v=4",
"profile": "https://github.com/drewbrend",
"contributions": [
"code"
]
},
{
"login": "jeanpan",
"name": "Jean Pan",
"avatar_url": "https://avatars.githubusercontent.com/u/1307026?v=4",
"profile": "https://github.com/jeanpan",
"contributions": [
"code"
]
},
{
"login": "tjenkinson",
"name": "Tom Jenkinson",
"avatar_url": "https://avatars.githubusercontent.com/u/3259993?v=4",
"profile": "https://tjenkinson.me",
"contributions": [
"infra"
]
},
{
"login": "aliceHendicott",
"name": "Alice Hendicott",
"avatar_url": "https://avatars.githubusercontent.com/u/40346716?v=4",
"profile": "https://github.com/aliceHendicott",
"contributions": [
"code",
"bug"
]
},
{
"login": "zmdavis",
"name": "Zach Davis",
"avatar_url": "https://avatars.githubusercontent.com/u/25305144?v=4",
"profile": "https://github.com/zmdavis",
"contributions": [
"code",
"bug"
]
}
],
"repoHost": "https://github.com",
"contributorsPerLine": 7,
"skipCi": true,
"commitConvention": "angular",
"commitType": "docs"
}
================================================
FILE: .flowconfig
================================================
[ignore]
.*/node_modules/
[include]
./test
[libs]
[lints]
all=error
[options]
================================================
FILE: .gitattributes
================================================
* text=auto
*.js text eol=lf
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
- `downshift` version:
- `node` version:
- `npm` (or `yarn`) version:
**Relevant code or config**
```javascript
```
**What you did**:
**What happened**:
**Reproduction repository**:
**Problem description**:
**Suggested solution**:
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
# Pull Request
## What
## Why
## How
## Changes
## Checklist
- [ ] Documentation
- [ ] Tests
- [ ] TypeScript Types
- [ ] Ready to be merged
================================================
FILE: .github/workflows/validate.yml
================================================
name: validate
on:
push:
branches:
- '+([0-9])?(.{+([0-9]),x}).x'
- 'master'
- 'next'
- 'next-major'
- 'beta'
- 'alpha'
- '!all-contributors/**'
pull_request: {}
jobs:
main:
# ignore all-contributors PRs
if: ${{ !contains(github.head_ref, 'all-contributors') }}
strategy:
matrix:
node: [20, 22, 24]
runs-on: ubuntu-latest
steps:
- name: 🛑 Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.12.1
with:
access_token: ${{ secrets.GITHUB_TOKEN }}
- name: ⬇️ Checkout repo
uses: actions/checkout@v5
- name: Increase watchers
run:
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
&& sudo sysctl -p
- name: ⎔ Setup node
uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node }}
- name: 📥 Download deps
uses: bahmutov/npm-install@v1
with:
useLockFile: false
- name: ▶️ Run validate script
run: npm run validate
env:
FORCE_COLOR: true
- name: ⬆️ Upload coverage report
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
release:
needs: main
runs-on: ubuntu-latest
if:
${{ github.repository == 'downshift-js/downshift' &&
contains('refs/heads/master,refs/heads/beta,refs/heads/next,refs/heads/alpha',
github.ref) && github.event_name == 'push' }}
steps:
- name: 🛑 Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.12.1
with:
access_token: ${{ secrets.GITHUB_TOKEN }}
- name: ⬇️ Checkout repo
uses: actions/checkout@v5
- name: ⎔ Setup node
uses: actions/setup-node@v5
with:
node-version: 24
- name: 📥 Download deps
uses: bahmutov/npm-install@v1
with:
useLockFile: false
- name: 🏗 Run build script
run: npm run build
- name: 🚀 Release
uses: cycjimmy/semantic-release-action@v6
with:
semantic_version: 24
branches: |
[
'+([0-9])?(.{+([0-9]),x}).x',
'master',
'next',
'next-major',
{name: 'beta', prerelease: true},
{name: 'alpha', prerelease: true}
]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .gitignore
================================================
node_modules
coverage
dist
.opt-in
.opt-out
.DS_Store
.next
.eslintcache
preact/
cypress/videos
cypress/screenshots
# these cause more harm than good
# when working with contributors
package-lock.json
yarn.lock
flow-coverage/
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
# IDE settings
.idea
================================================
FILE: .npmrc
================================================
registry=https://registry.npmjs.org/
package-lock=false
================================================
FILE: .nvmrc
================================================
16.14.0
================================================
FILE: .prettierignore
================================================
node_modules/
coverage/
dist/
preact/
package-lock.json
package.json
================================================
FILE: CHANGELOG.md
================================================
# CHANGELOG
The changelog is automatically updated using
[semantic-release](https://github.com/semantic-release/semantic-release). You
can see it on the [releases page](../../releases).
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
**Table of Contents**
- [Our Pledge](#our-pledge)
- [Our Standards](#our-standards)
- [Our Responsibilities](#our-responsibilities)
- [Scope](#scope)
- [Enforcement](#enforcement)
- [Attribution](#attribution)
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of
experience, nationality, personal appearance, race, religion, or sexual identity
and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, or to ban temporarily or permanently any
contributor for other behaviors that they deem inappropriate, threatening,
offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at kent+coc@doddsfamily.us. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an
incident. Further details of specific enforcement policies may be posted
separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
Thanks for being willing to contribute!
**Working on your first Pull Request?** You can learn how from this _free_
series [How to Contribute to an Open Source Project on GitHub][egghead]
## Project setup
1. Fork and clone the repo
2. `npm run setup` to setup and validate your clone of the project
3. Create a branch for your PR
> Tip: Keep your `master` branch pointing at the original repository and make
> pull requests from branches on your fork. To do this, run:
>
> ```
> git remote add upstream https://github.com/downshift-js/downshift.git
> git fetch upstream
> git branch --set-upstream-to=upstream/master master
> ```
>
> This will add the original repository as a "remote" called "upstream," Then
> fetch the git information from that remote, then set your local `master`
> branch to use the upstream master branch whenever you run `git pull`. Then you
> can make all of your pull request branches based on this `master` branch.
> Whenever you want to update your version of `master`, do a regular `git pull`.
## Committing and Pushing changes
Please make sure to run the tests before you commit your changes. You can run
`npm run test:update` which will update any snapshots that need updating. Make
sure to include those changes (if they exist) in your commit. We also track the
bundle sizes in a `.size-snapshot.json` file, this will likely update when you
make changes to the codebase.
### Tests
There are quite a few test scripts that run as part of a `validate` script in
this project:
- lint - ESLint stuff, pretty basic. Please fix any errors/warnings :)
- build-and-test - This ensures that the built version of `downshift` is what we
expect. These tests live in `other/misc-tests/__tests__`.
- test:cover - This is primarily unit tests on the source code and accounts for
most of the coverage. We enforce 100% code coverage on this library. These
tests live in `src/__tests__`
- test:ts - This runs `tsc` on the codebase to make sure the type script
definitions are correct for the `tsx` files in the `test` directory.
- test:ssr - This ensures that downshift works with server-side rendering (it
can run and render in an environment without the DOM). These tests live in
`other/ssr/__tests__`
- test:cypress - This runs tests in an actual browser. It runs and tests the
storybook examples. These tests live in `cypress/integration`.
### opt into git hooks
There are git hooks set up with this project that are automatically installed
when you install dependencies. They're really handy, but are turned off by
default (so as to not hinder new contributors). You can opt into these by
creating a file called `.opt-in` at the root of the project and putting this
inside:
```
pre-commit
```
## Help needed
Please checkout the [the open issues][issues]
Also, please watch the repo and respond to questions/bug reports/feature
requests! Thanks!
[egghead]:
https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github
[all-contributors]: https://github.com/kentcdodds/all-contributors
[issues]: https://github.com/downshift-js/downshift/issues
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2017 PayPal
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
downshift 🏎
Primitives to build simple, flexible, WAI-ARIA compliant React
autocomplete, combobox or select dropdown components.
> [Read the docs](https://downshift-js.com) |
> [See the intro blog post](https://kentcdodds.com/blog/introducing-downshift-for-react)
> |
> [Listen to the Episode 79 of the Full Stack Radio podcast](https://fullstackradio.com/79)
[![Build Status][build-badge]][build]
[![Code Coverage][coverage-badge]][coverage]
[![downloads][downloads-badge]][npmcharts] [![version][version-badge]][package]
[![MIT License][license-badge]][license]
[](#contributors)
[![PRs Welcome][prs-badge]][prs] [![Chat][chat-badge]][chat]
[![Code of Conduct][coc-badge]][coc]
[![Join the community on Spectrum][spectrum-badge]][spectrum]
[![Supports React and Preact][react-badge]][react]
[![size][size-badge]][unpkg-dist] [![gzip size][gzip-badge]][unpkg-dist]
[![module formats: umd, cjs, and es][module-formats-badge]][unpkg-dist]
## The problem
You need an autocomplete, a combobox or a select experience in your application
and you want it to be accessible. You also want it to be simple and flexible to
account for your use cases. Finally, it should follow the [ARIA][aria] design
pattern for a [combobox][combobox-aria-example] or a
[select][select-aria-example], depending on your use case.
## This solution
The library offers a couple of solutions. The first solution, which is the one
we recommend you to try first, is a set of React hooks. Each hook provides the
stateful logic needed to make the corresponding component functional and
accessible. Navigate to the documentation for each by using the links in the
list below.
- [useSelect][useselect-readme] for a custom select component.
- [useCombobox][combobox-readme] for a combobox or autocomplete input.
- [useTagGroup][tag-group-readme] for a tag group component. Also useful to
build a multiple selection combobox or select component with tags.
The second solution is the `Downshift` component, which can also be used to
create accessible combobox and select components, providing the logic in the
form of a render prop. It served as inspiration for developing the hooks and it
has been around for a while. It established a successful pattern for making
components accessible and functional while giving developers complete freedom
when building the UI.
Both _useSelect_ and _useCombobox_ support the latest ARIA combobox patterns for
W3C, which _Downshift_ does not. Consequently, we strongly recommend the you use
the hooks. The hooks have been migrated to the ARIA 1.2 combobox pattern in the
version 7 of _downshift_. There is a [Migration Guide][migration-guide-v7] that
documents the changes introduced in version 7.
The `README` on this page covers only the component while each hook has its own
`README` page. You can navigate to the [hooks page][hooks-readme] or go directly
to the hook you need by using the links in the list above.
For examples on how to use the hooks or the Downshift component, check out our
[docsite][docsite]!
**🚨 Use the Downshift hooks 🚨**
If you are new to the library, consider the _useSelect_ and _useCombobox_ hooks
as the first option. As mentioned above, the hooks benefit from the updated ARIA
patterns and are actively maintained and improved. If there are use cases that
are supported by the _Downshift_ component and not by the hooks, please create
an issue in our repo. The _Downshift_ component is going to be removed
completely once the hooks become mature.
### Downshift
This is a component that controls user interactions and state for you so you can
create autocomplete, combobox or select dropdown components. It uses a [render
prop][use-a-render-prop] which gives you maximum flexibility with a minimal API
because you are responsible for the rendering of everything and you simply apply
props to what you're rendering.
This differs from other solutions which render things for their use case and
then expose many options to allow for extensibility resulting in a bigger API
that is less flexible as well as making the implementation more complicated and
harder to contribute to.
> NOTE: The original use case of this component is autocomplete, however the API
> is powerful and flexible enough to build things like dropdowns as well.
## Table of Contents
- [Installation](#installation)
- [Usage](#usage)
- [Basic Props](#basic-props)
- [children](#children)
- [itemToString](#itemtostring)
- [onChange](#onchange)
- [stateReducer](#statereducer)
- [Advanced Props](#advanced-props)
- [initialSelectedItem](#initialselecteditem)
- [initialInputValue](#initialinputvalue)
- [initialHighlightedIndex](#initialhighlightedindex)
- [initialIsOpen](#initialisopen)
- [defaultHighlightedIndex](#defaulthighlightedindex)
- [defaultIsOpen](#defaultisopen)
- [selectedItemChanged](#selecteditemchanged)
- [getA11yStatusMessage](#geta11ystatusmessage)
- [onSelect](#onselect)
- [onStateChange](#onstatechange)
- [onInputValueChange](#oninputvaluechange)
- [itemCount](#itemcount)
- [highlightedIndex](#highlightedindex)
- [inputValue](#inputvalue)
- [isOpen](#isopen)
- [selectedItem](#selecteditem)
- [id](#id)
- [inputId](#inputid)
- [labelId](#labelid)
- [menuId](#menuid)
- [getItemId](#getitemid)
- [environment](#environment)
- [onOuterClick](#onouterclick)
- [scrollIntoView](#scrollintoview)
- [stateChangeTypes](#statechangetypes)
- [Control Props](#control-props)
- [Children Function](#children-function)
- [prop getters](#prop-getters)
- [actions](#actions)
- [state](#state)
- [props](#props)
- [Event Handlers](#event-handlers)
- [default handlers](#default-handlers)
- [customizing handlers](#customizing-handlers)
- [Utilities](#utilities)
- [resetIdCounter](#resetidcounter)
- [React Native](#react-native)
- [Gotchas](#gotchas)
- [Advanced React Component Patterns course](#advanced-react-component-patterns-course)
- [Examples](#examples)
- [FAQ](#faq)
- [Inspiration](#inspiration)
- [Other Solutions](#other-solutions)
- [Bindings for ReasonML](#bindings-for-reasonml)
- [Contributors](#contributors)
- [LICENSE](#license)
## Installation
This module is distributed via [npm][npm] which is bundled with [node][node] and
should be installed as one of your project's `dependencies`:
```
npm install --save downshift
```
> This package also depends on `react`. Please make sure you have it installed
> as well.
> Note also this library supports `preact` out of the box. If you are using
> `preact` then use the corresponding module in the `preact/dist` folder. You
> can even `import Downshift from 'downshift/preact'` 👍
## Usage
> [Try it out in the browser][code-sandbox-try-it-out]
```jsx
import * as React from 'react'
import {render} from 'react-dom'
import Downshift from 'downshift'
const items = [
{value: 'apple'},
{value: 'pear'},
{value: 'orange'},
{value: 'grape'},
{value: 'banana'},
]
render(
alert(selection ? `You selected ${selection.value}` : 'Selection Cleared')
}
itemToString={item => (item ? item.value : '')}
>
{({
getInputProps,
getItemProps,
getLabelProps,
getMenuProps,
isOpen,
inputValue,
highlightedIndex,
selectedItem,
getRootProps,
}) => (
)}
,
document.getElementById('root'),
)
```
There is also an [example without getRootProps][code-sandbox-no-get-root-props].
> Warning: The example without `getRootProps` is not fully accessible with
> screen readers as it's not possible to achieve the HTML structure suggested by
> ARIA. We recommend following the example with `getRootProps`. Examples on how
> to use `Downshift` component with and without `getRootProps` are on the
> [docsite](https://downshift-js.com/).
`Downshift` is the only component exposed by this package. It doesn't render
anything itself, it just calls the render function and renders that. ["Use a
render prop!"][use-a-render-prop]!
`{downshift => /* your JSX here! */
} `.
## Basic Props
This is the list of props that you should probably know about. There are some
[advanced props](#advanced-props) below as well.
### children
> `function({})` | _required_
This is called with an object. Read more about the properties of this object in
the section "[Children Function](#children-function)".
### itemToString
> `function(item: any)` | defaults to: `item => (item ? String(item) : '')`
If your items are stored as, say, objects instead of strings, downshift still
needs a string representation for each one (e.g., to set `inputValue`).
**Note:** This callback _must_ include a null check: it is invoked with `null`
whenever the user abandons input via ``.
### onChange
> `function(selectedItem: any, stateAndHelpers: object)` | optional, no useful
> default
Called when the selected item changes, either by the user selecting an item or
the user clearing the selection. Called with the item that was selected or
`null` and the new state of `downshift`. (see `onStateChange` for more info on
`stateAndHelpers`).
- `selectedItem`: The item that was just selected. `null` if the selection was
cleared.
- `stateAndHelpers`: This is the same thing your `children` function is called
with (see [Children Function](#children-function))
### stateReducer
> `function(state: object, changes: object)` | optional
**🚨 This is a really handy power feature 🚨**
This function will be called each time `downshift` sets its internal state (or
calls your `onStateChange` handler for control props). It allows you to modify
the state change that will take place which can give you fine grain control over
how the component interacts with user updates without having to use
[Control Props](#control-props). It gives you the current state and the state
that will be set, and you return the state that you want to set.
- `state`: The full current state of downshift.
- `changes`: These are the properties that are about to change. This also has a
`type` property which you can learn more about in the
[`stateChangeTypes`](#statechangetypes) section.
```jsx
const ui = (
{/* your callback */}
)
function stateReducer(state, changes) {
// this prevents the menu from being closed when the user
// selects an item with a keyboard or mouse
switch (changes.type) {
case Downshift.stateChangeTypes.keyDownEnter:
case Downshift.stateChangeTypes.clickItem:
return {
...changes,
isOpen: state.isOpen,
highlightedIndex: state.highlightedIndex,
}
default:
return changes
}
}
```
> NOTE: This is only called when state actually changes. You should not attempt
> to use this to handle events. If you wish to handle events, put your event
> handlers directly on the elements (make sure to use the prop getters though!
> For example: ` ` should be
> ` `). Also, your reducer
> function should be "pure." This means it should do nothing other than return
> the state changes you want to have happen.
## Advanced Props
### initialSelectedItem
> `any` | defaults to `null`
Pass an item or an array of items that should be selected when downshift is
initialized.
### initialInputValue
> `string` | defaults to `''`
This is the initial input value when downshift is initialized.
### initialHighlightedIndex
> `number`/`null` | defaults to `defaultHighlightedIndex`
This is the initial value to set the highlighted index to when downshift is
initialized.
### initialIsOpen
> `boolean` | defaults to `defaultIsOpen`
This is the initial `isOpen` value when downshift is initialized.
### defaultHighlightedIndex
> `number`/`null` | defaults to `null`
This is the value to set the `highlightedIndex` to anytime downshift is reset,
when the selection is cleared, when an item is selected or when the inputValue
is changed.
### defaultIsOpen
> `boolean` | defaults to `false`
This is the value to set the `isOpen` to anytime downshift is reset, when the
the selection is cleared, or when an item is selected.
### selectedItemChanged
> `function(prevItem: any, item: any)` | defaults to:
> `(prevItem, item) => (prevItem !== item)`
Used to determine if the new `selectedItem` has changed compared to the previous
`selectedItem` and properly update Downshift's internal state.
### getA11yStatusMessage
> `function({/* see below */})` | default messages provided in English
This function is passed as props to a `Status` component nested within and
allows you to create your own assertive ARIA statuses.
A default `getA11yStatusMessage` function is provided that will check
`resultCount` and return "No results are available." or if there are results ,
"`resultCount` results are available, use up and down arrow keys to navigate.
Press Enter key to select."
The object you are passed to generate your status message has the following
properties:
| property | type | description |
| --------------------- | --------------- | -------------------------------------------------------------------------------------------- |
| `highlightedIndex` | `number`/`null` | The currently highlighted index |
| `highlightedItem` | `any` | The value of the highlighted item |
| `inputValue` | `string` | The current input value |
| `isOpen` | `boolean` | The `isOpen` state |
| `itemToString` | `function(any)` | The `itemToString` function (see props) for getting the string value from one of the options |
| `previousResultCount` | `number` | The total items showing in the dropdown the last time the status was updated |
| `resultCount` | `number` | The total items showing in the dropdown |
| `selectedItem` | `any` | The value of the currently selected item |
### onSelect
> `function(selectedItem: any, stateAndHelpers: object)` | optional, no useful
> default
Called when the user selects an item, regardless of the previous selected item.
Called with the item that was selected and the new state of `downshift`. (see
`onStateChange` for more info on `stateAndHelpers`).
- `selectedItem`: The item that was just selected
- `stateAndHelpers`: This is the same thing your `children` function is called
with (see [Children Function](#children-function))
### onStateChange
> `function(changes: object, stateAndHelpers: object)` | optional, no useful
> default
This function is called anytime the internal state changes. This can be useful
if you're using downshift as a "controlled" component, where you manage some or
all of the state (e.g., isOpen, selectedItem, highlightedIndex, etc) and then
pass it as props, rather than letting downshift control all its state itself.
The parameters both take the shape of internal state
(`{highlightedIndex: number, inputValue: string, isOpen: boolean, selectedItem: any}`)
but differ slightly.
- `changes`: These are the properties that actually have changed since the last
state change. This also has a `type` property which you can learn more about
in the [`stateChangeTypes`](#statechangetypes) section.
- `stateAndHelpers`: This is the exact same thing your `children` function is
called with (see [Children Function](#children-function))
> Tip: This function will be called any time _any_ state is changed. The best
> way to determine whether any particular state was changed, you can use
> `changes.hasOwnProperty('propName')`.
> NOTE: This is only called when state actually changes. You should not attempt
> to use this to handle events. If you wish to handle events, put your event
> handlers directly on the elements (make sure to use the prop getters though!
> For example: ` ` should be
> ` `).
### onInputValueChange
> `function(inputValue: string, stateAndHelpers: object)` | optional, no useful
> default
Called whenever the input value changes. Useful to use instead or in combination
of `onStateChange` when `inputValue` is a controlled prop to
[avoid issues with cursor positions](https://github.com/downshift-js/downshift/issues/217).
- `inputValue`: The current value of the input
- `stateAndHelpers`: This is the same thing your `children` function is called
with (see [Children Function](#children-function))
### itemCount
> `number` | optional, defaults the number of times you call getItemProps
This is useful if you're using some kind of virtual listing component for
"windowing" (like
[`react-virtualized`](https://github.com/bvaughn/react-virtualized)).
### highlightedIndex
> `number` | **control prop** (read more about this in
> [the Control Props section](#control-props))
The index that should be highlighted
### inputValue
> `string` | **control prop** (read more about this in
> [the Control Props section](#control-props))
The value the input should have
### isOpen
> `boolean` | **control prop** (read more about this in
> [the Control Props section](#control-props))
Whether the menu should be considered open or closed. Some aspects of the
downshift component respond differently based on this value (for example, if
`isOpen` is true when the user hits "Enter" on the input field, then the item at
the `highlightedIndex` item is selected).
### selectedItem
> `any`/`Array(any)` | **control prop** (read more about this in
> [the Control Props section](#control-props))
The currently selected item.
### id
> `string` | defaults to a generated ID
You should not normally need to set this prop. It's only useful if you're server
rendering items (which each have an `id` prop generated based on the `downshift`
`id`). For more information see the `FAQ` below.
### inputId
> `string` | defaults to a generated ID
Used for `aria` attributes and the `id` prop of the element (`input`) you use
[`getInputProps`](#getinputprops) with.
### labelId
> `string` | defaults to a generated ID
Used for `aria` attributes and the `id` prop of the element (`label`) you use
[`getLabelProps`](#getlabelprops) with.
### menuId
> `string` | defaults to a generated ID
Used for `aria` attributes and the `id` prop of the element (`ul`) you use
[`getMenuProps`](#getmenuprops) with.
### getItemId
> `function(index)` | defaults to a function that generates an ID based on the
> index
Used for `aria` attributes and the `id` prop of the element (`li`) you use
[`getInputProps`](#getinputprops) with.
### environment
> `window` | defaults to `window`
This prop is only useful if you're rendering downshift within a different
`window` context from where your JavaScript is running; for example, an iframe
or a shadow-root. If the given context is lacking `document` and/or
`add|removeEventListener` on its prototype (as is the case for a shadow-root)
then you will need to pass in a custom object that is able to provide
[access to these properties](https://gist.github.com/Rendez/1dd55882e9b850dd3990feefc9d6e177)
for downshift.
### onOuterClick
> `function(stateAndHelpers: object)` | optional
A helper callback to help control internal state of downshift like `isOpen` as
mentioned in [this issue](https://github.com/downshift-js/downshift/issues/206).
The same behavior can be achieved using `onStateChange`, but this prop is
provided as a helper because it's a fairly common use-case if you're controlling
the `isOpen` state:
```jsx
const ui = (
this.setState({menuIsOpen: false})}
>
{/* your callback */}
)
```
This callback will only be called if `isOpen` is `true`.
### scrollIntoView
> `function(node: HTMLElement, menuNode: HTMLElement)` | defaults to internal
> implementation
This allows you to customize how the scrolling works when the highlighted index
changes. It receives the node to be scrolled to and the root node (the root node
you render in downshift). Internally we use
[`compute-scroll-into-view`](https://www.npmjs.com/package/compute-scroll-into-view)
so if you use that package then you wont be adding any additional bytes to your
bundle :)
## stateChangeTypes
There are a few props that expose changes to state
([`onStateChange`](#onstatechange) and [`stateReducer`](#statereducer)). For you
to make the most of these APIs, it's important for you to understand why state
is being changed. To accomplish this, there's a `type` property on the `changes`
object you get. This `type` corresponds to a `Downshift.stateChangeTypes`
property.
The list of all possible values this `type` property can take is defined in
[this file](https://github.com/downshift-js/downshift/blob/master/src/stateChangeTypes.js)
and is as follows:
- `Downshift.stateChangeTypes.unknown`
- `Downshift.stateChangeTypes.mouseUp`
- `Downshift.stateChangeTypes.itemMouseEnter`
- `Downshift.stateChangeTypes.keyDownArrowUp`
- `Downshift.stateChangeTypes.keyDownArrowDown`
- `Downshift.stateChangeTypes.keyDownEscape`
- `Downshift.stateChangeTypes.keyDownEnter`
- `Downshift.stateChangeTypes.keyDownHome`
- `Downshift.stateChangeTypes.keyDownEnd`
- `Downshift.stateChangeTypes.clickItem`
- `Downshift.stateChangeTypes.blurInput`
- `Downshift.stateChangeTypes.changeInput`
- `Downshift.stateChangeTypes.keyDownSpaceButton`
- `Downshift.stateChangeTypes.clickButton`
- `Downshift.stateChangeTypes.blurButton`
- `Downshift.stateChangeTypes.controlledPropUpdatedSelectedItem`
- `Downshift.stateChangeTypes.touchEnd`
See [`stateReducer`](#statereducer) for a concrete example on how to use the
`type` property.
## Control Props
downshift manages its own state internally and calls your `onChange` and
`onStateChange` handlers with any relevant changes. The state that downshift
manages includes: `isOpen`, `selectedItem`, `inputValue`, and
`highlightedIndex`. Your Children function (read more below) can be used to
manipulate this state and can likely support many of your use cases.
However, if more control is needed, you can pass any of these pieces of state as
a prop (as indicated above) and that state becomes controlled. As soon as
`this.props[statePropKey] !== undefined`, internally, `downshift` will determine
its state based on your prop's value rather than its own internal state. You
will be required to keep the state up to date (this is where `onStateChange`
comes in really handy), but you can also control the state from anywhere, be
that state from other components, `redux`, `react-router`, or anywhere else.
> Note: This is very similar to how normal controlled components work elsewhere
> in react (like ` `). If you want to learn more about this concept, you
> can learn about that from this the
> [Advanced React Component Patterns course](#advanced-react-component-patterns-course)
## Children Function
This is where you render whatever you want to based on the state of `downshift`.
You use it like so:
```javascript
const ui = (
{downshift => (
// use downshift utilities and state here, like downshift.isOpen,
// downshift.getInputProps, etc.
{/* more jsx here */}
)}
)
```
The properties of this `downshift` object can be split into three categories as
indicated below:
### prop getters
> See
> [the blog post about prop getters](https://kentcdodds.com/blog/how-to-give-rendering-control-to-users-with-prop-getters)
> NOTE: These prop-getters provide important `aria-` attributes which are very
> important to your component being accessible. It's recommended that you
> utilize these functions and apply the props they give you to your components.
These functions are used to apply props to the elements that you render. This
gives you maximum flexibility to render what, when, and wherever you like. You
call these on the element in question (for example:
`
| property | type | description |
| ---------------------- | ----------------- | ---------------------------------------------------------------------------------------------- |
| `getToggleButtonProps` | `function({})` | returns the props you should apply to any menu toggle button element you render. |
| `getInputProps` | `function({})` | returns the props you should apply to the `input` element that you render. |
| `getItemProps` | `function({})` | returns the props you should apply to any menu item elements you render. |
| `getLabelProps` | `function({})` | returns the props you should apply to the `label` element that you render. |
| `getMenuProps` | `function({},{})` | returns the props you should apply to the `ul` element (or root of your menu) that you render. |
| `getRootProps` | `function({},{})` | returns the props you should apply to the root element that you render. It can be optional. |
#### `getRootProps`
If you cannot render a div as the root element, then read this
Most of the time, you can just render a `div` yourself and `Downshift` will
apply the props it needs to do its job (and you don't need to call this
function). However, if you're rendering a composite component (custom component)
as the root element, then you'll need to call `getRootProps` and apply that to
your root element (downshift will throw an error otherwise).
There are no required properties for this method.
Optional properties:
- `refKey`: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call this `innerRef`. So you'd call: `getRootProps({refKey: 'innerRef'})` and
your composite component would forward like: `
`.
It defaults to `ref`.
If you're rendering a composite component, `Downshift` checks that
`getRootProps` is called and that `refKey` is a prop of the returned composite
component. This is done to catch common causes of errors but, in some cases, the
check could fail even if the ref is correctly forwarded to the root DOM
component. In these cases, you can provide the object
`{suppressRefError : true}` as the second argument to `getRootProps` to
completely bypass the check.\
**Please use it with extreme care and only if you are absolutely sure that the
ref is correctly forwarded otherwise `Downshift` will unexpectedly fail.**\
See [#235](https://github.com/downshift-js/downshift/issues/235) for the
discussion that lead to this.
#### `getInputProps`
This method should be applied to the `input` you render. It is recommended that
you pass all props as an object to this method which will compose together any
of the event handlers you need to apply to the `input` while preserving the ones
that `downshift` needs to apply to make the `input` behave.
There are no required properties for this method.
Optional properties:
- `disabled`: If this is set to true, then no event handlers will be returned
from `getInputProps` and a `disabled` prop will be returned (effectively
disabling the input).
- `aria-label`: By default the menu will add an `aria-labelledby` that refers to
the `` rendered with `getLabelProps`. However, if you provide
`aria-label` to give a more specific label that describes the options
available, then `aria-labelledby` will not be provided and screen readers can
use your `aria-label` instead.
#### `getLabelProps`
This method should be applied to the `label` you render. It is useful for
ensuring that the `for` attribute on the `` (`htmlFor` as a react prop)
is the same as the `id` that appears on the `input`. If no `htmlFor` is provided
(the normal case) then an ID will be generated and used for the `input` and the
`label` `for` attribute.
There are no required properties for this method.
> Note: For accessibility purposes, calling this method is highly recommended.
#### `getMenuProps`
This method should be applied to the element which contains your list of items.
Typically, this will be a `` or a `
` that surrounds a `map` expression.
This handles the proper ARIA roles and attributes.
Optional properties:
- `refKey`: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call this `innerRef`. So you'd call: `getMenuProps({refKey: 'innerRef'})` and
your composite component would forward like: ``.
However, if you are just rendering a primitive component like ``, there
is no need to specify this property. It defaults to `ref`.
Please keep in mind that menus, for accessibility purposes, should always be
rendered, regardless of whether you hide it or not. Otherwise, `getMenuProps`
may throw error if you unmount and remount the menu.
- `aria-label`: By default the menu will add an `aria-labelledby` that refers to
the `
` rendered with `getLabelProps`. However, if you provide
`aria-label` to give a more specific label that describes the options
available, then `aria-labelledby` will not be provided and screen readers can
use your `aria-label` instead.
In some cases, you might want to completely bypass the `refKey` check. Then you
can provide the object `{suppressRefError : true}` as the second argument to
`getMenuProps`. **Please use it with extreme care and only if you are absolutely
sure that the ref is correctly forwarded otherwise `Downshift` will unexpectedly
fail.**
```jsx
{!isOpen
? null
: items.map((item, index) => (
{item.name}
))}
```
> Note that for accessibility reasons it's best if you always render this
> element whether or not downshift is in an `isOpen` state.
#### `getItemProps`
The props returned from calling this function should be applied to any menu
items you render.
**This is an impure function**, so it should only be called when you will
actually be applying the props to an item.
What do you mean by impure function?
Basically just don't do this:
```jsx
items.map(item => {
const props = getItemProps({item}) // we're calling it here
if (!shouldRenderItem(item)) {
return null // but we're not using props, and downshift thinks we are...
}
return
})
```
Instead, you could do this:
```jsx
items.filter(shouldRenderItem).map(item =>
)
```
Required properties:
- `item`: this is the item data that will be selected when the user selects a
particular item.
Optional properties:
- `index`: This is how `downshift` keeps track of your item when updating the
`highlightedIndex` as the user keys around. By default, `downshift` will
assume the `index` is the order in which you're calling `getItemProps`. This
is often good enough, but if you find odd behavior, try setting this
explicitly. It's probably best to be explicit about `index` when using a
windowing library like `react-virtualized`.
- `disabled`: If this is set to `true`, then all of the downshift item event
handlers will be omitted. Items will not be highlighted when hovered, and
items will not be selected when clicked.
#### `getToggleButtonProps`
Call this and apply the returned props to a `button`. It allows you to toggle
the `Menu` component. You can definitely build something like this yourself (all
of the available APIs are exposed to you), but this is nice because it will also
apply all of the proper ARIA attributes.
Optional properties:
- `disabled`: If this is set to `true`, then all of the downshift button event
handlers will be omitted (it wont toggle the menu when clicked).
- `aria-label`: The `aria-label` prop is in English. You should probably
override this yourself so you can provide translations:
```jsx
const myButton = (
)
```
### actions
These are functions you can call to change the state of the downshift component.
| property | type | description |
| ----------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clearSelection` | `function(cb: Function)` | clears the selection |
| `clearItems` | `function()` | Clears downshift's record of all the items. Only really useful if you render your items asynchronously within downshift. See [#186](https://github.com/downshift-js/downshift/issues/186) |
| `closeMenu` | `function(cb: Function)` | closes the menu |
| `openMenu` | `function(cb: Function)` | opens the menu |
| `selectHighlightedItem` | `function(otherStateToSet: object, cb: Function)` | selects the item that is currently highlighted |
| `selectItem` | `function(item: any, otherStateToSet: object, cb: Function)` | selects the given item |
| `selectItemAtIndex` | `function(index: number, otherStateToSet: object, cb: Function)` | selects the item at the given index |
| `setHighlightedIndex` | `function(index: number, otherStateToSet: object, cb: Function)` | call to set a new highlighted index |
| `toggleMenu` | `function(otherStateToSet: object, cb: Function)` | toggle the menu open state |
| `reset` | `function(otherStateToSet: object, cb: Function)` | this resets downshift's state to a reasonable default |
| `setItemCount` | `function(count: number)` | this sets the `itemCount`. Handy in situations where you're using windowing and the items are loaded asynchronously from within downshift (so you can't use the `itemCount` prop. |
| `unsetItemCount` | `function()` | this unsets the `itemCount` which means the item count will be calculated instead by the `itemCount` prop or based on how many times you call `getItemProps`. |
| `setState` | `function(stateToSet: object, cb: Function)` | This is a general `setState` function. It uses downshift's `internalSetState` function which works with control props and calls your `onSelect`, `onChange`, etc. (Note, you can specify a `type` which you can reference in some other APIs like the `stateReducer`). |
> `otherStateToSet` refers to an object to set other internal state. It is
> recommended to avoid abusing this, but is available if you need it.
### state
These are values that represent the current state of the downshift component.
| property | type | description |
| ------------------ | ----------------- | ---------------------------------------------- |
| `highlightedIndex` | `number` / `null` | the currently highlighted item |
| `inputValue` | `string` / `null` | the current value of the `getInputProps` input |
| `isOpen` | `boolean` | the menu open state |
| `selectedItem` | `any` | the currently selected item input |
### props
As a convenience, the `id` and `itemToString` props which you pass to
` ` are available here as well.
## Event Handlers
Downshift has a few events for which it provides implicit handlers. Several of
these handlers call `event.preventDefault()`. Their additional functionality is
described below.
### default handlers
- `ArrowDown`: if menu is closed, opens it and moves the highlighted index to
`defaultHighlightedIndex + 1`, if `defaultHighlightedIndex` is provided, or to
the top-most item, if not. If menu is open, it moves the highlighted index
down by 1. If the shift key is held when this event fires, the highlighted
index will jump down 5 indices instead of 1. NOTE: if the current highlighted
index is within the bottom 5 indices, the top-most index will be highlighted.)
- `ArrowUp`: if menu is closed, opens it and moves the highlighted index to
`defaultHighlightedIndex - 1`, if `defaultHighlightedIndex` is provided, or to
the bottom-most item, if not. If menu is open, moves the highlighted index up
by 1. If the shift key is held when this event fires, the highlighted index
will jump up 5 indices instead of 1. NOTE: if the current highlighted index is
within the top 5 indices, the bottom-most index will be highlighted.)
- `Home`: if menu is closed, it will not add any other behavior. If menu is
open, the top-most index will get highlighted.
- `End`: if menu is closed, it will not add any other behavior. If menu is open,
the bottom-most index will get highlighted.
- `Enter`: if the menu is open, selects the currently highlighted item. If the
menu is open, the usual 'Enter' event is prevented by Downshift's default
implicit enter handler; so, for example, a form submission event will not work
as one might expect (though if the menu is closed the form submission will
work normally). See below for customizing the handlers.
- `Escape`: will clear downshift's state. This means that `highlightedIndex`
will be set to the `defaultHighlightedIndex` and the `isOpen` state will be
set to the `defaultIsOpen`. If `isOpen` is already false, the `inputValue`
will be set to an empty string and `selectedItem` will be set to `null`
### customizing handlers
You can provide your own event handlers to Downshift which will be called before
the default handlers:
```javascript
const ui = (
{({getInputProps}) => (
{
// your handler code
},
})}
/>
)}
)
```
If you would like to prevent the default handler behavior in some cases, you can
set the event's `preventDownshiftDefault` property to `true`:
```javascript
const ui = (
{({getInputProps}) => (
{
if (event.key === 'Enter') {
// Prevent Downshift's default 'Enter' behavior.
event.nativeEvent.preventDownshiftDefault = true
// your handler code
}
},
})}
/>
)}
)
```
If you would like to completely override Downshift's behavior for a handler, in
favor of your own, you can bypass prop getters:
```javascript
const ui = (
{({getInputProps}) => (
{
// your handler code
}}
/>
)}
)
```
## Utilities
### resetIdCounter
Allows reseting the internal id counter which is used to generate unique ids for
Downshift component.
**This is unnecessary if you are using React 18 or newer**
You should never need to use this in the browser. Only if you are running an
universal React app that is rendered on the server you should call
[resetIdCounter](#resetidcounter) before every render so that the ids that get
generated on the server match the ids generated in the browser.
```javascript
import {resetIdCounter} from 'downshift';
resetIdCounter()
ReactDOMServer.renderToString(...);
```
## React Native
Since Downshift renders it's UI using render props, Downshift supports rendering
on React Native with ease. Use components like ``, ``,
`` and others inside of your render method to generate awesome
autocomplete, dropdown, or selection components.
### Gotchas
- Your root view will need to either pass a ref to `getRootProps` or call
`getRootProps` with `{ suppressRefError: true }`. This ref is used to catch a
common set of errors around composite components.
[Learn more in `getRootProps`](#getrootprops).
- When using a `` or ``, be sure to supply the
[`keyboardShouldPersistTaps`](https://facebook.github.io/react-native/docs/scrollview.html#keyboardshouldpersisttaps)
prop to ensure that your text input stays focus, while allowing for taps on
the touchables rendered for your items.
## Advanced React Component Patterns course
[Kent C. Dodds](https://twitter.com/kentcdodds) has created learning material
based on the patterns implemented in this component. You can find it on various
platforms:
1. [egghead.io](https://egghead.io/courses/advanced-react-component-patterns)
2. [Frontend Masters](https://frontendmasters.com/courses/advanced-react-patterns/)
3. YouTube (for free!):
[Part 1](https://www.youtube.com/watch?v=SuzutbwjUp8&list=PLV5CVI1eNcJgNqzNwcs4UKrlJdhfDjshf)
and
[Part 2](https://www.youtube.com/watch?v=ubXtOROjILU&list=PLV5CVI1eNcJgNqzNwcs4UKrlJdhfDjshf)
## Examples
> 🚨 We're in the process of moving all examples to the
> [downshift-examples](https://github.com/downshift-js/downshift-examples) repo
> (which you can open, interact with, and contribute back to live on
> [codesandbox](https://codesandbox.io/p/sandbox/github/kentcdodds/downshift-examples?file=%2Fsrc%2Findex.js&moduleview=1))
> 🚨 We're also in the process of updating our examples from the
> [downshift-docs](https://github.com/downshift-js/downshift-docs) repo which is
> actually used to create our docsite at [downshift-js.com][docsite]). Make sure
> to check it out for the most relevant Downshift examples or try out the new
> hooks that aim to replace Downshift.
**Ordered Examples:**
If you're just learning downshift, review these in order:
0. [basic automplete with getRootProps](https://codesandbox.io/p/sandbox/github/kentcdodds/downshift-examples?file=%2Fsrc%2Fdownshift%2Fordered-examples%2F00-get-root-props-example.js%3A11%2C21&moduleview=1) -
the same as example #1 but using the correct HTML structure as suggested by
ARIA-WCAG.
1. [basic autocomplete](https://codesandbox.io/p/sandbox/github/kentcdodds/downshift-examples?file=%2Fsrc%2Fdownshift%2Fordered-examples%2F01-basic-autocomplete.js&moduleview=1) -
very bare bones, not styled at all. Good place to start.
2. [styled autocomplete](https://codesandbox.io/p/sandbox/github/kentcdodds/downshift-examples?file=%2Fsrc%2Fdownshift%2Fordered-examples%2F02-complete-autocomplete.js&moduleview=1) -
more complete autocomplete solution using emotion for styling and
match-sorter for filtering the items.
3. [typeahead](https://codesandbox.io/p/sandbox/github/kentcdodds/downshift-examples?file=%2Fsrc%2Fdownshift%2Fordered-examples%2F03-typeahead.js&moduleview=1) -
Shows how to control the `selectedItem` so the selected item can be one of
your items or whatever the user types.
4. [multi-select](https://codesandbox.io/p/sandbox/github/kentcdodds/downshift-examples?file=%2Fsrc%2Fdownshift%2Fordered-examples%2F04-multi-select.js&moduleview=1) -
Shows how to create a MultiDownshift component that allows for an array of
selectedItems for multiple selection using a state reducer
**Other Examples:**
Check out these examples of more advanced use/edge cases:
- [dropdown with select by key](https://codesandbox.io/p/sandbox/github/kentcdodds/downshift-examples?file=%2Fsrc%2Fdownshift%2Fother-examples%2Fdropdown-select-by-key%2FCustomDropdown%2Findex.js&moduleview=1) -
An example of using the render prop pattern to utilize a reusable component to
provide the downshift dropdown component with the functionality of being able
to highlight a selection item that starts with the key pressed.
- [using actions](https://codesandbox.io/p/sandbox/github/kentcdodds/downshift-examples?file=%2Fsrc%2Fdownshift%2Fother-examples%2Fusing-actions.js&moduleview=1) -
An example of using one of downshift's actions as an event handler.
- [gmail's composition recipients field](https://codesandbox.io/p/sandbox/github/kentcdodds/downshift-examples?file=%2Fsrc%2Fdownshift%2Fother-examples%2Fgmail%2Findex.js&moduleview=1) -
An example of a highly complex autocomplete component featuring asynchronously
loading items, multiple selection, and windowing (with react-virtualized)
- [Downshift HOC and Compound Components](https://codesandbox.io/p/sandbox/github/kentcdodds/downshift-examples?file=%2Fsrc%2Fdownshift%2Fother-examples%2Fhoc%2Findex.js&moduleview=1) -
An example of how to implementat compound components with
`React.createContext` and a downshift higher order component. This is
generally not recommended because the render prop API exported by downshift is
generally good enough for everyone, but there's nothing technically wrong with
doing something like this.
**Old Examples exist on [codesandbox.io][examples]:**
_🚨 This is a great contribution opportunity!_ These are examples that have not
yet been migrated to
[downshift-examples](https://codesandbox.io/p/sandbox/github/kentcdodds/downshift-examples?file=%2Fsrc%2Findex.js&moduleview=1).
You're more than welcome to make PRs to the examples repository to move these
examples over there.
[Watch this to learn how to contribute completely in the browser](https://www.youtube.com/watch?v=3PAQbhdkTtI&index=2&t=21s&list=PLV5CVI1eNcJgCrPH_e6d57KRUTiDZgs0u)
- [Integration with Apollo](https://codesandbox.io/s/m5zrvqj85p)
- [Integration with Redux](https://codesandbox.io/s/3ywmnyr0zq)
- [Integration with `react-instantsearch`](https://codesandbox.io/s/kvn0lpp83)
from Algolia
- [Material-UI (1.0.0-beta.4) Combobox Using Downshift](https://codesandbox.io/s/QMGq4kAY)
- [Material-UI (1.0.0-beta.33) Multiple select with autocomplete](https://codesandbox.io/s/7k3674z09q)
- [Integration with `GenieJS`](https://codesandbox.io/s/jRLKrxwgl)
([learn more about `genie` here](https://github.com/kentcdodds/genie))
- [Handling and displaying errors](https://codesandbox.io/s/zKE37vorr)
- [Integration with React Router](https://codesandbox.io/s/ww9lwloy8w)
- [Windowing with `react-tiny-virtual-list`](https://codesandbox.io/s/v670kq95l)
- [Section/option group example](https://codesandbox.io/s/zx1kj58npl)
- [Integration with `fuzzaldrin-plus` (Fuzzy matching)](https://codesandbox.io/s/pyq3v4o3j)
- [Dropdown/select implementation with Bootstrap](https://codesandbox.io/s/53y8jvpj0k)
- [Multiple editable tag selection](https://codesandbox.io/s/o4yp9vmm8z)
- [Downshift implemented as compound components and a Higher Order Component](https://codesandbox.io/s/017n1jqo00)
(exposes a `withDownshift` higher order component which you can use to get at
the state, actions, prop getters in a rendered downshift tree).
- [Downshift Spectre.css example](https://codesandbox.io/s/M89KQOBRB)
- [Integration with `redux-form`](https://codesandbox.io/s/k594964z13)
- [Integration with `react-final-form`](https://codesandbox.io/s/qzm43nn2mj)
- [Provider Pattern](https://codesandbox.io/s/mywzk3133p) - how to avoid
prop-drilling if you like to break up your render method into more components
- [React Native example](https://snack.expo.io/SkE0LxXqM)
- [React VR example](https://github.com/infiniteluke/bassdrop)
- [Multiple checkbox selection](https://codesandbox.io/s/5z711pmr3l)
## FAQ
How do I avoid the checksum error when server rendering (SSR)?
The checksum error you're seeing is most likely due to the automatically
generated `id` and/or `htmlFor` prop you get from `getInputProps` and
`getLabelProps` (respectively). It could also be from the automatically
generated `id` prop you get from `getItemProps` (though this is not likely as
you're probably not rendering any items when rendering a downshift component on
the server).
To avoid these problems, simply call [resetIdCounter](#resetidcounter) before
`ReactDOM.renderToString`.
Alternatively you could provide your own ids via the id props where you render
` `:
```javascript
const ui = (
{({getInputProps, getLabelProps}) => {/* your UI */}
}
)
```
## Inspiration
I was heavily inspired by [Ryan Florence][ryan]. Watch his (free) lesson about
["Compound Components"][compound-components-lecture]. Initially downshift was a
group of compound components using context to communicate. But then [Jared
Forsyth][jared] suggested I expose functions (the prop getters) to get props to
apply to the elements rendered. That bit of inspiration made a big impact on the
flexibility and simplicity of this API.
I also took a few ideas from the code in
[`react-autocomplete`][react-autocomplete] and [jQuery UI's
Autocomplete][jquery-complete].
You can watch me build the first iteration of `downshift` on YouTube:
- [Part 1](https://www.youtube.com/watch?v=2kzD1IjDy5s&list=PLV5CVI1eNcJh5CTgArGVwANebCrAh2OUE&index=11)
- [Part 2](https://www.youtube.com/watch?v=w1Z7Jvj08_s&list=PLV5CVI1eNcJh5CTgArGVwANebCrAh2OUE&index=10)
You'll find more recordings of me working on `downshift` on [my livestream
YouTube playlist][yt-playlist].
## Other Solutions
You can implement these other solutions using `downshift`, but if you'd prefer
to use these out of the box solutions, then that's fine too:
- [`react-select`](https://github.com/JedWatson/react-select)
- [`react-autosuggest`](https://github.com/moroshko/react-autosuggest)
## Bindings for ReasonML
If you're developing some React in ReasonML, check out the
[`Downshift` bindings](https://github.com/reasonml-community/bs-downshift) for
that.
## Contributors
Thanks goes to these people ([emoji key][emojis]):
This project follows the [all-contributors][all-contributors] specification.
Contributions of any kind welcome!
## LICENSE
MIT
[npm]: https://www.npmjs.com/
[node]: https://nodejs.org
[build-badge]:
https://img.shields.io/github/actions/workflow/status/downshift-js/downshift/validate.yml?branch=master&logo=github&style=flat-square
[build]:
https://github.com/downshift-js/downshift/actions?query=workflow%3Avalidate+branch%3Amaster
[coverage-badge]:
https://img.shields.io/codecov/c/github/downshift-js/downshift.svg?style=flat-square
[coverage]: https://codecov.io/github/downshift-js/downshift
[version-badge]: https://img.shields.io/npm/v/downshift.svg?style=flat-square
[package]: https://www.npmjs.com/package/downshift
[downloads-badge]: https://img.shields.io/npm/dm/downshift.svg?style=flat-square
[npmcharts]: http://npmcharts.com/compare/downshift
[license-badge]: https://img.shields.io/npm/l/downshift.svg?style=flat-square
[license]: https://github.com/downshift-js/downshift/blob/master/LICENSE
[prs-badge]:
https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
[prs]: http://makeapullrequest.com
[chat]: https://gitter.im/downshift-js/downshift
[chat-badge]:
https://img.shields.io/gitter/room/downshift-js/downshift.svg?style=flat-square
[coc-badge]:
https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square
[coc]: https://github.com/downshift-js/downshift/blob/master/CODE_OF_CONDUCT.md
[react-badge]:
https://img.shields.io/badge/%E2%9A%9B%EF%B8%8F-(p)react-00d8ff.svg?style=flat-square
[react]: https://facebook.github.io/react/
[gzip-badge]:
http://img.badgesize.io/https://unpkg.com/downshift/dist/downshift.umd.min.js?compression=gzip&label=gzip%20size&style=flat-square
[size-badge]:
http://img.badgesize.io/https://unpkg.com/downshift/dist/downshift.umd.min.js?label=size&style=flat-square
[unpkg-dist]: https://unpkg.com/downshift/dist/
[module-formats-badge]:
https://img.shields.io/badge/module%20formats-umd%2C%20cjs%2C%20es-green.svg?style=flat-square
[spectrum-badge]: https://withspectrum.github.io/badge/badge.svg
[spectrum]: https://spectrum.chat/downshift
[emojis]: https://github.com/kentcdodds/all-contributors#emoji-key
[all-contributors]: https://github.com/kentcdodds/all-contributors
[ryan]: https://github.com/ryanflorence
[compound-components-lecture]:
https://courses.reacttraining.com/courses/advanced-react/lectures/3060560
[react-autocomplete]: https://www.npmjs.com/package/react-autocomplete
[jquery-complete]: https://jqueryui.com/autocomplete/
[examples]:
https://codesandbox.io/search?refinementList%5Btags%5D%5B0%5D=downshift%3Aexample&page=1
[yt-playlist]:
https://www.youtube.com/playlist?list=PLV5CVI1eNcJh5CTgArGVwANebCrAh2OUE
[jared]: https://github.com/jaredly
[controlled-components-lecture]:
https://courses.reacttraining.com/courses/advanced-react/lectures/3172720
[react-training]: https://reacttraining.com/
[advanced-react]: https://courses.reacttraining.com/courses/enrolled/200086
[use-a-render-prop]: https://medium.com/@mjackson/use-a-render-prop-50de598f11ce
[semver]: http://semver.org/
[hooks-readme]: https://github.com/downshift-js/downshift/blob/master/src/hooks
[useselect-readme]:
https://github.com/downshift-js/downshift/blob/master/src/hooks/useSelect
[combobox-readme]:
https://github.com/downshift-js/downshift/tree/master/src/hooks/useCombobox
[tag-group-readme]:
https://github.com/downshift-js/downshift/tree/master/src/hooks/useTagGroup
[bundle-phobia-link]: https://bundlephobia.com/result?p=downshift@3.4.8
[aria]: https://www.w3.org/TR/wai-aria-practices/
[combobox-aria-example]:
https://www.w3.org/WAI/ARIA/apg/example-index/combobox/combobox-autocomplete-list.html
[select-aria-example]:
https://www.w3.org/WAI/ARIA/apg/example-index/combobox/combobox-select-only.html
[docsite]: https://downshift-js.com/
[code-sandbox-try-it-out]:
https://codesandbox.io/p/sandbox/github/kentcdodds/downshift-examples?file=%2Fsrc%2Fdownshift%2Fordered-examples%2F00-get-root-props-example.js&moduleview=1
[code-sandbox-no-get-root-props]:
https://codesandbox.io/p/sandbox/github/kentcdodds/downshift-examples?file=%2Fsrc%2Fdownshift%2Fordered-examples%2F01-basic-autocomplete.js&moduleview=1
[migration-guide-v7]:
https://github.com/downshift-js/downshift/tree/master/src/hooks/MIGRATION_V7.md
================================================
FILE: babel.config.js
================================================
const originalPreset = require('kcd-scripts/babel')
const customPreset = api => {
api.cache(true)
const evaluatedPreset = originalPreset(api)
const plugins = [
require.resolve('babel-plugin-dynamic-import-node'),
['no-side-effect-class-properties'],
['@babel/plugin-proposal-private-property-in-object', {loose: true}], // cypress warning because loose is false in preset-env
['@babel/plugin-proposal-private-methods', {loose: true}], // cypress warning because loose is false in preset-env
...evaluatedPreset.plugins,
]
return {
presets: evaluatedPreset.presets,
plugins,
}
}
module.exports = customPreset
================================================
FILE: cypress/.eslintrc
================================================
{
"plugins": ["cypress"],
"globals": {
"cy": true,
"Cypress": true,
"before": true
}
}
================================================
FILE: cypress/e2e/combobox.cy.js
================================================
// the combobox happens to be in the center of the page.
// without specifying an x and y for the body events
// we actually wind up firing events on the combobox.
const bodyX = 100
const bodyY = 500
describe('combobox', () => {
before(() => {
cy.visit('/combobox')
})
beforeEach(() => {
cy.findByTestId('clear-button').click()
})
it('can select an item', () => {
cy.findByTestId('combobox-input')
.type('ee{downarrow}{enter}')
.should('have.value', 'Green')
})
it('can arrow up to select last item', () => {
cy.findByTestId('combobox-input')
.type('{uparrow}{enter}') // open menu, last option is focused
.should('have.value', 'Skyblue')
})
it('can arrow down to select first item', () => {
cy.findByTestId('combobox-input')
.type('{downarrow}{enter}') // open menu, first option is focused
.should('have.value', 'Black')
})
it('can down arrow to select an item', () => {
cy.findByTestId('combobox-input')
.type('{downarrow}{downarrow}{enter}') // open and select second item
.should('have.value', 'Red')
})
it('can use home arrow to select first item', () => {
cy.findByTestId('combobox-input')
.type('{downarrow}{downarrow}{home}{enter}') // open to first, go down to second, return to first by home.
.should('have.value', 'Black')
})
it('can use end arrow to select last item', () => {
cy.findByTestId('combobox-input')
.type('{downarrow}{end}{enter}') // open to first, go to last by end.
.should('have.value', 'Skyblue')
})
it('resets the item on blur', () => {
cy.findByTestId('combobox-input')
.type('{downarrow}{enter}') // open and select first item
.should('have.value', 'Black')
.get('body')
.click(bodyX, bodyY, {force: true})
.findByTestId('combobox-input')
.should('have.value', 'Black')
})
it('can use the mouse to click an item', () => {
cy.findByTestId('combobox-input').type('red')
cy.findByTestId('downshift-item-0').click()
cy.findByTestId('combobox-input').should('have.value', 'Red')
})
it('does not reset the input when mouseup outside while the input is focused', () => {
cy.findByTestId('combobox-input').type('red')
cy.findByTestId('downshift-item-0').click()
cy.findByTestId('combobox-input')
.should('have.value', 'Red')
.type('{backspace}{backspace}')
.should('have.value', 'R')
.click()
.get('body')
.trigger('mouseup', bodyX, bodyY, {force: true})
.findByTestId('combobox-input')
.should('have.value', 'R')
.blur()
.get('body')
.trigger('click', bodyX, bodyY, {force: true})
.findByTestId('combobox-input')
.should('have.value', 'Red')
})
it('resets when bluring the input', () => {
cy.findByTestId('combobox-input')
.type('re')
.blur()
// https://github.com/kentcdodds/cypress-testing-library/issues/13
// eslint-disable-next-line testing-library/await-async-utils
.wait(1)
.findByTestId('downshift-item-0', {timeout: 10})
.should('not.exist')
})
it('does not reset when tabbing from input to the toggle button', () => {
cy.findByTestId('combobox-input').type('pu')
cy.findByTestId('combobox-toggle-button').focus()
cy.findByTestId('downshift-item-0').click()
cy.findByTestId('combobox-input').should('have.value', 'Purple')
})
it('does not reset when tabbing from the toggle button to the input', () => {
cy.findByTestId('combobox-toggle-button').click()
cy.findByTestId('combobox-input').focus()
cy.findByTestId('downshift-item-0').click()
cy.findByTestId('combobox-input').should('have.value', 'Black')
})
it('resets when tapping outside on a touch screen', () => {
cy.findByTestId('combobox-input')
.type('re')
.get('body')
.trigger('touchstart', bodyX, bodyY, {force: true})
.trigger('touchend', bodyX, bodyY, {force: true})
cy.findByTestId('downshift-item-0', {timeout: 100}).should('not.exist')
})
it('does not reset when swiping outside to scroll a touch screen', () => {
cy.findByTestId('combobox-input')
.type('re')
.get('body')
.trigger('touchstart', bodyX, bodyY, {force: true})
.trigger('touchmove', bodyX, bodyY + 20, {force: true})
.trigger('touchend', bodyX, bodyY + 20, {force: true})
cy.findByTestId('downshift-item-0', {timeout: 10}).should('be.visible')
})
})
================================================
FILE: cypress/e2e/useCombobox.cy.js
================================================
describe('useCombobox', () => {
before(() => {
cy.visit('/useCombobox')
})
it('should keep focus on the input when selecting by click', () => {
cy.findByTestId('combobox-toggle-button').click()
cy.findByTestId('downshift-item-0').click()
cy.findByTestId('combobox-input').should('have.focus')
})
})
================================================
FILE: cypress/e2e/useMultipleCombobox.cy.js
================================================
describe('useMultipleCombobox', () => {
before(() => {
cy.visit('/useMultipleCombobox')
})
it('can select multiple items', () => {
cy.findByRole('button', {name: 'toggle menu'}).click()
cy.findByRole('option', {name: 'Green'}).click()
cy.findByRole('option', {name: 'Gray'}).click()
cy.findByRole('button', {name: 'toggle menu'}).click()
cy.findByText('Black').should('be.visible')
cy.findByText('Red').should('be.visible')
cy.findByText('Green').should('be.visible')
cy.findByText('Gray').should('be.visible')
})
})
================================================
FILE: cypress/e2e/useMultipleSelect.cy.js
================================================
describe('useMultipleSelect', () => {
before(() => {
cy.visit('/useMultipleSelect')
})
it('can select multiple options', () => {
cy.findByRole('combobox').click()
cy.findByRole('option', {name: 'Green'}).click()
cy.findByRole('option', {name: 'Blue'}).click()
cy.findByRole('combobox').click()
cy.findByText('Black').should('be.visible')
cy.findByText('Red').should('be.visible')
cy.findByText('Green').should('be.visible')
cy.findByText('Blue').should('be.visible')
})
})
================================================
FILE: cypress/e2e/useSelect.cy.js
================================================
describe('useSelect', () => {
before(() => {
cy.visit('/useSelect')
})
it('can open and close a menu', () => {
cy.findByRole('combobox')
.click()
cy.findAllByRole('option')
.should('have.length.above', 0)
cy.findByRole('combobox')
.click()
cy.findAllByRole('option')
.should('have.length', 0)
cy.findByRole('combobox')
.click()
cy.findAllByRole('option')
.should('have.length.above', 0)
})
})
================================================
FILE: cypress/e2e/useTagGroup.cy.js
================================================
describe('useTagGroup', () => {
const colors = ['Black', 'Red', 'Green', 'Blue', 'Orange']
beforeEach(() => {
cy.visit('/useTagGroup')
// Ensure the listbox exists
cy.findByRole('listbox', {name: /colors example/i}).should('exist')
// Ensure it has 5 color tags
cy.findAllByRole('option').should('have.length', 5)
})
it('clicks a tag and navigates with circular arrow keys', () => {
// Click first tag ("Black")
cy.findByRole('option', {name: /Black/i}).click().should('have.focus')
// Arrow Right navigation through all tags
for (let index = 0; index < colors.length; index++) {
const nextIndex = (index + 1) % colors.length
cy.focused().trigger('keydown', {key: 'ArrowRight'})
cy.findByRole('option', {name: colors[nextIndex]}).should('have.focus')
}
// Arrow Left navigation through all tags (circular)
for (let index = colors.length - 1; index >= 0; index--) {
const prevIndex = (index + colors.length) % colors.length
cy.focused().trigger('keydown', {key: 'ArrowLeft'})
cy.findByRole('option', {name: colors[prevIndex]}).should('have.focus')
}
// Circular on the left.
cy.focused().trigger('keydown', {key: 'ArrowLeft'})
cy.findByRole('option', {name: colors[colors.length - 1]}).should(
'have.focus',
)
})
it('deletes a tag using Delete and Backspace', () => {
// Focus "Red"
cy.findByRole('option', {name: /Red/i}).click()
// Delete key
cy.focused().trigger('keydown', {key: 'Delete'})
cy.findAllByRole('option').should('have.length', 4)
// Next tag should be "Green"
cy.focused().should('contain.text', 'Green')
// Backspace key removes "Green"
cy.focused().trigger('keydown', {key: 'Backspace'})
cy.findAllByRole('option').should('have.length', 3)
// Focus should now be on "Blue"
cy.focused().should('contain.text', 'Blue')
})
it('removes a tag via remove button', () => {
// Remove "Blue" via its remove button
cy.findByRole('option', {name: /Blue/i}).within(() => {
cy.findByRole('button', {name: /remove/i}).click()
})
// Verify 4 tags remain
cy.findAllByRole('option').should('have.length', 4)
// Orange tag should have focus.
cy.findByRole('option', {name: /Orange/i}).should('have.focus')
})
it('adds a tag from the list', () => {
// Focus "Red"
cy.findByRole('option', {name: /Red/i}).click()
// Clicks the Lime option from the add tags list.
cy.findByRole('button', {name: /Lime/i}).click()
// Verify 6 tags are visible
cy.findAllByRole('option').should('have.length', 6)
cy.findByRole('option', {name: /Lime/i}).should('be.visible')
// Including the new option
})
})
================================================
FILE: cypress/fixtures/example.json
================================================
{}
================================================
FILE: cypress/plugins/index.js
================================================
module.exports = (_on, _config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
================================================
FILE: cypress/support/e2e.js
================================================
// eslint-disable-next-line
import '@testing-library/cypress/add-commands'
================================================
FILE: cypress.config.js
================================================
const webpackPreprocessor = require('@cypress/webpack-preprocessor')
const {defineConfig} = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:6006',
video: false,
testIsolation: false,
setupNodeEvents(on) {
on(
'file:preprocessor',
webpackPreprocessor({
webpackOptions: {
...webpackPreprocessor.defaultOptions.webpackOptions,
target: 'web',
},
}),
)
},
},
})
================================================
FILE: docusaurus/pages/combobox.js
================================================
import * as React from 'react'
import Downshift from '../../src'
import {colors, containerStyles, menuStyles} from '../utils'
export default function ComboBox() {
return (
{({
getInputProps,
getItemProps,
getMenuProps,
getLabelProps,
getToggleButtonProps,
highlightedIndex,
inputValue,
isOpen,
selectedItem,
getRootProps,
clearSelection,
}) => (
)}
)
}
================================================
FILE: docusaurus/pages/index.js
================================================
import * as React from 'react'
export default function Docs() {
return (
Downshift Docs for E2E Testing
)
}
================================================
FILE: docusaurus/pages/useCombobox.js
================================================
import * as React from 'react'
import {useCombobox} from '../../src'
import {colors, containerStyles, menuStyles} from '../utils'
export default function DropdownCombobox() {
const [inputItems, setInputItems] = React.useState(colors)
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
highlightedIndex,
getItemProps,
selectedItem,
selectItem,
} = useCombobox({
items: inputItems,
onInputValueChange: ({inputValue}) => {
setInputItems(
colors.filter(item =>
item.toLowerCase().startsWith(inputValue.toLowerCase()),
),
)
},
})
return (
)
}
================================================
FILE: docusaurus/pages/useMultipleCombobox.js
================================================
import * as React from 'react'
import {useCombobox, useMultipleSelection} from '../../src'
import {
colors,
containerStyles,
menuStyles,
tagGroupSyles,
tagStyles,
} from '../utils'
const initialSelectedItems = [colors[0], colors[1]]
function getFilteredItems(selectedItems, inputValue) {
const lowerCasedInputValue = inputValue.toLowerCase()
return colors.filter(
colour =>
!selectedItems.includes(colour) &&
colour.toLowerCase().startsWith(lowerCasedInputValue),
)
}
export default function DropdownMultipleCombobox() {
const [inputValue, setInputValue] = React.useState('')
const [selectedItems, setSelectedItems] = React.useState(initialSelectedItems)
const items = React.useMemo(
() => getFilteredItems(selectedItems, inputValue),
[selectedItems, inputValue],
)
const {getSelectedItemProps, getDropdownProps, removeSelectedItem} =
useMultipleSelection({
selectedItems,
onStateChange({selectedItems: newSelectedItems, type}) {
switch (type) {
case useMultipleSelection.stateChangeTypes
.SelectedItemKeyDownBackspace:
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
setSelectedItems(newSelectedItems)
break
default:
break
}
},
})
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
highlightedIndex,
getItemProps,
selectedItem,
clearSelection,
} = useCombobox({
items,
inputValue,
selectedItem: null,
stateReducer(state, actionAndChanges) {
const {changes, type} = actionAndChanges
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick:
case useCombobox.stateChangeTypes.InputBlur:
return {
...changes,
...(changes.selectedItem && {isOpen: true, highlightedIndex: 0}),
}
default:
return changes
}
},
onStateChange({
inputValue: newInputValue,
type,
selectedItem: newSelectedItem,
}) {
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick:
setSelectedItems([...selectedItems, newSelectedItem])
break
case useCombobox.stateChangeTypes.InputChange:
setInputValue(newInputValue)
break
default:
break
}
},
})
return (
Choose an element:
{selectedItems.map(function renderSelectedItem(
selectedItemForRender,
index,
) {
return (
{selectedItemForRender}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
{
e.stopPropagation()
removeSelectedItem(selectedItemForRender)
}}
>
✕
)
})}
{isOpen ? <>↑> : <>↓>}
✗
{isOpen
? items.map((item, index) => (
{item}
))
: null}
)
}
================================================
FILE: docusaurus/pages/useMultipleSelect.js
================================================
import * as React from 'react'
import {useSelect, useMultipleSelection} from '../../src'
import {
colors,
containerStyles,
menuStyles,
tagGroupSyles,
tagStyles,
} from '../utils'
const initialSelectedItems = [colors[0], colors[1]]
function getFilteredItems(selectedItems) {
return colors.filter(colour => !selectedItems.includes(colour))
}
export default function DropdownMultipleSelect() {
const {
getSelectedItemProps,
getDropdownProps,
addSelectedItem,
removeSelectedItem,
selectedItems,
} = useMultipleSelection({initialSelectedItems})
const items = getFilteredItems(selectedItems)
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect({
selectedItem: null,
defaultHighlightedIndex: 0, // after selection, highlight the first item.
items,
stateReducer: (state, actionAndChanges) => {
const {changes, type} = actionAndChanges
switch (type) {
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
case useSelect.stateChangeTypes.ItemClick:
return {
...changes,
isOpen: true, // keep the menu open after selection.
}
default:
return changes
}
},
onStateChange: ({type, selectedItem: newSelectedItem}) => {
switch (type) {
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
case useSelect.stateChangeTypes.ItemClick:
if (newSelectedItem) {
addSelectedItem(newSelectedItem)
}
break
default:
break
}
},
})
return (
Choose an element:
{selectedItems.map(function renderSelectedItem(
selectedItemForRender,
index,
) {
return (
{selectedItemForRender}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
{
e.stopPropagation()
removeSelectedItem(selectedItemForRender)
}}
>
✕
)
})}
Pick some colors {isOpen ? <>↑> : <>↓>}
{isOpen
? items.map((item, index) => (
{item}
))
: null}
)
}
================================================
FILE: docusaurus/pages/useSelect.js
================================================
import * as React from 'react'
import {useSelect} from '../../src'
import {colors, containerStyles, menuStyles} from '../utils'
export default function DropdownSelect() {
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect({items: colors})
return (
Choose an element:
{selectedItem ?? 'Elements'}
{isOpen ? <>↑> : <>↓>}
{isOpen ?
colors.map((item, index) => (
{item}
)) : null}
)
}
================================================
FILE: docusaurus/pages/useTagGroup.css
================================================
.tag-group {
display: inline-flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
padding: 6px;
}
.tag {
border: solid 1px darkgreen;
background-color: green;
padding: 0 6px;
margin: 0 2px;
border-radius: 10px;
cursor: default;
}
.tag:hover {
opacity: 0.5;
}
.tag:focus {
background-color: red;
border-color: darkred;
}
.tag-remove-button {
padding: 4px;
cursor: pointer;
border: none;
background-color: transparent;
}
.item-to-add {
cursor: pointer;
}
.selected-tag {
font-style: italic;
}
================================================
FILE: docusaurus/pages/useTagGroup.tsx
================================================
import * as React from 'react'
import {useTagGroup} from '../../src'
import {colors} from '../utils'
import './useTagGroup.css'
export default function TagGroup() {
const initialItems = colors.slice(0, 5)
const {
addItem,
getTagProps,
getTagRemoveProps,
getTagGroupProps,
items,
activeIndex,
} = useTagGroup({initialItems})
const itemsToAdd = colors.filter(color => !items.includes(color))
return (
{items.map((color, index) => (
{color}
✕
))}
Add more items:
{itemsToAdd.map(item => (
{
addItem(item)
}}
onKeyDown={({key}) => {
key === 'Enter' && addItem(item)
}}
>
{item}
))}
)
}
================================================
FILE: docusaurus/pages/useTagGroupCombobox.css
================================================
.wrapper {
width: 18rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.wrapper label {
width: fit-content;
}
.input-wrapper {
display: flex;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
background-color: white;
gap: 0.125rem;
}
.text-input {
width: 100%;
padding: 0.375rem;
}
.toggle-button {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.menu {
position: absolute;
width: 18rem;
background-color: white;
margin-top: 0.25rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-height: 20rem;
overflow-y: scroll;
padding: 0;
z-index: 10;
}
.menu.hidden {
display: none;
}
.menu-item {
padding: 0.5rem 0.75rem;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
}
.menu-item.highlighted {
background-color: #93c5fd;
}
================================================
FILE: docusaurus/pages/useTagGroupCombobox.tsx
================================================
import * as React from 'react'
import {UseComboboxInterface} from '../../typings'
import {useTagGroup, useCombobox as useComboboxUntyped} from '../../src'
import {colors} from '../utils'
import './useTagGroupCombobox.css'
export default function TagGroup() {
const initialItems = colors.slice(0, 5)
const [inputValue, setInputValue] = React.useState('')
const {
addItem,
getTagProps,
getTagRemoveProps,
getTagGroupProps,
items,
activeIndex,
} = useTagGroup({initialItems})
const itemsToAdd = colors.filter(
color =>
!items.includes(color) &&
(!inputValue || color.toLowerCase().includes(inputValue.toLowerCase())),
)
const useCombobox = useComboboxUntyped as UseComboboxInterface
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
highlightedIndex,
getItemProps,
} = useCombobox({
items: itemsToAdd,
inputValue,
onInputValueChange: changes => {
setInputValue(changes.inputValue)
},
onSelectedItemChange(changes) {
if (changes.selectedItem) {
addItem(changes.selectedItem)
}
},
selectedItem: null,
stateReducer(_state, actionAndChanges) {
const {changes, type} = actionAndChanges
if (
changes.selectedItem &&
type !== useCombobox.stateChangeTypes.InputBlur
) {
return {...changes, inputValue: '', highlightedIndex: 0, isOpen: true}
}
return changes
},
})
return (
{items.map((color, index) => (
{color}
✕
))}
{isOpen
? itemsToAdd.map((item, index) => (
{item}
))
: null}
)
}
================================================
FILE: docusaurus/plugins/webpack5polyfills.js
================================================
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')
// eslint-disable-next-line
module.exports = function () {
return {
name: 'custom-docusaurus-webpack-config-plugin',
configureWebpack() {
return {
resolve: {
fallback: {
fs: false,
module: false,
},
},
plugins: [new NodePolyfillPlugin()],
}
},
}
}
================================================
FILE: docusaurus/tsconfig.json
================================================
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true,
},
"include": ["./**/*.tsx*", "../typings/**/*.d.ts"]
}
================================================
FILE: docusaurus/utils.ts
================================================
import {type CSSProperties} from 'react'
export const colors = [
'Black',
'Red',
'Green',
'Blue',
'Orange',
'Purple',
'Pink',
'Orchid',
'Aqua',
'Lime',
'Gray',
'Brown',
'Teal',
'Skyblue',
]
export const menuStyles: CSSProperties = {
listStyle: 'none',
width: '100%',
padding: '0',
margin: '4px 0 0 0',
maxHeight: 120,
overflowY: 'scroll',
}
export const containerStyles: CSSProperties = {
display: 'flex',
flexDirection: 'column',
width: 'fit-content',
justifyContent: 'center',
marginTop: 100,
alignSelf: 'center',
}
export const tagGroupSyles: CSSProperties = {
display: 'inline-flex',
gap: '8px',
alignItems: 'center',
flexWrap: 'wrap',
padding: '6px',
}
export const tagStyles: CSSProperties = {
backgroundColor: 'green',
padding: '0 6px',
margin: '0 2px',
borderRadius: '10px',
cursor: 'auto',
}
export const removeTagStyles: CSSProperties = {
padding: '4px',
cursor: 'pointer',
}
================================================
FILE: docusaurus.config.js
================================================
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'Downshift',
url: 'https://downshift-js.com',
baseUrl: '/',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: 'downshift-js', // Usually your GitHub org/user name.
projectName: 'downshift', // Usually your repo name.
// Even if you don't use internalization, you can use this field to set useful
// metadata like html lang. For example, if your site is Chinese, you may want
// to replace "en" with "zh-Hans".
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: false,
blog: false,
pages: {
path: 'docusaurus/pages',
include: ['**/*.{js,jsx,tsx}'],
},
}),
],
],
plugins: [
// @ts-ignore
() => ({
name: 'configure-webpack-target',
configureWebpack(webpackConfig, isServer) {
webpackConfig.target = isServer ? 'node' : 'web'
},
}),
require.resolve('./docusaurus/plugins/webpack5polyfills.js'),
],
}
module.exports = config
================================================
FILE: flow-typed/npm/downshift_v2.x.x.js.flow
================================================
/**
* Flowtype definitions for index
* Generated by Flowgen from a Typescript Definition
* Flowgen v1.2.0
* Author: [Joar Wilk](http://twitter.com/joarwilk)
* Repo: http://github.com/joarwilk/flowgen
*/
import * as React from 'react'
declare module downshift {
declare type StateChangeTypes = {
unknown: '__autocomplete_unknown__',
mouseUp: '__autocomplete_mouseup__',
itemMouseEnter: '__autocomplete_item_mouseenter__',
keyDownArrowUp: '__autocomplete_keydown_arrow_up__',
keyDownArrowDown: '__autocomplete_keydown_arrow_down__',
keyDownEscape: '__autocomplete_keydown_escape__',
keyDownEnter: '__autocomplete_keydown_enter__',
clickItem: '__autocomplete_click_item__',
blurInput: '__autocomplete_blur_input__',
changeInput: '__autocomplete_change_input__',
keyDownSpaceButton: '__autocomplete_keydown_space_button__',
clickButton: '__autocomplete_click_button__',
blurButton: '__autocomplete_blur_button__',
controlledPropUpdatedSelectedItem: '__autocomplete_controlled_prop_updated_selected_item__',
}
declare type StateChangeValues =
| '__autocomplete_unknown__'
| '__autocomplete_mouseup__'
| '__autocomplete_item_mouseenter__'
| '__autocomplete_keydown_arrow_up__'
| '__autocomplete_keydown_arrow_down__'
| '__autocomplete_keydown_escape__'
| '__autocomplete_keydown_enter__'
| '__autocomplete_click_item__'
| '__autocomplete_blur_input__'
| '__autocomplete_change_input__'
| '__autocomplete_keydown_space_button__'
| '__autocomplete_click_button__'
| '__autocomplete_blur_button__'
| '__autocomplete_controlled_prop_updated_selected_item__'
declare type Callback = () => void
declare export interface DownshiftState- {
highlightedIndex: number | null;
inputValue: string | null;
isOpen: boolean;
selectedItem: Item | null;
}
declare export interface DownshiftProps
- {
defaultSelectedItem?: Item;
defaultHighlightedIndex?: number | null;
defaultInputValue?: string;
defaultIsOpen?: boolean;
itemToString?: (item: Item) => string;
selectedItemChanged?: (prevItem: Item, item: Item) => boolean;
getA11yStatusMessage?: (options: A11yStatusMessageOptions
- ) => string;
onChange?: (
selectedItem: Item,
stateAndHelpers: ControllerStateAndHelpers
- ,
) => void;
onSelect?: (
selectedItem: Item,
stateAndHelpers: ControllerStateAndHelpers
- ,
) => void;
onStateChange?: (
options: StateChangeOptions
- ,
stateAndHelpers: ControllerStateAndHelpers
- ,
) => void;
onInputValueChange?: (
inputValue: string,
stateAndHelpers: ControllerStateAndHelpers
- ,
) => void;
stateReducer?: (
state: DownshiftState
- ,
changes: StateChangeOptions
- ,
) => StateChangeOptions
- ;
itemCount?: number;
highlightedIndex?: number;
inputValue?: string;
isOpen?: boolean;
selectedItem?: Item;
children: ChildrenFunction
- ;
id?: string;
environment?: Environment;
onOuterClick?: () => void;
onUserAction?: (
options: StateChangeOptions
- ,
stateAndHelpers: ControllerStateAndHelpers
- ,
) => void;
}
declare export interface Environment {
addEventListener: typeof window.addEventListener;
removeEventListener: typeof window.removeEventListener;
document: Document;
}
declare export interface A11yStatusMessageOptions
- {
highlightedIndex: number | null;
inputValue: string;
isOpen: boolean;
itemToString: (item: Item) => string;
previousResultCount: number;
resultCount: number;
selectedItem: Item;
}
declare export interface StateChangeOptions
- {
type: StateChangeValues;
highlightedIndex: number;
inputValue: string;
isOpen: boolean;
selectedItem: Item;
}
declare export type StateChangeFunction
- = (
state: DownshiftState
- ,
) => StateChangeOptions
-
declare export type GetRootPropsReturn = {
role: 'combobox';
'aria-expanded': boolean;
'aria-haspopup': 'listbox';
'aria-owns': string | null;
'aria-labelledby': string;
}
declare export interface GetRootPropsOptions {
refKey: string;
}
declare type GetToggleButtonCallbacks = {
onMouseMove: (e: SyntheticEvent
) => void;
onMouseDown: (e: SyntheticEvent) => void;
onBlur: (e: SyntheticEvent) => void;
} | {
onPress: (e: SyntheticEvent) => void; // should be react native type
} | {}
declare export type GetToggleButtonReturn = {
type: 'button';
role: 'button';
'aria-label': 'close menu' | 'open menu';
'aria-haspopup': true;
'data-toggle': true;
} & GetInputPropsCallbacks
declare export interface getToggleButtonPropsOptions
extends React.HTMLProps {}
declare export interface GetLabelPropsReturn {
htmlFor: string;
id: string;
}
declare export interface GetLabelPropsOptions
extends React.HTMLProps {}
declare export type getMenuPropsReturn = {
role: 'listbox';
'aria-labelledby': string | null;
id: string;
}
declare type GetInputPropsCallbacks = ({
onKeyDown: (e: SyntheticEvent) => void;
onBlur: (e: SyntheticEvent) => void;
} & ({
onInput: (e: SyntheticEvent) => void;
} | {
onChangeText: (e: SyntheticEvent) => void;
} | {
onChange: (e: SyntheticEvent) => void;
})) | {}
declare export type GetInputPropsReturn = {
'aria-autocomplete': 'list';
'aria-activedescendant': string | null;
'aria-controls': string | null;
'aria-labelledby': string;
autoComplete: 'off';
value: string;
id: string;
} & GetInputPropsCallbacks;
declare export interface GetInputPropsOptions
extends React.HTMLProps {}
declare type GetItemPropsCallbacks = {
onMouseMove: (e: SyntheticEvent) => void;
onMouseDown: (e: SyntheticEvent) => void;
} & ({
onPress: (e: SyntheticEvent) => void;
} | {
onClick: (e: SyntheticEvent) => void;
})
declare export type GetItemPropsReturn = {
id: string;
role: 'option';
'aria-selected': boolean;
} & GetItemPropsCallbacks
declare export type GetItemPropsOptions- = {
index?: number,
item: Item,
}
declare export interface PropGetters
- {
getRootProps:
(options: GetRootPropsOptions & T) => GetRootPropsReturn & T;
getButtonProps: (options?: getToggleButtonPropsOptions & T) => GetToggleButtonReturn & T;
getToggleButtonProps: (options?: getToggleButtonPropsOptions & T) => GetToggleButtonReturn & T;
getLabelProps: (options?: GetLabelPropsOptions & T) => GetLabelPropsReturn & T;
getMenuProps: (options?: T) => getMenuPropsReturn & T;
getInputProps: (options?: GetInputPropsOptions & T) => GetInputPropsReturn & T;
getItemProps: (options: GetItemPropsOptions- & T) => GetItemPropsReturn & T;
}
declare export interface Actions
- {
reset: (otherStateToSet?: {}, cb?: Callback) => void;
openMenu: (cb?: Callback) => void;
closeMenu: (cb?: Callback) => void;
toggleMenu: (otherStateToSet?: {}, cb?: Callback) => void;
selectItem: (item: Item | null, otherStateToSet?: {}, cb?: Callback) => void;
selectItemAtIndex: (
index: number,
otherStateToSet?: {},
cb?: Callback,
) => void;
selectHighlightedItem: (otherStateToSet?: {}, cb?: Callback) => void;
setHighlightedIndex: (
index: number,
otherStateToSet?: {},
cb?: Callback,
) => void;
clearSelection: (cb?: Callback) => void;
clearItems: () => void;
setItemCount: (count: number) => void;
unsetItemCount: () => void;
setState: (
stateToSet: StateChangeOptions
- | StateChangeFunction
- ,
cb?: Callback,
) => void;
// props
itemToString: (item: Item) => string;
}
declare export type ControllerStateAndHelpers
- = DownshiftState
- &
PropGetters
- &
Actions
-
declare export type ChildrenFunction
- = (
options: ControllerStateAndHelpers
- ,
) => React.ReactNode
declare export type DownshiftType
- = Class<
React.Component
, DownshiftState- >,
> & {
stateChangeTypes: StateChangeTypes,
}
declare var DownshiftComponent: DownshiftType
declare export default DownshiftComponent
}
================================================
FILE: jest.config.js
================================================
const jestConfig = require('kcd-scripts/jest')
module.exports = Object.assign(jestConfig, {
coveragePathIgnorePatterns: [
...jestConfig.coveragePathIgnorePatterns,
'.macro.js$',
'/src/stateChangeTypes.js',
],
setupFilesAfterEnv: ['/test/setup.ts'],
moduleFileExtensions: ['ts', 'js', 'tsx', 'jsx'],
})
================================================
FILE: netlify.toml
================================================
# COMMENT: This a rule for Single Page Applications as Docz site is one
[[redirects]]
from = "/*"
to = "/"
status = 200
================================================
FILE: other/MAINTAINING.md
================================================
# Maintaining
**Table of Contents**
- [Code of Conduct](#code-of-conduct)
- [Issues](#issues)
- [Pull Requests](#pull-requests)
- [Release](#release)
- [Thanks!](#thanks)
This is documentation for maintainers of this project.
## Code of Conduct
Please review, understand, and be an example of it. Violations of the code of
conduct are taken seriously, even (especially) for maintainers.
## Issues
We want to support and build the community. We do that best by helping people
learn to solve their own problems. We have an issue template and hopefully most
folks follow it. If it's not clear what the issue is, invite them to create a
minimal reproduction of what they're trying to accomplish or the bug they think
they've found.
Once it's determined that a code change is necessary, point people to
[makeapullrequest.com](http://makeapullrequest.com) and invite them to make a
pull request. If they're the one who needs the feature, they're the one who can
build it. If they need some hand holding and you have time to lend a hand,
please do so. It's an investment into another human being, and an investment
into a potential maintainer.
Remember that this is open source, so the code is not yours, it's ours. If
someone needs a change in the codebase, you don't have to make it happen
yourself. Commit as much time to the project as you want/need to. Nobody can ask
any more of you than that.
## Pull Requests
As a maintainer, you're fine to make your branches on the main repo or on your
own fork. Either way is fine.
When we receive a pull request, a travis build is kicked off automatically (see
the `.travis.yml` for what runs in the travis build). We avoid merging anything
that breaks the travis build.
Please review PRs and focus on the code rather than the individual. You never
know when this is someone's first ever PR and we want their experience to be as
positive as possible, so be uplifting and constructive.
When you merge the pull request, 99% of the time you should use the
[Squash and merge](https://help.github.com/articles/merging-a-pull-request/)
feature. This keeps our git history clean, but more importantly, this allows us
to make any necessary changes to the commit message so we release what we want
to release. See the next section on Releases for more about that.
## Release
Our releases are automatic. They happen whenever code lands into `master`. A
travis build gets kicked off and if it's successful, a tool called
[`semantic-release`](https://github.com/semantic-release/semantic-release) is
used to automatically publish a new release to npm as well as a changelog to
GitHub. It is only able to determine the version and whether a release is
necessary by the git commit messages. With this in mind, **please brush up on
[the commit message convention][commit] which drives our releases.**
> One important note about this: Please make sure that commit messages do NOT
> contain the words "BREAKING CHANGE" in them unless we want to push a major
> version. I've been burned by this more than once where someone will include
> "BREAKING CHANGE: None" and it will end up releasing a new major version. Not
> a huge deal honestly, but kind of annoying...
## Thanks!
Thank you so much for helping to maintain this project!
[commit]: https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md
================================================
FILE: other/TYPESCRIPT_USAGE.md
================================================
# Typescript Usage
The current bundled Typescript definitions are incomplete and based around the
needs of the developers who contributed them.
Pull requests to improve them are welcome and appreciated. If you've never
contributed to open source before, then you may find
[this free video course](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github)
helpful.
================================================
FILE: other/USERS.md
================================================
# Users
If you or your company uses this project, add your name to this list! Eventually
we may have a website to showcase these (wanna build it!?)
> No users have been added yet!
================================================
FILE: other/manual-releases.md
================================================
# manual-releases
This project has an automated release set up. So things are only released when
there are useful changes in the code that justify a release. But sometimes
things get messed up one way or another and we need to trigger the release
ourselves. When this happens, simply bump the number below and commit that with
the following commit message based on your needs:
**Major**
```
fix(release): manually release a major version
There was an issue with a major release, so this manual-releases.md
change is to release a new major version.
Reference: #
BREAKING CHANGE:
```
**Minor**
```
feat(release): manually release a minor version
There was an issue with a minor release, so this manual-releases.md
change is to release a new minor version.
Reference: #
```
**Patch**
```
fix(release): manually release a patch version
There was an issue with a patch release, so this manual-releases.md
change is to release a new patch version.
Reference: #
```
The number of times we've had to do a manual release is: 12
================================================
FILE: other/misc-tests/__tests__/build.js
================================================
/*
* This file is here to validate that the built version
* of the library exposes the module in the way that we
* want it to. Specifically that the ES6 module import can
* get the downshift function via default import. Also that
* the CommonJS require returns the downshift function
* (rather than an object that has the downshift as a
* `default` property).
*
* This file is unable to validate the global export.
*/
import assert from 'assert'
import esImport, {
useCombobox as useComboboxEsImport,
useSelect as useSelectEsImport,
useMultipleSelection as useMultipleSelectionEsImport,
} from '../../../dist/downshift.esm.mjs'
import cjsImport, {
useCombobox as useComboboxCjsImport,
useSelect as useSelectCjsImport,
useMultipleSelection as useMultipleSelectionCjsImport,
} from '../../../' // picks up the main from package.json
import umdImport, {
useCombobox as useComboboxUmdImport,
useSelect as useSelectUmdImport,
useMultipleSelection as useMultipleSelectionUmdImport,
} from '../../../dist/downshift.umd'
import rnImport, {
useCombobox as useComboboxRnImport,
useSelect as useSelectRnImport,
useMultipleSelection as useMultipleSelectionRnImport,
} from '../../../dist/downshift.native.cjs.cjs'
import rnWebImport, {
useCombobox as useComboboxRnWebImport,
useSelect as useSelectRnWebImport,
useMultipleSelection as useMultipleSelectionRnWebImport,
} from '../../../dist/downshift.nativeweb.cjs.cjs'
// intentionally left out because you shouldn't ever
// try to require the ES file in CommonJS
// const esRequire = require('../../../dist/downshift.es')
const cjsRequire = require('../../../') // picks up the main from package.json
const umdRequire = require('../../../dist/downshift.umd')
const rnCjsRequire = require('../../../dist/downshift.native.cjs.cjs')
const rnWebCjsRequire = require('../../../dist/downshift.nativeweb.cjs.cjs')
test('downshift component is imported', () => {
assert(
isDownshiftComponent(esImport),
'ES build has a problem with ES Modules',
)
assert(
isDownshiftComponent(cjsImport),
'CJS build has a problem with ES Modules',
)
assert(
isDownshiftComponent(cjsRequire.default),
'CJS build has a problem with CJS',
)
assert(
isDownshiftComponent(umdImport),
'UMD build has a problem with ES Modules',
)
assert(
isDownshiftComponent(umdRequire.default),
'UMD build has a problem with CJS',
)
assert(
isDownshiftComponent(rnImport),
'React Native build has a problem with ES Modules',
)
assert(
isDownshiftComponent(rnCjsRequire.default),
'React Native build has a problem with CJS',
)
assert(
isDownshiftComponent(rnWebImport),
'React Native Web build has a problem with ES Modules',
)
assert(
isDownshiftComponent(rnWebCjsRequire.default),
'React Native Web build has a problem with CJS',
)
// TODO: how could we validate the global export?
})
test('useSelect hook is imported', () => {
assert(
isDownshiftComponent(useSelectEsImport),
'ES build has a problem with ES Modules',
)
assert(
isDownshiftComponent(useSelectCjsImport),
'CJS build has a problem with ES Modules',
)
assert(
isDownshiftComponent(cjsRequire.useSelect),
'CJS build has a problem with CJS',
)
assert(
isDownshiftComponent(useSelectUmdImport),
'UMD build has a problem with ES Modules',
)
assert(
isDownshiftComponent(umdRequire.useSelect),
'UMD build has a problem with CJS',
)
assert(
isDownshiftComponent(useSelectRnImport),
'React Native build has a problem with ES Modules',
)
assert(
isDownshiftComponent(rnCjsRequire.useSelect),
'React Native build has a problem with CJS',
)
assert(
isDownshiftComponent(useSelectRnWebImport),
'React Native Web build has a problem with ES Modules',
)
assert(
isDownshiftComponent(rnWebCjsRequire.useSelect),
'React Native Web build has a problem with CJS',
)
})
test('useCombobox hook is imported', () => {
assert(
isDownshiftComponent(useComboboxEsImport),
'ES build has a problem with ES Modules',
)
assert(
isDownshiftComponent(useComboboxCjsImport),
'CJS build has a problem with ES Modules',
)
assert(
isDownshiftComponent(cjsRequire.useCombobox),
'CJS build has a problem with CJS',
)
assert(
isDownshiftComponent(useComboboxUmdImport),
'UMD build has a problem with ES Modules',
)
assert(
isDownshiftComponent(umdRequire.useCombobox),
'UMD build has a problem with CJS',
)
assert(
isDownshiftComponent(useComboboxRnImport),
'React Native build has a problem with ES Modules',
)
assert(
isDownshiftComponent(rnCjsRequire.useCombobox),
'React Native build has a problem with CJS',
)
assert(
isDownshiftComponent(useComboboxRnWebImport),
'React Native Web build has a problem with ES Modules',
)
assert(
isDownshiftComponent(rnWebCjsRequire.useCombobox),
'React Native Web build has a problem with CJS',
)
})
test('useMultipleSelection hook is imported', () => {
assert(
isDownshiftComponent(useMultipleSelectionEsImport),
'ES build has a problem with ES Modules',
)
assert(
isDownshiftComponent(useMultipleSelectionCjsImport),
'CJS build has a problem with ES Modules',
)
assert(
isDownshiftComponent(cjsRequire.useMultipleSelection),
'CJS build has a problem with CJS',
)
assert(
isDownshiftComponent(useMultipleSelectionUmdImport),
'UMD build has a problem with ES Modules',
)
assert(
isDownshiftComponent(umdRequire.useMultipleSelection),
'UMD build has a problem with CJS',
)
assert(
isDownshiftComponent(useMultipleSelectionRnImport),
'React Native build has a problem with ES Modules',
)
assert(
isDownshiftComponent(rnCjsRequire.useMultipleSelection),
'React Native build has a problem with CJS',
)
assert(
isDownshiftComponent(useMultipleSelectionRnWebImport),
'React Native Web build has a problem with ES Modules',
)
assert(
isDownshiftComponent(rnWebCjsRequire.useMultipleSelection),
'React Native Web build has a problem with CJS',
)
})
function isDownshiftComponent(thing) {
if (typeof thing !== 'function') {
console.error(
`downshift thing should be a function. It's a ${typeof thing} with the properties of: ${Object.keys(
thing,
).join(', ')}`,
)
return false
}
return true
}
/*
eslint
no-console: 0,
import/extensions: 0,
import/no-unresolved: 0,
import/no-duplicates: 0,
no-duplicate-imports: 0,
*/
================================================
FILE: other/misc-tests/__tests__/preact.js
================================================
// Tell Babel to transform JSX into preact.h() calls:
/** @jsx preact.h */
/*
eslint-disable
react/prop-types,
no-console,
react/display-name,
import/extensions,
import/no-unresolved
*/
/*
Testing the preact version is a tiny bit complicated because
we need the preact build (the one that imports 'preact' rather
than 'react') otherwise things don't work very well.
So there's a script `test.build` which will run the cjs build
for preact before running this test.
*/
import preact from 'preact'
import {render} from '@testing-library/preact'
import Downshift from '../../../preact'
test('works with preact', () => {
const childrenSpy = jest.fn(({getInputProps, getItemProps}) => (
))
const ui = {childrenSpy}
render(ui)
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
isOpen: false,
highlightedIndex: null,
selectedItem: null,
inputValue: '',
}),
)
})
test('can render a composite component', () => {
const Div = ({innerRef, ...props}) =>
const childrenSpy = jest.fn(({getRootProps}) => (
))
const ui = {childrenSpy}
render(ui)
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
isOpen: false,
highlightedIndex: null,
selectedItem: null,
inputValue: '',
}),
)
})
test('getInputProps composes onChange with onInput', () => {
const onChange = jest.fn()
const onInput = jest.fn()
const Input = jest.fn(props => )
const {ui} = setup({
children({getInputProps}) {
return (
)
},
})
render(ui)
expect(Input).toHaveBeenCalledTimes(1)
const [[firstArg]] = Input.mock.calls
expect(firstArg).toMatchObject({
onInput: expect.any(Function),
})
expect(firstArg.onChange).toBeUndefined()
const fakeEvent = {defaultPrevented: false, target: {value: ''}}
firstArg.onInput(fakeEvent)
expect(onChange).toHaveBeenCalledTimes(1)
expect(onChange).toHaveBeenCalledWith(fakeEvent)
expect(onInput).toHaveBeenCalledTimes(1)
expect(onInput).toHaveBeenCalledWith(fakeEvent)
})
test('can use children instead of render prop', () => {
const childrenSpy = jest.fn()
render({childrenSpy} )
expect(childrenSpy).toHaveBeenCalledTimes(1)
})
function setup({children = () =>
, ...props} = {}) {
let renderArg
const childrenSpy = jest.fn(controllerArg => {
renderArg = controllerArg
return children(controllerArg)
})
const ui = {childrenSpy}
return {childrenSpy, ui, ...renderArg}
}
================================================
FILE: other/misc-tests/jest.config.js
================================================
const jestConfig = require('kcd-scripts/config').jest
const babelHelpersList = require('@babel/helpers').list
module.exports = Object.assign(jestConfig, {
roots: ['.'],
testEnvironment: 'jsdom',
transform: {
'^.+\\.(js|jsx|mjs)$': ['babel-jest', { rootMode: 'upward' }],
},
transformIgnorePatterns: [
'node_modules/(?!(dedent|@testing-library/preact)/)',
],
moduleNameMapper: babelHelpersList.reduce(
(aliasMap, helper) => {
aliasMap[`@babel/runtime/helpers/esm/${helper}`] =
`@babel/runtime/helpers/${helper}`
return aliasMap
},
{
'^preact(/(.*)|$)': 'preact$1',
},
),
})
================================================
FILE: other/react-native/.babelrc
================================================
{
"presets": ["react-native"]
}
================================================
FILE: other/react-native/__tests__/__snapshots__/render-tests.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders with React Native components 1`] = `
foo
bar
`;
================================================
FILE: other/react-native/__tests__/onBlur-tests.js
================================================
import {Text, TextInput, View} from 'react-native'
import * as React from 'react'
// Note: test renderer must be required after react-native.
import TestRenderer from 'react-test-renderer'
import Downshift from '../../../dist/downshift.native.cjs'
const RootView = ({innerRef, ...rest}) =>
test('calls onBlur and does not crash when there is no document', () => {
const Input = jest.fn(props => )
const element = (
{({getRootProps, getInputProps, getItemProps}) => (
foo
bar
)}
)
TestRenderer.create(element)
const [[firstArg]] = Input.mock.calls
expect(firstArg).toMatchObject({
onBlur: expect.any(Function),
})
const fakeEvent = 'blur'
firstArg.onBlur(fakeEvent)
})
/*
eslint
react/prop-types: 0,
import/extensions: 0,
import/no-unresolved: 0
*/
================================================
FILE: other/react-native/__tests__/onChange-tests.js
================================================
import {Text, TextInput, View} from 'react-native'
import * as React from 'react'
// Note: test renderer must be required after react-native.
import TestRenderer from 'react-test-renderer'
import Downshift from '../../../dist/downshift.native.cjs'
const RootView = ({innerRef, ...rest}) =>
test('calls onChange when TextInput changes values', () => {
const onChange = jest.fn()
const Input = jest.fn(props => )
const element = (
{({getRootProps, getInputProps, getItemProps}) => (
foo
bar
)}
)
TestRenderer.create(element)
const [[firstArg]] = Input.mock.calls
expect(firstArg).toMatchObject({
onChange: expect.any(Function),
})
const fakeEvent = {nativeEvent: {text: 'foobar'}}
firstArg.onChange(fakeEvent)
expect(onChange).toHaveBeenCalledTimes(1)
expect(onChange).toHaveBeenCalledWith(fakeEvent)
})
test('calls onChangeText when TextInput changes values', () => {
const onChangeText = jest.fn()
const Input = jest.fn(props => )
const element = (
{({getRootProps, getInputProps, getItemProps}) => (
foo
bar
)}
)
TestRenderer.create(element)
const [[firstArg]] = Input.mock.calls
expect(firstArg).toMatchObject({
onChangeText: expect.any(Function),
})
const fakeEvent = 'foobar'
firstArg.onChangeText(fakeEvent)
expect(onChangeText).toHaveBeenCalledTimes(1)
expect(onChangeText).toHaveBeenCalledWith(fakeEvent)
})
/*
eslint
react/prop-types: 0,
import/extensions: 0,
import/no-unresolved: 0
*/
================================================
FILE: other/react-native/__tests__/render-tests.js
================================================
import {Text, TextInput, View} from 'react-native'
import * as React from 'react'
// Note: test renderer must be required after react-native.
import TestRenderer from 'react-test-renderer'
import Downshift from '../../../dist/downshift.native.cjs'
test('renders with React Native components', () => {
const RootView = ({innerRef, ...rest}) =>
const childrenSpy = jest.fn(({getRootProps, getInputProps, getItemProps}) => (
foo
bar
))
const element = {childrenSpy}
const renderer = TestRenderer.create(element)
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
isOpen: false,
highlightedIndex: null,
selectedItem: null,
inputValue: '',
}),
)
const tree = renderer.toJSON()
expect(tree).toMatchSnapshot()
})
test('can use children instead of render prop', () => {
const RootView = ({innerRef, ...rest}) =>
const childrenSpy = jest.fn(({getRootProps, getInputProps, getItemProps}) => (
foo
bar
))
const element = {childrenSpy}
TestRenderer.create(element)
expect(childrenSpy).toHaveBeenCalledTimes(1)
})
/*
eslint
react/prop-types: 0,
import/extensions: 0,
import/no-unresolved: 0
*/
================================================
FILE: other/react-native/jest.config.js
================================================
// const jestConfig = require('kcd-scripts/config').jest
module.exports = {
preset: 'react-native',
rootDir: '../../',
roots: ['.'],
transform: {
'^.+\\.js$': '/node_modules/react-native/jest/preprocessor.js',
},
testMatch: ['/other/react-native/__tests__/**/*.js?(x)'],
}
================================================
FILE: other/ssr/__tests__/index.js
================================================
import * as React from 'react'
import * as ReactDOMServer from 'react-dom/server'
import Downshift, {resetIdCounter} from '../../../src'
test('does not throw an error when server rendering', () => {
expect(() => {
ReactDOMServer.renderToString(
{({getInputProps, getLabelProps}) => (
)}
,
)
}).not.toThrow()
})
if (!('useId' in React)) {
test('resets idCounter', () => {
const getRenderedString = () => {
resetIdCounter()
return ReactDOMServer.renderToString(
{({getInputProps, getLabelProps}) => (
)}
,
)
}
const firstRun = getRenderedString()
const secondRun = getRenderedString()
expect(firstRun).toBe(secondRun)
})
}
/* eslint jsx-a11y/label-has-for:0 */
================================================
FILE: other/ssr/jest.config.js
================================================
// This is separate because the test environment is set via the config
// and we want most of our tests to run with jsdom, but we still want
// to make sure that the server rendering use case continues to work.
const jestConfig = require('kcd-scripts/config').jest
module.exports = Object.assign(jestConfig, {
roots: ['.'],
testEnvironment: 'node',
})
================================================
FILE: package.json
================================================
{
"name": "downshift",
"version": "0.0.0-semantically-released",
"description": "🏎 A set of primitives to build simple, flexible, WAI-ARIA compliant React autocomplete, combobox or select dropdown components.",
"main": "dist/downshift.cjs.cjs",
"react-native": "dist/downshift.native.cjs.cjs",
"module": "dist/downshift.esm.mjs",
"typings": "typings/index.d.ts",
"types": "typings/index.d.ts",
"sideEffects": false,
"browserslist": [],
"scripts": {
"build": "npm run build:web --silent && npm run build:native --silent && npm run build:nativeWeb --silent",
"build:web": "kcd-scripts build --bundle --p-react --no-clean --size-snapshot",
"build:native": "cross-env BUILD_REACT_NATIVE=true BUILD_FILENAME_SUFFIX=.native kcd-scripts build --bundle cjs --no-clean",
"build:nativeWeb": "cross-env BUILD_REACT_NATIVE_WEB=true BUILD_FILENAME_SUFFIX=.nativeweb kcd-scripts build --bundle cjs --no-clean",
"lint": "kcd-scripts lint",
"test": "kcd-scripts test",
"test:cover": "kcd-scripts test --coverage",
"test:ssr": "kcd-scripts test --config other/ssr/jest.config.js --no-watch",
"test:update": "npm run test:cover -s -- --updateSnapshot",
"test:ts": "tsc --noEmit -p ./tsconfig.json",
"test:flow": "flow",
"test:flow:coverage": "flow-coverage-report",
"test:build": "jest --config other/misc-tests/jest.config.js",
"// FIXME: test:build": "jest --projects other/misc-tests other/react-native",
"test:cypress:dev": "npm-run-all --parallel --race docs:dev cy:open",
"pretest:cypress": "npm run docs:build --silent",
"test:cypress": "start-server-and-test docs:serve http://localhost:6006 cy:run",
"cy:run": "cypress run",
"cy:open": "cypress open",
"build-and-test": "npm run build -s && npm run test:build -s",
"docs:build": "docusaurus build",
"docs:dev": "docusaurus start",
"docs:serve": "docusaurus serve --port 6006",
"docs:clear": "docusaurus clear",
"setup": "npm install && npm run validate",
"validate": "kcd-scripts validate lint,build-and-test,test:cover,test:ts,test:ssr,test:cypress"
},
"husky": {
"hooks": {
"pre-commit": "kcd-scripts pre-commit"
}
},
"files": [
"dist",
"typings",
"preact",
"flow-typed"
],
"exports": {
".": {
"import": "./dist/downshift.esm.mjs",
"require": "./dist/downshift.cjs.cjs",
"types": "./typings/index.d.ts",
"default": "./dist/downshift.esm.mjs"
},
"./react-native": {
"require": "./dist/downshift.native.cjs.cjs",
"types": "./typings/index.d.ts"
}
},
"keywords": [
"enhanced input",
"react",
"autocomplete",
"autosuggest",
"typeahead",
"dropdown",
"select",
"combobox",
"omnibox",
"accessibility",
"WAI-ARIA",
"multiselect",
"multiple selection"
],
"author": "Kent C. Dodds (http://kentcdodds.com/)",
"license": "MIT",
"peerDependencies": {
"react": ">=16.12.0"
},
"dependencies": {
"@babel/runtime": "^7.28.6",
"compute-scroll-into-view": "^3.1.1",
"prop-types": "^15.8.1",
"react-is": "^18.2.0",
"tslib": "^2.8.1"
},
"devDependencies": {
"@babel/helpers": "^7.28.6",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@cypress/webpack-preprocessor": "^7.0.2",
"@docusaurus/core": "3.3.2",
"@docusaurus/module-type-aliases": "3.3.2",
"@docusaurus/preset-classic": "3.3.2",
"@mdx-js/react": "^3.0.1",
"@rollup/plugin-babel": "^6.1.0",
"@rollup/plugin-commonjs": "^29.0.0",
"@testing-library/cypress": "^10.1.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/preact": "^3.2.4",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^30.0.0",
"@types/prop-types": "^15.7.15",
"@types/react": "^18.2.0",
"@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-no-side-effect-class-properties": "0.0.7",
"babel-preset-react-native": "^4.0.1",
"buble": "^0.20.0",
"cpy-cli": "^6.0.0",
"cross-env": "^10.1.0",
"cypress": "15.9.0",
"eslint": "^8.57.0",
"eslint-plugin-cypress": "^3.6.0",
"eslint-plugin-react": "7.37.5",
"flow-bin": "^0.299.0",
"flow-coverage-report": "^0.8.0",
"get-pkg-repo": "5.0.0",
"kcd-scripts": "^17.0.0",
"node-polyfill-webpack-plugin": "^4.1.0",
"npm-run-all": "^4.1.5",
"preact": "^10.28.2",
"prism-react-renderer": "^2.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-is": "^18.3.1",
"react-native": "^0.76.0",
"react-test-renderer": "^18.3.1",
"serve": "^14.2.5",
"start-server-and-test": "^2.1.3",
"typescript": "^5.9.3"
},
"eslintConfig": {
"parserOptions": {
"ecmaVersion": 2023,
"project": [
"./tsconfig.json",
"./docusaurus/tsconfig.json",
"./test/tsconfig.json"
],
"sourceType": "module"
},
"settings": {
"import/no-unresolved": [
2,
{
"ignore": [
"^@theme"
]
}
],
"import/resolver": {
"node": {
"extensions": [
".js",
".jsx",
".ts",
".tsx"
]
}
}
},
"extends": "./node_modules/kcd-scripts/eslint.js",
"rules": {
"react/jsx-indent": "off",
"react/prop-types": "off",
"max-lines-per-function": "off",
"jsx-a11y/label-has-for": "off",
"jsx-a11y/label-has-associated-control": "off",
"jsx-a11y/autocomplete-valid": "off",
"testing-library/prefer-user-event": "off",
"testing-library/no-node-access": "off",
"testing-library/no-container": "off",
"testing-library/render-result-naming-convention": "off"
},
"overrides": [
{
"files": [
"cypress/**/*.js"
],
"rules": {
"testing-library/prefer-screen-queries": "off",
"testing-library/await-async-query": "off"
}
}
]
},
"eslintIgnore": [
"node_modules",
"coverage",
"dist",
".docusaurus",
"build",
"typings",
"test"
],
"repository": {
"type": "git",
"url": "https://github.com/downshift-js/downshift.git"
},
"bugs": {
"url": "https://github.com/downshift-js/downshift/issues"
},
"homepage": "https://downshift-js.com",
"flow-coverage-report": {
"includeGlob": [
"test/**/*.js"
],
"threshold": 90,
"type": [
"text"
]
}
}
================================================
FILE: prettier.config.js
================================================
// this is really only here for editor integrations
module.exports = require('kcd-scripts/prettier')
================================================
FILE: rollup.config.js
================================================
const commonjs = require('@rollup/plugin-commonjs')
const {babel} = require('@rollup/plugin-babel')
const config = require('kcd-scripts/dist/config/rollup.config')
const babelPlugin = babel({
babelHelpers: 'runtime',
extensions: ['.js', '.jsx', '.ts', '.tsx'],
exclude: '**/node_modules/**',
})
const cjsPlugin = commonjs({include: 'node_modules/**'})
config.plugins = [
babelPlugin,
cjsPlugin,
...config.plugins.filter(
p => !['babel', 'typescript', 'commonjs'].includes(p.name),
),
]
const prevExternal = config.external
config.external = id => {
if (id.includes('productionEnum.macro') || id.includes('is.macro')) {
return true
}
if (typeof prevExternal === 'function') return prevExternal(id)
if (Array.isArray(prevExternal)) return prevExternal.includes(id)
return false
}
module.exports = config
================================================
FILE: src/__mocks__/set-a11y-status.js
================================================
module.exports = {setStatus: jest.fn(), cleanupStatusDiv: jest.fn()}
================================================
FILE: src/__mocks__/utils.js
================================================
const actualUtils = jest.requireActual('../utils')
module.exports = Object.assign(actualUtils, {
scrollIntoView: jest.fn(), // hard to write tests for this thing...
})
================================================
FILE: src/__tests__/.eslintrc
================================================
{
"rules": {
"jsx-a11y/label-has-for": "off",
"jsx-a11y/click-events-have-key-events": "off",
"react/prop-types": "off",
"react/display-name": "off",
"react/no-deprecated": "off",
"no-console": "off"
}
}
================================================
FILE: src/__tests__/__snapshots__/downshift.aria.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`basic snapshot 1`] = `
label
`;
exports[`can override the ids 1`] = `
label
`;
================================================
FILE: src/__tests__/__snapshots__/downshift.get-item-props.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getItemProps defaults the index when no index is given 1`] = `
0
1
2
3
4
`;
exports[`getItemProps logs a helpful error when no object is given 1`] = `The property "item" is required in "getItemProps"`;
exports[`getItemProps logs error when no item is given 1`] = `The property "item" is required in "getItemProps"`;
================================================
FILE: src/__tests__/__snapshots__/downshift.get-menu-props.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`not applying the ref prop results in an error 1`] = `downshift: The ref prop "ref" from getMenuProps was not applied correctly on your menu element.`;
exports[`using a composite component and calling getMenuProps without a refKey results in an error 1`] = `downshift: The ref prop "ref" from getMenuProps was not applied correctly on your menu element.`;
================================================
FILE: src/__tests__/__snapshots__/downshift.get-root-props.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`not applying the ref prop results in an error 1`] = `downshift: You must apply the ref prop "ref" from getRootProps onto your root element.`;
exports[`returning a DOM element and calling getRootProps with a refKey results in an error 1`] = `downshift: You returned a DOM element. You should not specify a refKey in getRootProps. You specified "blah"`;
exports[`returning a composite component and calling getRootProps without a refKey results in an error 1`] = `downshift: You returned a non-DOM element. You must specify a refKey in getRootProps`;
exports[`returning a composite component without calling getRootProps results in an error 1`] = `downshift: If you return a non-DOM element, you must apply the getRootProps function`;
================================================
FILE: src/__tests__/__snapshots__/downshift.misc.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`expect console.warn to fire—depending on process.env.NODE_ENV value it should warn exactly one time when value !== production 1`] = `
[
downshift: An object was passed to the default implementation of \`itemToString\`. You should probably provide your own \`itemToString\` implementation. Please refer to the \`itemToString\` API documentation.,
The object that was passed:,
{
label: test,
value: any,
},
]
`;
exports[`warns when controlled component becomes uncontrolled 1`] = `
[
[
downshift: A component has changed the controlled prop "selectedItem" to be uncontrolled. This prop should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled Downshift element for the lifetime of the component. More info: https://github.com/downshift-js/downshift#control-props,
],
]
`;
exports[`warns when uncontrolled component becomes controlled 1`] = `
[
[
downshift: A component has changed the uncontrolled prop "selectedItem" to be controlled. This prop should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled Downshift element for the lifetime of the component. More info: https://github.com/downshift-js/downshift#control-props,
],
]
`;
================================================
FILE: src/__tests__/__snapshots__/set-a11y-status.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`creates new status div if there is none 1`] = `
[
[
id,
a11y-status-message,
],
[
role,
status,
],
[
aria-live,
polite,
],
[
aria-relevant,
additions text,
],
]
`;
exports[`escapes HTML 1`] = `
<script>alert("!!!")</script>
`;
exports[`performs cleanup after a timeout 1`] = `
`;
exports[`replaces the status with a different one 1`] = `
goodbye
`;
exports[`sets the status 1`] = `
hello
`;
================================================
FILE: src/__tests__/downshift.aria.js
================================================
import * as React from 'react'
import {render, screen} from '@testing-library/react'
import Downshift from '../'
import {resetIdCounter} from '../utils-ts'
beforeEach(() => {
if (!('useId' in React)) resetIdCounter()
})
test('basic snapshot', () => {
const {container} = renderDownshift({props: {selectedItem: 'item'}})
expect(container.firstChild).toMatchSnapshot()
})
test('can override the ids', () => {
const {container} = renderDownshift({
props: {
inputId: 'custom-input-id',
labelId: 'custom-label-id',
menuId: 'custom-menu-id',
getItemId: index => `custom-item-id-${index}`,
},
})
expect(container.firstChild).toMatchSnapshot()
})
test('if aria-label is provided to the menu then aria-labelledby is not applied to the menu', () => {
const customLabel = 'custom menu label'
const {menu} = renderDownshift({
menuProps: {'aria-label': customLabel},
})
expect(menu).not.toHaveAttribute('aria-labelledby')
expect(menu).toHaveAttribute('aria-label', customLabel)
})
test('if aria-label is provided to the input then aria-labelledby is not applied to the input', () => {
const customLabel = 'custom menu label'
const {input} = renderDownshift({
inputProps: {'aria-label': customLabel},
})
expect(input).not.toHaveAttribute('aria-labelledby')
expect(input).toHaveAttribute('aria-label', customLabel)
})
function renderDownshift({renderFn, props, menuProps, inputProps} = {}) {
function defaultRenderFn({
getInputProps,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getItemProps,
}) {
return (
)
}
let renderArg
const childrenSpy = jest.fn(controllerArg => {
renderArg = controllerArg
return renderFn || defaultRenderFn(controllerArg)
})
const utils = render({childrenSpy} )
return {
...utils,
renderArg,
root: screen.queryByTestId('root'),
input: screen.queryByTestId('input'),
menu: screen.queryByTestId('menu'),
}
}
================================================
FILE: src/__tests__/downshift.focus-restoration.js
================================================
import * as React from 'react'
import {render, screen} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Downshift from '../'
test('focus restored upon item mouse click', () => {
renderDownshift(['A', 'B'])
const inputNode = screen.getByRole(`textbox`)
const buttonNode = screen.getByRole('button')
const item = screen.queryByTestId('A')
expect(document.activeElement.nodeName).toEqual('BODY')
inputNode.focus()
expect(inputNode).toHaveFocus()
userEvent.click(item)
expect(inputNode).toHaveFocus()
buttonNode.focus()
expect(buttonNode).toHaveFocus()
userEvent.click(item)
expect(buttonNode).toHaveFocus()
})
function renderDownshift(items) {
const id = 'languages[0].name'
return render(
{({getInputProps, getItemProps, getToggleButtonProps}) => (
{items.map(item => (
{item}
))}
)}
,
)
}
================================================
FILE: src/__tests__/downshift.get-button-props.js
================================================
import * as React from 'react'
import {render, fireEvent, act} from '@testing-library/react'
import Downshift from '../'
jest.useFakeTimers()
test('space on button opens and closes the menu', () => {
const {button, childrenSpy} = setup()
fireEvent.keyDown(button, {key: ' '})
fireEvent.keyUp(button, {key: ' '})
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({isOpen: true}),
)
fireEvent.keyDown(button, {key: ' '})
fireEvent.keyUp(button, {key: ' '})
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({isOpen: false}),
)
})
test('clicking on the button opens and closes the menu', () => {
const {button, childrenSpy} = setup()
fireEvent.click(button)
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({isOpen: true}),
)
fireEvent.click(button)
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({isOpen: false}),
)
})
test('button ignores key events it does not handle', () => {
const {button, childrenSpy} = setup()
childrenSpy.mockClear()
fireEvent.keyDown(button, {key: 's'})
expect(childrenSpy).not.toHaveBeenCalled()
})
test('on button blur resets the state', () => {
const {button, childrenSpy} = setup()
fireEvent.blur(button)
act(() => {
jest.runAllTimers()
})
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: false,
}),
)
})
test('on button blur does not reset the state when the mouse is down', () => {
const {button, childrenSpy} = setup()
childrenSpy.mockClear()
// mousedown somwhere
fireEvent.mouseDown(document.body)
fireEvent.blur(button)
jest.runAllTimers()
expect(childrenSpy).not.toHaveBeenCalled()
})
test('on open it will highlight item if state has highlightedIndex', () => {
const highlightedIndex = 4
const {button, childrenSpy} = setup({props: {highlightedIndex}})
fireEvent.click(button)
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex}),
)
})
test('getToggleButtonProps returns all given props', () => {
const buttonProps = {'data-foo': 'bar'}
const Button = jest.fn(props => )
setup({buttonProps, Button})
expect(Button).toHaveBeenCalledTimes(1)
expect(Button).toHaveBeenCalledWith(expect.objectContaining(buttonProps), expect.anything())
})
// normally this test would be like the others where we render and then simulate a click on the
// button to ensure that a disabled button cannot be clicked, however this is only a problem in IE11
// so we have to get into the implementation details a little bit (unless we want to run these tests
// in IE11... no thank you 🙅)
test(`getToggleButtonProps doesn't include event handlers when disabled is passed (for IE11 support)`, () => {
const {getToggleButtonProps} = setup()
const props = getToggleButtonProps({disabled: true})
const entry = Object.entries(props).find(
([_key, value]) => typeof value === 'function',
)
// eslint-disable-next-line jest/no-conditional-in-test
if (entry) {
throw new Error(
`getToggleButtonProps should not have any props that are callbacks. It has ${entry[0]}.`,
)
}
})
describe('Expect timer to trigger on process.env.NODE_ENV !== test value', () => {
const originalEnv = process.env.NODE_ENV
afterEach(() => {
process.env.NODE_ENV = originalEnv
})
test('clicking on the button opens and closes the menu for test', () => {
process.env.NODE_ENV = 'production'
const {button, childrenSpy} = setup()
fireEvent.click(button)
act(() => {
jest.runAllTimers()
})
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({isOpen: true}),
)
fireEvent.click(button)
act(() => {
jest.runAllTimers()
})
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({isOpen: false}),
)
})
})
function setup({
buttonProps,
props,
Button = propsArg => ,
} = {}) {
let renderArg
const childrenSpy = jest.fn(controllerArg => {
renderArg = controllerArg
return (
{/* Added items to test toggleMenu with highlight. */}
)
})
const utils = render({childrenSpy} )
const button = utils.container.querySelector('button')
return {...utils, button, childrenSpy, ...renderArg}
}
================================================
FILE: src/__tests__/downshift.get-input-props.js
================================================
import * as React from 'react'
import {
render,
fireEvent,
screen,
createEvent,
act,
} from '@testing-library/react'
import Downshift from '../'
jest.useFakeTimers()
const colors = [
'Red',
'Green',
'Blue',
'Orange',
'Purple',
'Pink',
'Palevioletred',
'Rebeccapurple',
'Navy Blue',
]
test('manages arrow up and down behavior', () => {
const {arrowUpInput, arrowDownInput, childrenSpy, endOnInput, homeOnInput} =
renderDownshift()
// ↓
arrowDownInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({isOpen: true, highlightedIndex: 0}),
)
// ↓
arrowDownInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: 1}),
)
// ↓
arrowDownInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: 2}),
)
// ↓
arrowDownInput({shiftKey: true})
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: 7}),
)
// ↑
arrowUpInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: 6}),
)
// ↑
arrowUpInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: 5}),
)
// ↑
arrowUpInput({shiftKey: true})
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: 0}),
)
// ↑
arrowUpInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: colors.length - 1}),
)
// ↓
arrowDownInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: 0}),
)
endOnInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: colors.length - 1}),
)
homeOnInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: 0}),
)
})
describe('arrow down opens menu and highlights item at index', () => {
test('0 by default', () => {
const {arrowDownInput, childrenSpy} = renderDownshift()
// ↓
arrowDownInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({isOpen: true, highlightedIndex: 0}),
)
})
test('initialHighlightedIndex + 1', () => {
const initialHighlightedIndex = 3
const {arrowDownInput, childrenSpy} = renderDownshift({
// provide only highlightedIndex
props: {initialHighlightedIndex},
})
// ↓
arrowDownInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: true,
highlightedIndex: initialHighlightedIndex + 1,
}),
)
})
test('defaultHighlightedIndex + 1', () => {
const defaultHighlightedIndex = 2
const {arrowDownInput, childrenSpy} = renderDownshift({
// provide only defaultHighlightedIndex
props: {defaultHighlightedIndex},
})
// ↓
arrowDownInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: true,
highlightedIndex: defaultHighlightedIndex + 1,
}),
)
})
test('initialHighlightedIndex + 1 then defaultHighlightedIndex + 1', () => {
const initialHighlightedIndex = 3
const defaultHighlightedIndex = 2
const {arrowDownInput, escapeOnInput, childrenSpy} = renderDownshift({
// provide both initialHighlightedIndex and defaultHighlightedIndex
props: {defaultHighlightedIndex, initialHighlightedIndex},
})
// ↓
arrowDownInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: true,
highlightedIndex: initialHighlightedIndex + 1,
}),
)
escapeOnInput()
// ↓
arrowDownInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: true,
highlightedIndex: defaultHighlightedIndex + 1,
}),
)
})
test('0 if defaultHighlightedIndex is length - 1', () => {
const defaultHighlightedIndex = colors.length - 1
const {arrowDownInput, childrenSpy} = renderDownshift({
// provide defaultHighlightedIndex as last in the list.
props: {defaultHighlightedIndex},
})
// ↓
arrowDownInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: true,
highlightedIndex: 0,
}),
)
})
test('0 if defaultHighlightedIndex is out of bounds', () => {
const {arrowDownInput, childrenSpy} = renderDownshift({
// provide defaultHighlightedIndex as invalid
props: {defaultHighlightedIndex: colors.length + 5},
})
// ↓
arrowDownInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: true,
highlightedIndex: 0,
}),
)
})
test('highlightedIndex if controlled', () => {
const highlightedIndex = 2
const {arrowDownInput, childrenSpy} = renderDownshift({
// control highlightedIndex
props: {highlightedIndex},
})
// ↓
arrowDownInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: true,
highlightedIndex,
}),
)
})
})
describe('arrow up opens menu and highlights item at index', () => {
test('length - 1 by default', () => {
const {arrowUpInput, childrenSpy} = renderDownshift()
// ↑
arrowUpInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: true,
highlightedIndex: colors.length - 1,
}),
)
})
test('initialHighlightedIndex - 1', () => {
const initialHighlightedIndex = 3
const {arrowUpInput, childrenSpy} = renderDownshift({
// provide only initialHighlightedIndex
props: {initialHighlightedIndex},
})
// ↑
arrowUpInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: true,
highlightedIndex: initialHighlightedIndex - 1,
}),
)
})
test('defaultHighlightedIndex - 1', () => {
const defaultHighlightedIndex = 2
const {arrowUpInput, childrenSpy} = renderDownshift({
// provide only defaultHighlightedIndex
props: {defaultHighlightedIndex},
})
// ↑
arrowUpInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: true,
highlightedIndex: defaultHighlightedIndex - 1,
}),
)
})
test('initialHighlightedIndex - 1 then defaultHighlightedIndex - 1', () => {
const initialHighlightedIndex = 3
const defaultHighlightedIndex = 2
const {arrowUpInput, escapeOnInput, childrenSpy} = renderDownshift({
// provide both initialHighlightedIndex and defaultHighlightedIndex
props: {defaultHighlightedIndex, initialHighlightedIndex},
})
// ↑
arrowUpInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: true,
highlightedIndex: initialHighlightedIndex - 1,
}),
)
escapeOnInput()
// ↑
arrowUpInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: true,
highlightedIndex: defaultHighlightedIndex - 1,
}),
)
})
test('length - 1 if defaultHighlightedIndex is 0', () => {
const defaultHighlightedIndex = 0
const {arrowUpInput, childrenSpy} = renderDownshift({
// provide defaultHighlightedIndex as first in the list
props: {defaultHighlightedIndex},
})
// ↑
arrowUpInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: true,
highlightedIndex: colors.length - 1,
}),
)
})
test('length - 1 if defaultHighlightedIndex is out of bounds', () => {
const {arrowUpInput, childrenSpy} = renderDownshift({
// provide defaultHighlightedIndex as invalid
props: {defaultHighlightedIndex: colors.length + 5},
})
// ↑
arrowUpInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: true,
highlightedIndex: colors.length - 1,
}),
)
})
test('highlightedIndex if controlled', () => {
const highlightedIndex = 2
const {arrowUpInput, childrenSpy} = renderDownshift({
// control highlightedIndex
props: {highlightedIndex},
})
// ↑
arrowUpInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: true,
highlightedIndex,
}),
)
})
})
test('navigation key down events do nothing when no items are rendered', () => {
const {
arrowDownInput,
arrowUpInput,
endOnInput,
homeOnInput,
escapeOnInput,
childrenSpy,
} = renderDownshift({items: []})
const keysOnInput = [arrowDownInput, arrowUpInput, endOnInput, homeOnInput]
// ↓ ↑ end home
keysOnInput.forEach(keyOnInput => {
keyOnInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: null}),
)
escapeOnInput() // close dropdown after each opening.
})
// ↓ ↑ end home
keysOnInput.forEach(keyOnInput => {
keyOnInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: null}),
)
// do not close dropdown, but still there should be no update.
})
})
test('home and end keys should not call highlighting method when menu is closed', () => {
const {childrenSpy, endOnInput, homeOnInput} = renderDownshift()
// home
homeOnInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({isOpen: false, highlightedIndex: null}),
)
// end
endOnInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({isOpen: false, highlightedIndex: null}),
)
})
test('home and end keys should not prevent event default when menu is closed', () => {
const {input} = renderDownshift()
const homeKeyDownEvent = createEvent.keyDown(input, {key: 'Home'})
const endKeyDownEvent = createEvent.keyDown(input, {key: 'End'})
// home
fireEvent(input, homeKeyDownEvent)
expect(homeKeyDownEvent.defaultPrevented).toBe(false)
// end
fireEvent(input, endKeyDownEvent)
expect(endKeyDownEvent.defaultPrevented).toBe(false)
})
test('home and end keys should prevent event default when menu is open', () => {
const {input} = renderDownshift({props: {defaultIsOpen: true}})
const homeKeyDownEvent = createEvent.keyDown(input, {key: 'Home'})
const endKeyDownEvent = createEvent.keyDown(input, {key: 'End'})
// home
fireEvent(input, homeKeyDownEvent)
expect(homeKeyDownEvent.defaultPrevented).toBe(true)
// end
fireEvent(input, endKeyDownEvent)
expect(endKeyDownEvent.defaultPrevented).toBe(true)
})
test('enter on an input with a closed menu does nothing', () => {
const {enterOnInput, childrenSpy} = renderDownshift()
childrenSpy.mockClear()
// ENTER
enterOnInput()
// does not even rerender
expect(childrenSpy).not.toHaveBeenCalled()
})
test('enter on an input with an open menu does nothing without a highlightedIndex', () => {
const {enterOnInput, childrenSpy} = renderDownshift({props: {isOpen: true}})
childrenSpy.mockClear()
// ENTER
enterOnInput()
// does not even rerender
expect(childrenSpy).not.toHaveBeenCalled()
})
test('enter on an input with an open menu and a highlightedIndex but with IME composing will not select that item', () => {
const {enterOnInput, childrenSpy} = renderDownshift({
props: {initialIsOpen: true, initialHighlightedIndex: 0},
})
const extraEventProps = {keyCode: 229}
childrenSpy.mockClear()
// Enter but for IME
enterOnInput(extraEventProps)
// does not even rerender
expect(childrenSpy).not.toHaveBeenCalled()
// Enter without IME
enterOnInput()
// now it behaves normally
expect(childrenSpy).toHaveBeenCalledTimes(1)
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
selectedItem: colors[0],
inputValue: colors[0],
isOpen: false,
highlightedIndex: null,
}),
)
})
test('enter on an input with an open menu and a highlightedIndex selects that item', () => {
const onChange = jest.fn()
const isOpen = true
const {arrowDownInput, enterOnInput, childrenSpy} = renderDownshift({
props: {isOpen, onChange},
})
// ↓
arrowDownInput()
// ENTER
enterOnInput()
expect(onChange).toHaveBeenCalledTimes(1)
const newState = expect.objectContaining({
selectedItem: colors[0],
isOpen,
highlightedIndex: null,
inputValue: colors[0],
})
expect(onChange).toHaveBeenCalledWith(colors[0], newState)
expect(childrenSpy).toHaveBeenLastCalledWith(newState)
})
test('escape on an input without a selection should reset downshift and close the menu', () => {
const {changeInputValue, input, escapeOnInput, childrenSpy} =
renderDownshift()
changeInputValue('p')
escapeOnInput()
expect(input).toHaveValue('')
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: false,
selectedItem: null,
inputValue: '',
}),
)
})
test('escape on an input with a selection and open should only reset downshift', () => {
const {escapeOnInput, childrenSpy} = renderDownshift()
escapeOnInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({isOpen: false}),
)
})
test('escape on an input with a selection and closed menu should reset downshift, clear input and close the menu', () => {
const {escapeOnInput, childrenSpy} = setupDownshiftWithState()
escapeOnInput()
escapeOnInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: false,
inputValue: '',
selectedItem: null,
}),
)
})
test('on input blur resets the state', () => {
const {blurOnInput, childrenSpy, items} = setupDownshiftWithState()
blurOnInput()
act(() => {
jest.runAllTimers()
})
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
isOpen: false,
inputValue: items[0],
selectedItem: items[0],
}),
)
})
test('on input blur does not reset the state when the mouse is down', () => {
const {blurOnInput, childrenSpy} = setupDownshiftWithState()
// mousedown somwhere
fireEvent.mouseDown(document.body)
blurOnInput()
jest.runAllTimers()
expect(childrenSpy).not.toHaveBeenCalled()
})
test('on input blur does not reset the state when new focus is on downshift button', () => {
const {blurOnInput, childrenSpy, button} = setupDownshiftWithState()
blurOnInput()
button.focus()
jest.runAllTimers()
expect(childrenSpy).not.toHaveBeenCalled()
})
test('on toggle button blur does not reset the state when there is no environment', () => {
const items = ['animal', 'bug', 'cat']
const utils = renderDownshift({items, props: {environment: null}})
const {childrenSpy, changeInputValue, arrowDownInput, enterOnInput, button} =
utils
changeInputValue('a')
// ↓
arrowDownInput()
// ENTER to select the first one
enterOnInput()
childrenSpy.mockReset()
button.focus()
button.blur()
act(() => {
jest.runAllTimers()
})
expect(childrenSpy).not.toHaveBeenCalled()
})
test('on toggle button blur does not reset the state when input gets focused', () => {
const items = ['animal', 'bug', 'cat']
const utils = renderDownshift({
items,
})
const {childrenSpy, changeInputValue, arrowDownInput, enterOnInput, button, input} =
utils
changeInputValue('a')
// ↓
arrowDownInput()
// ENTER to select the first one
enterOnInput()
childrenSpy.mockReset()
button.focus()
button.blur()
input.focus()
act(() => {
jest.runAllTimers()
})
expect(childrenSpy).not.toHaveBeenCalled()
})
test('keydown of things that are not handled do nothing', () => {
const modifiers = [undefined, 'Shift']
const {input, childrenSpy} = renderDownshift()
childrenSpy.mockClear()
modifiers.forEach(key => {
fireEvent.keyDown(input, {key})
})
// does not even rerender
expect(childrenSpy).not.toHaveBeenCalled()
})
test('highlightedIndex uses the given itemCount prop to determine the last index', () => {
const itemCount = 200
const {arrowUpInput, childrenSpy} = renderDownshift({
props: {itemCount, isOpen: true},
})
// ↑
arrowUpInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: itemCount - 1}),
)
})
test('itemCount can be set and unset asynchronously', () => {
let downshift
const childrenSpy = jest.fn(d => {
downshift = d
return (
)
})
render(
{childrenSpy}
,
)
const input = screen.queryByTestId('input')
const up = () => fireEvent.keyDown(input, {key: 'ArrowUp'})
const down = () => fireEvent.keyDown(input, {key: 'ArrowDown'})
downshift.setItemCount(100)
up()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: 99}),
)
down()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: 0}),
)
downshift.setItemCount(40)
up()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: 39}),
)
down()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: 0}),
)
downshift.unsetItemCount()
up()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({highlightedIndex: 9}),
)
})
test('Enter when there is no item at index 0 still selects the highlighted item', () => {
// test inspired by https://github.com/downshift-js/downshift/issues/119
// use case is virtualized lists
const items = [
{value: 'cat', index: 1},
{value: 'dog', index: 2},
{value: 'bird', index: 3},
{value: 'cheetah', index: 4},
]
const {arrowDownInput, enterOnInput, childrenSpy} = renderDownshift({
items,
props: {
itemToString: i => i.value,
defaultHighlightedIndex: 1,
isOpen: true,
},
})
// ↓
arrowDownInput()
// ENTER
childrenSpy.mockClear()
enterOnInput()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
selectedItem: items[1],
}),
)
})
// normally this test would be like the others where we render and then simulate a click on the
// button to ensure that a disabled input cannot be interacted with, however this is only a problem in IE11
// so we have to get into the implementation details a little bit (unless we want to run these tests
// in IE11... no thank you 🙅)
test(`getInputProps doesn't include event handlers when disabled is passed (for IE11 support)`, () => {
const {getInputProps} = setupWithDownshiftController()
const props = getInputProps({disabled: true})
const entry = Object.entries(props).find(
([_key, value]) => typeof value === 'function',
)
// eslint-disable-next-line jest/no-conditional-in-test
if (entry) {
throw new Error(
`getInputProps should not have any props that are callbacks. It has ${entry[0]}.`,
)
}
})
test('highlightedIndex is reset to defaultHighlightedIndex when inputValue changes', () => {
const defaultHighlightedIndex = 0
const {childrenSpy, arrowDownInput, changeInputValue} = renderDownshift({
props: {defaultHighlightedIndex},
})
childrenSpy.mockClear()
arrowDownInput() // highlightedIndex = 1
changeInputValue('r')
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
highlightedIndex: defaultHighlightedIndex,
}),
)
})
test('highlight should be removed on inputValue change if defaultHighlightedIndex is not provided', () => {
const {childrenSpy, arrowDownInput, changeInputValue} = renderDownshift()
childrenSpy.mockClear()
arrowDownInput() // highlightedIndex = 1
changeInputValue('r')
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
highlightedIndex: null,
}),
)
})
function setupDownshiftWithState() {
const items = ['animal', 'bug', 'cat']
const utils = renderDownshift({items})
const {input, changeInputValue, arrowDownInput, enterOnInput, childrenSpy} =
utils
// input.fireEvent('keydown')
changeInputValue('a')
// ↓
arrowDownInput()
// ENTER to select the first one
enterOnInput()
expect(input).toHaveValue(items[0])
// ↓
arrowDownInput()
changeInputValue('bu')
childrenSpy.mockClear()
return {childrenSpy, items, ...utils}
}
function setup({items = colors} = {}) {
/* eslint-disable react/jsx-closing-bracket-location */
const childrenSpy = jest.fn(
({isOpen, getInputProps, getToggleButtonProps, getItemProps}) => (
{isOpen ? (
{items.map((item, index) => (
{item.value ? item.value : item}
))}
) : null}
),
)
function BasicDownshift(props) {
return {childrenSpy}
}
return {
Component: BasicDownshift,
childrenSpy,
}
}
function renderDownshift({items, props} = {}) {
const {Component, childrenSpy} = setup({items})
const utils = render( )
const input = screen.queryByTestId('input')
return {
Component,
childrenSpy,
...utils,
input,
button: screen.queryByTestId('button'),
arrowDownInput: extraEventProps =>
fireEvent.keyDown(input, {key: 'ArrowDown', ...extraEventProps}),
arrowUpInput: extraEventProps =>
fireEvent.keyDown(input, {key: 'ArrowUp', ...extraEventProps}),
endOnInput: extraEventProps =>
fireEvent.keyDown(input, {key: 'End', ...extraEventProps}),
escapeOnInput: extraEventProps =>
fireEvent.keyDown(input, {key: 'Escape', ...extraEventProps}),
enterOnInput: extraEventProps =>
fireEvent.keyDown(input, {key: 'Enter', ...extraEventProps}),
homeOnInput: extraEventProps =>
fireEvent.keyDown(input, {key: 'Home', ...extraEventProps}),
changeInputValue: (value, extraEventProps) =>
fireEvent.change(input, {target: {value}, ...extraEventProps}),
blurOnInput: extraEventProps => fireEvent.blur(input, extraEventProps),
}
}
function setupWithDownshiftController() {
let renderArg
render(
{controllerArg => {
renderArg = controllerArg
return null
}}
,
)
return renderArg
}
================================================
FILE: src/__tests__/downshift.get-item-props.js
================================================
import * as React from 'react'
import {render, fireEvent, screen} from '@testing-library/react'
import Downshift from '../'
import {setIdCounter} from '../utils-ts'
beforeEach(() => {
setIdCounter(1)
jest.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => console.error.mockRestore())
test('clicking on a DOM node within an item selects that item', () => {
// inspiration: https://github.com/downshift-js/downshift/issues/113
const items = [{item: 'Chess'}, {item: 'Dominion'}, {item: 'Checkers'}]
const {childrenSpy} = renderDownshift({items})
const firstButton = screen.queryByTestId('item-0-button')
childrenSpy.mockClear()
fireEvent.click(firstButton)
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
selectedItem: items[0].item,
}),
)
})
test('clicking anywhere within the rendered downshift but outside an item does not select an item', () => {
const childrenSpy = jest.fn(() => (
))
const {container} = render({childrenSpy} )
childrenSpy.mockClear()
fireEvent.click(container.querySelector('button'))
expect(childrenSpy).not.toHaveBeenCalled()
})
test('on mousemove of an item updates the highlightedIndex to that item', () => {
const {childrenSpy} = renderDownshift()
const thirdButton = screen.queryByTestId('item-2-button')
childrenSpy.mockClear()
fireEvent.mouseMove(thirdButton)
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
highlightedIndex: 2,
}),
)
})
test('on mousemove of the highlighted item should not emit changes', () => {
const {childrenSpy} = renderDownshift({
props: {defaultHighlightedIndex: 1},
})
const secondButton = screen.queryByTestId('item-1-button')
childrenSpy.mockClear()
fireEvent.mouseMove(secondButton)
expect(childrenSpy).not.toHaveBeenCalled()
})
test('on mousedown of the item should not change current focused element', () => {
const childrenSpy = jest.fn(({getItemProps}) => (
))
render({childrenSpy} )
const externalButton = screen.queryByTestId('external-button')
const inItemButton = screen.queryByTestId('in-item-button')
childrenSpy.mockClear()
externalButton.focus()
expect(externalButton).toHaveFocus()
fireEvent.mouseDown(inItemButton)
expect(externalButton).toHaveFocus()
})
test('after selecting an item highlightedIndex should be reset to defaultHighlightIndex', () => {
const {childrenSpy} = renderDownshift({
props: {defaultHighlightedIndex: 1},
})
const firstButton = screen.queryByTestId('item-0-button')
childrenSpy.mockClear()
fireEvent.click(firstButton)
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
highlightedIndex: 1,
}),
)
})
test('getItemProps logs a helpful error when no object is given', () => {
render(
{({getItemProps}) => (
)}
,
)
expect(console.error.mock.calls[0][0]).toMatchSnapshot()
})
test('getItemProps defaults the index when no index is given', () => {
expect(
render(
{({getItemProps}) => (
0
1
2
3
4
)}
,
).container.firstChild,
).toMatchSnapshot()
})
test('getItemProps logs error when no item is given', () => {
render(
{({getItemProps}) => (
)}
,
)
expect(console.error.mock.calls[0][0]).toMatchSnapshot()
})
// normally this test would be like the others where we render and then simulate a click on an
// item to ensure that a disabled item cannot be clicked, however this is only a problem in IE11
// so we have to get into the implementation details a little bit (unless we want to run these tests
// in IE11... no thank you 🙅)
test(`getItemProps doesn't include event handlers when disabled is passed (for IE11 support)`, () => {
const {getItemProps} = setupWithDownshiftController()
const props = getItemProps({item: 'dog', disabled: true})
const entry = Object.entries(props).find(
// eslint-disable-next-line jest/no-conditional-in-test
([key, value]) => key !== 'onMouseDown' && typeof value === 'function',
)
// eslint-disable-next-line jest/no-conditional-in-test
if (entry) {
throw new Error(
`getItemProps should not have any props that are callbacks. It has ${entry[0]}.`,
)
}
})
test(`disabled item can't be selected by pressing enter`, () => {
const items = [
{item: 'Chess', disabled: true},
{item: 'Dominion', disabled: true},
{item: 'Checkers', disabled: true},
]
const utils = renderDownshift({items})
const {input, arrowDownInput, enterOnInput, changeInputValue} = utils
const firstItem = screen.queryByTestId('item-0')
// eslint-disable-next-line jest-dom/prefer-enabled-disabled
expect(firstItem).toHaveAttribute('disabled')
changeInputValue('c')
// ↓
arrowDownInput()
// ENTER to select the first one
enterOnInput()
// item was not selected -> input value should still be 'c'
expect(input).toHaveValue('c')
})
test(`disabled item can't be highlighted when navigating via keyDown`, () => {
const items = [
{item: 'Chess'},
{item: 'Dominion', disabled: true},
{item: 'Checkers'},
{item: 'Backgammon'},
]
const utils = renderDownshift({items, props: {initialHighlightedIndex: 0}})
const {input, arrowDownInput, enterOnInput} = utils
// ↓
arrowDownInput()
// ↓ (should skip the first and second option)
// ENTER to select
enterOnInput()
expect(input).toHaveValue('Checkers')
})
test(`disabled item can't be highlighted and may wrap when navigating via keyDown`, () => {
const items = [
{item: 'Chess'},
{item: 'Dominion'},
{item: 'Checkers', disabled: true},
{item: 'Backgammon', disabled: true},
]
const utils = renderDownshift({items, props: {initialHighlightedIndex: 1}})
const {input, arrowDownInput, enterOnInput} = utils
// ↓
arrowDownInput()
// ↓ (should skip the first and second option)
// ENTER to select
enterOnInput()
expect(input).toHaveValue('Chess')
})
test(`disabled item can't be highlighted when navigating via keyUp`, () => {
const items = [
{item: 'Chess'},
{item: 'Dominion', disabled: true},
{item: 'Checkers'},
{item: 'Backgammon'},
]
const utils = renderDownshift({items, props: {initialHighlightedIndex: 2}})
const {input, arrowUpInput, enterOnInput} = utils
// ↑
arrowUpInput()
// ENTER to select
enterOnInput()
expect(input).toHaveValue('Chess')
})
test(`disabled item can't be highlighted and it may wrap when navigating via keyUp`, () => {
const items = [
{item: 'Chess', disabled: true},
{item: 'Dominion', disabled: true},
{item: 'Checkers'},
{item: 'Backgammon'},
]
const utils = renderDownshift({items, props: {initialHighlightedIndex: 2}})
const {input, arrowUpInput, enterOnInput} = utils
// ↑
arrowUpInput()
// ENTER to select
enterOnInput()
expect(input).toHaveValue('Backgammon')
})
test(`disabled item can't be highlighted when navigating via end`, () => {
const items = [
{item: 'Backgammon'},
{item: 'Chess'},
{item: 'Dominion', disabled: true},
{item: 'Checkers', disabled: true},
]
const utils = renderDownshift({items})
const {input, endOnInput, enterOnInput} = utils
// end
endOnInput()
// ENTER to select
enterOnInput()
expect(input).toHaveValue('Chess')
})
test(`disabled item can't be highlighted when navigating via home`, () => {
const items = [
{item: 'Chess', disabled: true},
{item: 'Dominion', disabled: true},
{item: 'Checkers'},
{item: 'Backgammon'},
]
const utils = renderDownshift({items})
const {input, homeOnInput, enterOnInput} = utils
// home
homeOnInput()
// ENTER to select
enterOnInput()
expect(input).toHaveValue('Checkers')
})
test(`highlight wrapping works with disabled items upwards`, () => {
const items = [
{item: 'Chess', disabled: true},
{item: 'Dominion'},
{item: 'Checkers'},
]
const utils = renderDownshift({items, props: {initialHighlightedIndex: 1}})
const {input, arrowUpInput, enterOnInput} = utils
// ↑
arrowUpInput()
// ENTER to select
enterOnInput()
expect(input).toHaveValue('Checkers')
})
test(`highlight wrapping works with disabled items downwards`, () => {
const items = [
{item: 'Chess'},
{item: 'Dominion'},
{item: 'Checkers', disabled: true},
]
const utils = renderDownshift({items, props: {initialHighlightedIndex: 1}})
const {input, arrowDownInput, enterOnInput} = utils
// ↓
arrowDownInput()
// ENTER to select
enterOnInput()
expect(input).toHaveValue('Chess')
})
test('cannot check if node is disabled without environment', () => {
const items = [
{item: 'Chess', disabled: true},
]
const utils = renderDownshift({items, props: {initialHighlightedIndex: 1, environment: null}})
const {input, arrowDownInput, enterOnInput} = utils
// ↓
arrowDownInput()
// ENTER to select
enterOnInput()
expect(input).toHaveValue('Chess')
})
function renderDownshift({
items = [{item: 'Chess'}, {item: 'Dominion'}, {item: 'Checkers'}],
props,
} = {}) {
const childrenSpy = jest.fn(({getItemProps, getInputProps}) => (
{items.map((item, index) => (
{item.item}
))}
))
const utils = render(
{}}
children={childrenSpy}
{...props}
/>,
)
const input = screen.queryByTestId('input')
return {
...utils,
childrenSpy,
input,
homeOnInput: extraEventProps =>
fireEvent.keyDown(input, {key: 'Home', ...extraEventProps}),
endOnInput: extraEventProps =>
fireEvent.keyDown(input, {key: 'End', ...extraEventProps}),
arrowDownInput: extraEventProps =>
fireEvent.keyDown(input, {key: 'ArrowDown', ...extraEventProps}),
arrowUpInput: extraEventProps =>
fireEvent.keyDown(input, {key: 'ArrowUp', ...extraEventProps}),
enterOnInput: extraEventProps =>
fireEvent.keyDown(input, {key: 'Enter', ...extraEventProps}),
changeInputValue: (value, extraEventProps) => {
fireEvent.change(input, {target: {value}, ...extraEventProps})
},
}
}
function setupWithDownshiftController() {
let renderArg
render(
{controllerArg => {
renderArg = controllerArg
return null
}}
,
)
return renderArg
}
================================================
FILE: src/__tests__/downshift.get-label-props.js
================================================
import * as React from 'react'
import {render, screen} from '@testing-library/react'
import Downshift from '../'
beforeEach(() => jest.spyOn(console, 'error').mockImplementation(() => {}))
afterEach(() => console.error.mockRestore())
test('label "for" attribute is set to the input "id" attribute', () => {
const {label, input} = renderDownshift()
expect(label).toHaveAttribute('for', input.getAttribute('id'))
})
test('when the inputId prop is set, the label for is set to it', () => {
const id = 'foo'
const {label, input} = renderDownshift({
props: {inputId: id},
})
expect(label).toHaveAttribute('for', input.getAttribute('id'))
expect(label).toHaveAttribute('for', id)
})
function renderDownshift({props} = {}) {
const utils = render( )
return {
...utils,
input: screen.queryByTestId('input'),
label: screen.queryByTestId('label'),
}
}
function BasicDownshift({
inputProps,
labelProps,
getLabelPropsFirst = false,
...rest
}) {
return (
{({getInputProps, getLabelProps}) => {
if (getLabelPropsFirst) {
labelProps = getLabelProps(labelProps)
inputProps = getInputProps(inputProps)
} else {
inputProps = getInputProps(inputProps)
labelProps = getLabelProps(labelProps)
}
return (
)
}}
)
}
================================================
FILE: src/__tests__/downshift.get-menu-props.js
================================================
import * as React from 'react'
import {render} from '@testing-library/react'
import Downshift from '../'
beforeEach(() => jest.spyOn(console, 'error').mockImplementation(() => {}))
afterEach(() => console.error.mockRestore())
const Menu = ({innerRef, ...rest}) =>
const RefMenu = React.forwardRef((props, ref) =>
)
test('using a composite component and calling getMenuProps without a refKey results in an error', () => {
const MyComponent = () => (
(
)}
/>
)
render( )
expect(console.error.mock.calls[1][0]).toMatchSnapshot()
})
test('not applying the ref prop results in an error', () => {
const MyComponent = () => (
{
getMenuProps()
return (
)
}}
/>
)
render( )
expect(console.error.mock.calls[0][0]).toMatchSnapshot()
})
test('renders fine when rendering a composite component and applying getMenuProps properly', () => {
const MyComponent = () => (
(
)}
/>
)
render( )
expect(console.error.mock.calls).toHaveLength(0)
})
test('using a composite component and calling getMenuProps without a refKey does not result in an error if suppressRefError is true', () => {
const MyComponent = () => (
(
)}
/>
)
render( )
expect(console.error.mock.calls).toHaveLength(0)
})
test('returning a DOM element and calling getMenuProps with a refKey does not result in an error if suppressRefError is true', () => {
const MyComponent = () => (
(
)}
/>
)
render( )
expect(console.error.mock.calls).toHaveLength(1)
})
test('not applying the ref prop results in an error does not result in an error if suppressRefError is true', () => {
const MyComponent = () => (
{
getMenuProps({}, {suppressRefError: true})
return (
)
}}
/>
)
render( )
expect(console.error.mock.calls).toHaveLength(0)
})
test('renders fine when rendering a composite component and applying getMenuProps properly even if suppressRefError is true', () => {
const MyComponent = () => (
(
)}
/>
)
render( )
expect(console.error.mock.calls).toHaveLength(0)
})
test('has access to element when a ref function is passed to getMenuProps', () => {
const ref = {current: null}
const MyComponent = () => {
return (
(
{
ref.current = e
},
})}
/>
)}
/>
)
}
render( )
expect(ref.current).not.toBeNull()
expect(ref.current).toBeInstanceOf(HTMLDivElement)
})
================================================
FILE: src/__tests__/downshift.get-root-props.js
================================================
import * as React from 'react'
import {render} from '@testing-library/react'
import Downshift from '../'
const MyDiv = ({innerRef, ...rest}) =>
const MyDivWithForwardedRef = React.forwardRef((props, ref) => (
))
const oldError = console.error
beforeEach(() => {
console.error = jest.fn()
})
afterEach(() => {
console.error = oldError
})
test('no children provided renders nothing', () => {
const MyComponent = () =>
expect(render( ).container).toBeEmptyDOMElement()
})
test('returning null renders nothing', () => {
const MyComponent = () => null} />
expect(render( ).container).toBeEmptyDOMElement()
})
test('returning a composite component without calling getRootProps results in an error', () => {
const MyComponent = () => } />
expect(() => render( )).toThrowErrorMatchingSnapshot()
})
test('returning a composite component and calling getRootProps without a refKey results in an error', () => {
const MyComponent = () => (
} />
)
render( )
expect(console.error.mock.calls[0][0]).toMatchSnapshot()
})
test('returning a DOM element and calling getRootProps with a refKey results in an error', () => {
const MyComponent = () => (
}
/>
)
render( )
expect(console.error.mock.calls[0][0]).toMatchSnapshot()
})
test('not applying the ref prop results in an error', () => {
const MyComponent = () => (
{
const {onClick} = getRootProps()
return
}}
/>
)
render( )
expect(console.error.mock.calls[0][0]).toMatchSnapshot()
})
test('renders fine when rendering a composite component and applying getRootProps properly', () => {
const MyComponent = () => (
(
)}
/>
)
render( )
expect(console.error.mock.calls).toHaveLength(0)
})
test('returning a composite component and calling getRootProps without a refKey does not result in an error if suppressRefError is true', () => {
const MyComponent = () => (
(
)}
/>
)
render( )
expect(console.error.mock.calls).toHaveLength(0)
})
test('returning a DOM element and calling getRootProps with a refKey does not result in an error if suppressRefError is true', () => {
const MyComponent = () => (
(
)}
/>
)
render( )
expect(console.error.mock.calls).toHaveLength(0)
})
test('not applying the ref prop results in an error does not result in an error if suppressRefError is true', () => {
const MyComponent = () => (
{
const {onClick} = getRootProps({}, {suppressRefError: true})
return
}}
/>
)
render( )
expect(console.error.mock.calls).toHaveLength(0)
})
test('renders fine when rendering a composite component and applying getRootProps properly even if suppressRefError is true', () => {
const MyComponent = () => (
(
)}
/>
)
render( )
expect(console.error.mock.calls).toHaveLength(0)
})
test('renders fine when rendering a composite component and suppressRefError prop is true', () => {
const MyComponent = () => (
}
/>
)
render( )
expect(console.error.mock.calls).toHaveLength(0)
})
test('renders fine when rendering a composite component that uses refs forwarding', () => {
const MyComponent = () => (
(
)}
/>
)
render( )
expect(console.error.mock.calls).toHaveLength(0)
})
test('has access to element when a ref is passed to getRootProps', () => {
const ref = {current: null}
const MyComponent = () => (
(
{
ref.current = e
},
})}
/>
)}
/>
)
render( )
expect(ref.current).not.toBeNull()
expect(ref.current).toBeInstanceOf(HTMLDivElement)
})
================================================
FILE: src/__tests__/downshift.lifecycle.js
================================================
import * as React from 'react'
import {act, fireEvent, render, screen} from '@testing-library/react'
import Downshift from '../'
import {setStatus, scrollIntoView} from '../utils-ts'
jest.useFakeTimers()
jest.mock('../utils-ts/scrollIntoView.ts', () => ({
scrollIntoView: jest.fn(),
}))
jest.mock('../utils-ts/setA11yStatus.ts', () => ({
setStatus: jest.fn(),
}))
afterEach(() => {
scrollIntoView.mockReset()
})
test('do not set state after unmount', () => {
const handleStateChange = jest.fn()
const childrenSpy = jest.fn(({getInputProps}) => (
Toggle
))
const MyComponent = () => (
{childrenSpy}
)
const {container, unmount} = render( )
const button = screen.queryByTestId('button')
document.body.appendChild(container)
// blur toggle button
fireEvent.blur(button)
handleStateChange.mockClear()
// unmount
unmount()
expect(handleStateChange).toHaveBeenCalledTimes(0)
})
test('handles mouse events properly to reset state', () => {
const handleStateChange = jest.fn()
const childrenSpy = jest.fn(({getInputProps}) => (
))
const MyComponent = () => (
{childrenSpy}
)
const {container, unmount} = render( )
const input = screen.queryByTestId('input')
document.body.appendChild(container)
// open the menu
fireEvent.keyDown(input, {key: 'ArrowDown'})
handleStateChange.mockClear()
// mouse down and up on within the autocomplete node
mouseDownAndUp(input)
expect(handleStateChange).toHaveBeenCalledTimes(0)
// mouse down and up on outside the autocomplete node
mouseDownAndUp(document.body)
expect(handleStateChange).toHaveBeenCalledTimes(1)
childrenSpy.mockClear()
// does not call our state change handler when no state changes
mouseDownAndUp(document.body)
expect(handleStateChange).toHaveBeenCalledTimes(1)
// does not rerender when no state changes
expect(childrenSpy).not.toHaveBeenCalled()
// cleans up
unmount()
mouseDownAndUp(document.body)
expect(handleStateChange).toHaveBeenCalledTimes(1)
})
test('handles state change for touchevent events', () => {
const handleStateChange = jest.fn()
const childrenSpy = jest.fn(({getToggleButtonProps}) => (
))
const MyComponent = () => (
{childrenSpy}
)
const {container, unmount} = render( )
document.body.appendChild(container)
const button = screen.queryByTestId('button')
// touch outside for coverage
fireEvent.touchStart(document.body)
fireEvent.touchEnd(document.body)
// open menu
fireEvent.click(button)
jest.runAllTimers()
expect(handleStateChange).toHaveBeenCalledTimes(1)
// touchmove (scroll) outside downshift should not trigger state change
fireEvent.touchStart(document.body)
fireEvent.touchMove(document.body)
fireEvent.touchEnd(document.body)
jest.runAllTimers()
expect(handleStateChange).toHaveBeenCalledTimes(1)
// touch outside downshift
fireEvent.touchStart(document.body)
fireEvent.touchEnd(document.body)
jest.runAllTimers()
expect(handleStateChange).toHaveBeenCalledTimes(2)
unmount()
})
test('props update causes the a11y status to be updated', () => {
setStatus.mockReset()
const MyComponent = () => (
{({getInputProps, getItemProps, isOpen}) => (
{/* eslint-disable-next-line jest/no-conditional-in-test */}
{isOpen ?
: null}
)}
)
const {container, unmount} = render( )
render( , {container})
jest.runAllTimers()
expect(setStatus).toHaveBeenCalledTimes(1)
render( , {container})
unmount()
jest.runAllTimers()
expect(setStatus).toHaveBeenCalledTimes(1)
})
test('inputValue initializes properly if the selectedItem is controlled and set', () => {
const childrenSpy = jest.fn(() => null)
render({childrenSpy} )
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
inputValue: 'foo',
}),
)
})
test('inputValue initializes properly if selectedItem is set to 0', () => {
const childrenSpy = jest.fn(() => null)
render({childrenSpy} )
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
inputValue: '0',
}),
)
})
test('props update of selectedItem will update the inputValue state', () => {
const childrenSpy = jest.fn(() => null)
const {container} = render(
{childrenSpy} ,
)
childrenSpy.mockClear()
render({childrenSpy} , {container})
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
inputValue: 'foo',
}),
)
})
test('the callback is invoked on selected item only if it is a function', () => {
let renderArg
const childrenSpy = jest.fn(controllerArg => {
renderArg = controllerArg
return
})
const callbackSpy = jest.fn(x => x)
render({childrenSpy} )
childrenSpy.mockClear()
callbackSpy.mockClear()
act(() => {
renderArg.selectItem('foo', {}, callbackSpy)
})
expect(callbackSpy).toHaveBeenCalledTimes(1)
act(() => {
renderArg.selectItem('foo', {})
})
})
test('props update of selectedItem will not update inputValue state', () => {
const onInputValueChangeSpy = jest.fn(() => null)
const initialProps = {
onInputValueChange: onInputValueChangeSpy,
selectedItemChanged: (prevItem, item) => prevItem.id !== item.id,
selectedItem: {id: '123', value: 'wow'},
// eslint-disable-next-line jest/no-conditional-in-test
itemToString: i => (i ? i.value : ''),
render: () => null,
}
const {container} = render( )
onInputValueChangeSpy.mockClear()
render(
,
{container},
)
expect(onInputValueChangeSpy).not.toHaveBeenCalled()
})
test('controlled highlighted index change scrolls the item into view', () => {
// sadly, testing scroll is really difficult in a jsdom environment.
// Perhaps eventually we'll add real integration tests with cypress
// or something, but for now we'll just mock the implementation of
// utils.scrollIntoView and ensure it's called with the proper arguments
// assuming that the test suite for utils.scrollIntoView will ensure
// this functionality doesn't break.
const oneHundredItems = Array.from({length: 100})
const renderFn = jest.fn(({getItemProps, getMenuProps}) => (
{oneHundredItems.map((x, i) => (
))}
))
const {container, updateProps} = setup({
highlightedIndex: 1,
render: renderFn,
})
document.body.appendChild(container)
renderFn.mockClear()
updateProps({highlightedIndex: 75})
expect(renderFn).toHaveBeenCalledTimes(1)
expect(scrollIntoView).toHaveBeenCalledTimes(1)
const menuDiv = screen.queryByTestId('menu')
expect(scrollIntoView).toHaveBeenCalledWith(
screen.queryByTestId('item-75'),
menuDiv,
)
})
function mouseDownAndUp(node) {
fireEvent.mouseDown(node)
fireEvent.mouseUp(node)
}
function setup({render: renderFn = () =>
, ...props} = {}) {
// eslint-disable-next-line prefer-const
let container, renderArg
const childrenSpy = jest.fn(controllerArg => {
renderArg = controllerArg
return renderFn(controllerArg)
})
const updateProps = newProps => {
return render(
,
{
container,
},
)
}
const renderUtils = updateProps()
container = renderUtils.container
return {
childrenSpy,
updateProps,
...renderUtils,
...renderArg,
}
}
================================================
FILE: src/__tests__/downshift.misc-with-utils-mocked.js
================================================
// this is stuff that I couldn't think fit anywhere else
// but we still want to have tested.
import * as React from 'react'
import {render, fireEvent, screen} from '@testing-library/react'
import Downshift from '../'
import {scrollIntoView} from '../utils-ts'
jest.useFakeTimers()
jest.mock('../utils-ts/scrollIntoView.ts', () => ({
scrollIntoView: jest.fn(),
}))
test('does not scroll from an onMouseMove event', () => {
class HighlightedIndexController extends React.Component {
state = {highlightedIndex: 10}
handleStateChange = changes => {
// eslint-disable-next-line jest/no-conditional-in-test
if (changes.hasOwnProperty('highlightedIndex')) {
this.setState({highlightedIndex: changes.highlightedIndex})
}
}
render() {
return (
{({getInputProps, getItemProps}) => (
)}
)
}
}
render( )
const input = screen.queryByTestId('input')
const item = screen.queryByTestId('item-2')
fireEvent.mouseMove(item)
jest.runAllTimers()
expect(scrollIntoView).not.toHaveBeenCalled()
// now let's make sure that we can still scroll items into view
// ↓
fireEvent.keyDown(input, {key: 'ArrowDown'})
expect(scrollIntoView).toHaveBeenCalledWith(item, undefined)
})
================================================
FILE: src/__tests__/downshift.misc.js
================================================
// this is stuff that I couldn't think fit anywhere else
// but we still want to have tested.
import * as React from 'react'
import * as ReactDOM from 'react-dom/client'
import {act, render} from '@testing-library/react'
import Downshift from '../'
beforeEach(() => jest.spyOn(console, 'error').mockImplementation(() => {}))
afterEach(() => console.error.mockRestore())
test('closeMenu closes the menu', () => {
const {openMenu, closeMenu, childrenSpy} = setup()
openMenu()
closeMenu()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({isOpen: false}),
)
})
test('clearSelection clears an existing selection', () => {
const {openMenu, selectItem, childrenSpy, clearSelection} = setup()
openMenu()
selectItem('foo')
clearSelection()
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({selectedItem: null}),
)
})
test('selectItemAtIndex does nothing if there is no item at that index', () => {
const {openMenu, selectItemAtIndex, childrenSpy} = setup()
openMenu()
childrenSpy.mockClear()
selectItemAtIndex(100)
expect(childrenSpy).not.toHaveBeenCalled()
})
test('selectItemAtIndex can select item that is an empty string', () => {
const items = ['Chess', '']
const children = ({getItemProps}) => (
{items.map((item, index) => (
{item}
))}
)
const {selectItemAtIndex, childrenSpy} = setup({children})
act(() => {
selectItemAtIndex(1)
})
expect(childrenSpy).toHaveBeenLastCalledWith(
expect.objectContaining({selectedItem: ''}),
)
})
test('toggleMenu can take no arguments at all', () => {
const {toggleMenu, childrenSpy} = setup()
act(() => {
toggleMenu()
})
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
isOpen: true,
}),
)
})
test('clearItems clears all items', () => {
const item = 'Chess'
const children = ({getItemProps}) => (
)
// Wrap Downshift to expose its instance methods through a ref
const DownshiftWrapper = React.forwardRef((_props, ref) => {
const innerRef = React.useRef(null)
React.useImperativeHandle(ref, () => innerRef.current)
return {children}
})
const container = document.createElement('div')
const root = ReactDOM.createRoot(container)
const dsRef = React.createRef()
// eslint-disable-next-line testing-library/no-unnecessary-act
act(() => {
root.render( )
})
const downshiftInstance = dsRef.current
expect(downshiftInstance.items).toEqual([item])
act(() => {
downshiftInstance.clearItems()
})
expect(downshiftInstance.items).toEqual([])
root.unmount()
})
test('reset can take no arguments at all', () => {
const {reset, childrenSpy} = setup()
reset()
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
isOpen: false,
}),
)
})
test('setHighlightedIndex can take no arguments at all', () => {
const defaultHighlightedIndex = 2
const {setHighlightedIndex, childrenSpy} = setup({
defaultHighlightedIndex,
})
setHighlightedIndex()
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
highlightedIndex: defaultHighlightedIndex,
}),
)
})
test('can specify a custom ID which is used in item IDs (good for SSR)', () => {
const id = 'my-custom-id'
const {getItemProps} = setup({id})
expect(getItemProps({item: 'blah'}).id).toContain(id)
})
test('can use children instead of render prop', () => {
const childrenSpy = jest.fn()
render({childrenSpy} )
expect(childrenSpy).toHaveBeenCalledTimes(1)
})
test('should not log error during strict mode during reset', () => {
const children = () =>
render(
{children}
,
)
expect(console.error.mock.calls).toHaveLength(0)
})
test('can use setState for ultimate power', () => {
const {childrenSpy, setState} = setup()
childrenSpy.mockClear()
act(() => {
setState({isOpen: true, selectedItem: 'hi'})
})
expect(childrenSpy).toHaveBeenCalledTimes(1)
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({isOpen: true, selectedItem: 'hi'}),
)
})
test('warns when controlled component becomes uncontrolled', () => {
const children = () =>
const {rerender} = render({children} )
rerender({children} )
expect(console.error.mock.calls).toHaveLength(1)
expect(console.error.mock.calls).toMatchSnapshot()
})
test('warns when uncontrolled component becomes controlled', () => {
const children = () =>
const {rerender} = render({children} )
rerender({children} )
expect(console.error.mock.calls).toHaveLength(1)
expect(console.error.mock.calls).toMatchSnapshot()
})
describe('expect console.warn to fire—depending on process.env.NODE_ENV value', () => {
const originalEnv = process.env.NODE_ENV
beforeEach(() => {
jest.spyOn(console, 'warn').mockImplementation(() => {})
})
afterEach(() => {
process.env.NODE_ENV = originalEnv
console.warn.mockRestore()
})
test(`it shouldn't log anything when value === 'production'`, () => {
process.env.NODE_ENV = 'production'
setup({selectedItem: {label: 'test', value: 'any'}})
expect(console.warn).toHaveBeenCalledTimes(0)
})
test('it should warn exactly one time when value !== production', () => {
process.env.NODE_ENV = 'development'
setup({selectedItem: {label: 'test', value: 'any'}})
expect(console.warn).toHaveBeenCalledTimes(1)
expect(console.warn.mock.calls[0]).toMatchSnapshot()
})
})
test('initializes with the initial* props', () => {
const initialState = {
initialIsOpen: true,
initialHighlightedIndex: 2,
initialInputValue: 'hey',
initialSelectedItem: 'sup',
}
const {childrenSpy} = setup(initialState)
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
isOpen: initialState.initialIsOpen,
highlightedIndex: initialState.initialHighlightedIndex,
inputValue: initialState.initialInputValue,
selectedItem: initialState.initialSelectedItem,
}),
)
})
function setup({children = () =>
, ...props} = {}) {
let renderArg
const childrenSpy = jest.fn(controllerArg => {
renderArg = controllerArg
return children(controllerArg)
})
const renderUtils = render({childrenSpy} )
return {childrenSpy, ...renderUtils, ...renderArg}
}
================================================
FILE: src/__tests__/downshift.props.js
================================================
// this is stuff that I couldn't think fit anywhere else
// but we still want to have tested.
import * as React from 'react'
import {render, fireEvent, act} from '@testing-library/react'
import Downshift from '../'
test('onStateChange called with changes and downshift state and helpers', () => {
const handleStateChange = jest.fn()
const controlledState = {
inputValue: '',
selectedItem: null,
}
const {selectItem} = setup({
...controlledState,
onStateChange: handleStateChange,
})
const itemToSelect = 'foo'
act(() => {
selectItem(itemToSelect)
})
const changes = {
type: Downshift.stateChangeTypes.unknown,
inputValue: itemToSelect,
selectedItem: itemToSelect,
}
const stateAndHelpers = {
...controlledState,
isOpen: false,
highlightedIndex: null,
selectItem,
}
expect(handleStateChange).toHaveBeenLastCalledWith(
changes,
expect.objectContaining(stateAndHelpers),
)
})
test('onChange called when clearSelection is triggered', () => {
const handleChange = jest.fn()
const {clearSelection} = setup({
selectedItem: 'foo',
onChange: handleChange,
})
act(() => {
clearSelection()
})
expect(handleChange).toHaveBeenCalledTimes(1)
expect(handleChange).toHaveBeenCalledWith(null, expect.any(Object))
})
test('onChange only called when the selection changes', () => {
const handleChange = jest.fn()
const {selectItem} = setup({
onChange: handleChange,
})
act(() => {
selectItem('foo')
})
expect(handleChange).toHaveBeenCalledTimes(1)
expect(handleChange).toHaveBeenCalledWith('foo', expect.any(Object))
handleChange.mockClear()
act(() => {
selectItem('foo')
})
expect(handleChange).toHaveBeenCalledTimes(0)
})
test('onSelect called whenever selection happens, even if the item is the same', () => {
const handleSelect = jest.fn()
const {selectItem} = setup({
onSelect: handleSelect,
})
act(() => {
selectItem('foo')
})
expect(handleSelect).toHaveBeenCalledTimes(1)
expect(handleSelect).toHaveBeenCalledWith('foo', expect.any(Object))
handleSelect.mockClear()
act(() => {
selectItem('foo')
})
expect(handleSelect).toHaveBeenCalledTimes(1)
})
test('onSelect not called when nothing was selected', () => {
const handleSelect = jest.fn()
const {openMenu} = setup({
onSelect: handleSelect,
})
act(() => {
openMenu()
})
expect(handleSelect).not.toHaveBeenCalled()
})
test('uses given environment', () => {
const environment = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
Node,
document: {
getElementById: jest.fn(() => document.createElement('div')),
createElement: jest.fn(),
body: {},
activeElement: {},
},
}
const {unmount, setHighlightedIndex} = setup({environment})
act(() => {
setHighlightedIndex()
})
unmount()
expect(environment.addEventListener).toHaveBeenCalledTimes(5)
expect(environment.removeEventListener).toHaveBeenCalledTimes(5)
})
test('can override onOuterClick callback to maintain isOpen state', () => {
const renderFn = () =>
const onOuterClick = jest.fn()
const {openMenu} = setup({render: renderFn, onOuterClick})
act(() => {
openMenu()
})
mouseDownAndUp(document.body)
expect(onOuterClick).toHaveBeenCalledTimes(1)
expect(onOuterClick).toHaveBeenCalledWith(
expect.objectContaining({
// just verify that it's the controller object
isOpen: false,
getItemProps: expect.any(Function),
}),
)
})
test('onInputValueChange called when changes contain inputValue', () => {
const handleInputValueChange = jest.fn()
const {selectItem} = setup({
onInputValueChange: handleInputValueChange,
})
act(() => {
selectItem('foo')
})
expect(handleInputValueChange).toHaveBeenCalledTimes(1)
expect(handleInputValueChange).toHaveBeenCalledWith('foo', expect.any(Object))
})
test('onInputValueChange not called when changes do not contain inputValue', () => {
const handleInputValueChange = jest.fn()
const {openMenu} = setup({
onInputValueChange: handleInputValueChange,
})
act(() => {
openMenu()
})
expect(handleInputValueChange).toHaveBeenCalledTimes(0)
})
test('onInputValueChange called with empty string on reset', () => {
const handleInputValueChange = jest.fn()
const {reset} = setup({
onInputValueChange: handleInputValueChange,
})
act(() => {
reset()
})
expect(handleInputValueChange).toHaveBeenCalledTimes(1)
expect(handleInputValueChange).toHaveBeenCalledWith('', expect.any(Object))
})
test('defaultHighlightedIndex will be used for the highlighted index on reset', () => {
const {reset, childrenSpy} = setup({defaultHighlightedIndex: 0})
childrenSpy.mockClear()
act(() => {
reset()
})
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
highlightedIndex: 0,
}),
)
})
test('stateReducer customizes the final state after keyDownEnter handled', () => {
const {childrenSpy, openMenu, selectHighlightedItem} = setup({
defaultHighlightedIndex: 0,
stateReducer: (state, stateToSet) => {
// eslint-disable-next-line jest/no-conditional-in-test
switch (stateToSet.type) {
case Downshift.stateChangeTypes.keyDownEnter:
return {
...stateToSet,
isOpen: state.isOpen,
highlightedIndex: state.highlightedIndex,
}
default:
return stateToSet
}
},
})
childrenSpy.mockClear()
act(() => {
openMenu()
})
selectHighlightedItem({
type: Downshift.stateChangeTypes.keyDownEnter,
})
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
isOpen: true,
highlightedIndex: 0,
}),
)
})
function mouseDownAndUp(node) {
fireEvent.mouseDown(node)
fireEvent.mouseUp(node)
}
function setup({render: renderFn = () =>
, ...props} = {}) {
let renderArg
const childrenSpy = jest.fn(controllerArg => {
renderArg = controllerArg
return renderFn(controllerArg)
})
const utils = render({childrenSpy} )
return {childrenSpy, ...utils, ...renderArg}
}
================================================
FILE: src/__tests__/portal-support.js
================================================
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import {render, fireEvent, screen, act} from '@testing-library/react'
import Downshift from '../'
test('will not reset when clicking within the menu', () => {
class MyMenu extends React.Component {
el = document.createElement('div')
componentDidMount() {
document.body.appendChild(this.el)
}
componentWillUnmount() {
document.body.removeChild(this.el)
}
render() {
return ReactDOM.createPortal(
I am not an item
The item
,
this.el,
)
}
}
function MyPortalAutocomplete() {
return (
{({
getMenuProps,
getItemProps,
getToggleButtonProps,
isOpen,
selectedItem,
}) => (
{/* eslint-disable-next-line jest/no-conditional-in-test */}
{selectedItem ? (
{selectedItem}
) : null}
Open Menu
{/* eslint-disable-next-line jest/no-conditional-in-test */}
{isOpen ?
: null}
)}
)
}
render( )
expect(screen.queryByTestId('menu')).not.toBeInTheDocument()
act(() => {
screen.getByTestId('button').click()
})
expect(screen.getByTestId('menu')).toBeInstanceOf(HTMLElement)
const notAnItem = screen.getByTestId('not-an-item')
// Mouse events
fireEvent.mouseDown(notAnItem)
notAnItem.focus() // sets document.activeElement
fireEvent.mouseUp(notAnItem)
expect(screen.getByTestId('menu')).toBeInstanceOf(HTMLElement)
// Touch events
fireEvent.touchStart(notAnItem)
fireEvent.touchEnd(notAnItem)
notAnItem.focus() // sets document.activeElement
expect(screen.getByTestId('menu')).toBeInstanceOf(HTMLElement)
act(() => {
screen.getByTestId('item').click()
})
expect(screen.queryByTestId('menu')).not.toBeInTheDocument()
expect(screen.getByTestId('selection')).toHaveTextContent('The item')
})
================================================
FILE: src/__tests__/set-a11y-status.js
================================================
jest.useFakeTimers()
beforeEach(() => {
document.body.innerHTML = ''
})
test('sets the status', () => {
const setA11yStatus = setup()
setA11yStatus('hello', document)
expect(document.body.firstChild).toMatchSnapshot()
})
test('replaces the status with a different one', () => {
const setA11yStatus = setup()
setA11yStatus('hello', document)
setA11yStatus('goodbye', document)
expect(document.body.firstChild).toMatchSnapshot()
})
test('does add anything for an empty string', () => {
const setA11yStatus = setup()
setA11yStatus('', document)
expect(document.body).toBeEmptyDOMElement()
})
test('escapes HTML', () => {
const setA11yStatus = setup()
setA11yStatus('', document)
expect(document.body.firstChild).toMatchSnapshot()
})
test('performs cleanup after a timeout', () => {
const setA11yStatus = setup()
setA11yStatus('hello', document)
jest.runAllTimers()
expect(document.body.firstChild).toMatchSnapshot()
})
test('creates new status div if there is none', () => {
const statusDiv = {setAttribute: jest.fn(), style: {}}
const document = {
getElementById: jest.fn(() => null),
createElement: jest.fn().mockReturnValue(statusDiv),
body: {
appendChild: jest.fn(),
},
}
const setA11yStatus = setup()
setA11yStatus('hello', document)
expect(document.getElementById).toHaveBeenCalledTimes(1)
expect(document.getElementById).toHaveBeenCalledWith('a11y-status-message')
expect(document.createElement).toHaveBeenCalledTimes(1)
expect(document.createElement).toHaveBeenCalledWith('div')
expect(statusDiv.setAttribute).toHaveBeenCalledTimes(4)
expect(statusDiv.setAttribute.mock.calls).toMatchSnapshot()
expect(statusDiv.style).toEqual({
border: '0',
clip: 'rect(0 0 0 0)',
height: '1px',
margin: '-1px',
overflow: 'hidden',
padding: '0',
position: 'absolute',
width: '1px',
})
expect(document.body.appendChild).toHaveBeenCalledTimes(1)
expect(document.body.appendChild).toHaveBeenCalledWith(statusDiv)
// eslint-disable-next-line jest-dom/prefer-to-have-text-content
expect(statusDiv.textContent).toEqual('hello')
})
test('creates no status div if there is no document', () => {
const setA11yStatus = setup()
setA11yStatus('')
expect(document.body).toBeEmptyDOMElement()
})
function setup() {
jest.resetModules()
return require('../utils-ts').setStatus
}
================================================
FILE: src/__tests__/utils.call-all-event-handlers.js
================================================
import {callAllEventHandlers} from '../utils'
test('prevent default handlers when defaultDownshiftPrevented is true', () => {
const customHandler = jest.fn(e => {
e.preventDownshiftDefault = true
})
const defaultHandler = jest.fn()
const composedHandler = callAllEventHandlers(customHandler, defaultHandler)
composedHandler({})
expect(customHandler).toHaveBeenCalledTimes(1)
expect(defaultHandler).toHaveBeenCalledTimes(0)
})
test('call default handler when defaultDownshiftPrevented and defaultPrevented are false', () => {
const customHandler = jest.fn()
const defaultHandler = jest.fn()
const composedHandler = callAllEventHandlers(customHandler, defaultHandler)
composedHandler({})
expect(customHandler).toHaveBeenCalledTimes(1)
expect(defaultHandler).toHaveBeenCalledTimes(1)
})
================================================
FILE: src/__tests__/utils.get-a11y-status-message.js
================================================
import {getA11yStatusMessage} from '../utils'
const tests = [
{
input: {
isOpen: false,
selectedItem: null,
},
output: '',
},
{
input: {
isOpen: true,
resultCount: 0,
},
output: 'No results are available.',
},
{
input: {
isOpen: true,
resultCount: 10,
},
output:
'10 results are available, use up and down arrow keys to navigate. Press Enter key to select.',
},
{
input: {
isOpen: true,
resultCount: 9,
previousResultCount: 12,
},
output:
'9 results are available, use up and down arrow keys to navigate. Press Enter key to select.',
},
{
input: {
isOpen: true,
resultCount: 8,
previousResultCount: 20,
highlightedItem: 'Oranges',
},
output:
'8 results are available, use up and down arrow keys to navigate. Press Enter key to select.',
},
{
input: {
isOpen: true,
resultCount: 1,
},
output:
'1 result is available, use up and down arrow keys to navigate. Press Enter key to select.',
},
{
input: {
isOpen: true,
resultCount: 5,
previousResultCount: 5,
},
output: '',
},
]
tests.forEach(({input, output}) => {
test(`${JSON.stringify(input)} results in ${JSON.stringify(output)}`, () => {
expect(getA11yStatusMessage(input)).toBe(output)
})
})
================================================
FILE: src/__tests__/utils.get-highlighted-index.js
================================================
import {getHighlightedIndex} from '../utils'
test('should return next index', () => {
const offset = 1
const start = 0
const items = {length: 3}
function isItemDisabled() {
return false
}
expect(getHighlightedIndex(start, offset, items, isItemDisabled)).toEqual(1)
})
test('should return previous index', () => {
const offset = -1
const start = 2
const items = {length: 3}
function isItemDisabled() {
return false
}
expect(getHighlightedIndex(start, offset, items, isItemDisabled)).toEqual(1)
})
test('should return previous index based on moveAmount', () => {
const offset = -2
const start = 2
const items = {length: 3}
function isItemDisabled() {
return false
}
expect(getHighlightedIndex(start, offset, items, isItemDisabled)).toEqual(0)
})
test('should wrap to first if circular and reached end', () => {
const offset = 3
const start = 2
const items = {length: 3}
function isItemDisabled() {
return false
}
const circular = true
expect(
getHighlightedIndex(start, offset, items, isItemDisabled, circular),
).toEqual(0)
})
test('should not wrap to first if not circular and reached end', () => {
const offset = 3
const start = 2
const items = {length: 3}
function isItemDisabled() {
return false
}
const circular = false
expect(
getHighlightedIndex(start, offset, items, isItemDisabled, circular),
).toEqual(2)
})
test('should wrap to last if circular and reached start', () => {
const offset = -3
const start = 2
const items = {length: 3}
function isItemDisabled() {
return false
}
const circular = true
expect(
getHighlightedIndex(start, offset, items, isItemDisabled, circular),
).toEqual(2)
})
test('should not wrap to last if not circular and reached start', () => {
const offset = -3
const start = 2
const items = {length: 3}
function isItemDisabled() {
return false
}
const circular = false
expect(
getHighlightedIndex(start, offset, items, isItemDisabled, circular),
).toEqual(0)
})
test('should skip disabled when moving downwards', () => {
const offset = 1
const start = 0
const items = {length: 3}
function isItemDisabled(_item, index) {
return index === 1
}
expect(getHighlightedIndex(start, offset, items, isItemDisabled)).toEqual(2)
})
test('should skip disabled when moving upwards', () => {
const offset = -1
const start = 2
const items = {length: 3}
function isItemDisabled(_item, index) {
return index === 1
}
expect(getHighlightedIndex(start, offset, items, isItemDisabled)).toEqual(0)
})
test('should skip disabled and wrap to last if circular when reaching first', () => {
const offset = -1
const start = 1
const items = {length: 3}
function isItemDisabled(_item, index) {
return index === 0
}
const circular = true
expect(
getHighlightedIndex(start, offset, items, isItemDisabled, circular),
).toEqual(2)
})
test('should skip disabled and wrap to second to last if circular when reaching first and last is disabled', () => {
const offset = -1
const start = 1
const items = {length: 3}
function isItemDisabled(_item, index) {
return [0, 2].includes(index)
}
const circular = true
expect(
getHighlightedIndex(start, offset, items, isItemDisabled, circular),
).toEqual(1)
})
test('should skip disabled and not wrap to last if circular when reaching first', () => {
const offset = -1
const start = 1
const items = {length: 3}
function isItemDisabled(_item, index) {
return index === 0
}
const circular = false
expect(
getHighlightedIndex(start, offset, items, isItemDisabled, circular),
).toEqual(1)
})
test('should skip disabled and wrap to first if circular when reaching last', () => {
const offset = 1
const start = 1
const items = {length: 3}
function isItemDisabled(_item, index) {
return index === 2
}
const circular = true
expect(
getHighlightedIndex(start, offset, items, isItemDisabled, circular),
).toEqual(0)
})
test('should skip disabled and wrap to second if circular when reaching last and first is disabled', () => {
const offset = 1
const start = 1
const items = {length: 3}
function isItemDisabled(_item, index) {
return [0, 2].includes(index)
}
const circular = true
expect(
getHighlightedIndex(start, offset, items, isItemDisabled, circular),
).toEqual(1)
})
test('should skip disabled and not wrap to first if circular when reaching last', () => {
const offset = 1
const start = 1
const items = {length: 3}
function isItemDisabled(_item, index) {
return index === 2
}
const circular = false
expect(
getHighlightedIndex(start, offset, items, isItemDisabled, circular),
).toEqual(1)
})
test('should not select any if all disabled when arrow up', () => {
const offset = -1
const start = -1
const items = {length: 3}
function isItemDisabled() {
return true
}
const circular = true
expect(
getHighlightedIndex(start, offset, items, isItemDisabled, circular),
).toEqual(-1)
})
test('should not select any if all disabled when arrow down', () => {
const offset = 1
const start = -1
const items = {length: 3}
function isItemDisabled() {
return true
}
const circular = true
expect(
getHighlightedIndex(start, offset, items, isItemDisabled, circular),
).toEqual(-1)
})
================================================
FILE: src/__tests__/utils.handle-refs.js
================================================
import {handleRefs} from '../utils'
test('handle object and functinonal refs', () => {
const refValue = 'Here could be your HTMLElement'
const objectRef = {current: null}
let functionRefValue = null
const functionRef = ref => {
functionRefValue = ref
}
const handleRef = handleRefs(functionRef, objectRef)
handleRef(refValue)
expect(objectRef.current).toBe(refValue)
expect(functionRefValue).toBe(refValue)
})
================================================
FILE: src/__tests__/utils.pick-state.js
================================================
import {pickState} from '../utils'
test('pickState only picks state that downshift cares about', () => {
const otherStateToSet = {
isOpen: true,
foo: 0,
}
const result = pickState(otherStateToSet)
const expected = {isOpen: true}
const resultKeys = Object.keys(result)
const expectedKeys = Object.keys(expected)
resultKeys.sort()
expectedKeys.sort()
expect(result).toEqual(expected)
expect(resultKeys).toEqual(expectedKeys)
})
================================================
FILE: src/__tests__/utils.reset-id-counter.js
================================================
import * as React from 'react'
import {render, screen} from '@testing-library/react'
import Downshift from '../'
import {resetIdCounter} from '../utils-ts'
jest.mock('react', () => {
const {useId, ...react} = jest.requireActual('react')
return react
})
test('renders with correct and predictable auto generated id upon resetIdCounter call', () => {
resetIdCounter()
const renderDownshift = ({getInputProps, getLabelProps, getItemProps}) => (
)
const setup1 = setup({renderDownshift})
expect(setup1.id).toBe('downshift-0')
expect(setup1.label).toHaveAttribute('for', 'downshift-0-input')
expect(setup1.input).toHaveAttribute('id', 'downshift-0-input')
expect(setup1.item).toHaveAttribute('id', 'downshift-0-item-0')
setup1.unmount()
const setup2 = setup({renderDownshift})
expect(setup2.id).toBe('downshift-1')
expect(setup2.label).toHaveAttribute('for', 'downshift-1-input')
expect(setup2.input).toHaveAttribute('id', 'downshift-1-input')
expect(setup2.item).toHaveAttribute('id', 'downshift-1-item-0')
setup2.unmount()
resetIdCounter()
const setup3 = setup({renderDownshift})
expect(setup3.id).toBe('downshift-0')
expect(setup3.label).toHaveAttribute('for', 'downshift-0-input')
expect(setup3.input).toHaveAttribute('id', 'downshift-0-input')
expect(setup3.item).toHaveAttribute('id', 'downshift-0-item-0')
setup3.unmount()
})
function setup({renderDownshift = () =>
, ...props} = {}) {
let renderArg
const childrenSpy = jest.fn(controllerArg => {
renderArg = controllerArg
return renderDownshift(controllerArg)
})
const utils = render({childrenSpy} )
const input = screen.queryByTestId('input')
const label = screen.queryByTestId('label')
const item = screen.queryByTestId('item-0')
return {...utils, input, label, item, ...renderArg}
}
================================================
FILE: src/__tests__/utils.reset-id-counter.r18.js
================================================
import * as React from 'react'
import {render, screen} from '@testing-library/react'
import Downshift from '..'
import {resetIdCounter} from '../utils-ts'
afterAll(() => {
jest.restoreAllMocks()
})
test('renders with correct and predictable auto generated id upon resetIdCounter call', () => {
jest.spyOn(console, 'warn').mockImplementation(() => {})
const renderDownshift = ({getInputProps, getLabelProps, getItemProps}) => (
)
setup({renderDownshift})
resetIdCounter()
expect(console.warn).toHaveBeenCalledTimes(1)
expect(console.warn).toHaveBeenCalledWith(
'It is not necessary to call resetIdCounter when using React 18+',
)
})
function setup({renderDownshift = () =>
, ...props} = {}) {
let renderArg
const childrenSpy = jest.fn(controllerArg => {
renderArg = controllerArg
return renderDownshift(controllerArg)
})
const utils = render({childrenSpy} )
const input = screen.queryByTestId('input')
const label = screen.queryByTestId('label')
const item = screen.queryByTestId('item-0')
return {...utils, input, label, item, ...renderArg}
}
================================================
FILE: src/__tests__/utils.scroll-into-view.js
================================================
import {scrollIntoView} from '../utils-ts'
test('does not throw with a null node', () => {
expect(() => scrollIntoView(null)).not.toThrow()
})
test('does not throw if the given node is the root node', () => {
const node = getNode()
expect(() => scrollIntoView(node, node)).not.toThrow()
})
test('does nothing if the node is within the scrollable area', () => {
const scrollableScrollTop = 2
const node = getNode({height: 10, top: 6})
const scrollableNode = getScrollableNode({
scrollTop: scrollableScrollTop,
children: [node],
})
scrollIntoView(node, scrollableNode)
expect(scrollableNode.scrollTop).toBe(scrollableScrollTop)
})
test('does nothing if parent.top is above view area and node within view', () => {
const scrollableScrollTop = 1000
const node = getNode({height: 40, top: 300})
// parent bounds is [-1000 + 1000, -500 + 1000] = [0, 500]
// node bounds is [300, 340] => node within visible area
const scrollableNode = getScrollableNode({
top: -1000,
height: 500,
scrollTop: scrollableScrollTop,
children: [node],
})
scrollIntoView(node, scrollableNode)
expect(scrollableNode.scrollTop).toBe(scrollableScrollTop)
})
test('aligns to top when the node is above the scrollable parent', () => {
// TODO: make this test a tiny bit more readable/maintainable...
const nodeTop = 2
const scrollableScrollTop = 13
const node = getNode({height: 10, top: nodeTop})
const scrollableNode = getScrollableNode({
top: 10,
scrollTop: scrollableScrollTop,
children: [node],
})
scrollIntoView(node, scrollableNode)
expect(scrollableNode.scrollTop).toBe(5)
})
test('aligns to top of scrollable parent when the node is above view area', () => {
const node = getNode({height: 40, top: -50})
const scrollableNode = getScrollableNode({
top: 50,
scrollTop: 100,
children: [node],
})
scrollIntoView(node, scrollableNode)
expect(scrollableNode.scrollTop).toBe(0)
})
test('aligns to bottom when the node is below the scrollable parent', () => {
const nodeTop = 115
const node = getNode({height: 10, top: nodeTop})
const scrollableNode = getScrollableNode({
height: 100,
children: [node],
})
scrollIntoView(node, scrollableNode)
expect(scrollableNode.scrollTop).toBe(25)
})
function getScrollableNode(overrides = {}) {
return getNode({
height: 100,
top: 0,
scrollTop: 0,
scrollHeight: 150,
...overrides,
})
}
function getNode({
top = 0,
height = 0,
scrollTop = 0,
scrollHeight = height,
clientHeight = height,
children = [],
borderBottomWidth = 0,
borderTopWidth = 0,
} = {}) {
const div = document.createElement('div')
div.getBoundingClientRect = () => ({
width: 50,
height,
top,
left: 0,
right: 50,
bottom: top + height,
})
div.style.borderTopWidth = borderTopWidth
div.style.borderBottomWidth = borderBottomWidth
div.scrollTop = scrollTop
Object.defineProperties(div, {
clientHeight: {value: clientHeight},
offsetHeight: {value: clientHeight},
scrollHeight: {value: scrollHeight},
})
children.forEach(child => div.appendChild(child))
document.documentElement.appendChild(div)
return div
}
================================================
FILE: src/downshift.js
================================================
/* eslint camelcase:0 */
import PropTypes from 'prop-types'
import {Component, cloneElement} from 'react'
import {isForwardRef} from 'react-is'
import {isPreact, isReactNative, isReactNativeWeb} from './is.macro'
import * as stateChangeTypes from './stateChangeTypes'
import {
handleRefs,
callAllEventHandlers,
cbToCb,
debounce,
getA11yStatusMessage,
getElementProps,
isDOMElement,
targetWithinDownshift,
isPlainObject,
normalizeArrowKey,
pickState,
requiredProp,
unwrapArray,
isControlledProp,
validateControlledUnchanged,
getHighlightedIndex,
getNonDisabledIndex,
} from './utils'
import {generateId, scrollIntoView, setStatus, getState, noop} from './utils-ts'
class Downshift extends Component {
static propTypes = {
children: PropTypes.func,
defaultHighlightedIndex: PropTypes.number,
defaultIsOpen: PropTypes.bool,
initialHighlightedIndex: PropTypes.number,
initialSelectedItem: PropTypes.any,
initialInputValue: PropTypes.string,
initialIsOpen: PropTypes.bool,
getA11yStatusMessage: PropTypes.func,
itemToString: PropTypes.func,
onChange: PropTypes.func,
onSelect: PropTypes.func,
onStateChange: PropTypes.func,
onInputValueChange: PropTypes.func,
onUserAction: PropTypes.func,
onOuterClick: PropTypes.func,
selectedItemChanged: PropTypes.func,
stateReducer: PropTypes.func,
itemCount: PropTypes.number,
id: PropTypes.string,
environment: PropTypes.shape({
addEventListener: PropTypes.func.isRequired,
removeEventListener: PropTypes.func.isRequired,
document: PropTypes.shape({
createElement: PropTypes.func.isRequired,
getElementById: PropTypes.func.isRequired,
activeElement: PropTypes.any.isRequired,
body: PropTypes.any.isRequired,
}).isRequired,
Node: PropTypes.func.isRequired,
}),
suppressRefError: PropTypes.bool,
scrollIntoView: PropTypes.func,
// things we keep in state for uncontrolled components
// but can accept as props for controlled components
/* eslint-disable react/no-unused-prop-types */
selectedItem: PropTypes.any,
isOpen: PropTypes.bool,
inputValue: PropTypes.string,
highlightedIndex: PropTypes.number,
labelId: PropTypes.string,
inputId: PropTypes.string,
menuId: PropTypes.string,
getItemId: PropTypes.func,
/* eslint-enable react/no-unused-prop-types */
}
static defaultProps = {
defaultHighlightedIndex: null,
defaultIsOpen: false,
getA11yStatusMessage,
itemToString: i => {
if (i == null) {
return ''
}
if (
process.env.NODE_ENV !== 'production' &&
isPlainObject(i) &&
!i.hasOwnProperty('toString')
) {
// eslint-disable-next-line no-console
console.warn(
'downshift: An object was passed to the default implementation of `itemToString`. You should probably provide your own `itemToString` implementation. Please refer to the `itemToString` API documentation.',
'The object that was passed:',
i,
)
}
return String(i)
},
onStateChange: noop,
onInputValueChange: noop,
onUserAction: noop,
onChange: noop,
onSelect: noop,
onOuterClick: noop,
selectedItemChanged: (prevItem, item) => prevItem !== item,
environment:
/* istanbul ignore next (ssr) */
typeof window === 'undefined' || isReactNative ? undefined : window,
stateReducer: (state, stateToSet) => stateToSet,
suppressRefError: false,
scrollIntoView,
}
static stateChangeTypes = stateChangeTypes
constructor(props) {
super(props)
// fancy destructuring + defaults + aliases
// this basically says each value of state should either be set to
// the initial value or the default value if the initial value is not provided
const {
defaultHighlightedIndex,
initialHighlightedIndex: highlightedIndex = defaultHighlightedIndex,
defaultIsOpen,
initialIsOpen: isOpen = defaultIsOpen,
initialInputValue: inputValue = '',
initialSelectedItem: selectedItem = null,
} = this.props
const state = this.getState({
highlightedIndex,
isOpen,
inputValue,
selectedItem,
})
if (
state.selectedItem != null &&
this.props.initialInputValue === undefined
) {
state.inputValue = this.props.itemToString(state.selectedItem)
}
this.state = state
}
id = this.props.id || `downshift-${generateId()}`
menuId = this.props.menuId || `${this.id}-menu`
labelId = this.props.labelId || `${this.id}-label`
inputId = this.props.inputId || `${this.id}-input`
getItemId = this.props.getItemId || (index => `${this.id}-item-${index}`)
items = []
// itemCount can be changed asynchronously
// from within downshift (so it can't come from a prop)
// this is why we store it as an instance and use
// getItemCount rather than just use items.length
// (to support windowing + async)
itemCount = null
previousResultCount = 0
timeoutIds = []
/**
* @param {Function} fn the function to call after the time
* @param {Number} time the time to wait
*/
internalSetTimeout = (fn, time) => {
const id = setTimeout(() => {
this.timeoutIds = this.timeoutIds.filter(i => i !== id)
fn()
}, time)
this.timeoutIds.push(id)
}
/**
* Clear all running timeouts
*/
internalClearTimeouts() {
this.timeoutIds.forEach(id => {
clearTimeout(id)
})
this.timeoutIds = []
}
/**
* Gets the state based on internal state or props
* If a state value is passed via props, then that
* is the value given, otherwise it's retrieved from
* stateToMerge
*
* @param {Object} stateToMerge defaults to this.state
* @return {Object} the state
*/
getState(stateToMerge = this.state) {
return getState(stateToMerge, this.props)
}
getItemCount() {
// things read better this way. They're in priority order:
// 1. `this.itemCount`
// 2. `this.props.itemCount`
// 3. `this.items.length`
let itemCount = this.items.length
if (this.itemCount != null) {
itemCount = this.itemCount
} else if (this.props.itemCount !== undefined) {
itemCount = this.props.itemCount
}
return itemCount
}
setItemCount = count => {
this.itemCount = count
}
unsetItemCount = () => {
this.itemCount = null
}
getItemNodeFromIndex(index) {
return this.props.environment
? this.props.environment.document.getElementById(this.getItemId(index))
: null
}
isItemDisabled = (_item, index) => {
const currentElementNode = this.getItemNodeFromIndex(index)
return currentElementNode && currentElementNode.hasAttribute('disabled')
}
setHighlightedIndex = (
highlightedIndex = this.props.defaultHighlightedIndex,
otherStateToSet = {},
) => {
otherStateToSet = pickState(otherStateToSet)
this.internalSetState({highlightedIndex, ...otherStateToSet})
}
scrollHighlightedItemIntoView() {
/* istanbul ignore else (react-native) */
if (!isReactNative) {
const node = this.getItemNodeFromIndex(this.getState().highlightedIndex)
this.props.scrollIntoView(node, this._menuNode)
}
}
moveHighlightedIndex(amount, otherStateToSet) {
const itemCount = this.getItemCount()
const {highlightedIndex} = this.getState()
if (itemCount > 0) {
const nextHighlightedIndex = getHighlightedIndex(
highlightedIndex,
amount,
{length: itemCount},
this.isItemDisabled,
true,
)
this.setHighlightedIndex(nextHighlightedIndex, otherStateToSet)
}
}
clearSelection = cb => {
this.internalSetState(
{
selectedItem: null,
inputValue: '',
highlightedIndex: this.props.defaultHighlightedIndex,
isOpen: this.props.defaultIsOpen,
},
cb,
)
}
selectItem = (item, otherStateToSet, cb) => {
otherStateToSet = pickState(otherStateToSet)
this.internalSetState(
{
isOpen: this.props.defaultIsOpen,
highlightedIndex: this.props.defaultHighlightedIndex,
selectedItem: item,
inputValue: this.props.itemToString(item),
...otherStateToSet,
},
cb,
)
}
selectItemAtIndex = (itemIndex, otherStateToSet, cb) => {
const item = this.items[itemIndex]
if (item == null) {
return
}
this.selectItem(item, otherStateToSet, cb)
}
selectHighlightedItem = (otherStateToSet, cb) => {
return this.selectItemAtIndex(
this.getState().highlightedIndex,
otherStateToSet,
cb,
)
}
// any piece of our state can live in two places:
// 1. Uncontrolled: it's internal (this.state)
// We will call this.setState to update that state
// 2. Controlled: it's external (this.props)
// We will call this.props.onStateChange to update that state
//
// In addition, we'll call this.props.onChange if the
// selectedItem is changed.
internalSetState = (stateToSet, cb) => {
let isItemSelected, onChangeArg
const onStateChangeArg = {}
const isStateToSetFunction = typeof stateToSet === 'function'
// we want to call `onInputValueChange` before the `setState` call
// so someone controlling the `inputValue` state gets notified of
// the input change as soon as possible. This avoids issues with
// preserving the cursor position.
// See https://github.com/downshift-js/downshift/issues/217 for more info.
if (!isStateToSetFunction && stateToSet.hasOwnProperty('inputValue')) {
this.props.onInputValueChange(stateToSet.inputValue, {
...this.getStateAndHelpers(),
...stateToSet,
})
}
return this.setState(
state => {
state = this.getState(state)
let newStateToSet = isStateToSetFunction
? stateToSet(state)
: stateToSet
// Your own function that could modify the state that will be set.
newStateToSet = this.props.stateReducer(state, newStateToSet)
// checks if an item is selected, regardless of if it's different from
// what was selected before
// used to determine if onSelect and onChange callbacks should be called
isItemSelected = newStateToSet.hasOwnProperty('selectedItem')
// this keeps track of the object we want to call with setState
const nextState = {}
// this is just used to tell whether the state changed
const nextFullState = {}
// we need to call on change if the outside world is controlling any of our state
// and we're trying to update that state. OR if the selection has changed and we're
// trying to update the selection
if (
isItemSelected &&
newStateToSet.selectedItem !== state.selectedItem
) {
onChangeArg = newStateToSet.selectedItem
}
newStateToSet.type ||= stateChangeTypes.unknown
Object.keys(newStateToSet).forEach(key => {
// onStateChangeArg should only have the state that is
// actually changing
if (state[key] !== newStateToSet[key]) {
onStateChangeArg[key] = newStateToSet[key]
}
// the type is useful for the onStateChangeArg
// but we don't actually want to set it in internal state.
// this is an undocumented feature for now... Not all internalSetState
// calls support it and I'm not certain we want them to yet.
// But it enables users controlling the isOpen state to know when
// the isOpen state changes due to mouseup events which is quite handy.
if (key === 'type') {
return
}
nextFullState[key] = newStateToSet[key]
// if it's coming from props, then we don't care to set it internally
if (!isControlledProp(this.props, key)) {
nextState[key] = newStateToSet[key]
}
})
// if stateToSet is a function, then we weren't able to call onInputValueChange
// earlier, so we'll call it now that we know what the inputValue state will be.
if (
isStateToSetFunction &&
newStateToSet.hasOwnProperty('inputValue')
) {
this.props.onInputValueChange(newStateToSet.inputValue, {
...this.getStateAndHelpers(),
...newStateToSet,
})
}
return nextState
},
() => {
// call the provided callback if it's a function
cbToCb(cb)()
// only call the onStateChange and onChange callbacks if
// we have relevant information to pass them.
const hasMoreStateThanType = Object.keys(onStateChangeArg).length > 1
if (hasMoreStateThanType) {
this.props.onStateChange(onStateChangeArg, this.getStateAndHelpers())
}
if (isItemSelected) {
this.props.onSelect(
stateToSet.selectedItem,
this.getStateAndHelpers(),
)
}
if (onChangeArg !== undefined) {
this.props.onChange(onChangeArg, this.getStateAndHelpers())
}
// this is currently undocumented and therefore subject to change
// We'll try to not break it, but just be warned.
this.props.onUserAction(onStateChangeArg, this.getStateAndHelpers())
},
)
}
getStateAndHelpers() {
const {highlightedIndex, inputValue, selectedItem, isOpen} = this.getState()
const {itemToString} = this.props
const {id} = this
const {
getRootProps,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
getItemProps,
openMenu,
closeMenu,
toggleMenu,
selectItem,
selectItemAtIndex,
selectHighlightedItem,
setHighlightedIndex,
clearSelection,
clearItems,
reset,
setItemCount,
unsetItemCount,
internalSetState: setState,
} = this
return {
// prop getters
getRootProps,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
getItemProps,
// actions
reset,
openMenu,
closeMenu,
toggleMenu,
selectItem,
selectItemAtIndex,
selectHighlightedItem,
setHighlightedIndex,
clearSelection,
clearItems,
setItemCount,
unsetItemCount,
setState,
// props
itemToString,
// derived
id,
// state
highlightedIndex,
inputValue,
isOpen,
selectedItem,
}
}
//////////////////////////// ROOT
rootRef = node => (this._rootNode = node)
getRootProps = (
{refKey = 'ref', ref, ...rest} = {},
{suppressRefError = false} = {},
) => {
// this is used in the render to know whether the user has called getRootProps.
// It uses that to know whether to apply the props automatically
this.getRootProps.called = true
this.getRootProps.refKey = refKey
this.getRootProps.suppressRefError = suppressRefError
const {isOpen} = this.getState()
return {
[refKey]: handleRefs(ref, this.rootRef),
role: 'combobox',
'aria-expanded': isOpen,
'aria-haspopup': 'listbox',
'aria-owns': isOpen ? this.menuId : undefined,
'aria-labelledby': this.labelId,
...rest,
}
}
//\\\\\\\\\\\\\\\\\\\\\\\\\\ ROOT
keyDownHandlers = {
ArrowDown(event) {
event.preventDefault()
if (this.getState().isOpen) {
const amount = event.shiftKey ? 5 : 1
this.moveHighlightedIndex(amount, {
type: stateChangeTypes.keyDownArrowDown,
})
} else {
this.internalSetState(
{
isOpen: true,
type: stateChangeTypes.keyDownArrowDown,
},
() => {
const itemCount = this.getItemCount()
if (itemCount > 0) {
const {highlightedIndex} = this.getState()
const nextHighlightedIndex = getHighlightedIndex(
highlightedIndex,
1,
{length: itemCount},
this.isItemDisabled,
true,
)
this.setHighlightedIndex(nextHighlightedIndex, {
type: stateChangeTypes.keyDownArrowDown,
})
}
},
)
}
},
ArrowUp(event) {
event.preventDefault()
if (this.getState().isOpen) {
const amount = event.shiftKey ? -5 : -1
this.moveHighlightedIndex(amount, {
type: stateChangeTypes.keyDownArrowUp,
})
} else {
this.internalSetState(
{
isOpen: true,
type: stateChangeTypes.keyDownArrowUp,
},
() => {
const itemCount = this.getItemCount()
if (itemCount > 0) {
const {highlightedIndex} = this.getState()
const nextHighlightedIndex = getHighlightedIndex(
highlightedIndex,
-1,
{length: itemCount},
this.isItemDisabled,
true,
)
this.setHighlightedIndex(nextHighlightedIndex, {
type: stateChangeTypes.keyDownArrowUp,
})
}
},
)
}
},
Enter(event) {
if (event.which === 229) {
return
}
const {isOpen, highlightedIndex} = this.getState()
if (isOpen && highlightedIndex != null) {
event.preventDefault()
const item = this.items[highlightedIndex]
const itemNode = this.getItemNodeFromIndex(highlightedIndex)
if (item == null || (itemNode && itemNode.hasAttribute('disabled'))) {
return
}
this.selectHighlightedItem({
type: stateChangeTypes.keyDownEnter,
})
}
},
Escape(event) {
event.preventDefault()
this.reset({
type: stateChangeTypes.keyDownEscape,
...(!this.state.isOpen && {selectedItem: null, inputValue: ''}),
})
},
}
//////////////////////////// BUTTON
buttonKeyDownHandlers = {
...this.keyDownHandlers,
' '(event) {
event.preventDefault()
this.toggleMenu({type: stateChangeTypes.keyDownSpaceButton})
},
}
inputKeyDownHandlers = {
...this.keyDownHandlers,
Home(event) {
const {isOpen} = this.getState()
if (!isOpen) {
return
}
event.preventDefault()
const itemCount = this.getItemCount()
if (itemCount <= 0 || !isOpen) {
return
}
// get next non-disabled starting downwards from 0 if that's disabled.
const newHighlightedIndex = getNonDisabledIndex(
0,
false,
{length: itemCount},
this.isItemDisabled,
)
this.setHighlightedIndex(newHighlightedIndex, {
type: stateChangeTypes.keyDownHome,
})
},
End(event) {
const {isOpen} = this.getState()
if (!isOpen) {
return
}
event.preventDefault()
const itemCount = this.getItemCount()
if (itemCount <= 0 || !isOpen) {
return
}
// get next non-disabled starting upwards from last index if that's disabled.
const newHighlightedIndex = getNonDisabledIndex(
itemCount - 1,
true,
{length: itemCount},
this.isItemDisabled,
)
this.setHighlightedIndex(newHighlightedIndex, {
type: stateChangeTypes.keyDownEnd,
})
},
}
getToggleButtonProps = ({
onClick,
onPress,
onKeyDown,
onKeyUp,
onBlur,
...rest
} = {}) => {
const {isOpen} = this.getState()
const enabledEventHandlers =
isReactNative || isReactNativeWeb
? /* istanbul ignore next (react-native) */
{
onPress: callAllEventHandlers(onPress, this.buttonHandleClick),
}
: {
onClick: callAllEventHandlers(onClick, this.buttonHandleClick),
onKeyDown: callAllEventHandlers(
onKeyDown,
this.buttonHandleKeyDown,
),
onKeyUp: callAllEventHandlers(onKeyUp, this.buttonHandleKeyUp),
onBlur: callAllEventHandlers(onBlur, this.buttonHandleBlur),
}
const eventHandlers = rest.disabled ? {} : enabledEventHandlers
return {
type: 'button',
role: 'button',
'aria-label': isOpen ? 'close menu' : 'open menu',
'aria-haspopup': true,
'data-toggle': true,
...eventHandlers,
...rest,
}
}
buttonHandleKeyUp = event => {
// Prevent click event from emitting in Firefox
event.preventDefault()
}
buttonHandleKeyDown = event => {
const key = normalizeArrowKey(event)
if (this.buttonKeyDownHandlers[key]) {
this.buttonKeyDownHandlers[key].call(this, event)
}
}
buttonHandleClick = event => {
event.preventDefault()
// handle odd case for Safari and Firefox which
// don't give the button the focus properly.
/* istanbul ignore if (can't reasonably test this) */
if (!isReactNative && this.props.environment) {
const {body, activeElement} = this.props.environment.document
if (body && body === activeElement) {
event.target.focus()
}
}
// to simplify testing components that use downshift, we'll not wrap this in a setTimeout
// if the NODE_ENV is test. With the proper build system, this should be dead code eliminated
// when building for production and should therefore have no impact on production code.
if (process.env.NODE_ENV === 'test') {
this.toggleMenu({type: stateChangeTypes.clickButton})
} else {
// Ensure that toggle of menu occurs after the potential blur event in iOS
this.internalSetTimeout(() =>
this.toggleMenu({type: stateChangeTypes.clickButton}),
)
}
}
buttonHandleBlur = event => {
const blurTarget = event.target // Save blur target for comparison with activeElement later
// Need setTimeout, so that when the user presses Tab, the activeElement is the next focused element, not body element
this.internalSetTimeout(() => {
if (this.isMouseDown || !this.props.environment) {
return
}
const {activeElement} = this.props.environment.document
if (
(activeElement == null || activeElement.id !== this.inputId) &&
activeElement !== blurTarget // Do nothing if we refocus the same element again (to solve issue in Safari on iOS)
) {
this.reset({type: stateChangeTypes.blurButton})
}
})
}
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ BUTTON
/////////////////////////////// LABEL
getLabelProps = props => {
return {htmlFor: this.inputId, id: this.labelId, ...props}
}
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ LABEL
/////////////////////////////// INPUT
getInputProps = ({
onKeyDown,
onBlur,
onChange,
onInput,
onChangeText,
...rest
} = {}) => {
let onChangeKey
let eventHandlers = {}
/* istanbul ignore next (preact) */
if (isPreact) {
onChangeKey = 'onInput'
} else {
onChangeKey = 'onChange'
}
const {inputValue, isOpen, highlightedIndex} = this.getState()
if (!rest.disabled) {
eventHandlers = {
[onChangeKey]: callAllEventHandlers(
onChange,
onInput,
this.inputHandleChange,
),
onKeyDown: callAllEventHandlers(onKeyDown, this.inputHandleKeyDown),
onBlur: callAllEventHandlers(onBlur, this.inputHandleBlur),
}
}
/* istanbul ignore if (react-native) */
if (isReactNative) {
eventHandlers = {
onChange: callAllEventHandlers(
onChange,
onInput,
this.inputHandleChange,
),
onChangeText: callAllEventHandlers(onChangeText, onInput, text =>
this.inputHandleChange({nativeEvent: {text}}),
),
onBlur: callAllEventHandlers(onBlur, this.inputHandleBlur),
}
}
return {
'aria-autocomplete': 'list',
'aria-activedescendant':
isOpen && typeof highlightedIndex === 'number' && highlightedIndex >= 0
? this.getItemId(highlightedIndex)
: undefined,
'aria-controls': isOpen ? this.menuId : undefined,
'aria-labelledby': rest && rest['aria-label'] ? undefined : this.labelId,
// https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
// revert back since autocomplete="nope" is ignored on latest Chrome and Opera
autoComplete: 'off',
value: inputValue,
id: this.inputId,
...eventHandlers,
...rest,
}
}
inputHandleKeyDown = event => {
const key = normalizeArrowKey(event)
if (key && this.inputKeyDownHandlers[key]) {
this.inputKeyDownHandlers[key].call(this, event)
}
}
inputHandleChange = event => {
this.internalSetState({
type: stateChangeTypes.changeInput,
isOpen: true,
inputValue:
isReactNative || isReactNativeWeb
? /* istanbul ignore next (react-native) */ event.nativeEvent.text
: event.target.value,
highlightedIndex: this.props.defaultHighlightedIndex,
})
}
inputHandleBlur = () => {
// Need setTimeout, so that when the user presses Tab, the activeElement is the next focused element, not the body element
this.internalSetTimeout(() => {
if (this.isMouseDown || !this.props.environment) {
return
}
const {activeElement} = this.props.environment.document
const downshiftButtonIsActive =
activeElement?.dataset?.toggle &&
this._rootNode &&
this._rootNode.contains(activeElement)
if (!downshiftButtonIsActive) {
this.reset({type: stateChangeTypes.blurInput})
}
})
}
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ INPUT
/////////////////////////////// MENU
menuRef = node => {
this._menuNode = node
}
getMenuProps = (
{refKey = 'ref', ref, ...props} = {},
{suppressRefError = false} = {},
) => {
this.getMenuProps.called = true
this.getMenuProps.refKey = refKey
this.getMenuProps.suppressRefError = suppressRefError
return {
[refKey]: handleRefs(ref, this.menuRef),
role: 'listbox',
'aria-labelledby':
props && props['aria-label'] ? undefined : this.labelId,
id: this.menuId,
...props,
}
}
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ MENU
/////////////////////////////// ITEM
getItemProps = ({
onMouseMove,
onMouseDown,
onClick,
onPress,
index,
item = process.env.NODE_ENV === 'production'
? /* istanbul ignore next */ undefined
: requiredProp('getItemProps', 'item'),
...rest
} = {}) => {
if (index === undefined) {
this.items.push(item)
index = this.items.indexOf(item)
} else {
this.items[index] = item
}
const onSelectKey =
isReactNative || isReactNativeWeb
? /* istanbul ignore next (react-native) */ 'onPress'
: 'onClick'
const customClickHandler = isReactNative
? /* istanbul ignore next (react-native) */ onPress
: onClick
const enabledEventHandlers = {
// onMouseMove is used over onMouseEnter here. onMouseMove
// is only triggered on actual mouse movement while onMouseEnter
// can fire on DOM changes, interrupting keyboard navigation
onMouseMove: callAllEventHandlers(onMouseMove, () => {
if (index === this.getState().highlightedIndex) {
return
}
this.setHighlightedIndex(index, {
type: stateChangeTypes.itemMouseEnter,
})
// We never want to manually scroll when changing state based
// on `onMouseMove` because we will be moving the element out
// from under the user which is currently scrolling/moving the
// cursor
this.avoidScrolling = true
this.internalSetTimeout(() => (this.avoidScrolling = false), 250)
}),
onMouseDown: callAllEventHandlers(onMouseDown, event => {
// This prevents the activeElement from being changed
// to the item so it can remain with the current activeElement
// which is a more common use case.
event.preventDefault()
}),
[onSelectKey]: callAllEventHandlers(customClickHandler, () => {
this.selectItemAtIndex(index, {
type: stateChangeTypes.clickItem,
})
}),
}
// Passing down the onMouseDown handler to prevent redirect
// of the activeElement if clicking on disabled items
const eventHandlers = rest.disabled
? {onMouseDown: enabledEventHandlers.onMouseDown}
: enabledEventHandlers
return {
id: this.getItemId(index),
role: 'option',
'aria-selected': this.getState().highlightedIndex === index,
...eventHandlers,
...rest,
}
}
//\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ ITEM
clearItems = () => {
this.items = []
}
reset = (otherStateToSet = {}, cb) => {
otherStateToSet = pickState(otherStateToSet)
this.internalSetState(
({selectedItem}) => ({
isOpen: this.props.defaultIsOpen,
highlightedIndex: this.props.defaultHighlightedIndex,
inputValue: this.props.itemToString(selectedItem),
...otherStateToSet,
}),
cb,
)
}
toggleMenu = (otherStateToSet = {}, cb) => {
otherStateToSet = pickState(otherStateToSet)
this.internalSetState(
({isOpen}) => {
return {
isOpen: !isOpen,
...(isOpen && {
highlightedIndex: this.props.defaultHighlightedIndex,
}),
...otherStateToSet,
}
},
() => {
const {isOpen, highlightedIndex} = this.getState()
if (isOpen) {
if (this.getItemCount() > 0 && typeof highlightedIndex === 'number') {
this.setHighlightedIndex(highlightedIndex, otherStateToSet)
}
}
cbToCb(cb)()
},
)
}
openMenu = cb => {
this.internalSetState({isOpen: true}, cb)
}
closeMenu = cb => {
this.internalSetState({isOpen: false}, cb)
}
updateStatus = debounce(() => {
if (!this.props?.environment?.document) {
return
}
const state = this.getState()
const item = this.items[state.highlightedIndex]
const resultCount = this.getItemCount()
const status = this.props.getA11yStatusMessage({
itemToString: this.props.itemToString,
previousResultCount: this.previousResultCount,
resultCount,
highlightedItem: item,
...state,
})
this.previousResultCount = resultCount
setStatus(status, this.props.environment.document)
}, 200)
componentDidMount() {
/* istanbul ignore if (react-native) */
if (
process.env.NODE_ENV !== 'production' &&
!isReactNative &&
this.getMenuProps.called &&
!this.getMenuProps.suppressRefError
) {
validateGetMenuPropsCalledCorrectly(this._menuNode, this.getMenuProps)
}
/* istanbul ignore if (react-native or SSR) */
if (isReactNative || !this.props.environment) {
this.cleanup = () => {
this.internalClearTimeouts()
}
} else {
// this.isMouseDown helps us track whether the mouse is currently held down.
// This is useful when the user clicks on an item in the list, but holds the mouse
// down long enough for the list to disappear (because the blur event fires on the input)
// this.isMouseDown is used in the blur handler on the input to determine whether the blur event should
// trigger hiding the menu.
const onMouseDown = () => {
this.isMouseDown = true
}
const onMouseUp = event => {
this.isMouseDown = false
// if the target element or the activeElement is within a downshift node
// then we don't want to reset downshift
const contextWithinDownshift = targetWithinDownshift(
event.target,
[this._rootNode, this._menuNode],
this.props.environment,
)
if (!contextWithinDownshift && this.getState().isOpen) {
this.reset({type: stateChangeTypes.mouseUp}, () =>
this.props.onOuterClick(this.getStateAndHelpers()),
)
}
}
// Touching an element in iOS gives focus and hover states, but touching out of
// the element will remove hover, and persist the focus state, resulting in the
// blur event not being triggered.
// this.isTouchMove helps us track whether the user is tapping or swiping on a touch screen.
// If the user taps outside of Downshift, the component should be reset,
// but not if the user is swiping
const onTouchStart = () => {
this.isTouchMove = false
}
const onTouchMove = () => {
this.isTouchMove = true
}
const onTouchEnd = event => {
const contextWithinDownshift = targetWithinDownshift(
event.target,
[this._rootNode, this._menuNode],
this.props.environment,
false,
)
if (
!this.isTouchMove &&
!contextWithinDownshift &&
this.getState().isOpen
) {
this.reset({type: stateChangeTypes.touchEnd}, () =>
this.props.onOuterClick(this.getStateAndHelpers()),
)
}
}
const {environment} = this.props
environment.addEventListener('mousedown', onMouseDown)
environment.addEventListener('mouseup', onMouseUp)
environment.addEventListener('touchstart', onTouchStart)
environment.addEventListener('touchmove', onTouchMove)
environment.addEventListener('touchend', onTouchEnd)
this.cleanup = () => {
this.internalClearTimeouts()
this.updateStatus.cancel()
environment.removeEventListener('mousedown', onMouseDown)
environment.removeEventListener('mouseup', onMouseUp)
environment.removeEventListener('touchstart', onTouchStart)
environment.removeEventListener('touchmove', onTouchMove)
environment.removeEventListener('touchend', onTouchEnd)
}
}
}
shouldScroll(prevState, prevProps) {
const {highlightedIndex: currentHighlightedIndex} =
this.props.highlightedIndex === undefined ? this.getState() : this.props
const {highlightedIndex: prevHighlightedIndex} =
prevProps.highlightedIndex === undefined ? prevState : prevProps
const scrollWhenOpen =
currentHighlightedIndex && this.getState().isOpen && !prevState.isOpen
const scrollWhenNavigating =
currentHighlightedIndex !== prevHighlightedIndex
return scrollWhenOpen || scrollWhenNavigating
}
componentDidUpdate(prevProps, prevState) {
if (process.env.NODE_ENV !== 'production') {
validateControlledUnchanged(this.state, prevProps, this.props)
/* istanbul ignore if (react-native) */
if (
!isReactNative &&
this.getMenuProps.called &&
!this.getMenuProps.suppressRefError
) {
validateGetMenuPropsCalledCorrectly(this._menuNode, this.getMenuProps)
}
}
if (
isControlledProp(this.props, 'selectedItem') &&
this.props.selectedItemChanged(
prevProps.selectedItem,
this.props.selectedItem,
)
) {
this.internalSetState({
type: stateChangeTypes.controlledPropUpdatedSelectedItem,
inputValue: this.props.itemToString(this.props.selectedItem),
})
}
if (!this.avoidScrolling && this.shouldScroll(prevState, prevProps)) {
this.scrollHighlightedItemIntoView()
}
/* istanbul ignore else (react-native) */
if (!isReactNative) {
this.updateStatus()
}
}
componentWillUnmount() {
this.cleanup() // avoids memory leak
}
render() {
const children = unwrapArray(this.props.children, noop)
// because the items are rerendered every time we call the children
// we clear this out each render and it will be populated again as
// getItemProps is called.
this.clearItems()
// we reset this so we know whether the user calls getRootProps during
// this render. If they do then we don't need to do anything,
// if they don't then we need to clone the element they return and
// apply the props for them.
this.getRootProps.called = false
this.getRootProps.refKey = undefined
this.getRootProps.suppressRefError = undefined
// we do something similar for getMenuProps
this.getMenuProps.called = false
this.getMenuProps.refKey = undefined
this.getMenuProps.suppressRefError = undefined
// we do something similar for getLabelProps
this.getLabelProps.called = false
// and something similar for getInputProps
this.getInputProps.called = false
const element = unwrapArray(children(this.getStateAndHelpers()))
if (!element) {
return null
}
if (this.getRootProps.called || this.props.suppressRefError) {
if (
process.env.NODE_ENV !== 'production' &&
!this.getRootProps.suppressRefError &&
!this.props.suppressRefError
) {
validateGetRootPropsCalledCorrectly(element, this.getRootProps)
}
return element
} else if (isDOMElement(element)) {
// they didn't apply the root props, but we can clone
// this and apply the props ourselves
return cloneElement(element, this.getRootProps(getElementProps(element)))
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// they didn't apply the root props, but they need to
// otherwise we can't query around the autocomplete
throw new Error(
'downshift: If you return a non-DOM element, you must apply the getRootProps function',
)
}
/* istanbul ignore next */
return undefined
}
}
export default Downshift
function validateGetMenuPropsCalledCorrectly(node, {refKey}) {
if (!node) {
// eslint-disable-next-line no-console
console.error(
`downshift: The ref prop "${refKey}" from getMenuProps was not applied correctly on your menu element.`,
)
}
}
function validateGetRootPropsCalledCorrectly(element, {refKey}) {
const refKeySpecified = refKey !== 'ref'
const isComposite = !isDOMElement(element)
if (isComposite && !refKeySpecified && !isForwardRef(element)) {
// eslint-disable-next-line no-console
console.error(
'downshift: You returned a non-DOM element. You must specify a refKey in getRootProps',
)
} else if (!isComposite && refKeySpecified) {
// eslint-disable-next-line no-console
console.error(
`downshift: You returned a DOM element. You should not specify a refKey in getRootProps. You specified "${refKey}"`,
)
}
if (!isForwardRef(element) && !getElementProps(element)[refKey]) {
// eslint-disable-next-line no-console
console.error(
`downshift: You must apply the ref prop "${refKey}" from getRootProps onto your root element.`,
)
}
}
================================================
FILE: src/hooks/MIGRATION_V7.md
================================================
# Migration from v6 to v7
Since version _7.0.0_ Downshift follows the (ARIA 1.2 guideline for the
combobox)[select-aria]. This brought a series of changes that are considered
breaking, both to the API and the behaviour of downshift's _useSelect_ and
_useCombobox_ hooks. The list of changes, as well as the migration itself, is
detailed below.
## Table of Contents
- [useSelect](#useselect)
- [Focus](#focus)
- [HTML Attributes](#html-attributes)
- [Events](#events)
- [stateChangeTypes](#statechangetypes)
- [circularNavigation](#circularnavigation)
- [useCombobox](#usecombobox)
- [HTML Attributes](#html-attributes-1)
- [Events](#events-1)
- [stateChangeTypes](#statechangetypes-1)
- [circularNavigation](#circularnavigation-1)
## useSelect
### Focus
Since [ARIA 1.2](select-aria-example), focus stays on the trigger element at all
times. (Previously)[deprecated-select-aria], it toggled between the trigger and
the menu depending on the open state of the _select_ element. If any of your
custom implementation involved the focus on the menu element, please change it
as the focus stays on the trigger even when the menu is open.
### HTML Attributes
Similar to 1.1, _useSelect_ communicates to the screen reader the currently
highlighted item via the _aria-activedescendant_ attribute. However, since now
the focus is always on the trigger element, this attribute, along with others,
have shifted as shown below:
- getToggleButtonProps additions:
- role=combobox
- aria-activedescendant=${highlightedItemId}
- aria-controls=${menuId}
- tabindex=0
- getMenuProps removals:
- aria-activedescendant=${highlightedItemId}
- getItemProps changes:
- aria-selected=${item === selectedItem} - now the item that is selected
received _aria-selected=true_, the rest receive it as false. Previously, the
highlighted item was marked with _aria-selected=true_.
### Events
Event changes occured because of the focus shift, as well as new accessibility
pattern recommendantions.
- getToggleButtonProps additions:
- _ArrowDown+Alt_: opens the menu without any item highlighted.
- _ArrowUp+Alt_: closes the menu and selects the highlighted item.
- _End_: highlights the last item and opens the menu if closed.
- _Home_: highlights the first item and opens the menu if closed.
- _PageUp_: if menu is open, moves highlight by 10 positions to the start.
- _PageDown_: if menu is open, moves highlight by 10 positions to the end.
- _${characterKey}_: always opens the menu if closed, highlights the item
starting with that key (same behaviour as before when the menu is opened).
- _Enter_: if menu is open, closes the menu and selects the highlighted item.
- _SpaceBar_: if menu is open, closes the menu and selects the highlighted
item. If the space is part of a search query, it will be added to the search
query instead.
- _Escape_: closes the menu if open, without selecting anything.
- _Tab_ or any other _Blur_: closes the menu if open, selects highlighted
item, focus moves naturally.
- _ArrowUp_: moves highlight one position up. _Shift_ modifier is not
supported anymore.
- _ArrowDown_: moves highlight one position down. _Shift_ modifier is not
supported anymore.
- getToggleButtonProps changes:
- _ArrowUp_: if there is an item selected, opens the menu with that item
highlighted, not with the -1 offset as it did in v6 (ARIA 1.1).
- _ArrowDown_: if there is an item selected, opens the menu with that item
highlighted, not with the +1 offset as it did in v6 (ARIA 1.1).
- getMenuProps removals:
- _ArrowUp_, _ArrowDown_, _End_, _Home_, _Enter_, _Escape_, _SpaceBar_, _Tab_.
### stateChangeTypes
As a consequence of the [event changes](#events), the _stateChangeTypes_
received in the _stateReducer_ and _on${statePropery}Change_ received the
following modifications:
- MenuKeyDownArrowDown -> ToggleButtonKeyDownArrowDown
- MenuKeyDownArrowUp -> ToggleButtonKeyDownArrowUp
- MenuKeyDownEscape -> ToggleButtonKeyDownEscape
- MenuKeyDownHome -> ToggleButtonKeyDownHome
- MenuKeyDownEnd -> ToggleButtonKeyDownEnd
- MenuKeyDownEnter -> ToggleButtonKeyDownEnter
- MenuKeyDownSpaceButton -> ToggleButtonKeyDownSpaceButton
- MenuKeyDownCharacter -> ToggleButtonKeyDownCharacter
- MenuBlur -> ToggleButtonBlur
- ToggleButtonKeyDownPageUp: new state change type.
- ToggleButtonKeyDownPageDown: new state change type.
Please change your reducer / onChange code accordingly. For instance:
```js
function stateReducer(state, actionAndChanges) {
const {changes, type} = actionAndChanges
switch (type) {
case useSelect.stateChangeTypes.MenuKeyDownEnter:
case useSelect.stateChangeTypes.MenuKeyDownSpaceButton:
case useSelect.stateChangeTypes.ItemClick:
return {
...changes,
isOpen: true, // keep the menu open after selection.
}
default:
return changes
}
}
```
Becomes:
```js
function stateReducer(state, actionAndChanges) {
const {changes, type} = actionAndChanges
switch (type) {
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
case useSelect.stateChangeTypes.ItemClick:
return {
...changes,
isOpen: true, // keep the menu open after selection.
}
default:
return changes
}
}
```
> Another thing to mention is that, since ARIA 1.2 does not recommend a
> `` element for the toggle button anymore, _Enter_ and _SpaceBar_ do
> not perform click events out of the box, when the menu is closed.
> Consequently, we are triggering these events manually. For backwards
> compatibility to pre v7, when menu is closed and toggle element receives
> _Enter_ or _SpaceBar_ key event, it will trigger a
> `useSelect.stateChangeTypes.ToggleButtonClick` type change.
### circularNavigation
The prop _circularNavigation_ has been removed. Navigation inside the menu is
standard and non-circular. If you wish to make it circular, use the
_stateReducer_:
```js
function stateReducer(state, actionAndChanges) {
const {changes, type} = actionAndChanges
switch (type) {
case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowDown:
if (state.highlightedIndex === items.length - 1) {
return {...changes, highlightedIndex: state.highlightedIndex}
} else {
return changes
}
case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowUp:
if (state.highlightedIndex === 0) {
return {...changes, highlightedIndex: state.highlightedIndex}
} else {
return changes
}
default:
return changes
}
}
```
## useCombobox
### HTML Attributes
The biggest change in [ARIA 1.2](combobox-aria-example) is that the input
wrapper element does not receive the combobox role attributes anymore.
Previously[deprecated-combobox-aria], the role of _combobox_, as well as other
HTML attributes, had to be added on the input parent element. Some attributes
that belonged to the wrapper element are now added on the input element. The
changes are as follows:
- getComboboxProps has been removed.
- getInputProps additions:
- role=combobox
- aria-expanded=${isOpen}
- getToggleButtonProps additions:
- aria-controls=${menuId}
- aria-expanded=${isOpen}
### Events
As a result of the 1.2 pattern, there are a few event handling changes detailed
below.
- getInputProps additions:
- _ArrowDown+Alt_: opens the menu without any item highlighted.
- _ArrowUp+Alt_: closes the menu and selects the highlighted item.
- _PageUp_: if menu is open, moves highlight by 10 positions to the start.
- _PageDown_: if menu is open, moves highlight by 10 positions to the end.
- _Focus_: if menu is closed, opens the menu.
- getInputProps changes:
- _ArrowUp_: moves highlight one position up. _Shift_ modifier is not
supported anymore.
- _ArrowDown_: moves highlight one position down. _Shift_ modifier is not
supported anymore.
### stateChangeTypes
As a consequence of the [event changes](#events), the _stateChangeTypes_
received in the _stateReducer_ and _on${statePropery}Change_ received the
following additions:
- InputKeyDownPageUp
- InputKeyDownPageDown
- InputFocus
You don't need to change your reducer if you want to keep the 1.2 functionality
provided by default. However, if you want to keep the menu closed when the input
gets focus, you can do:
```js
function stateReducer(state, actionAndChanges) {
const {changes, type} = actionAndChanges
switch (type) {
case useCombobox.stateChangeTypes.InputFocus:
return {
...changes,
isOpen: state.isOpen, // keep the menu closed when input gets focused.
}
default:
return changes
}
}
```
### circularNavigation
The prop _circularNavigation_ has been removed. Navigation inside the menu is
standard and circular. If you wish to make it non-circular, use the
_stateReducer_:
```js
function stateReducer(state, actionAndChanges) {
const {changes, type} = actionAndChanges
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownArrowDown:
if (state.highlightedIndex === items.length - 1) {
return {...changes, highlightedIndex: state.highlightedIndex}
}
break
case useCombobox.stateChangeTypes.InputKeyDownArrowUp:
if (state.highlightedIndex === 0) {
return {...changes, highlightedIndex: state.highlightedIndex}
}
break
default:
return changes
}
return changes
}
```
[combobox-aria]: https://w3c.github.io/aria-practices/#combobox
[select-aria-example]:
https://w3c.github.io/aria-practices/examples/combobox/combobox-select-only.html
[combobox-aria-example]:
https://www.w3.org/WAI/ARIA/apg/example-index/combobox/combobox-autocomplete-list.html
[deprecated-select-aria]:
https://www.w3.org/TR/2017/NOTE-wai-aria-practices-1.1-20171214/examples/listbox/listbox-collapsible.html
[deprecated-combobox-aria]:
https://www.w3.org/TR/2017/NOTE-wai-aria-practices-1.1-20171214/examples/combobox/aria1.1pattern/listbox-combo.html
================================================
FILE: src/hooks/MIGRATION_V8.md
================================================
# Migration from v7 to v8
Downshift v8 receives a list of breaking changes, which are necessary to improve
both the user and the developer experience. The changes are only affecting the
hooks and are detailed below.
## Table of Contents
- [isItemDisabled](#isitemdisabled)
- [useCombobox input click](#usecombobox-input-click)
- [Getter props return value types](#getter-props-return-value-types)
- [environment propTypes](#environment-proptypes)
## isItemDisabled
Both `useCombobox` and `useSelect` now support the `isItemDisabled` function.
This new API is used to mark menu items as disabled, and as such remove the from
the navigation and prevent them from being selected. The old API required
passing the `disabled` prop to the `getItemProps` function. This old API has
been removed and you will receive a console warning if you are trying to use the
`disabled` prop in getItemProps.
Example of API migration:
Old:
```jsx
const items = [{value: 'item1'}, {value: 'item2'}]
const {getInputProps, ...rest} = useCombobox({items})
return (
// ... rest
)
```
New:
```jsx
const items = [{value: 'item1'}, {value: 'item2'}]
const {getInputProps, ...rest} = useCombobox({items, isItemDisabled(item, _index) { return item.value === 'item2' }})
return (
// ... rest
)
```
The API for Downshift remains unchange.
Related PR: https://github.com/downshift-js/downshift/pull/1510
## useCombobox input click
[ARIA 1.2](combobox-aria-example) recommends to toggle the menu open state at
input click. Previously, in v7, the menu was opened on receiving focus, from
both mouse and keyboard. Starting with v8, input focus will not trigger any
state change anymore. Only the input click event will be handled and will
trigger a menu toggle. Consequently:
- getInputProps **will not** return any _Focus_ event handler.
- getInputProps **will** return a _Click_ event handler.
- `useCombobox.stateChangeTypes.InputFocus` has been removed.
- `useCombobox.stateChangeTypes.InputClick` has been added instead.
We recommend having the default toggle on input click behaviour as it's part of
the official ARIA combobox 1.2 spec, but if you wish to override it and not
toggle the menu on click, use the stateReducer:
```js
function stateReducer(state, actionAndChanges) {
const {changes, type} = actionAndChanges
switch (type) {
case useCombobox.stateChangeTypes.InputClick:
return {
...changes,
isOpen: state.isOpen, // do not toggle the menu when input is clicked.
}
default:
return changes
}
}
```
If you want to return to the v7 behaviour and open the menu on focus, keep the
reducer above so you remove the toggle behaviour, and call the _openMenu_
imperative function, returned by useCombobox, in a _onFocus_ handler passed to
_getInputProps_:
```js
```
Consider to use the default 1.2 ARIA behaviour provided by default in order to
align your widget to the accessibility official spec. This behaviour consistency
improves the user experience, since all comboboxes should behave the same and
there won't be need for any additional guess work done by your users.
Related PR: https://github.com/downshift-js/downshift/pull/1440
## Getter props return value types
Previously, the return value from the getter props returned by both Downshift
and the hooks was `any`. In v8 we improved the types in order to reflect what is
actually returned: the default values return by the getter prop function and
whatever else the user passes as arguments. The type changes are done in
[this PR](https://github.com/downshift-js/downshift/pull/1482) and the
[8.0.2 PR](https://github.com/downshift-js/downshift/pull/1524). Make sure you
adapt your TS code, if applicable.
Also, in the `Downshift` component, the return values for some getter prop
values have changed from `null` to `undefined`, since that is what HTML elements
expect (value or undefined). These values are also reflected in the TS types.
- getRootProps: 'aria-owns': isOpen ? this.menuId : ~~null~~undefined,
- getInputProps:
- 'aria-controls': isOpen ? this.menuId : ~~null~~undefined
- 'aria-activedescendant': isOpen && typeof highlightedIndex === 'number' &&
highlightedIndex >= 0 ? this.getItemId(highlightedIndex) : ~~null~~undefined
- getMenuProps: props && props['aria-label'] ? ~~null~~undefined : this.labelId,
Related PR: https://github.com/downshift-js/downshift/pull/1482
## environment propTypes
The `environment` prop is useful when you are using downshift in a context
different than regular DOM. Its TS type has been updated to contain `Node` and
the propTypes have also been updated to reflect the properties which are
required by downshift from `environment`.
Related PR: https://github.com/downshift-js/downshift/pull/1463
[combobox-aria-example]:
https://www.w3.org/WAI/ARIA/apg/example-index/combobox/combobox-autocomplete-list.html
================================================
FILE: src/hooks/MIGRATION_V9.md
================================================
# Migration from v8 to v9
Downshift v8 receives a list of breaking changes, which are necessary to improve
both the user and the developer experience. The changes are only affecting the
hooks and are detailed below.
## Table of Contents
- [onChange Typescript Improvements](#onchange-typescript-improvements)
- [getA11ySelectionMessage](#geta11yselectionmessage)
- [getA11yRemovalMessage](#geta11yremovalmessage)
- [getA11yStatusMessage](#geta11ystatusmessage)
- [selectedItemChanged](#selecteditemchanged)
## onChange Typescript Improvements
The handlers below have their types improved to reflect that they will always
get called with their corresponding state prop:
- useCombobox
- onSelectedItemChange: selectedItem is non optional
- onIsOpenChange: isOpen is non optional
- onHighlightedIndexChange: highlightedIndex is non optional
- useSelect
- onSelectedItemChange: selectedItem is non optional
- onIsOpenChange: isOpen is non optional
- onHighlightedIndexChange: highlightedIndex is non optional
- onInputValueChange: inputValue is non optional
- useMultipleSelection
- onActiveIndexChange: activeIndex is non optional
- onSelectedItemsChange: selectedItems is non optional
## getA11ySelectionMessage
The prop has been removed from useSelect and useCombobox. If you still need an
a11y selection message, use either `getA11yStatusMessage` or your own aria-live
implementation inside a `onStateChange` callback.
## getA11yRemovalMessage
The prop has been removed from useMultipleSelection. If you still need an a11y
removal message, use either `getA11yStatusMessage` or your own aria-live
implementation inside a `onStateChange` callback.
## getA11yStatusMessage
The prop has been also added to useMultipleSelection, but has some changes
reflected in each of the hook's readme.
- there is no default function provided, so you will not get any aria-live
message anymore if you don't provide the prop directly to the hooks.
- the function is called only with the hook's state, and you should already have
access to the props, such as items or itemToString. Values such as
highlightedItem or resultsCount have been removed, so you need to compute them
yourself if needed.
- `Downshift` is not affected, it has the same `getA11yStatusMessage` as before,
no changes there at all.
The HTML markup with the ARIA attributes we provide through the getter props
should be enough for screen readers to report:
- results count.
- highlighted item.
- item selection.
- what actions the user can take.
If you need anything more specific as part of an aria-live region, please use
the new version of `getA11yStatusMessage` or your own aria-live implementation.
References:
- [useCombobox docs](https://github.com/downshift-js/downshift/blob/master/src/hooks/useCombobox/README.md#geta11ystatusmessage)
- [useSelect docs](https://github.com/downshift-js/downshift/blob/master/src/hooks/useSelect/README.md#geta11ystatusmessage)
- [useMultipleSelection docs](https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/README.md#geta11ystatusmessage)
## selectedItemChanged
This prop has been removed from `useCombobox`. You should use `itemToKey`
instead.
Reference:
[itemToKey docs](https://github.com/downshift-js/downshift/blob/master/src/hooks/useCombobox/README.md#itemtokey)
================================================
FILE: src/hooks/README.md
================================================
# Downshift Hooks
A set of hooks to build simple, flexible, WAI-ARIA compliant React dropdown
components. Developed as a follow up on [this issue][hooks-issue] about using
hooks in our API.
## Migration to v7
`useSelect` and `useCombobox` received some changes related to their API and how
they works in version 7, as a conequence of adapting both hooks to the ARIA 1.2
combobox patterns. If you were using _useSelect_ and/or _useCombobox_ previous
to 7.0.0, check the [migration guide][migration-guide] and update if necessary.
## Hooks
Check out one of the hooks below to use in your application and create fully
accessible widgets without any constraint about the UI library used.
### useSelect
For a custom `select` dropdown check out [useSelect][select-readme].
### useCombobox
For a `combobox/autocomplete` input check out [useCombobox][combobox-readme].
### useTagGroup
For a `tag group` that could also be used to build a multiple selection `select`
or a `combobox` with tags, check out [useTagGroup][tag-group-readme].
## Downshift Hooks API talk
[Silviu](https://silviuaavram.com/) delivered a talk about using the Downshift
hooks at the [axe-con][axe-con] 2021 conference. The talk, which is also
[recorded][axe-con-recording], illustrates how to build an accessible select,
combobox, and support multiple selection using Downshift hooks and custom
components from [ChakraUI][chakra-ui]. It offers a brief crash course to:
- build a custom Select.
- build a custom Combobox.
- enhance the Select and Combobox with multiple selection.
- use custom features like control props, the state reducer and action props.
## Roadmap and contributions
Next steps:
- iterate on `useSelect`, `useCombobox`, `useMultipleSelection` to improve them.
- remove the `Downshift` component once the hooks are mature.
[hooks-issue]: https://github.com/downshift-js/downshift/issues/683
[select-readme]:
https://github.com/downshift-js/downshift/tree/master/src/hooks/useSelect
[combobox-readme]:
https://github.com/downshift-js/downshift/tree/master/src/hooks/useCombobox
[tag-group-readme]:
https://github.com/downshift-js/downshift/tree/master/src/hooks/useTagGroup
[migration-guide]:
https://github.com/downshift-js/downshift/tree/master/src/hooks/MIGRATION_V7.md
[axe-con]: https://www.deque.com/axe-con/
[axe-con-recording]:
https://www.youtube.com/watch?v=iDEETM9Pa4Q&ab_channel=DequeSystems
[chakra-ui]: https://chakra-ui.com/
================================================
FILE: src/hooks/__tests__/__snapshots__/utils.test.js.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`utils useMouseAndTouchTracker adds and removes listeners to environment: element change rerender adding events 1`] = `
[
[
mousedown,
[Function],
],
[
mouseup,
[Function],
],
[
touchstart,
[Function],
],
[
touchmove,
[Function],
],
[
touchend,
[Function],
],
]
`;
exports[`utils useMouseAndTouchTracker adds and removes listeners to environment: element change rerender remove events 1`] = `
[
[
mousedown,
[Function],
],
[
mouseup,
[Function],
],
[
touchstart,
[Function],
],
[
touchmove,
[Function],
],
[
touchend,
[Function],
],
]
`;
exports[`utils useMouseAndTouchTracker adds and removes listeners to environment: initial adding events 1`] = `
[
[
mousedown,
[Function],
],
[
mouseup,
[Function],
],
[
touchstart,
[Function],
],
[
touchmove,
[Function],
],
[
touchend,
[Function],
],
]
`;
exports[`utils useMouseAndTouchTracker adds and removes listeners to environment: unmount remove events 1`] = `
[
[
mousedown,
[Function],
],
[
mouseup,
[Function],
],
[
touchstart,
[Function],
],
[
touchmove,
[Function],
],
[
touchend,
[Function],
],
]
`;
================================================
FILE: src/hooks/__tests__/utils.test.js
================================================
import {renderHook} from '@testing-library/react'
import {useMouseAndTouchTracker, isDropdownsStateEqual} from '../utils'
import {getInitialValue, getDefaultValue, getItemAndIndex} from '../utils-ts'
import {dropdownDefaultProps} from '../utils.dropdown'
describe('utils', () => {
describe('itemToString', () => {
test('returns empty string if item is falsy', () => {
const emptyString = dropdownDefaultProps.itemToString(null)
expect(emptyString).toBe('')
})
})
describe('getItemAndIndex', () => {
test('returns arguments if passed as defined', () => {
expect(getItemAndIndex({}, 5, [])).toEqual([{}, 5])
})
test('throws an error when item and index are not passed', () => {
const errorMessage = 'Pass either item or index to the item getter prop!'
expect(() =>
getItemAndIndex(undefined, undefined, [1, 2, 3], errorMessage),
).toThrow(errorMessage)
})
test('returns index if item is passed', () => {
const item = {}
expect(getItemAndIndex(item, undefined, [{x: 1}, item, {x: 2}])).toEqual([
item,
1,
])
})
test('returns item if index is passed', () => {
const index = 2
const item = {x: 2}
expect(getItemAndIndex(undefined, 2, [{x: 1}, {x: 3}, item])).toEqual([
item,
index,
])
})
})
test('getInitialValue will not return undefined as initial value', () => {
const defaults = {bogusValue: 'hello'}
const value = getInitialValue(
{initialBogusValue: undefined},
'bogusValue',
defaults,
)
expect(value).toEqual(defaults.bogusValue)
})
test('getInitialValue will not return undefined as value', () => {
const defaults = {bogusValue: 'hello'}
const value = getInitialValue(
{bogusValue: undefined},
'bogusValue',
defaults,
)
expect(value).toEqual(defaults.bogusValue)
})
test('getDefaultValue will not return undefined as value', () => {
const defaults = {bogusValue: 'hello'}
const value = getDefaultValue(
{defaultBogusValue: undefined},
'bogusValue',
defaults,
)
expect(value).toEqual(defaults.bogusValue)
})
describe('useMouseAndTouchTracker', () => {
test('renders without error', () => {
expect(() => {
renderHook(() => useMouseAndTouchTracker(undefined, jest.fn(), []))
}).not.toThrow()
})
test('adds and removes listeners to environment', () => {
const environment = {
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
}
const elements = [{}, {}]
const handleBlur = jest.fn()
const initialProps = {environment, handleBlur, elements}
const {unmount, rerender, result} = renderHook(
props =>
useMouseAndTouchTracker(
props.environment,
props.handleBlur,
props.elements,
),
{initialProps},
)
expect(environment.addEventListener).toHaveBeenCalledTimes(5)
expect(environment.removeEventListener).not.toHaveBeenCalled()
expect(environment.addEventListener.mock.calls).toMatchSnapshot(
'initial adding events',
)
environment.addEventListener.mockReset()
environment.removeEventListener.mockReset()
rerender(initialProps)
expect(environment.removeEventListener).not.toHaveBeenCalled()
expect(environment.addEventListener).not.toHaveBeenCalled()
rerender({...initialProps, elements: [...elements]})
expect(environment.addEventListener).toHaveBeenCalledTimes(5)
expect(environment.removeEventListener).toHaveBeenCalledTimes(5)
expect(environment.addEventListener.mock.calls).toMatchSnapshot(
'element change rerender adding events',
)
expect(environment.removeEventListener.mock.calls).toMatchSnapshot(
'element change rerender remove events',
)
environment.addEventListener.mockReset()
environment.removeEventListener.mockReset()
unmount()
expect(environment.addEventListener).not.toHaveBeenCalled()
expect(environment.removeEventListener).toHaveBeenCalledTimes(5)
expect(environment.removeEventListener.mock.calls).toMatchSnapshot(
'unmount remove events',
)
expect(result.current).toEqual({
isMouseDown: false,
isTouchMove: false,
isTouchEnd: false,
})
})
})
describe('isDropdownsStateEqual', () => {
test('is true when each property is equal', () => {
const selectedItem = 'hello'
const prevState = {
highlightedIndex: 2,
isOpen: true,
selectedItem,
inputValue: selectedItem,
}
const newState = {
...prevState,
}
expect(isDropdownsStateEqual(prevState, newState)).toBe(true)
})
test('is false when at least one property is not equal', () => {
const selectedItem = {value: 'hello'}
const prevState = {
highlightedIndex: 2,
isOpen: true,
selectedItem,
inputValue: selectedItem,
}
const newState = {
...prevState,
selectedItem: {...selectedItem},
}
expect(isDropdownsStateEqual(prevState, newState)).toBe(false)
})
})
})
================================================
FILE: src/hooks/index.ts
================================================
export {default as useSelect} from './useSelect'
export {default as useCombobox} from './useCombobox'
export {default as useMultipleSelection} from './useMultipleSelection'
export {default as useTagGroup} from './useTagGroup'
================================================
FILE: src/hooks/reducer.js
================================================
import {getHighlightedIndexOnOpen, getDefaultHighlightedIndex} from './utils'
import {getDefaultValue} from './utils-ts'
import {dropdownDefaultStateValues} from './utils.dropdown'
export default function downshiftCommonReducer(
state,
props,
action,
stateChangeTypes,
) {
const {type} = action
let changes
switch (type) {
case stateChangeTypes.ItemMouseMove:
changes = {
highlightedIndex: action.disabled ? -1 : action.index,
}
break
case stateChangeTypes.MenuMouseLeave:
changes = {
highlightedIndex: -1,
}
break
case stateChangeTypes.ToggleButtonClick:
case stateChangeTypes.FunctionToggleMenu:
changes = {
isOpen: !state.isOpen,
highlightedIndex: state.isOpen
? -1
: getHighlightedIndexOnOpen(props, state, 0),
}
break
case stateChangeTypes.FunctionOpenMenu:
changes = {
isOpen: true,
highlightedIndex: getHighlightedIndexOnOpen(props, state, 0),
}
break
case stateChangeTypes.FunctionCloseMenu:
changes = {
isOpen: false,
}
break
case stateChangeTypes.FunctionSetHighlightedIndex:
changes = {
highlightedIndex: props.isItemDisabled(
props.items[action.highlightedIndex],
action.highlightedIndex,
)
? -1
: action.highlightedIndex,
}
break
case stateChangeTypes.FunctionSetInputValue:
changes = {
inputValue: action.inputValue,
}
break
case stateChangeTypes.FunctionReset:
changes = {
highlightedIndex: getDefaultHighlightedIndex(props),
isOpen: getDefaultValue(props, 'isOpen', dropdownDefaultStateValues),
selectedItem: getDefaultValue(
props,
'selectedItem',
dropdownDefaultStateValues,
),
inputValue: getDefaultValue(
props,
'inputValue',
dropdownDefaultStateValues,
),
}
break
default:
throw new Error('Reducer called without proper action type.')
}
return {
...state,
...changes,
}
}
/* eslint-enable complexity */
================================================
FILE: src/hooks/testUtils.js
================================================
import React from 'react'
import {screen, act} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
export const items = [
'Neptunium',
'Plutonium',
'Americium',
'Curium',
'Berkelium',
'Californium',
'Einsteinium',
'Fermium',
'Mendelevium',
'Nobelium',
'Lawrencium',
'Rutherfordium',
'Dubnium',
'Seaborgium',
'Bohrium',
'Hassium',
'Meitnerium',
'Darmstadtium',
'Roentgenium',
'Copernicium',
'Nihonium',
'Flerovium',
'Moscovium',
'Livermorium',
'Tennessine',
'Oganesson',
]
export const dataTestIds = {
toggleButton: 'toggle-button-id',
menu: 'menu-id',
item: index => `item-id-${index}`,
input: 'input-id',
selectedItemPrefix: 'selected-item-id',
selectedItem: index => `selected-item-id-${index}`,
}
export const defaultIds = {
labelId: 'downshift-test-id-label',
menuId: 'downshift-test-id-menu',
getItemId: index => `downshift-test-id-item-${index}`,
toggleButtonId: 'downshift-test-id-toggle-button',
inputId: 'downshift-test-id-input',
}
export const waitForDebouncedA11yStatusUpdate = (shouldBeCleared = false) =>
act(() => jest.advanceTimersByTime(shouldBeCleared ? 700 : 200))
export const MemoizedItem = React.memo(function Item({
index,
item,
getItemProps,
stringItem,
...rest
}) {
return (
{stringItem}
)
})
export const user = userEvent.setup({delay: null})
export function getLabel() {
return screen.getByText(/choose an element/i)
}
export function getMenu() {
return screen.getByRole('listbox')
}
export function getToggleButton() {
return screen.getByTestId(dataTestIds.toggleButton)
}
export function getItemAtIndex(index) {
return getItems()[index]
}
export function getItems() {
return screen.queryAllByRole('option')
}
export async function clickOnItemAtIndex(index) {
await user.click(getItemAtIndex(index))
}
export async function clickOnToggleButton() {
await user.click(getToggleButton())
}
export async function mouseMoveItemAtIndex(index) {
await user.hover(getItemAtIndex(index))
}
export async function mouseLeaveItemAtIndex(index) {
await user.unhover(getItemAtIndex(index))
}
export async function keyDownOnToggleButton(keys) {
if (document.activeElement !== getToggleButton()) {
getToggleButton().focus()
}
await user.keyboard(keys)
}
export function getA11yStatusContainer() {
return screen.queryByRole('status')
}
export async function tab(shiftKey = false) {
await user.tab({shift: shiftKey})
}
// format is: [initialIsOpen, defaultIsOpen, props.isOpen]
export const initialFocusAndOpenTestCases = [
[undefined, undefined, true, true],
[true, true, true, true],
[true, false, true, true],
[false, true, true, true],
[false, false, true, true],
[true, undefined, undefined, true],
[true, false, undefined, true],
[true, true, undefined, true],
[undefined, true, undefined, true],
]
// format is: [initialIsOpen, defaultIsOpen, props.isOpen]
export const initialNoFocusOrOpenTestCases = [
[undefined, undefined, undefined, false],
[undefined, undefined, false, false],
[true, true, false, false],
[true, false, false, false],
[false, true, false, false],
[false, false, false, false],
[false, undefined, undefined, false],
[false, false, undefined, false],
[false, true, undefined, false],
[undefined, false, undefined, false],
]
================================================
FILE: src/hooks/useCombobox/README.md
================================================
# useCombobox
## The problem
You have a combobox or autocomplete dropdown in your application and you want it
to be accessible and functional. For consistency reasons you want it to follow
the [ARIA design pattern][combobox-aria-example] for a combobox. You also want
this solution to be simple to use and flexible so you can tailor it further to
your specific needs.
## This solution
`useCombobox` is a React hook that manages all the stateful logic needed to make
the combobox functional and accessible. It returns a set of props that are meant
to be called and their results destructured on the combobox's elements: its
label, toggle button, input, list and list items. The props are similar to the
ones provided by vanilla `` to the children render prop.
These props are called getter props and their return values are destructured as
a set of ARIA attributes and event listeners. Together with the action props and
state props, they create all the stateful logic needed for the combobox to
implement the corresponding ARIA pattern. Every functionality needed should be
provided out-of-the-box: menu toggle, item selection and up/down movement
between them, screen reader support, highlight by character keys etc.
## Types of Autocomplete
By default, our implementation and examples illustrate an autocomplete of type
_list_. This involves performing your own items filtering logic as well as
keeping the _aria_autocomplete_ value returned by the
[getInputProps](#getinputprops).
There are, in total, 3 types of autocomplete you can opt for, and these are as
follows:
- no autocomplete:
- [ARIA example][combobox-aria-example-none]
- use _aria-autocomplete="none"_ attribute to override the default value from
_getInputProps_.
- do not implement any filtering logic yourself, just render the listbox
items. Basically, take the [code example](#usage) below, remove the useState
with items, the onInputValueChange function, pass _colors_ as _items_ prop
and render the _colors_ if _isOpen_ is _true_.
- list autocomplete:
- [ARIA example][combobox-aria-example]
- just use the [example provided below](#usage) or anything equivalent.
- filtering logic inside the menu is done by the _useCombobox_ consumer.
- list and inline autocomplete:
- [ARIA example][combobox-aria-example-both]
- use _aria-autocomplete="both"_ attribute to override the default value from
_getInputProps_.
- filtering logic inside the menu is done by the _useCombobox_ consumer.
- inline autocomplete based on the highlighted item in the menu is also
performed by the consumer.
## Migration through breaking changes
The hook received breaking changes related to how it works, as well as the API,
starting with v7. They are documented here:
- [v7 migration guide][migration-guide-v7]
- [v8 migration guide][migration-guide-v8]
- [v9 migration guide][migration-guide-v9]
## Table of Contents
- [Usage](#usage)
- [Basic Props](#basic-props)
- [items](#items)
- [itemToString](#itemtostring)
- [onSelectedItemChange](#onselecteditemchange)
- [stateReducer](#statereducer)
- [Advanced Props](#advanced-props)
- [isItemDisabled](#isitemdisabled)
- [initialSelectedItem](#initialselecteditem)
- [initialIsOpen](#initialisopen)
- [initialHighlightedIndex](#initialhighlightedindex)
- [initialInputValue](#initialinputvalue)
- [defaultSelectedItem](#defaultselecteditem)
- [defaultIsOpen](#defaultisopen)
- [defaultHighlightedIndex](#defaulthighlightedindex)
- [defaultInputValue](#defaultinputvalue)
- [itemToKey](#itemtokey)
- [getA11yStatusMessage](#geta11ystatusmessage)
- [onHighlightedIndexChange](#onhighlightedindexchange)
- [onIsOpenChange](#onisopenchange)
- [onInputValueChange](#oninputvaluechange)
- [onStateChange](#onstatechange)
- [highlightedIndex](#highlightedindex)
- [isOpen](#isopen)
- [selectedItem](#selecteditem)
- [inputValue](#inputvalue)
- [id](#id)
- [labelId](#labelid)
- [menuId](#menuid)
- [toggleButtonId](#togglebuttonid)
- [inputId](#inputid)
- [getItemId](#getitemid)
- [environment](#environment)
- [stateChangeTypes](#statechangetypes)
- [Control Props](#control-props)
- [Returned props](#returned-props)
- [prop getters](#prop-getters)
- [actions](#actions)
- [state](#state)
- [Event Handlers](#event-handlers)
- [Default handlers](#default-handlers)
- [Customizing Handlers](#customizing-handlers)
- [Examples](#examples)
## Usage
> [Try it out in the browser][sandbox-example]
```jsx
import * as React from 'react'
import {render} from 'react-dom'
import {useCombobox} from 'downshift'
const colors = [
'Black',
'Red',
'Green',
'Blue',
'Orange',
'Purple',
'Pink',
'Orchid',
'Aqua',
'Lime',
'Gray',
'Brown',
'Teal',
'Skyblue',
]
function DropdownCombobox() {
const [inputItems, setInputItems] = React.useState(colors)
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
highlightedIndex,
getItemProps,
selectedItem,
selectItem,
} = useCombobox({
items: inputItems,
onInputValueChange: ({inputValue}) => {
setInputItems(
colors.filter(item =>
item.toLowerCase().startsWith(inputValue.toLowerCase()),
),
)
},
})
return (
)
}
render( , document.getElementById('root'))
```
## Basic Props
This is the list of props that you should probably know about. There are some
[advanced props](#advanced-props) below as well.
### items
> `any[]` | _required_
The main difference from vanilla `Downshift` is that we pass the items we want
to render to the hook as well. Opening the menu with an item already selected
means the hook has to know in advance what items you plan to render and what is
the position of that item in the list. Consequently, there won't be any need for
two state changes: one for opening the menu and one for setting the highlighted
index, like in `Downshift`.
### itemToString
> `function(item: any)` | defaults to: `item => (item ? String(item) : '')`
If your items are stored as, say, objects instead of strings, downshift still
needs a string representation for each one. This is required for accessibility
aria-live messages (e.g., after making a selection).
**Note:** This callback _must_ include a null check: it is invoked with `null`
whenever the user abandons input via ``.
### onSelectedItemChange
> `function(changes: object)` | optional, no useful default
Called each time the selected item was changed. Selection can be performed by
item click, Enter Key while item is highlighted or by blurring the menu while an
item is highlighted (Tab, Shift-Tab or clicking away).
- `changes`: These are the properties that actually have changed since the last
state change. This object is guaranteed to contain the `selectedItem` property
with the newly selected value. This also has a `type` property which you can
learn more about in the [`stateChangeTypes`](#statechangetypes) section. This
property will be part of the actions that can trigger a `selectedItem` change,
for example `useCombobox.stateChangeTypes.ItemClick`.
### stateReducer
> `function(state: object, actionAndChanges: object)` | optional
**🚨 This is a really handy power feature 🚨**
This function will be called each time `useCombobox` sets its internal state (or
calls your `onStateChange` handler for control props). It allows you to modify
the state change that will take place which can give you fine grain control over
how the component interacts with user updates. It gives you the current state
and the state that will be set, and you return the state that you want to set.
- `state`: The full current state of downshift.
- `actionAndChanges`: Object that contains the action `type`, props needed to
return a new state based on that type and the changes suggested by the
Downshift default reducer. About the `type` property you can learn more about
in the [`stateChangeTypes`](#statechangetypes) section.
```javascript
import {useCombobox} from 'downshift'
import {items} from './utils'
const {getMenuProps, getItemProps, ...rest} = useCombobox({
items,
stateReducer,
})
function stateReducer(state, actionAndChanges) {
const {type, changes} = actionAndChanges
// this prevents the menu from being closed when the user selects an item with 'Enter' or mouse
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick:
return {
...changes, // default Downshift new state changes on item selection.
isOpen: state.isOpen, // but keep menu open.
highlightedIndex: state.highlightedIndex, // with the item highlighted.
}
default:
return changes // otherwise business as usual.
}
}
```
> NOTE: This is only called when state actually changes. You should not attempt
> use this to handle events. If you wish to handle events, put your event
> handlers directly on the elements (make sure to use the prop getters though!
> For example ` ` should be
> ` `). Also, your
> reducer function should be "pure." This means it should do nothing other than
> return the state changes you want to have happen.
## Advanced Props
### isItemDisabled
> `function(item: any, index: number)` | defaults to: `(_item, _index) => false`
If an item needs to be marked as disabled, this function needs to return `true`
for that item. Disabled items will be skipped from keyboard navigation, will not
be selected and will be marked as disabled for screen readers.
### initialSelectedItem
> `any` | defaults to `null`
Pass an item that should be selected when downshift is initialized.
### initialIsOpen
> `boolean` | defaults to `false`
Pass a boolean that sets the open state of the menu when downshift is
initialized.
### initialHighlightedIndex
> `number` | defaults to `-1`
Pass a number that sets the index of the highlighted item when downshift is
initialized.
### initialInputValue
> `string` | defaults to `''`
Pass a string that sets the content of the input when downshift is initialized.
### defaultSelectedItem
> `any` | defaults to `null`
Pass an item that should be selected when downshift is reset.
### defaultIsOpen
> `boolean` | defaults to `false`
Pass a boolean that sets the open state of the menu when downshift is reset or
when an item is selected.
### defaultHighlightedIndex
> `number` | defaults to `-1`
Pass a number that sets the index of the highlighted item when downshift is
reset or when an item is selected.
### defaultInputValue
> `string` | defaults to `''`
Pass a string that sets the content of the input when downshift is reset or when
an item is selected.
### itemToKey
> `function(item: any)` | defaults to: `item => item`
Used to determine the uniqueness of an item when searching for the item or
comparing the item with another. Returns the item itself, by default, so the
comparing/searching is done internally via referential equality.
If using items as objects and their reference will change during use, you can
use the function to generate a unique key for each item, such as an `id` prop.
```js
function itemToKey(item) {
return item.id
}
```
> This deprecates the "selectedItemChanged" prop. If you are using the prop
> already, make sure you change to "itemToKey" as the former is removed in v9. A
> migration example:
```js
// initial items.
const items = [
{id: 1, value: 'Apples'},
{id: 2, value: 'Oranges'},
]
// the same items but with different references, for any reason.
const newItems = [
{id: 1, value: 'Apples'},
{id: 2, value: 'Oranges'},
]
// previously, if you probably had something like this.
function selectedItemChanged(item1, item2) {
return item1.id === item2.id
}
// moving forward, switch to this one.
function itemToKey(item) {
return item.id
// and we will do the comparison like: const isChanged = itemToKey(prevSelectedItem) !== itemToKey(nextSelectedItem)
}
```
### getA11yStatusMessage
> `function({/* see below */})` | default messages provided in English
This function is passed as props to a status updating function nested within
that allows you to create your own ARIA statuses. It is called when the state
changes: `selectedItem`, `highlightedIndex`, `inputValue` or `isOpen`.
There is no default function provided anymore since v9, so if there's no prop
passed, no aria live status message is created. An implementation that resembles
the previous default is written below, should you want to keep pre v9 behaviour.
We don't provide this as a default anymore since we consider that screen readers
have been significantly improved and they can convey information about items
count, possible actions and highlighted items only from the HTML markup, without
the need for aria-live regions.
```js
function getA11yStatusMessage(state) {
if (!state.isOpen) {
return ''
}
// you need to get resultCount and previousResultCount yourself now, since we don't pass them as arguments anymore
const resultCount = items.length
const previousResultCount = previousResultCountRef.current
if (!resultCount) {
return 'No results are available.'
}
if (resultCount !== previousResultCount) {
return `${resultCount} result${
resultCount === 1 ? ' is' : 's are'
} available, use up and down arrow keys to navigate. Press Enter key to select.`
}
return ''
}
```
### onHighlightedIndexChange
> `function(changes: object)` | optional, no useful default
Called each time the highlighted item was changed. Items can be highlighted
while hovering the mouse over them or by keyboard keys such as Up Arrow, Down
Arrow, Home and End. Items can also be highlighted by hitting character keys
that are part of their starting string equivalent.
- `changes`: These are the properties that actually have changed since the last
state change. This object is guaranteed to contain the `highlightedIndex`
property with the new value. This also has a `type` property which you can
learn more about in the [`stateChangeTypes`](#statechangetypes) section. This
property will be part of the actions that can trigger a `highlightedIndex`
change, for example `useCombobox.stateChangeTypes.InputKeyDownArrowUp`.
### onIsOpenChange
> `function(changes: object)` | optional, no useful default
Called each time the menu is open or closed. Menu can be open by toggle button
click, Enter, Space, Up Arrow or Down Arrow keys. Can be closed by selecting an
item, blur (Tab, Shift-Tab or clicking outside), clicking the toggle button
again or hitting Escape key.
- `changes`: These are the properties that actually have changed since the last
state change. This object is guaranteed to contain the `isOpen` property with
the new value. This also has a `type` property which you can learn more about
in the [`stateChangeTypes`](#statechangetypes) section. This property will be
part of the actions that can trigger a `isOpen` change, for example
`useCombobox.stateChangeTypes.ToggleButtonClick`.
### onInputValueChange
> `function(changes: object)` | optional, no useful default
Called each time the value in the input text changes. The input value should
change like any input of type text, at any character key press, `Space`,
`Backspace`, `Escape` etc.
- `changes`: These are the properties that actually have changed since the last
state change. This object is guaranteed to contain the `inputValue` property
with the new value. This also has a `type` property which you can learn more
about in the [`stateChangeTypes`](#statechangetypes) section. This property
will be part of the actions that can trigger a `inputValue` change, for
example `useCombobox.stateChangeTypes.InputChange`.
### onStateChange
> `function(changes: object)` | optional, no useful default
This function is called anytime the internal state changes. This can be useful
if you're using downshift as a "controlled" component, where you manage some or
all of the state (e.g., isOpen, selectedItem, highlightedIndex, etc) and then
pass it as props, rather than letting downshift control all its state itself.
- `changes`: These are the properties that actually have changed since the last
state change. This also has a `type` property which you can learn more about
in the [`stateChangeTypes`](#statechangetypes) section.
> Tip: This function will be called any time _any_ state is changed. The best
> way to determine whether any particular state was changed, you can use
> `changes.hasOwnProperty('propName')` or use the `on[statePropKey]Change` props
> described above.
> NOTE: This is only called when state actually changes. You should not attempt
> to use this to handle events. If you wish handle events, put your event
> handlers directly on the elements (make sure to use the prop getters though!
> For example: ` ` should be
> ` `).
### highlightedIndex
> `number` | **control prop** (read more about this in
> [the Control Props section](#control-props))
The index of the item that should be highlighted when menu is open.
### isOpen
> `boolean` | **control prop** (read more about this in
> [the Control Props section](#control-props))
The open state of the menu.
### selectedItem
> `any` | **control prop** (read more about this in
> [the Control Props section](#control-props))
The item that should be selected.
### inputValue
> `string` | **control prop** (read more about this in
> [the Control Props section](#control-props))
The value to be displayed in the text input.
🚨 Important 🚨
If you use `onInputValueChange`, `onStateChange` or anything similar in order to
update a state variable that will end up controlling `inputValue`, you will
encounter a
[cursor jump issue](https://github.com/downshift-js/downshift/issues/1108).
There's no way to properly fix this in our current `React.useReducer` setup, so
in order to work around the issue, consider the change below.
```jsx
const [value, setValue] = useState('')
const {getInputProps} = useCombobox({
items: [],
inputValue: value,
// change this:
onInputValueChange: ({inputValue}) => {
setValue(inputValue)
},
})
return (
{
setValue(e.target.value)
},
})}
/>
)
```
### id
> `string` | defaults to a generated ID
Used to generate the first part of the `Downshift` id on the elements. You can
override this `id` with one of your own, provided as a prop, or you can override
the `id` for each element altogether using the props below.
### labelId
> `string` | defaults to a generated ID
Used for `aria` attributes and the `id` prop of the element (`label`) you use
[`getLabelProps`](#getlabelprops) with.
### menuId
> `string` | defaults to a generated ID
Used for `aria` attributes and the `id` prop of the element (`ul`) you use
[`getMenuProps`](#getmenuprops) with.
### toggleButtonId
> `string` | defaults to a generated ID
Used for `aria` attributes and the `id` prop of the element (`button`) you use
[`getToggleButtonProps`](#gettogglebuttonprops) with.
### inputId
> `string` | defaults to a generated ID
Used for `aria` attributes and the `id` prop of the element (`input`) you use
[`getInputProps`](#getmenuprops) with.
### getItemId
> `function(index)` | defaults to a function that generates an ID based on the
> index
Used for `aria` attributes and the `id` prop of the element (`li`) you use
[`getItemProps`](#getitemprops) with.
### environment
> `window` | defaults to `window`
This prop is only useful if you're rendering downshift within a different
`window` context from where your JavaScript is running; for example, an iframe
or a shadow-root. If the given context is lacking `document` and/or
`add|removeEventListener` on its prototype (as is the case for a shadow-root)
then you will need to pass in a custom object that is able to provide
[access to these properties](https://gist.github.com/Rendez/1dd55882e9b850dd3990feefc9d6e177)
for downshift.
## stateChangeTypes
There are a few props that expose changes to state
([`onStateChange`](#onstatechange) and [`stateReducer`](#statereducer)). For you
to make the most of these APIs, it's important for you to understand why state
is being changed. To accomplish this, there's a `type` property on the `changes`
object you get. This `type` corresponds to a `stateChangeTypes` property.
The list of all possible values this `type` property can take is defined in
[this file][state-change-file] and is as follows:
- `useCombobox.stateChangeTypes.InputKeyDownArrowDown`
- `useCombobox.stateChangeTypes.InputKeyDownArrowUp`
- `useCombobox.stateChangeTypes.InputKeyDownEscape`
- `useCombobox.stateChangeTypes.InputKeyDownHome`
- `useCombobox.stateChangeTypes.InputKeyDownEnd`
- `useCombobox.stateChangeTypes.InputKeyDownPageUp`
- `useCombobox.stateChangeTypes.InputKeyDownPadeDown`
- `useCombobox.stateChangeTypes.InputKeyDownEnter`
- `useCombobox.stateChangeTypes.InputChange`
- `useCombobox.stateChangeTypes.InputClick`
- `useCombobox.stateChangeTypes.InputBlur`
- `useCombobox.stateChangeTypes.MenuMouseLeave`
- `useCombobox.stateChangeTypes.ItemMouseMove`
- `useCombobox.stateChangeTypes.ItemClick`
- `useCombobox.stateChangeTypes.ToggleButtonClick`
- `useCombobox.stateChangeTypes.FunctionToggleMenu`
- `useCombobox.stateChangeTypes.FunctionOpenMenu`
- `useCombobox.stateChangeTypes.FunctionCloseMenu`
- `useCombobox.stateChangeTypes.FunctionSetHighlightedIndex`
- `useCombobox.stateChangeTypes.FunctionSelectItem`
- `useCombobox.stateChangeTypes.FunctionSetInputValue`
- `useCombobox.stateChangeTypes.FunctionReset`
See [`stateReducer`](#statereducer) for a concrete example on how to use the
`type` property.
## Control Props
Downshift manages its own state internally and calls your
`onSelectedItemChange`, `onIsOpenChange`, `onHighlightedIndexChange`,
`onInputChange` and `onStateChange` handlers with any relevant changes. The
state that downshift manages includes: `isOpen`, `selectedItem`, `inputValue`
and `highlightedIndex`. Returned action function (read more below) can be used
to manipulate this state and can likely support many of your use cases.
However, if more control is needed, you can pass any of these pieces of state as
a prop (as indicated above) and that state becomes controlled. As soon as
`this.props[statePropKey] !== undefined`, internally, `downshift` will determine
its state based on your prop's value rather than its own internal state. You
will be required to keep the state up to date (this is where `onStateChange`
comes in really handy), but you can also control the state from anywhere, be
that state from other components, `redux`, `react-router`, or anywhere else.
> Note: This is very similar to how normal controlled components work elsewhere
> in react (like ` `). If you want to learn more about this concept, you
> can learn about that from the [Advanced React Component Patterns
> course][advanced-react-component-patterns-course]
## Returned props
You use the hook like so:
```javascript
import {useCombobox} from 'downshift'
import {items} from './utils'
const {getInputProps, reset, ...rest} = useCombobox({
items,
...otherProps,
})
return (
{/* render the menu and items */}
{/* render a button that resets the select to defaults */}
{
reset()
}}
>
Reset
)
```
> NOTE: In this example we used both a getter prop `getInputProps` and an action
> prop `reset`. The properties of `useCombobox` can be split into three
> categories as indicated below:
### prop getters
> See [the blog post about prop getters][blog-post-prop-getters]
> NOTE: These prop-getters provide `aria-` attributes which are very important
> to your component being accessible. It's recommended that you utilize these
> functions and apply the props they give you to your components.
These functions are used to apply props to the elements that you render. This
gives you maximum flexibility to render what, when, and wherever you like. You
call these on the element in question, for example on the toggle button:
`
| property | type | description |
| ---------------------- | -------------- | ---------------------------------------------------------------------------------------------- |
| `getToggleButtonProps` | `function({})` | returns the props you should apply to any menu toggle button element you render. |
| `getItemProps` | `function({})` | returns the props you should apply to any menu item elements you render. |
| `getLabelProps` | `function({})` | returns the props you should apply to the `label` element that you render. |
| `getMenuProps` | `function({})` | returns the props you should apply to the `ul` element (or root of your menu) that you render. |
| `getInputProps` | `function({})` | returns the props you should apply to the `input` element that you render. |
#### `getLabelProps`
This method should be applied to the `label` you render. It will generate an
`id` that will be used to label the toggle button and the menu.
There are no required properties for this method.
> Note: For accessibility purposes, calling this method is highly recommended.
#### `getMenuProps`
This method should be applied to the element which contains your list of items.
Typically, this will be a `` or a `
` that surrounds a `map` expression.
This handles the proper ARIA roles and attributes.
Optional properties:
- `refKey`: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call this `innerRef`. So you'd call: `getMenuProps({refKey: 'innerRef'})` and
your composite component would forward like: ``.
However, if you are just rendering a primitive component like ``, there
is no need to specify this property. It defaults to `ref`.
Please keep in mind that menus, for accessiblity purposes, should always be
rendered, regardless of whether you hide it or not. Otherwise, `getMenuProps`
may throw error if you unmount and remount the menu.
- `aria-label`: By default the menu will add an `aria-labelledby` that refers to
the `
` rendered with `getLabelProps`. However, if you provide
`aria-label` to give a more specific label that describes the options
available, then `aria-labelledby` will not be provided and screen readers can
use your `aria-label` instead.
In some cases, you might want to completely bypass the `refKey` check. Then you
can provide the object `{suppressRefError : true}` as the second argument to
`getMenuProps`. **Please use it with extreme care and only if you are absolutely
sure that the ref is correctly forwarded otherwise `useCombobox` will
unexpectedly fail.**
```jsx
const {getMenuProps} = useCombobox({items})
const ui = (
{!isOpen
? null
: items.map((item, index) => (
{item.name}
))}
)
```
> Note that for accessibility reasons it's best if you always render this
> element whether or not downshift is in an `isOpen` state.
#### `getItemProps`
The props returned from calling this function should be applied to any menu
items you render.
**This is an impure function**, so it should only be called when you will
actually be applying the props to an item.
What do you mean by impure function?
Basically just don't do this:
```jsx
items.map((item, index) => {
const props = getItemProps({item, index}) // we're calling it here
if (!shouldRenderItem(item)) {
return null // but we're not using props, and downshift thinks we are...
}
return
})
```
Instead, you could do this:
```jsx
items.filter(shouldRenderItem).map(item =>
)
```
Required properties:
The main difference from vanilla `Downshift` is that we require the items as
props before rendering. The reason is to open the menu with items already
highlighted, and we need to know the items before the actual render. It is still
required to pass either `item` or `index` to `getItemProps`.
- `item`: this is the item data that will be selected when the user selects a
particular item.
- `index`: This is how `downshift` keeps track of your item when updating the
`highlightedIndex` as the user keys around. By default, `downshift` will
assume the `index` is the order in which you're calling `getItemProps`. This
is often good enough, but if you find odd behavior, try setting this
explicitly. It's probably best to be explicit about `index` when using a
windowing library like `react-virtualized`.
Optional properties:
- `ref`: if you need to access the item element via a ref object, you'd call the
function like this: `getItemProps({ref: yourItemRef})`. As a result, the item
element will receive a composed `ref` property, which guarantees that both
your code and `useCombobox` use the same correct reference to the element.
- `refKey`: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call this `innerRef`. So you'd call: `getItemProps({refKey: 'innerRef'})` and
your composite component would forward like: ` `.
However, if you are just rendering a primitive component like ``, there
is no need to specify this property. It defaults to `ref`.
#### `getToggleButtonProps`
Call this and apply the returned props to a `button`. It allows you to toggle
the `Menu` component.
Optional properties:
- `ref`: if you need to access the button element via a ref object, you'd call
the function like this: `getToggleButton({ref: yourButtonRef})`. As a result,
the button element will receive a composed `ref` property, which guarantees
that both your code and `useCombobox` use the same correct reference to the
element.
- `refKey`: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call this `innerRef`. So you'd call: `getToggleButton({refKey: 'innerRef'})`
and your composite component would forward like:
`
`. However, if you are just rendering a
primitive component like `
`, there is no need to specify this property.
It defaults to `ref`.
- `disabled`: If this is set to `true`, then all of the downshift button event
handlers will be omitted (it won't toggle the menu when clicked).
```jsx
const {getToggleButtonProps} = useCombobox({items})
const myButton = (
Click me
{/* menu and items */}
)
```
#### `getInputProps`
This method should be applied to the `input` you render. It is recommended that
you pass all props as an object to this method which will compose together any
of the event handlers you need to apply to the `input` while preserving the ones
that `downshift` needs to apply to make the `input` behave.
There are no required properties for this method.
Optional properties:
- `disabled`: If this is set to true, then no event handlers will be returned
from `getInputProps` and a `disabled` prop will be returned (effectively
disabling the input).
- `ref`: if you need to access the input element via a ref object, you'd call
the function like this: `getInputProps({ref: yourInputRef})`. As a result, the
input element will receive a composed `ref` property, which guarantees that
both your code and `useCombobox` use the same correct reference to the
element.
- `refKey`: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call this `innerRef`. So you'd call: `getInputProps({refKey: 'innerRef'})` and
your composite component would forward like: `
`.
However, if you are just rendering a primitive component like `
`, there
is no need to specify this property. It defaults to `ref`.
- `aria-label`: By default the input will add an `aria-labelledby` that refers
to the `
` rendered with `getLabelProps`. However, if you provide
`aria-label` to give a more specific label that describes the options
available, then `aria-labelledby` will not be provided and screen readers can
use your `aria-label` instead.
In some cases, you might want to completely bypass the `refKey` check. Then you
can provide the object `{suppressRefError : true}` as the second argument to
`getInput`. **Please use it with extreme care and only if you are absolutely
sure that the ref is correctly forwarded otherwise `useCombobox` will
unexpectedly fail.**
### actions
These are functions you can call to change the state of the downshift
`useCombobox` hook.
| property | type | description |
| --------------------- | ------------------------- | ----------------------------------------------------- |
| `closeMenu` | `function()` | closes the menu |
| `openMenu` | `function()` | opens the menu |
| `selectItem` | `function(item: any)` | selects the given item |
| `setHighlightedIndex` | `function(index: number)` | call to set a new highlighted index |
| `setInputValue` | `function(value: string)` | call to set a new value in the input |
| `toggleMenu` | `function()` | toggle the menu open state |
| `reset` | `function()` | this resets downshift's state to a reasonable default |
### state
These are values that represent the current state of the downshift component.
| property | type | description |
| ------------------ | --------- | --------------------------------- |
| `highlightedIndex` | `number` | the currently highlighted item |
| `isOpen` | `boolean` | the menu open state |
| `selectedItem` | `any` | the currently selected item input |
| `inputValue` | `string` | the value in the input |
## Event Handlers
Downshift has a few events for which it provides implicit handlers. Several of
these handlers call `event.preventDefault()`. Their additional functionality is
described below.
### Default handlers
#### Toggle Button
- `Click`: If the menu is not displayed, it will open it. Otherwise it will
close it. It will additionally move focus on the input in order for screen
readers to correctly narrate which item is currently highlighted. If there is
already an item selected, the menu will be opened with that item already
highlighted.
- `Enter`: Has the same effect as `Click`. Button not in the tab order by
default.
- `Space`: Has the same effect as `Click`. Button not in the tab order by
default.
#### Input
- `ArrowDown`: Moves `highlightedIndex` one position down. When reaching the
last option, `ArrowDown` will move `highlightedIndex` to first position.
- `ArrowUp`: Moves `highlightedIndex` one position up. When reaching the first
option, `ArrowUp` will move `highlightedIndex` to last position.
- `Alt+ArrowDown`: If the menu is closed, it will open it, without highlighting
any item.
- `Alt+ArrowUp`: If the menu is open, it will close it and will select the item
that was highlighted.
- `CharacterKey`: Will change the `inputValue` according to the value visible in
the ` `. `Backspace` or `Space` trigger the same event.
- `End`: If the menu is open, it will highlight the last item in the list.
- `Home`: If the menu is open, it will highlight the first item in the list.
- `PageUp`: If the menu is open, it will move the highlight the item 10
positions before the current selection.
- `PageDown`: If the menu is open, it will move the highlight the item 10
positions after the current selection.
- `Enter`: If there is a highlighted option, it will select it and close the
menu.
- `Escape`: It will close the menu if open. If the menu is closed, it will clear
selection: the value in the `input` will become an empty string and the item
stored as `selectedItem` will become `null`.
- `Click`: If the menu is closed, it will open it. If the menu is open, it will
close it.
- `Blur(Tab, Shift+Tab)`: It will close the menu and select the highlighted
item, if any. The focus will move naturally to the next/previous element in
the Tab order.
- `Blur(mouse click outside)`: It will close the menu without selecting any
element, even if there is one highlighted.
#### Menu
- `MouseLeave`: Will clear the value of the `highlightedIndex` if it was set.
#### Item
- `Click`: It will select the item, close the menu and move focus to the toggle
button (unless `defaultIsOpen` is true).
- `MouseOver`: It will highlight the item.
### Customizing Handlers
You can provide your own event handlers to `useCombobox` which will be called
before the default handlers:
```javascript
const items = [...] // items here.
const {getMenuProps} = useCombobox({items})
const ui = (
/* button, label, ... */
{
// your custom keyDown handler here.
},
})}
/>
)
```
If you would like to prevent the default handler behavior in some cases, you can
set the event's `preventDownshiftDefault` property to `true`:
```javascript
const {getMenuProps} = useCombobox({items})
const ui = (
/* button, label, ... */
{
// your custom keyDown handler here.
if (event.key === 'Enter') {
// Prevent Downshift's default 'Enter' behavior.
event.nativeEvent.preventDownshiftDefault = true
// your handler code
}
},
})}
/>
)
```
If you would like to completely override Downshift's behavior for a handler, in
favor of your own, you can bypass prop getters:
```javascript
const items = [...] // items here.
const {getMenuProps} = useCombobox({items})
const ui = (
/* button, label, ... */