Search code examples
javascriptangulartypescriptkeycode

How can I set an automatic focus in the input for a keyCode navigation?


I have written a table and in it all contain input fields. For this template I have developed a keyCode navigation. A navigation up, down, left and right is possible. What do I have to add to my code so that the cursor is focused directly in the when navigating?

My Code:

// Snippet from HTML
...
<tbody formArrayName="rows">
  <tr *ngFor="let rowControl of rows.controls; let rowIndex = index">
   <td [formGroupName]="rowIndex" appArrowKeyNav *ngFor="let column of displayedColumns; let columnIndex = index;">
   <div>
        <span>
               <label>
                   <input [formControl]="rowControl.get(column.attribute)">
                </label>
         </span>
     </div>
   </td>
 </tr>
</tbody>
// TS
public move(object: any) {
    const inputToArray = this.inputs.toArray();
    const cols = this.displayedColumns.length;
    let index = inputToArray.findIndex((x) => x.element === object.element);
    // console.log('Index found:', index);
    switch (object.action) {
      case 'UP':
        index -= cols;
        break;
      case 'DOWN':
        index += cols;
        break;
      case 'LEFT':
        index -= 1;
        break;
      case 'RIGHT':
        index += 1;
        break;
    }
    if (index >= 0 && index < this.inputs.length) {
      console.log('Navigating to index:', index);
      inputToArray[index].element.nativeElement.focus();
    }
  }
// Directive
  @HostListener('keydown', ['$event']) onKeyUp(event: KeyboardEvent) {
    switch (event.key) {
      case 'ArrowUp':
        this.keyboardService.sendMessage({ element: this.element, action: 'UP' });
        break;
      case 'ArrowLeft':
        this.keyboardService.sendMessage({ element: this.element, action: 'LEFT' });
        break;
      case 'ArrowDown':
        this.keyboardService.sendMessage({ element: this.element, action: 'DOWN' });
        break;
      case 'ArrowRight':
        this.keyboardService.sendMessage({ element: this.element, action: 'RIGHT' });
        break;
      case 'Enter':
        this.keyboardService.sendMessage({ element: this.element, action: 'ENTER' });
        break;
    }
}

Here is myStackblitz: https://stackblitz.com/edit/angular-wmfjhh-zfkyyx?file=app%2Ftable-basic-example.html


Solution

  • One possible way to have a table with cells that can be navigated with Arrow keys

    use the id attribute to store row and col information using the *ngFor index

        <tr *ngFor="let rowControl of rows.controls; let i = index">
    
          <ng-container [formGroupName]="i">
    
            <td>
              <input [id]="'row-' + i + '-col-0'" formControlName="name" (focus)="onFocus($event)">
            </td>
            <td>
              <input [id]="'row-' + i + '-col-1'" formControlName="age" (focus)="onFocus($event)">
            </td>
            <td>
              <input [id]="'row-' + i + '-col-2'" formControlName="color" (focus)="onFocus($event)">
            </td>
    
          </ng-container>
    
        </tr>
    

    in this case id for first row will be row-0-col-0, row-0-col-1 etc. 2nd row row-1-col-0

    also there is a onFocus event handler which will set the current cell in focus

    once keyUp is triggered

     @HostListener('keydown', ['$event'])
      onKeyUp(event: KeyboardEvent) {
    
        console.log(event.key);
    
        if (this.currentInputInFocus) {
          const id = this.currentInputInFocus.id;
    
          if (id && id.includes('row-')) {
            const arr = id.split('-');
            let row: number = Number(arr[1]);
            let col: number = Number(arr[3]);
    
            switch (event.key) {
              case 'ArrowUp':
                --row;
                break;
    
              case 'ArrowLeft':
                --col;
                break;
    
              case 'ArrowDown':
                ++row;
                break;
    
              case 'ArrowRight':
                ++col;
                break;
    
              case 'Enter':
                // do nothing
                break;
            }
            this.setFocus(row, col);
          }
        }
      }
    

    get the currently focused element id, eg. 'row-0-col-0' and make changes to row or col values depending on a keypress then try to focus the new element

      private setFocus(row: number, col: number) {
        const newElementToFocusOn = document.getElementById(`row-${row}-col-${col}`);
        if (newElementToFocusOn) {
          this.currentInputInFocus = newElementToFocusOn;
          this.currentInputInFocus.focus();
        }
      }
    

    in ngAfterViewInit initially focused cell can be set with:

      ngAfterViewInit() {
        const row = 1;
        const col = 0;
        this.setFocus(row, col);
      }
    

    Working demo

    UPDATE

    to use your Stackblitz as context very few mods to make it work:

    • set the proper id attributes [id]="'row-' + rowIndex + '-col-' + columnIndex"
    • keep track of currently focused input: currentInputInFocus!: HTMLElement;
    • rest is the same as in original demo

    Forked and updated your Stackblitz

    UPDATE 2nd

    The HTMLElement.focus() method sets focus on the specified element, if it can be focused.

    Docs

    One workaround is to add the tabindex="0" attribute according to this answer

    Back to demo. As @Aviad P. mentioned the directive on ng-template is not going to work. Instead:

      <ng-template #otherColumns>
        <div tabindex="0" [id]="'row-' + rowIndex + '-col-' + columnIndex" arrow-div>
          Here is a Number
        </div>
      </ng-template>
    

    Demo

    UPDATE 3rd

    to continue navigating you can try this on case 'RIGTH':

    ++col; // try to move to next column
    
    if (col >= this.columns.length) { // check if moved past last column
      col = 0; // go to column at zero index  (1st column)
      ++row; // go to next row
    }
    

    Demo