Skip to content

Commit

Permalink
fix(components/ag-grid): refocus cell editors on api refresh (#3087) (#…
Browse files Browse the repository at this point in the history
…3092)

🍒 Cherry picked from #3087 [fix(components/ag-grid): refocus
cell editors on api
refresh](#3087)


[AB#3240868](https://dev.azure.com/blackbaud/f565481a-7bc9-4083-95d5-4f953da6d499/_workitems/edit/3240868)

Co-authored-by: John White <750350+johnhwhite@users.noreply.github.com>
  • Loading branch information
blackbaud-sky-build-user and johnhwhite authored Feb 3, 2025
1 parent c0c9c48 commit c387ac2
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,21 @@ describe('SkyCellEditorAutocompleteComponent', () => {
});

describe('agInit', () => {
const api = jasmine.createSpyObj<GridApi>('api', [
'getDisplayNameForColumn',
'getGridOption',
'stopEditing',
]);
api.getGridOption.and.returnValue(true);
let api: jasmine.SpyObj<GridApi>;
let cellEditorParams: Partial<SkyCellEditorAutocompleteParams>;
let column: AgColumn;
let gridCell: HTMLDivElement;
const selection = data[0];
const rowNode = new RowNode({} as BeanCollection);
rowNode.rowHeight = 37;

beforeEach(() => {
api = jasmine.createSpyObj<GridApi>('api', [
'getDisplayNameForColumn',
'getGridOption',
'stopEditing',
]);
api.getGridOption.and.returnValue(true);
column = new AgColumn(
{
colId: 'col',
Expand All @@ -72,11 +74,13 @@ describe('SkyCellEditorAutocompleteComponent', () => {
'col',
true,
);
gridCell = document.createElement('div');

cellEditorParams = {
api,
value: selection,
column,
eGridCell: gridCell,
node: rowNode,
colDef: {},
cellStartedEdit: true,
Expand All @@ -95,14 +99,30 @@ describe('SkyCellEditorAutocompleteComponent', () => {
tick();

component.onAutocompleteOpenChange(true);
component.onBlur();
component.onBlur({} as FocusEvent);
expect(cellEditorParams.api?.stopEditing).not.toHaveBeenCalled();

component.onAutocompleteOpenChange(false);
component.onBlur();
component.onBlur({} as FocusEvent);
expect(cellEditorParams.api?.stopEditing).toHaveBeenCalled();
}));

it('should respond to refocus', fakeAsync(() => {
fixture.detectChanges();

const input = nativeElement.querySelector('input') as HTMLInputElement;
spyOn(input, 'focus');

component.agInit(cellEditorParams as SkyCellEditorAutocompleteParams);
component.onBlur({
relatedTarget: gridCell,
} as unknown as FocusEvent);
tick();
expect(input).toBeVisible();
expect(input.focus).toHaveBeenCalled();
expect(cellEditorParams.api?.stopEditing).not.toHaveBeenCalled();
}));

it('should set the correct aria label', () => {
api.getDisplayNameForColumn.and.returnValue('Testing');
component.agInit({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,18 @@ export class SkyAgGridCellEditorAutocompleteComponent
@ViewChild('skyCellEditorAutocomplete', { read: ElementRef })
public input: ElementRef | undefined;

@HostListener('blur')
public onBlur(): void {
this.#stopEditingOnBlur();
@HostListener('focusout', ['$event'])
public onBlur(event: FocusEvent): void {
if (
event.relatedTarget &&
event.relatedTarget === this.#params?.eGridCell
) {
// If focus is being set to the grid cell, schedule focus on the input.
// This happens when the refreshCells API is called.
this.afterGuiAttached();
} else {
this.#stopEditingOnBlur();
}
}

public agInit(params: SkyCellEditorAutocompleteParams): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ describe('SkyCellEditorCurrencyComponent', () => {
describe('afterGuiAttached', () => {
let cellEditorParams: Partial<SkyCellEditorCurrencyParams>;
let column: AgColumn;
let gridCell: HTMLDivElement;
const rowNode = new RowNode({} as BeanCollection);
rowNode.rowHeight = 37;
const value = 15;
Expand All @@ -174,16 +175,19 @@ describe('SkyCellEditorCurrencyComponent', () => {
true,
);

const api = {} as GridApi;
const api = jasmine.createSpyObj<GridApi>([
'getDisplayNameForColumn',
'stopEditing',
]);

api.getDisplayNameForColumn = (): string => {
return '';
};
api.getDisplayNameForColumn.and.returnValue('');
gridCell = document.createElement('div');

cellEditorParams = {
api,
value: value,
column,
eGridCell: gridCell,
node: rowNode,
colDef: {},
cellStartedEdit: true,
Expand Down Expand Up @@ -514,6 +518,24 @@ describe('SkyCellEditorCurrencyComponent', () => {
expect(input).toBeVisible();
expect(input.focus).toHaveBeenCalled();
}));

it('should respond to refocus', fakeAsync(() => {
currencyEditorComponent.agInit(cellEditorParams as ICellEditorParams);
currencyEditorFixture.detectChanges();

const input = currencyEditorFixture.nativeElement.querySelector(
'input',
) as HTMLInputElement;
spyOn(input, 'focus');

currencyEditorComponent.onFocusOut({
relatedTarget: gridCell,
} as unknown as FocusEvent);
tick();
expect(input).toBeVisible();
expect(input.focus).toHaveBeenCalled();
expect(cellEditorParams.api?.stopEditing).not.toHaveBeenCalled();
}));
});

it('returns undefined if the value is not set', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ChangeDetectorRef,
Component,
ElementRef,
HostListener,
ViewChild,
inject,
} from '@angular/core';
Expand Down Expand Up @@ -46,6 +47,15 @@ export class SkyAgGridCellEditorCurrencyComponent
@ViewChild('skyCellEditorCurrency', { read: ElementRef })
public input: ElementRef | undefined;

@HostListener('focusout', ['$event'])
public onFocusOut(event: FocusEvent): void {
if (event.relatedTarget && event.relatedTarget === this.params?.eGridCell) {
// If focus is being set to the grid cell, schedule focus on the input.
// This happens when the refreshCells API is called.
this.afterGuiAttached();
}
}

#triggerType: SkyAgGridCellEditorInitialAction | undefined;
readonly #changeDetector = inject(ChangeDetectorRef);
#initialized = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { SkyAgGridCellEditorLookupComponent } from './cell-editor-lookup.compone
import { SkyAgGridCellEditorLookupModule } from './cell-editor-lookup.module';

describe('SkyAgGridCellEditorLookupComponent', () => {
let api: jasmine.SpyObj<GridApi>;
let component: SkyAgGridCellEditorLookupComponent;
const data = [
{ id: '1', name: 'John Doe', town: 'Daniel Island' },
Expand All @@ -32,6 +33,7 @@ describe('SkyAgGridCellEditorLookupComponent', () => {
{ id: '4', name: 'Jane Smith', town: 'Mt Pleasant' },
];
let fixture: ComponentFixture<SkyAgGridCellEditorLookupComponent>;
let gridCell: HTMLDivElement;
let nativeElement: HTMLElement;
let callback: ((args: Record<string, unknown>) => void) | undefined;
const selection = [data[0]];
Expand All @@ -52,18 +54,21 @@ describe('SkyAgGridCellEditorLookupComponent', () => {
],
});

api = jasmine.createSpyObj('GridApi', [
'addEventListener',
'getGridOption',
'stopEditing',
]);
api.addEventListener.and.callFake(
(_event: string, listener: (params: any) => void) => {
callback = listener;
},
);
api.getGridOption.and.returnValue(true);
gridCell = document.createElement('div');

cellEditorParams = {
api: {
addEventListener: (
event: string,
listener: (args: Record<string, unknown>) => void,
) => {
callback = listener;
[event].pop();
},
getGridOption: jasmine.createSpy('getGridOption').and.returnValue(true),
stopEditing: jasmine.createSpy('stopEditing'),
} as unknown as GridApi,
api,
cellStartedEdit: true,
colDef: {
headerName: 'header',
Expand All @@ -76,6 +81,7 @@ describe('SkyAgGridCellEditorLookupComponent', () => {
gridOptions: {} as Partial<GridOptions>,
},
data: undefined,
eGridCell: gridCell,
formatValue: jasmine.createSpy('formatValue'),
onKeyDown: jasmine.createSpy('onKeyDown'),
parseValue: jasmine.createSpy('parseValue'),
Expand Down Expand Up @@ -304,7 +310,7 @@ describe('SkyAgGridCellEditorLookupComponent', () => {
expect(cellEditorParams.api?.stopEditing).toHaveBeenCalledTimes(1);

(cellEditorParams.api?.stopEditing as jasmine.Spy).calls.reset();
component.onBlur();
component.onBlur({} as FocusEvent);
tick();
expect(cellEditorParams.api?.getGridOption).toHaveBeenCalledTimes(2);
expect(
Expand All @@ -317,6 +323,27 @@ describe('SkyAgGridCellEditorLookupComponent', () => {
]);
expect(cellEditorParams.api?.stopEditing).toHaveBeenCalledTimes(1);
}));

it('should respond to refocus', fakeAsync(() => {
component.agInit(cellEditorParams as ICellEditorParams);
fixture.detectChanges();

const input = nativeElement.querySelector(
'textarea',
) as HTMLTextAreaElement;
spyOn(input, 'focus');

component.afterGuiAttached();
tick();

component.onBlur({
relatedTarget: gridCell,
} as unknown as FocusEvent);
tick();
expect(input).toBeVisible();
expect(input.focus).toHaveBeenCalled();
expect(cellEditorParams.api?.stopEditing).not.toHaveBeenCalled();
}));
});

describe('cellStartedEdit is false', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,18 @@ export class SkyAgGridCellEditorLookupComponent
#changeDetector = inject(ChangeDetectorRef);
#elementRef = inject(ElementRef<HTMLElement>);

@HostListener('blur')
public onBlur(): void {
this.#stopEditingOnBlur();
@HostListener('focusout', ['$event'])
public onBlur(event: FocusEvent): void {
if (
event.relatedTarget &&
event.relatedTarget === this.#params?.eGridCell
) {
// If focus is being set to the grid cell, schedule focus on the input.
// This happens when the refreshCells API is called.
this.afterGuiAttached();
} else {
this.#stopEditingOnBlur();
}
}

public agInit(params: SkyCellEditorLookupParams): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing';
import { expect, expectAsync } from '@skyux-sdk/testing';

import {
Expand Down Expand Up @@ -331,6 +336,7 @@ describe('SkyCellEditorNumberComponent', () => {
describe('afterGuiAttached', () => {
let cellEditorParams: Partial<SkyCellEditorNumberParams>;
let column: AgColumn;
let gridCell: HTMLDivElement;
const rowNode = new RowNode({} as BeanCollection);
rowNode.rowHeight = 37;
const value = 15;
Expand All @@ -351,10 +357,13 @@ describe('SkyCellEditorNumberComponent', () => {
return '';
};

gridCell = document.createElement('div');

cellEditorParams = {
api: gridApi,
value: value,
column,
eGridCell: gridCell,
node: rowNode,
colDef: {},
cellStartedEdit: true,
Expand All @@ -365,7 +374,7 @@ describe('SkyCellEditorNumberComponent', () => {
};
});

it('focuses on the input after it attaches to the DOM', () => {
it('focuses on the input after it attaches to the DOM', fakeAsync(() => {
numberEditorFixture.detectChanges();

const input = numberEditorNativeElement.querySelector(
Expand All @@ -374,10 +383,31 @@ describe('SkyCellEditorNumberComponent', () => {
spyOn(input, 'focus');

numberEditorComponent.afterGuiAttached();
tick();

expect(input).toBeVisible();
expect(input.focus).toHaveBeenCalled();
});
}));

it('should respond to refocus', fakeAsync(() => {
numberEditorComponent.agInit(cellEditorParams as ICellEditorParams);
numberEditorFixture.detectChanges();

const input = numberEditorNativeElement.querySelector(
'input',
) as HTMLInputElement;
spyOn(input, 'focus');

numberEditorComponent.afterGuiAttached();
tick();

numberEditorComponent.onFocusOut({
relatedTarget: gridCell,
} as unknown as FocusEvent);
tick();
expect(input).toBeVisible();
expect(input.focus).toHaveBeenCalled();
}));

describe('cellStartedEdit is true', () => {
it('does not select the input value if Backspace triggers the edit', () => {
Expand Down Expand Up @@ -431,7 +461,7 @@ describe('SkyCellEditorNumberComponent', () => {
expect(selectSpy).not.toHaveBeenCalled();
});

it('selects the input value if Enter triggers the edit', () => {
it('selects the input value if Enter triggers the edit', fakeAsync(() => {
numberEditorComponent.agInit({
...(cellEditorParams as ICellEditorParams),
eventKey: KeyCode.ENTER,
Expand All @@ -443,10 +473,11 @@ describe('SkyCellEditorNumberComponent', () => {
const selectSpy = spyOn(input, 'select');

numberEditorComponent.afterGuiAttached();
tick();

expect(input.value).toBe('15');
expect(selectSpy).toHaveBeenCalled();
});
}));

it('does not select the input value when a standard keyboard event triggers the edit', () => {
numberEditorComponent.agInit({
Expand Down
Loading

0 comments on commit c387ac2

Please sign in to comment.