Repository: mui/material-ui (TypeScript)
Date: 2020-06-15 22:47:48+00:00
Task type: Feature

Issue:
add a disableTags prop for Autocomplete 
I'm not sure that is this a feature request or a bug fix 
in [this example](https://material-ui.com/components/autocomplete/#githubs-picker) of Autocomplete component you used `renderTags={() => null}` to disable showing tags but when we do this tags will not be disabled completely and [the `handleKeyDown` listener](https://github.com/mui-org/material-ui/blob/75c06bcda7fdc6bd316d3d8c196c0d1ce94905cc/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js#L458)  will be attached to component which cause removing selected elements using backspace in search box which is not which we need in this example 

I think adding a `disableTag={true}` prop to AutoComplete component would be a good solution


Issue discussion:
@MHA15 Thanks for raising, this is concerning:

1. Navigate to http://0.0.0.0:3000/components/autocomplete#githubs-picker
2. Open the picker
3. Press <kbd>ArrowLeft</kbd>
4. 💥 `TypeError: Cannot read property 'focus' of null`

I think that the rendering logic of `useAutocomplete` should be more resilient, it should support missing tags. While I appreciate your proposal around `disableTag`, if feels that it's something that is specific to the Autocomplete component. If we can get the same outcome with `renderTags={() => null}`, I think that we should rather go in this direction, it's more explicit.

What do you think of this fix, considering a missing item as disabled? Do you want to work on a pull request? We would also need a regression test :).

```diff
diff --git a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
index 3a7001d2d..661a812d8 100644
--- a/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
+++ b/packages/material-ui-lab/src/useAutocomplete/useAutocomplete.js
@@ -265,7 +265,7 @@ export default function useAutocomplete(props) {
       // Same logic as MenuList.js
       const nextFocusDisabled = disabledItemsFocusable
         ? false
-        : option && (option.disabled || option.getAttribute('aria-disabled') === 'true');
+        : !option || option.disabled || option.getAttribute('aria-disabled') === 'true';

       if ((option && !option.hasAttribute('tabindex')) || nextFocusDisabled) {
         // Move to the next element.
@@ -568,10 +568,10 @@ export default function useAutocomplete(props) {

       // Same logic as MenuList.js
       if (
-        option &&
-        (!option.hasAttribute('tabindex') ||
-          option.disabled ||
-          option.getAttribute('aria-disabled') === 'true')
+        !option ||
+        !option.hasAttribute('tabindex') ||
+        option.disabled ||
+        option.getAttribute('aria-disabled') === 'true'
       ) {
         nextFocus += direction === 'next' ? 1 : -1;
       } else {
```
will this code fix this bug? I think that it could be a good fix and should not make new bugs
@MHA15 Oh right, I have overlooked the issue with <kbd>Backspace</kbd>, thanks. I think that we should also update the demo:

```diff
diff --git a/docs/src/pages/components/autocomplete/GitHubLabel.tsx b/docs/src/pages/components/autocomplete/GitHubLabel.tsx
index bd4eec984..6b4ae11e5 100644
--- a/docs/src/pages/components/autocomplete/GitHubLabel.tsx
+++ b/docs/src/pages/components/autocomplete/GitHubLabel.tsx
@@ -187,7 +187,15 @@ export default function GitHubLabel() {
             popperDisablePortal: classes.popperDisablePortal,
           }}
           value={pendingValue}
-          onChange={(event, newValue) => {
+          onChange={(event, newValue, reason) => {
+            if (
+              event.type === 'keydown' &&
+              (event as React.KeyboardEvent).key === 'Backspace' &&
+              reason === 'remove-option'
+            ) {
+              return;
+            }
+
             setPendingValue(newValue);
           }}
           disableCloseOnSelect
```

(cc @dmtrKovalenko I think that it's a good case in favor of always exposing the `event` in the handle props.)
> I think that we should also update the demo:

I think that it's not a good idea to force all users to handle `onChange` like this when using null tags 
it should be handled internally to not remove item and not call `onChange` callback 

edit: and also what about null.focus error when using arrow keys?
This would depend on the frequency of the need. I think that until we know how common it's, the best option is to use the exisiting API rather than adding a new one. 
and even it's possible to render some filtered tags and also we shouldn't remove the last one with backspace
maybe we should pass `renderTag` instead of `renderTags` to `useAutocomplete` itself to render tags and remove last showing tag or nothing if there is no tag rendered 
or maybe this logic (backspace listener) should not be handled by useAutocomplete and Autocomplete component could implement it and check renderTags returned value to not be null
Is anyone working on it? As a newbie to open source, I wouldn't mind submitting a pr to this issue.
@matthenschke You are clear to go :)
@oliviertassinari sounds good! Are there are dependencies I should go over before diving into the code for autocomplete?

Solution patch:
diff --git a/docs/src/pages/components/autocomplete/GitHubLabel.js b/docs/src/pages/components/autocomplete/GitHubLabel.js
--- a/docs/src/pages/components/autocomplete/GitHubLabel.js
+++ b/docs/src/pages/components/autocomplete/GitHubLabel.js
@@ -185,7 +185,14 @@ export default function GitHubLabel() {
             popperDisablePortal: classes.popperDisablePortal,
           }}
           value={pendingValue}
-          onChange={(event, newValue) => {
+          onChange={(event, newValue, reason) => {
+            if (
+              event.type === 'keydown' &&
+              event.key === 'Backspace' &&
+              reason === 'remove-option'
+            ) {
+              return;
+            }
             setPendingValue(newValue);
           }}
           disableCloseOnSelect
diff --git a/docs/src/pages/components/autocomplete/GitHubLabel.tsx b/docs/src/pages/components/autocomplete/GitHubLabel.tsx
--- a/docs/src/pages/components/autocomplete/GitHubLabel.tsx
+++ b/docs/src/pages/components/autocomplete/GitHubLabel.tsx
@@ -187,7 +187,14 @@ export default function GitHubLabel() {
             popperDisablePortal: classes.popperDisablePortal,
           }}
           value={pendingValue}
-          onChange={(event, newValue) => {
+          onChange={(event, newValue, reason) => {
+            if (
+              event.type === 'keydown' &&
+              (event as React.KeyboardEvent).key === 'Backspace' &&
+              reason === 'remove-option'
+            ) {
+              return;
+            }
             setPendingValue(newValue);
           }}
           disableCloseOnSelect
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
@@ -265,7 +265,7 @@ export default function useAutocomplete(props) {
       // Same logic as MenuList.js
       const nextFocusDisabled = disabledItemsFocusable
         ? false
-        : option && (option.disabled || option.getAttribute('aria-disabled') === 'true');
+        : !option || option.disabled || option.getAttribute('aria-disabled') === 'true';
 
       if ((option && !option.hasAttribute('tabindex')) || nextFocusDisabled) {
         // Move to the next element.
@@ -596,10 +596,10 @@ export default function useAutocomplete(props) {
 
       // Same logic as MenuList.js
       if (
-        option &&
-        (!option.hasAttribute('tabindex') ||
-          option.disabled ||
-          option.getAttribute('aria-disabled') === 'true')
+        !option ||
+        !option.hasAttribute('tabindex') ||
+        option.disabled ||
+        option.getAttribute('aria-disabled') === 'true'
       ) {
         nextFocus += direction === 'next' ? 1 : -1;
       } else {


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
@@ -9,6 +9,7 @@ import { act, createClientRender, fireEvent, screen } from 'test/utils/createCli
 import { createFilterOptions } from '../useAutocomplete/useAutocomplete';
 import Autocomplete from './Autocomplete';
 import TextField from '@material-ui/core/TextField';
+import Chip from '@material-ui/core/Chip';
 
 describe('<Autocomplete />', () => {
   const mount = createMount();
@@ -399,6 +400,36 @@ describe('<Autocomplete />', () => {
       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(
+        <Autocomplete
+          {...defaultProps}
+          defaultValue={options}
+          options={options}
+          value={options}
+          renderTags={(value, getTagProps) =>
+            value
+              .filter((x, index) => index === 1)
+              .map((option, index) => <Chip label={option.title} {...getTagProps({ index })} />)
+          }
+          onChange={handleChange}
+          renderInput={(params) => <TextField {...params} autoFocus />}
+          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('should trigger a form expectedly', () => {