30

I am attempting to create a search match list that updates as the user types in their query. However, I can't figure out how to maintain focus on the input element. The pop-up always gets focussed. I have tried programmatically setting the focus using refs but I cannot give a stateless function component (I'm assuming this is my TextField input) a ref.

Here is a gif of the behavior. https://i.stack.imgur.com/aWmRL.jpg

Notice how the popup steals focus and prevents the user from typing further.

<TextField
              id='contact'
              label='Contact Name'
              className={classes.textField}
              margin='normal'
              ref={this.nameInput}
              onChange={this.handleContactSearch.bind(this)}
              value={this.state.contactSearch}
            />
            <Popover
              open={Boolean(anchorEl)}
              anchorEl={anchorEl}
              onClick={this.handlePopoverClose}
              anchorOrigin={{
                vertical: 'bottom',
                horizontal: 'center'
              }}
              transformOrigin={{
                vertical: 'top',
                horizontal: 'center'
              }}
              autoFocus={false}
            >
              <List>{this.createContactList()}</List>
            </Popover>

These are the related functions:

  handleContactSearch(event) {
    this.handlePopoverClick(event);
    this.setState({ contactSearch: handleText(event) });
    this.props.filterContacts(
      event.target.value,
      this.props.accountInfo.AccountName
    );
  }
  handlePopoverClick = event => {
    this.setState({
      anchorEl: event.currentTarget
    });
  };

  handlePopoverClose = () => {
    this.setState({
      anchorEl: null
    });
  };

How can I make the TextField element maintain focus so a user can type their query without interruption?

Sandbox: https://codesandbox.io/s/mjqoj9lxkj

Chris Voss
  • 708
  • 1
  • 9
  • 22
  • 1
    Could you provide a sandbox? I ran an somewhat oversimplified version and the input is not losing focus: https://codesandbox.io/s/mmx68yjlm9. – fsl Jul 17 '18 at 19:53
  • Sure. Here it is. https://codesandbox.io/s/mjqoj9lxkj The only difference between our two samples seems to be that mine is within a modal? Click the '+' button and then type in the 'contact name' input field. – Chris Voss Jul 17 '18 at 21:24

5 Answers5

45

Pass 'disableAutoFocus', 'disableEnforceFocus' props to your popover. It worked for me!

<Popover
 open={Boolean(anchorEl)}

 // pass these props to the popover component
 disableAutoFocus={true}
 disableEnforceFocus={true}
 >

https://material-ui.com/api/modal/

nymphadora
  • 839
  • 11
  • 18
5

The reason why this is happening is that you are calling this.showPopover(event) every time the onChange={this.handleContactSearch.bind(this)} event is fired in your <TextField>.

In order to fix this, you'll need to find a way to call this.showPopover(event) only once.

I was able to make it work using a combination of autoFocus={true} and the onFocus={this.showPopover} event on the <TextField/>. The only issue with this is that the popover will show up empty when you first open the modal. I used a ref on the textfield and a conditional to set the opacity of the popover so it only shows once there's a value in the textfield.

Maybe not the ultimate solution, but it works and should at least send you in the right direction.

<div className={classes.paper}>
    <TextField
        id="contact123"
        label="Contact Name"
        className={classes.textField}
        margin="normal"
        onChange={this.handleContactSearch.bind(this)}
        value={this.state.contactSearch}
        autoFocus={true}
        onFocus={this.showPopover}
        inputRef={input => (this.tf = input)}
    />
    <Popover
        open={Boolean(anchorEl)}
        anchorEl={document.getElementById("contact123")}
        onClick={this.closePopover}
        anchorOrigin={{
            vertical: "bottom",
            horizontal: "center"
        }}
        transformOrigin={{
            vertical: "top",
            horizontal: "center"
        }}
        style={{ opacity: this.tf && !this.tf.value.length ? 0 : 1 }}
    >
        <List>{this.state.contactSearch}</List>
    </Popover>
    <div>
        <Button color="primary" className={classes.saveButton}>
            Save
        </Button>
    </div>
</div>

Sandbox: Working Demo

basseur
  • 153
  • 1
  • 1
  • 11
CaseyC
  • 1,453
  • 14
  • 23
  • Don’t bind the function directly in render because it will create a new function every time your component renders and it’s not recommended. You should bind it in constructor only – Hemadri Dasari May 20 '20 at 15:55
3

An alternative to this approach is to use Popper, ClickAwayListener and Backdrop components. Using Popper allows you to preserve focus on the input field and keep typing. The solution would look roughly like:

class Foo extends React.Component {
  inputRef = React.createRef(),

  render() {
    const { open, searchValue } = this.state

    <RootRef rootRef={this.inputRef}>
      <div className={classes.container}>
        // You may be able to use TextField as well
        <FormControl
          onKeyDown={//set open = false}
          onClick={// set open = true (e.g. only when searchValue !== '' }
        >
          <InputBase
            value={searchValue}
            onChange={this.handleSearchValueChange}
            inputRef={this.inputRef}
          />
        </FormControl>
        <Popper anchorEl={this.inputRef.current} open={open} >
          <ClickAwayListener onClick={//set open = false} onClickAway={//set open = false}>
            Popover content
          </ClickAwayListener>
        </Popper>
      </div>
    </RootRef>
  }
}

Not a working example, but shows how to solve the problem of being able to type in an input while having popover/popper opened.

Maciej Gurban
  • 5,615
  • 4
  • 40
  • 55
0

You can add onKeyDown property to the popover that will close it every time the user will type, and onKeyUp property to the search input which pops up the popover again. not the best solution but it's worked for me.

<InputBase
              autoFocus={true}
              value={searchText}
              onChange={(e) => handleSearch(e)}
              onKeyUp={e => setAnchorEl(e.currentTarget)}
              placeholder="Search…"
              classes={{
                root: classes.inputRoot,
                input: classes.inputInput,
              }}
              inputProps={{ "aria-label": "search" }}
            />
            {searchResult && (
              <Popover
              disableAutoFocus 
              className={classes.pop}
              onKeyDown={handleClose}
                id={id}
                open={open}
                anchorEl={anchorEl}
                onClose={handleClose}
                anchorOrigin={{
                  vertical: "bottom",
                  horizontal: "center",
                }}
                transformOrigin={{
                  vertical: "top",
                  horizontal: "center",
                }}
              >
                {searchResult.map((song, i) => {
                  return (
                      <Link to={`/Songs/${song.unique_id}?Artist=${song.artist_id}`}>
                    <ListItem>
                    <IconButton variant="h6" >
                      <PlayCircleFilledRounded/>
                    </IconButton>
                      {song.title}
                      </ListItem>
                      </Link>
                  );
                })}
              </Popover>
            )}
          </div>
0

I had the same problem with the MUI <Menu> component. In that case, the prop is called autoFocus.

<Menu autoFocus={false}>
  ...
</Menu>

https://mui.com/material-ui/api/menu/

Michael Lynch
  • 2,682
  • 3
  • 31
  • 59