Skip to content

Commit 40e49c5

Browse files
authored
feat: add item-toggle event to notify when user toggles an item (#8231)
1 parent 2deab65 commit 40e49c5

10 files changed

+244
-2
lines changed

packages/grid/src/vaadin-grid-mixin.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ export type GridDataProviderChangedEvent<TItem> = CustomEvent<{ value: GridDataP
9898
*/
9999
export type GridExpandedItemsChangedEvent<TItem> = CustomEvent<{ value: TItem[] }>;
100100

101+
/**
102+
* Fired when the user selects or deselects an item through the selection column.
103+
*/
104+
export type GridItemToggleEvent<TItem> = CustomEvent<{ item: TItem; selected: boolean; shiftKey: boolean }>;
105+
101106
/**
102107
* Fired when starting to drag grid rows.
103108
*/
@@ -157,6 +162,8 @@ export interface GridCustomEventMap<TItem> {
157162
'selected-items-changed': GridSelectedItemsChangedEvent<TItem>;
158163

159164
'size-changed': GridSizeChangedEvent;
165+
166+
'item-toggle': GridItemToggleEvent<TItem>;
160167
}
161168

162169
export interface GridEventMap<TItem> extends HTMLElementEventMap, GridCustomEventMap<TItem> {}

packages/grid/src/vaadin-grid-selection-column-base-mixin.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ export declare class GridSelectionColumnBaseMixinClass<TItem> {
3939
*/
4040
dragSelect: boolean;
4141

42+
/**
43+
* Indicates whether the shift key is currently pressed.
44+
*/
45+
protected _shiftKeyDown: boolean;
46+
4247
/**
4348
* Override to handle the user selecting all items.
4449
*/

packages/grid/src/vaadin-grid-selection-column-base-mixin.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,16 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
9292

9393
/** @protected */
9494
_selectAllHidden: Boolean,
95+
96+
/**
97+
* Indicates whether the shift key is currently pressed.
98+
*
99+
* @protected
100+
*/
101+
_shiftKeyDown: {
102+
type: Boolean,
103+
value: false,
104+
},
95105
};
96106
}
97107

@@ -106,6 +116,7 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
106116
this.__onCellTrack = this.__onCellTrack.bind(this);
107117
this.__onCellClick = this.__onCellClick.bind(this);
108118
this.__onCellMouseDown = this.__onCellMouseDown.bind(this);
119+
this.__onGridInteraction = this.__onGridInteraction.bind(this);
109120
this.__onActiveItemChanged = this.__onActiveItemChanged.bind(this);
110121
this.__onSelectRowCheckboxChange = this.__onSelectRowCheckboxChange.bind(this);
111122
this.__onSelectAllCheckboxChange = this.__onSelectAllCheckboxChange.bind(this);
@@ -115,6 +126,9 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
115126
connectedCallback() {
116127
super.connectedCallback();
117128
if (this._grid) {
129+
this._grid.addEventListener('keyup', this.__onGridInteraction);
130+
this._grid.addEventListener('keydown', this.__onGridInteraction, { capture: true });
131+
this._grid.addEventListener('mousedown', this.__onGridInteraction);
118132
this._grid.addEventListener('active-item-changed', this.__onActiveItemChanged);
119133
}
120134
}
@@ -123,6 +137,9 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
123137
disconnectedCallback() {
124138
super.disconnectedCallback();
125139
if (this._grid) {
140+
this._grid.removeEventListener('keyup', this.__onGridInteraction);
141+
this._grid.removeEventListener('keydown', this.__onGridInteraction, { capture: true });
142+
this._grid.removeEventListener('mousedown', this.__onGridInteraction);
126143
this._grid.removeEventListener('active-item-changed', this.__onActiveItemChanged);
127144
}
128145
}
@@ -187,6 +204,20 @@ export const GridSelectionColumnBaseMixin = (superClass) =>
187204
}
188205
}
189206

207+
/** @private */
208+
__onGridInteraction(e) {
209+
if (e instanceof KeyboardEvent) {
210+
this._shiftKeyDown = e.key !== 'Shift' && e.shiftKey;
211+
} else {
212+
this._shiftKeyDown = e.shiftKey;
213+
}
214+
215+
if (this.autoSelect) {
216+
// Prevent text selection when shift-clicking to select a range of items.
217+
this._grid.$.scroller.toggleAttribute('range-selecting', this._shiftKeyDown);
218+
}
219+
}
220+
190221
/**
191222
* Selects or deselects the row when the Select Row checkbox is switched.
192223
* The listener handles only user-fired events.

packages/grid/src/vaadin-grid-selection-column-mixin.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,15 @@ export const GridSelectionColumnMixin = (superClass) =>
113113
_selectItem(item) {
114114
if (this._grid.__isItemSelectable(item)) {
115115
this._grid.selectItem(item);
116+
this._grid.dispatchEvent(
117+
new CustomEvent('item-toggle', {
118+
detail: {
119+
item,
120+
selected: true,
121+
shiftKey: this._shiftKeyDown,
122+
},
123+
}),
124+
);
116125
}
117126
}
118127

@@ -127,6 +136,15 @@ export const GridSelectionColumnMixin = (superClass) =>
127136
_deselectItem(item) {
128137
if (this._grid.__isItemSelectable(item)) {
129138
this._grid.deselectItem(item);
139+
this._grid.dispatchEvent(
140+
new CustomEvent('item-toggle', {
141+
detail: {
142+
item,
143+
selected: false,
144+
shiftKey: this._shiftKeyDown,
145+
},
146+
}),
147+
);
130148
}
131149
}
132150

packages/grid/src/vaadin-grid-selection-mixin.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,14 @@ export const SelectionMixin = (superClass) =>
130130
*
131131
* @event selected-items-changed
132132
*/
133+
134+
/**
135+
* Fired when the user selects or deselects an item through the selection column.
136+
*
137+
* @event item-toggle
138+
* @param {Object} detail
139+
* @param {GridItem} detail.item the item that was selected or deselected
140+
* @param {boolean} detail.selected true if the item was selected
141+
* @param {boolean} detail.shiftKey true if the shift key was pressed
142+
*/
133143
};

packages/grid/src/vaadin-grid-styles.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,8 @@ export const gridStyles = css`
292292
display: none;
293293
}
294294
295-
#scroller[column-resizing] {
295+
#scroller[column-resizing],
296+
#scroller[range-selecting] {
296297
-webkit-user-select: none;
297298
user-select: none;
298299
}

packages/grid/src/vaadin-grid.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ export type GridDefaultItem = any;
256256
* @fires {CustomEvent} loading-changed - Fired when the `loading` property changes.
257257
* @fires {CustomEvent} selected-items-changed - Fired when the `selectedItems` property changes.
258258
* @fires {CustomEvent} size-changed - Fired when the `size` property changes.
259+
* @fires {CustomEvent} item-toggle - Fired when the user selects or deselects an item through the selection column.
259260
*/
260261
declare class Grid<TItem = GridDefaultItem> extends HTMLElement {
261262
addEventListener<K extends keyof GridEventMap<TItem>>(

packages/grid/test/selection.common.js

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from '@vaadin/chai-plugins';
22
import { click, fixtureSync, listenOnce, mousedown, nextFrame, nextRender } from '@vaadin/testing-helpers';
3-
import { sendKeys } from '@web/test-runner-commands';
3+
import { resetMouse, sendKeys, sendMouse } from '@web/test-runner-commands';
44
import sinon from 'sinon';
55
import {
66
fire,
@@ -856,4 +856,157 @@ describe('multi selection column', () => {
856856
expect(grid.$.table.scrollTop).to.be.eq(prevScrollTop);
857857
});
858858
});
859+
860+
describe('item-toggle event', () => {
861+
let itemSelectionSpy, rows, checkboxes;
862+
863+
async function mouseClick(element) {
864+
const { x, y, width, height } = element.getBoundingClientRect();
865+
await sendMouse({
866+
type: 'click',
867+
position: [x + width / 2, y + height / 2].map(Math.floor),
868+
});
869+
}
870+
871+
function assertEvent(detail) {
872+
expect(itemSelectionSpy).to.be.calledOnce;
873+
expect(itemSelectionSpy.args[0][0].detail).to.eql(detail);
874+
itemSelectionSpy.resetHistory();
875+
}
876+
877+
beforeEach(async () => {
878+
grid = fixtureSync(`
879+
<vaadin-grid style="width: 200px; height: 450px;">
880+
<vaadin-grid-selection-column></vaadin-grid-selection-column>
881+
<vaadin-grid-column path="name"></vaadin-grid-column>
882+
</vaadin-grid>
883+
`);
884+
grid.items = [{ name: 'Item 0' }, { name: 'Item 1' }, { name: 'Item 2' }];
885+
await nextRender();
886+
887+
rows = getRows(grid.$.items);
888+
checkboxes = [...grid.querySelectorAll('vaadin-checkbox[aria-label="Select Row"]')];
889+
890+
itemSelectionSpy = sinon.spy();
891+
grid.addEventListener('item-toggle', itemSelectionSpy);
892+
});
893+
894+
afterEach(async () => {
895+
await resetMouse();
896+
});
897+
898+
it('should fire the event when toggling an item with click', async () => {
899+
await mouseClick(checkboxes[0]);
900+
assertEvent({ item: grid.items[0], selected: true, shiftKey: false });
901+
902+
await mouseClick(checkboxes[0]);
903+
assertEvent({ item: grid.items[0], selected: false, shiftKey: false });
904+
});
905+
906+
it('should fire the event when toggling an item with Shift + click', async () => {
907+
await sendKeys({ down: 'Shift' });
908+
await mouseClick(checkboxes[0]);
909+
await sendKeys({ up: 'Shift' });
910+
assertEvent({ item: grid.items[0], selected: true, shiftKey: true });
911+
912+
await sendKeys({ down: 'Shift' });
913+
await mouseClick(checkboxes[0]);
914+
await sendKeys({ up: 'Shift' });
915+
assertEvent({ item: grid.items[0], selected: false, shiftKey: true });
916+
});
917+
918+
it('should fire the event when toggling an item with Space', async () => {
919+
checkboxes[0].focus();
920+
921+
await sendKeys({ press: 'Space' });
922+
assertEvent({ item: grid.items[0], selected: true, shiftKey: false });
923+
924+
await sendKeys({ press: 'Space' });
925+
assertEvent({ item: grid.items[0], selected: false, shiftKey: false });
926+
});
927+
928+
it('should fire the event when toggling an item with Shift + Space', async () => {
929+
checkboxes[0].focus();
930+
931+
await sendKeys({ down: 'Shift' });
932+
await sendKeys({ press: 'Space' });
933+
await sendKeys({ up: 'Shift' });
934+
assertEvent({ item: grid.items[0], selected: true, shiftKey: true });
935+
936+
await sendKeys({ down: 'Shift' });
937+
await sendKeys({ press: 'Space' });
938+
await sendKeys({ up: 'Shift' });
939+
assertEvent({ item: grid.items[0], selected: false, shiftKey: true });
940+
});
941+
942+
describe('autoSelect', () => {
943+
beforeEach(() => {
944+
const selectionColumn = grid.querySelector('vaadin-grid-selection-column');
945+
selectionColumn.autoSelect = true;
946+
});
947+
948+
it('should fire the event when toggling an item with click', async () => {
949+
await mouseClick(rows[0]);
950+
assertEvent({ item: grid.items[0], selected: true, shiftKey: false });
951+
952+
await mouseClick(rows[0]);
953+
assertEvent({ item: grid.items[0], selected: false, shiftKey: false });
954+
});
955+
956+
it('should fire the event when toggling an item with Shift + click', async () => {
957+
await sendKeys({ down: 'Shift' });
958+
await mouseClick(rows[0]);
959+
await sendKeys({ up: 'Shift' });
960+
assertEvent({ item: grid.items[0], selected: true, shiftKey: true });
961+
962+
await sendKeys({ down: 'Shift' });
963+
await mouseClick(rows[0]);
964+
await sendKeys({ up: 'Shift' });
965+
assertEvent({ item: grid.items[0], selected: false, shiftKey: true });
966+
});
967+
968+
it('should fire the event when toggling an item with Space', async () => {
969+
getRowCells(rows[0])[1].focus();
970+
971+
await sendKeys({ press: 'Space' });
972+
assertEvent({ item: grid.items[0], selected: true, shiftKey: false });
973+
974+
await sendKeys({ press: 'Space' });
975+
assertEvent({ item: grid.items[0], selected: false, shiftKey: false });
976+
});
977+
978+
it('should fire the event when toggling an item with Shift + Space', async () => {
979+
getRowCells(rows[0])[1].focus();
980+
981+
await sendKeys({ down: 'Shift' });
982+
await sendKeys({ press: 'Space' });
983+
await sendKeys({ up: 'Shift' });
984+
assertEvent({ item: grid.items[0], selected: true, shiftKey: true });
985+
986+
await sendKeys({ down: 'Shift' });
987+
await sendKeys({ press: 'Space' });
988+
await sendKeys({ up: 'Shift' });
989+
assertEvent({ item: grid.items[0], selected: false, shiftKey: true });
990+
});
991+
992+
it('should prevent text selection when selecting a range of items with Shift + click', async () => {
993+
await mouseClick(rows[0]);
994+
await sendKeys({ down: 'Shift' });
995+
await mouseClick(rows[1]);
996+
await sendKeys({ up: 'Shift' });
997+
expect(document.getSelection().toString()).to.be.empty;
998+
});
999+
1000+
it('should allow text selection after selecting a range of items with Shift + click', async () => {
1001+
await mouseClick(rows[0]);
1002+
await sendKeys({ down: 'Shift' });
1003+
await mouseClick(rows[1]);
1004+
await sendKeys({ up: 'Shift' });
1005+
1006+
const row2CellContent1 = getBodyCellContent(grid, 2, 1);
1007+
document.getSelection().selectAllChildren(row2CellContent1);
1008+
expect(document.getSelection().toString()).to.be.not.empty;
1009+
});
1010+
});
1011+
});
8591012
});

packages/grid/test/typings/grid.types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import type {
5050
GridExpandedItemsChangedEvent,
5151
GridFilterDefinition,
5252
GridItemModel,
53+
GridItemToggleEvent,
5354
GridLoadingChangedEvent,
5455
GridRowDetailsRenderer,
5556
GridSelectedItemsChangedEvent,
@@ -146,6 +147,13 @@ narrowedGrid.addEventListener('grid-drop', (event) => {
146147
assertType<GridDropLocation>(event.detail.dropLocation);
147148
});
148149

150+
narrowedGrid.addEventListener('item-toggle', (event) => {
151+
assertType<GridItemToggleEvent<TestGridItem>>(event);
152+
assertType<TestGridItem>(event.detail.item);
153+
assertType<boolean>(event.detail.selected);
154+
assertType<boolean>(event.detail.shiftKey);
155+
});
156+
149157
narrowedGrid.dataProvider = (params, callback) => {
150158
assertType<GridFilterDefinition[]>(params.filters);
151159
assertType<number>(params.page);

packages/grid/test/typings/lit-grid.types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
GridFilter,
77
GridFilterColumn,
88
GridFilterValueChangedEvent,
9+
GridItemToggleEvent,
910
GridSelectionColumn,
1011
GridSelectionColumnSelectAllChangedEvent,
1112
GridSortColumn,
@@ -146,6 +147,13 @@ narrowedGrid.addEventListener('grid-drop', (event) => {
146147
assertType<GridDropLocation>(event.detail.dropLocation);
147148
});
148149

150+
narrowedGrid.addEventListener('item-toggle', (event) => {
151+
assertType<GridItemToggleEvent<TestGridItem>>(event);
152+
assertType<TestGridItem>(event.detail.item);
153+
assertType<boolean>(event.detail.selected);
154+
assertType<boolean>(event.detail.shiftKey);
155+
});
156+
149157
narrowedGrid.dataProvider = (params, callback) => {
150158
assertType<GridFilterDefinition[]>(params.filters);
151159
assertType<number>(params.page);

0 commit comments

Comments
 (0)