@progress/kendo-angular-listbox 21.0.0-develop.2 → 21.0.0-develop.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,52 @@
1
+ /**-----------------------------------------------------------------------------------------
2
+ * Copyright © 2025 Progress Software Corporation. All rights reserved.
3
+ * Licensed under commercial license. See LICENSE.md in the project root for more information
4
+ *-------------------------------------------------------------------------------------------*/
5
+ "use strict";
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || function (mod) {
23
+ if (mod && mod.__esModule) return mod;
24
+ var result = {};
25
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
26
+ __setModuleDefault(result, mod);
27
+ return result;
28
+ };
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.default = default_1;
31
+ const utils_1 = require("../utils");
32
+ const fs = __importStar(require("fs"));
33
+ const index_1 = require("../template-transformer/index");
34
+ function default_1(fileInfo, api) {
35
+ const filePath = fileInfo.path;
36
+ if (filePath.endsWith('.html')) {
37
+ if ((0, utils_1.hasKendoInTemplate)(fileInfo.source)) {
38
+ let updatedContent = fileInfo.source;
39
+ updatedContent = (0, utils_1.htmlEventTransformer)({ ...fileInfo, source: updatedContent }, 'kendo-listbox', 'actionClick', 'action');
40
+ // Only write to file once after all transformations
41
+ fs.writeFileSync(filePath, updatedContent, 'utf-8');
42
+ }
43
+ return;
44
+ }
45
+ const j = api.jscodeshift;
46
+ const rootSource = j(fileInfo.source);
47
+ (0, index_1.templateTransformer)(rootSource, j, (root) => {
48
+ (0, utils_1.templateEventTransformer)(root, 'kendo-listbox', 'actionClick', 'action');
49
+ });
50
+ (0, utils_1.tsPropertyTransformer)(fileInfo.source, rootSource, j, 'ListBoxComponent', 'actionClick', 'action');
51
+ return rootSource.toSource();
52
+ }
@@ -0,0 +1,125 @@
1
+ /**-----------------------------------------------------------------------------------------
2
+ * Copyright © 2025 Progress Software Corporation. All rights reserved.
3
+ * Licensed under commercial license. See LICENSE.md in the project root for more information
4
+ *-------------------------------------------------------------------------------------------*/
5
+ "use strict";
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.default = default_1;
8
+ function default_1(fileInfo, api) {
9
+ const filePath = fileInfo.path;
10
+ if (filePath.endsWith('.html')) {
11
+ // HTML transformations not needed for this migration
12
+ return;
13
+ }
14
+ const j = api.jscodeshift;
15
+ const rootSource = j(fileInfo.source);
16
+ tsSelectedIndexTransformer(fileInfo.source, rootSource, j, 'ListBoxComponent');
17
+ return rootSource.toSource();
18
+ }
19
+ /**
20
+ * Custom transformer to convert selectedIndex property access to selectedIndices[0]
21
+ * for ListBoxComponent instances
22
+ */
23
+ const tsSelectedIndexTransformer = (source, root, j, componentType) => {
24
+ if (source.includes(componentType)) {
25
+ // Find all class properties that are of type ListBoxComponent
26
+ const properties = new Set();
27
+ // Find properties with type annotations
28
+ root.find(j.ClassProperty, {
29
+ typeAnnotation: {
30
+ typeAnnotation: {
31
+ typeName: {
32
+ name: componentType,
33
+ },
34
+ },
35
+ },
36
+ }).forEach((path) => {
37
+ if (path.node.key.type === 'Identifier') {
38
+ properties.add(path.node.key.name);
39
+ }
40
+ });
41
+ // Find function parameters of type componentType
42
+ const parameters = new Set();
43
+ root.find(j.FunctionDeclaration).forEach((path) => {
44
+ if (path.node.params) {
45
+ path.node.params.forEach((param) => {
46
+ if (param.type === 'Identifier' &&
47
+ param.typeAnnotation &&
48
+ param.typeAnnotation.typeAnnotation?.type === 'TSTypeReference' &&
49
+ param.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
50
+ param.typeAnnotation.typeAnnotation.typeName.name === componentType) {
51
+ parameters.add(param.name);
52
+ }
53
+ });
54
+ }
55
+ });
56
+ // Also check method declarations in classes
57
+ root.find(j.ClassMethod).forEach((path) => {
58
+ if (path.node.params) {
59
+ path.node.params.forEach((param) => {
60
+ if (param.type === 'Identifier' &&
61
+ param.typeAnnotation &&
62
+ param.typeAnnotation.typeAnnotation?.type === 'TSTypeReference' &&
63
+ param.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
64
+ param.typeAnnotation.typeAnnotation.typeName.name === componentType) {
65
+ parameters.add(param.name);
66
+ }
67
+ });
68
+ }
69
+ });
70
+ // Also check arrow functions
71
+ root.find(j.ArrowFunctionExpression).forEach((path) => {
72
+ if (path.node.params) {
73
+ path.node.params.forEach((param) => {
74
+ if (param.type === 'Identifier' &&
75
+ param.typeAnnotation &&
76
+ param.typeAnnotation.typeAnnotation?.type === 'TSTypeReference' &&
77
+ param.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
78
+ param.typeAnnotation.typeAnnotation.typeName.name === componentType) {
79
+ parameters.add(param.name);
80
+ }
81
+ });
82
+ }
83
+ });
84
+ // Find local variable declarations of type componentType
85
+ const localVariables = new Set();
86
+ root.find(j.VariableDeclarator).forEach((path) => {
87
+ if (path.node.id.type === 'Identifier' &&
88
+ path.node.id.typeAnnotation &&
89
+ path.node.id.typeAnnotation.typeAnnotation?.type === 'TSTypeReference' &&
90
+ path.node.id.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
91
+ path.node.id.typeAnnotation.typeAnnotation.typeName.name === componentType) {
92
+ localVariables.add(path.node.id.name);
93
+ }
94
+ });
95
+ // Find all member expressions where selectedIndex property is accessed on any ListBoxComponent instance
96
+ root.find(j.MemberExpression, {
97
+ property: {
98
+ type: 'Identifier',
99
+ name: 'selectedIndex',
100
+ },
101
+ })
102
+ .filter((path) => {
103
+ // Filter to only include accesses on properties that are ListBoxComponent instances
104
+ if (path.node.object.type === 'MemberExpression' && path.node.object.property.type === 'Identifier') {
105
+ // handle properties of this
106
+ if (path.node.object.object.type === 'ThisExpression' &&
107
+ properties.has(path.node.object.property.name)) {
108
+ return true;
109
+ }
110
+ }
111
+ // Handle function parameters and local variables
112
+ if (path.node.object.type === 'Identifier') {
113
+ return parameters.has(path.node.object.name) || localVariables.has(path.node.object.name);
114
+ }
115
+ return false;
116
+ })
117
+ .forEach((path) => {
118
+ // Replace selectedIndex with selectedIndices[0]
119
+ const memberExpression = j.memberExpression(j.memberExpression(path.node.object, j.identifier('selectedIndices')), j.numericLiteral(0), true // computed property access (uses brackets)
120
+ );
121
+ // Replace the entire member expression
122
+ j(path).replaceWith(memberExpression);
123
+ });
124
+ }
125
+ };
@@ -0,0 +1,14 @@
1
+ /**-----------------------------------------------------------------------------------------
2
+ * Copyright © 2025 Progress Software Corporation. All rights reserved.
3
+ * Licensed under commercial license. See LICENSE.md in the project root for more information
4
+ *-------------------------------------------------------------------------------------------*/
5
+ "use strict";
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.default = default_1;
8
+ const utils_1 = require("../utils");
9
+ function default_1(fileInfo, api) {
10
+ const j = api.jscodeshift;
11
+ const rootSource = j(fileInfo.source);
12
+ (0, utils_1.tsInterfaceTransformer)(fileInfo, rootSource, j, '@progress/kendo-angular-listbox', 'Toolbar', 'ListBoxToolbarConfig');
13
+ return rootSource.toSource();
14
+ }
@@ -2,7 +2,7 @@
2
2
  * Copyright © 2025 Progress Software Corporation. All rights reserved.
3
3
  * Licensed under commercial license. See LICENSE.md in the project root for more information
4
4
  *-------------------------------------------------------------------------------------------*/
5
- import { OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
5
+ import { NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
6
6
  import { ListBoxComponent } from './listbox.component';
7
7
  import * as i0 from "@angular/core";
8
8
  /**
@@ -29,16 +29,17 @@ import * as i0 from "@angular/core";
29
29
  */
30
30
  export declare class DataBindingDirective implements OnChanges, OnDestroy {
31
31
  private listbox;
32
+ private zone;
32
33
  /**
33
34
  * Specifies the `ListBoxComponent` instance with which the current ListBox connects.
34
35
  * When you link two listboxes through this input, you can transfer items between them.
35
36
  */
36
37
  connectedWith: ListBoxComponent;
37
- private actionClickSub;
38
+ private actionSub;
38
39
  private selectedBoxSub;
39
40
  private connectedWithSub;
40
41
  private selectedBox;
41
- constructor(listbox: ListBoxComponent);
42
+ constructor(listbox: ListBoxComponent, zone: NgZone);
42
43
  /**
43
44
  * @hidden
44
45
  */
@@ -48,8 +49,8 @@ export declare class DataBindingDirective implements OnChanges, OnDestroy {
48
49
  */
49
50
  ngOnDestroy(): void;
50
51
  private moveVertically;
51
- private removeItem;
52
- private transferItem;
52
+ private removeSelectedItems;
53
+ private transferSelectedItems;
53
54
  private transferAll;
54
55
  static ɵfac: i0.ɵɵFactoryDeclaration<DataBindingDirective, never>;
55
56
  static ɵdir: i0.ɵɵDirectiveDeclaration<DataBindingDirective, "[kendoListBoxDataBinding]", never, { "connectedWith": { "alias": "connectedWith"; "required": false; }; }, {}, never, never, true, never>;
@@ -2,9 +2,10 @@
2
2
  * Copyright © 2025 Progress Software Corporation. All rights reserved.
3
3
  * Licensed under commercial license. See LICENSE.md in the project root for more information
4
4
  *-------------------------------------------------------------------------------------------*/
5
- import { Directive, Input } from '@angular/core';
5
+ import { Directive, Input, NgZone } from '@angular/core';
6
6
  import { isChanged } from '@progress/kendo-angular-common';
7
7
  import { Subscription } from 'rxjs';
8
+ import { take } from 'rxjs/operators';
8
9
  import { ListBoxComponent } from './listbox.component';
9
10
  import { isPresent } from './util';
10
11
  import * as i0 from "@angular/core";
@@ -33,22 +34,24 @@ import * as i1 from "./listbox.component";
33
34
  */
34
35
  export class DataBindingDirective {
35
36
  listbox;
37
+ zone;
36
38
  /**
37
39
  * Specifies the `ListBoxComponent` instance with which the current ListBox connects.
38
40
  * When you link two listboxes through this input, you can transfer items between them.
39
41
  */
40
42
  connectedWith;
41
- actionClickSub = new Subscription();
43
+ actionSub = new Subscription();
42
44
  selectedBoxSub = new Subscription();
43
45
  connectedWithSub = new Subscription();
44
46
  selectedBox;
45
- constructor(listbox) {
47
+ constructor(listbox, zone) {
46
48
  this.listbox = listbox;
49
+ this.zone = zone;
47
50
  this.selectedBox = this.listbox;
48
51
  this.connectedWithSub.add(this.listbox.getChildListbox.subscribe(() => {
49
52
  this.listbox.childListbox = this.connectedWith;
50
53
  }));
51
- this.actionClickSub.add(this.listbox.actionClick.subscribe((actionName) => {
54
+ this.actionSub.add(this.listbox.action.subscribe((actionName) => {
52
55
  switch (actionName) {
53
56
  case 'moveUp': {
54
57
  this.moveVertically('up');
@@ -59,11 +62,11 @@ export class DataBindingDirective {
59
62
  break;
60
63
  }
61
64
  case 'transferFrom': {
62
- this.transferItem(this.connectedWith, this.listbox);
65
+ this.transferSelectedItems(this.connectedWith, this.listbox);
63
66
  break;
64
67
  }
65
68
  case 'transferTo': {
66
- this.transferItem(this.listbox, this.connectedWith);
69
+ this.transferSelectedItems(this.listbox, this.connectedWith);
67
70
  break;
68
71
  }
69
72
  case 'transferAllTo': {
@@ -75,7 +78,7 @@ export class DataBindingDirective {
75
78
  break;
76
79
  }
77
80
  case 'remove': {
78
- this.removeItem();
81
+ this.removeSelectedItems();
79
82
  break;
80
83
  }
81
84
  default: {
@@ -107,9 +110,9 @@ export class DataBindingDirective {
107
110
  * @hidden
108
111
  */
109
112
  ngOnDestroy() {
110
- if (this.actionClickSub) {
111
- this.actionClickSub.unsubscribe();
112
- this.actionClickSub = null;
113
+ if (this.actionSub) {
114
+ this.actionSub.unsubscribe();
115
+ this.actionSub = null;
113
116
  }
114
117
  if (this.selectedBoxSub) {
115
118
  this.selectedBoxSub.unsubscribe();
@@ -117,38 +120,73 @@ export class DataBindingDirective {
117
120
  }
118
121
  }
119
122
  moveVertically(dir) {
120
- const index = this.selectedBox.selectedIndex;
121
- if (!isPresent(index)) {
123
+ const selectedIndices = this.selectedBox.selectedIndices;
124
+ if (!isPresent(selectedIndices) || selectedIndices.length === 0) {
122
125
  return;
123
126
  }
124
- const topReached = dir === 'up' && index <= 0;
125
- const bottomReached = dir === 'down' && index >= this.selectedBox.data.length - 1;
127
+ const sortedIndices = [...selectedIndices].sort((a, b) => a - b);
128
+ const topIndex = sortedIndices[0];
129
+ const bottomIndex = sortedIndices[sortedIndices.length - 1];
130
+ const topReached = dir === 'up' && topIndex <= 0;
131
+ const bottomReached = dir === 'down' && bottomIndex >= this.selectedBox.data.length - 1;
126
132
  if (topReached || bottomReached) {
127
133
  return;
128
134
  }
129
- const newIndex = dir === 'up' ? index - 1 : index + 1;
135
+ const data = this.selectedBox.data;
136
+ const newSelectedIndices = [];
137
+ if (dir === 'up') {
138
+ for (const index of sortedIndices) {
139
+ const newIndex = index - 1;
140
+ [data[newIndex], data[index]] = [data[index], data[newIndex]];
141
+ newSelectedIndices.push(newIndex);
142
+ }
143
+ }
144
+ else {
145
+ for (let i = sortedIndices.length - 1; i >= 0; i--) {
146
+ const index = sortedIndices[i];
147
+ const newIndex = index + 1;
148
+ [data[newIndex], data[index]] = [data[index], data[newIndex]];
149
+ newSelectedIndices.push(newIndex);
150
+ }
151
+ }
152
+ newSelectedIndices.sort((a, b) => a - b);
153
+ this.selectedBox.selectionService.setSelectedIndices(newSelectedIndices);
130
154
  const navigation = this.selectedBox.keyboardNavigationService;
131
- navigation.focusedListboxItemIndex = navigation.selectedListboxItemIndex = newIndex;
132
- [this.selectedBox.data[newIndex], this.selectedBox.data[index]] = [this.selectedBox.data[index], this.selectedBox.data[newIndex]];
133
- this.selectedBox.selectionService.select(newIndex);
155
+ const currentFocusedIndex = navigation.focusedListboxItemIndex;
156
+ const focusedItemIndexInSelection = sortedIndices.indexOf(currentFocusedIndex);
157
+ let newFocusIndex;
158
+ if (focusedItemIndexInSelection !== -1) {
159
+ newFocusIndex = newSelectedIndices[focusedItemIndexInSelection];
160
+ }
161
+ else {
162
+ newFocusIndex = dir === 'up' ? topIndex - 1 : bottomIndex + 1;
163
+ }
164
+ this.zone.onStable.pipe(take(1)).subscribe(() => {
165
+ const listboxItems = this.selectedBox.listboxItems.toArray();
166
+ const previousItem = listboxItems[currentFocusedIndex]?.nativeElement;
167
+ const currentItem = listboxItems[newFocusIndex]?.nativeElement;
168
+ navigation.changeTabindex(previousItem, currentItem);
169
+ navigation.focusedListboxItemIndex = newFocusIndex;
170
+ navigation.selectedListboxItemIndex = newFocusIndex;
171
+ });
134
172
  }
135
- removeItem() {
136
- const index = this.selectedBox.selectedIndex;
137
- if (!isPresent(index)) {
173
+ removeSelectedItems() {
174
+ const itemIndices = this.selectedBox.selectedIndices;
175
+ if (!isPresent(itemIndices) || itemIndices.length === 0) {
138
176
  return;
139
177
  }
140
- this.selectedBox.data.splice(index, 1);
178
+ this.selectedBox.data = this.selectedBox.data.filter((_, index) => !itemIndices.includes(index));
141
179
  this.selectedBox.selectionService.clearSelection();
142
180
  }
143
- transferItem(source, target) {
144
- const item = source && source.data[source.selectedIndex];
145
- if (!item || !target || !source) {
181
+ transferSelectedItems(source, target) {
182
+ const selectedIndices = source?.data && source?.selectedIndices;
183
+ if (!target || !source || !isPresent(selectedIndices) || selectedIndices.length === 0) {
146
184
  return;
147
185
  }
148
- target.data.push(item);
149
- source.data.splice(source.selectedIndex, 1);
186
+ target.data.push(...selectedIndices.map(index => source.data[index]));
187
+ source.data = source.data.filter((_, index) => !selectedIndices.includes(index));
150
188
  source.clearSelection();
151
- target.selectItem(target.data.length - 1);
189
+ target.select([target.data.length - 1]);
152
190
  this.selectedBox = target;
153
191
  }
154
192
  transferAll(source, target) {
@@ -156,10 +194,10 @@ export class DataBindingDirective {
156
194
  return;
157
195
  }
158
196
  target.data.splice(target.data.length, 0, ...source.data.splice(0, source.data.length));
159
- target.selectItem(target.data.length - 1);
197
+ target.select([target.data.length - 1]);
160
198
  this.selectedBox = target;
161
199
  }
162
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DataBindingDirective, deps: [{ token: i1.ListBoxComponent }], target: i0.ɵɵFactoryTarget.Directive });
200
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DataBindingDirective, deps: [{ token: i1.ListBoxComponent }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Directive });
163
201
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: DataBindingDirective, isStandalone: true, selector: "[kendoListBoxDataBinding]", inputs: { connectedWith: "connectedWith" }, usesOnChanges: true, ngImport: i0 });
164
202
  }
165
203
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DataBindingDirective, decorators: [{
@@ -168,6 +206,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
168
206
  selector: '[kendoListBoxDataBinding]',
169
207
  standalone: true
170
208
  }]
171
- }], ctorParameters: () => [{ type: i1.ListBoxComponent }], propDecorators: { connectedWith: [{
209
+ }], ctorParameters: () => [{ type: i1.ListBoxComponent }, { type: i0.NgZone }], propDecorators: { connectedWith: [{
172
210
  type: Input
173
211
  }] } });
@@ -20,7 +20,12 @@ export class ItemSelectableDirective {
20
20
  }
21
21
  onClick(event) {
22
22
  event.stopPropagation();
23
- this.selectionService.select(this.index);
23
+ const ctrlKey = event.ctrlKey || event.metaKey;
24
+ const shiftKey = event.shiftKey;
25
+ if (shiftKey) {
26
+ event.preventDefault();
27
+ }
28
+ this.selectionService.select(this.index, ctrlKey, shiftKey);
24
29
  }
25
30
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ItemSelectableDirective, deps: [{ token: i1.ListBoxSelectionService }], target: i0.ɵɵFactoryTarget.Directive });
26
31
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: ItemSelectableDirective, isStandalone: true, selector: "[kendoListBoxItemSelectable]", inputs: { index: "index" }, host: { listeners: { "mousedown": "onClick($event)" }, properties: { "class.k-selected": "this.selectedClassName" } }, ngImport: i0 });
@@ -22,6 +22,8 @@ export class KeyboardNavigationService {
22
22
  onTransferAllEvent = new EventEmitter();
23
23
  onShiftSelectedItem = new EventEmitter();
24
24
  onSelectionChange = new EventEmitter();
25
+ onSelectAll = new EventEmitter();
26
+ onSelectToEnd = new EventEmitter();
25
27
  constructor(renderer, zone) {
26
28
  this.renderer = renderer;
27
29
  this.zone = zone;
@@ -48,6 +50,17 @@ export class KeyboardNavigationService {
48
50
  this.onDeleteEvent.emit(this.selectedListboxItemIndex);
49
51
  }
50
52
  }
53
+ if (ctrlOrMetaKey && (event.key === 'a' || event.key === 'A')) {
54
+ event.preventDefault();
55
+ this.onSelectAll.emit();
56
+ return;
57
+ }
58
+ if (ctrlOrMetaKey && event.shiftKey && (keyCode === Keys.Home || keyCode === Keys.End)) {
59
+ event.preventDefault();
60
+ const direction = keyCode === Keys.Home ? 'home' : 'end';
61
+ this.onSelectToEnd.emit({ direction });
62
+ return;
63
+ }
51
64
  const isTargetListboxItem = listboxItems.find(elem => elem.nativeElement === target);
52
65
  if (isTargetListboxItem) {
53
66
  let isTransferToolVisible;
@@ -64,9 +77,7 @@ export class KeyboardNavigationService {
64
77
  this.onSelectChange(event, listboxItems);
65
78
  }
66
79
  else if (keyCode === Keys.Space) {
67
- if (this.selectedListboxItemIndex !== this.focusedListboxItemIndex) {
68
- this.onSpaceKey(event, listboxItems);
69
- }
80
+ this.onSpaceKey(event, listboxItems);
70
81
  }
71
82
  }
72
83
  }
@@ -90,18 +101,30 @@ export class KeyboardNavigationService {
90
101
  }
91
102
  const offset = dir === 'up' ? -1 : 1;
92
103
  this.focusedToolIndex += offset;
93
- const prevItem = toolsRef[this.focusedToolIndex + (offset * -1)].element;
94
- const currentItem = toolsRef[this.focusedToolIndex].element;
104
+ const prevItem = toolsRef[this.focusedToolIndex + (offset * -1)]?.element;
105
+ const currentItem = toolsRef[this.focusedToolIndex]?.element;
95
106
  this.changeTabindex(prevItem, currentItem);
96
107
  }
97
108
  onSpaceKey(event, listboxItems) {
98
109
  event.stopImmediatePropagation();
99
110
  event.preventDefault();
100
- const previousItem = listboxItems[this.selectedListboxItemIndex]?.nativeElement;
101
- const currentItem = listboxItems[this.focusedListboxItemIndex]?.nativeElement;
102
- this.changeTabindex(previousItem, currentItem);
103
- this.onSelectionChange.emit({ index: this.focusedListboxItemIndex, prevIndex: this.selectedListboxItemIndex });
104
- this.selectedListboxItemIndex = this.focusedListboxItemIndex;
111
+ event.stopPropagation();
112
+ const ctrlKey = event.ctrlKey || event.metaKey;
113
+ const shiftKey = event.shiftKey;
114
+ if (this.selectedListboxItemIndex !== this.focusedListboxItemIndex) {
115
+ const previousItem = listboxItems[this.selectedListboxItemIndex]?.nativeElement;
116
+ const currentItem = listboxItems[this.focusedListboxItemIndex]?.nativeElement;
117
+ this.changeTabindex(previousItem, currentItem);
118
+ }
119
+ this.onSelectionChange.emit({
120
+ index: this.focusedListboxItemIndex,
121
+ prevIndex: this.selectedListboxItemIndex,
122
+ ctrlKey,
123
+ shiftKey
124
+ });
125
+ if (!shiftKey) {
126
+ this.selectedListboxItemIndex = this.focusedListboxItemIndex;
127
+ }
105
128
  }
106
129
  onArrowUpOrDown(keyCode, ctrlOrMetaKey, event, activeToolbar, listboxItems) {
107
130
  event.preventDefault();
@@ -118,6 +141,10 @@ export class KeyboardNavigationService {
118
141
  this.changeFocusedItem(dir, listboxItems);
119
142
  return;
120
143
  }
144
+ if (event.shiftKey) {
145
+ this.onShiftArrow(dir, listboxItems);
146
+ return;
147
+ }
121
148
  dir === 'moveUp' ? this.onArrowUp(listboxItems) : this.onArrowDown(listboxItems);
122
149
  this.onSelectionChange.emit({ index: this.selectedListboxItemIndex, prevIndex: this.focusedListboxItemIndex });
123
150
  this.focusedListboxItemIndex = this.selectedListboxItemIndex;
@@ -151,7 +178,8 @@ export class KeyboardNavigationService {
151
178
  this.selectedListboxItemIndex = this.focusedListboxItemIndex;
152
179
  }
153
180
  this.changeTabindex(previousItem, currentItem, !!currentItem);
154
- this.onSelectionChange.emit({ index: this.selectedListboxItemIndex, prevIndex });
181
+ const ctrlKey = event.ctrlKey || event.metaKey;
182
+ this.onSelectionChange.emit({ index: this.selectedListboxItemIndex, prevIndex, ctrlKey });
155
183
  }
156
184
  onF10Key(tools) {
157
185
  if (this.focusedToolIndex && this.focusedToolIndex > -1) {
@@ -253,14 +281,36 @@ export class KeyboardNavigationService {
253
281
  });
254
282
  }
255
283
  changeFocusedItem(dir, listboxItems) {
256
- listboxItems[this.focusedListboxItemIndex].nativeElement.blur();
284
+ const previousIndex = this.focusedListboxItemIndex;
285
+ const previousItem = listboxItems[previousIndex].nativeElement;
257
286
  if (this.focusedListboxItemIndex > 0 && dir === 'moveUp') {
258
287
  this.focusedListboxItemIndex -= 1;
259
288
  }
260
289
  else if (this.focusedListboxItemIndex < listboxItems.length - 1 && dir === 'moveDown') {
261
290
  this.focusedListboxItemIndex += 1;
262
291
  }
263
- listboxItems[this.focusedListboxItemIndex].nativeElement.focus();
292
+ const currentItem = listboxItems[this.focusedListboxItemIndex].nativeElement;
293
+ this.changeTabindex(previousItem, currentItem);
294
+ }
295
+ onShiftArrow(dir, listboxItems) {
296
+ const previousFocusIndex = this.focusedListboxItemIndex;
297
+ if (dir === 'moveUp' && this.focusedListboxItemIndex > 0) {
298
+ this.focusedListboxItemIndex -= 1;
299
+ }
300
+ else if (dir === 'moveDown' && this.focusedListboxItemIndex < listboxItems.length - 1) {
301
+ this.focusedListboxItemIndex += 1;
302
+ }
303
+ if (previousFocusIndex !== this.focusedListboxItemIndex) {
304
+ const previousItem = listboxItems[previousFocusIndex]?.nativeElement;
305
+ const currentItem = listboxItems[this.focusedListboxItemIndex]?.nativeElement;
306
+ this.changeTabindex(previousItem, currentItem);
307
+ this.onSelectionChange.emit({
308
+ index: this.focusedListboxItemIndex,
309
+ prevIndex: this.selectedListboxItemIndex,
310
+ shiftKey: true
311
+ });
312
+ this.selectedListboxItemIndex = this.focusedListboxItemIndex;
313
+ }
264
314
  }
265
315
  onArrowDown(listboxItems) {
266
316
  if (this.selectedListboxItemIndex < listboxItems.length - 1) {