html - Issue with Tabbing into Button Column Using Keyboard Suppress Event - Stack Overflow

admin2025-04-16  5

I'm encountering an issue in my AG Grid implementation in Angular. I have added a keyboard suppress event to allow users to tab into a cell and interact with a button inside that cell using the keyboard. However, when the user tabs into the button column, they seem to get "locked" into the button, preventing further navigation using the Tab key. Tabbing through the headers works fine, but this issue only arises with the button inside the grid cell.

appponent.ts

import { Component } from '@angular/core';
import { AgGridAngular } from 'ag-grid-angular';
import type { ColDef, GridApi, SuppressKeyboardEventParams } from 'ag-grid-community';
import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community';

// Register all Community features
ModuleRegistry.registerModules([AllCommunityModule]);
import { ButtonRendererComponent } from './button-renderer/button-rendererponent';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [AgGridAngular, ButtonRendererComponent],
  templateUrl: './appponent.html',
  styleUrls: ['./appponent.scss']
})
export class AppComponent {
  private gridApi!: GridApi;
  searchText = '';

  rowData = [
    { name: "Dog", type: "Mammal", size: "Medium", speed: "Fast", lifespan: "10-15 years" },
    { name: "Cat", type: "Mammal", size: "Small", speed: "Fast", lifespan: "12-18 years" },
    { name: "Turtle", type: "Reptile", size: "Small", speed: "Slow", lifespan: "50-100 years" },
    { name: "Rabbit", type: "Mammal", size: "Small", speed: "Fast", lifespan: "8-12 years" },
    { name: "Parrot", type: "Bird", size: "Small", speed: "Medium", lifespan: "50-80 years" },
    { name: "Elephant", type: "Mammal", size: "Large", speed: "Slow", lifespan: "60-70 years" }
  ];

  columnDefs: ColDef[] = [
    { field: "name", sortable: true, filter: true },
    { field: "type", sortable: true, filter: true },
    { field: "size", sortable: true, filter: true },
    { field: "speed", sortable: true, filter: true },
    { field: "lifespan", sortable: true, filter: true },
    {
      headerName: 'Action',
      field: 'action',
      cellRenderer: ButtonRendererComponent
    }
  ];

  public defaultColDef: ColDef = {
    minWidth: 130,
    suppressKeyboardEvent : this.suppressKeyboardEvent
  };

  onGridReady(params: any) {
    this.gridApi = params.api;
  }


  // Function to trigger the alert
  sayHello() {
    alert('Hello!');
  }

  GRID_CELL_CLASSNAME = 'ag-cell';

  getAllFocusableElementsOf(el: HTMLElement) {
  return Array.from<HTMLElement>(
    el.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    ),
  ).filter((focusableEl) => {
    return focusableEl.tabIndex !== -1;
  });
}

 getEventPath(event: Event): HTMLElement[] {
  const path: HTMLElement[] = [];
  let currentTarget: any = event.target;
  while (currentTarget) {
    path.push(currentTarget);
    currentTarget = currentTarget.parentElement;
  }
  return path;
}

 suppressKeyboardEvent({ event }: SuppressKeyboardEventParams<any>) {
  const { key, shiftKey } = event;
  const path = this.getEventPath(event);
  const isTabForward = key === 'Tab' && shiftKey === false;
  const isTabBackward = key === 'Tab' && shiftKey === true;
  let suppressEvent = false;

  if (isTabForward || isTabBackward) {
    const eGridCell = path.find((el) => {
      return el.classList?.contains(this.GRID_CELL_CLASSNAME);
    });

    if (!eGridCell) {
      return suppressEvent;
    }

    const focusableChildrenElements = this.getAllFocusableElementsOf(eGridCell);

    const lastCellChildEl = focusableChildrenElements[focusableChildrenElements.length - 1];
    const firstCellChildEl = focusableChildrenElements[0];

    if (focusableChildrenElements.length === 0) {
      return false;
    }

    const currentIndex = focusableChildrenElements.indexOf(document.activeElement as HTMLElement);

    if (isTabForward) {
      const isLastChildFocused = lastCellChildEl && document.activeElement === lastCellChildEl;
      if (!isLastChildFocused) {
        suppressEvent = true;
        event.preventDefault();
        focusableChildrenElements[currentIndex + 1].focus();
      }
    } else {
      const isFirstChildFocused = firstCellChildEl && document.activeElement === firstCellChildEl;
      if (!isFirstChildFocused) {
        suppressEvent = true;
        event.preventDefault();
        focusableChildrenElements[currentIndex - 1].focus();
      }
    }
  }
  return suppressEvent;
}

}

appponent.html

<ag-grid-angular 
  style="width: 100%; height: 500px;"
  class="ag-theme-quartz"
  [rowData]="rowData"
  [columnDefs]="columnDefs"
  [defaultColDef]="defaultColDef"
  [pagination]="true"
  [rowSelection]="'single'"
  (gridReady)="onGridReady($event)">
</ag-grid-angular>

button-rendererponent

import { Component } from '@angular/core';

@Component({
  standalone: true,
  templateUrl: './button-renderer.html',
})
export class ButtonRendererComponent {
  params: any;

  agInit(params: any): void {
    this.params = params;
  }

  onClick() {
    alert('Hello');
  }
}

button-renderer.html

<button (click)="onClick()">Click</button>

I'm encountering an issue in my AG Grid implementation in Angular. I have added a keyboard suppress event to allow users to tab into a cell and interact with a button inside that cell using the keyboard. However, when the user tabs into the button column, they seem to get "locked" into the button, preventing further navigation using the Tab key. Tabbing through the headers works fine, but this issue only arises with the button inside the grid cell.

app.component.ts

import { Component } from '@angular/core';
import { AgGridAngular } from 'ag-grid-angular';
import type { ColDef, GridApi, SuppressKeyboardEventParams } from 'ag-grid-community';
import { AllCommunityModule, ModuleRegistry } from 'ag-grid-community';

// Register all Community features
ModuleRegistry.registerModules([AllCommunityModule]);
import { ButtonRendererComponent } from './button-renderer/button-renderer.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [AgGridAngular, ButtonRendererComponent],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  private gridApi!: GridApi;
  searchText = '';

  rowData = [
    { name: "Dog", type: "Mammal", size: "Medium", speed: "Fast", lifespan: "10-15 years" },
    { name: "Cat", type: "Mammal", size: "Small", speed: "Fast", lifespan: "12-18 years" },
    { name: "Turtle", type: "Reptile", size: "Small", speed: "Slow", lifespan: "50-100 years" },
    { name: "Rabbit", type: "Mammal", size: "Small", speed: "Fast", lifespan: "8-12 years" },
    { name: "Parrot", type: "Bird", size: "Small", speed: "Medium", lifespan: "50-80 years" },
    { name: "Elephant", type: "Mammal", size: "Large", speed: "Slow", lifespan: "60-70 years" }
  ];

  columnDefs: ColDef[] = [
    { field: "name", sortable: true, filter: true },
    { field: "type", sortable: true, filter: true },
    { field: "size", sortable: true, filter: true },
    { field: "speed", sortable: true, filter: true },
    { field: "lifespan", sortable: true, filter: true },
    {
      headerName: 'Action',
      field: 'action',
      cellRenderer: ButtonRendererComponent
    }
  ];

  public defaultColDef: ColDef = {
    minWidth: 130,
    suppressKeyboardEvent : this.suppressKeyboardEvent
  };

  onGridReady(params: any) {
    this.gridApi = params.api;
  }


  // Function to trigger the alert
  sayHello() {
    alert('Hello!');
  }

  GRID_CELL_CLASSNAME = 'ag-cell';

  getAllFocusableElementsOf(el: HTMLElement) {
  return Array.from<HTMLElement>(
    el.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    ),
  ).filter((focusableEl) => {
    return focusableEl.tabIndex !== -1;
  });
}

 getEventPath(event: Event): HTMLElement[] {
  const path: HTMLElement[] = [];
  let currentTarget: any = event.target;
  while (currentTarget) {
    path.push(currentTarget);
    currentTarget = currentTarget.parentElement;
  }
  return path;
}

 suppressKeyboardEvent({ event }: SuppressKeyboardEventParams<any>) {
  const { key, shiftKey } = event;
  const path = this.getEventPath(event);
  const isTabForward = key === 'Tab' && shiftKey === false;
  const isTabBackward = key === 'Tab' && shiftKey === true;
  let suppressEvent = false;

  if (isTabForward || isTabBackward) {
    const eGridCell = path.find((el) => {
      return el.classList?.contains(this.GRID_CELL_CLASSNAME);
    });

    if (!eGridCell) {
      return suppressEvent;
    }

    const focusableChildrenElements = this.getAllFocusableElementsOf(eGridCell);

    const lastCellChildEl = focusableChildrenElements[focusableChildrenElements.length - 1];
    const firstCellChildEl = focusableChildrenElements[0];

    if (focusableChildrenElements.length === 0) {
      return false;
    }

    const currentIndex = focusableChildrenElements.indexOf(document.activeElement as HTMLElement);

    if (isTabForward) {
      const isLastChildFocused = lastCellChildEl && document.activeElement === lastCellChildEl;
      if (!isLastChildFocused) {
        suppressEvent = true;
        event.preventDefault();
        focusableChildrenElements[currentIndex + 1].focus();
      }
    } else {
      const isFirstChildFocused = firstCellChildEl && document.activeElement === firstCellChildEl;
      if (!isFirstChildFocused) {
        suppressEvent = true;
        event.preventDefault();
        focusableChildrenElements[currentIndex - 1].focus();
      }
    }
  }
  return suppressEvent;
}

}

app.component.html

<ag-grid-angular 
  style="width: 100%; height: 500px;"
  class="ag-theme-quartz"
  [rowData]="rowData"
  [columnDefs]="columnDefs"
  [defaultColDef]="defaultColDef"
  [pagination]="true"
  [rowSelection]="'single'"
  (gridReady)="onGridReady($event)">
</ag-grid-angular>

button-renderer.component

import { Component } from '@angular/core';

@Component({
  standalone: true,
  templateUrl: './button-renderer.html',
})
export class ButtonRendererComponent {
  params: any;

  agInit(params: any): void {
    this.params = params;
  }

  onClick() {
    alert('Hello');
  }
}

button-renderer.html

<button (click)="onClick()">Click</button>
Share Improve this question edited Feb 3 at 9:36 DarkBee 15.5k8 gold badges72 silver badges118 bronze badges asked Feb 3 at 4:29 MogurMogur 193 bronze badges
Add a comment  | 

1 Answer 1

Reset to default -1

It looks like the suppressKeyboardEvent prevents the normal tab behavior even if you're already on the first/last element in the cell.

You should be able to fix it by using this version of the suppressKeyboardEvent function, which stops supressing the event once the first/last element has been reached!

suppressKeyboardEvent({ event }: SuppressKeyboardEventParams<any>) {
  const { key, shiftKey } = event;
  const path = this.getEventPath(event);
  const isTabForward = key === 'Tab' && !shiftKey;
  const isTabBackward = key === 'Tab' && shiftKey;
  let suppressEvent = false;

  if (isTabForward || isTabBackward) {
    const eGridCell = path.find((el) => el.classList?.contains(this.GRID_CELL_CLASSNAME));

    if (!eGridCell) {
      return suppressEvent;
    }

    const focusableChildrenElements = this.getAllFocusableElementsOf(eGridCell);

    if (focusableChildrenElements.length === 0) {
      return false; // No focusable elements, allow default tab behavior
    }

    const lastCellChildEl = focusableChildrenElements[focusableChildrenElements.length - 1];
    const firstCellChildEl = focusableChildrenElements[0];
    const currentIndex = focusableChildrenElements.indexOf(document.activeElement as HTMLElement);

    if (isTabForward) {
      if (document.activeElement === lastCellChildEl) {
        return false; // Allow tabbing out of the cell
      }
      suppressEvent = true;
      event.preventDefault();
      focusableChildrenElements[currentIndex + 1]?.focus();
    } else {
      if (document.activeElement === firstCellChildEl) {
        return false; // Allow shift+tab to move out of the cell
      }
      suppressEvent = true;
      event.preventDefault();
      focusableChildrenElements[currentIndex - 1]?.focus();
    }
  }
  return suppressEvent;
}
转载请注明原文地址:http://anycun.com/QandA/1744779002a87504.html