3

Context:

I have a component that loads a <table mat-table> with a given data source. I have added a (keydown) function to the <tr mat-row>.

The function allows me to shift the row highlighting up and down the rows as I press the up/down arrow keys.

Issues:

  1. In order for the (keydown) function to execute, I have to either click one of the table rows to set the focus, or use the Tab through multiple items to eventually focus on the table rows.

  2. The (keydown) function is only changing the highlighting of the rows, and not the :focus. So if I hit the down arrow, the next row down highlights, but the :focus outline remains on the previous row.

I've tried a few solutions, and it seems I cannot focus the <tr> tags without clicking / Tabbing. The autofocus directive is not supported for <tr> tags. Using .focus() errors out, claiming that .focus() is not a function, even if I cast it to the HTMLElement type.

Example:

let firstRow = this.dataSource.connect().value[0] as unknown as HTMLTableRowElement;
firstRow.focus();

ERROR TypeError: firstRow.focus is not a function

I have also tried applying tabIndex="0" to the mat-row, but it does not become the first tab-able element on the page.

The table is sort-able, so I am using dataSource.connect().value[0] to find the first row, rather than dataSource.data[0].

This is because dataSource.data[0] was giving the pre-sorted array, which was defeating the purpose of the up/down arrow functionality.

TS Component:

import { Component, OnInit, ViewChild } from '@angular/core';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { TableService } from '../table.service';
import { SelectionModel } from '@angular/cdk/collections';
import { TableGrid } from '../TableGrid';

@Component({
  selector: 'app-table',
  templateUrl: './table.component.html',
  styleUrls: ['./table.component.css']
})
export class TableComponent implements OnInit {

  @ViewChild(MatSort, { static: true }) sort: MatSort;
  dataSource: MatTableDataSource<TableGrid> = null;
  displayedColumns = [
    'locked',
    'includeRe',
    'includeMv'];

  selection = new SelectionModel<TableGrid>(false, []);
  selectedRowId = 0;
  selectedRowIndex = 0;

  constructor( private tableService: TableService) { }


  ngOnInit() {
    this.tableService.get();
    this.tableService.tableGrid
      .subscribe(result => {
        this.selectedRowId = 0;
        this.dataSource = new MatTableDataSource(result);
        this.dataSource.sort = this.sort;
        if (this.dataSource.data.length > 0) {
          this.onRowClicked(this.dataSource.data[0]);
        }
      });
  }

  onRowClicked(row) {
    this.selection.clear();
    this.selection.select(row);
    this.selectedRowId = row.id;
    this.selectedRowIndex = this.dataSource.connect().value.indexOf(row);
    this.tableService.setId(row.id, row.name, row.locked, row.isDeleteable, row.includeHistory);

  }

  tableKeydown(event: KeyboardEvent) {
    if (!this.selection.isEmpty()) {
      let newSelection;
      const currentIndex = this.selectedRowIndex;
      if (event.key === 'ArrowDown') {
        newSelection = this.dataSource.connect().value[currentIndex + 1];
      } else if (event.key === 'ArrowUp') {
        newSelection = this.dataSource.connect().value[currentIndex - 1];
      }
      if (newSelection) {
        this.selectedRowId = newSelection.id;
        this.onRowClicked(newSelection);
      }
    }
  }
}

HTML:

<div class="container">
  <div class="table-container">
    <table mat-table [dataSource]="dataSource" matSort aria-label="Elements">
      <ng-container matColumnDef="locked">
        <th mat-header-cell *matHeaderCellDef mat-sort-header>Locked</th>
        <td mat-cell *matCellDef="let row">
          <i *ngIf="row.locked === true" class="material-icons">lock</i>
        </td>
      </ng-container>
      <ng-container matColumnDef="includeRe">
        <th mat-header-cell *matHeaderCellDef mat-sort-header>RE</th>
        <td mat-cell *matCellDef="let row">
          <i *ngIf="row.includeRe === true" class="material-icons">house</i>
        </td>
      </ng-container>
      <ng-container matColumnDef="includeMv">
        <th mat-header-cell *matHeaderCellDef mat-sort-header>MV</th>
        <td mat-cell *matCellDef="let row">
          <i *ngIf="row.includeMv === true" class="material-icons">directions_car</i>
        </td>
      </ng-container>
      <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky:true"></tr>
      <tr mat-row tabIndex="0" *matRowDef="let row; columns: displayedColumns;"
          (click)="onRowClicked(row)"
          (keydown)="tableKeydown($event)"
          [ngClass] = "{'highlight' : row.id == selectedRowId}">
      </tr>
    </table>
  </div>
</div>

Desired Functionality:

I would like to set the :focus attribute to the first <tr> of the table on init, so that the (keydown) function is immediately usable.

I would also like to get the (keydown) function to set the :focus on the <tr> tags.

What I'm trying to avoid

I would like to avoid creating a custom focus class to reproduce the focus outline effect. I would also like to avoid disabling the focus-outline on table-rows. What I want, is to put the :focus pseudo-class on the <tr>. If this is impossible, I will consider alternatives.

I can add more info if needed, but I'm trying to keep it relatively anonymous as this is work-related.

I'm relatively new to Angular, but have developed for a few years in React. Any advice would be appreciated.

Thank you.

Pep
  • 371
  • 2
  • 8
  • 17
  • Have you tried adding like this , then set focus to the – Seth Winters Oct 11 '19 at 19:14
  • I have tried putting tabIndex="1" but I'm not sure what you mean by set the focus to the , outside of what I have already tried. Do you have an example? – Pep Oct 11 '19 at 19:18
  • you can try setting the focus like you did, but try it with the tabindex. – Seth Winters Oct 11 '19 at 20:09
  • unfortunately, that does not work, I still get this error: `ERROR TypeError: firstRow.focus is not a function` – Pep Oct 11 '19 at 20:10
  • Using jQuery, $('.input').keyup(function() { $(this).closest('tr').focus();}); – Seth Winters Oct 11 '19 at 20:11
  • [This jsfiddle shows a very simple table demonstrating the desired behavior](https://jsfiddle.net/mefnxLzh/1/) with the `:focus` pseudo-class applied and functioning as expected. I don't believe that `this.dataSource.connect().value[0]` results in an HTMLTableRowElement or indeed an HTMLElement... – Heretic Monkey Oct 11 '19 at 20:12
  • I think you'd be better off passing the `row` in your `tableKeydown` event, along with `$event`... – Heretic Monkey Oct 11 '19 at 20:14
  • @HereticMonkey thanks, this gets the arrows to shift the focus. I'm going to try to use this to work in the ngOnInit as well. – Pep Oct 12 '19 at 12:51
  • @SethWinters I appreciate the effort but I can't use jQuery for this issue. – Pep Oct 12 '19 at 12:51

1 Answers1

1

The focus() method works on very few HTML elements (Which HTML elements can receive focus?). You can test this directly in the browser's developer tools after having added a button to your html with an (click)="myfunc()" event handler.

In the element inspector set the id attribute for the first tr, e.g. id="myrow". Then on the console:

var myrow = document.getElementById('myrow')
var myfunc = function () {console.log('fired'), myrow.focus(); }

Now click the button, and - due to my tests - the row does not get the focus. Using this test you should try whether focus() works on the row's first cell. As td is not in the list of elements mentioned above, it's necessary to have an element inside td on which focus() works, e.g. an input. In this case you can set the id attribute on the input, than call getElementById in your code (attention: the element must be rendered already (see the discussion here: How to check if element exists in the visible DOM?)) and than call focus() on it.

Attention again, in angular it seems necessary to call focus() with a timeout:

setFocus(el_ref: HTMLElement) {
    setTimeout(() => el_ref.focus(), 10);
}

.

FrankL
  • 374
  • 4
  • 5
  • you need'nt use this '10' in your setTimeout: simple `setTimeout(()=>{...})`, Angular, when see a setTimeout "save in a queue" the instruction to execute after refresh the dom. (really it's some more complex, but I can't not find a sort way to explain it) – Eliseo Oct 12 '19 at 10:09
  • @Eliseo: Thanks, it works. Now I understand a little more ... – FrankL Oct 12 '19 at 16:41