diff --git a/packages/cdk/keycodes/keycodes.ts b/packages/cdk/keycodes/keycodes.ts
index 8939e5912..22a7a2783 100644
--- a/packages/cdk/keycodes/keycodes.ts
+++ b/packages/cdk/keycodes/keycodes.ts
@@ -122,7 +122,7 @@ export const MAC_META = 224;
type ModifierKey = 'altKey' | 'shiftKey' | 'ctrlKey' | 'metaKey';
-export function hasModifierKey(event: KeyboardEvent, ...modifiers: ModifierKey[]): boolean {
+export function hasModifierKey(event: KeyboardEvent | MouseEvent, ...modifiers: ModifierKey[]): boolean {
if (modifiers.length) {
return modifiers.some((modifier) => event[modifier]);
}
diff --git a/packages/mosaic-dev/list/template.html b/packages/mosaic-dev/list/template.html
index b8a9adba1..b0e7f84f8 100644
--- a/packages/mosaic-dev/list/template.html
+++ b/packages/mosaic-dev/list/template.html
@@ -14,13 +14,13 @@
multiple selection
- Disabled
Normal
Hovered
Focused
+ Disabled
Selected
diff --git a/packages/mosaic-dev/tree/module.ts b/packages/mosaic-dev/tree/module.ts
index ecd7eec6a..b31fcd942 100644
--- a/packages/mosaic-dev/tree/module.ts
+++ b/packages/mosaic-dev/tree/module.ts
@@ -112,7 +112,7 @@ export class DemoComponent {
filterValue: string = '';
- modelValue: any = ['Chrome'];
+ modelValue: any = [];
// modelValue: any[] = ['rootNode_1', 'Documents', 'Calendar', 'Chrome'];
disableState: boolean = false;
diff --git a/packages/mosaic-moment-adapter/adapter/moment-date-adapter.ts b/packages/mosaic-moment-adapter/adapter/moment-date-adapter.ts
index 37711d51b..f2c98cbb4 100644
--- a/packages/mosaic-moment-adapter/adapter/moment-date-adapter.ts
+++ b/packages/mosaic-moment-adapter/adapter/moment-date-adapter.ts
@@ -421,7 +421,6 @@ export class MomentDateAdapter extends DateAdapter {
}
openedRangeDateTime(startDate: Moment | null, endDate: Moment | null, template: IFormatterRangeTemplate) {
- console.log('openedRangeDateTime: '); // tslint:disable-line:no-console
if (!moment.isMoment(startDate) && !moment.isMoment(endDate)) {
throw new Error(this.invalidDateErrorText);
}
diff --git a/packages/mosaic/list/list-selection.component.ts b/packages/mosaic/list/list-selection.component.ts
index 1b4d0962e..6bc40b24d 100644
--- a/packages/mosaic/list/list-selection.component.ts
+++ b/packages/mosaic/list/list-selection.component.ts
@@ -68,7 +68,7 @@ export interface McOptionEvent {
host: {
'[attr.tabindex]': 'tabIndex',
- class: 'mc-list-option',
+ class: 'mc-list-option mc-no-select',
'[class.mc-selected]': 'selected',
'[class.mc-focused]': 'hasFocus',
'[class.mc-disabled]': 'disabled',
@@ -212,7 +212,9 @@ export class McListOption implements OnDestroy, OnInit, IFocusableOption {
handleClick($event) {
if (this.disabled) { return; }
- this.listSelection.setFocusedOption(this, $event);
+ this.listSelection.setSelectedOptionsByClick(
+ this, hasModifierKey($event, 'shiftKey'), hasModifierKey($event, 'ctrlKey')
+ );
}
focus() {
@@ -365,6 +367,11 @@ export class McListSelection extends McListSelectionMixinBase implements CanDisa
this.multipleMode = MultipleMode.CHECKBOX;
}
+ if (this.multipleMode === MultipleMode.CHECKBOX) {
+ this.autoSelect = false;
+ this.noUnselect = false;
+ }
+
this._tabIndex = parseInt(tabIndex) || 0;
this.selectionModel = new SelectionModel(this.multiple);
@@ -453,31 +460,29 @@ export class McListSelection extends McListSelectionMixinBase implements CanDisa
this.keyManager.withScrollSize(Math.floor(this.getHeight() / this.options.first.getHeight()));
}
- // Sets the focused option of the selection-list.
- setFocusedOption(option: McListOption, $event?: KeyboardEvent): void {
- this.keyManager.setActiveItem(option);
-
- const withShift = $event ? hasModifierKey($event, 'shiftKey') : false;
- const withCtrl = $event ? hasModifierKey($event, 'ctrlKey') : false;
-
- if (withShift && this.multiple) {
- const previousIndex = this.keyManager.previousActiveItemIndex;
- const activeIndex = this.keyManager.activeItemIndex;
+ setSelectedOptionsByClick(option: McListOption, shiftKey: boolean, ctrlKey: boolean): void {
+ if (shiftKey && this.multiple) {
+ this.setSelectedOptions(option);
+ } else if (ctrlKey) {
+ if (!this.canDeselectLast(option)) { return; }
- if (previousIndex < activeIndex) {
- this.options.forEach((item, index) => {
- if (index >= previousIndex && index <= activeIndex) { item.setSelected(true); }
- });
- } else {
- this.options.forEach((item, index) => {
- if (index >= activeIndex && index <= previousIndex) { item.setSelected(true); }
- });
+ this.selectionModel.toggle(option);
+ } else {
+ if (this.autoSelect) {
+ this.options.forEach((item) => item.setSelected(false));
+ option.setSelected(true);
}
- } else if (withCtrl) {
+ }
- if (!this.canDeselectLast(option)) { return; }
+ this.emitChangeEvent(option);
+ this.reportValueChange();
+ }
- option.toggle();
+ setSelectedOptionsByKey(option: McListOption, shiftKey: boolean, ctrlKey: boolean): void {
+ if (shiftKey && this.multiple) {
+ this.setSelectedOptions(option);
+ } else if (ctrlKey) {
+ if (!this.canDeselectLast(option)) { return; }
} else {
if (this.autoSelect) {
this.options.forEach((item) => item.setSelected(false));
@@ -489,6 +494,31 @@ export class McListSelection extends McListSelectionMixinBase implements CanDisa
this.reportValueChange();
}
+ setSelectedOptions(option: McListOption): void {
+ const selectedOptionState = option.selected;
+
+ let fromIndex = this.keyManager.previousActiveItemIndex;
+ let toIndex = this.keyManager.previousActiveItemIndex = this.keyManager.activeItemIndex;
+
+ if (toIndex === fromIndex) { return; }
+
+ if (fromIndex > toIndex) {
+ [fromIndex, toIndex] = [toIndex, fromIndex];
+ }
+
+ this.options
+ .toArray()
+ .slice(fromIndex, toIndex + 1)
+ .filter((item) => !item.disabled)
+ .forEach((renderedOption) => {
+ const isLastRenderedOption = renderedOption === this.keyManager.activeItem;
+
+ if (isLastRenderedOption && renderedOption.selected && this.noUnselect) { return; }
+
+ renderedOption.setSelected(!selectedOptionState);
+ });
+ }
+
// Implemented as part of ControlValueAccessor.
writeValue(values: string[]): void {
if (this.options) {
@@ -607,7 +637,11 @@ export class McListSelection extends McListSelectionMixinBase implements CanDisa
event.preventDefault();
- this.setFocusedOption(this.keyManager.activeItem as McListOption, event);
+ this.setSelectedOptionsByKey(
+ this.keyManager.activeItem as McListOption,
+ hasModifierKey(event, 'shiftKey'),
+ hasModifierKey(event, 'ctrlKey')
+ );
}
// Reports a value change to the ControlValueAccessor
diff --git a/packages/mosaic/list/list.md b/packages/mosaic/list/list.md
index 5977df8bf..79dab42b1 100644
--- a/packages/mosaic/list/list.md
+++ b/packages/mosaic/list/list.md
@@ -1,14 +1,11 @@
#### With default parameters (autoselect="true", no-unselect="true")
+### Single mode with groups
+
### Multiple mode with checkboxes
-
### Multiple mode without checkboxes
-
-
-### Multiple mode without checkboxes
-
\ No newline at end of file
diff --git a/packages/mosaic/tree-select/tree-select.component.spec.ts b/packages/mosaic/tree-select/tree-select.component.spec.ts
index 9830fd86a..dbd9df8a8 100644
--- a/packages/mosaic/tree-select/tree-select.component.spec.ts
+++ b/packages/mosaic/tree-select/tree-select.component.spec.ts
@@ -649,7 +649,7 @@ class BasicSelectOnPushPreselected {
template: `
@@ -1063,7 +1063,7 @@ class BasicSelectWithoutFormsPreselected {
@Component({
template: `
-
+
@@ -4345,6 +4345,7 @@ describe('McTreeSelect', () => {
it('should be able to select multiple values', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
+ flush();
const options: NodeListOf = overlayContainerElement.querySelectorAll('mc-tree-option');
@@ -4438,6 +4439,7 @@ describe('McTreeSelect', () => {
it('should not close the panel when clicking on options', fakeAsync(() => {
trigger.click();
fixture.detectChanges();
+ flush();
expect(testInstance.select.panelOpen).toBe(true);
diff --git a/packages/mosaic/tree-select/tree-select.component.ts b/packages/mosaic/tree-select/tree-select.component.ts
index b502d6d63..ab884fb1e 100644
--- a/packages/mosaic/tree-select/tree-select.component.ts
+++ b/packages/mosaic/tree-select/tree-select.component.ts
@@ -54,7 +54,10 @@ import {
RIGHT_ARROW,
SPACE,
UP_ARROW,
- A, PAGE_UP, PAGE_DOWN
+ A,
+ PAGE_UP,
+ PAGE_DOWN,
+ hasModifierKey
} from '@ptsecurity/cdk/keycodes';
import { CdkTree } from '@ptsecurity/cdk/tree';
import {
@@ -487,7 +490,10 @@ export class McTreeSelect extends McTreeSelectMixinBase implements
this.options = this.tree.renderedOptions;
this.tree.autoSelect = this.autoSelect;
- this.tree.multipleMode = this.multiple ? MultipleMode.CHECKBOX : null;
+
+ if (this.tree.multipleMode === null) {
+ this.tree.multipleMode = this.multiple ? MultipleMode.CHECKBOX : null;
+ }
if (this.multiple) {
this.tree.noUnselectLast = false;
@@ -518,6 +524,7 @@ export class McTreeSelect extends McTreeSelectMixinBase implements
.pipe(takeUntil(this.destroy))
.subscribe((event) => {
if (event.added.length) {
+ this.tree.keyManager.setFocusOrigin('program');
this.tree.keyManager.setActiveItem(
this.options.find((option) => option.data === event.added[0]) as any
);
@@ -925,6 +932,7 @@ export class McTreeSelect extends McTreeSelectMixinBase implements
} else {
const previouslyFocusedIndex = this.tree.keyManager.activeItemIndex;
+ this.tree.keyManager.setFocusOrigin('keyboard');
this.tree.keyManager.onKeydown(event);
if (
@@ -935,7 +943,9 @@ export class McTreeSelect extends McTreeSelectMixinBase implements
}
if (this.autoSelect && this.tree.keyManager.activeItem) {
- this.tree.setSelectedOption(this.tree.keyManager.activeItem);
+ this.tree.setSelectedOptionsByKey(
+ this.tree.keyManager.activeItem, hasModifierKey(event, 'shiftKey'), hasModifierKey(event, 'ctrlKey')
+ );
}
}
}
diff --git a/packages/mosaic/tree/tree-option.component.ts b/packages/mosaic/tree/tree-option.component.ts
index 91d6d481f..f8650494a 100644
--- a/packages/mosaic/tree/tree-option.component.ts
+++ b/packages/mosaic/tree/tree-option.component.ts
@@ -13,6 +13,8 @@ import {
AfterContentInit,
NgZone
} from '@angular/core';
+import { FocusOrigin } from '@ptsecurity/cdk/a11y';
+import { hasModifierKey } from '@ptsecurity/cdk/keycodes';
import { CdkTreeNode } from '@ptsecurity/cdk/tree';
import { CanDisable } from '@ptsecurity/mosaic/core';
import { Subject } from 'rxjs';
@@ -164,7 +166,9 @@ export class McTreeOption extends CdkTreeNode implements CanDisabl
this.changeDetectorRef.markForCheck();
}
- focus() {
+ focus(focusOrigin?: FocusOrigin) {
+ if (focusOrigin === 'program') { return; }
+
if (this.disabled || this.hasFocus) { return; }
this.elementRef.nativeElement.focus();
@@ -227,9 +231,10 @@ export class McTreeOption extends CdkTreeNode implements CanDisabl
this.changeDetectorRef.markForCheck();
this.emitSelectionChangeEvent(true);
- if (this.tree.setSelectedOption) {
- this.tree.setSelectedOption(this, $event);
- }
+ const shiftKey = $event ? hasModifierKey($event, 'shiftKey') : false;
+ const ctrlKey = $event ? hasModifierKey($event, 'ctrlKey') : false;
+
+ this.tree.setSelectedOptionsByClick(this, shiftKey, ctrlKey);
}
}
diff --git a/packages/mosaic/tree/tree-selection.component.spec.ts b/packages/mosaic/tree/tree-selection.component.spec.ts
index 5e6124d86..a182c91ca 100644
--- a/packages/mosaic/tree/tree-selection.component.spec.ts
+++ b/packages/mosaic/tree/tree-selection.component.spec.ts
@@ -1,6 +1,6 @@
/* tslint:disable:no-magic-numbers max-func-body-length no-reserved-keywords */
import { Component, ViewChild } from '@angular/core';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { createMouseEvent, dispatchEvent } from '@ptsecurity/cdk/testing';
import { FlatTreeControl } from '@ptsecurity/cdk/tree';
@@ -246,58 +246,56 @@ describe('McTreeSelection', () => {
});
describe('when shift is pressed', () => {
- it('should select nodes', () => {
+ it('should select nodes', fakeAsync(() => {
expect(component.modelValue.length).toBe(0);
const nodes = getNodes(treeElement);
const event = createMouseEvent('click');
+ (nodes[0] as HTMLElement).focus();
dispatchEvent(nodes[0], event);
- fixture.detectChanges();
-
expect(component.modelValue.length).toBe(1);
const targetNode: HTMLElement = nodes[3] as HTMLElement;
- targetNode.focus();
-
Object.defineProperty(event, 'shiftKey', { get: () => true });
+ targetNode.focus();
dispatchEvent(targetNode, event);
fixture.detectChanges();
expect(component.modelValue.length).toBe(4);
- });
+ }));
- it('should deselect nodes', () => {
+ it('should deselect nodes', fakeAsync(() => {
expect(component.modelValue.length).toBe(0);
const nodes = getNodes(treeElement);
- const event = createMouseEvent('click');
+ let event = createMouseEvent('click');
Object.defineProperty(event, 'ctrlKey', { get: () => true });
- dispatchEvent(nodes[0], event);
- dispatchEvent(nodes[1], event);
- dispatchEvent(nodes[2], event);
- (nodes[2] as HTMLElement).focus();
- fixture.detectChanges();
+ component.tree.renderedOptions.toArray().forEach((option, index) => {
+ if (index < 3) {option.selected = true; }
+ });
+ component.tree.keyManager.setActiveItem(2);
expect(component.modelValue.length).toBe(3);
const targetNode: HTMLElement = nodes[0] as HTMLElement;
- Object.defineProperty(event, 'ctrlKey', { get: () => false });
+ event = createMouseEvent('click');
Object.defineProperty(event, 'shiftKey', { get: () => true });
+ component.tree.keyManager.setActiveItem(0);
dispatchEvent(targetNode, event);
fixture.detectChanges();
- expect(component.modelValue.length).toBe(0);
- });
+ expect(component.modelValue.length).toBe(1);
+ }));
- it('should set last selected status', () => {
+ it('should set last selected status', fakeAsync(() => {
expect(component.modelValue.length).toBe(0);
const nodes = getNodes(treeElement);
@@ -313,12 +311,13 @@ describe('McTreeSelection', () => {
dispatchEvent(nodes[4], event);
fixture.detectChanges();
+ component.tree.keyManager.setActiveItem(4);
expect(component.modelValue.length).toBe(3);
const targetNode: HTMLElement = nodes[2] as HTMLElement;
- targetNode.focus();
+ component.tree.keyManager.setActiveItem(2);
Object.defineProperty(event, 'ctrlKey', { get: () => false });
Object.defineProperty(event, 'shiftKey', { get: () => true });
@@ -326,8 +325,8 @@ describe('McTreeSelection', () => {
dispatchEvent(targetNode, event);
fixture.detectChanges();
- expect(component.modelValue.length).toBe(1);
- });
+ expect(component.modelValue.length).toBe(2);
+ }));
});
});
diff --git a/packages/mosaic/tree/tree-selection.component.ts b/packages/mosaic/tree/tree-selection.component.ts
index 2d1c035fa..c4dc4ec37 100644
--- a/packages/mosaic/tree/tree-selection.component.ts
+++ b/packages/mosaic/tree/tree-selection.component.ts
@@ -31,7 +31,9 @@ import {
PAGE_DOWN,
PAGE_UP,
RIGHT_ARROW,
- SPACE
+ SPACE,
+ DOWN_ARROW,
+ UP_ARROW
} from '@ptsecurity/cdk/keycodes';
import { CdkTree, CdkTreeNodeOutlet, FlatTreeControl } from '@ptsecurity/cdk/tree';
import { CanDisable, getMcSelectNonArrayValueError, HasTabIndex, MultipleMode } from '@ptsecurity/mosaic/core';
@@ -105,7 +107,7 @@ export class McTreeSelection extends CdkTree
@Output() readonly selectionChange = new EventEmitter>();
- multipleMode: MultipleMode | null;
+ multipleMode: MultipleMode | null = null;
userTabIndex: number | null = null;
@@ -218,10 +220,9 @@ export class McTreeSelection extends CdkTree
if (this.keyManager.activeItem) {
this.emitNavigationEvent(this.keyManager.activeItem);
+ // todo need check this logic
if (this.autoSelect && !this.keyManager.activeItem.disabled) {
this.updateOptionsFocus();
-
- this.setSelectedOption(this.keyManager.activeItem);
}
}
});
@@ -280,10 +281,20 @@ export class McTreeSelection extends CdkTree
}
onKeyDown(event: KeyboardEvent): void {
+ this.keyManager.setFocusOrigin('keyboard');
+ console.log('onKeyDown: '); // tslint:disable-line:no-console
// tslint:disable-next-line: deprecation
const keyCode = event.keyCode;
switch (keyCode) {
+ case DOWN_ARROW:
+ this.keyManager.setNextItemActive();
+
+ break;
+ case UP_ARROW:
+ this.keyManager.setPreviousItemActive();
+
+ break;
case LEFT_ARROW:
if (this.keyManager.activeItem) {
this.treeControl.collapse(this.keyManager.activeItem.data as T);
@@ -327,7 +338,13 @@ export class McTreeSelection extends CdkTree
break;
default:
- this.keyManager.onKeydown(event);
+ return;
+ }
+
+ if (this.keyManager.activeItem) {
+ this.setSelectedOptionsByKey(
+ this.keyManager.activeItem, hasModifierKey(event, 'shiftKey'), hasModifierKey(event, 'ctrlKey')
+ );
}
}
@@ -337,49 +354,65 @@ export class McTreeSelection extends CdkTree
this.keyManager.withScrollSize(Math.floor(this.getHeight() / this.renderedOptions.first.getHeight()));
}
- setSelectedOption(option: T, $event?: KeyboardEvent): void {
- const withShift = $event ? hasModifierKey($event, 'shiftKey') : false;
- const withCtrl = $event ? hasModifierKey($event, 'ctrlKey') : false;
-
- if (this.multiple) {
- if (withShift) {
- const previousIndex = this.keyManager.previousActiveItemIndex;
- const activeIndex = this.keyManager.activeItemIndex;
- const activeOption = this.renderedOptions.toArray()[activeIndex];
-
- const targetSelected = !activeOption.selected;
+ setSelectedOptionsByKey(option: T, shiftKey: boolean, ctrlKey: boolean): void {
+ if (shiftKey && this.multiple) {
+ this.setSelectedOptions(option);
+ } else if (ctrlKey) {
+ if (!this.canDeselectLast(option)) { return; }
+ } else if (this.autoSelect) {
+ this.selectionModel.clear();
+ this.selectionModel.toggle(option.data);
+ }
- if (previousIndex < activeIndex) {
- this.renderedOptions.forEach((item, index) => {
- if (index >= previousIndex && index <= activeIndex) { item.setSelected(targetSelected); }
- });
- } else {
- this.renderedOptions.forEach((item, index) => {
- if (index >= activeIndex && index <= previousIndex) { item.setSelected(targetSelected); }
- });
- }
- } else if (withCtrl) {
- if (!this.canDeselectLast(option)) { return; }
+ this.emitChangeEvent(option);
+ }
- this.selectionModel.toggle(option.data);
- } else {
- if (this.multipleMode === MultipleMode.KEYBOARD) {
- this.selectionModel.clear();
- }
+ setSelectedOptionsByClick(option: T, shiftKey: boolean, ctrlKey: boolean): void {
+ if (!shiftKey && !ctrlKey) {
+ this.keyManager.setActiveItem(option);
+ }
- this.selectionModel.toggle(option.data);
- }
- } else {
+ if (shiftKey && this.multiple) {
+ this.setSelectedOptions(option);
+ } else if (ctrlKey) {
if (!this.canDeselectLast(option)) { return; }
- if (this.autoSelect) {
- this.selectionModel.toggle(option.data);
- }
+ this.selectionModel.toggle(option.data);
+ } else if (this.autoSelect) {
+ this.selectionModel.clear();
+ this.selectionModel.toggle(option.data);
+ } else {
+ this.selectionModel.toggle(option.data);
}
this.emitChangeEvent(option);
}
+ setSelectedOptions(option: T): void {
+ const selectedOptionState = option.selected;
+
+ let fromIndex = this.keyManager.previousActiveItemIndex;
+ let toIndex = this.keyManager.previousActiveItemIndex = this.keyManager.activeItemIndex;
+
+ if (toIndex === fromIndex) { return; }
+
+ if (fromIndex > toIndex) {
+ [fromIndex, toIndex] = [toIndex, fromIndex];
+ }
+
+ this.renderedOptions
+ .toArray()
+ .slice(fromIndex, toIndex + 1)
+ .filter((item) => !item.disabled)
+ .forEach((renderedOption) => {
+ const isLastRenderedOption = renderedOption === this.keyManager.activeItem;
+
+ if (isLastRenderedOption && renderedOption.selected && this.noUnselectLast) { return; }
+
+ renderedOption.setSelected(!selectedOptionState);
+ });
+ }
+
setFocusedOption(option: T): void {
this.keyManager.setActiveItem(option);
}
@@ -540,6 +573,10 @@ export class McTreeSelection extends CdkTree
.subscribe((event) => {
const index: number = this.renderedOptions.toArray().indexOf(event.option as T);
+ this.renderedOptions
+ .filter((option) => option.hasFocus)
+ .forEach((option) => option.hasFocus = false);
+
if (this.isValidIndex(index)) {
this.keyManager.updateActiveItem(index);
}