Skip to content

Commit

Permalink
feat(common): create ColumnPicker dynamically every time (#1165)
Browse files Browse the repository at this point in the history
* feat(common): create ColumnPicker dynamically every time
- similarly to the updated GridMenu behavior, we should not create the Column Picker in the DOM when the grid gets created, instead we should create/recreate the picker every time we need to open it
  • Loading branch information
ghiscoding authored Oct 31, 2023
1 parent 29579b2 commit 7e8d80e
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 62 deletions.
52 changes: 28 additions & 24 deletions packages/common/src/extensions/__tests__/slickColumnPicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,16 @@ describe('ColumnPickerControl', () => {

const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub);
const inputElm = control.menuElement.querySelector('input[type="checkbox"]') as HTMLInputElement;
const inputElm = control.menuElement!.querySelector('input[type="checkbox"]') as HTMLInputElement;
inputElm.dispatchEvent(new Event('click', { bubbles: true, cancelable: true, composed: false }));

expect(control.menuElement.style.display).toBe('block');
expect(control.menuElement!.style.display).toBe('block');
expect(setSelectionSpy).toHaveBeenCalledWith(mockRowSelection);
expect(control.getAllColumns()).toEqual(columnsMock);
expect(control.getVisibleColumns()).toEqual(columnsMock);
});

it('should open the column picker via "onHeaderContextMenu" and then expect it to hide when clicking anywhere in the DOM body', () => {
it('should open the Column Picker and then expect it to hide when clicking anywhere in the DOM body', () => {
const mockRowSelection = [0, 3, 5];
jest.spyOn(control.eventHandler, 'subscribe');
jest.spyOn(gridStub, 'getColumnIndex').mockReturnValue(undefined as any).mockReturnValue(1);
Expand All @@ -122,13 +122,17 @@ describe('ColumnPickerControl', () => {

const eventData = { ...new Slick.EventData(), preventDefault: jest.fn() };
gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub);
control.menuElement!.dispatchEvent(new Event('mousedown', { bubbles: true }));

expect(control.menuElement.style.display).toBe('block');
// click inside menu shouldn't close it
expect(control.menuElement!.style.display).toBe('block');
expect(control.menuElement).toBeTruthy();

// click anywhere else should close it
const bodyElm = document.body;
bodyElm.dispatchEvent(new Event('mousedown', { bubbles: true }));

expect(control.menuElement.style.display).toBe('none');
expect(control.menuElement).toBeFalsy();
});

it('should query an input checkbox change event and expect "readjustFrozenColumnIndexWhenNeeded" method to be called when the grid is detected to be a frozen grid', () => {
Expand All @@ -141,7 +145,7 @@ describe('ColumnPickerControl', () => {
control.init();

gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub);
control.menuElement.querySelector('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));
control.menuElement!.querySelector('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));

expect(handlerSpy).toHaveBeenCalledTimes(2);
expect(readjustSpy).toHaveBeenCalledWith(0, columnsMock, columnsMock);
Expand All @@ -159,8 +163,8 @@ describe('ColumnPickerControl', () => {
control.init();

gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub);
control.menuElement.querySelector<HTMLInputElement>('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));
const liElmList = control.menuElement.querySelectorAll<HTMLLIElement>('li');
control.menuElement!.querySelector<HTMLInputElement>('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));
const liElmList = control.menuElement!.querySelectorAll<HTMLLIElement>('li');

expect(handlerSpy).toHaveBeenCalledTimes(2);
expect(readjustSpy).toHaveBeenCalledWith(0, columnsMock, columnsMock);
Expand All @@ -179,9 +183,9 @@ describe('ColumnPickerControl', () => {
control.init();

gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub);
control.menuElement.querySelector<HTMLInputElement>('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));
const inputForcefitElm = control.menuElement.querySelector('#slickgrid_124343-colpicker-forcefit') as HTMLInputElement;
const labelSyncElm = control.menuElement.querySelector('label[for=slickgrid_124343-colpicker-forcefit]') as HTMLDivElement;
control.menuElement!.querySelector<HTMLInputElement>('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));
const inputForcefitElm = control.menuElement!.querySelector('#slickgrid_124343-colpicker-forcefit') as HTMLInputElement;
const labelSyncElm = control.menuElement!.querySelector('label[for=slickgrid_124343-colpicker-forcefit]') as HTMLDivElement;

expect(handlerSpy).toHaveBeenCalledTimes(2);
expect(control.getAllColumns()).toEqual(columnsMock);
Expand All @@ -201,9 +205,9 @@ describe('ColumnPickerControl', () => {
control.init();

gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub);
control.menuElement.querySelector<HTMLInputElement>('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));
const inputSyncElm = control.menuElement.querySelector('#slickgrid_124343-colpicker-syncresize') as HTMLInputElement;
const labelSyncElm = control.menuElement.querySelector('label[for=slickgrid_124343-colpicker-syncresize]') as HTMLDivElement;
control.menuElement!.querySelector<HTMLInputElement>('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));
const inputSyncElm = control.menuElement!.querySelector('#slickgrid_124343-colpicker-syncresize') as HTMLInputElement;
const labelSyncElm = control.menuElement!.querySelector('label[for=slickgrid_124343-colpicker-syncresize]') as HTMLDivElement;

expect(handlerSpy).toHaveBeenCalledTimes(2);
expect(control.getAllColumns()).toEqual(columnsMock);
Expand All @@ -224,7 +228,7 @@ describe('ColumnPickerControl', () => {
control.init();

gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub);
control.menuElement.querySelector<HTMLInputElement>('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));
control.menuElement!.querySelector<HTMLInputElement>('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));

const expectedCallbackArgs = {
columnId: 'field1',
Expand Down Expand Up @@ -254,8 +258,8 @@ describe('ColumnPickerControl', () => {
control.init();

gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub);
const inputForcefitElm = control.menuElement.querySelector('#slickgrid_124343-colpicker-forcefit') as HTMLInputElement;
const labelSyncElm = control.menuElement.querySelector('label[for=slickgrid_124343-colpicker-forcefit]') as HTMLDivElement;
const inputForcefitElm = control.menuElement!.querySelector('#slickgrid_124343-colpicker-forcefit') as HTMLInputElement;
const labelSyncElm = control.menuElement!.querySelector('label[for=slickgrid_124343-colpicker-forcefit]') as HTMLDivElement;
inputForcefitElm.dispatchEvent(new Event('click', { bubbles: true }));

expect(handlerSpy).toHaveBeenCalledTimes(2);
Expand All @@ -280,8 +284,8 @@ describe('ColumnPickerControl', () => {
control.init();

gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub);
const inputSyncElm = control.menuElement.querySelector('#slickgrid_124343-colpicker-syncresize') as HTMLInputElement;
const labelSyncElm = control.menuElement.querySelector('label[for=slickgrid_124343-colpicker-syncresize]') as HTMLDivElement;
const inputSyncElm = control.menuElement!.querySelector('#slickgrid_124343-colpicker-syncresize') as HTMLInputElement;
const labelSyncElm = control.menuElement!.querySelector('label[for=slickgrid_124343-colpicker-syncresize]') as HTMLDivElement;
inputSyncElm.dispatchEvent(new Event('click', { bubbles: true }));

expect(handlerSpy).toHaveBeenCalledTimes(2);
Expand Down Expand Up @@ -314,8 +318,8 @@ describe('ColumnPickerControl', () => {

gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub);
gridStub.onColumnsReordered.notify({ impactedColumns: columnsUnorderedMock, grid: gridStub }, eventData, gridStub);
control.menuElement.querySelector<HTMLInputElement>('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));
const col4 = control.menuElement.querySelector<HTMLInputElement>('li.hidden input[data-columnid=field4]');
control.menuElement!.querySelector<HTMLInputElement>('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));
const col4 = control.menuElement!.querySelector<HTMLInputElement>('li.hidden input[data-columnid=field4]');

expect(handlerSpy).toHaveBeenCalledTimes(2);
expect(control.getAllColumns()).toEqual(columnsMock);
Expand All @@ -341,9 +345,9 @@ describe('ColumnPickerControl', () => {
control.translateColumnPicker();

gridStub.onHeaderContextMenu.notify({ column: columnsMock[1], grid: gridStub }, eventData, gridStub);
control.menuElement.querySelector<HTMLInputElement>('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));
const labelForcefitElm = control.menuElement.querySelector('label[for=slickgrid_124343-colpicker-forcefit]') as HTMLDivElement;
const labelSyncElm = control.menuElement.querySelector('label[for=slickgrid_124343-colpicker-syncresize]') as HTMLDivElement;
control.menuElement!.querySelector<HTMLInputElement>('input[type="checkbox"]')!.dispatchEvent(new Event('click', { bubbles: true }));
const labelForcefitElm = control.menuElement!.querySelector('label[for=slickgrid_124343-colpicker-forcefit]') as HTMLDivElement;
const labelSyncElm = control.menuElement!.querySelector('label[for=slickgrid_124343-colpicker-syncresize]') as HTMLDivElement;

expect(handlerSpy).toHaveBeenCalledTimes(2);
expect(labelForcefitElm.textContent).toBe('Ajustement forcé des colonnes');
Expand Down
90 changes: 52 additions & 38 deletions packages/common/src/extensions/slickColumnPicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class SlickColumnPicker {
protected _eventHandler!: SlickEventHandler;
protected _gridUid = '';
protected _listElm!: HTMLSpanElement;
protected _menuElm!: HTMLDivElement;
protected _menuElm: HTMLDivElement | null = null;
protected _columnCheckboxes: HTMLInputElement[] = [];
onColumnsChanged = new Slick.Event();

Expand Down Expand Up @@ -85,7 +85,7 @@ export class SlickColumnPicker {
return this.sharedService.slickGrid;
}

get menuElement(): HTMLDivElement {
get menuElement(): HTMLDivElement | null {
return this._menuElm;
}

Expand All @@ -102,34 +102,45 @@ export class SlickColumnPicker {
this._eventHandler.subscribe(this.grid.onHeaderContextMenu, this.handleHeaderContextMenu.bind(this) as EventListener);
this._eventHandler.subscribe(this.grid.onColumnsReordered, updateColumnPickerOrder.bind(this) as EventListener);

this._menuElm = createDomElement('div', {
ariaExpanded: 'false',
className: `slick-column-picker ${this._gridUid}`, role: 'menu',
style: { display: 'none' },
});

// add Close button and optiona a Column list title
addColumnTitleElementWhenDefined.call(this, this._menuElm);
addCloseButtomElement.call(this, this._menuElm);

this._listElm = createDomElement('div', { className: 'slick-column-picker-list', role: 'menu' });
this._bindEventService.bind(this._menuElm, 'click', handleColumnPickerItemClick.bind(this) as EventListener, undefined, 'parent-menu');

// Hide the menu on outside click.
this._bindEventService.bind(document.body, 'mousedown', this.handleBodyMouseDown.bind(this) as EventListener, { capture: true });
this._bindEventService.bind(document.body, 'mousedown', this.handleBodyMouseDown.bind(this) as EventListener, undefined, 'body');

// destroy the picker if user leaves the page
this._bindEventService.bind(document.body, 'beforeunload', this.dispose.bind(this) as EventListener);

document.body.appendChild(this._menuElm);
this._bindEventService.bind(document.body, 'beforeunload', this.dispose.bind(this) as EventListener, undefined, 'body');
}

/** Dispose (destroy) the SlickGrid 3rd party plugin */
dispose() {
this._eventHandler.unsubscribeAll();
this._bindEventService.unbindAll();
this._listElm?.remove?.();
this._menuElm?.remove?.();
this.disposeMenu();
}

disposeMenu() {
this._bindEventService.unbindAll('parent-menu');
this._listElm?.remove();
this._menuElm?.remove();
this._menuElm = null;
}

createPickerMenu() {
const menuElm = createDomElement('div', {
ariaExpanded: 'true',
className: `slick-column-picker ${this._gridUid}`,
role: 'menu',
});
updateColumnPickerOrder.call(this);

// add Close button and optiona a Column list title
addColumnTitleElementWhenDefined.call(this, menuElm);
addCloseButtomElement.call(this, menuElm);

this._listElm = createDomElement('div', { className: 'slick-column-picker-list', role: 'menu' });
this._bindEventService.bind(menuElm, 'click', handleColumnPickerItemClick.bind(this) as EventListener, undefined, 'parent-menu');

document.body.appendChild(menuElm);

return menuElm;
}

/**
Expand Down Expand Up @@ -164,9 +175,7 @@ export class SlickColumnPicker {
this.extensionUtility.translateItems(this._columns, 'nameKey', 'name');

// update the Titles of each sections (command, commandTitle, ...)
if (this.addonOptions) {
this.updateAllTitles(this.addonOptions);
}
this.translateTitleLabels(this.addonOptions);
}

// --
Expand All @@ -175,38 +184,43 @@ export class SlickColumnPicker {

/** Mouse down handler when clicking anywhere in the DOM body */
protected handleBodyMouseDown(e: DOMMouseOrTouchEvent<HTMLDivElement>) {
if ((this._menuElm !== e.target && !this._menuElm.contains(e.target)) || (e.target.className === 'close' && e.target.closest('.slick-column-picker'))) {
this._menuElm.setAttribute('aria-expanded', 'false');
this._menuElm.style.display = 'none';
if ((this._menuElm !== e.target && !this._menuElm?.contains(e.target)) || (e.target.className === 'close' && e.target.closest('.slick-column-picker'))) {
this.disposeMenu();
}
}

/** Mouse header context handler when doing a right+click on any of the header column title */
protected handleHeaderContextMenu(e: DOMMouseOrTouchEvent<HTMLDivElement>) {
e.preventDefault();
emptyElement(this._listElm);
updateColumnPickerOrder.call(this);
this._columnCheckboxes = [];

this._menuElm = this.createPickerMenu();

// load the column & create column picker list
populateColumnPicker.call(this, this.addonOptions);
document.body.appendChild(this._menuElm);

this.repositionMenu(e);
}

protected repositionMenu(event: DOMMouseOrTouchEvent<HTMLDivElement>) {
const targetEvent: MouseEvent | Touch = (event as TouchEvent)?.touches?.[0] ?? event;
this._menuElm.style.top = `${targetEvent.pageY - 10}px`;
this._menuElm.style.left = `${targetEvent.pageX - 10}px`;
this._menuElm.style.minHeight = findWidthOrDefault(this.addonOptions.minHeight, '');
this._menuElm.style.maxHeight = findWidthOrDefault(this.addonOptions.maxHeight, `${window.innerHeight - targetEvent.clientY}px`);
this._menuElm.style.display = 'block';
this._menuElm.setAttribute('aria-expanded', 'true');
this._menuElm.appendChild(this._listElm);
if (this._menuElm) {
this._menuElm.style.top = `${targetEvent.pageY - 10}px`;
this._menuElm.style.left = `${targetEvent.pageX - 10}px`;
this._menuElm.style.minHeight = findWidthOrDefault(this.addonOptions.minHeight, '');
this._menuElm.style.maxHeight = findWidthOrDefault(this.addonOptions.maxHeight, `${window.innerHeight - targetEvent.clientY}px`);
this._menuElm.style.display = 'block';
this._menuElm.setAttribute('aria-expanded', 'true');
this._menuElm.appendChild(this._listElm);
}
}

/** Update the Titles of each sections (command, commandTitle, ...) */
protected updateAllTitles(options: ColumnPickerOption) {
if (this._columnTitleElm?.textContent && options.columnTitle) {
this._columnTitleElm.textContent = options.columnTitle;
protected translateTitleLabels(pickerOptions: ColumnPickerOption) {
if (pickerOptions) {
pickerOptions.columnTitle = this.extensionUtility.getPickerTitleOutputString('columnTitle', 'gridMenu');
}
}
}

0 comments on commit 7e8d80e

Please sign in to comment.