From c7539fb1dd00bfbad820b27a3937436f568e83ea Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 24 Sep 2020 02:02:43 +0100 Subject: [PATCH 1/6] [Autocomplete] Move from lab to core --- docs/pages/api-docs/autocomplete.md | 8 +- docs/src/pages.js | 2 +- .../components/autocomplete/Asynchronous.js | 2 +- .../components/autocomplete/Asynchronous.tsx | 2 +- .../components/autocomplete/CheckboxesTags.js | 2 +- .../autocomplete/CheckboxesTags.tsx | 2 +- .../pages/components/autocomplete/ComboBox.js | 2 +- .../components/autocomplete/ComboBox.tsx | 2 +- .../autocomplete/ControllableStates.js | 2 +- .../autocomplete/ControllableStates.tsx | 2 +- .../components/autocomplete/CountrySelect.js | 2 +- .../components/autocomplete/CountrySelect.tsx | 2 +- .../autocomplete/CustomInputAutocomplete.js | 2 +- .../autocomplete/CustomInputAutocomplete.tsx | 2 +- .../components/autocomplete/CustomizedHook.js | 2 +- .../autocomplete/CustomizedHook.tsx | 2 +- .../autocomplete/DisabledOptions.js | 2 +- .../autocomplete/DisabledOptions.tsx | 2 +- .../pages/components/autocomplete/Filter.js | 2 +- .../pages/components/autocomplete/Filter.tsx | 2 +- .../components/autocomplete/FixedTags.js | 2 +- .../components/autocomplete/FixedTags.tsx | 2 +- .../pages/components/autocomplete/FreeSolo.js | 2 +- .../components/autocomplete/FreeSolo.tsx | 2 +- .../autocomplete/FreeSoloCreateOption.js | 2 +- .../autocomplete/FreeSoloCreateOption.tsx | 2 +- .../FreeSoloCreateOptionDialog.js | 2 +- .../FreeSoloCreateOptionDialog.tsx | 2 +- .../components/autocomplete/GitHubLabel.js | 2 +- .../components/autocomplete/GitHubLabel.tsx | 2 +- .../components/autocomplete/GoogleMaps.js | 2 +- .../components/autocomplete/GoogleMaps.tsx | 2 +- .../pages/components/autocomplete/Grouped.js | 2 +- .../pages/components/autocomplete/Grouped.tsx | 2 +- .../components/autocomplete/Highlights.js | 2 +- .../components/autocomplete/Highlights.tsx | 2 +- .../components/autocomplete/LimitTags.js | 2 +- .../components/autocomplete/LimitTags.tsx | 2 +- .../components/autocomplete/Playground.js | 2 +- .../components/autocomplete/Playground.tsx | 2 +- .../pages/components/autocomplete/Sizes.js | 2 +- .../pages/components/autocomplete/Sizes.tsx | 2 +- .../src/pages/components/autocomplete/Tags.js | 2 +- .../pages/components/autocomplete/Tags.tsx | 2 +- .../autocomplete/UseAutocomplete.js | 2 +- .../autocomplete/UseAutocomplete.tsx | 2 +- .../components/autocomplete/Virtualize.js | 2 +- .../components/autocomplete/Virtualize.tsx | 2 +- .../autocomplete/autocomplete-de.md | 4 +- .../autocomplete/autocomplete-es.md | 4 +- .../autocomplete/autocomplete-fr.md | 4 +- .../autocomplete/autocomplete-ja.md | 4 +- .../autocomplete/autocomplete-pt.md | 4 +- .../autocomplete/autocomplete-ru.md | 4 +- .../autocomplete/autocomplete-zh.md | 4 +- .../components/autocomplete/autocomplete.md | 5 +- docs/src/pages/guides/localization/Locales.js | 2 +- .../src/pages/guides/localization/Locales.tsx | 2 +- .../pages/guides/migration-v4/migration-v4.md | 9 + .../src/Autocomplete/Autocomplete.d.ts | 279 +-- .../src/Autocomplete/Autocomplete.js | 898 +------ .../src/Autocomplete/Autocomplete.test.js | 2070 ----------------- .../material-ui-lab/src/Autocomplete/index.js | 2 +- packages/material-ui-lab/src/index.d.ts | 3 + .../src/internal/svg-icons/ArrowDropDown.js | 7 - .../src/internal/svg-icons/Close.js | 10 - .../src/themeAugmentation/components.d.ts | 4 - .../src/themeAugmentation/overrides.d.ts | 2 - .../src/themeAugmentation/props.d.ts | 2 - .../src/themeAugmentation/props.spec.ts | 18 - .../src/useAutocomplete/index.d.ts | 4 +- .../src/useAutocomplete/index.js | 2 +- .../src/Autocomplete/Autocomplete.d.ts | 277 +++ .../src/Autocomplete/Autocomplete.js | 880 +++++++ .../src/Autocomplete/Autocomplete.spec.tsx | 0 .../src/Autocomplete/Autocomplete.test.js | 2070 +++++++++++++++++ .../material-ui/src/Autocomplete/index.d.ts | 2 + .../material-ui/src/Autocomplete/index.js | 1 + packages/material-ui/src/index.d.ts | 6 + packages/material-ui/src/index.js | 6 + .../material-ui/src/styles/overrides.d.ts | 2 + packages/material-ui/src/styles/props.d.ts | 2 + .../src/useAutocomplete/index.d.ts | 2 + .../material-ui/src/useAutocomplete/index.js | 1 + .../src/useAutocomplete/useAutocomplete.d.ts | 0 .../src/useAutocomplete/useAutocomplete.js | 0 .../useAutocomplete/useAutocomplete.spec.ts | 0 .../useAutocomplete/useAutocomplete.test.js | 0 88 files changed, 3357 insertions(+), 3341 deletions(-) delete mode 100644 packages/material-ui-lab/src/internal/svg-icons/ArrowDropDown.js delete mode 100644 packages/material-ui-lab/src/internal/svg-icons/Close.js create mode 100644 packages/material-ui/src/Autocomplete/Autocomplete.d.ts create mode 100644 packages/material-ui/src/Autocomplete/Autocomplete.js rename packages/{material-ui-lab => material-ui}/src/Autocomplete/Autocomplete.spec.tsx (100%) create mode 100644 packages/material-ui/src/Autocomplete/Autocomplete.test.js create mode 100644 packages/material-ui/src/Autocomplete/index.d.ts create mode 100644 packages/material-ui/src/Autocomplete/index.js create mode 100644 packages/material-ui/src/useAutocomplete/index.d.ts create mode 100644 packages/material-ui/src/useAutocomplete/index.js rename packages/{material-ui-lab => material-ui}/src/useAutocomplete/useAutocomplete.d.ts (100%) rename packages/{material-ui-lab => material-ui}/src/useAutocomplete/useAutocomplete.js (100%) rename packages/{material-ui-lab => material-ui}/src/useAutocomplete/useAutocomplete.spec.ts (100%) rename packages/{material-ui-lab => material-ui}/src/useAutocomplete/useAutocomplete.test.js (100%) diff --git a/docs/pages/api-docs/autocomplete.md b/docs/pages/api-docs/autocomplete.md index 93cc13d2a66efa..20ee121743e3a7 100644 --- a/docs/pages/api-docs/autocomplete.md +++ b/docs/pages/api-docs/autocomplete.md @@ -1,5 +1,5 @@ --- -filename: /packages/material-ui-lab/src/Autocomplete/Autocomplete.js +filename: /packages/material-ui/src/Autocomplete/Autocomplete.js --- @@ -11,9 +11,9 @@ filename: /packages/material-ui-lab/src/Autocomplete/Autocomplete.js ## Import ```js -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; // or -import { Autocomplete } from '@material-ui/lab'; +import { Autocomplete } from '@material-ui/core'; ``` You can learn more about the difference by [reading this guide](/guides/minimizing-bundle-size/). @@ -127,7 +127,7 @@ You can override the style of the component thanks to one of these customization - With a [global class name](/customization/components/#overriding-styles-with-global-class-names). - With a theme and an [`overrides` property](/customization/globals/#css). -If that's not sufficient, you can check the [implementation of the component](https://github.com/mui-org/material-ui/blob/next/packages/material-ui-lab/src/Autocomplete/Autocomplete.js) for more detail. +If that's not sufficient, you can check the [implementation of the component](https://github.com/mui-org/material-ui/blob/next/packages/material-ui/src/Autocomplete/Autocomplete.js) for more detail. ## Demos diff --git a/docs/src/pages.js b/docs/src/pages.js index 31263dedf5cb11..9e5b3ebd70430a 100644 --- a/docs/src/pages.js +++ b/docs/src/pages.js @@ -33,6 +33,7 @@ const pages = [ pathname: '/components', subheader: '/components/inputs', children: [ + { pathname: '/components/autocomplete' }, { pathname: '/components/buttons' }, { pathname: '/components/button-group' }, { pathname: '/components/checkboxes' }, @@ -117,7 +118,6 @@ const pages = [ subheader: '/components/lab', children: [ { pathname: '/components/about-the-lab' }, - { pathname: '/components/autocomplete' }, { pathname: '/components/pagination' }, { pathname: '/components/rating' }, { pathname: '/components/skeleton' }, diff --git a/docs/src/pages/components/autocomplete/Asynchronous.js b/docs/src/pages/components/autocomplete/Asynchronous.js index 9a1003699b1472..24205297c725e2 100644 --- a/docs/src/pages/components/autocomplete/Asynchronous.js +++ b/docs/src/pages/components/autocomplete/Asynchronous.js @@ -1,6 +1,6 @@ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import CircularProgress from '@material-ui/core/CircularProgress'; function sleep(delay = 0) { diff --git a/docs/src/pages/components/autocomplete/Asynchronous.tsx b/docs/src/pages/components/autocomplete/Asynchronous.tsx index 27827d6e134688..c756038e1c6c77 100644 --- a/docs/src/pages/components/autocomplete/Asynchronous.tsx +++ b/docs/src/pages/components/autocomplete/Asynchronous.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import CircularProgress from '@material-ui/core/CircularProgress'; interface CountryType { diff --git a/docs/src/pages/components/autocomplete/CheckboxesTags.js b/docs/src/pages/components/autocomplete/CheckboxesTags.js index a6dbb0d644e483..6fb3ca96b63dff 100644 --- a/docs/src/pages/components/autocomplete/CheckboxesTags.js +++ b/docs/src/pages/components/autocomplete/CheckboxesTags.js @@ -2,7 +2,7 @@ import * as React from 'react'; import Checkbox from '@material-ui/core/Checkbox'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; import CheckBoxIcon from '@material-ui/icons/CheckBox'; diff --git a/docs/src/pages/components/autocomplete/CheckboxesTags.tsx b/docs/src/pages/components/autocomplete/CheckboxesTags.tsx index a6dbb0d644e483..6fb3ca96b63dff 100644 --- a/docs/src/pages/components/autocomplete/CheckboxesTags.tsx +++ b/docs/src/pages/components/autocomplete/CheckboxesTags.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Checkbox from '@material-ui/core/Checkbox'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; import CheckBoxIcon from '@material-ui/icons/CheckBox'; diff --git a/docs/src/pages/components/autocomplete/ComboBox.js b/docs/src/pages/components/autocomplete/ComboBox.js index ca3f69a91df2dd..24ab3f73ec45ef 100644 --- a/docs/src/pages/components/autocomplete/ComboBox.js +++ b/docs/src/pages/components/autocomplete/ComboBox.js @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; export default function ComboBox() { return ( diff --git a/docs/src/pages/components/autocomplete/ComboBox.tsx b/docs/src/pages/components/autocomplete/ComboBox.tsx index ca3f69a91df2dd..24ab3f73ec45ef 100644 --- a/docs/src/pages/components/autocomplete/ComboBox.tsx +++ b/docs/src/pages/components/autocomplete/ComboBox.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; export default function ComboBox() { return ( diff --git a/docs/src/pages/components/autocomplete/ControllableStates.js b/docs/src/pages/components/autocomplete/ControllableStates.js index 2a2ba28f2880d6..50aa61fff3853e 100644 --- a/docs/src/pages/components/autocomplete/ControllableStates.js +++ b/docs/src/pages/components/autocomplete/ControllableStates.js @@ -1,6 +1,6 @@ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; const options = ['Option 1', 'Option 2']; diff --git a/docs/src/pages/components/autocomplete/ControllableStates.tsx b/docs/src/pages/components/autocomplete/ControllableStates.tsx index 863706b7ae05d0..ead13832a69cab 100644 --- a/docs/src/pages/components/autocomplete/ControllableStates.tsx +++ b/docs/src/pages/components/autocomplete/ControllableStates.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; const options = ['Option 1', 'Option 2']; diff --git a/docs/src/pages/components/autocomplete/CountrySelect.js b/docs/src/pages/components/autocomplete/CountrySelect.js index f6767be3d58107..319cacfb62d7e8 100644 --- a/docs/src/pages/components/autocomplete/CountrySelect.js +++ b/docs/src/pages/components/autocomplete/CountrySelect.js @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import { makeStyles } from '@material-ui/core/styles'; // ISO 3166-1 alpha-2 diff --git a/docs/src/pages/components/autocomplete/CountrySelect.tsx b/docs/src/pages/components/autocomplete/CountrySelect.tsx index 635723cf9e6e9a..0c49a9539b9b1e 100644 --- a/docs/src/pages/components/autocomplete/CountrySelect.tsx +++ b/docs/src/pages/components/autocomplete/CountrySelect.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import { makeStyles } from '@material-ui/core/styles'; // ISO 3166-1 alpha-2 diff --git a/docs/src/pages/components/autocomplete/CustomInputAutocomplete.js b/docs/src/pages/components/autocomplete/CustomInputAutocomplete.js index 9abf20015e3d6b..34dc45eb2e6d10 100644 --- a/docs/src/pages/components/autocomplete/CustomInputAutocomplete.js +++ b/docs/src/pages/components/autocomplete/CustomInputAutocomplete.js @@ -1,5 +1,5 @@ import * as React from 'react'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; const options = ['Option 1', 'Option 2']; diff --git a/docs/src/pages/components/autocomplete/CustomInputAutocomplete.tsx b/docs/src/pages/components/autocomplete/CustomInputAutocomplete.tsx index 9abf20015e3d6b..34dc45eb2e6d10 100644 --- a/docs/src/pages/components/autocomplete/CustomInputAutocomplete.tsx +++ b/docs/src/pages/components/autocomplete/CustomInputAutocomplete.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; const options = ['Option 1', 'Option 2']; diff --git a/docs/src/pages/components/autocomplete/CustomizedHook.js b/docs/src/pages/components/autocomplete/CustomizedHook.js index 3b77a4d2a2b102..3aa11b1e38f8fe 100644 --- a/docs/src/pages/components/autocomplete/CustomizedHook.js +++ b/docs/src/pages/components/autocomplete/CustomizedHook.js @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; -import useAutocomplete from '@material-ui/lab/useAutocomplete'; +import useAutocomplete from '@material-ui/core/useAutocomplete'; import NoSsr from '@material-ui/core/NoSsr'; import { useTheme, createMuiTheme } from '@material-ui/core/styles'; import CheckIcon from '@material-ui/icons/Check'; diff --git a/docs/src/pages/components/autocomplete/CustomizedHook.tsx b/docs/src/pages/components/autocomplete/CustomizedHook.tsx index c493731607c531..44499b77edd305 100644 --- a/docs/src/pages/components/autocomplete/CustomizedHook.tsx +++ b/docs/src/pages/components/autocomplete/CustomizedHook.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; -import useAutocomplete from '@material-ui/lab/useAutocomplete'; +import useAutocomplete from '@material-ui/core/useAutocomplete'; import NoSsr from '@material-ui/core/NoSsr'; import { useTheme, createMuiTheme } from '@material-ui/core/styles'; import CheckIcon from '@material-ui/icons/Check'; diff --git a/docs/src/pages/components/autocomplete/DisabledOptions.js b/docs/src/pages/components/autocomplete/DisabledOptions.js index a4c6719009ed1c..56f777a6cf42dc 100644 --- a/docs/src/pages/components/autocomplete/DisabledOptions.js +++ b/docs/src/pages/components/autocomplete/DisabledOptions.js @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; export default function DisabledOptions() { return ( diff --git a/docs/src/pages/components/autocomplete/DisabledOptions.tsx b/docs/src/pages/components/autocomplete/DisabledOptions.tsx index a4c6719009ed1c..56f777a6cf42dc 100644 --- a/docs/src/pages/components/autocomplete/DisabledOptions.tsx +++ b/docs/src/pages/components/autocomplete/DisabledOptions.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; export default function DisabledOptions() { return ( diff --git a/docs/src/pages/components/autocomplete/Filter.js b/docs/src/pages/components/autocomplete/Filter.js index ed393ff8cd40d1..fde5cc7b731424 100644 --- a/docs/src/pages/components/autocomplete/Filter.js +++ b/docs/src/pages/components/autocomplete/Filter.js @@ -3,7 +3,7 @@ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; import Autocomplete, { createFilterOptions, -} from '@material-ui/lab/Autocomplete'; +} from '@material-ui/core/Autocomplete'; const filterOptions = createFilterOptions({ matchFrom: 'start', diff --git a/docs/src/pages/components/autocomplete/Filter.tsx b/docs/src/pages/components/autocomplete/Filter.tsx index c717a16ab9feaf..9e3ab9faa8770f 100644 --- a/docs/src/pages/components/autocomplete/Filter.tsx +++ b/docs/src/pages/components/autocomplete/Filter.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; import Autocomplete, { createFilterOptions, -} from '@material-ui/lab/Autocomplete'; +} from '@material-ui/core/Autocomplete'; const filterOptions = createFilterOptions({ matchFrom: 'start', diff --git a/docs/src/pages/components/autocomplete/FixedTags.js b/docs/src/pages/components/autocomplete/FixedTags.js index fa79e6b7fa550f..333003c861399c 100644 --- a/docs/src/pages/components/autocomplete/FixedTags.js +++ b/docs/src/pages/components/autocomplete/FixedTags.js @@ -2,7 +2,7 @@ import * as React from 'react'; import Chip from '@material-ui/core/Chip'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; export default function FixedTags() { const fixedOptions = [top100Films[6]]; diff --git a/docs/src/pages/components/autocomplete/FixedTags.tsx b/docs/src/pages/components/autocomplete/FixedTags.tsx index fa79e6b7fa550f..333003c861399c 100644 --- a/docs/src/pages/components/autocomplete/FixedTags.tsx +++ b/docs/src/pages/components/autocomplete/FixedTags.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Chip from '@material-ui/core/Chip'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; export default function FixedTags() { const fixedOptions = [top100Films[6]]; diff --git a/docs/src/pages/components/autocomplete/FreeSolo.js b/docs/src/pages/components/autocomplete/FreeSolo.js index 544d0707bbb201..30d11d71a514cd 100644 --- a/docs/src/pages/components/autocomplete/FreeSolo.js +++ b/docs/src/pages/components/autocomplete/FreeSolo.js @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; export default function FreeSolo() { return ( diff --git a/docs/src/pages/components/autocomplete/FreeSolo.tsx b/docs/src/pages/components/autocomplete/FreeSolo.tsx index 544d0707bbb201..30d11d71a514cd 100644 --- a/docs/src/pages/components/autocomplete/FreeSolo.tsx +++ b/docs/src/pages/components/autocomplete/FreeSolo.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; export default function FreeSolo() { return ( diff --git a/docs/src/pages/components/autocomplete/FreeSoloCreateOption.js b/docs/src/pages/components/autocomplete/FreeSoloCreateOption.js index 82680686659043..3e6a7585102051 100644 --- a/docs/src/pages/components/autocomplete/FreeSoloCreateOption.js +++ b/docs/src/pages/components/autocomplete/FreeSoloCreateOption.js @@ -3,7 +3,7 @@ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; import Autocomplete, { createFilterOptions, -} from '@material-ui/lab/Autocomplete'; +} from '@material-ui/core/Autocomplete'; const filter = createFilterOptions(); diff --git a/docs/src/pages/components/autocomplete/FreeSoloCreateOption.tsx b/docs/src/pages/components/autocomplete/FreeSoloCreateOption.tsx index 9c8ed60e036a76..20cc1072b6c936 100644 --- a/docs/src/pages/components/autocomplete/FreeSoloCreateOption.tsx +++ b/docs/src/pages/components/autocomplete/FreeSoloCreateOption.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; import Autocomplete, { createFilterOptions, -} from '@material-ui/lab/Autocomplete'; +} from '@material-ui/core/Autocomplete'; const filter = createFilterOptions(); diff --git a/docs/src/pages/components/autocomplete/FreeSoloCreateOptionDialog.js b/docs/src/pages/components/autocomplete/FreeSoloCreateOptionDialog.js index 12dee5be43d7ec..da058e22766006 100644 --- a/docs/src/pages/components/autocomplete/FreeSoloCreateOptionDialog.js +++ b/docs/src/pages/components/autocomplete/FreeSoloCreateOptionDialog.js @@ -9,7 +9,7 @@ import DialogActions from '@material-ui/core/DialogActions'; import Button from '@material-ui/core/Button'; import Autocomplete, { createFilterOptions, -} from '@material-ui/lab/Autocomplete'; +} from '@material-ui/core/Autocomplete'; const filter = createFilterOptions(); diff --git a/docs/src/pages/components/autocomplete/FreeSoloCreateOptionDialog.tsx b/docs/src/pages/components/autocomplete/FreeSoloCreateOptionDialog.tsx index 2bceac20e64c67..4448ee43e56e0f 100644 --- a/docs/src/pages/components/autocomplete/FreeSoloCreateOptionDialog.tsx +++ b/docs/src/pages/components/autocomplete/FreeSoloCreateOptionDialog.tsx @@ -9,7 +9,7 @@ import DialogActions from '@material-ui/core/DialogActions'; import Button from '@material-ui/core/Button'; import Autocomplete, { createFilterOptions, -} from '@material-ui/lab/Autocomplete'; +} from '@material-ui/core/Autocomplete'; const filter = createFilterOptions(); diff --git a/docs/src/pages/components/autocomplete/GitHubLabel.js b/docs/src/pages/components/autocomplete/GitHubLabel.js index c7b7844044d1df..2a380b6a1a877c 100644 --- a/docs/src/pages/components/autocomplete/GitHubLabel.js +++ b/docs/src/pages/components/autocomplete/GitHubLabel.js @@ -5,7 +5,7 @@ import Popper from '@material-ui/core/Popper'; import SettingsIcon from '@material-ui/icons/Settings'; import CloseIcon from '@material-ui/icons/Close'; import DoneIcon from '@material-ui/icons/Done'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import ButtonBase from '@material-ui/core/ButtonBase'; import InputBase from '@material-ui/core/InputBase'; diff --git a/docs/src/pages/components/autocomplete/GitHubLabel.tsx b/docs/src/pages/components/autocomplete/GitHubLabel.tsx index d3d5c72b02bdcc..4f22b0d258c796 100644 --- a/docs/src/pages/components/autocomplete/GitHubLabel.tsx +++ b/docs/src/pages/components/autocomplete/GitHubLabel.tsx @@ -13,7 +13,7 @@ import CloseIcon from '@material-ui/icons/Close'; import DoneIcon from '@material-ui/icons/Done'; import Autocomplete, { AutocompleteCloseReason, -} from '@material-ui/lab/Autocomplete'; +} from '@material-ui/core/Autocomplete'; import ButtonBase from '@material-ui/core/ButtonBase'; import InputBase from '@material-ui/core/InputBase'; diff --git a/docs/src/pages/components/autocomplete/GoogleMaps.js b/docs/src/pages/components/autocomplete/GoogleMaps.js index ed53ab65fd3955..e1fe4c44d3d485 100644 --- a/docs/src/pages/components/autocomplete/GoogleMaps.js +++ b/docs/src/pages/components/autocomplete/GoogleMaps.js @@ -1,6 +1,6 @@ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import LocationOnIcon from '@material-ui/icons/LocationOn'; import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; diff --git a/docs/src/pages/components/autocomplete/GoogleMaps.tsx b/docs/src/pages/components/autocomplete/GoogleMaps.tsx index 2ee5ea1a417bf3..fe0b616a79a061 100644 --- a/docs/src/pages/components/autocomplete/GoogleMaps.tsx +++ b/docs/src/pages/components/autocomplete/GoogleMaps.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import LocationOnIcon from '@material-ui/icons/LocationOn'; import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; diff --git a/docs/src/pages/components/autocomplete/Grouped.js b/docs/src/pages/components/autocomplete/Grouped.js index b7c18843405af6..2062d1f284a70f 100644 --- a/docs/src/pages/components/autocomplete/Grouped.js +++ b/docs/src/pages/components/autocomplete/Grouped.js @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; export default function Grouped() { const options = top100Films.map((option) => { diff --git a/docs/src/pages/components/autocomplete/Grouped.tsx b/docs/src/pages/components/autocomplete/Grouped.tsx index b7c18843405af6..2062d1f284a70f 100644 --- a/docs/src/pages/components/autocomplete/Grouped.tsx +++ b/docs/src/pages/components/autocomplete/Grouped.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; export default function Grouped() { const options = top100Films.map((option) => { diff --git a/docs/src/pages/components/autocomplete/Highlights.js b/docs/src/pages/components/autocomplete/Highlights.js index aeedac89d26455..5c931dbee01fe2 100644 --- a/docs/src/pages/components/autocomplete/Highlights.js +++ b/docs/src/pages/components/autocomplete/Highlights.js @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import parse from 'autosuggest-highlight/parse'; import match from 'autosuggest-highlight/match'; diff --git a/docs/src/pages/components/autocomplete/Highlights.tsx b/docs/src/pages/components/autocomplete/Highlights.tsx index aeedac89d26455..5c931dbee01fe2 100644 --- a/docs/src/pages/components/autocomplete/Highlights.tsx +++ b/docs/src/pages/components/autocomplete/Highlights.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import parse from 'autosuggest-highlight/parse'; import match from 'autosuggest-highlight/match'; diff --git a/docs/src/pages/components/autocomplete/LimitTags.js b/docs/src/pages/components/autocomplete/LimitTags.js index 5dc6fb63353321..b0962307d01886 100644 --- a/docs/src/pages/components/autocomplete/LimitTags.js +++ b/docs/src/pages/components/autocomplete/LimitTags.js @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import { makeStyles } from '@material-ui/core/styles'; import TextField from '@material-ui/core/TextField'; diff --git a/docs/src/pages/components/autocomplete/LimitTags.tsx b/docs/src/pages/components/autocomplete/LimitTags.tsx index aca6b8846b67c9..a8d97594dc5449 100644 --- a/docs/src/pages/components/autocomplete/LimitTags.tsx +++ b/docs/src/pages/components/autocomplete/LimitTags.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import { makeStyles, createStyles, Theme } from '@material-ui/core/styles'; import TextField from '@material-ui/core/TextField'; diff --git a/docs/src/pages/components/autocomplete/Playground.js b/docs/src/pages/components/autocomplete/Playground.js index a9d479ed219331..d928eb72d90d89 100644 --- a/docs/src/pages/components/autocomplete/Playground.js +++ b/docs/src/pages/components/autocomplete/Playground.js @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; export default function Playground() { const defaultProps = { diff --git a/docs/src/pages/components/autocomplete/Playground.tsx b/docs/src/pages/components/autocomplete/Playground.tsx index ae92a5876d3b7c..cb8e525c251fa1 100644 --- a/docs/src/pages/components/autocomplete/Playground.tsx +++ b/docs/src/pages/components/autocomplete/Playground.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; export default function Playground() { const defaultProps = { diff --git a/docs/src/pages/components/autocomplete/Sizes.js b/docs/src/pages/components/autocomplete/Sizes.js index 14b5704caaf446..68e2f8894d48b4 100644 --- a/docs/src/pages/components/autocomplete/Sizes.js +++ b/docs/src/pages/components/autocomplete/Sizes.js @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import Chip from '@material-ui/core/Chip'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import { makeStyles } from '@material-ui/core/styles'; import TextField from '@material-ui/core/TextField'; diff --git a/docs/src/pages/components/autocomplete/Sizes.tsx b/docs/src/pages/components/autocomplete/Sizes.tsx index 4eadfea0669f7d..bea77e5a89228b 100644 --- a/docs/src/pages/components/autocomplete/Sizes.tsx +++ b/docs/src/pages/components/autocomplete/Sizes.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import Chip from '@material-ui/core/Chip'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; import TextField from '@material-ui/core/TextField'; diff --git a/docs/src/pages/components/autocomplete/Tags.js b/docs/src/pages/components/autocomplete/Tags.js index 9d9a50b14e9e66..c174cba7caca39 100644 --- a/docs/src/pages/components/autocomplete/Tags.js +++ b/docs/src/pages/components/autocomplete/Tags.js @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import Chip from '@material-ui/core/Chip'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import { makeStyles } from '@material-ui/core/styles'; import TextField from '@material-ui/core/TextField'; diff --git a/docs/src/pages/components/autocomplete/Tags.tsx b/docs/src/pages/components/autocomplete/Tags.tsx index ba0eba7de3e013..f89f4e2bb95e5b 100644 --- a/docs/src/pages/components/autocomplete/Tags.tsx +++ b/docs/src/pages/components/autocomplete/Tags.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; import Chip from '@material-ui/core/Chip'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; import TextField from '@material-ui/core/TextField'; diff --git a/docs/src/pages/components/autocomplete/UseAutocomplete.js b/docs/src/pages/components/autocomplete/UseAutocomplete.js index 7d0f17275dec12..fad0741c570702 100644 --- a/docs/src/pages/components/autocomplete/UseAutocomplete.js +++ b/docs/src/pages/components/autocomplete/UseAutocomplete.js @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; -import useAutocomplete from '@material-ui/lab/useAutocomplete'; +import useAutocomplete from '@material-ui/core/useAutocomplete'; import { makeStyles } from '@material-ui/core/styles'; const useStyles = makeStyles((theme) => ({ diff --git a/docs/src/pages/components/autocomplete/UseAutocomplete.tsx b/docs/src/pages/components/autocomplete/UseAutocomplete.tsx index c572b9ff035604..3dbb5a0073cf21 100644 --- a/docs/src/pages/components/autocomplete/UseAutocomplete.tsx +++ b/docs/src/pages/components/autocomplete/UseAutocomplete.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import * as React from 'react'; -import useAutocomplete from '@material-ui/lab/useAutocomplete'; +import useAutocomplete from '@material-ui/core/useAutocomplete'; import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; const useStyles = makeStyles((theme: Theme) => diff --git a/docs/src/pages/components/autocomplete/Virtualize.js b/docs/src/pages/components/autocomplete/Virtualize.js index 4eaa53df7f4641..eecd305703cc8d 100644 --- a/docs/src/pages/components/autocomplete/Virtualize.js +++ b/docs/src/pages/components/autocomplete/Virtualize.js @@ -1,7 +1,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import TextField from '@material-ui/core/TextField'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import ListSubheader from '@material-ui/core/ListSubheader'; import { useTheme, makeStyles } from '@material-ui/core/styles'; diff --git a/docs/src/pages/components/autocomplete/Virtualize.tsx b/docs/src/pages/components/autocomplete/Virtualize.tsx index 66788860bc66c9..7861cfa5f7289f 100644 --- a/docs/src/pages/components/autocomplete/Virtualize.tsx +++ b/docs/src/pages/components/autocomplete/Virtualize.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import TextField from '@material-ui/core/TextField'; import Autocomplete, { AutocompleteRenderGroupParams, -} from '@material-ui/lab/Autocomplete'; +} from '@material-ui/core/Autocomplete'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import ListSubheader from '@material-ui/core/ListSubheader'; import { useTheme, makeStyles } from '@material-ui/core/styles'; diff --git a/docs/src/pages/components/autocomplete/autocomplete-de.md b/docs/src/pages/components/autocomplete/autocomplete-de.md index 1e0fa46f64cd34..48d75d01cd0042 100644 --- a/docs/src/pages/components/autocomplete/autocomplete-de.md +++ b/docs/src/pages/components/autocomplete/autocomplete-de.md @@ -115,7 +115,7 @@ You can group the options with the `groupBy` prop. If you do so, make sure that For advanced customization use cases, we expose a `useAutocomplete()` hook. It accepts almost the same options as the Autocomplete component minus all the props related to the rendering of JSX. The Autocomplete component uses this hook internally. ```jsx -import useAutocomplete from '@material-ui/lab/useAutocomplete'; +import useAutocomplete from '@material-ui/core/useAutocomplete'; ``` - 📦 [4.5 kB gzipped](/size-snapshot). @@ -221,7 +221,7 @@ The following demo relies on [autosuggest-highlight](https://github.com/moroshko The component exposes a factory to create a filter method that can provided to the `filterOptions` prop. Sie können es verwenden, um das Standard-Filterverhalten der Option zu ändern. ```js -import { createFilterOptions } from '@material-ui/lab/Autocomplete'; +import { createFilterOptions } from '@material-ui/core/Autocomplete'; ``` ### `createFilterOptions(config) => filterOptions` diff --git a/docs/src/pages/components/autocomplete/autocomplete-es.md b/docs/src/pages/components/autocomplete/autocomplete-es.md index 049f61275f4e33..d72b74445525e1 100644 --- a/docs/src/pages/components/autocomplete/autocomplete-es.md +++ b/docs/src/pages/components/autocomplete/autocomplete-es.md @@ -115,7 +115,7 @@ Puedes agrupar las opciones con el accesorio `groupBy`. Si lo haces, asegúrate Para la personalización avanzada de casos de uso, exponemos un hook `useAutocomplete()`. Acepta casi las mismas opciones que el componente Autocompletar menus las propiedades relacionadas al renderizado de JSX. El componente Autocompletar usa este hook internamente. ```jsx -import useAutocomplete from '@material-ui/lab/useAutocomplete'; +import useAutocomplete from '@material-ui/core/useAutocomplete'; ``` - 📦 [4.5 kB comprimido](/size-snapshot). @@ -221,7 +221,7 @@ La siguiente demostración se basa en [autosuggest-highlight](https://github.com El componente expone una factoría para crear un método de filtrado para proveer a la propiedad `filterOptions`. Puede usarse para cambiar el comportamiento de filtrado por defecto. ```js -import { createFilterOptions } from '@material-ui/lab/Autocomplete'; +import { createFilterOptions } from '@material-ui/core/Autocomplete'; ``` ### `createFilterOptions(config) => filterOptions` diff --git a/docs/src/pages/components/autocomplete/autocomplete-fr.md b/docs/src/pages/components/autocomplete/autocomplete-fr.md index 99ef981cb115da..df6f184ea141ae 100644 --- a/docs/src/pages/components/autocomplete/autocomplete-fr.md +++ b/docs/src/pages/components/autocomplete/autocomplete-fr.md @@ -115,7 +115,7 @@ You can group the options with the `groupBy` prop. If you do so, make sure that Pour les cas de personnalisation avancée, nous exposons un hook `useAutocomplete()`. Il accepte presque les mêmes options que le composant de saisie automatique moins tous les props liés au rendu de JSX. Le composant Autocomplete utilise ce hook en interne. ```jsx -import useAutocomplete from '@material-ui/lab/useAutocomplete'; +import useAutocomplete from '@material-ui/core/useAutocomplete'; ``` - 📦 [4.5 kB gzippé](/size-snapshot). @@ -221,7 +221,7 @@ La démo suivante repose sur [autosuggest-highlight](https://github.com/moroshko Le composant expose une usine pour créer une méthode de filtre qui peut être fournie à la propriété `filterOptions`. Vous pouvez l'utiliser pour modifier le comportement de filtre par défaut. ```js -import { createFilterOptions } from '@material-ui/lab/Autocomplete'; +import { createFilterOptions } from '@material-ui/core/Autocomplete'; ``` ### `createFilterOptions(config) => filterOptions` diff --git a/docs/src/pages/components/autocomplete/autocomplete-ja.md b/docs/src/pages/components/autocomplete/autocomplete-ja.md index be8a5f2b1acab0..6a111e7634bd5e 100644 --- a/docs/src/pages/components/autocomplete/autocomplete-ja.md +++ b/docs/src/pages/components/autocomplete/autocomplete-ja.md @@ -115,7 +115,7 @@ You can group the options with the `groupBy` prop. If you do so, make sure that 高度な利用方法のために、 `useAutocomplete()` hooksがあります。 JSXのレンダリングに関連する値以外は、Autocompleteコンポーネントとほぼ同じ値をとります。 Autocompleteコンポーネントは内部でこのhookを使用しています。 ```jsx -import useAutocomplete from '@material-ui/lab/useAutocomplete'; +import useAutocomplete from '@material-ui/core/useAutocomplete'; ``` - [4.5 kB gzipped](/size-snapshot). @@ -221,7 +221,7 @@ GitHubのラベルピッカーを再現したデモです。 `filterOptions`に流せるフィルターメソッドを作成できるファクトリーを露出しているコンポーネント デフォルトのフィルター挙動を変更するのに使うことができます。 ```js -import { createFilterOptions } from '@material-ui/lab/Autocomplete'; +import { createFilterOptions } from '@material-ui/core/Autocomplete'; ``` ### `createFilterOptions(config) => filterOptions` diff --git a/docs/src/pages/components/autocomplete/autocomplete-pt.md b/docs/src/pages/components/autocomplete/autocomplete-pt.md index 114ce0ee7046d4..5f737dc8aeff0e 100644 --- a/docs/src/pages/components/autocomplete/autocomplete-pt.md +++ b/docs/src/pages/components/autocomplete/autocomplete-pt.md @@ -115,7 +115,7 @@ You can group the options with the `groupBy` prop. If you do so, make sure that Para casos de customização avançada nós expomos o hook `useAutocomplete()`. Ele aceita quase as mesmas opções do componente autocompletar exceto todas as propriedades relacionadas a renderização do JSX. O componente autocompletar usa esse hook internamente. ```jsx -import useAutocomplete from '@material-ui/lab/useAutocomplete'; +import useAutocomplete from '@material-ui/core/useAutocomplete'; ``` - 📦 [4.5 kB gzipado](/size-snapshot). @@ -221,7 +221,7 @@ A demonstração a seguir dependem do [autosuggest-highlight](https://github.com O componente expõe uma fábrica para criar um método de filtro que pode ser fornecido para a propriedade `filterOptions`. Você pode usar ela para modificar o comportamento padrão do filtro. ```js -import { createFilterOptions } from '@material-ui/lab/Autocomplete'; +import { createFilterOptions } from '@material-ui/core/Autocomplete'; ``` ### `createFilterOptions(config) => filterOptions` diff --git a/docs/src/pages/components/autocomplete/autocomplete-ru.md b/docs/src/pages/components/autocomplete/autocomplete-ru.md index fa140770001d75..e422caa82798de 100644 --- a/docs/src/pages/components/autocomplete/autocomplete-ru.md +++ b/docs/src/pages/components/autocomplete/autocomplete-ru.md @@ -115,7 +115,7 @@ You can group the options with the `groupBy` prop. If you do so, make sure that Для продвинутой кастомизации используйте `useAutocomplete()` хук. It accepts almost the same options as the Autocomplete component minus all the props related to the rendering of JSX. The Autocomplete component uses this hook internally. ```jsx -import useAutocomplete from '@material-ui/lab/useAutocomplete'; +import useAutocomplete from '@material-ui/core/useAutocomplete'; ``` - 4.5 [4,5 кБ в сжатом виде](/size-snapshot). @@ -221,7 +221,7 @@ The following demo relies on [autosuggest-highlight](https://github.com/moroshko The component exposes a factory to create a filter method that can provided to the `filterOptions` prop. You can use it to change the default option filter behavior. ```js -import { createFilterOptions } from '@material-ui/lab/Autocomplete'; +import { createFilterOptions } from '@material-ui/core/Autocomplete'; ``` ### `createFilterOptions(config) => filterOptions` diff --git a/docs/src/pages/components/autocomplete/autocomplete-zh.md b/docs/src/pages/components/autocomplete/autocomplete-zh.md index ac9fd1da22b0d8..8702432e04038a 100644 --- a/docs/src/pages/components/autocomplete/autocomplete-zh.md +++ b/docs/src/pages/components/autocomplete/autocomplete-zh.md @@ -115,7 +115,7 @@ const options = ['The Godfather', 'Pulp Fiction']; 作为一种高级定制方式,我们提供了一个 `useAutocomplete()` hook。 它接受几乎与 Autocomplete 组件相同的参数,辅以与 JSX 渲染有关的所有参数。 Autocomplete 组件内部也是使用的此 hook。 ```jsx -import useAutocomplete from '@material-ui/lab/useAutocomplete'; +import useAutocomplete from '@material-ui/core/useAutocomplete'; ``` - 📦 [4.5kB 的压缩包](/size-snapshot)。 @@ -216,7 +216,7 @@ import useAutocomplete from '@material-ui/lab/useAutocomplete'; 此组件提供了一个 factory 来构建一个筛选的方法,供给 `filterOptions` 属性使来用。 用此你可以更改默认的筛选行为。 ```js -import { createFilterOptions } from '@material-ui/lab/Autocomplete'; +import { createFilterOptions } from '@material-ui/core/Autocomplete'; ``` ### `createFilterOptions(config) => filterOptions` diff --git a/docs/src/pages/components/autocomplete/autocomplete.md b/docs/src/pages/components/autocomplete/autocomplete.md index aee5a2a94a52c6..7054d8f97c5907 100644 --- a/docs/src/pages/components/autocomplete/autocomplete.md +++ b/docs/src/pages/components/autocomplete/autocomplete.md @@ -3,7 +3,6 @@ title: Autocomplete React component components: TextField, Popper, Autocomplete githubLabel: 'component: Autocomplete' waiAria: https://www.w3.org/TR/wai-aria-practices/#combobox -packageName: '@material-ui/lab' --- # Autocomplete @@ -118,7 +117,7 @@ related to the rendering of JSX. The Autocomplete component uses this hook internally. ```jsx -import useAutocomplete from '@material-ui/lab/useAutocomplete'; +import useAutocomplete from '@material-ui/core/useAutocomplete'; ``` - 📦 [4.5 kB gzipped](/size-snapshot). @@ -224,7 +223,7 @@ The component exposes a factory to create a filter method that can provided to t You can use it to change the default option filter behavior. ```js -import { createFilterOptions } from '@material-ui/lab/Autocomplete'; +import { createFilterOptions } from '@material-ui/core/Autocomplete'; ``` ### `createFilterOptions(config) => filterOptions` diff --git a/docs/src/pages/guides/localization/Locales.js b/docs/src/pages/guides/localization/Locales.js index 885aaab87b6c63..2aa50b1127203b 100644 --- a/docs/src/pages/guides/localization/Locales.js +++ b/docs/src/pages/guides/localization/Locales.js @@ -2,7 +2,7 @@ import * as React from 'react'; import TablePagination from '@material-ui/core/TablePagination'; import Pagination from '@material-ui/lab/Pagination'; import Rating from '@material-ui/lab/Rating'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import TextField from '@material-ui/core/TextField'; import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; import * as locales from '@material-ui/core/locale'; diff --git a/docs/src/pages/guides/localization/Locales.tsx b/docs/src/pages/guides/localization/Locales.tsx index d4263865c39820..f19b386d4f1325 100644 --- a/docs/src/pages/guides/localization/Locales.tsx +++ b/docs/src/pages/guides/localization/Locales.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import TablePagination from '@material-ui/core/TablePagination'; import Pagination from '@material-ui/lab/Pagination'; import Rating from '@material-ui/lab/Rating'; -import Autocomplete from '@material-ui/lab/Autocomplete'; +import Autocomplete from '@material-ui/core/Autocomplete'; import TextField from '@material-ui/core/TextField'; import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'; import * as locales from '@material-ui/core/locale'; diff --git a/docs/src/pages/guides/migration-v4/migration-v4.md b/docs/src/pages/guides/migration-v4/migration-v4.md index f679b23667fa48..cac9009c37c21a 100644 --- a/docs/src/pages/guides/migration-v4/migration-v4.md +++ b/docs/src/pages/guides/migration-v4/migration-v4.md @@ -207,6 +207,15 @@ const theme = createMuitheme({ +import AlertTitle from '@material-ui/core/AlertTitle'; ``` + ### Autocomplete + +- Move the component from the lab to the core. The component is now stable. + + ```diff + -import Autocomplete from '@material-ui/lab/Autocomplete'; + +import Autocomplete from '@material-ui/core/Autocomplete'; + ``` + ### Avatar - Rename `circle` to `circular` for consistency. The possible values should be adjectives, not nouns: diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.d.ts b/packages/material-ui-lab/src/Autocomplete/Autocomplete.d.ts index 2808ee08ee3169..54262dc0b5e440 100644 --- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.d.ts +++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.d.ts @@ -1,277 +1,2 @@ -import * as React from 'react'; -import { InternalStandardProps as StandardProps } from '@material-ui/core'; -import { PopperProps } from '@material-ui/core/Popper'; -import { - AutocompleteChangeDetails, - AutocompleteChangeReason, - AutocompleteCloseReason, - AutocompleteInputChangeReason, - createFilterOptions, - UseAutocompleteProps, -} from '../useAutocomplete'; - -export { - AutocompleteChangeDetails, - AutocompleteChangeReason, - AutocompleteCloseReason, - AutocompleteInputChangeReason, - createFilterOptions, -}; - -export interface AutocompleteRenderOptionState { - inputValue: string; - selected: boolean; -} - -export type AutocompleteGetTagProps = ({ index }: { index: number }) => {}; - -export interface AutocompleteRenderGroupParams { - key: string; - group: string; - children?: React.ReactNode; -} - -export interface AutocompleteRenderInputParams { - id: string; - disabled: boolean; - fullWidth: boolean; - size: 'small' | undefined; - InputLabelProps: object; - InputProps: { - ref: React.Ref; - className: string; - startAdornment: React.ReactNode; - endAdornment: React.ReactNode; - }; - inputProps: object; -} - -export interface AutocompleteProps< - T, - Multiple extends boolean | undefined, - DisableClearable extends boolean | undefined, - FreeSolo extends boolean | undefined -> extends UseAutocompleteProps, - StandardProps, 'defaultValue' | 'onChange' | 'children'> { - /** - * Props applied to the [`Chip`](/api/chip/) element. - */ - ChipProps?: object; - /** - * Override or extend the styles applied to the component. - */ - classes?: { - /** Styles applied to the root element. */ - root?: string; - /** Styles applied to the root element if `fullWidth={true}`. */ - fullWidth?: string; - /** Pseudo-class applied to the root element if focused. */ - focused?: string; - /** Styles applied to the tag elements, e.g. the chips. */ - tag?: string; - /** Styles applied to the tag elements, e.g. the chips if `size="small"`. */ - tagSizeSmall?: string; - /** Styles applied when the popup icon is rendered. */ - hasPopupIcon?: string; - /** Styles applied when the clear icon is rendered. */ - hasClearIcon?: string; - /** Styles applied to the Input element. */ - inputRoot?: string; - /** Styles applied to the input element. */ - input?: string; - /** Styles applied to the input element if tag focused. */ - inputFocused?: string; - /** Styles applied to the endAdornment element. */ - endAdornment?: string; - /** Styles applied to the clear indicator. */ - clearIndicator?: string; - /** Styles applied to the clear indicator if the input is dirty. */ - clearIndicatorDirty?: string; - /** Styles applied to the popup indicator. */ - popupIndicator?: string; - /** Styles applied to the popup indicator if the popup is open. */ - popupIndicatorOpen?: string; - /** Styles applied to the popper element. */ - popper?: string; - /** Styles applied to the popper element if `disablePortal={true}`. */ - popperDisablePortal?: string; - /** Styles applied to the `Paper` component. */ - paper?: string; - /** Styles applied to the `listbox` component. */ - listbox?: string; - /** Styles applied to the loading wrapper. */ - loading?: string; - /** Styles applied to the no option wrapper. */ - noOptions?: string; - /** Styles applied to the option elements. */ - option?: string; - /** Styles applied to the group's label elements. */ - groupLabel?: string; - /** Styles applied to the group's ul elements. */ - groupUl?: string; - }; - /** - * The icon to display in place of the default close icon. - * @default - */ - closeIcon?: React.ReactNode; - /** - * Override the default text for the *clear* icon button. - * - * For localization purposes, you can use the provided [translations](/guides/localization/). - * @default 'Clear' - */ - clearText?: string; - /** - * Override the default text for the *close popup* icon button. - * - * For localization purposes, you can use the provided [translations](/guides/localization/). - * @default 'Close' - */ - closeText?: string; - /** - * If `true`, the input will be disabled. - * @default false - */ - disabled?: boolean; - /** - * The `Popper` content will be inside the DOM hierarchy of the parent component. - * @default false - */ - disablePortal?: boolean; - /** - * Force the visibility display of the popup icon. - * @default 'auto' - */ - forcePopupIcon?: true | false | 'auto'; - /** - * If `true`, the input will take up the full width of its container. - * @default false - */ - fullWidth?: boolean; - /** - * The label to display when the tags are truncated (`limitTags`). - * - * @param {number} more The number of truncated tags. - * @returns {ReactNode} - * @default (more) => `+${more}` - */ - getLimitTagsText?: (more: number) => React.ReactNode; - /** - * The component used to render the listbox. - * @default 'ul' - */ - ListboxComponent?: React.ComponentType>; - /** - * Props applied to the Listbox element. - */ - ListboxProps?: object; - /** - * If `true`, the component is in a loading state. - * @default false - */ - loading?: boolean; - /** - * Text to display when in a loading state. - * - * For localization purposes, you can use the provided [translations](/guides/localization/). - * @default 'Loading…' - */ - loadingText?: React.ReactNode; - /** - * The maximum number of tags that will be visible when not focused. - * Set `-1` to disable the limit. - * @default -1 - */ - limitTags?: number; - /** - * Text to display when there are no options. - * - * For localization purposes, you can use the provided [translations](/guides/localization/). - * @default 'No options' - */ - noOptionsText?: React.ReactNode; - /** - * Override the default text for the *open popup* icon button. - * - * For localization purposes, you can use the provided [translations](/guides/localization/). - * @default 'Open' - */ - openText?: string; - /** - * The component used to render the body of the popup. - * @default Paper - */ - PaperComponent?: React.ComponentType>; - /** - * The component used to position the popup. - * @default Popper - */ - PopperComponent?: React.ComponentType; - /** - * The icon to display in place of the default popup icon. - * @default - */ - popupIcon?: React.ReactNode; - /** - * Render the group. - * - * @param {any} option The group to render. - * @returns {ReactNode} - */ - renderGroup?: (params: AutocompleteRenderGroupParams) => React.ReactNode; - /** - * Render the input. - * - * @param {object} params - * @returns {ReactNode} - */ - renderInput: (params: AutocompleteRenderInputParams) => React.ReactNode; - /** - * Render the option, use `getOptionLabel` by default. - * - * @param {object} props The props to apply on the li element. - * @param {T} option The option to render. - * @param {object} state The state of the component. - * @returns {ReactNode} - */ - renderOption?: ( - props: React.HTMLAttributes, - option: T, - state: AutocompleteRenderOptionState - ) => React.ReactNode; - /** - * Render the selected value. - * - * @param {T[]} value The `value` provided to the component. - * @param {function} getTagProps A tag props getter. - * @returns {ReactNode} - */ - renderTags?: (value: T[], getTagProps: AutocompleteGetTagProps) => React.ReactNode; - /** - * The size of the autocomplete. - * @default 'medium' - */ - size?: 'small' | 'medium'; -} - -export type AutocompleteClassKey = keyof NonNullable< - AutocompleteProps['classes'] ->; - -/** - * - * Demos: - * - * - [Autocomplete](https://material-ui.com/components/autocomplete/) - * - * API: - * - * - [Autocomplete API](https://material-ui.com/api/autocomplete/) - */ -export default function Autocomplete< - T, - Multiple extends boolean | undefined = undefined, - DisableClearable extends boolean | undefined = undefined, - FreeSolo extends boolean | undefined = undefined ->(props: AutocompleteProps): JSX.Element; +export { default } from '@material-ui/core/Autocomplete'; +export * from '@material-ui/core/Autocomplete'; diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js index b0edc3a2257ee8..c3854fa5ed257f 100644 --- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js +++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js @@ -1,880 +1,24 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import clsx from 'clsx'; -import { withStyles } from '@material-ui/core/styles'; -import Popper from '@material-ui/core/Popper'; -import ListSubheader from '@material-ui/core/ListSubheader'; -import Paper from '@material-ui/core/Paper'; -import IconButton from '@material-ui/core/IconButton'; -import Chip from '@material-ui/core/Chip'; -import CloseIcon from '../internal/svg-icons/Close'; -import ArrowDropDownIcon from '../internal/svg-icons/ArrowDropDown'; -import useAutocomplete, { createFilterOptions } from '../useAutocomplete'; - -export { createFilterOptions }; - -export const styles = (theme) => ({ - /* Styles applied to the root element. */ - root: { - '&$focused $clearIndicatorDirty': { - visibility: 'visible', - }, - /* Avoid double tap issue on iOS */ - '@media (pointer: fine)': { - '&:hover $clearIndicatorDirty': { - visibility: 'visible', - }, - }, - }, - /* Styles applied to the root element if `fullWidth={true}`. */ - fullWidth: { - width: '100%', - }, - /* Pseudo-class applied to the root element if focused. */ - focused: {}, - /* Styles applied to the tag elements, e.g. the chips. */ - tag: { - margin: 3, - maxWidth: 'calc(100% - 6px)', - }, - /* Styles applied to the tag elements, e.g. the chips if `size="small"`. */ - tagSizeSmall: { - margin: 2, - maxWidth: 'calc(100% - 4px)', - }, - /* Styles applied when the popup icon is rendered. */ - hasPopupIcon: {}, - /* Styles applied when the clear icon is rendered. */ - hasClearIcon: {}, - /* Styles applied to the Input element. */ - inputRoot: { - flexWrap: 'wrap', - '$hasPopupIcon &, $hasClearIcon &': { - paddingRight: 26 + 4, - }, - '$hasPopupIcon$hasClearIcon &': { - paddingRight: 52 + 4, - }, - '& $input': { - width: 0, - minWidth: 30, - }, - '&[class*="MuiInput-root"]': { - paddingBottom: 1, - '& $input': { - padding: 4, - }, - '& $input:first-child': { - padding: '6px 0', - }, - }, - '&[class*="MuiInput-root"][class*="MuiInput-marginDense"]': { - '& $input': { - padding: '2px 4px 3px', - }, - '& $input:first-child': { - padding: '1px 0 4px', - }, - }, - '&[class*="MuiOutlinedInput-root"]': { - padding: 9, - '$hasPopupIcon &, $hasClearIcon &': { - paddingRight: 26 + 4 + 9, - }, - '$hasPopupIcon$hasClearIcon &': { - paddingRight: 52 + 4 + 9, - }, - '& $input': { - padding: '7.5px 4px', - }, - '& $input:first-child': { - paddingLeft: 6, - }, - '& $endAdornment': { - right: 9, - }, - }, - '&[class*="MuiOutlinedInput-root"][class*="MuiOutlinedInput-marginDense"]': { - padding: 6, - '& $input': { - padding: '2.5px 4px', - }, - }, - '&[class*="MuiFilledInput-root"]': { - paddingTop: 19, - paddingLeft: 8, - '$hasPopupIcon &, $hasClearIcon &': { - paddingRight: 26 + 4 + 9, - }, - '$hasPopupIcon$hasClearIcon &': { - paddingRight: 52 + 4 + 9, - }, - '& $input': { - padding: '7px 4px', - }, - '& $endAdornment': { - right: 9, - }, - }, - '&[class*="MuiFilledInput-root"][class*="MuiFilledInput-marginDense"]': { - paddingBottom: 1, - '& $input': { - padding: '2.5px 4px', - }, - }, - }, - /* Styles applied to the input element. */ - input: { - flexGrow: 1, - textOverflow: 'ellipsis', - opacity: 0, - }, - /* Styles applied to the input element if tag focused. */ - inputFocused: { - opacity: 1, - }, - /* Styles applied to the endAdornment element. */ - endAdornment: { - // We use a position absolute to support wrapping tags. - position: 'absolute', - right: 0, - top: 'calc(50% - 14px)', // Center vertically - }, - /* Styles applied to the clear indicator. */ - clearIndicator: { - marginRight: -2, - padding: 4, - visibility: 'hidden', - }, - /* Styles applied to the clear indicator if the input is dirty. */ - clearIndicatorDirty: {}, - /* Styles applied to the popup indicator. */ - popupIndicator: { - padding: 2, - marginRight: -2, - }, - /* Styles applied to the popup indicator if the popup is open. */ - popupIndicatorOpen: { - transform: 'rotate(180deg)', - }, - /* Styles applied to the popper element. */ - popper: { - zIndex: theme.zIndex.modal, - }, - /* Styles applied to the popper element if `disablePortal={true}`. */ - popperDisablePortal: { - position: 'absolute', - }, - /* Styles applied to the `Paper` component. */ - paper: { - ...theme.typography.body1, - overflow: 'hidden', - margin: '4px 0', - }, - /* Styles applied to the `listbox` component. */ - listbox: { - listStyle: 'none', - margin: 0, - padding: '8px 0', - maxHeight: '40vh', - overflow: 'auto', - }, - /* Styles applied to the loading wrapper. */ - loading: { - color: theme.palette.text.secondary, - padding: '14px 16px', - }, - /* Styles applied to the no option wrapper. */ - noOptions: { - color: theme.palette.text.secondary, - padding: '14px 16px', - }, - /* Styles applied to the option elements. */ - option: { - minHeight: 48, - display: 'flex', - justifyContent: 'flex-start', - alignItems: 'center', - cursor: 'pointer', - paddingTop: 6, - boxSizing: 'border-box', - outline: '0', - WebkitTapHighlightColor: 'transparent', - paddingBottom: 6, - paddingLeft: 16, - paddingRight: 16, - [theme.breakpoints.up('sm')]: { - minHeight: 'auto', - }, - '&[aria-selected="true"]': { - backgroundColor: theme.palette.action.selected, - }, - '&[data-focus="true"]': { - backgroundColor: theme.palette.action.hover, - }, - '&:active': { - backgroundColor: theme.palette.action.selected, - }, - '&[aria-disabled="true"]': { - opacity: theme.palette.action.disabledOpacity, - pointerEvents: 'none', - }, - }, - /* Styles applied to the group's label elements. */ - groupLabel: { - backgroundColor: theme.palette.background.paper, - top: -8, - }, - /* Styles applied to the group's ul elements. */ - groupUl: { - padding: 0, - '& $option': { - paddingLeft: 24, - }, - }, -}); - -function DisablePortal(props) { - // eslint-disable-next-line react/prop-types - const { anchorEl, open, ...other } = props; - return
; -} - -const Autocomplete = React.forwardRef(function Autocomplete(props, ref) { - /* eslint-disable @typescript-eslint/no-unused-vars */ - const { - autoComplete = false, - autoHighlight = false, - autoSelect = false, - blurOnSelect = false, - ChipProps, - classes, - className, - clearOnBlur = !props.freeSolo, - clearOnEscape = false, - clearText = 'Clear', - closeIcon = , - closeText = 'Close', - debug = false, - defaultValue = props.multiple ? [] : null, - disableClearable = false, - disableCloseOnSelect = false, - disabled = false, - disabledItemsFocusable = false, - disableListWrap = false, - disablePortal = false, - filterOptions, - filterSelectedOptions = false, - forcePopupIcon = 'auto', - freeSolo = false, - fullWidth = false, - getLimitTagsText = (more) => `+${more}`, - getOptionDisabled, - getOptionLabel = (option) => option.label ?? option, - getOptionSelected, - groupBy, - handleHomeEndKeys = !props.freeSolo, - id: idProp, - includeInputInList = false, - inputValue: inputValueProp, - limitTags = -1, - ListboxComponent = 'ul', - ListboxProps, - loading = false, - loadingText = 'Loading…', - multiple = false, - noOptionsText = 'No options', - onChange, - onClose, - onHighlightChange, - onInputChange, - onOpen, - open, - openOnFocus = false, - openText = 'Open', - options, - PaperComponent = Paper, - PopperComponent: PopperComponentProp = Popper, - popupIcon = , - renderGroup: renderGroupProp, - renderInput, - renderOption: renderOptionProp, - renderTags, - selectOnFocus = !props.freeSolo, - size = 'medium', - value: valueProp, - ...other - } = props; - /* eslint-enable @typescript-eslint/no-unused-vars */ - - const PopperComponent = disablePortal ? DisablePortal : PopperComponentProp; - - const { - getRootProps, - getInputProps, - getInputLabelProps, - getPopupIndicatorProps, - getClearProps, - getTagProps, - getListboxProps, - getOptionProps, - value, - dirty, - id, - popupOpen, - focused, - focusedTag, - anchorEl, - setAnchorEl, - inputValue, - groupedOptions, - } = useAutocomplete({ ...props, componentName: 'Autocomplete' }); - - let startAdornment; - - if (multiple && value.length > 0) { - const getCustomizedTagProps = (params) => ({ - className: clsx(classes.tag, { - [classes.tagSizeSmall]: size === 'small', - }), - disabled, - ...getTagProps(params), - }); - - if (renderTags) { - startAdornment = renderTags(value, getCustomizedTagProps); - } else { - startAdornment = value.map((option, index) => ( - - )); - } +import React from 'react'; +import Autocomplete from '@material-ui/core/Autocomplete'; + +let warnedOnce = false; + +/** + * @ignore - do not document. + */ +export default React.forwardRef(function DeprecatedAutocomplete(props, ref) { + if (!warnedOnce) { + console.warn( + [ + 'Material-UI: The Autocomplete component was moved from the lab to the core.', + '', + "You should use `import { Autocomplete } from '@material-ui/core'`", + "or `import Autocomplete from '@material-ui/core/Autocomplete'`", + ].join('\n'), + ); + + warnedOnce = true; } - if (limitTags > -1 && Array.isArray(startAdornment)) { - const more = startAdornment.length - limitTags; - if (!focused && more > 0) { - startAdornment = startAdornment.splice(0, limitTags); - startAdornment.push( - - {getLimitTagsText(more)} - , - ); - } - } - - const defaultRenderGroup = (params) => ( -
  • - - {params.group} - -
      {params.children}
    -
  • - ); - - const renderGroup = renderGroupProp || defaultRenderGroup; - const defaultRenderOption = (props2, option) =>
  • {getOptionLabel(option)}
  • ; - const renderOption = renderOptionProp || defaultRenderOption; - - const renderListOption = (option, index) => { - const optionProps = getOptionProps({ option, index }); - - return renderOption({ ...optionProps, className: classes.option }, option, { - selected: optionProps['aria-selected'], - inputValue, - }); - }; - - const hasClearIcon = !disableClearable && !disabled; - const hasPopupIcon = (!freeSolo || forcePopupIcon === true) && forcePopupIcon !== false; - - return ( - -
    - {renderInput({ - id, - disabled, - fullWidth: true, - size: size === 'small' ? 'small' : undefined, - InputLabelProps: getInputLabelProps(), - InputProps: { - ref: setAnchorEl, - className: classes.inputRoot, - startAdornment, - endAdornment: ( -
    - {hasClearIcon ? ( - - {closeIcon} - - ) : null} - - {hasPopupIcon ? ( - - {popupIcon} - - ) : null} -
    - ), - }, - inputProps: { - className: clsx(classes.input, { - [classes.inputFocused]: focusedTag === -1, - }), - disabled, - ...getInputProps(), - }, - })} -
    - {popupOpen && anchorEl ? ( - - - {loading && groupedOptions.length === 0 ? ( -
    {loadingText}
    - ) : null} - {groupedOptions.length === 0 && !freeSolo && !loading ? ( -
    {noOptionsText}
    - ) : null} - {groupedOptions.length > 0 ? ( - - {groupedOptions.map((option, index) => { - if (groupBy) { - return renderGroup({ - key: option.key, - group: option.group, - children: option.options.map((option2, index2) => - renderListOption(option2, option.index + index2), - ), - }); - } - return renderListOption(option, index); - })} - - ) : null} -
    -
    - ) : null} -
    - ); + return ; }); - -Autocomplete.propTypes = { - // ----------------------------- Warning -------------------------------- - // | These PropTypes are generated from the TypeScript type definitions | - // | To update them edit the d.ts file and run "yarn proptypes" | - // ---------------------------------------------------------------------- - /** - * If `true`, the portion of the selected suggestion that has not been typed by the user, - * known as the completion string, appears inline after the input cursor in the textbox. - * The inline completion string is visually highlighted and has a selected state. - * @default false - */ - autoComplete: PropTypes.bool, - /** - * If `true`, the first option is automatically highlighted. - * @default false - */ - autoHighlight: PropTypes.bool, - /** - * If `true`, the selected option becomes the value of the input - * when the Autocomplete loses focus unless the user chooses - * a different option or changes the character string in the input. - * @default false - */ - autoSelect: PropTypes.bool, - /** - * Control if the input should be blurred when an option is selected: - * - * - `false` the input is not blurred. - * - `true` the input is always blurred. - * - `touch` the input is blurred after a touch event. - * - `mouse` the input is blurred after a mouse event. - * @default false - */ - blurOnSelect: PropTypes.oneOfType([PropTypes.oneOf(['mouse', 'touch']), PropTypes.bool]), - /** - * Props applied to the [`Chip`](/api/chip/) element. - */ - ChipProps: PropTypes.object, - /** - * Override or extend the styles applied to the component. - */ - classes: PropTypes.object, - /** - * @ignore - */ - className: PropTypes.string, - /** - * If `true`, the input's text will be cleared on blur if no value is selected. - * - * Set to `true` if you want to help the user enter a new value. - * Set to `false` if you want to help the user resume his search. - * @default !props.freeSolo - */ - clearOnBlur: PropTypes.bool, - /** - * If `true`, clear all values when the user presses escape and the popup is closed. - * @default false - */ - clearOnEscape: PropTypes.bool, - /** - * Override the default text for the *clear* icon button. - * - * For localization purposes, you can use the provided [translations](/guides/localization/). - * @default 'Clear' - */ - clearText: PropTypes.string, - /** - * The icon to display in place of the default close icon. - * @default - */ - closeIcon: PropTypes.node, - /** - * Override the default text for the *close popup* icon button. - * - * For localization purposes, you can use the provided [translations](/guides/localization/). - * @default 'Close' - */ - closeText: PropTypes.string, - /** - * If `true`, the popup will ignore the blur event if the input is filled. - * You can inspect the popup markup with your browser tools. - * Consider this option when you need to customize the component. - * @default false - */ - debug: PropTypes.bool, - /** - * The default input value. Use when the component is not controlled. - * @default props.multiple ? [] : null - */ - defaultValue: PropTypes.any, - /** - * If `true`, the input can't be cleared. - * @default false - */ - disableClearable: PropTypes.bool, - /** - * If `true`, the popup won't close when a value is selected. - * @default false - */ - disableCloseOnSelect: PropTypes.bool, - /** - * If `true`, the input will be disabled. - * @default false - */ - disabled: PropTypes.bool, - /** - * If `true`, will allow focus on disabled items. - * @default false - */ - disabledItemsFocusable: PropTypes.bool, - /** - * If `true`, the list box in the popup will not wrap focus. - * @default false - */ - disableListWrap: PropTypes.bool, - /** - * The `Popper` content will be inside the DOM hierarchy of the parent component. - * @default false - */ - disablePortal: PropTypes.bool, - /** - * A filter function that determines the options that are eligible. - * - * @param {T[]} options The options to render. - * @param {object} state The state of the component. - * @returns {T[]} - */ - filterOptions: PropTypes.func, - /** - * If `true`, hide the selected options from the list box. - * @default false - */ - filterSelectedOptions: PropTypes.bool, - /** - * Force the visibility display of the popup icon. - * @default 'auto' - */ - forcePopupIcon: PropTypes.oneOfType([PropTypes.oneOf(['auto']), PropTypes.bool]), - /** - * If `true`, the Autocomplete is free solo, meaning that the user input is not bound to provided options. - * @default false - */ - freeSolo: PropTypes.bool, - /** - * If `true`, the input will take up the full width of its container. - * @default false - */ - fullWidth: PropTypes.bool, - /** - * The label to display when the tags are truncated (`limitTags`). - * - * @param {number} more The number of truncated tags. - * @returns {ReactNode} - * @default (more) => `+${more}` - */ - getLimitTagsText: PropTypes.func, - /** - * Used to determine the disabled state for a given option. - * - * @param {T} option The option to test. - * @returns {boolean} - */ - getOptionDisabled: PropTypes.func, - /** - * Used to determine the string value for a given option. - * It's used to fill the input (and the list box options if `renderOption` is not provided). - * - * @param {T} option - * @returns {string} - * @default (option) => option.label ?? option - */ - getOptionLabel: PropTypes.func, - /** - * Used to determine if an option is selected, considering the current value. - * Uses strict equality by default. - * - * @param {T} option The option to test. - * @param {T} value The value to test against. - * @returns {boolean} - */ - getOptionSelected: PropTypes.func, - /** - * If provided, the options will be grouped under the returned string. - * The groupBy value is also used as the text for group headings when `renderGroup` is not provided. - * - * @param {T} options The options to group. - * @returns {string} - */ - groupBy: PropTypes.func, - /** - * If `true`, the component handles the "Home" and "End" keys when the popup is open. - * It should move focus to the first option and last option, respectively. - * @default !props.freeSolo - */ - handleHomeEndKeys: PropTypes.bool, - /** - * This prop is used to help implement the accessibility logic. - * If you don't provide this prop. It falls back to a randomly generated id. - */ - id: PropTypes.string, - /** - * If `true`, the highlight can move to the input. - * @default false - */ - includeInputInList: PropTypes.bool, - /** - * The input value. - */ - inputValue: PropTypes.string, - /** - * The maximum number of tags that will be visible when not focused. - * Set `-1` to disable the limit. - * @default -1 - */ - limitTags: PropTypes.number, - /** - * The component used to render the listbox. - * @default 'ul' - */ - ListboxComponent: PropTypes.elementType, - /** - * Props applied to the Listbox element. - */ - ListboxProps: PropTypes.object, - /** - * If `true`, the component is in a loading state. - * @default false - */ - loading: PropTypes.bool, - /** - * Text to display when in a loading state. - * - * For localization purposes, you can use the provided [translations](/guides/localization/). - * @default 'Loading…' - */ - loadingText: PropTypes.node, - /** - * If `true`, `value` must be an array and the menu will support multiple selections. - * @default false - */ - multiple: PropTypes.bool, - /** - * Text to display when there are no options. - * - * For localization purposes, you can use the provided [translations](/guides/localization/). - * @default 'No options' - */ - noOptionsText: PropTypes.node, - /** - * Callback fired when the value changes. - * - * @param {object} event The event source of the callback. - * @param {T|T[]} value The new value of the component. - * @param {string} reason One of "create-option", "select-option", "remove-option", "blur" or "clear". - */ - onChange: PropTypes.func, - /** - * Callback fired when the popup requests to be closed. - * Use in controlled mode (see open). - * - * @param {object} event The event source of the callback. - * @param {string} reason Can be: `"toggleInput"`, `"escape"`, `"select-option"`, `"remove-option"`, `"blur"`. - */ - onClose: PropTypes.func, - /** - * Callback fired when the highlight option changes. - * - * @param {object} event The event source of the callback. - * @param {T} option The highlighted option. - * @param {string} reason Can be: `"keyboard"`, `"auto"`, `"mouse"`. - */ - onHighlightChange: PropTypes.func, - /** - * Callback fired when the input value changes. - * - * @param {object} event The event source of the callback. - * @param {string} value The new value of the text input. - * @param {string} reason Can be: `"input"` (user input), `"reset"` (programmatic change), `"clear"`. - */ - onInputChange: PropTypes.func, - /** - * Callback fired when the popup requests to be opened. - * Use in controlled mode (see open). - * - * @param {object} event The event source of the callback. - */ - onOpen: PropTypes.func, - /** - * Control the popup` open state. - */ - open: PropTypes.bool, - /** - * If `true`, the popup will open on input focus. - * @default false - */ - openOnFocus: PropTypes.bool, - /** - * Override the default text for the *open popup* icon button. - * - * For localization purposes, you can use the provided [translations](/guides/localization/). - * @default 'Open' - */ - openText: PropTypes.string, - /** - * Array of options. - */ - options: PropTypes.array.isRequired, - /** - * The component used to render the body of the popup. - * @default Paper - */ - PaperComponent: PropTypes.elementType, - /** - * The component used to position the popup. - * @default Popper - */ - PopperComponent: PropTypes.elementType, - /** - * The icon to display in place of the default popup icon. - * @default - */ - popupIcon: PropTypes.node, - /** - * Render the group. - * - * @param {any} option The group to render. - * @returns {ReactNode} - */ - renderGroup: PropTypes.func, - /** - * Render the input. - * - * @param {object} params - * @returns {ReactNode} - */ - renderInput: PropTypes.func.isRequired, - /** - * Render the option, use `getOptionLabel` by default. - * - * @param {object} props The props to apply on the li element. - * @param {T} option The option to render. - * @param {object} state The state of the component. - * @returns {ReactNode} - */ - renderOption: PropTypes.func, - /** - * Render the selected value. - * - * @param {T[]} value The `value` provided to the component. - * @param {function} getTagProps A tag props getter. - * @returns {ReactNode} - */ - renderTags: PropTypes.func, - /** - * If `true`, the input's text will be selected on focus. - * It helps the user clear the selected value. - * @default !props.freeSolo - */ - selectOnFocus: PropTypes.bool, - /** - * The size of the autocomplete. - * @default 'medium' - */ - size: PropTypes.oneOf(['medium', 'small']), - /** - * The value of the autocomplete. - * - * The value must have reference equality with the option in order to be selected. - * You can customize the equality behavior with the `getOptionSelected` prop. - */ - value: PropTypes.any, -}; - -export default withStyles(styles, { name: 'MuiAutocomplete' })(Autocomplete); diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js index 5ea98df02ed999..e69de29bb2d1d6 100644 --- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js +++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js @@ -1,2070 +0,0 @@ -import * as React from 'react'; -import { expect } from 'chai'; -import { - getClasses, - createMount, - describeConformance, - act, - createClientRender, - fireEvent, - screen, -} from 'test/utils'; -import { spy } from 'sinon'; -import TextField from '@material-ui/core/TextField'; -import Chip from '@material-ui/core/Chip'; -import { createFilterOptions } from '../useAutocomplete/useAutocomplete'; -import Autocomplete from './Autocomplete'; - -describe('', () => { - const mount = createMount(); - let classes; - const render = createClientRender(); - const defaultProps = { - options: [], - openOnFocus: true, - }; - - before(() => { - classes = getClasses( null} />); - }); - - describeConformance( null} />, () => ({ - classes, - inheritComponent: 'div', - mount, - refInstanceof: window.HTMLDivElement, - testComponentPropWith: 'div', - })); - - describe('combobox', () => { - it('should clear the input when blur', () => { - const { getByRole } = render( - } />, - ); - const input = getByRole('textbox'); - - act(() => { - input.focus(); - fireEvent.change(document.activeElement, { target: { value: 'a' } }); - }); - - expect(input.value).to.equal('a'); - - act(() => { - document.activeElement.blur(); - }); - expect(input.value).to.equal(''); - }); - - it('should apply the icon classes', () => { - const { container } = render( - } />, - ); - expect(container.querySelector(`.${classes.root}`)).to.have.class(classes.hasClearIcon); - expect(container.querySelector(`.${classes.root}`)).to.have.class(classes.hasPopupIcon); - }); - }); - - describe('prop: loading', () => { - it('should show a loading message when open', () => { - render( - } - />, - ); - fireEvent.keyDown(screen.getByRole('textbox'), { key: 'ArrowDown' }); - expect(document.querySelector(`.${classes.paper}`).textContent).to.equal('Loading…'); - }); - }); - - describe('prop: autoHighlight', () => { - it('should set the focus on the first item', () => { - const options = ['one', 'two']; - const { getByRole } = render( - } - />, - ); - - function checkHighlightIs(expected) { - expect(getByRole('listbox').querySelector('li[data-focus]')).to.have.text(expected); - } - - checkHighlightIs('one'); - fireEvent.change(document.activeElement, { target: { value: 'oo' } }); - fireEvent.change(document.activeElement, { target: { value: 'o' } }); - checkHighlightIs('one'); - }); - - it('should keep the highlight on the first item', () => { - const options = ['one', 'two']; - const { getByRole } = render( - } - />, - ); - - function checkHighlightIs(expected) { - expect(getByRole('listbox').querySelector('li[data-focus]')).to.have.text(expected); - } - - checkHighlightIs('one'); - fireEvent.change(document.activeElement, { target: { value: 'two' } }); - checkHighlightIs('two'); - }); - - it('should set the focus on the first item when possible', () => { - const options = ['one', 'two']; - const { getByRole, setProps } = render( - } - />, - ); - const textbox = getByRole('textbox'); - expect(textbox).not.to.have.attribute('aria-activedescendant'); - - setProps({ options, loading: false }); - expect(textbox).to.have.attribute( - 'aria-activedescendant', - screen.getAllByRole('option')[0].getAttribute('id'), - ); - }); - - it('should set the highlight on selected item when dropdown is expanded', () => { - const { getByRole, setProps } = render( - } - />, - ); - - function checkHighlightIs(expected) { - expect(getByRole('listbox').querySelector('li[data-focus]')).to.have.text(expected); - } - - checkHighlightIs('one'); - setProps({ value: 'two' }); - checkHighlightIs('two'); - }); - - it('should keep the current highlight if possible', () => { - const { getByRole } = render( - } - />, - ); - const textbox = getByRole('textbox'); - - function checkHighlightIs(expected) { - expect(getByRole('listbox').querySelector('li[data-focus]')).to.have.text(expected); - } - - checkHighlightIs('one'); - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - checkHighlightIs('two'); - fireEvent.keyDown(textbox, { key: 'Enter' }); - checkHighlightIs('two'); - }); - }); - - describe('highlight synchronisation', () => { - it('should not update the highlight when multiple open and value change', () => { - const { setProps, getByRole } = render( - } - />, - ); - - function checkHighlightIs(expected) { - expect(getByRole('listbox').querySelector('li[data-focus]')).to.have.text(expected); - } - - checkHighlightIs('two'); - setProps({ - value: [], - }); - checkHighlightIs('two'); - }); - }); - - describe('prop: limitTags', () => { - it('show all items on focus', () => { - const { container, getAllByRole, getByRole } = render( - } - />, - ); - - expect(container.textContent).to.equal('onetwo+1'); - // include hidden clear button because JSDOM thinks it's visible - expect(getAllByRole('button', { hidden: true })).to.have.lengthOf(4); - - act(() => { - getByRole('textbox').focus(); - }); - expect(container.textContent).to.equal('onetwothree'); - expect(getAllByRole('button', { hidden: false })).to.have.lengthOf(5); - }); - - it('show 0 item on close when set 0 to limitTags', () => { - const { container, getAllByRole, getByRole } = render( - } - />, - ); - - expect(container.textContent).to.equal('+3'); - // include hidden clear button because JSDOM thinks it's visible - expect(getAllByRole('button', { hidden: true })).to.have.lengthOf(2); - - act(() => { - getByRole('textbox').focus(); - }); - expect(container.textContent).to.equal('onetwothree'); - expect(getAllByRole('button', { hidden: false })).to.have.lengthOf(5); - }); - }); - - describe('prop: filterSelectedOptions', () => { - it('when the last item is selected, highlights the new last item', () => { - const { getByRole } = render( - } - />, - ); - const textbox = getByRole('textbox'); - - function checkHighlightIs(expected) { - expect(getByRole('listbox').querySelector('li[data-focus]')).to.have.text(expected); - } - - fireEvent.keyDown(textbox, { key: 'ArrowUp' }); - checkHighlightIs('three'); - fireEvent.keyDown(textbox, { key: 'Enter' }); // selects the last option (three) - const input = getByRole('textbox'); - act(() => { - input.blur(); - input.focus(); // opens the listbox again - }); - checkHighlightIs('two'); - }); - }); - - describe('prop: autoSelect', () => { - it('should not clear on blur when value does not match any option', () => { - const handleChange = spy(); - const options = ['one', 'two']; - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.change(textbox, { target: { value: 'o' } }); - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.change(textbox, { target: { value: 'oo' } }); - act(() => { - textbox.blur(); - }); - - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.deep.equal('oo'); - }); - - it('should add new value when autoSelect & multiple on blur', () => { - const handleChange = spy(); - const options = ['one', 'two']; - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - act(() => { - fireEvent.change(textbox, { target: { value: 't' } }); - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - textbox.blur(); - }); - - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.deep.equal(options); - }); - - it('should add new value when autoSelect & multiple & freeSolo on blur', () => { - const handleChange = spy(); - render( - } - />, - ); - - fireEvent.change(document.activeElement, { target: { value: 'a' } }); - act(() => { - document.activeElement.blur(); - }); - - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.deep.equal(['a']); - }); - }); - - describe('prop: multiple', () => { - it('should not crash', () => { - const { getByRole } = render( - } - multiple - />, - ); - const input = getByRole('textbox'); - - act(() => { - input.focus(); - document.activeElement.blur(); - input.focus(); - }); - }); - - it('should remove the last option', () => { - const handleChange = spy(); - const options = ['one', 'two']; - const { getAllByTestId } = render( - } - multiple - />, - ); - fireEvent.click(getAllByTestId('CancelIcon')[1]); - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.deep.equal([options[0]]); - }); - - it('navigates between different tags', () => { - const handleChange = spy(); - const options = ['one', 'two']; - render( - } - multiple - />, - ); - const textbox = screen.getByRole('textbox'); - const [firstSelectedValue, secondSelectedValue] = screen.getAllByRole('button'); - - fireEvent.keyDown(textbox, { key: 'ArrowLeft' }); - expect(secondSelectedValue).toHaveFocus(); - - fireEvent.keyDown(secondSelectedValue, { key: 'ArrowLeft' }); - expect(firstSelectedValue).toHaveFocus(); - - fireEvent.keyDown(firstSelectedValue, { key: 'Backspace' }); - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.deep.equal([options[1]]); - expect(textbox).toHaveFocus(); - }); - - it('should not crash if a tag is missing', () => { - const handleChange = spy(); - const options = ['one', 'two']; - render( - - value - .filter((x, index) => index === 1) - .map((option, index) => ) - } - onChange={handleChange} - renderInput={(params) => } - multiple - />, - ); - const textbox = screen.getByRole('textbox'); - const [firstSelectedValue] = screen.getAllByRole('button'); - - fireEvent.keyDown(textbox, { key: 'ArrowLeft' }); - // skip value "two" - expect(firstSelectedValue).toHaveFocus(); - - fireEvent.keyDown(firstSelectedValue, { key: 'ArrowRight' }); - expect(textbox).toHaveFocus(); - }); - - it('has no textbox value', () => { - render( - } - multiple - value={['one', 'two']} - />, - ); - - expect(screen.getByRole('textbox')).to.have.property('value', ''); - }); - - it('should fail validation if a required field has no value', function test() { - if (/jsdom/.test(window.navigator.userAgent)) { - // Enable once https://github.com/jsdom/jsdom/issues/2898 is resolved - this.skip(); - } - - const handleSubmit = spy((event) => event.preventDefault()); - render( -
    - } - value={[]} - /> - - , - ); - - screen.getByRole('button', { name: 'Submit' }).click(); - - expect(handleSubmit.callCount).to.equal(0); - }); - - it('should fail validation if a required field has a value', function test() { - // Unclear how native Constraint validation can be enabled for `multiple` - if (/jsdom/.test(window.navigator.userAgent)) { - // Enable once https://github.com/jsdom/jsdom/issues/2898 is resolved - // The test is passing in JSDOM but form validation is buggy in JSDOM so we rather skip than have false confidence - this.skip(); - } - - const handleSubmit = spy((event) => event.preventDefault()); - render( -
    - } - value={['one']} - /> - - , - ); - - screen.getByRole('button', { name: 'Submit' }).click(); - - expect(handleSubmit.callCount).to.equal(0); - }); - }); - - it('should trigger a form expectedly', () => { - const handleSubmit = spy(); - const { setProps } = render( - { - if (!event.defaultPrevented && event.key === 'Enter') { - handleSubmit(); - } - }} - renderInput={(props2) => } - />, - ); - let textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(handleSubmit.callCount).to.equal(1); - - fireEvent.change(textbox, { target: { value: 'o' } }); - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(handleSubmit.callCount).to.equal(1); - - fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(handleSubmit.callCount).to.equal(2); - - setProps({ key: 'test-2', multiple: true, freeSolo: true }); - textbox = screen.getByRole('textbox'); - - fireEvent.change(textbox, { target: { value: 'o' } }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(handleSubmit.callCount).to.equal(2); - - fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(handleSubmit.callCount).to.equal(3); - - setProps({ key: 'test-3', freeSolo: true }); - textbox = screen.getByRole('textbox'); - - fireEvent.change(textbox, { target: { value: 'o' } }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(handleSubmit.callCount).to.equal(4); - }); - - describe('prop: getOptionDisabled', () => { - it('should disable the option but allow focus with disabledItemsFocusable', () => { - const handleSubmit = spy(); - const handleChange = spy(); - const { getAllByRole } = render( - option === 'two'} - onKeyDown={(event) => { - if (!event.defaultPrevented && event.key === 'Enter') { - handleSubmit(); - } - }} - onChange={handleChange} - renderInput={(props2) => } - />, - ); - - let options; - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - options = getAllByRole('option'); - expect(textbox).to.have.attribute('aria-activedescendant', options[1].getAttribute('id')); - - fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(handleSubmit.callCount).to.equal(0); - expect(handleChange.callCount).to.equal(0); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - options = getAllByRole('option'); - expect(textbox).to.have.attribute('aria-activedescendant', options[0].getAttribute('id')); - - fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(handleSubmit.callCount).to.equal(0); - expect(handleChange.callCount).to.equal(1); - }); - }); - - describe('WAI-ARIA conforming markup', () => { - specify('when closed', () => { - const { getAllByRole, getByRole, queryByRole } = render( - } />, - ); - - const combobox = getByRole('combobox'); - expect(combobox).to.have.attribute('aria-expanded', 'false'); - // reflected aria-haspopup is `listbox` - // this assertion can fail if the value is `listbox` - expect(combobox).not.to.have.attribute('aria-haspopup'); - - const textbox = getByRole('textbox'); - expect(combobox).to.contain(textbox); - // reflected aria-multiline has to be false i.e. not present or false - expect(textbox).not.to.have.attribute('aria-multiline'); - expect(textbox).to.have.attribute('aria-autocomplete', 'list'); - expect(textbox, 'no option is focused when openened').not.to.have.attribute( - 'aria-activedescendant', - ); - - // listbox is not only inaccessible but not in the DOM - const listbox = queryByRole('listbox', { hidden: true }); - expect(listbox).to.equal(null); - - const buttons = getAllByRole('button', { hidden: true }); - if (!/jsdom/.test(window.navigator.userAgent)) { - expect(buttons[0]).toBeInaccessible(); - } else { - // JSDOM thinks the "Clear"-button has `visibility: visible` - // Leaving this to be notified once the JSDOM is fixed. - expect(buttons[0]).not.toBeInaccessible(); - } - expect(buttons[1]).toHaveAccessibleName('Open'); - expect(buttons[1]).to.have.attribute('title', 'Open'); - expect(buttons).to.have.length(2); - buttons.forEach((button) => { - expect(button, 'button is not in tab order').to.have.property('tabIndex', -1); - }); - }); - - specify('when open', () => { - const { getAllByRole, getByRole } = render( - } - />, - ); - - const combobox = getByRole('combobox'); - expect(combobox).to.have.attribute('aria-expanded', 'true'); - - const textbox = getByRole('textbox'); - - const listbox = getByRole('listbox'); - expect(combobox, 'combobox owns listbox').to.have.attribute( - 'aria-owns', - listbox.getAttribute('id'), - ); - expect(textbox).to.have.attribute('aria-controls', listbox.getAttribute('id')); - expect(textbox, 'no option is focused when openened').not.to.have.attribute( - 'aria-activedescendant', - ); - - const options = getAllByRole('option'); - expect(options).to.have.length(2); - options.forEach((option) => { - expect(listbox).to.contain(option); - }); - - const buttons = getAllByRole('button', { hidden: true }); - if (!/jsdom/.test(window.navigator.userAgent)) { - expect(buttons[0]).toBeInaccessible(); - } - expect(buttons[1]).toHaveAccessibleName('Close'); - expect(buttons[1]).to.have.attribute('title', 'Close'); - expect(buttons).to.have.length(2); - buttons.forEach((button) => { - expect(button, 'button is not in tab order').to.have.property('tabIndex', -1); - }); - }); - - it('should add and remove aria-activedescendant', () => { - const { getAllByRole, getByRole, setProps } = render( - } - />, - ); - const textbox = getByRole('textbox'); - expect(textbox, 'no option is focused when openened').not.to.have.attribute( - 'aria-activedescendant', - ); - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - - const options = getAllByRole('option'); - expect(textbox).to.have.attribute('aria-activedescendant', options[0].getAttribute('id')); - setProps({ open: false }); - expect(textbox, 'no option is focused when openened').not.to.have.attribute( - 'aria-activedescendant', - ); - }); - }); - - describe('when popup closed', () => { - it('opens when the textbox is focused', () => { - const handleOpen = spy(); - render( - } - />, - ); - - expect(handleOpen.callCount).to.equal(1); - }); - - it('does not open on clear', () => { - const handleOpen = spy(); - const handleChange = spy(); - const { container } = render( - } - />, - ); - - const clear = container.querySelector('button'); - fireEvent.click(clear); - - expect(handleOpen.callCount).to.equal(0); - expect(handleChange.callCount).to.equal(1); - }); - - ['ArrowDown', 'ArrowUp'].forEach((key) => { - it(`opens on ${key} when focus is on the textbox without moving focus`, () => { - const handleOpen = spy(); - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key }); - - // first from focus - expect(handleOpen.callCount).to.equal(2); - expect(textbox).not.to.have.attribute('aria-activedescendant'); - }); - }); - - it('does not clear the textbox on Escape', () => { - const handleChange = spy(); - render( - } - />, - ); - - fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Escape' }); - - expect(handleChange.callCount).to.equal(0); - }); - }); - - describe('prop: clearOnEscape', () => { - it('should clear on escape', () => { - const handleChange = spy(); - render( - } - />, - ); - - fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Escape' }); - fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Escape' }); - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.deep.equal([]); - }); - }); - - describe('when popup open', () => { - it('closes the popup if Escape is pressed ', () => { - const handleClose = spy(); - render( - } - />, - ); - - fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Escape' }); - expect(handleClose.callCount).to.equal(1); - }); - - it('does not close the popup when option selected if Control is pressed', () => { - const handleClose = spy(); - const { getAllByRole } = render( - } - />, - ); - - const options = getAllByRole('option'); - fireEvent.click(options[0], { ctrlKey: true }); - expect(handleClose.callCount).to.equal(0); - }); - - it('does not close the popup when option selected if Meta is pressed', () => { - const handleClose = spy(); - const { getAllByRole } = render( - } - />, - ); - - const options = getAllByRole('option'); - fireEvent.click(options[0], { metaKey: true }); - expect(handleClose.callCount).to.equal(0); - }); - - it('moves focus to the first option on ArrowDown', () => { - const { getAllByRole, getByRole } = render( - } - />, - ); - - fireEvent.keyDown(screen.getByRole('textbox'), { key: 'ArrowDown' }); - expect(getByRole('textbox')).to.have.attribute( - 'aria-activedescendant', - getAllByRole('option')[0].getAttribute('id'), - ); - }); - - it('moves focus to the last option on ArrowUp', () => { - const { getAllByRole, getByRole } = render( - } - />, - ); - - fireEvent.keyDown(screen.getByRole('textbox'), { key: 'ArrowUp' }); - const options = getAllByRole('option'); - expect(getByRole('textbox')).to.have.attribute( - 'aria-activedescendant', - options[options.length - 1].getAttribute('id'), - ); - }); - }); - - describe('prop: openOnFocus', () => { - it('enables open on input focus', () => { - const { getByRole } = render( - } - />, - ); - const textbox = getByRole('textbox'); - const combobox = getByRole('combobox'); - - expect(combobox).to.have.attribute('aria-expanded', 'true'); - expect(textbox).toHaveFocus(); - - fireEvent.mouseDown(textbox); - fireEvent.click(textbox); - expect(combobox).to.have.attribute('aria-expanded', 'false'); - - act(() => { - document.activeElement.blur(); - }); - - expect(combobox).to.have.attribute('aria-expanded', 'false'); - expect(textbox).not.toHaveFocus(); - - fireEvent.mouseDown(textbox); - fireEvent.click(textbox); - expect(combobox).to.have.attribute('aria-expanded', 'true'); - expect(textbox).toHaveFocus(); - - fireEvent.mouseDown(textbox); - fireEvent.click(textbox); - expect(combobox).to.have.attribute('aria-expanded', 'false'); - }); - }); - - describe('listbox wrapping behavior', () => { - it('wraps around when navigating the list by default', () => { - const { getAllByRole } = render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'ArrowUp' }); - - const options = getAllByRole('option'); - expect(textbox).toHaveFocus(); - expect(textbox).to.have.attribute( - 'aria-activedescendant', - options[options.length - 1].getAttribute('id'), - ); - }); - - it('selects the first item if on the last item and pressing up by default', () => { - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowUp' }); - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - - const options = screen.getAllByRole('option'); - expect(textbox).toHaveFocus(); - expect(textbox).to.have.attribute('aria-activedescendant', options[0].getAttribute('id')); - }); - - describe('prop: includeInputInList', () => { - it('considers the textbox the predessor of the first option when pressing Up', () => { - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'ArrowUp' }); - - expect(textbox).toHaveFocus(); - expect(textbox).not.to.have.attribute('aria-activedescendant'); - }); - - it('considers the textbox the successor of the last option when pressing Down', () => { - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowUp' }); - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - - expect(textbox).toHaveFocus(); - expect(textbox).not.to.have.attribute('aria-activedescendant'); - }); - }); - - describe('prop: disableListWrap', () => { - it('keeps focus on the first item if focus is on the first item and pressing Up', () => { - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'ArrowUp' }); - - expect(textbox).toHaveFocus(); - expect(textbox).to.have.attribute( - 'aria-activedescendant', - screen.getAllByRole('option')[0].getAttribute('id'), - ); - }); - - it('focuses the last item when pressing Up when no option is active', () => { - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowUp' }); - - const options = screen.getAllByRole('option'); - expect(textbox).toHaveFocus(); - expect(textbox).to.have.attribute( - 'aria-activedescendant', - options[options.length - 1].getAttribute('id'), - ); - }); - - it('keeps focus on the last item if focus is on the last item and pressing Down', () => { - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - - const options = screen.getAllByRole('option'); - expect(textbox).toHaveFocus(); - expect(textbox).to.have.attribute( - 'aria-activedescendant', - options[options.length - 1].getAttribute('id'), - ); - }); - }); - }); - - describe('prop: disabled', () => { - it('should disable the input', () => { - const { getByRole } = render( - } - />, - ); - const input = getByRole('textbox'); - expect(input.disabled).to.equal(true); - }); - - it('should disable the popup button', () => { - const { queryByTitle } = render( - } - />, - ); - expect(queryByTitle('Open').disabled).to.equal(true); - }); - - it('should not render the clear button', () => { - const { queryByTitle } = render( - } - />, - ); - expect(queryByTitle('Clear')).to.equal(null); - }); - - it('should not apply the hasClearIcon class', () => { - const { container } = render( - } - />, - ); - expect(container.querySelector(`.${classes.root}`)).not.to.have.class(classes.hasClearIcon); - expect(container.querySelector(`.${classes.root}`)).to.have.class(classes.hasPopupIcon); - }); - }); - - describe('prop: disableClearable', () => { - it('should not render the clear button', () => { - const { queryByTitle, container } = render( - } - />, - ); - expect(queryByTitle('Clear')).to.equal(null); - expect(container.querySelector(`.${classes.root}`)).to.have.class(classes.hasPopupIcon); - expect(container.querySelector(`.${classes.root}`)).not.to.have.class(classes.hasClearIcon); - }); - }); - - describe('warnings', () => { - it('warn if getOptionLabel do not return a string', () => { - const handleChange = spy(); - render( - option.name} - renderInput={(params) => } - />, - ); - const textbox = screen.getByRole('textbox'); - - expect(() => { - fireEvent.change(textbox, { target: { value: 'a' } }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - }).toErrorDev([ - 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', - // strict mode renders twice - 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', - 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', - 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', - ]); - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.equal('a'); - }); - - it('warn if getOptionSelected match multiple values for a given option', () => { - const value = [ - { id: '10', text: 'One' }, - { id: '20', text: 'Two' }, - ]; - const options = [ - { id: '10', text: 'One' }, - { id: '20', text: 'Two' }, - { id: '30', text: 'Three' }, - ]; - - render( - option.text} - getOptionSelected={(option) => value.find((v) => v.id === option.id)} - renderInput={(params) => } - />, - ); - const textbox = screen.getByRole('textbox'); - - expect(() => { - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - }).toErrorDev( - 'The component expects a single value to match a given option but found 2 matches.', - ); - }); - - it('warn if value does not exist in options list', () => { - const value = 'not a good value'; - const options = ['first option', 'second option']; - - expect(() => { - render( - } - />, - ); - }).toWarnDev([ - 'None of the options match with `"not a good value"`', - // strict mode renders twice - 'None of the options match with `"not a good value"`', - 'None of the options match with `"not a good value"`', - 'None of the options match with `"not a good value"`', - ]); - }); - - it('warn if groups options are not sorted', () => { - const data = [ - { group: 1, value: 'A' }, - { group: 2, value: 'D' }, - { group: 2, value: 'E' }, - { group: 1, value: 'B' }, - { group: 3, value: 'G' }, - { group: 2, value: 'F' }, - { group: 1, value: 'C' }, - ]; - expect(() => { - render( - option.value} - renderInput={(params) => } - groupBy={(option) => option.group} - />, - ); - }).toWarnDev([ - // strict mode renders twice - 'returns duplicated headers', - 'returns duplicated headers', - ]); - const options = screen.getAllByRole('option').map((el) => el.textContent); - expect(options).to.have.length(7); - expect(options).to.deep.equal(['A', 'D', 'E', 'B', 'G', 'F', 'C']); - }); - }); - - describe('prop: options', () => { - it('should keep focus on selected option and not reset to top option when options updated', () => { - const { setProps } = render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - const listbox = screen.getByRole('listbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); // goes to 'one' - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); // goes to 'two' - - function checkHighlightIs(expected) { - expect(listbox.querySelector('li[data-focus]')).to.have.text(expected); - } - - checkHighlightIs('two'); - - // three option is added and autocomplete re-renders, two should still be highlighted - setProps({ options: ['one', 'two', 'three'] }); - checkHighlightIs('two'); - }); - - it('should not select undefined', () => { - const handleChange = spy(); - const { getByRole } = render( - } - />, - ); - const input = getByRole('textbox'); - fireEvent.click(input); - - const listbox = getByRole('listbox'); - const firstOption = listbox.querySelector('li'); - fireEvent.click(firstOption); - - expect(handleChange.args[0][1]).to.equal('one'); - }); - - it('should work if options are the default data structure', () => { - const options = [ - { - label: 'one', - }, - ]; - const handleChange = spy(); - const { getByRole } = render( - } - />, - ); - - const input = getByRole('textbox'); - fireEvent.click(input); - - const listbox = getByRole('listbox'); - const htmlOptions = listbox.querySelectorAll('li'); - - expect(htmlOptions[0].innerHTML).to.equal('one'); - }); - }); - - describe('enter', () => { - it('select a single value when enter is pressed', () => { - const handleChange = spy(); - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.equal('one'); - fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(handleChange.callCount).to.equal(1); - }); - - it('select multiple value when enter is pressed', () => { - const handleChange = spy(); - const options = [{ name: 'one' }, { name: 'two ' }]; - render( - option.name} - renderInput={(params) => } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.deep.equal([options[0]]); - fireEvent.keyDown(textbox, { key: 'Enter' }); - expect(handleChange.callCount).to.equal(1); - }); - }); - - describe('prop: autoComplete', () => { - it('add a completion string', () => { - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.change(document.activeElement, { target: { value: 'O' } }); - - expect(document.activeElement.value).to.equal('O'); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - - expect(document.activeElement.value).to.equal('one'); - expect(document.activeElement.selectionStart).to.equal(1); - expect(document.activeElement.selectionEnd).to.equal(3); - - fireEvent.keyDown(textbox, { key: 'Enter' }); - - expect(document.activeElement.value).to.equal('one'); - expect(document.activeElement.selectionStart).to.equal(3); - expect(document.activeElement.selectionEnd).to.equal(3); - }); - }); - - describe('click input', () => { - it('toggles if empty', () => { - const { getByRole } = render( - } - />, - ); - const textbox = getByRole('textbox'); - const combobox = getByRole('combobox'); - expect(combobox).to.have.attribute('aria-expanded', 'false'); - fireEvent.mouseDown(textbox); - expect(combobox).to.have.attribute('aria-expanded', 'true'); - fireEvent.mouseDown(textbox); - expect(combobox).to.have.attribute('aria-expanded', 'false'); - }); - - it('selects all the first time', () => { - const { getByRole } = render( - } - />, - ); - const textbox = getByRole('textbox'); - fireEvent.click(textbox); - expect(textbox.selectionStart).to.equal(0); - expect(textbox.selectionEnd).to.equal(3); - }); - - it('should focus the input when clicking on the open action', () => { - const { getByRole, queryByTitle } = render( - } - />, - ); - - const textbox = getByRole('textbox'); - fireEvent.click(textbox); - expect(textbox).toHaveFocus(); - - act(() => { - textbox.blur(); - }); - fireEvent.click(queryByTitle('Open')); - - expect(textbox).toHaveFocus(); - }); - - it('should mantain list box open clicking on input when it is not empty', () => { - const handleHighlightChange = spy(); - const { getByRole, getAllByRole } = render( - } - />, - ); - const combobox = getByRole('combobox'); - const textbox = getByRole('textbox'); - - expect(combobox).to.have.attribute('aria-expanded', 'false'); - fireEvent.mouseDown(textbox); // Open listbox - expect(combobox).to.have.attribute('aria-expanded', 'true'); - const options = getAllByRole('option'); - fireEvent.click(options[0]); - expect(combobox).to.have.attribute('aria-expanded', 'false'); - fireEvent.mouseDown(textbox); // Open listbox - expect(combobox).to.have.attribute('aria-expanded', 'true'); - fireEvent.mouseDown(textbox); // Remain open listbox - expect(combobox).to.have.attribute('aria-expanded', 'true'); - }); - - it('should not toggle list box', () => { - const handleHighlightChange = spy(); - const { getByRole } = render( - } - />, - ); - const combobox = getByRole('combobox'); - const textbox = getByRole('textbox'); - - expect(combobox).to.have.attribute('aria-expanded', 'false'); - fireEvent.mouseDown(textbox); - expect(combobox).to.have.attribute('aria-expanded', 'true'); - fireEvent.mouseDown(textbox); - expect(combobox).to.have.attribute('aria-expanded', 'true'); - }); - }); - - describe('controlled', () => { - it('controls the input value', () => { - const handleChange = spy(); - function MyComponent() { - const [, setInputValue] = React.useState(''); - const handleInputChange = (event, value) => { - handleChange(value); - setInputValue(value); - }; - return ( - } - /> - ); - } - - render(); - - expect(handleChange.callCount).to.equal(0); - fireEvent.change(document.activeElement, { target: { value: 'a' } }); - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][0]).to.equal('a'); - expect(document.activeElement.value).to.equal(''); - }); - - it('should fire the input change event before the change event', () => { - const handleChange = spy(); - const handleInputChange = spy(); - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - - expect(handleInputChange.calledBefore(handleChange)).to.equal(true); - }); - }); - - describe('prop: filterOptions', () => { - it('should ignore object keys by default', () => { - const { queryAllByRole } = render( - option.label} - renderInput={(params) => } - />, - ); - let options; - options = queryAllByRole('option'); - expect(options.length).to.equal(2); - - fireEvent.change(document.activeElement, { target: { value: 'value' } }); - options = queryAllByRole('option'); - expect(options.length).to.equal(0); - - fireEvent.change(document.activeElement, { target: { value: 'one' } }); - options = queryAllByRole('option'); - expect(options.length).to.equal(1); - }); - - it('limits the amount of rendered options when `limit` is set in `createFilterOptions`', () => { - const filterOptions = createFilterOptions({ limit: 2 }); - const { queryAllByRole } = render( - } - filterOptions={filterOptions} - />, - ); - expect(queryAllByRole('option').length).to.equal(2); - }); - - it('does not limit the amount of rendered options when `limit` is not set in `createFilterOptions`', () => { - const filterOptions = createFilterOptions({}); - const { queryAllByRole } = render( - } - filterOptions={filterOptions} - />, - ); - expect(queryAllByRole('option').length).to.equal(3); - }); - }); - - describe('prop: freeSolo', () => { - it('pressing twice enter should not call onChange listener twice', () => { - const handleChange = spy(); - const options = [{ name: 'foo' }]; - render( - option.name} - renderInput={(params) => } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.deep.equal(options[0]); - - fireEvent.keyDown(textbox, { key: 'Enter' }); - - expect(handleChange.callCount).to.equal(1); - }); - - it('should not delete exiting tag when try to add it twice', () => { - const handleChange = spy(); - const options = ['one', 'two']; - const { container } = render( - } - multiple - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.change(textbox, { target: { value: 'three' } }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - - expect(container.querySelectorAll('[class*="MuiChip-root"]')).to.have.length(3); - - fireEvent.change(textbox, { target: { value: 'three' } }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - - expect(container.querySelectorAll('[class*="MuiChip-root"]')).to.have.length(3); - }); - - it('should not fire change event until the IME is confirmed', () => { - const handleChange = spy(); - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - // Actual behavior when "あ" (Japanese) is entered on macOS/Safari with IME - fireEvent.change(textbox, { target: { value: 'あ' } }); - fireEvent.keyDown(textbox, { key: 'Enter', keyCode: 229 }); - - expect(handleChange.callCount).to.equal(0); - - fireEvent.keyDown(textbox, { key: 'Enter', keyCode: 13 }); - - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.equal('あ'); - }); - }); - - describe('prop: onChange', () => { - it('provides a reason and details on option creation', () => { - const handleChange = spy(); - const options = ['one', 'two', 'three']; - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.change(textbox, { target: { value: options[2] } }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.equal(options[2]); - expect(handleChange.args[0][2]).to.equal('create-option'); - expect(handleChange.args[0][3]).to.deep.equal({ option: options[2] }); - }); - - it('provides a reason and details on option selection', () => { - const handleChange = spy(); - const options = ['one', 'two', 'three']; - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.equal(options[0]); - expect(handleChange.args[0][2]).to.equal('select-option'); - expect(handleChange.args[0][3]).to.deep.equal({ option: options[0] }); - }); - - it('provides a reason and details on option removing', () => { - const handleChange = spy(); - const options = ['one', 'two', 'three']; - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'Backspace' }); - - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.deep.equal(options.slice(0, 2)); - expect(handleChange.args[0][2]).to.equal('remove-option'); - expect(handleChange.args[0][3]).to.deep.equal({ option: options[2] }); - }); - - it('provides a reason and details on blur', () => { - const handleChange = spy(); - const options = ['one', 'two', 'three']; - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - act(() => { - textbox.blur(); - }); - - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.equal(options[0]); - expect(handleChange.args[0][2]).to.equal('blur'); - expect(handleChange.args[0][3]).to.deep.equal({ option: options[0] }); - }); - - it('provides a reason and details on clear', () => { - const handleChange = spy(); - const options = ['one', 'two', 'three']; - const { container } = render( - } - />, - ); - - const button = container.querySelector('button'); - fireEvent.click(button); - expect(handleChange.callCount).to.equal(1); - expect(handleChange.args[0][1]).to.deep.equal([]); - expect(handleChange.args[0][2]).to.equal('clear'); - expect(handleChange.args[0][3]).to.equal(undefined); - }); - }); - - describe('prop: onInputChange', () => { - it('provides a reason on input change', () => { - const handleInputChange = spy(); - const options = [{ name: 'foo' }]; - render( - option.name} - renderInput={(params) => } - />, - ); - fireEvent.change(document.activeElement, { target: { value: 'a' } }); - expect(handleInputChange.callCount).to.equal(1); - expect(handleInputChange.args[0][1]).to.equal('a'); - expect(handleInputChange.args[0][2]).to.equal('input'); - }); - - it('provides a reason on select reset', () => { - const handleInputChange = spy(); - const options = [{ name: 'foo' }]; - render( - option.name} - renderInput={(params) => } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'Enter' }); - - expect(handleInputChange.callCount).to.equal(1); - expect(handleInputChange.args[0][1]).to.equal(options[0].name); - expect(handleInputChange.args[0][2]).to.equal('reset'); - }); - }); - - describe('prop: blurOnSelect', () => { - it('[blurOnSelect=true] should blur the input when clicking or touching options', () => { - const options = [{ name: 'foo' }]; - const { getByRole, queryByTitle } = render( - option.name} - renderInput={(params) => } - blurOnSelect - />, - ); - const textbox = getByRole('textbox'); - let firstOption = getByRole('option'); - expect(textbox).toHaveFocus(); - fireEvent.click(firstOption); - expect(textbox).not.toHaveFocus(); - - fireEvent.click(queryByTitle('Open')); - expect(textbox).toHaveFocus(); - firstOption = getByRole('option'); - fireEvent.touchStart(firstOption); - fireEvent.click(firstOption); - expect(textbox).not.toHaveFocus(); - }); - - it('[blurOnSelect="touch"] should only blur the input when an option is touched', () => { - const options = [{ name: 'foo' }]; - const { getByRole, queryByTitle } = render( - option.name} - renderInput={(params) => } - blurOnSelect="touch" - />, - ); - const textbox = getByRole('textbox'); - let firstOption = getByRole('option'); - fireEvent.click(firstOption); - expect(textbox).toHaveFocus(); - - fireEvent.click(queryByTitle('Open')); - firstOption = getByRole('option'); - fireEvent.touchStart(firstOption); - fireEvent.click(firstOption); - expect(textbox).not.toHaveFocus(); - }); - - it('[blurOnSelect="mouse"] should only blur the input when an option is clicked', () => { - const options = [{ name: 'foo' }]; - const { getByRole, queryByTitle } = render( - option.name} - renderInput={(params) => } - blurOnSelect="mouse" - />, - ); - const textbox = getByRole('textbox'); - let firstOption = getByRole('option'); - fireEvent.touchStart(firstOption); - fireEvent.click(firstOption); - expect(textbox).toHaveFocus(); - - fireEvent.click(queryByTitle('Open')); - firstOption = getByRole('option'); - fireEvent.click(firstOption); - expect(textbox).not.toHaveFocus(); - }); - }); - - describe('prop: getOptionLabel', () => { - it('is considered for falsy values when filtering the list of options', () => { - const { getAllByRole } = render( - (option === 0 ? 'Any' : option.toString())} - renderInput={(params) => } - value={0} - />, - ); - - const options = getAllByRole('option'); - expect(options).to.have.length(3); - }); - - it('is not considered for nullish values when filtering the list of options', () => { - const { getAllByRole } = render( - (option === null ? 'Any' : option.toString())} - renderInput={(params) => } - value={null} - />, - ); - - const options = getAllByRole('option'); - expect(options).to.have.length(3); - }); - }); - - describe('prop: fullWidth', () => { - it('should have the fullWidth class', () => { - const { container } = render( - } - value={null} - />, - ); - - expect(container.querySelector(`.${classes.root}`)).to.have.class(classes.fullWidth); - }); - }); - - describe('prop: onHighlightChange', () => { - it('should trigger event when default value is passed', () => { - const handleHighlightChange = spy(); - const options = ['one', 'two', 'three']; - render( - } - />, - ); - expect(handleHighlightChange.callCount).to.equal(1); - expect(handleHighlightChange.args[0][0]).to.equal(undefined); - expect(handleHighlightChange.args[0][1]).to.equal(options[0]); - expect(handleHighlightChange.args[0][2]).to.equal('auto'); - }); - - it('should support keyboard event', () => { - const handleHighlightChange = spy(); - const options = ['one', 'two', 'three']; - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - expect(handleHighlightChange.callCount).to.equal(3); - expect(handleHighlightChange.args[2][0]).to.not.equal(undefined); - expect(handleHighlightChange.args[2][1]).to.equal(options[0]); - expect(handleHighlightChange.args[2][2]).to.equal('keyboard'); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - expect(handleHighlightChange.callCount).to.equal(4); - expect(handleHighlightChange.args[3][0]).to.not.equal(undefined); - expect(handleHighlightChange.args[3][1]).to.equal(options[1]); - expect(handleHighlightChange.args[3][2]).to.equal('keyboard'); - }); - - it('should support mouse event', () => { - const handleHighlightChange = spy(); - const options = ['one', 'two', 'three']; - const { getAllByRole } = render( - } - />, - ); - const firstOption = getAllByRole('option')[0]; - fireEvent.mouseOver(firstOption); - expect(handleHighlightChange.callCount).to.equal(3); - expect(handleHighlightChange.args[2][0]).to.not.equal(undefined); - expect(handleHighlightChange.args[2][1]).to.equal(options[0]); - expect(handleHighlightChange.args[2][2]).to.equal('mouse'); - }); - - it('should pass to onHighlightChange the correct value after filtering', () => { - const handleHighlightChange = spy(); - const options = ['one', 'three', 'onetwo']; - render( - } - />, - ); - const textbox = screen.getByRole('textbox'); - - fireEvent.change(document.activeElement, { target: { value: 'one' } }); - expect(screen.getAllByRole('option').length).to.equal(2); - - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - fireEvent.keyDown(textbox, { key: 'ArrowDown' }); - expect(handleHighlightChange.args[handleHighlightChange.args.length - 1][1]).to.equal( - options[2], - ); - }); - }); -}); diff --git a/packages/material-ui-lab/src/Autocomplete/index.js b/packages/material-ui-lab/src/Autocomplete/index.js index 3c3fef79e84133..005b9c1ee3c365 100644 --- a/packages/material-ui-lab/src/Autocomplete/index.js +++ b/packages/material-ui-lab/src/Autocomplete/index.js @@ -1 +1 @@ -export { default, createFilterOptions } from './Autocomplete'; +export { default } from './Autocomplete'; diff --git a/packages/material-ui-lab/src/index.d.ts b/packages/material-ui-lab/src/index.d.ts index 304a09b93325d5..c5ab1318de1c97 100644 --- a/packages/material-ui-lab/src/index.d.ts +++ b/packages/material-ui-lab/src/index.d.ts @@ -1,3 +1,6 @@ +export { default as Alert } from './Alert'; +export * from './Alert'; + export { default as Autocomplete } from './Autocomplete'; export * from './Autocomplete'; diff --git a/packages/material-ui-lab/src/internal/svg-icons/ArrowDropDown.js b/packages/material-ui-lab/src/internal/svg-icons/ArrowDropDown.js deleted file mode 100644 index fdaa9c00cb395f..00000000000000 --- a/packages/material-ui-lab/src/internal/svg-icons/ArrowDropDown.js +++ /dev/null @@ -1,7 +0,0 @@ -import * as React from 'react'; -import { createSvgIcon } from '@material-ui/core/utils'; - -/** - * @ignore - internal component. - */ -export default createSvgIcon(, 'ArrowDropDown'); diff --git a/packages/material-ui-lab/src/internal/svg-icons/Close.js b/packages/material-ui-lab/src/internal/svg-icons/Close.js deleted file mode 100644 index cd47ee13c9db3f..00000000000000 --- a/packages/material-ui-lab/src/internal/svg-icons/Close.js +++ /dev/null @@ -1,10 +0,0 @@ -import * as React from 'react'; -import { createSvgIcon } from '@material-ui/core/utils'; - -/** - * @ignore - internal component. - */ -export default createSvgIcon( - , - 'Close', -); diff --git a/packages/material-ui-lab/src/themeAugmentation/components.d.ts b/packages/material-ui-lab/src/themeAugmentation/components.d.ts index 9e3e3bd7a7cbef..ea1cfaa1bfc985 100644 --- a/packages/material-ui-lab/src/themeAugmentation/components.d.ts +++ b/packages/material-ui-lab/src/themeAugmentation/components.d.ts @@ -1,10 +1,6 @@ import { ComponentsProps, ComponentsOverrides, ComponentsVariants } from '@material-ui/core'; export interface LabComponents { - MuiAutocomplete?: { - defaultProps?: ComponentsProps['MuiAutocomplete']; - styleOverrides?: ComponentsOverrides['MuiAutocomplete']; - }; MuiAvatarGroup?: { defaultProps?: ComponentsProps['MuiAvatarGroup']; styleOverrides?: ComponentsOverrides['MuiAvatarGroup']; diff --git a/packages/material-ui-lab/src/themeAugmentation/overrides.d.ts b/packages/material-ui-lab/src/themeAugmentation/overrides.d.ts index 01e87f683316cc..4c5347f56063fd 100644 --- a/packages/material-ui-lab/src/themeAugmentation/overrides.d.ts +++ b/packages/material-ui-lab/src/themeAugmentation/overrides.d.ts @@ -1,4 +1,3 @@ -import { AutocompleteClassKey } from '../Autocomplete'; import { AvatarGroupClassKey } from '../AvatarGroup'; import { PaginationClassKey } from '../Pagination'; import { PaginationItemClassKey } from '../PaginationItem'; @@ -22,7 +21,6 @@ import { TreeItemClassKey } from '../TreeItem'; import { TreeViewClassKey } from '../TreeView'; export interface LabComponentNameToClassKey { - MuiAutocomplete: AutocompleteClassKey; MuiAvatarGroup: AvatarGroupClassKey; MuiPagination: PaginationClassKey; MuiPaginationItem: PaginationItemClassKey; diff --git a/packages/material-ui-lab/src/themeAugmentation/props.d.ts b/packages/material-ui-lab/src/themeAugmentation/props.d.ts index c13dbfc17e1dcd..c98ef458288583 100644 --- a/packages/material-ui-lab/src/themeAugmentation/props.d.ts +++ b/packages/material-ui-lab/src/themeAugmentation/props.d.ts @@ -1,4 +1,3 @@ -import { AutocompleteProps } from '../Autocomplete'; import { AvatarGroupProps } from '../AvatarGroup'; import { PaginationProps } from '../Pagination'; import { PaginationItemProps } from '../PaginationItem'; @@ -22,7 +21,6 @@ import { TreeItemProps } from '../TreeItem'; import { TreeViewProps } from '../TreeView'; export interface LabComponentsPropsList { - MuiAutocomplete: AutocompleteProps; MuiAvatarGroup: AvatarGroupProps; MuiPagination: PaginationProps; MuiPaginationItem: PaginationItemProps; diff --git a/packages/material-ui-lab/src/themeAugmentation/props.spec.ts b/packages/material-ui-lab/src/themeAugmentation/props.spec.ts index 9c5ab1d5454e81..28e85a3cbdde47 100644 --- a/packages/material-ui-lab/src/themeAugmentation/props.spec.ts +++ b/packages/material-ui-lab/src/themeAugmentation/props.spec.ts @@ -11,21 +11,3 @@ createMuiTheme({ }, }, }); - -// Ensure Autocomplete generics are loose -const trueOrFalse = true as boolean; -const val = '' as string | null | string[]; -createMuiTheme({ - components: { - MuiAutocomplete: { - defaultProps: { - multiple: trueOrFalse, - disableClearable: trueOrFalse, - freeSolo: trueOrFalse, - options: [], - value: val, - defaultValue: val, - }, - }, - }, -}); diff --git a/packages/material-ui-lab/src/useAutocomplete/index.d.ts b/packages/material-ui-lab/src/useAutocomplete/index.d.ts index 6f01318688c9f2..be5eaf5a01cf15 100644 --- a/packages/material-ui-lab/src/useAutocomplete/index.d.ts +++ b/packages/material-ui-lab/src/useAutocomplete/index.d.ts @@ -1,2 +1,2 @@ -export { default } from './useAutocomplete'; -export * from './useAutocomplete'; +export { default } from '@material-ui/core/useAutocomplete'; +export * from '@material-ui/core/useAutocomplete'; diff --git a/packages/material-ui-lab/src/useAutocomplete/index.js b/packages/material-ui-lab/src/useAutocomplete/index.js index a6c05b480618e9..a0f3c16b272b9f 100644 --- a/packages/material-ui-lab/src/useAutocomplete/index.js +++ b/packages/material-ui-lab/src/useAutocomplete/index.js @@ -1 +1 @@ -export { default, createFilterOptions } from './useAutocomplete'; +export { default, createFilterOptions } from '@material-ui/core/useAutocomplete'; diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.d.ts b/packages/material-ui/src/Autocomplete/Autocomplete.d.ts new file mode 100644 index 00000000000000..2808ee08ee3169 --- /dev/null +++ b/packages/material-ui/src/Autocomplete/Autocomplete.d.ts @@ -0,0 +1,277 @@ +import * as React from 'react'; +import { InternalStandardProps as StandardProps } from '@material-ui/core'; +import { PopperProps } from '@material-ui/core/Popper'; +import { + AutocompleteChangeDetails, + AutocompleteChangeReason, + AutocompleteCloseReason, + AutocompleteInputChangeReason, + createFilterOptions, + UseAutocompleteProps, +} from '../useAutocomplete'; + +export { + AutocompleteChangeDetails, + AutocompleteChangeReason, + AutocompleteCloseReason, + AutocompleteInputChangeReason, + createFilterOptions, +}; + +export interface AutocompleteRenderOptionState { + inputValue: string; + selected: boolean; +} + +export type AutocompleteGetTagProps = ({ index }: { index: number }) => {}; + +export interface AutocompleteRenderGroupParams { + key: string; + group: string; + children?: React.ReactNode; +} + +export interface AutocompleteRenderInputParams { + id: string; + disabled: boolean; + fullWidth: boolean; + size: 'small' | undefined; + InputLabelProps: object; + InputProps: { + ref: React.Ref; + className: string; + startAdornment: React.ReactNode; + endAdornment: React.ReactNode; + }; + inputProps: object; +} + +export interface AutocompleteProps< + T, + Multiple extends boolean | undefined, + DisableClearable extends boolean | undefined, + FreeSolo extends boolean | undefined +> extends UseAutocompleteProps, + StandardProps, 'defaultValue' | 'onChange' | 'children'> { + /** + * Props applied to the [`Chip`](/api/chip/) element. + */ + ChipProps?: object; + /** + * Override or extend the styles applied to the component. + */ + classes?: { + /** Styles applied to the root element. */ + root?: string; + /** Styles applied to the root element if `fullWidth={true}`. */ + fullWidth?: string; + /** Pseudo-class applied to the root element if focused. */ + focused?: string; + /** Styles applied to the tag elements, e.g. the chips. */ + tag?: string; + /** Styles applied to the tag elements, e.g. the chips if `size="small"`. */ + tagSizeSmall?: string; + /** Styles applied when the popup icon is rendered. */ + hasPopupIcon?: string; + /** Styles applied when the clear icon is rendered. */ + hasClearIcon?: string; + /** Styles applied to the Input element. */ + inputRoot?: string; + /** Styles applied to the input element. */ + input?: string; + /** Styles applied to the input element if tag focused. */ + inputFocused?: string; + /** Styles applied to the endAdornment element. */ + endAdornment?: string; + /** Styles applied to the clear indicator. */ + clearIndicator?: string; + /** Styles applied to the clear indicator if the input is dirty. */ + clearIndicatorDirty?: string; + /** Styles applied to the popup indicator. */ + popupIndicator?: string; + /** Styles applied to the popup indicator if the popup is open. */ + popupIndicatorOpen?: string; + /** Styles applied to the popper element. */ + popper?: string; + /** Styles applied to the popper element if `disablePortal={true}`. */ + popperDisablePortal?: string; + /** Styles applied to the `Paper` component. */ + paper?: string; + /** Styles applied to the `listbox` component. */ + listbox?: string; + /** Styles applied to the loading wrapper. */ + loading?: string; + /** Styles applied to the no option wrapper. */ + noOptions?: string; + /** Styles applied to the option elements. */ + option?: string; + /** Styles applied to the group's label elements. */ + groupLabel?: string; + /** Styles applied to the group's ul elements. */ + groupUl?: string; + }; + /** + * The icon to display in place of the default close icon. + * @default + */ + closeIcon?: React.ReactNode; + /** + * Override the default text for the *clear* icon button. + * + * For localization purposes, you can use the provided [translations](/guides/localization/). + * @default 'Clear' + */ + clearText?: string; + /** + * Override the default text for the *close popup* icon button. + * + * For localization purposes, you can use the provided [translations](/guides/localization/). + * @default 'Close' + */ + closeText?: string; + /** + * If `true`, the input will be disabled. + * @default false + */ + disabled?: boolean; + /** + * The `Popper` content will be inside the DOM hierarchy of the parent component. + * @default false + */ + disablePortal?: boolean; + /** + * Force the visibility display of the popup icon. + * @default 'auto' + */ + forcePopupIcon?: true | false | 'auto'; + /** + * If `true`, the input will take up the full width of its container. + * @default false + */ + fullWidth?: boolean; + /** + * The label to display when the tags are truncated (`limitTags`). + * + * @param {number} more The number of truncated tags. + * @returns {ReactNode} + * @default (more) => `+${more}` + */ + getLimitTagsText?: (more: number) => React.ReactNode; + /** + * The component used to render the listbox. + * @default 'ul' + */ + ListboxComponent?: React.ComponentType>; + /** + * Props applied to the Listbox element. + */ + ListboxProps?: object; + /** + * If `true`, the component is in a loading state. + * @default false + */ + loading?: boolean; + /** + * Text to display when in a loading state. + * + * For localization purposes, you can use the provided [translations](/guides/localization/). + * @default 'Loading…' + */ + loadingText?: React.ReactNode; + /** + * The maximum number of tags that will be visible when not focused. + * Set `-1` to disable the limit. + * @default -1 + */ + limitTags?: number; + /** + * Text to display when there are no options. + * + * For localization purposes, you can use the provided [translations](/guides/localization/). + * @default 'No options' + */ + noOptionsText?: React.ReactNode; + /** + * Override the default text for the *open popup* icon button. + * + * For localization purposes, you can use the provided [translations](/guides/localization/). + * @default 'Open' + */ + openText?: string; + /** + * The component used to render the body of the popup. + * @default Paper + */ + PaperComponent?: React.ComponentType>; + /** + * The component used to position the popup. + * @default Popper + */ + PopperComponent?: React.ComponentType; + /** + * The icon to display in place of the default popup icon. + * @default + */ + popupIcon?: React.ReactNode; + /** + * Render the group. + * + * @param {any} option The group to render. + * @returns {ReactNode} + */ + renderGroup?: (params: AutocompleteRenderGroupParams) => React.ReactNode; + /** + * Render the input. + * + * @param {object} params + * @returns {ReactNode} + */ + renderInput: (params: AutocompleteRenderInputParams) => React.ReactNode; + /** + * Render the option, use `getOptionLabel` by default. + * + * @param {object} props The props to apply on the li element. + * @param {T} option The option to render. + * @param {object} state The state of the component. + * @returns {ReactNode} + */ + renderOption?: ( + props: React.HTMLAttributes, + option: T, + state: AutocompleteRenderOptionState + ) => React.ReactNode; + /** + * Render the selected value. + * + * @param {T[]} value The `value` provided to the component. + * @param {function} getTagProps A tag props getter. + * @returns {ReactNode} + */ + renderTags?: (value: T[], getTagProps: AutocompleteGetTagProps) => React.ReactNode; + /** + * The size of the autocomplete. + * @default 'medium' + */ + size?: 'small' | 'medium'; +} + +export type AutocompleteClassKey = keyof NonNullable< + AutocompleteProps['classes'] +>; + +/** + * + * Demos: + * + * - [Autocomplete](https://material-ui.com/components/autocomplete/) + * + * API: + * + * - [Autocomplete API](https://material-ui.com/api/autocomplete/) + */ +export default function Autocomplete< + T, + Multiple extends boolean | undefined = undefined, + DisableClearable extends boolean | undefined = undefined, + FreeSolo extends boolean | undefined = undefined +>(props: AutocompleteProps): JSX.Element; diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.js b/packages/material-ui/src/Autocomplete/Autocomplete.js new file mode 100644 index 00000000000000..b0edc3a2257ee8 --- /dev/null +++ b/packages/material-ui/src/Autocomplete/Autocomplete.js @@ -0,0 +1,880 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; +import { withStyles } from '@material-ui/core/styles'; +import Popper from '@material-ui/core/Popper'; +import ListSubheader from '@material-ui/core/ListSubheader'; +import Paper from '@material-ui/core/Paper'; +import IconButton from '@material-ui/core/IconButton'; +import Chip from '@material-ui/core/Chip'; +import CloseIcon from '../internal/svg-icons/Close'; +import ArrowDropDownIcon from '../internal/svg-icons/ArrowDropDown'; +import useAutocomplete, { createFilterOptions } from '../useAutocomplete'; + +export { createFilterOptions }; + +export const styles = (theme) => ({ + /* Styles applied to the root element. */ + root: { + '&$focused $clearIndicatorDirty': { + visibility: 'visible', + }, + /* Avoid double tap issue on iOS */ + '@media (pointer: fine)': { + '&:hover $clearIndicatorDirty': { + visibility: 'visible', + }, + }, + }, + /* Styles applied to the root element if `fullWidth={true}`. */ + fullWidth: { + width: '100%', + }, + /* Pseudo-class applied to the root element if focused. */ + focused: {}, + /* Styles applied to the tag elements, e.g. the chips. */ + tag: { + margin: 3, + maxWidth: 'calc(100% - 6px)', + }, + /* Styles applied to the tag elements, e.g. the chips if `size="small"`. */ + tagSizeSmall: { + margin: 2, + maxWidth: 'calc(100% - 4px)', + }, + /* Styles applied when the popup icon is rendered. */ + hasPopupIcon: {}, + /* Styles applied when the clear icon is rendered. */ + hasClearIcon: {}, + /* Styles applied to the Input element. */ + inputRoot: { + flexWrap: 'wrap', + '$hasPopupIcon &, $hasClearIcon &': { + paddingRight: 26 + 4, + }, + '$hasPopupIcon$hasClearIcon &': { + paddingRight: 52 + 4, + }, + '& $input': { + width: 0, + minWidth: 30, + }, + '&[class*="MuiInput-root"]': { + paddingBottom: 1, + '& $input': { + padding: 4, + }, + '& $input:first-child': { + padding: '6px 0', + }, + }, + '&[class*="MuiInput-root"][class*="MuiInput-marginDense"]': { + '& $input': { + padding: '2px 4px 3px', + }, + '& $input:first-child': { + padding: '1px 0 4px', + }, + }, + '&[class*="MuiOutlinedInput-root"]': { + padding: 9, + '$hasPopupIcon &, $hasClearIcon &': { + paddingRight: 26 + 4 + 9, + }, + '$hasPopupIcon$hasClearIcon &': { + paddingRight: 52 + 4 + 9, + }, + '& $input': { + padding: '7.5px 4px', + }, + '& $input:first-child': { + paddingLeft: 6, + }, + '& $endAdornment': { + right: 9, + }, + }, + '&[class*="MuiOutlinedInput-root"][class*="MuiOutlinedInput-marginDense"]': { + padding: 6, + '& $input': { + padding: '2.5px 4px', + }, + }, + '&[class*="MuiFilledInput-root"]': { + paddingTop: 19, + paddingLeft: 8, + '$hasPopupIcon &, $hasClearIcon &': { + paddingRight: 26 + 4 + 9, + }, + '$hasPopupIcon$hasClearIcon &': { + paddingRight: 52 + 4 + 9, + }, + '& $input': { + padding: '7px 4px', + }, + '& $endAdornment': { + right: 9, + }, + }, + '&[class*="MuiFilledInput-root"][class*="MuiFilledInput-marginDense"]': { + paddingBottom: 1, + '& $input': { + padding: '2.5px 4px', + }, + }, + }, + /* Styles applied to the input element. */ + input: { + flexGrow: 1, + textOverflow: 'ellipsis', + opacity: 0, + }, + /* Styles applied to the input element if tag focused. */ + inputFocused: { + opacity: 1, + }, + /* Styles applied to the endAdornment element. */ + endAdornment: { + // We use a position absolute to support wrapping tags. + position: 'absolute', + right: 0, + top: 'calc(50% - 14px)', // Center vertically + }, + /* Styles applied to the clear indicator. */ + clearIndicator: { + marginRight: -2, + padding: 4, + visibility: 'hidden', + }, + /* Styles applied to the clear indicator if the input is dirty. */ + clearIndicatorDirty: {}, + /* Styles applied to the popup indicator. */ + popupIndicator: { + padding: 2, + marginRight: -2, + }, + /* Styles applied to the popup indicator if the popup is open. */ + popupIndicatorOpen: { + transform: 'rotate(180deg)', + }, + /* Styles applied to the popper element. */ + popper: { + zIndex: theme.zIndex.modal, + }, + /* Styles applied to the popper element if `disablePortal={true}`. */ + popperDisablePortal: { + position: 'absolute', + }, + /* Styles applied to the `Paper` component. */ + paper: { + ...theme.typography.body1, + overflow: 'hidden', + margin: '4px 0', + }, + /* Styles applied to the `listbox` component. */ + listbox: { + listStyle: 'none', + margin: 0, + padding: '8px 0', + maxHeight: '40vh', + overflow: 'auto', + }, + /* Styles applied to the loading wrapper. */ + loading: { + color: theme.palette.text.secondary, + padding: '14px 16px', + }, + /* Styles applied to the no option wrapper. */ + noOptions: { + color: theme.palette.text.secondary, + padding: '14px 16px', + }, + /* Styles applied to the option elements. */ + option: { + minHeight: 48, + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + cursor: 'pointer', + paddingTop: 6, + boxSizing: 'border-box', + outline: '0', + WebkitTapHighlightColor: 'transparent', + paddingBottom: 6, + paddingLeft: 16, + paddingRight: 16, + [theme.breakpoints.up('sm')]: { + minHeight: 'auto', + }, + '&[aria-selected="true"]': { + backgroundColor: theme.palette.action.selected, + }, + '&[data-focus="true"]': { + backgroundColor: theme.palette.action.hover, + }, + '&:active': { + backgroundColor: theme.palette.action.selected, + }, + '&[aria-disabled="true"]': { + opacity: theme.palette.action.disabledOpacity, + pointerEvents: 'none', + }, + }, + /* Styles applied to the group's label elements. */ + groupLabel: { + backgroundColor: theme.palette.background.paper, + top: -8, + }, + /* Styles applied to the group's ul elements. */ + groupUl: { + padding: 0, + '& $option': { + paddingLeft: 24, + }, + }, +}); + +function DisablePortal(props) { + // eslint-disable-next-line react/prop-types + const { anchorEl, open, ...other } = props; + return
    ; +} + +const Autocomplete = React.forwardRef(function Autocomplete(props, ref) { + /* eslint-disable @typescript-eslint/no-unused-vars */ + const { + autoComplete = false, + autoHighlight = false, + autoSelect = false, + blurOnSelect = false, + ChipProps, + classes, + className, + clearOnBlur = !props.freeSolo, + clearOnEscape = false, + clearText = 'Clear', + closeIcon = , + closeText = 'Close', + debug = false, + defaultValue = props.multiple ? [] : null, + disableClearable = false, + disableCloseOnSelect = false, + disabled = false, + disabledItemsFocusable = false, + disableListWrap = false, + disablePortal = false, + filterOptions, + filterSelectedOptions = false, + forcePopupIcon = 'auto', + freeSolo = false, + fullWidth = false, + getLimitTagsText = (more) => `+${more}`, + getOptionDisabled, + getOptionLabel = (option) => option.label ?? option, + getOptionSelected, + groupBy, + handleHomeEndKeys = !props.freeSolo, + id: idProp, + includeInputInList = false, + inputValue: inputValueProp, + limitTags = -1, + ListboxComponent = 'ul', + ListboxProps, + loading = false, + loadingText = 'Loading…', + multiple = false, + noOptionsText = 'No options', + onChange, + onClose, + onHighlightChange, + onInputChange, + onOpen, + open, + openOnFocus = false, + openText = 'Open', + options, + PaperComponent = Paper, + PopperComponent: PopperComponentProp = Popper, + popupIcon = , + renderGroup: renderGroupProp, + renderInput, + renderOption: renderOptionProp, + renderTags, + selectOnFocus = !props.freeSolo, + size = 'medium', + value: valueProp, + ...other + } = props; + /* eslint-enable @typescript-eslint/no-unused-vars */ + + const PopperComponent = disablePortal ? DisablePortal : PopperComponentProp; + + const { + getRootProps, + getInputProps, + getInputLabelProps, + getPopupIndicatorProps, + getClearProps, + getTagProps, + getListboxProps, + getOptionProps, + value, + dirty, + id, + popupOpen, + focused, + focusedTag, + anchorEl, + setAnchorEl, + inputValue, + groupedOptions, + } = useAutocomplete({ ...props, componentName: 'Autocomplete' }); + + let startAdornment; + + if (multiple && value.length > 0) { + const getCustomizedTagProps = (params) => ({ + className: clsx(classes.tag, { + [classes.tagSizeSmall]: size === 'small', + }), + disabled, + ...getTagProps(params), + }); + + if (renderTags) { + startAdornment = renderTags(value, getCustomizedTagProps); + } else { + startAdornment = value.map((option, index) => ( + + )); + } + } + + if (limitTags > -1 && Array.isArray(startAdornment)) { + const more = startAdornment.length - limitTags; + if (!focused && more > 0) { + startAdornment = startAdornment.splice(0, limitTags); + startAdornment.push( + + {getLimitTagsText(more)} + , + ); + } + } + + const defaultRenderGroup = (params) => ( +
  • + + {params.group} + +
      {params.children}
    +
  • + ); + + const renderGroup = renderGroupProp || defaultRenderGroup; + const defaultRenderOption = (props2, option) =>
  • {getOptionLabel(option)}
  • ; + const renderOption = renderOptionProp || defaultRenderOption; + + const renderListOption = (option, index) => { + const optionProps = getOptionProps({ option, index }); + + return renderOption({ ...optionProps, className: classes.option }, option, { + selected: optionProps['aria-selected'], + inputValue, + }); + }; + + const hasClearIcon = !disableClearable && !disabled; + const hasPopupIcon = (!freeSolo || forcePopupIcon === true) && forcePopupIcon !== false; + + return ( + +
    + {renderInput({ + id, + disabled, + fullWidth: true, + size: size === 'small' ? 'small' : undefined, + InputLabelProps: getInputLabelProps(), + InputProps: { + ref: setAnchorEl, + className: classes.inputRoot, + startAdornment, + endAdornment: ( +
    + {hasClearIcon ? ( + + {closeIcon} + + ) : null} + + {hasPopupIcon ? ( + + {popupIcon} + + ) : null} +
    + ), + }, + inputProps: { + className: clsx(classes.input, { + [classes.inputFocused]: focusedTag === -1, + }), + disabled, + ...getInputProps(), + }, + })} +
    + {popupOpen && anchorEl ? ( + + + {loading && groupedOptions.length === 0 ? ( +
    {loadingText}
    + ) : null} + {groupedOptions.length === 0 && !freeSolo && !loading ? ( +
    {noOptionsText}
    + ) : null} + {groupedOptions.length > 0 ? ( + + {groupedOptions.map((option, index) => { + if (groupBy) { + return renderGroup({ + key: option.key, + group: option.group, + children: option.options.map((option2, index2) => + renderListOption(option2, option.index + index2), + ), + }); + } + return renderListOption(option, index); + })} + + ) : null} +
    +
    + ) : null} +
    + ); +}); + +Autocomplete.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the d.ts file and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * If `true`, the portion of the selected suggestion that has not been typed by the user, + * known as the completion string, appears inline after the input cursor in the textbox. + * The inline completion string is visually highlighted and has a selected state. + * @default false + */ + autoComplete: PropTypes.bool, + /** + * If `true`, the first option is automatically highlighted. + * @default false + */ + autoHighlight: PropTypes.bool, + /** + * If `true`, the selected option becomes the value of the input + * when the Autocomplete loses focus unless the user chooses + * a different option or changes the character string in the input. + * @default false + */ + autoSelect: PropTypes.bool, + /** + * Control if the input should be blurred when an option is selected: + * + * - `false` the input is not blurred. + * - `true` the input is always blurred. + * - `touch` the input is blurred after a touch event. + * - `mouse` the input is blurred after a mouse event. + * @default false + */ + blurOnSelect: PropTypes.oneOfType([PropTypes.oneOf(['mouse', 'touch']), PropTypes.bool]), + /** + * Props applied to the [`Chip`](/api/chip/) element. + */ + ChipProps: PropTypes.object, + /** + * Override or extend the styles applied to the component. + */ + classes: PropTypes.object, + /** + * @ignore + */ + className: PropTypes.string, + /** + * If `true`, the input's text will be cleared on blur if no value is selected. + * + * Set to `true` if you want to help the user enter a new value. + * Set to `false` if you want to help the user resume his search. + * @default !props.freeSolo + */ + clearOnBlur: PropTypes.bool, + /** + * If `true`, clear all values when the user presses escape and the popup is closed. + * @default false + */ + clearOnEscape: PropTypes.bool, + /** + * Override the default text for the *clear* icon button. + * + * For localization purposes, you can use the provided [translations](/guides/localization/). + * @default 'Clear' + */ + clearText: PropTypes.string, + /** + * The icon to display in place of the default close icon. + * @default + */ + closeIcon: PropTypes.node, + /** + * Override the default text for the *close popup* icon button. + * + * For localization purposes, you can use the provided [translations](/guides/localization/). + * @default 'Close' + */ + closeText: PropTypes.string, + /** + * If `true`, the popup will ignore the blur event if the input is filled. + * You can inspect the popup markup with your browser tools. + * Consider this option when you need to customize the component. + * @default false + */ + debug: PropTypes.bool, + /** + * The default input value. Use when the component is not controlled. + * @default props.multiple ? [] : null + */ + defaultValue: PropTypes.any, + /** + * If `true`, the input can't be cleared. + * @default false + */ + disableClearable: PropTypes.bool, + /** + * If `true`, the popup won't close when a value is selected. + * @default false + */ + disableCloseOnSelect: PropTypes.bool, + /** + * If `true`, the input will be disabled. + * @default false + */ + disabled: PropTypes.bool, + /** + * If `true`, will allow focus on disabled items. + * @default false + */ + disabledItemsFocusable: PropTypes.bool, + /** + * If `true`, the list box in the popup will not wrap focus. + * @default false + */ + disableListWrap: PropTypes.bool, + /** + * The `Popper` content will be inside the DOM hierarchy of the parent component. + * @default false + */ + disablePortal: PropTypes.bool, + /** + * A filter function that determines the options that are eligible. + * + * @param {T[]} options The options to render. + * @param {object} state The state of the component. + * @returns {T[]} + */ + filterOptions: PropTypes.func, + /** + * If `true`, hide the selected options from the list box. + * @default false + */ + filterSelectedOptions: PropTypes.bool, + /** + * Force the visibility display of the popup icon. + * @default 'auto' + */ + forcePopupIcon: PropTypes.oneOfType([PropTypes.oneOf(['auto']), PropTypes.bool]), + /** + * If `true`, the Autocomplete is free solo, meaning that the user input is not bound to provided options. + * @default false + */ + freeSolo: PropTypes.bool, + /** + * If `true`, the input will take up the full width of its container. + * @default false + */ + fullWidth: PropTypes.bool, + /** + * The label to display when the tags are truncated (`limitTags`). + * + * @param {number} more The number of truncated tags. + * @returns {ReactNode} + * @default (more) => `+${more}` + */ + getLimitTagsText: PropTypes.func, + /** + * Used to determine the disabled state for a given option. + * + * @param {T} option The option to test. + * @returns {boolean} + */ + getOptionDisabled: PropTypes.func, + /** + * Used to determine the string value for a given option. + * It's used to fill the input (and the list box options if `renderOption` is not provided). + * + * @param {T} option + * @returns {string} + * @default (option) => option.label ?? option + */ + getOptionLabel: PropTypes.func, + /** + * Used to determine if an option is selected, considering the current value. + * Uses strict equality by default. + * + * @param {T} option The option to test. + * @param {T} value The value to test against. + * @returns {boolean} + */ + getOptionSelected: PropTypes.func, + /** + * If provided, the options will be grouped under the returned string. + * The groupBy value is also used as the text for group headings when `renderGroup` is not provided. + * + * @param {T} options The options to group. + * @returns {string} + */ + groupBy: PropTypes.func, + /** + * If `true`, the component handles the "Home" and "End" keys when the popup is open. + * It should move focus to the first option and last option, respectively. + * @default !props.freeSolo + */ + handleHomeEndKeys: PropTypes.bool, + /** + * This prop is used to help implement the accessibility logic. + * If you don't provide this prop. It falls back to a randomly generated id. + */ + id: PropTypes.string, + /** + * If `true`, the highlight can move to the input. + * @default false + */ + includeInputInList: PropTypes.bool, + /** + * The input value. + */ + inputValue: PropTypes.string, + /** + * The maximum number of tags that will be visible when not focused. + * Set `-1` to disable the limit. + * @default -1 + */ + limitTags: PropTypes.number, + /** + * The component used to render the listbox. + * @default 'ul' + */ + ListboxComponent: PropTypes.elementType, + /** + * Props applied to the Listbox element. + */ + ListboxProps: PropTypes.object, + /** + * If `true`, the component is in a loading state. + * @default false + */ + loading: PropTypes.bool, + /** + * Text to display when in a loading state. + * + * For localization purposes, you can use the provided [translations](/guides/localization/). + * @default 'Loading…' + */ + loadingText: PropTypes.node, + /** + * If `true`, `value` must be an array and the menu will support multiple selections. + * @default false + */ + multiple: PropTypes.bool, + /** + * Text to display when there are no options. + * + * For localization purposes, you can use the provided [translations](/guides/localization/). + * @default 'No options' + */ + noOptionsText: PropTypes.node, + /** + * Callback fired when the value changes. + * + * @param {object} event The event source of the callback. + * @param {T|T[]} value The new value of the component. + * @param {string} reason One of "create-option", "select-option", "remove-option", "blur" or "clear". + */ + onChange: PropTypes.func, + /** + * Callback fired when the popup requests to be closed. + * Use in controlled mode (see open). + * + * @param {object} event The event source of the callback. + * @param {string} reason Can be: `"toggleInput"`, `"escape"`, `"select-option"`, `"remove-option"`, `"blur"`. + */ + onClose: PropTypes.func, + /** + * Callback fired when the highlight option changes. + * + * @param {object} event The event source of the callback. + * @param {T} option The highlighted option. + * @param {string} reason Can be: `"keyboard"`, `"auto"`, `"mouse"`. + */ + onHighlightChange: PropTypes.func, + /** + * Callback fired when the input value changes. + * + * @param {object} event The event source of the callback. + * @param {string} value The new value of the text input. + * @param {string} reason Can be: `"input"` (user input), `"reset"` (programmatic change), `"clear"`. + */ + onInputChange: PropTypes.func, + /** + * Callback fired when the popup requests to be opened. + * Use in controlled mode (see open). + * + * @param {object} event The event source of the callback. + */ + onOpen: PropTypes.func, + /** + * Control the popup` open state. + */ + open: PropTypes.bool, + /** + * If `true`, the popup will open on input focus. + * @default false + */ + openOnFocus: PropTypes.bool, + /** + * Override the default text for the *open popup* icon button. + * + * For localization purposes, you can use the provided [translations](/guides/localization/). + * @default 'Open' + */ + openText: PropTypes.string, + /** + * Array of options. + */ + options: PropTypes.array.isRequired, + /** + * The component used to render the body of the popup. + * @default Paper + */ + PaperComponent: PropTypes.elementType, + /** + * The component used to position the popup. + * @default Popper + */ + PopperComponent: PropTypes.elementType, + /** + * The icon to display in place of the default popup icon. + * @default + */ + popupIcon: PropTypes.node, + /** + * Render the group. + * + * @param {any} option The group to render. + * @returns {ReactNode} + */ + renderGroup: PropTypes.func, + /** + * Render the input. + * + * @param {object} params + * @returns {ReactNode} + */ + renderInput: PropTypes.func.isRequired, + /** + * Render the option, use `getOptionLabel` by default. + * + * @param {object} props The props to apply on the li element. + * @param {T} option The option to render. + * @param {object} state The state of the component. + * @returns {ReactNode} + */ + renderOption: PropTypes.func, + /** + * Render the selected value. + * + * @param {T[]} value The `value` provided to the component. + * @param {function} getTagProps A tag props getter. + * @returns {ReactNode} + */ + renderTags: PropTypes.func, + /** + * If `true`, the input's text will be selected on focus. + * It helps the user clear the selected value. + * @default !props.freeSolo + */ + selectOnFocus: PropTypes.bool, + /** + * The size of the autocomplete. + * @default 'medium' + */ + size: PropTypes.oneOf(['medium', 'small']), + /** + * The value of the autocomplete. + * + * The value must have reference equality with the option in order to be selected. + * You can customize the equality behavior with the `getOptionSelected` prop. + */ + value: PropTypes.any, +}; + +export default withStyles(styles, { name: 'MuiAutocomplete' })(Autocomplete); diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.spec.tsx b/packages/material-ui/src/Autocomplete/Autocomplete.spec.tsx similarity index 100% rename from packages/material-ui-lab/src/Autocomplete/Autocomplete.spec.tsx rename to packages/material-ui/src/Autocomplete/Autocomplete.spec.tsx diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.test.js b/packages/material-ui/src/Autocomplete/Autocomplete.test.js new file mode 100644 index 00000000000000..5ea98df02ed999 --- /dev/null +++ b/packages/material-ui/src/Autocomplete/Autocomplete.test.js @@ -0,0 +1,2070 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { + getClasses, + createMount, + describeConformance, + act, + createClientRender, + fireEvent, + screen, +} from 'test/utils'; +import { spy } from 'sinon'; +import TextField from '@material-ui/core/TextField'; +import Chip from '@material-ui/core/Chip'; +import { createFilterOptions } from '../useAutocomplete/useAutocomplete'; +import Autocomplete from './Autocomplete'; + +describe('', () => { + const mount = createMount(); + let classes; + const render = createClientRender(); + const defaultProps = { + options: [], + openOnFocus: true, + }; + + before(() => { + classes = getClasses( null} />); + }); + + describeConformance( null} />, () => ({ + classes, + inheritComponent: 'div', + mount, + refInstanceof: window.HTMLDivElement, + testComponentPropWith: 'div', + })); + + describe('combobox', () => { + it('should clear the input when blur', () => { + const { getByRole } = render( + } />, + ); + const input = getByRole('textbox'); + + act(() => { + input.focus(); + fireEvent.change(document.activeElement, { target: { value: 'a' } }); + }); + + expect(input.value).to.equal('a'); + + act(() => { + document.activeElement.blur(); + }); + expect(input.value).to.equal(''); + }); + + it('should apply the icon classes', () => { + const { container } = render( + } />, + ); + expect(container.querySelector(`.${classes.root}`)).to.have.class(classes.hasClearIcon); + expect(container.querySelector(`.${classes.root}`)).to.have.class(classes.hasPopupIcon); + }); + }); + + describe('prop: loading', () => { + it('should show a loading message when open', () => { + render( + } + />, + ); + fireEvent.keyDown(screen.getByRole('textbox'), { key: 'ArrowDown' }); + expect(document.querySelector(`.${classes.paper}`).textContent).to.equal('Loading…'); + }); + }); + + describe('prop: autoHighlight', () => { + it('should set the focus on the first item', () => { + const options = ['one', 'two']; + const { getByRole } = render( + } + />, + ); + + function checkHighlightIs(expected) { + expect(getByRole('listbox').querySelector('li[data-focus]')).to.have.text(expected); + } + + checkHighlightIs('one'); + fireEvent.change(document.activeElement, { target: { value: 'oo' } }); + fireEvent.change(document.activeElement, { target: { value: 'o' } }); + checkHighlightIs('one'); + }); + + it('should keep the highlight on the first item', () => { + const options = ['one', 'two']; + const { getByRole } = render( + } + />, + ); + + function checkHighlightIs(expected) { + expect(getByRole('listbox').querySelector('li[data-focus]')).to.have.text(expected); + } + + checkHighlightIs('one'); + fireEvent.change(document.activeElement, { target: { value: 'two' } }); + checkHighlightIs('two'); + }); + + it('should set the focus on the first item when possible', () => { + const options = ['one', 'two']; + const { getByRole, setProps } = render( + } + />, + ); + const textbox = getByRole('textbox'); + expect(textbox).not.to.have.attribute('aria-activedescendant'); + + setProps({ options, loading: false }); + expect(textbox).to.have.attribute( + 'aria-activedescendant', + screen.getAllByRole('option')[0].getAttribute('id'), + ); + }); + + it('should set the highlight on selected item when dropdown is expanded', () => { + const { getByRole, setProps } = render( + } + />, + ); + + function checkHighlightIs(expected) { + expect(getByRole('listbox').querySelector('li[data-focus]')).to.have.text(expected); + } + + checkHighlightIs('one'); + setProps({ value: 'two' }); + checkHighlightIs('two'); + }); + + it('should keep the current highlight if possible', () => { + const { getByRole } = render( + } + />, + ); + const textbox = getByRole('textbox'); + + function checkHighlightIs(expected) { + expect(getByRole('listbox').querySelector('li[data-focus]')).to.have.text(expected); + } + + checkHighlightIs('one'); + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + checkHighlightIs('two'); + fireEvent.keyDown(textbox, { key: 'Enter' }); + checkHighlightIs('two'); + }); + }); + + describe('highlight synchronisation', () => { + it('should not update the highlight when multiple open and value change', () => { + const { setProps, getByRole } = render( + } + />, + ); + + function checkHighlightIs(expected) { + expect(getByRole('listbox').querySelector('li[data-focus]')).to.have.text(expected); + } + + checkHighlightIs('two'); + setProps({ + value: [], + }); + checkHighlightIs('two'); + }); + }); + + describe('prop: limitTags', () => { + it('show all items on focus', () => { + const { container, getAllByRole, getByRole } = render( + } + />, + ); + + expect(container.textContent).to.equal('onetwo+1'); + // include hidden clear button because JSDOM thinks it's visible + expect(getAllByRole('button', { hidden: true })).to.have.lengthOf(4); + + act(() => { + getByRole('textbox').focus(); + }); + expect(container.textContent).to.equal('onetwothree'); + expect(getAllByRole('button', { hidden: false })).to.have.lengthOf(5); + }); + + it('show 0 item on close when set 0 to limitTags', () => { + const { container, getAllByRole, getByRole } = render( + } + />, + ); + + expect(container.textContent).to.equal('+3'); + // include hidden clear button because JSDOM thinks it's visible + expect(getAllByRole('button', { hidden: true })).to.have.lengthOf(2); + + act(() => { + getByRole('textbox').focus(); + }); + expect(container.textContent).to.equal('onetwothree'); + expect(getAllByRole('button', { hidden: false })).to.have.lengthOf(5); + }); + }); + + describe('prop: filterSelectedOptions', () => { + it('when the last item is selected, highlights the new last item', () => { + const { getByRole } = render( + } + />, + ); + const textbox = getByRole('textbox'); + + function checkHighlightIs(expected) { + expect(getByRole('listbox').querySelector('li[data-focus]')).to.have.text(expected); + } + + fireEvent.keyDown(textbox, { key: 'ArrowUp' }); + checkHighlightIs('three'); + fireEvent.keyDown(textbox, { key: 'Enter' }); // selects the last option (three) + const input = getByRole('textbox'); + act(() => { + input.blur(); + input.focus(); // opens the listbox again + }); + checkHighlightIs('two'); + }); + }); + + describe('prop: autoSelect', () => { + it('should not clear on blur when value does not match any option', () => { + const handleChange = spy(); + const options = ['one', 'two']; + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.change(textbox, { target: { value: 'o' } }); + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.change(textbox, { target: { value: 'oo' } }); + act(() => { + textbox.blur(); + }); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.deep.equal('oo'); + }); + + it('should add new value when autoSelect & multiple on blur', () => { + const handleChange = spy(); + const options = ['one', 'two']; + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + act(() => { + fireEvent.change(textbox, { target: { value: 't' } }); + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + textbox.blur(); + }); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.deep.equal(options); + }); + + it('should add new value when autoSelect & multiple & freeSolo on blur', () => { + const handleChange = spy(); + render( + } + />, + ); + + fireEvent.change(document.activeElement, { target: { value: 'a' } }); + act(() => { + document.activeElement.blur(); + }); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.deep.equal(['a']); + }); + }); + + describe('prop: multiple', () => { + it('should not crash', () => { + const { getByRole } = render( + } + multiple + />, + ); + const input = getByRole('textbox'); + + act(() => { + input.focus(); + document.activeElement.blur(); + input.focus(); + }); + }); + + it('should remove the last option', () => { + const handleChange = spy(); + const options = ['one', 'two']; + const { getAllByTestId } = render( + } + multiple + />, + ); + fireEvent.click(getAllByTestId('CancelIcon')[1]); + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.deep.equal([options[0]]); + }); + + it('navigates between different tags', () => { + const handleChange = spy(); + const options = ['one', 'two']; + render( + } + multiple + />, + ); + const textbox = screen.getByRole('textbox'); + const [firstSelectedValue, secondSelectedValue] = screen.getAllByRole('button'); + + fireEvent.keyDown(textbox, { key: 'ArrowLeft' }); + expect(secondSelectedValue).toHaveFocus(); + + fireEvent.keyDown(secondSelectedValue, { key: 'ArrowLeft' }); + expect(firstSelectedValue).toHaveFocus(); + + fireEvent.keyDown(firstSelectedValue, { key: 'Backspace' }); + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.deep.equal([options[1]]); + expect(textbox).toHaveFocus(); + }); + + it('should not crash if a tag is missing', () => { + const handleChange = spy(); + const options = ['one', 'two']; + render( + + value + .filter((x, index) => index === 1) + .map((option, index) => ) + } + onChange={handleChange} + renderInput={(params) => } + multiple + />, + ); + const textbox = screen.getByRole('textbox'); + const [firstSelectedValue] = screen.getAllByRole('button'); + + fireEvent.keyDown(textbox, { key: 'ArrowLeft' }); + // skip value "two" + expect(firstSelectedValue).toHaveFocus(); + + fireEvent.keyDown(firstSelectedValue, { key: 'ArrowRight' }); + expect(textbox).toHaveFocus(); + }); + + it('has no textbox value', () => { + render( + } + multiple + value={['one', 'two']} + />, + ); + + expect(screen.getByRole('textbox')).to.have.property('value', ''); + }); + + it('should fail validation if a required field has no value', function test() { + if (/jsdom/.test(window.navigator.userAgent)) { + // Enable once https://github.com/jsdom/jsdom/issues/2898 is resolved + this.skip(); + } + + const handleSubmit = spy((event) => event.preventDefault()); + render( +
    + } + value={[]} + /> + + , + ); + + screen.getByRole('button', { name: 'Submit' }).click(); + + expect(handleSubmit.callCount).to.equal(0); + }); + + it('should fail validation if a required field has a value', function test() { + // Unclear how native Constraint validation can be enabled for `multiple` + if (/jsdom/.test(window.navigator.userAgent)) { + // Enable once https://github.com/jsdom/jsdom/issues/2898 is resolved + // The test is passing in JSDOM but form validation is buggy in JSDOM so we rather skip than have false confidence + this.skip(); + } + + const handleSubmit = spy((event) => event.preventDefault()); + render( +
    + } + value={['one']} + /> + + , + ); + + screen.getByRole('button', { name: 'Submit' }).click(); + + expect(handleSubmit.callCount).to.equal(0); + }); + }); + + it('should trigger a form expectedly', () => { + const handleSubmit = spy(); + const { setProps } = render( + { + if (!event.defaultPrevented && event.key === 'Enter') { + handleSubmit(); + } + }} + renderInput={(props2) => } + />, + ); + let textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'Enter' }); + expect(handleSubmit.callCount).to.equal(1); + + fireEvent.change(textbox, { target: { value: 'o' } }); + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + expect(handleSubmit.callCount).to.equal(1); + + fireEvent.keyDown(textbox, { key: 'Enter' }); + expect(handleSubmit.callCount).to.equal(2); + + setProps({ key: 'test-2', multiple: true, freeSolo: true }); + textbox = screen.getByRole('textbox'); + + fireEvent.change(textbox, { target: { value: 'o' } }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + expect(handleSubmit.callCount).to.equal(2); + + fireEvent.keyDown(textbox, { key: 'Enter' }); + expect(handleSubmit.callCount).to.equal(3); + + setProps({ key: 'test-3', freeSolo: true }); + textbox = screen.getByRole('textbox'); + + fireEvent.change(textbox, { target: { value: 'o' } }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + expect(handleSubmit.callCount).to.equal(4); + }); + + describe('prop: getOptionDisabled', () => { + it('should disable the option but allow focus with disabledItemsFocusable', () => { + const handleSubmit = spy(); + const handleChange = spy(); + const { getAllByRole } = render( + option === 'two'} + onKeyDown={(event) => { + if (!event.defaultPrevented && event.key === 'Enter') { + handleSubmit(); + } + }} + onChange={handleChange} + renderInput={(props2) => } + />, + ); + + let options; + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + options = getAllByRole('option'); + expect(textbox).to.have.attribute('aria-activedescendant', options[1].getAttribute('id')); + + fireEvent.keyDown(textbox, { key: 'Enter' }); + expect(handleSubmit.callCount).to.equal(0); + expect(handleChange.callCount).to.equal(0); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + options = getAllByRole('option'); + expect(textbox).to.have.attribute('aria-activedescendant', options[0].getAttribute('id')); + + fireEvent.keyDown(textbox, { key: 'Enter' }); + expect(handleSubmit.callCount).to.equal(0); + expect(handleChange.callCount).to.equal(1); + }); + }); + + describe('WAI-ARIA conforming markup', () => { + specify('when closed', () => { + const { getAllByRole, getByRole, queryByRole } = render( + } />, + ); + + const combobox = getByRole('combobox'); + expect(combobox).to.have.attribute('aria-expanded', 'false'); + // reflected aria-haspopup is `listbox` + // this assertion can fail if the value is `listbox` + expect(combobox).not.to.have.attribute('aria-haspopup'); + + const textbox = getByRole('textbox'); + expect(combobox).to.contain(textbox); + // reflected aria-multiline has to be false i.e. not present or false + expect(textbox).not.to.have.attribute('aria-multiline'); + expect(textbox).to.have.attribute('aria-autocomplete', 'list'); + expect(textbox, 'no option is focused when openened').not.to.have.attribute( + 'aria-activedescendant', + ); + + // listbox is not only inaccessible but not in the DOM + const listbox = queryByRole('listbox', { hidden: true }); + expect(listbox).to.equal(null); + + const buttons = getAllByRole('button', { hidden: true }); + if (!/jsdom/.test(window.navigator.userAgent)) { + expect(buttons[0]).toBeInaccessible(); + } else { + // JSDOM thinks the "Clear"-button has `visibility: visible` + // Leaving this to be notified once the JSDOM is fixed. + expect(buttons[0]).not.toBeInaccessible(); + } + expect(buttons[1]).toHaveAccessibleName('Open'); + expect(buttons[1]).to.have.attribute('title', 'Open'); + expect(buttons).to.have.length(2); + buttons.forEach((button) => { + expect(button, 'button is not in tab order').to.have.property('tabIndex', -1); + }); + }); + + specify('when open', () => { + const { getAllByRole, getByRole } = render( + } + />, + ); + + const combobox = getByRole('combobox'); + expect(combobox).to.have.attribute('aria-expanded', 'true'); + + const textbox = getByRole('textbox'); + + const listbox = getByRole('listbox'); + expect(combobox, 'combobox owns listbox').to.have.attribute( + 'aria-owns', + listbox.getAttribute('id'), + ); + expect(textbox).to.have.attribute('aria-controls', listbox.getAttribute('id')); + expect(textbox, 'no option is focused when openened').not.to.have.attribute( + 'aria-activedescendant', + ); + + const options = getAllByRole('option'); + expect(options).to.have.length(2); + options.forEach((option) => { + expect(listbox).to.contain(option); + }); + + const buttons = getAllByRole('button', { hidden: true }); + if (!/jsdom/.test(window.navigator.userAgent)) { + expect(buttons[0]).toBeInaccessible(); + } + expect(buttons[1]).toHaveAccessibleName('Close'); + expect(buttons[1]).to.have.attribute('title', 'Close'); + expect(buttons).to.have.length(2); + buttons.forEach((button) => { + expect(button, 'button is not in tab order').to.have.property('tabIndex', -1); + }); + }); + + it('should add and remove aria-activedescendant', () => { + const { getAllByRole, getByRole, setProps } = render( + } + />, + ); + const textbox = getByRole('textbox'); + expect(textbox, 'no option is focused when openened').not.to.have.attribute( + 'aria-activedescendant', + ); + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + + const options = getAllByRole('option'); + expect(textbox).to.have.attribute('aria-activedescendant', options[0].getAttribute('id')); + setProps({ open: false }); + expect(textbox, 'no option is focused when openened').not.to.have.attribute( + 'aria-activedescendant', + ); + }); + }); + + describe('when popup closed', () => { + it('opens when the textbox is focused', () => { + const handleOpen = spy(); + render( + } + />, + ); + + expect(handleOpen.callCount).to.equal(1); + }); + + it('does not open on clear', () => { + const handleOpen = spy(); + const handleChange = spy(); + const { container } = render( + } + />, + ); + + const clear = container.querySelector('button'); + fireEvent.click(clear); + + expect(handleOpen.callCount).to.equal(0); + expect(handleChange.callCount).to.equal(1); + }); + + ['ArrowDown', 'ArrowUp'].forEach((key) => { + it(`opens on ${key} when focus is on the textbox without moving focus`, () => { + const handleOpen = spy(); + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key }); + + // first from focus + expect(handleOpen.callCount).to.equal(2); + expect(textbox).not.to.have.attribute('aria-activedescendant'); + }); + }); + + it('does not clear the textbox on Escape', () => { + const handleChange = spy(); + render( + } + />, + ); + + fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Escape' }); + + expect(handleChange.callCount).to.equal(0); + }); + }); + + describe('prop: clearOnEscape', () => { + it('should clear on escape', () => { + const handleChange = spy(); + render( + } + />, + ); + + fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Escape' }); + fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Escape' }); + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.deep.equal([]); + }); + }); + + describe('when popup open', () => { + it('closes the popup if Escape is pressed ', () => { + const handleClose = spy(); + render( + } + />, + ); + + fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Escape' }); + expect(handleClose.callCount).to.equal(1); + }); + + it('does not close the popup when option selected if Control is pressed', () => { + const handleClose = spy(); + const { getAllByRole } = render( + } + />, + ); + + const options = getAllByRole('option'); + fireEvent.click(options[0], { ctrlKey: true }); + expect(handleClose.callCount).to.equal(0); + }); + + it('does not close the popup when option selected if Meta is pressed', () => { + const handleClose = spy(); + const { getAllByRole } = render( + } + />, + ); + + const options = getAllByRole('option'); + fireEvent.click(options[0], { metaKey: true }); + expect(handleClose.callCount).to.equal(0); + }); + + it('moves focus to the first option on ArrowDown', () => { + const { getAllByRole, getByRole } = render( + } + />, + ); + + fireEvent.keyDown(screen.getByRole('textbox'), { key: 'ArrowDown' }); + expect(getByRole('textbox')).to.have.attribute( + 'aria-activedescendant', + getAllByRole('option')[0].getAttribute('id'), + ); + }); + + it('moves focus to the last option on ArrowUp', () => { + const { getAllByRole, getByRole } = render( + } + />, + ); + + fireEvent.keyDown(screen.getByRole('textbox'), { key: 'ArrowUp' }); + const options = getAllByRole('option'); + expect(getByRole('textbox')).to.have.attribute( + 'aria-activedescendant', + options[options.length - 1].getAttribute('id'), + ); + }); + }); + + describe('prop: openOnFocus', () => { + it('enables open on input focus', () => { + const { getByRole } = render( + } + />, + ); + const textbox = getByRole('textbox'); + const combobox = getByRole('combobox'); + + expect(combobox).to.have.attribute('aria-expanded', 'true'); + expect(textbox).toHaveFocus(); + + fireEvent.mouseDown(textbox); + fireEvent.click(textbox); + expect(combobox).to.have.attribute('aria-expanded', 'false'); + + act(() => { + document.activeElement.blur(); + }); + + expect(combobox).to.have.attribute('aria-expanded', 'false'); + expect(textbox).not.toHaveFocus(); + + fireEvent.mouseDown(textbox); + fireEvent.click(textbox); + expect(combobox).to.have.attribute('aria-expanded', 'true'); + expect(textbox).toHaveFocus(); + + fireEvent.mouseDown(textbox); + fireEvent.click(textbox); + expect(combobox).to.have.attribute('aria-expanded', 'false'); + }); + }); + + describe('listbox wrapping behavior', () => { + it('wraps around when navigating the list by default', () => { + const { getAllByRole } = render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'ArrowUp' }); + + const options = getAllByRole('option'); + expect(textbox).toHaveFocus(); + expect(textbox).to.have.attribute( + 'aria-activedescendant', + options[options.length - 1].getAttribute('id'), + ); + }); + + it('selects the first item if on the last item and pressing up by default', () => { + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowUp' }); + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + + const options = screen.getAllByRole('option'); + expect(textbox).toHaveFocus(); + expect(textbox).to.have.attribute('aria-activedescendant', options[0].getAttribute('id')); + }); + + describe('prop: includeInputInList', () => { + it('considers the textbox the predessor of the first option when pressing Up', () => { + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'ArrowUp' }); + + expect(textbox).toHaveFocus(); + expect(textbox).not.to.have.attribute('aria-activedescendant'); + }); + + it('considers the textbox the successor of the last option when pressing Down', () => { + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowUp' }); + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + + expect(textbox).toHaveFocus(); + expect(textbox).not.to.have.attribute('aria-activedescendant'); + }); + }); + + describe('prop: disableListWrap', () => { + it('keeps focus on the first item if focus is on the first item and pressing Up', () => { + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'ArrowUp' }); + + expect(textbox).toHaveFocus(); + expect(textbox).to.have.attribute( + 'aria-activedescendant', + screen.getAllByRole('option')[0].getAttribute('id'), + ); + }); + + it('focuses the last item when pressing Up when no option is active', () => { + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowUp' }); + + const options = screen.getAllByRole('option'); + expect(textbox).toHaveFocus(); + expect(textbox).to.have.attribute( + 'aria-activedescendant', + options[options.length - 1].getAttribute('id'), + ); + }); + + it('keeps focus on the last item if focus is on the last item and pressing Down', () => { + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + + const options = screen.getAllByRole('option'); + expect(textbox).toHaveFocus(); + expect(textbox).to.have.attribute( + 'aria-activedescendant', + options[options.length - 1].getAttribute('id'), + ); + }); + }); + }); + + describe('prop: disabled', () => { + it('should disable the input', () => { + const { getByRole } = render( + } + />, + ); + const input = getByRole('textbox'); + expect(input.disabled).to.equal(true); + }); + + it('should disable the popup button', () => { + const { queryByTitle } = render( + } + />, + ); + expect(queryByTitle('Open').disabled).to.equal(true); + }); + + it('should not render the clear button', () => { + const { queryByTitle } = render( + } + />, + ); + expect(queryByTitle('Clear')).to.equal(null); + }); + + it('should not apply the hasClearIcon class', () => { + const { container } = render( + } + />, + ); + expect(container.querySelector(`.${classes.root}`)).not.to.have.class(classes.hasClearIcon); + expect(container.querySelector(`.${classes.root}`)).to.have.class(classes.hasPopupIcon); + }); + }); + + describe('prop: disableClearable', () => { + it('should not render the clear button', () => { + const { queryByTitle, container } = render( + } + />, + ); + expect(queryByTitle('Clear')).to.equal(null); + expect(container.querySelector(`.${classes.root}`)).to.have.class(classes.hasPopupIcon); + expect(container.querySelector(`.${classes.root}`)).not.to.have.class(classes.hasClearIcon); + }); + }); + + describe('warnings', () => { + it('warn if getOptionLabel do not return a string', () => { + const handleChange = spy(); + render( + option.name} + renderInput={(params) => } + />, + ); + const textbox = screen.getByRole('textbox'); + + expect(() => { + fireEvent.change(textbox, { target: { value: 'a' } }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + }).toErrorDev([ + 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', + // strict mode renders twice + 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', + 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', + 'Material-UI: The `getOptionLabel` method of Autocomplete returned undefined instead of a string', + ]); + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.equal('a'); + }); + + it('warn if getOptionSelected match multiple values for a given option', () => { + const value = [ + { id: '10', text: 'One' }, + { id: '20', text: 'Two' }, + ]; + const options = [ + { id: '10', text: 'One' }, + { id: '20', text: 'Two' }, + { id: '30', text: 'Three' }, + ]; + + render( + option.text} + getOptionSelected={(option) => value.find((v) => v.id === option.id)} + renderInput={(params) => } + />, + ); + const textbox = screen.getByRole('textbox'); + + expect(() => { + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + }).toErrorDev( + 'The component expects a single value to match a given option but found 2 matches.', + ); + }); + + it('warn if value does not exist in options list', () => { + const value = 'not a good value'; + const options = ['first option', 'second option']; + + expect(() => { + render( + } + />, + ); + }).toWarnDev([ + 'None of the options match with `"not a good value"`', + // strict mode renders twice + 'None of the options match with `"not a good value"`', + 'None of the options match with `"not a good value"`', + 'None of the options match with `"not a good value"`', + ]); + }); + + it('warn if groups options are not sorted', () => { + const data = [ + { group: 1, value: 'A' }, + { group: 2, value: 'D' }, + { group: 2, value: 'E' }, + { group: 1, value: 'B' }, + { group: 3, value: 'G' }, + { group: 2, value: 'F' }, + { group: 1, value: 'C' }, + ]; + expect(() => { + render( + option.value} + renderInput={(params) => } + groupBy={(option) => option.group} + />, + ); + }).toWarnDev([ + // strict mode renders twice + 'returns duplicated headers', + 'returns duplicated headers', + ]); + const options = screen.getAllByRole('option').map((el) => el.textContent); + expect(options).to.have.length(7); + expect(options).to.deep.equal(['A', 'D', 'E', 'B', 'G', 'F', 'C']); + }); + }); + + describe('prop: options', () => { + it('should keep focus on selected option and not reset to top option when options updated', () => { + const { setProps } = render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + const listbox = screen.getByRole('listbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); // goes to 'one' + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); // goes to 'two' + + function checkHighlightIs(expected) { + expect(listbox.querySelector('li[data-focus]')).to.have.text(expected); + } + + checkHighlightIs('two'); + + // three option is added and autocomplete re-renders, two should still be highlighted + setProps({ options: ['one', 'two', 'three'] }); + checkHighlightIs('two'); + }); + + it('should not select undefined', () => { + const handleChange = spy(); + const { getByRole } = render( + } + />, + ); + const input = getByRole('textbox'); + fireEvent.click(input); + + const listbox = getByRole('listbox'); + const firstOption = listbox.querySelector('li'); + fireEvent.click(firstOption); + + expect(handleChange.args[0][1]).to.equal('one'); + }); + + it('should work if options are the default data structure', () => { + const options = [ + { + label: 'one', + }, + ]; + const handleChange = spy(); + const { getByRole } = render( + } + />, + ); + + const input = getByRole('textbox'); + fireEvent.click(input); + + const listbox = getByRole('listbox'); + const htmlOptions = listbox.querySelectorAll('li'); + + expect(htmlOptions[0].innerHTML).to.equal('one'); + }); + }); + + describe('enter', () => { + it('select a single value when enter is pressed', () => { + const handleChange = spy(); + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.equal('one'); + fireEvent.keyDown(textbox, { key: 'Enter' }); + expect(handleChange.callCount).to.equal(1); + }); + + it('select multiple value when enter is pressed', () => { + const handleChange = spy(); + const options = [{ name: 'one' }, { name: 'two ' }]; + render( + option.name} + renderInput={(params) => } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.deep.equal([options[0]]); + fireEvent.keyDown(textbox, { key: 'Enter' }); + expect(handleChange.callCount).to.equal(1); + }); + }); + + describe('prop: autoComplete', () => { + it('add a completion string', () => { + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.change(document.activeElement, { target: { value: 'O' } }); + + expect(document.activeElement.value).to.equal('O'); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + + expect(document.activeElement.value).to.equal('one'); + expect(document.activeElement.selectionStart).to.equal(1); + expect(document.activeElement.selectionEnd).to.equal(3); + + fireEvent.keyDown(textbox, { key: 'Enter' }); + + expect(document.activeElement.value).to.equal('one'); + expect(document.activeElement.selectionStart).to.equal(3); + expect(document.activeElement.selectionEnd).to.equal(3); + }); + }); + + describe('click input', () => { + it('toggles if empty', () => { + const { getByRole } = render( + } + />, + ); + const textbox = getByRole('textbox'); + const combobox = getByRole('combobox'); + expect(combobox).to.have.attribute('aria-expanded', 'false'); + fireEvent.mouseDown(textbox); + expect(combobox).to.have.attribute('aria-expanded', 'true'); + fireEvent.mouseDown(textbox); + expect(combobox).to.have.attribute('aria-expanded', 'false'); + }); + + it('selects all the first time', () => { + const { getByRole } = render( + } + />, + ); + const textbox = getByRole('textbox'); + fireEvent.click(textbox); + expect(textbox.selectionStart).to.equal(0); + expect(textbox.selectionEnd).to.equal(3); + }); + + it('should focus the input when clicking on the open action', () => { + const { getByRole, queryByTitle } = render( + } + />, + ); + + const textbox = getByRole('textbox'); + fireEvent.click(textbox); + expect(textbox).toHaveFocus(); + + act(() => { + textbox.blur(); + }); + fireEvent.click(queryByTitle('Open')); + + expect(textbox).toHaveFocus(); + }); + + it('should mantain list box open clicking on input when it is not empty', () => { + const handleHighlightChange = spy(); + const { getByRole, getAllByRole } = render( + } + />, + ); + const combobox = getByRole('combobox'); + const textbox = getByRole('textbox'); + + expect(combobox).to.have.attribute('aria-expanded', 'false'); + fireEvent.mouseDown(textbox); // Open listbox + expect(combobox).to.have.attribute('aria-expanded', 'true'); + const options = getAllByRole('option'); + fireEvent.click(options[0]); + expect(combobox).to.have.attribute('aria-expanded', 'false'); + fireEvent.mouseDown(textbox); // Open listbox + expect(combobox).to.have.attribute('aria-expanded', 'true'); + fireEvent.mouseDown(textbox); // Remain open listbox + expect(combobox).to.have.attribute('aria-expanded', 'true'); + }); + + it('should not toggle list box', () => { + const handleHighlightChange = spy(); + const { getByRole } = render( + } + />, + ); + const combobox = getByRole('combobox'); + const textbox = getByRole('textbox'); + + expect(combobox).to.have.attribute('aria-expanded', 'false'); + fireEvent.mouseDown(textbox); + expect(combobox).to.have.attribute('aria-expanded', 'true'); + fireEvent.mouseDown(textbox); + expect(combobox).to.have.attribute('aria-expanded', 'true'); + }); + }); + + describe('controlled', () => { + it('controls the input value', () => { + const handleChange = spy(); + function MyComponent() { + const [, setInputValue] = React.useState(''); + const handleInputChange = (event, value) => { + handleChange(value); + setInputValue(value); + }; + return ( + } + /> + ); + } + + render(); + + expect(handleChange.callCount).to.equal(0); + fireEvent.change(document.activeElement, { target: { value: 'a' } }); + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][0]).to.equal('a'); + expect(document.activeElement.value).to.equal(''); + }); + + it('should fire the input change event before the change event', () => { + const handleChange = spy(); + const handleInputChange = spy(); + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + + expect(handleInputChange.calledBefore(handleChange)).to.equal(true); + }); + }); + + describe('prop: filterOptions', () => { + it('should ignore object keys by default', () => { + const { queryAllByRole } = render( + option.label} + renderInput={(params) => } + />, + ); + let options; + options = queryAllByRole('option'); + expect(options.length).to.equal(2); + + fireEvent.change(document.activeElement, { target: { value: 'value' } }); + options = queryAllByRole('option'); + expect(options.length).to.equal(0); + + fireEvent.change(document.activeElement, { target: { value: 'one' } }); + options = queryAllByRole('option'); + expect(options.length).to.equal(1); + }); + + it('limits the amount of rendered options when `limit` is set in `createFilterOptions`', () => { + const filterOptions = createFilterOptions({ limit: 2 }); + const { queryAllByRole } = render( + } + filterOptions={filterOptions} + />, + ); + expect(queryAllByRole('option').length).to.equal(2); + }); + + it('does not limit the amount of rendered options when `limit` is not set in `createFilterOptions`', () => { + const filterOptions = createFilterOptions({}); + const { queryAllByRole } = render( + } + filterOptions={filterOptions} + />, + ); + expect(queryAllByRole('option').length).to.equal(3); + }); + }); + + describe('prop: freeSolo', () => { + it('pressing twice enter should not call onChange listener twice', () => { + const handleChange = spy(); + const options = [{ name: 'foo' }]; + render( + option.name} + renderInput={(params) => } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.deep.equal(options[0]); + + fireEvent.keyDown(textbox, { key: 'Enter' }); + + expect(handleChange.callCount).to.equal(1); + }); + + it('should not delete exiting tag when try to add it twice', () => { + const handleChange = spy(); + const options = ['one', 'two']; + const { container } = render( + } + multiple + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.change(textbox, { target: { value: 'three' } }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + + expect(container.querySelectorAll('[class*="MuiChip-root"]')).to.have.length(3); + + fireEvent.change(textbox, { target: { value: 'three' } }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + + expect(container.querySelectorAll('[class*="MuiChip-root"]')).to.have.length(3); + }); + + it('should not fire change event until the IME is confirmed', () => { + const handleChange = spy(); + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + // Actual behavior when "あ" (Japanese) is entered on macOS/Safari with IME + fireEvent.change(textbox, { target: { value: 'あ' } }); + fireEvent.keyDown(textbox, { key: 'Enter', keyCode: 229 }); + + expect(handleChange.callCount).to.equal(0); + + fireEvent.keyDown(textbox, { key: 'Enter', keyCode: 13 }); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.equal('あ'); + }); + }); + + describe('prop: onChange', () => { + it('provides a reason and details on option creation', () => { + const handleChange = spy(); + const options = ['one', 'two', 'three']; + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.change(textbox, { target: { value: options[2] } }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.equal(options[2]); + expect(handleChange.args[0][2]).to.equal('create-option'); + expect(handleChange.args[0][3]).to.deep.equal({ option: options[2] }); + }); + + it('provides a reason and details on option selection', () => { + const handleChange = spy(); + const options = ['one', 'two', 'three']; + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.equal(options[0]); + expect(handleChange.args[0][2]).to.equal('select-option'); + expect(handleChange.args[0][3]).to.deep.equal({ option: options[0] }); + }); + + it('provides a reason and details on option removing', () => { + const handleChange = spy(); + const options = ['one', 'two', 'three']; + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'Backspace' }); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.deep.equal(options.slice(0, 2)); + expect(handleChange.args[0][2]).to.equal('remove-option'); + expect(handleChange.args[0][3]).to.deep.equal({ option: options[2] }); + }); + + it('provides a reason and details on blur', () => { + const handleChange = spy(); + const options = ['one', 'two', 'three']; + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + act(() => { + textbox.blur(); + }); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.equal(options[0]); + expect(handleChange.args[0][2]).to.equal('blur'); + expect(handleChange.args[0][3]).to.deep.equal({ option: options[0] }); + }); + + it('provides a reason and details on clear', () => { + const handleChange = spy(); + const options = ['one', 'two', 'three']; + const { container } = render( + } + />, + ); + + const button = container.querySelector('button'); + fireEvent.click(button); + expect(handleChange.callCount).to.equal(1); + expect(handleChange.args[0][1]).to.deep.equal([]); + expect(handleChange.args[0][2]).to.equal('clear'); + expect(handleChange.args[0][3]).to.equal(undefined); + }); + }); + + describe('prop: onInputChange', () => { + it('provides a reason on input change', () => { + const handleInputChange = spy(); + const options = [{ name: 'foo' }]; + render( + option.name} + renderInput={(params) => } + />, + ); + fireEvent.change(document.activeElement, { target: { value: 'a' } }); + expect(handleInputChange.callCount).to.equal(1); + expect(handleInputChange.args[0][1]).to.equal('a'); + expect(handleInputChange.args[0][2]).to.equal('input'); + }); + + it('provides a reason on select reset', () => { + const handleInputChange = spy(); + const options = [{ name: 'foo' }]; + render( + option.name} + renderInput={(params) => } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'Enter' }); + + expect(handleInputChange.callCount).to.equal(1); + expect(handleInputChange.args[0][1]).to.equal(options[0].name); + expect(handleInputChange.args[0][2]).to.equal('reset'); + }); + }); + + describe('prop: blurOnSelect', () => { + it('[blurOnSelect=true] should blur the input when clicking or touching options', () => { + const options = [{ name: 'foo' }]; + const { getByRole, queryByTitle } = render( + option.name} + renderInput={(params) => } + blurOnSelect + />, + ); + const textbox = getByRole('textbox'); + let firstOption = getByRole('option'); + expect(textbox).toHaveFocus(); + fireEvent.click(firstOption); + expect(textbox).not.toHaveFocus(); + + fireEvent.click(queryByTitle('Open')); + expect(textbox).toHaveFocus(); + firstOption = getByRole('option'); + fireEvent.touchStart(firstOption); + fireEvent.click(firstOption); + expect(textbox).not.toHaveFocus(); + }); + + it('[blurOnSelect="touch"] should only blur the input when an option is touched', () => { + const options = [{ name: 'foo' }]; + const { getByRole, queryByTitle } = render( + option.name} + renderInput={(params) => } + blurOnSelect="touch" + />, + ); + const textbox = getByRole('textbox'); + let firstOption = getByRole('option'); + fireEvent.click(firstOption); + expect(textbox).toHaveFocus(); + + fireEvent.click(queryByTitle('Open')); + firstOption = getByRole('option'); + fireEvent.touchStart(firstOption); + fireEvent.click(firstOption); + expect(textbox).not.toHaveFocus(); + }); + + it('[blurOnSelect="mouse"] should only blur the input when an option is clicked', () => { + const options = [{ name: 'foo' }]; + const { getByRole, queryByTitle } = render( + option.name} + renderInput={(params) => } + blurOnSelect="mouse" + />, + ); + const textbox = getByRole('textbox'); + let firstOption = getByRole('option'); + fireEvent.touchStart(firstOption); + fireEvent.click(firstOption); + expect(textbox).toHaveFocus(); + + fireEvent.click(queryByTitle('Open')); + firstOption = getByRole('option'); + fireEvent.click(firstOption); + expect(textbox).not.toHaveFocus(); + }); + }); + + describe('prop: getOptionLabel', () => { + it('is considered for falsy values when filtering the list of options', () => { + const { getAllByRole } = render( + (option === 0 ? 'Any' : option.toString())} + renderInput={(params) => } + value={0} + />, + ); + + const options = getAllByRole('option'); + expect(options).to.have.length(3); + }); + + it('is not considered for nullish values when filtering the list of options', () => { + const { getAllByRole } = render( + (option === null ? 'Any' : option.toString())} + renderInput={(params) => } + value={null} + />, + ); + + const options = getAllByRole('option'); + expect(options).to.have.length(3); + }); + }); + + describe('prop: fullWidth', () => { + it('should have the fullWidth class', () => { + const { container } = render( + } + value={null} + />, + ); + + expect(container.querySelector(`.${classes.root}`)).to.have.class(classes.fullWidth); + }); + }); + + describe('prop: onHighlightChange', () => { + it('should trigger event when default value is passed', () => { + const handleHighlightChange = spy(); + const options = ['one', 'two', 'three']; + render( + } + />, + ); + expect(handleHighlightChange.callCount).to.equal(1); + expect(handleHighlightChange.args[0][0]).to.equal(undefined); + expect(handleHighlightChange.args[0][1]).to.equal(options[0]); + expect(handleHighlightChange.args[0][2]).to.equal('auto'); + }); + + it('should support keyboard event', () => { + const handleHighlightChange = spy(); + const options = ['one', 'two', 'three']; + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + expect(handleHighlightChange.callCount).to.equal(3); + expect(handleHighlightChange.args[2][0]).to.not.equal(undefined); + expect(handleHighlightChange.args[2][1]).to.equal(options[0]); + expect(handleHighlightChange.args[2][2]).to.equal('keyboard'); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + expect(handleHighlightChange.callCount).to.equal(4); + expect(handleHighlightChange.args[3][0]).to.not.equal(undefined); + expect(handleHighlightChange.args[3][1]).to.equal(options[1]); + expect(handleHighlightChange.args[3][2]).to.equal('keyboard'); + }); + + it('should support mouse event', () => { + const handleHighlightChange = spy(); + const options = ['one', 'two', 'three']; + const { getAllByRole } = render( + } + />, + ); + const firstOption = getAllByRole('option')[0]; + fireEvent.mouseOver(firstOption); + expect(handleHighlightChange.callCount).to.equal(3); + expect(handleHighlightChange.args[2][0]).to.not.equal(undefined); + expect(handleHighlightChange.args[2][1]).to.equal(options[0]); + expect(handleHighlightChange.args[2][2]).to.equal('mouse'); + }); + + it('should pass to onHighlightChange the correct value after filtering', () => { + const handleHighlightChange = spy(); + const options = ['one', 'three', 'onetwo']; + render( + } + />, + ); + const textbox = screen.getByRole('textbox'); + + fireEvent.change(document.activeElement, { target: { value: 'one' } }); + expect(screen.getAllByRole('option').length).to.equal(2); + + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + fireEvent.keyDown(textbox, { key: 'ArrowDown' }); + expect(handleHighlightChange.args[handleHighlightChange.args.length - 1][1]).to.equal( + options[2], + ); + }); + }); +}); diff --git a/packages/material-ui/src/Autocomplete/index.d.ts b/packages/material-ui/src/Autocomplete/index.d.ts new file mode 100644 index 00000000000000..f153149a711c3f --- /dev/null +++ b/packages/material-ui/src/Autocomplete/index.d.ts @@ -0,0 +1,2 @@ +export { default } from './Autocomplete'; +export * from './Autocomplete'; diff --git a/packages/material-ui/src/Autocomplete/index.js b/packages/material-ui/src/Autocomplete/index.js new file mode 100644 index 00000000000000..3c3fef79e84133 --- /dev/null +++ b/packages/material-ui/src/Autocomplete/index.js @@ -0,0 +1 @@ +export { default, createFilterOptions } from './Autocomplete'; diff --git a/packages/material-ui/src/index.d.ts b/packages/material-ui/src/index.d.ts index 35108fbe2391f8..776538f39497e3 100644 --- a/packages/material-ui/src/index.d.ts +++ b/packages/material-ui/src/index.d.ts @@ -101,6 +101,9 @@ export * from './AlertTitle'; export { default as AppBar } from './AppBar'; export * from './AppBar'; +export { default as Autocomplete } from './Autocomplete'; +export * from './Autocomplete'; + export { default as Avatar } from './Avatar'; export * from './Avatar'; @@ -436,3 +439,6 @@ export * from './withWidth'; export { default as Zoom } from './Zoom'; export * from './Zoom'; + +export { default as useAutocomplete } from './useAutocomplete'; +export * from './useAutocomplete'; diff --git a/packages/material-ui/src/index.js b/packages/material-ui/src/index.js index 0cb8381dc7924c..deb3385c71f37e 100644 --- a/packages/material-ui/src/index.js +++ b/packages/material-ui/src/index.js @@ -27,6 +27,9 @@ export * from './AlertTitle'; export { default as AppBar } from './AppBar'; export * from './AppBar'; +export { default as Autocomplete } from './Autocomplete'; +export * from './Autocomplete'; + export { default as Avatar } from './Avatar'; export * from './Avatar'; @@ -362,3 +365,6 @@ export * from './withWidth'; export { default as Zoom } from './Zoom'; export * from './Zoom'; + +// createFilterOptions is exported from Autocomplete +export { default as useAutocomplete } from './useAutocomplete'; diff --git a/packages/material-ui/src/styles/overrides.d.ts b/packages/material-ui/src/styles/overrides.d.ts index 7e35ad6e14a672..7284701f3a18d2 100644 --- a/packages/material-ui/src/styles/overrides.d.ts +++ b/packages/material-ui/src/styles/overrides.d.ts @@ -2,6 +2,7 @@ import { CSSProperties, StyleRules } from './withStyles'; import { AlertClassKey } from '../Alert'; import { AlertTitleClassKey } from '../AlertTitle'; import { AppBarClassKey } from '../AppBar'; +import { AutocompleteClassKey } from '../Autocomplete'; import { AvatarClassKey } from '../Avatar'; import { BackdropClassKey } from '../Backdrop'; import { BadgeClassKey } from '../Badge'; @@ -112,6 +113,7 @@ export interface ComponentNameToClassKey { MuiAlert: AlertClassKey; MuiAlertTitle: AlertTitleClassKey; MuiAppBar: AppBarClassKey; + MuiAutocomplete: AutocompleteClassKey; MuiAvatar: AvatarClassKey; MuiBackdrop: BackdropClassKey; MuiBadge: BadgeClassKey; diff --git a/packages/material-ui/src/styles/props.d.ts b/packages/material-ui/src/styles/props.d.ts index 73cb9aaa6370c8..521b47eba5d215 100644 --- a/packages/material-ui/src/styles/props.d.ts +++ b/packages/material-ui/src/styles/props.d.ts @@ -1,6 +1,7 @@ import { AlertProps } from '../Alert'; import { AlertTitleProps } from '../AlertTitle'; import { AppBarProps } from '../AppBar'; +import { AutocompleteProps } from '../Autocomplete'; import { AvatarProps } from '../Avatar'; import { BackdropProps } from '../Backdrop'; import { BadgeProps } from '../Badge'; @@ -112,6 +113,7 @@ export interface ComponentsPropsList { MuiAlert: AlertProps; MuiAlertTitle: AlertTitleProps; MuiAppBar: AppBarProps; + MuiAutocomplete: AutocompleteProps; MuiAvatar: AvatarProps; MuiBackdrop: BackdropProps; MuiBadge: BadgeProps; diff --git a/packages/material-ui/src/useAutocomplete/index.d.ts b/packages/material-ui/src/useAutocomplete/index.d.ts new file mode 100644 index 00000000000000..6f01318688c9f2 --- /dev/null +++ b/packages/material-ui/src/useAutocomplete/index.d.ts @@ -0,0 +1,2 @@ +export { default } from './useAutocomplete'; +export * from './useAutocomplete'; diff --git a/packages/material-ui/src/useAutocomplete/index.js b/packages/material-ui/src/useAutocomplete/index.js new file mode 100644 index 00000000000000..a6c05b480618e9 --- /dev/null +++ b/packages/material-ui/src/useAutocomplete/index.js @@ -0,0 +1 @@ +export { default, createFilterOptions } from './useAutocomplete'; diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts b/packages/material-ui/src/useAutocomplete/useAutocomplete.d.ts similarity index 100% rename from packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts rename to packages/material-ui/src/useAutocomplete/useAutocomplete.d.ts diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui/src/useAutocomplete/useAutocomplete.js similarity index 100% rename from packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js rename to packages/material-ui/src/useAutocomplete/useAutocomplete.js diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.spec.ts b/packages/material-ui/src/useAutocomplete/useAutocomplete.spec.ts similarity index 100% rename from packages/material-ui-lab/src/useAutocomplete/useAutocomplete.spec.ts rename to packages/material-ui/src/useAutocomplete/useAutocomplete.spec.ts diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.test.js b/packages/material-ui/src/useAutocomplete/useAutocomplete.test.js similarity index 100% rename from packages/material-ui-lab/src/useAutocomplete/useAutocomplete.test.js rename to packages/material-ui/src/useAutocomplete/useAutocomplete.test.js From 1844e3b33154df4272848ac744b09f728dff3948 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 24 Sep 2020 03:10:27 +0100 Subject: [PATCH 2/6] fix imports --- .../material-ui/src/Autocomplete/Autocomplete.js | 12 ++++++------ .../src/useAutocomplete/useAutocomplete.js | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.js b/packages/material-ui/src/Autocomplete/Autocomplete.js index b0edc3a2257ee8..9ee4eef45d4630 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.js +++ b/packages/material-ui/src/Autocomplete/Autocomplete.js @@ -1,12 +1,12 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -import { withStyles } from '@material-ui/core/styles'; -import Popper from '@material-ui/core/Popper'; -import ListSubheader from '@material-ui/core/ListSubheader'; -import Paper from '@material-ui/core/Paper'; -import IconButton from '@material-ui/core/IconButton'; -import Chip from '@material-ui/core/Chip'; +import { withStyles } from '../styles'; +import Popper from '../Popper'; +import ListSubheader from '../ListSubheader'; +import Paper from '../Paper'; +import IconButton from '../IconButton'; +import Chip from '../Chip'; import CloseIcon from '../internal/svg-icons/Close'; import ArrowDropDownIcon from '../internal/svg-icons/ArrowDropDown'; import useAutocomplete, { createFilterOptions } from '../useAutocomplete'; diff --git a/packages/material-ui/src/useAutocomplete/useAutocomplete.js b/packages/material-ui/src/useAutocomplete/useAutocomplete.js index 75b3a2093ab5fb..08b39ca5b49633 100644 --- a/packages/material-ui/src/useAutocomplete/useAutocomplete.js +++ b/packages/material-ui/src/useAutocomplete/useAutocomplete.js @@ -5,7 +5,7 @@ import { useEventCallback, useControlled, unstable_useId as useId, -} from '@material-ui/core/utils'; +} from '../utils'; // https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript // Give up on IE 11 support for this feature From 725a969280b594965ace523b60f0305e2e23d736 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 24 Sep 2020 03:30:03 +0100 Subject: [PATCH 3/6] prettier --- .../material-ui/src/useAutocomplete/useAutocomplete.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/material-ui/src/useAutocomplete/useAutocomplete.js b/packages/material-ui/src/useAutocomplete/useAutocomplete.js index 08b39ca5b49633..372e1ff8baab28 100644 --- a/packages/material-ui/src/useAutocomplete/useAutocomplete.js +++ b/packages/material-ui/src/useAutocomplete/useAutocomplete.js @@ -1,11 +1,6 @@ /* eslint-disable no-constant-condition */ import * as React from 'react'; -import { - setRef, - useEventCallback, - useControlled, - unstable_useId as useId, -} from '../utils'; +import { setRef, useEventCallback, useControlled, unstable_useId as useId } from '../utils'; // https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript // Give up on IE 11 support for this feature From a8ca4812d9d14aefd0cb721b1798e368a9f6321e Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Thu, 24 Sep 2020 16:03:12 +0200 Subject: [PATCH 4/6] Fix jsdom tests --- .../src/Autocomplete/Autocomplete.test.js | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.test.js b/packages/material-ui/src/Autocomplete/Autocomplete.test.js index d0f8216ef3bb38..5f0f126f2c8236 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.test.js +++ b/packages/material-ui/src/Autocomplete/Autocomplete.test.js @@ -234,7 +234,11 @@ describe('', () => { getByRole('textbox').focus(); }); expect(container.textContent).to.equal('onetwothree'); - expect(getAllByRole('button', { hidden: false })).to.have.lengthOf(5); + // Depending on the subset of components used in this test run the computed `visibility` changes. + // Running this assertion only in browsers. + if (!/jsdom/.test(window.navigator.userAgent)) { + expect(getAllByRole('button', { hidden: false })).to.have.lengthOf(5); + } }); it('show 0 item on close when set 0 to limitTags', () => { @@ -257,7 +261,11 @@ describe('', () => { getByRole('textbox').focus(); }); expect(container.textContent).to.equal('onetwothree'); - expect(getAllByRole('button', { hidden: false })).to.have.lengthOf(5); + // Depending on the subset of components used in this test run the computed `visibility` changes. + // Running this assertion only in browsers. + if (!/jsdom/.test(window.navigator.userAgent)) { + expect(getAllByRole('button', { hidden: false })).to.have.lengthOf(5); + } }); }); @@ -637,12 +645,10 @@ describe('', () => { expect(listbox).to.equal(null); const buttons = getAllByRole('button', { hidden: true }); + // Depending on the subset of components used in this test run the computed `visibility` changes. + // Running this assertion only in browsers. if (!/jsdom/.test(window.navigator.userAgent)) { expect(buttons[0]).toBeInaccessible(); - } else { - // JSDOM thinks the "Clear"-button has `visibility: visible` - // Leaving this to be notified once the JSDOM is fixed. - expect(buttons[0]).not.toBeInaccessible(); } expect(buttons[1]).toHaveAccessibleName('Open'); expect(buttons[1]).to.have.attribute('title', 'Open'); @@ -1339,7 +1345,7 @@ describe('', () => { expect(htmlOptions[0].innerHTML).to.equal('one'); }); - it('should display a \'no options\' message if no options are available', () => { + it("should display a 'no options' message if no options are available", () => { const { getByRole } = render( Date: Thu, 24 Sep 2020 16:07:17 +0200 Subject: [PATCH 5/6] more concrete explainer --- .../material-ui/src/Autocomplete/Autocomplete.test.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/material-ui/src/Autocomplete/Autocomplete.test.js b/packages/material-ui/src/Autocomplete/Autocomplete.test.js index 5f0f126f2c8236..07345638e726a3 100644 --- a/packages/material-ui/src/Autocomplete/Autocomplete.test.js +++ b/packages/material-ui/src/Autocomplete/Autocomplete.test.js @@ -234,8 +234,7 @@ describe('', () => { getByRole('textbox').focus(); }); expect(container.textContent).to.equal('onetwothree'); - // Depending on the subset of components used in this test run the computed `visibility` changes. - // Running this assertion only in browsers. + // Depending on the subset of components used in this test run the computed `visibility` changes in JSDOM. if (!/jsdom/.test(window.navigator.userAgent)) { expect(getAllByRole('button', { hidden: false })).to.have.lengthOf(5); } @@ -261,8 +260,7 @@ describe('', () => { getByRole('textbox').focus(); }); expect(container.textContent).to.equal('onetwothree'); - // Depending on the subset of components used in this test run the computed `visibility` changes. - // Running this assertion only in browsers. + // Depending on the subset of components used in this test run the computed `visibility` changes in JSDOM. if (!/jsdom/.test(window.navigator.userAgent)) { expect(getAllByRole('button', { hidden: false })).to.have.lengthOf(5); } @@ -645,8 +643,7 @@ describe('', () => { expect(listbox).to.equal(null); const buttons = getAllByRole('button', { hidden: true }); - // Depending on the subset of components used in this test run the computed `visibility` changes. - // Running this assertion only in browsers. + // Depending on the subset of components used in this test run the computed `visibility` changes in JSDOM. if (!/jsdom/.test(window.navigator.userAgent)) { expect(buttons[0]).toBeInaccessible(); } From d3b257ec1e798baf010462774d967a553b171fc6 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 24 Sep 2020 16:50:21 +0100 Subject: [PATCH 6/6] remove empty tests files --- packages/material-ui-lab/src/Alert/Alert.test.js | 0 packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/material-ui-lab/src/Alert/Alert.test.js delete mode 100644 packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js diff --git a/packages/material-ui-lab/src/Alert/Alert.test.js b/packages/material-ui-lab/src/Alert/Alert.test.js deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js deleted file mode 100644 index e69de29bb2d1d6..00000000000000