Search code examples
javascripthtmlweb-componentlit-element

Tab-back with a custom component that wraps an input field


Where I have a custom component that wraps an input element, I'm trying to ensure that tabindex is working correctly.

The following code will work for tabbing forward. But tabbing backwards (Shift + Tab) does not work correctly, it'll focus the outer wrapper x-input instead.

@customElement('x-input')
export class CustomInput extends LitElement {
  @query("input")
  private _input: HTMLInputElement;
  
  constructor() {
    super();
    this.tabIndex = this.tabIndex < 0 ? 0 : this.tabIndex;
    this.addEventListener("focusin", this._onFocusIn);
  }
  
  render() {
    return html`<input tabindex="-1">`;
  }
  
  private _onFocusIn(): void {
   this._input.focus();
  }
}

A full playgound/example can be found here.


Solution

  • Just make use of delegateFocus property of the shadowRoot. It would work naturally. You don't need to setup complex focus event behavior:

    import {html, css, LitElement} from 'lit';
    import {customElement, query} from 'lit/decorators.js';
    
    @customElement('x-input')
    export class CustomInput extends LitElement {
      static shadowRootOptions = {
        ...LitElement.shadowRootOptions,
        delegatesFocus: true
      };
      
      @query("input")
      private _input: HTMLInputElement;
      
      constructor() {
        super();
        this.tabIndex = this.tabIndex < 0 ? 0 : this.tabIndex;
      }
      
      render() {
        // The input element is naturally tabbable
        return html`<input tabindex="0">`;
      }
    }
    

    The delegateFocus will do two things:

    • When the custom element is focused, the focus is given to the first focusable element inside the shadow root.
    • When the inside element gets focused, the :focus style gets applied to your host/custom element. Read more about the delegateFocus here. Also this article has a nice explainer about handling focus.