Focus and Tabbing

Relevant components

  1. Focusable
  2. FocusManager and FocusGroup
  3. FocusLock

Changing Focus Context

In most cases, focus will follow the tabbing order. However, sometimes it is necessary to change the context of the user's task. In doing so, focus is placed and constrained on the new task. The changing of the user's context and focus is considered navigating to/between focus groups.

Drawer Focus Context

The contents of a drawer should be contained withing a focus group.

  1. When a user opens a drawer, then the focus should always be moved to the first focusable item in the drawer.
  2. Only fields in the drawer can be focused.
  3. Upon closing the drawer, the user's focus should return to the element that triggered the drawer to open.

Opening a drawer should always move the focus to something within the drawer.

{() => {
    class StateManager extends Component {
      constructor() {
        super()
        this.state = {
          open: false,
        }
        this.open = this.open.bind(this)
        this.handleClose = this.handleClose.bind(this)
      }
      open() {
        this.setState({
          open: true,
        })
      }
      handleClose() {
        this.setState(
          () => ({
            open: false,
          }),
          this.pop,
        )
      }
      render() {
        return (
          <FocusManager>
            {pop => {
              this.pop = pop
              return (
                <Fragment>
                  <FocusGroup disableLock={true}>
                    {bind => (
                      <Button {...bind} onClick={this.open}>
                        Open Drawer
                      
                    )}
                  </FocusGroup>
                  <Drawer
                    {...this.state}
                    handleClickOutside={this.handleClose}
                    placement="right"
                  >
                    <FocusGroup>
                      {bind => (
                        <SpacedGroup direction="vertical">
                          <TextField {...bind} focused={this.state.open} />
                          <TextField {...bind} />
                          <TextField {...bind} />
                          <TextField {...bind} />
                          <Button {...bind} onClick={this.handleClose}>
                            Close
                          </Button>
                        </SpacedGroup>
                      )}
                    </FocusGroup>
                  </Drawer>
                </Fragment>
              )
            }}
          </FocusManager>
        )
      }
    }
    return <StateManager />
  }}
</Playground>

Menu Focus Context

Menu does not moves focus between the element that opened the menu and the list items within the menu. Therefore, tabbing does not cycle the user through the menu list items.

My Shared View
Project A

Backlog Shared View
Project A

Backlog Shared View
Project B

Holiday Party
Project A

Roadmaping Latest Quarter
Project A

Team Q Backlog
Project A
{() => {
    class StateManager extends Component {
      constructor() {
        super()
        this.state = {
          isOpenIndex: -1,
          items: [
            { name: 'My Shared View', project: 'Project A' },
            { name: 'Backlog Shared View', project: 'Project A' },
            { name: 'Backlog Shared View', project: 'Project B' },
            { name: 'Holiday Party', project: 'Project A' },
            {
              name: 'Roadmaping Latest Quarter',
              project: 'Project A',
            },
            { name: 'Team Q Backlog', project: 'Project A' },
          ],
        }
        this.open = this.open.bind(this)
        this.close = this.close.bind(this)
      }
      open(index) {
        return () => this.setState({ isOpenIndex: index })
      }
      close() {
        this.setState({ isOpenIndex: -1 })
      }
      render() {
        const getMenuForItemAt = index => {
          const isOpen = this.state.isOpenIndex === index
          return (
            <Menu
              anchor={
                <IconButton
                  title="more-actions"
                  icon={AlertIcon}
                  onClick={this.open(index)}
                />
              }
              open={isOpen}
              placement="bottom-end"
              onClickOutside={this.close}
            >
              <List bordered>
                <ListItem onClick={() => {}}>
                  <ListItemText primary="Action 1" />
                
                <ListItem onClick={() => {}}>
                  <ListItemText primary="Action 2" />
                </ListItem>
              </List>
            </Menu>
          )
        }
        const ListItems = this.state.items.map((item, index) => (
          <ListItem key={index} secondaryAction={getMenuForItemAt(index)}>
            <ListItemText primary={item.name} secondary={item.project} />
          </ListItem>
        ))
        return (
          <SpacedGroup xs={24}>
            <Paper>
              <List bordered>{ListItems}</List>
            </Paper>
          </SpacedGroup>
        )
      }
    }
    return <StateManager />
  }}
</Playground>

Drawer Opened From Menu

In some cases, a drawer may open in response to a menu item's primary action; in which case opening the drawer will close the menu and move the user's focus to the drawer. Ther user's focus is locked to the contents of the drawer and should return to the element which opened the menu upon closing the drawer.

My Shared View
Project A

Backlog Shared View
Project A

Backlog Shared View
Project B

Holiday Party
Project A

Roadmaping Latest Quarter
Project A

Team Q Backlog
Project A
{() => {
    class StateManager extends Component {
      constructor() {
        super()
        this.state = {
          isOpenIndex: -1,
          items: [
            { name: 'My Shared View', project: 'Project A' },
            { name: 'Backlog Shared View', project: 'Project A' },
            { name: 'Backlog Shared View', project: 'Project B' },
            { name: 'Holiday Party', project: 'Project A' },
            {
              name: 'Roadmaping Latest Quarter',
              project: 'Project A',
            },
            { name: 'Team Q Backlog', project: 'Project A' },
          ],
          open: false,
        }
        this.open = this.open.bind(this)
        this.close = this.close.bind(this)
        this.openDrawer = this.openDrawer.bind(this)
        this.closeDrawer = this.closeDrawer.bind(this)
      }
      open(index) {
        return () => this.setState({ isOpenIndex: index })
      }
      close() {
        this.setState({ isOpenIndex: -1 })
      }
      openDrawer() {
        this.setState({ open: true, isOpenIndex: -1 })
      }
      closeDrawer() {
        this.setState({ open: false }, () => {
          this.pop()
        })
      }
      render() {
        const getMenuForItemAt = index => {
          const isOpen = this.state.isOpenIndex === index
          return (
            <Menu
              anchor={
                <IconButton
                  title="more-actions"
                  icon={AlertIcon}
                  onClick={this.open(index)}
                />
              }
              open={isOpen}
              placement="bottom-end"
              onClickOutside={this.close}
            >
              <List bordered>
                <ListItem onClick={this.openDrawer} focused={isOpen}>
                  <ListItemText primary="Action 1" />
                
                <ListItem onClick={this.openDrawer}>
                  <ListItemText primary="Action 2" />
                </ListItem>
              </List>
            </Menu>
          )
        }
        const ListItems = this.state.items.map((item, index) => (
          <ListItem key={index} secondaryAction={getMenuForItemAt(index)}>
            <ListItemText primary={item.name} secondary={item.project} />
          </ListItem>
        ))
        return (
          <SpacedGroup xs={24}>
            <Paper>
              <FocusManager>
                {pop => {
                  this.pop = pop
                  return (
                    <Fragment>
                      <List bordered>{ListItems}</List>
                      <Drawer
                        open={this.state.open}
                        handleClickOutside={this.closeDrawer}
                        placement="right"
                      >
                        <FocusGroup>
                          {bind => (
                            <SpacedGroup direction="vertical">
                              <TextField {...bind} focused={this.state.open} />
                              <TextField {...bind} />
                              <TextField {...bind} />
                              <TextField {...bind} />
                              <Button {...bind} onClick={this.closeDrawer}>
                                Close
                              </Button>
                            </SpacedGroup>
                          )}
                        </FocusGroup>
                      </Drawer>
                    </Fragment>
                  )
                }}
              </FocusManager>
            </Paper>
          </SpacedGroup>
        )
      }
    }
    return <StateManager />
  }}
</Playground>