最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

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

programmeradmin2浏览0评论

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.

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>
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;
}
发布评论

评论列表(0)

  1. 暂无评论