Skip to content

wip: cdk selection #11770

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
/src/cdk-experimental/** @jelbourn
/src/cdk-experimental/dialog/** @jelbourn @josephperrott @crisbeto
/src/cdk-experimental/scrolling/** @mmalerba
/src/cdk-experimental/selection/** @amcdnl
/src/cdk-experimental/drag-drop/** @crisbeto

# Docs examples & guides
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 49 additions & 0 deletions src/cdk-experimental/selection/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package(default_visibility=["//visibility:public"])
load("@angular//:index.bzl", "ng_module")
load("@build_bazel_rules_typescript//:defs.bzl", "ts_library", "ts_web_test")
load("@io_bazel_rules_sass//sass:sass.bzl", "sass_binary")


ng_module(
name = "selection",
srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts"]),
module_name = "@angular/cdk-experimental/selection",
assets = [] + glob(["**/*.html"]),
deps = [
"//src/cdk/coercion",
"//src/cdk/collections",
"@rxjs",
],
tsconfig = "//src/cdk-experimental:tsconfig-build.json",
)


ts_library(
name = "selection_test_sources",
testonly = 1,
srcs = glob(["**/*.spec.ts"]),
deps = [
":selection",
"//src/cdk/collections",
"//src/cdk/testing",
"@rxjs",
],
tsconfig = "//src/cdk-experimental:tsconfig-build.json",
)

ts_web_test(
name = "unit_tests",
bootstrap = [
"//:web_test_bootstrap_scripts",
],
tags = ["manual"],

# Do not sort
deps = [
"//:tslib_bundle",
"//:angular_bundles",
"//:angular_test_bundles",
"//test:angular_test_init",
":selection_test_sources",
],
)
9 changes: 9 additions & 0 deletions src/cdk-experimental/selection/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

export * from './public-api';
11 changes: 11 additions & 0 deletions src/cdk-experimental/selection/public-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

export * from './selection-module';
export * from './selection';
export * from './selection-toggle';
18 changes: 18 additions & 0 deletions src/cdk-experimental/selection/selection-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {NgModule} from '@angular/core';
import {CdkSelection} from './selection';
import {CdkSelectionToggle} from './selection-toggle';


@NgModule({
declarations: [CdkSelection, CdkSelectionToggle],
exports: [CdkSelection, CdkSelectionToggle]
})
export class CdkSelectionModule {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When this is merged into the cdk for real, I think it would make sense to add it to the existing collections subpackage

121 changes: 121 additions & 0 deletions src/cdk-experimental/selection/selection-toggle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, Input, OnInit, OnDestroy, ElementRef, ChangeDetectorRef} from '@angular/core';
import {CdkSelection} from './selection';
import {Subject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
import {coerceBooleanProperty} from '@angular/cdk/coercion';


@Directive({
selector: '[cdkSelectionToggle]',
host: {
'class': 'cdk-selection-toggle',
'tabindex': '-1',
'[class.cdk-selection-selected]': 'selected',
'(mousedown)': '_setModifiers($event)',
'(click)': 'toggle()',
// Right click should select item.
'(contextmenu)': 'toggle()',
'(keydown.enter)': 'toggle()',
'(keydown.space)': 'toggle()',
'(keydown.shift)': '_disableTextSelection()',
'(blur)': '_enableTextSelection()',
}
})
export class CdkSelectionToggle<T> implements OnInit, OnDestroy {

/** The value(s) that represent the selection of this directive. */
@Input('cdkSelectionToggle') model: T | T[];

/** Whether the selection is disabled or not. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need the "or not" at the end of the comment (here and other boolean descriptions)

@Input()
get disabled(): boolean { return this._disabled; }
set disabled(val) { this._disabled = coerceBooleanProperty(val); }
private _disabled: boolean;

/** Whether the toggle is selected. */
selected: boolean;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be an @Input?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, similar to the sort you set the active states on the parent. This is just a internal state. Perhaps I should underscore it?


/** The modifier that was invoked. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"that was invoked" when?

  • Should this be internal?
  • I don't quite follow why you need to remember the modifiers; don't you only care when the click occurs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my original implementation I listened for this separately because in cases like a checkbox I would listen to the change event rather than click and this information would not be available on that change event. I figured as we carry this forward to other implementations like the checkbox having this at the root would simplify those scenarios.

The modifier is public so the selection directive parent can read it out w/o having to pass it.

Let me know your thoughts here.

modifier: 'shift' | 'meta' | null;

private _destroyed = new Subject();

constructor(
/** @docs-private */
public elementRef: ElementRef,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the elementRef have to be public?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, its used in the parent directive to identify the element order.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack; it needs a user-facing JsDoc, then

private _selection: CdkSelection<T>,
private _changeDetectorRef: ChangeDetectorRef
) {}

ngOnInit() {
this._selection.selectionChange
.pipe(takeUntil(this._destroyed))
.subscribe(() => this._updateSelected());

this._updateSelected();
this._selection.registerToggle(this);
}

ngOnDestroy() {
this._destroyed.next();
this._destroyed.complete();

this._selection.deregisterToggle(this);
}

/** Toggle the selection. */
toggle() {
if (!this.disabled) {
this._selection.toggle(this);
}
}

/** Set the modifiers for the mouse event. */
_setModifiers(event) {
if (event.metaKey || event.ctrlKey) {
this.modifier = 'meta';
} else if (event.shiftKey) {
this.modifier = 'shift';
}

// Clear the modifier if we don't use it
setTimeout(() => this.modifier = null, 200);
}

/** Update the state of the selection based on the selection model. */
_updateSelected() {
if (Array.isArray(this.model)) {
let has = true;
for (const model of this.model) {
if (!this._selection._selectionModel.isSelected(model)) {
has = false;
break;
}
}
this.selected = has;
} else {
this.selected = this._selection._selectionModel.isSelected(this.model);
}

this._changeDetectorRef.markForCheck();
}

/** Shift key was captured to set user selection. */
_disableTextSelection() {
this._selection.setTextSelection('none');
}

/** Element was blurred, lets reset user selection. */
_enableTextSelection() {
setTimeout(() => this._selection.setTextSelection('initial'), 200);
}

}
63 changes: 63 additions & 0 deletions src/cdk-experimental/selection/selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
The `selection` package handles managing selection state on components
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, I'll do an editing pass on this after it's submitted since it's easier than doing back and forth review of longer copy.

with support for single and multi select with keyboard modifiers.

Components such as buttons, checkboxes, etc can all be decorated with
the `cdkSelectionToggle` directive. Once decorated, the directive
will listen for mouse and keyboard events and coordinate the
selection options with the parent `cdkSelection` directive.

In the example below, the div container of the buttons is decorated with `cdkSelection`. To populate
default selections pass an array of the selections to this option like:
`[cdkSelection]="['yellow']"`. Nested beneath the selection directive is a button decorated
with the `cdkSelectionToggle` directive with an argument of the item's value property.

```html
<ul cdkSelection cdkSelectionMode="single">
<li *ngFor="let item of items">
<button [cdkSelectionToggle]="item.value">
{{item.label}}
</button>
</li>
</ul>
```

### Modes
The selection directive has 2 different modes for selection:

- Single: Only one item can be selected at a time.
- Multiple: Multiple items can be selected.

### Key Modifier
When multi-selection mode is enabled anytime a user clicks a toggle element it will be selected.
The `requireModifier` property will only allow multi-selection when using key modifiers. For example,
CTRL/Command + Click would select multiples and SHIFT + Click would select a range.

### Deselectable
The ability to deselect a selection that has been made can be prevented using the `cdkSelectionDeselectable`.
This means that after a selection (or many selections) have been made users can deselect
all but one of the selections. This concept can be related to radio buttons selection strategy.

### Tracking
When using complex objects with the selection toggle, custom tracking strategies
can ensure correct identification of the values. The `cdkSelectionTrackBy` accepts
a function that will be invoked with the values of the toggle. This can be used to
return a identifying attribute for the object.

```TS
@Component({
template: `
<ul cdkSelection cdkSelectionMode="multiple" [cdkSelectionTrackBy]=""trackBy>
<li *ngFor="let item of items">
<button [cdkSelectionToggle]="item">
{{item.label}}
</button>
</li>
</ul>
`
})
export class MyComponent {
trackBy(model) {
return model.value;
}
}
```
Loading