Skip to content

Commit

Permalink
feat: add item-toggle event to notify when user toggles an item (#8231)
Browse files Browse the repository at this point in the history
  • Loading branch information
vursen authored Dec 10, 2024
1 parent 2deab65 commit 40e49c5
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 2 deletions.
7 changes: 7 additions & 0 deletions packages/grid/src/vaadin-grid-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ export type GridDataProviderChangedEvent<TItem> = CustomEvent<{ value: GridDataP
*/
export type GridExpandedItemsChangedEvent<TItem> = CustomEvent<{ value: TItem[] }>;

/**
* Fired when the user selects or deselects an item through the selection column.
*/
export type GridItemToggleEvent<TItem> = CustomEvent<{ item: TItem; selected: boolean; shiftKey: boolean }>;

/**
* Fired when starting to drag grid rows.
*/
Expand Down Expand Up @@ -157,6 +162,8 @@ export interface GridCustomEventMap<TItem> {
'selected-items-changed': GridSelectedItemsChangedEvent<TItem>;

'size-changed': GridSizeChangedEvent;

'item-toggle': GridItemToggleEvent<TItem>;
}

export interface GridEventMap<TItem> extends HTMLElementEventMap, GridCustomEventMap<TItem> {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export declare class GridSelectionColumnBaseMixinClass<TItem> {
*/
dragSelect: boolean;

/**
* Indicates whether the shift key is currently pressed.
*/
protected _shiftKeyDown: boolean;

/**
* Override to handle the user selecting all items.
*/
Expand Down
31 changes: 31 additions & 0 deletions packages/grid/src/vaadin-grid-selection-column-base-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ export const GridSelectionColumnBaseMixin = (superClass) =>

/** @protected */
_selectAllHidden: Boolean,

/**
* Indicates whether the shift key is currently pressed.
*
* @protected
*/
_shiftKeyDown: {
type: Boolean,
value: false,
},
};
}

Expand All @@ -106,6 +116,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
this.__onCellTrack = this.__onCellTrack.bind(this);
this.__onCellClick = this.__onCellClick.bind(this);
this.__onCellMouseDown = this.__onCellMouseDown.bind(this);
this.__onGridInteraction = this.__onGridInteraction.bind(this);
this.__onActiveItemChanged = this.__onActiveItemChanged.bind(this);
this.__onSelectRowCheckboxChange = this.__onSelectRowCheckboxChange.bind(this);
this.__onSelectAllCheckboxChange = this.__onSelectAllCheckboxChange.bind(this);
Expand All @@ -115,6 +126,9 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
connectedCallback() {
super.connectedCallback();
if (this._grid) {
this._grid.addEventListener('keyup', this.__onGridInteraction);
this._grid.addEventListener('keydown', this.__onGridInteraction, { capture: true });
this._grid.addEventListener('mousedown', this.__onGridInteraction);
this._grid.addEventListener('active-item-changed', this.__onActiveItemChanged);
}
}
Expand All @@ -123,6 +137,9 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
disconnectedCallback() {
super.disconnectedCallback();
if (this._grid) {
this._grid.removeEventListener('keyup', this.__onGridInteraction);
this._grid.removeEventListener('keydown', this.__onGridInteraction, { capture: true });
this._grid.removeEventListener('mousedown', this.__onGridInteraction);
this._grid.removeEventListener('active-item-changed', this.__onActiveItemChanged);
}
}
Expand Down Expand Up @@ -187,6 +204,20 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
}
}

/** @private */
__onGridInteraction(e) {
if (e instanceof KeyboardEvent) {
this._shiftKeyDown = e.key !== 'Shift' && e.shiftKey;
} else {
this._shiftKeyDown = e.shiftKey;
}

if (this.autoSelect) {
// Prevent text selection when shift-clicking to select a range of items.
this._grid.$.scroller.toggleAttribute('range-selecting', this._shiftKeyDown);
}
}

/**
* Selects or deselects the row when the Select Row checkbox is switched.
* The listener handles only user-fired events.
Expand Down
18 changes: 18 additions & 0 deletions packages/grid/src/vaadin-grid-selection-column-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ export const GridSelectionColumnMixin = (superClass) =>
_selectItem(item) {
if (this._grid.__isItemSelectable(item)) {
this._grid.selectItem(item);
this._grid.dispatchEvent(
new CustomEvent('item-toggle', {
detail: {
item,
selected: true,
shiftKey: this._shiftKeyDown,
},
}),
);
}
}

Expand All @@ -127,6 +136,15 @@ export const GridSelectionColumnMixin = (superClass) =>
_deselectItem(item) {
if (this._grid.__isItemSelectable(item)) {
this._grid.deselectItem(item);
this._grid.dispatchEvent(
new CustomEvent('item-toggle', {
detail: {
item,
selected: false,
shiftKey: this._shiftKeyDown,
},
}),
);
}
}

Expand Down
10 changes: 10 additions & 0 deletions packages/grid/src/vaadin-grid-selection-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,14 @@ export const SelectionMixin = (superClass) =>
*
* @event selected-items-changed
*/

/**
* Fired when the user selects or deselects an item through the selection column.
*
* @event item-toggle
* @param {Object} detail
* @param {GridItem} detail.item the item that was selected or deselected
* @param {boolean} detail.selected true if the item was selected
* @param {boolean} detail.shiftKey true if the shift key was pressed
*/
};
3 changes: 2 additions & 1 deletion packages/grid/src/vaadin-grid-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,8 @@ export const gridStyles = css`
display: none;
}
#scroller[column-resizing] {
#scroller[column-resizing],
#scroller[range-selecting] {
-webkit-user-select: none;
user-select: none;
}
Expand Down
1 change: 1 addition & 0 deletions packages/grid/src/vaadin-grid.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ export type GridDefaultItem = any;
* @fires {CustomEvent} loading-changed - Fired when the `loading` property changes.
* @fires {CustomEvent} selected-items-changed - Fired when the `selectedItems` property changes.
* @fires {CustomEvent} size-changed - Fired when the `size` property changes.
* @fires {CustomEvent} item-toggle - Fired when the user selects or deselects an item through the selection column.
*/
declare class Grid<TItem = GridDefaultItem> extends HTMLElement {
addEventListener<K extends keyof GridEventMap<TItem>>(
Expand Down
155 changes: 154 additions & 1 deletion packages/grid/test/selection.common.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from '@vaadin/chai-plugins';
import { click, fixtureSync, listenOnce, mousedown, nextFrame, nextRender } from '@vaadin/testing-helpers';
import { sendKeys } from '@web/test-runner-commands';
import { resetMouse, sendKeys, sendMouse } from '@web/test-runner-commands';
import sinon from 'sinon';
import {
fire,
Expand Down Expand Up @@ -856,4 +856,157 @@ describe('multi selection column', () => {
expect(grid.$.table.scrollTop).to.be.eq(prevScrollTop);
});
});

describe('item-toggle event', () => {
let itemSelectionSpy, rows, checkboxes;

async function mouseClick(element) {
const { x, y, width, height } = element.getBoundingClientRect();
await sendMouse({
type: 'click',
position: [x + width / 2, y + height / 2].map(Math.floor),
});
}

function assertEvent(detail) {
expect(itemSelectionSpy).to.be.calledOnce;
expect(itemSelectionSpy.args[0][0].detail).to.eql(detail);
itemSelectionSpy.resetHistory();
}

beforeEach(async () => {
grid = fixtureSync(`
<vaadin-grid style="width: 200px; height: 450px;">
<vaadin-grid-selection-column></vaadin-grid-selection-column>
<vaadin-grid-column path="name"></vaadin-grid-column>
</vaadin-grid>
`);
grid.items = [{ name: 'Item 0' }, { name: 'Item 1' }, { name: 'Item 2' }];
await nextRender();

rows = getRows(grid.$.items);
checkboxes = [...grid.querySelectorAll('vaadin-checkbox[aria-label="Select Row"]')];

itemSelectionSpy = sinon.spy();
grid.addEventListener('item-toggle', itemSelectionSpy);
});

afterEach(async () => {
await resetMouse();
});

it('should fire the event when toggling an item with click', async () => {
await mouseClick(checkboxes[0]);
assertEvent({ item: grid.items[0], selected: true, shiftKey: false });

await mouseClick(checkboxes[0]);
assertEvent({ item: grid.items[0], selected: false, shiftKey: false });
});

it('should fire the event when toggling an item with Shift + click', async () => {
await sendKeys({ down: 'Shift' });
await mouseClick(checkboxes[0]);
await sendKeys({ up: 'Shift' });
assertEvent({ item: grid.items[0], selected: true, shiftKey: true });

await sendKeys({ down: 'Shift' });
await mouseClick(checkboxes[0]);
await sendKeys({ up: 'Shift' });
assertEvent({ item: grid.items[0], selected: false, shiftKey: true });
});

it('should fire the event when toggling an item with Space', async () => {
checkboxes[0].focus();

await sendKeys({ press: 'Space' });
assertEvent({ item: grid.items[0], selected: true, shiftKey: false });

await sendKeys({ press: 'Space' });
assertEvent({ item: grid.items[0], selected: false, shiftKey: false });
});

it('should fire the event when toggling an item with Shift + Space', async () => {
checkboxes[0].focus();

await sendKeys({ down: 'Shift' });
await sendKeys({ press: 'Space' });
await sendKeys({ up: 'Shift' });
assertEvent({ item: grid.items[0], selected: true, shiftKey: true });

await sendKeys({ down: 'Shift' });
await sendKeys({ press: 'Space' });
await sendKeys({ up: 'Shift' });
assertEvent({ item: grid.items[0], selected: false, shiftKey: true });
});

describe('autoSelect', () => {
beforeEach(() => {
const selectionColumn = grid.querySelector('vaadin-grid-selection-column');
selectionColumn.autoSelect = true;
});

it('should fire the event when toggling an item with click', async () => {
await mouseClick(rows[0]);
assertEvent({ item: grid.items[0], selected: true, shiftKey: false });

await mouseClick(rows[0]);
assertEvent({ item: grid.items[0], selected: false, shiftKey: false });
});

it('should fire the event when toggling an item with Shift + click', async () => {
await sendKeys({ down: 'Shift' });
await mouseClick(rows[0]);
await sendKeys({ up: 'Shift' });
assertEvent({ item: grid.items[0], selected: true, shiftKey: true });

await sendKeys({ down: 'Shift' });
await mouseClick(rows[0]);
await sendKeys({ up: 'Shift' });
assertEvent({ item: grid.items[0], selected: false, shiftKey: true });
});

it('should fire the event when toggling an item with Space', async () => {
getRowCells(rows[0])[1].focus();

await sendKeys({ press: 'Space' });
assertEvent({ item: grid.items[0], selected: true, shiftKey: false });

await sendKeys({ press: 'Space' });
assertEvent({ item: grid.items[0], selected: false, shiftKey: false });
});

it('should fire the event when toggling an item with Shift + Space', async () => {
getRowCells(rows[0])[1].focus();

await sendKeys({ down: 'Shift' });
await sendKeys({ press: 'Space' });
await sendKeys({ up: 'Shift' });
assertEvent({ item: grid.items[0], selected: true, shiftKey: true });

await sendKeys({ down: 'Shift' });
await sendKeys({ press: 'Space' });
await sendKeys({ up: 'Shift' });
assertEvent({ item: grid.items[0], selected: false, shiftKey: true });
});

it('should prevent text selection when selecting a range of items with Shift + click', async () => {
await mouseClick(rows[0]);
await sendKeys({ down: 'Shift' });
await mouseClick(rows[1]);
await sendKeys({ up: 'Shift' });
expect(document.getSelection().toString()).to.be.empty;
});

it('should allow text selection after selecting a range of items with Shift + click', async () => {
await mouseClick(rows[0]);
await sendKeys({ down: 'Shift' });
await mouseClick(rows[1]);
await sendKeys({ up: 'Shift' });

const row2CellContent1 = getBodyCellContent(grid, 2, 1);
document.getSelection().selectAllChildren(row2CellContent1);
expect(document.getSelection().toString()).to.be.not.empty;
});
});
});
});
8 changes: 8 additions & 0 deletions packages/grid/test/typings/grid.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import type {
GridExpandedItemsChangedEvent,
GridFilterDefinition,
GridItemModel,
GridItemToggleEvent,
GridLoadingChangedEvent,
GridRowDetailsRenderer,
GridSelectedItemsChangedEvent,
Expand Down Expand Up @@ -146,6 +147,13 @@ narrowedGrid.addEventListener('grid-drop', (event) => {
assertType<GridDropLocation>(event.detail.dropLocation);
});

narrowedGrid.addEventListener('item-toggle', (event) => {
assertType<GridItemToggleEvent<TestGridItem>>(event);
assertType<TestGridItem>(event.detail.item);
assertType<boolean>(event.detail.selected);
assertType<boolean>(event.detail.shiftKey);
});

narrowedGrid.dataProvider = (params, callback) => {
assertType<GridFilterDefinition[]>(params.filters);
assertType<number>(params.page);
Expand Down
8 changes: 8 additions & 0 deletions packages/grid/test/typings/lit-grid.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
GridFilter,
GridFilterColumn,
GridFilterValueChangedEvent,
GridItemToggleEvent,
GridSelectionColumn,
GridSelectionColumnSelectAllChangedEvent,
GridSortColumn,
Expand Down Expand Up @@ -146,6 +147,13 @@ narrowedGrid.addEventListener('grid-drop', (event) => {
assertType<GridDropLocation>(event.detail.dropLocation);
});

narrowedGrid.addEventListener('item-toggle', (event) => {
assertType<GridItemToggleEvent<TestGridItem>>(event);
assertType<TestGridItem>(event.detail.item);
assertType<boolean>(event.detail.selected);
assertType<boolean>(event.detail.shiftKey);
});

narrowedGrid.dataProvider = (params, callback) => {
assertType<GridFilterDefinition[]>(params.filters);
assertType<number>(params.page);
Expand Down

0 comments on commit 40e49c5

Please sign in to comment.