Repository: mui/material-ui (TypeScript)
Date: 2019-11-02 17:33:26+00:00
Task type: Bug Fix

Issue:
[Docs] Autocomplete Multiple Values delete bug
- [x] The issue is present in the latest release.
- [x] I have searched the [issues](https://github.com/mui-org/material-ui/issues) of this repository and believe that this is not a duplicate.

## Current Behavior 😯

Clicking the delete button for an option in the "Multiple Values"  and "Fixed Options" examples removes whatever is first in the last.

## Expected Behavior 🤔

Clicking the delete button for an option removes its own optoin.

## Steps to Reproduce 🕹

Link:
1. https://material-ui.com/components/autocomplete/#multiple-values
2. https://material-ui.com/components/autocomplete/#fixed-options

Steps:

1. Add a new option
2. Try to delete the new option
3. The option already present will be deleted

## Your Environment 🌎

MUI Documentation


Issue discussion:
On the same page, but unrelated to the issue, the following is written:

> The component exposes a factory to create a filter method that can provided to the `filerOption` prop. You can use it to change the default option filter behavior.

I guess that's supposed to say `filterOption` and not `filerOption`.
@Studio384 Thank you for the report. I can think of two possible solutions:

1. We continue with the attribute path, it's relatively simple, outside the IE 11 ponyfill:
```diff
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
index bc02f10f1..f43a99e08 100644
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
@@ -11,6 +11,26 @@ function stripDiacritics(string) {
     : string;
 }

+// Support IE 11
+// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
+function closest(element, string) {
+  let closestPonyfill = Element.prototype.closest;
+
+  if (!closestPonyfill) {
+    closestPonyfill = () => {
+      let el = element;
+
+      do {
+        if (el.matches(string)) return el;
+        el = el.parentNode;
+      } while (el !== null && el.nodeType === 1);
+      return null;
+    };
+  }
+
+  return closestPonyfill(string);
+}
+
 export function createFilterOptions(config = {}) {
   const {
     ignoreAccents = true,
@@ -584,7 +604,8 @@ export default function useAutocomplete(props) {
   };

   const handleTagDelete = event => {
-    const index = Number(event.currentTarget.getAttribute('data-tag-index'));
+    const tagRoot = closest(event.currentTarget, '[data-tag-index]');
+    const index = Number(tagRoot.getAttribute('data-tag-index'));
     const newValue = [...value];
     newValue.splice(index, 1);
     handleValue(event, newValue);
```
2. We require `getTagProps()` to collect the tag index. Something like this:
```diff
diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
index 4c6aa5260..eddf2c0cb 100644
--- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
+++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
@@ -220,21 +220,20 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
   let startAdornment;

   if (multiple && value.length > 0) {
-    const tagProps = {
-      ...getTagProps(),
+    const getTagProps2 = params => ({
       className: classes.tag,
-    };
+      ...getTagProps(params),
+    });

     if (renderTags) {
-      startAdornment = renderTags(value, tagProps);
+      startAdornment = renderTags(value, getTagProps2);
     } else {
       startAdornment = value.map((option, index) => (
         <Chip
           key={index}
-          data-tag-index={index}
           tabIndex={-1}
           label={getOptionLabel(option)}
-          {...tagProps}
+          {...getTagProps2({ index })}
         />
       ));
     }
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
index bc02f10f1..ea709e7f7 100644
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
@@ -583,8 +583,7 @@ export default function useAutocomplete(props) {
     selectNewValue(event, filteredOptions[highlightedIndexRef.current]);
   };

-  const handleTagDelete = event => {
-    const index = Number(event.currentTarget.getAttribute('data-tag-index'));
+  const handleTagDelete = index => event => {
     const newValue = [...value];
     newValue.splice(index, 1);
     handleValue(event, newValue);
@@ -672,8 +671,9 @@ export default function useAutocomplete(props) {
         event.preventDefault();
       },
     }),
-    getTagProps: () => ({
-      onDelete: handleTagDelete,
+    getTagProps: ({ index }) => ({
+      'data-tag-index': index,
+      onDelete: handleTagDelete(index),
     }),
     getPopupProps: () => ({
       role: 'presentation',
```

I might prefer 2. 🤔
2 looks better to me as well.
@joshwooding Alright, then let's go for it. I like the mental model of it, it makes the `getTagProps()` similar to the `getOptionProps()`.

Solution patch:
diff --git a/docs/pages/api/autocomplete.md b/docs/pages/api/autocomplete.md
--- a/docs/pages/api/autocomplete.md
+++ b/docs/pages/api/autocomplete.md
@@ -60,7 +60,7 @@ You can learn more about the difference by [reading this guide](/guides/minimizi
 | <span class="prop-name">renderGroup</span> | <span class="prop-type">func</span> |  | Render the group.<br><br>**Signature:**<br>`function(option: any) => ReactNode`<br>*option:* The group to render. |
 | <span class="prop-name required">renderInput&nbsp;*</span> | <span class="prop-type">func</span> |  | Render the input.<br><br>**Signature:**<br>`function(params: object) => ReactNode`<br>*params:* null |
 | <span class="prop-name">renderOption</span> | <span class="prop-type">func</span> |  | Render the option, use `getOptionLabel` by default.<br><br>**Signature:**<br>`function(option: any, state: object) => ReactNode`<br>*option:* The option to render.<br>*state:* The state of the component. |
-| <span class="prop-name">renderTags</span> | <span class="prop-type">func</span> |  | Render the selected value.<br><br>**Signature:**<br>`function(value: any) => ReactNode`<br>*value:* The `value` provided to the component. |
+| <span class="prop-name">renderTags</span> | <span class="prop-type">func</span> |  | Render the selected value.<br><br>**Signature:**<br>`function(value: any, getTagProps: function) => ReactNode`<br>*value:* The `value` provided to the component.<br>*getTagProps:* A tag props getter. |
 | <span class="prop-name">value</span> | <span class="prop-type">any</span> |  | The value of the autocomplete. |
 
 The `ref` is forwarded to the root element.
diff --git a/docs/src/pages/components/autocomplete/CustomizedHook.js b/docs/src/pages/components/autocomplete/CustomizedHook.js
--- a/docs/src/pages/components/autocomplete/CustomizedHook.js
+++ b/docs/src/pages/components/autocomplete/CustomizedHook.js
@@ -146,13 +146,7 @@ export default function CustomizedHook() {
         <Label {...getInputLabelProps()}>Customized hook</Label>
         <InputWrapper ref={setAnchorEl} className={focused ? 'focused' : ''}>
           {value.map((option, index) => (
-            <Tag
-              key={index}
-              data-tag-index={index}
-              tabIndex={-1}
-              label={option.title}
-              {...getTagProps()}
-            />
+            <Tag label={option.title} {...getTagProps({ index })} />
           ))}
 
           <input {...getInputProps()} />
diff --git a/docs/src/pages/components/autocomplete/CustomizedHook.tsx b/docs/src/pages/components/autocomplete/CustomizedHook.tsx
--- a/docs/src/pages/components/autocomplete/CustomizedHook.tsx
+++ b/docs/src/pages/components/autocomplete/CustomizedHook.tsx
@@ -146,13 +146,7 @@ export default function CustomizedHook() {
         <Label {...getInputLabelProps()}>Customized hook</Label>
         <InputWrapper ref={setAnchorEl} className={focused ? 'focused' : ''}>
           {value.map((option: FilmOptionType, index: number) => (
-            <Tag
-              key={index}
-              data-tag-index={index}
-              tabIndex={-1}
-              label={option.title}
-              {...getTagProps()}
-            />
+            <Tag label={option.title} {...getTagProps({ index })} />
           ))}
           <input {...getInputProps()} />
         </InputWrapper>
diff --git a/docs/src/pages/components/autocomplete/FixedTags.js b/docs/src/pages/components/autocomplete/FixedTags.js
--- a/docs/src/pages/components/autocomplete/FixedTags.js
+++ b/docs/src/pages/components/autocomplete/FixedTags.js
@@ -11,17 +11,9 @@ export default function FixedTags() {
       options={top100Films}
       getOptionLabel={option => option.title}
       defaultValue={[top100Films[6], top100Films[13]]}
-      renderTags={(value, { className, onDelete }) =>
+      renderTags={(value, getTagProps) =>
         value.map((option, index) => (
-          <Chip
-            key={index}
-            disabled={index === 0}
-            data-tag-index={index}
-            tabIndex={-1}
-            label={option.title}
-            className={className}
-            onDelete={onDelete}
-          />
+          <Chip disabled={index === 0} label={option.title} {...getTagProps({ index })} />
         ))
       }
       style={{ width: 500 }}
diff --git a/docs/src/pages/components/autocomplete/FixedTags.tsx b/docs/src/pages/components/autocomplete/FixedTags.tsx
--- a/docs/src/pages/components/autocomplete/FixedTags.tsx
+++ b/docs/src/pages/components/autocomplete/FixedTags.tsx
@@ -11,17 +11,9 @@ export default function FixedTags() {
       options={top100Films}
       getOptionLabel={(option: FilmOptionType) => option.title}
       defaultValue={[top100Films[6], top100Films[13]]}
-      renderTags={(value: FilmOptionType[], { className, onDelete }) =>
+      renderTags={(value: FilmOptionType[], getTagProps) =>
         value.map((option: FilmOptionType, index: number) => (
-          <Chip
-            key={index}
-            disabled={index === 0}
-            data-tag-index={index}
-            tabIndex={-1}
-            label={option.title}
-            className={className}
-            onDelete={onDelete}
-          />
+          <Chip disabled={index === 0} label={option.title} {...getTagProps({ index })} />
         ))
       }
       style={{ width: 500 }}
diff --git a/docs/src/pages/components/autocomplete/Tags.js b/docs/src/pages/components/autocomplete/Tags.js
--- a/docs/src/pages/components/autocomplete/Tags.js
+++ b/docs/src/pages/components/autocomplete/Tags.js
@@ -45,17 +45,9 @@ export default function Tags() {
         options={top100Films.map(option => option.title)}
         defaultValue={[top100Films[13].title]}
         freeSolo
-        renderTags={(value, { className, onDelete }) =>
+        renderTags={(value, getTagProps) =>
           value.map((option, index) => (
-            <Chip
-              key={index}
-              variant="outlined"
-              data-tag-index={index}
-              tabIndex={-1}
-              label={option}
-              className={className}
-              onDelete={onDelete}
-            />
+            <Chip variant="outlined" label={option} {...getTagProps({ index })} />
           ))
         }
         renderInput={params => (
diff --git a/docs/src/pages/components/autocomplete/Tags.tsx b/docs/src/pages/components/autocomplete/Tags.tsx
--- a/docs/src/pages/components/autocomplete/Tags.tsx
+++ b/docs/src/pages/components/autocomplete/Tags.tsx
@@ -45,17 +45,9 @@ export default function Tags() {
         options={top100Films.map(option => option.title)}
         defaultValue={[top100Films[13].title]}
         freeSolo
-        renderTags={(value: string[], { className, onDelete }) =>
+        renderTags={(value: string[], getTagProps) =>
           value.map((option: string, index: number) => (
-            <Chip
-              key={index}
-              variant="outlined"
-              data-tag-index={index}
-              tabIndex={-1}
-              label={option}
-              className={className}
-              onDelete={onDelete}
-            />
+            <Chip variant="outlined" label={option} {...getTagProps({ index })} />
           ))
         }
         renderInput={params => (
diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.d.ts b/packages/material-ui-lab/src/Autocomplete/Autocomplete.d.ts
--- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.d.ts
+++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.d.ts
@@ -15,10 +15,7 @@ export interface RenderOptionState {
   selected: boolean;
 }
 
-export interface RenderValueState {
-  className: string;
-  onDelete: () => {};
-}
+export type GetTagProps = ({ index }: { index: number }) => {};
 
 export interface RenderGroupParams {
   key: string;
@@ -103,9 +100,10 @@ export interface AutocompleteProps
    * Render the selected value.
    *
    * @param {any} value The `value` provided to the component.
+   * @param {function} getTagProps A tag props getter.
    * @returns {ReactNode}
    */
-  renderTags?: (value: any, state: RenderValueState) => React.ReactNode;
+  renderTags?: (value: any, getTagProps: GetTagProps) => React.ReactNode;
 }
 
 export type AutocompleteClassKey =
diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
--- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
+++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.js
@@ -232,22 +232,16 @@ const Autocomplete = React.forwardRef(function Autocomplete(props, ref) {
   let startAdornment;
 
   if (multiple && value.length > 0) {
-    const tagProps = {
-      ...getTagProps(),
+    const getCustomizedTagProps = params => ({
       className: classes.tag,
-    };
+      ...getTagProps(params),
+    });
 
     if (renderTags) {
-      startAdornment = renderTags(value, tagProps);
+      startAdornment = renderTags(value, getCustomizedTagProps);
     } else {
       startAdornment = value.map((option, index) => (
-        <Chip
-          key={index}
-          data-tag-index={index}
-          tabIndex={-1}
-          label={getOptionLabel(option)}
-          {...tagProps}
-        />
+        <Chip label={getOptionLabel(option)} {...getCustomizedTagProps({ index })} />
       ));
     }
   }
@@ -570,6 +564,7 @@ Autocomplete.propTypes = {
    * Render the selected value.
    *
    * @param {any} value The `value` provided to the component.
+   * @param {function} getTagProps A tag props getter.
    * @returns {ReactNode}
    */
   renderTags: PropTypes.func,
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.d.ts
@@ -158,7 +158,7 @@ export default function useAutocomplete(
   getInputLabelProps: () => {};
   getClearProps: () => {};
   getPopupIndicatorProps: () => {};
-  getTagProps: () => {};
+  getTagProps: ({ index }: { index: number }) => {};
   getListboxProps: () => {};
   getOptionProps: ({ option, index }: { option: any; index: number }) => {};
   id: string;
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
@@ -607,8 +607,7 @@ export default function useAutocomplete(props) {
     selectNewValue(event, filteredOptions[highlightedIndexRef.current]);
   };
 
-  const handleTagDelete = event => {
-    const index = Number(event.currentTarget.getAttribute('data-tag-index'));
+  const handleTagDelete = index => event => {
     const newValue = [...value];
     newValue.splice(index, 1);
     handleValue(event, newValue);
@@ -699,8 +698,11 @@ export default function useAutocomplete(props) {
         event.preventDefault();
       },
     }),
-    getTagProps: () => ({
-      onDelete: handleTagDelete,
+    getTagProps: ({ index }) => ({
+      key: index,
+      'data-tag-index': index,
+      tabIndex: -1,
+      onDelete: handleTagDelete(index),
     }),
     getListboxProps: () => ({
       role: 'listbox',
@@ -716,9 +718,9 @@ export default function useAutocomplete(props) {
       const disabled = getOptionDisabled ? getOptionDisabled(option) : false;
 
       return {
+        key: index,
         tabIndex: -1,
         role: 'option',
-        key: index,
         id: `${id}-option-${index}`,
         onMouseOver: handleOptionMouseOver,
         onClick: handleOptionClick,


Test patch:
diff --git a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
--- a/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
+++ b/packages/material-ui-lab/src/Autocomplete/Autocomplete.test.js
@@ -51,6 +51,23 @@ describe('<Autocomplete />', () => {
       document.activeElement.blur();
       input.focus();
     });
+
+    it('should remove the last option', () => {
+      const handleChange = spy();
+      const options = ['one', 'two'];
+      const { container } = render(
+        <Autocomplete
+          defaultValue={options}
+          options={options}
+          onChange={handleChange}
+          renderInput={params => <TextField {...params} />}
+          multiple
+        />,
+      );
+      fireEvent.click(container.querySelectorAll('svg[data-mui-test="CancelIcon"]')[1]);
+      expect(handleChange.callCount).to.equal(1);
+      expect(handleChange.args[0][1]).to.deep.equal([options[0]]);
+    });
   });
 
   describe('WAI-ARIA conforming markup', () => {