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); }