Skip to content
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

feat(chips): Add remove functionality/styling. #2476

Conversation

topherfangio
Copy link
Contributor

@topherfangio topherfangio commented Dec 30, 2016

Add events, styling and keyboard controls to allow removable chips.

  • Add basic styling for a user-provided remove icon.
  • Add keyboard controls for backspace/delete.
  • Add (remove) event which is emitted when the remove icon or
    one of the delete keys is pressed.
  • Add md-chip-remove directive which can be applied to <md-icon>
    (or others) to inform the chip of the remove request.

Add new directive mdChipInput for controlling:

  • (chipAdded) - Event fired when a chip should be added.
  • [separatorKeys] - The list of keycodes that will fire the
    (chipAdded) event.
  • [addOnBlur] - Whether or not to fire the (chipAdded) event
    when the input is blurred.

Additionally, fix some issues with dark theme and add styling/support
for usage inside the md-input-container and add styling for focused
chips.

BREAKING CHANGE - The selectable property of the md-chip-list has
now been moved to md-chip to maintain consistency with the new
removable option.

If you used the following code,

<md-chip-list [selectable]="selectable">
  <md-chip>My Chip</md-chip>
</md-chip-list>

you should switch it to

<md-chip-list>
  <md-chip [selectable]="selectable">My Chip</md-chip>
</md-chip-list>

References #120. Closes #3143.

Hat tip to @willshowell for suggestion to use secondary-text
for focus color :-)

@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Dec 30, 2016
@@ -36,6 +38,9 @@ export class ChipsDemo {
{ name: 'Warn', color: 'warn' }
];

constructor() {
}
Copy link
Member

Choose a reason for hiding this comment

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

Is constructor unnecessary?

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; I think it got added by accident or something. I'll remove it.

@@ -47,6 +52,16 @@ export class ChipsDemo {
}
}

remove(person: Person): void {
if (person) {
Copy link
Member

Choose a reason for hiding this comment

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

I think this check can be dropped, because TypeScript already validates the person is not undefined.

Also the indexOf would just return -1 if it's really undefined.

// Focus the middle item
midItem.focus();
it('focuses the first chip on focus', () => {
let FOCUS_EVENT: Event = {} as Event;
Copy link
Member

Choose a reason for hiding this comment

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

Looks like you are casting it twice.


it('BACKSPACE focuses the last chip', () => {
let nativeInput = chipListNativeElement.querySelector('input');
let BACKSPACE_EVENT: KeyboardEvent = new FakeKeyboardEvent(BACKSPACE, nativeInput) as any;
Copy link
Member

Choose a reason for hiding this comment

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

Same as above. The cast to any seems unnecessary here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The first one is declaring the type; not casting it. When attempting to cast it to KeyboardEvent it requires me to override everything in KeyboardEvent so @jelbourn told me to just us any for now.

Copy link
Member

Choose a reason for hiding this comment

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

Hm, you should be able to do.

let backspaceEvent = new FakeKeyboardEvent(BACKSPACE, nativeInput) as KeyboardEvent;

as in this example

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@devversion So, it looks like you are correct and this does indeed compile/run correctly, it's just that Webstorm is showing it as an error. I guess I need to play with my settings to figure out why.

@topherfangio
Copy link
Contributor Author

Please hold off on reviewing this; I accidentally pushed some focus code into this branch that needs to be removed. Will update when done, but I'm working on some md1 issues at the moment.

Copy link
Member

@jelbourn jelbourn left a comment

Choose a reason for hiding this comment

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

I started reviewing this before I saw your last comment 😫 Some of the comments might be useful, though

@@ -49,69 +49,125 @@ import {SPACE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes';
})
export class MdChipList implements AfterContentInit {

/** Track which chips we're listening to for focus/destruction. */
private _subscribed: WeakMap<MdChip, boolean> = new WeakMap();
/** Whether or not the chip list is currently focusable via keyboard interaction. */
Copy link
Member

Choose a reason for hiding this comment

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

This isn't a boolean, so Whether... isn't really the right way to phrase it. I think it's fine to say "tabIndex of the chip-list element."

protected _tabIndex = -1;

/** When a chip is destroyed, we track the index so we can focus the appropriate next chip. */
protected _destroyedIndex: number = null;
Copy link
Member

Choose a reason for hiding this comment

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

Why store this instead of moving the focus immediately?

*/
protected _checkTabIndex(): void {
// If we have 0 chips, we should not allow keyboard focus
this._tabIndex = (this.chips.length == 0 ? -1 : 0);
Copy link
Member

Choose a reason for hiding this comment

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

Instead of manually calling _checkTabIndex and updating _tabIndex, why not just create a host binding like

host: {
  '[attr.tabindex]': 'chips.length ? 0 : -1'
}

However, this should also be overridden by any user-provided tabIndex value, so that makes it a bit more complex.

*
* TODO: Remove when md-input-container allows md-empty configuration.
* */
protected _hackLabelElement() {
Copy link
Member

Choose a reason for hiding this comment

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

cc @mmalerba

What is it that you need the input to do here? I'd rather avoid merging something like this.


@Component({
template: `
<md-input-container>
Copy link
Member

Choose a reason for hiding this comment

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

cc @mmalerba - thoughts?

I'm not sure putting the chip-list inside of the input-container is the approach we ultimately want to take. Right now I don't think the input-container is in itself designed to have any content other than the input/textarea and friends.

I know we want to make the chips be part of the input's underline, but perhaps there's another way we could approach that.

Copy link
Contributor

Choose a reason for hiding this comment

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

I do want to support this sort of thing in the future (the mdInput directive would go on the md-chip-list though and md-chips-list would have to provide some input-like interface for mdInput to work with). For now though what about something like this:

<md-input-container>
  <md-chip-list md-prefix>
    <md-chip>Chip 1</md-chip>
    ...
  </md-chip-list>
  <input mdInput>
</md-input-container>


/** The ListKeyManager which handles focus. */
_keyManager: ListKeyManager;

/** The chip components contained within this chip list. */
chips: QueryList<MdChip>;

constructor(private _elementRef: ElementRef) { }
constructor(protected _renderer: Renderer, protected _elementRef: ElementRef,
@Optional() protected _mdInputContainer: MdInputContainer) {
Copy link
Member

Choose a reason for hiding this comment

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

So far we've been trying to avoid introducing any inter-component dependencies. In this case, I don't think we want to tightly couple the md-input with the chip-list, since people may use chips in combination with their own custom input (which is something Jen's team wants to do).

A less-coupled approach would be to create an interface like ChipInput, and accept that as an @Input (and making sure that either MdInput or MdInputContainer conforms to that interface. Then we could connect the two without introducing a dependency.

@jelbourn jelbourn added in progress This issue is currently in progress and removed pr: needs review labels Jan 13, 2017
@fxck
Copy link
Contributor

fxck commented Feb 15, 2017

Is there any progress on chips? Now that autocomplete is more or less done, it'd be nice to start working on https://material.angularjs.org/latest/demo/chips

Is it assigned to someone? I already have my own version working, I could try and make a PR..

@topherfangio
Copy link
Contributor Author

@fxck I am the developer assigned and am indeed working on the chips. I just took some time off to have a baby and am getting back to it :-)

I pushed some updates yesterday and should have more today. Hoping to have a mostly final version by this weekend, but it will need to go through the review process before being merged to master.

@topherfangio topherfangio force-pushed the team/topher/chips-remove-120 branch 2 times, most recently from 5794658 to f81b595 Compare February 17, 2017 22:46
$unselected-foreground: if($is-dark-theme, mat-color($foreground, text), $light-foreground);

$selected-background: if($is-dark-theme, mat-color($background, app-bar), #808080);
$selected-foreground: if($is-dark-theme, mat-color($foreground, text), $light-selected-foreground);

$focus-color: mat-color($foreground, text);
Copy link
Contributor

Choose a reason for hiding this comment

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

What about using mat-color($foreground, secondary-text) instead? It would match the borders of the checkbox and radio components.

@topherfangio topherfangio force-pushed the team/topher/chips-remove-120 branch 2 times, most recently from 1bc5658 to 44a73d2 Compare February 22, 2017 23:14
@topherfangio topherfangio added pr: needs review and removed in progress This issue is currently in progress labels Feb 22, 2017
@topherfangio
Copy link
Contributor Author

@kara @devversion Can you guys please review this updated PR at your convenience? It adds the mdChipRemove and the mdChipInput functionality along with some other small changes.

@willshowell willshowell mentioned this pull request Feb 27, 2017

// Fix the label offset
.mat-input-container mat-chip-list ~ label.mat-empty {
transform: translateY(22px);
Copy link
Member

Choose a reason for hiding this comment

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

Where are the 22px coming from?

.mat-input-container .mat-chip-list-wrapper input {
width: auto;
height: 38px;
margin-left: 8px;
Copy link
Member

Choose a reason for hiding this comment

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

Is there a way we can move those hard-coded values to variables?

And above for .mat-chip-remove, couldn't we calculate those left, right values?

@@ -145,6 +246,7 @@ export class MdChip implements Focusable, OnInit, OnDestroy {
if (el.nodeName.toLowerCase() == 'mat-basic-chip' || el.hasAttribute('mat-basic-chip') ||
el.nodeName.toLowerCase() == 'md-basic-chip' || el.hasAttribute('md-basic-chip')) {
el.classList.add('mat-basic-chip');
this._renderer.setElementClass(this._elementRef.nativeElement, `md-chip`, true);
Copy link
Member

Choose a reason for hiding this comment

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

Is this correct? Do we really need to have the md-chip class on the host-element?

And also above with classList.add('mat-chip'), I think this is obsolete since we already set the class in the host metadata.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch; this was leftover and I'm removing.

/** Emitted when the chip is focused. */
onFocus = new EventEmitter<MdChipEvent>();

/** Emitted when the removable property changes. */
onRemovableChange = new EventEmitter<boolean>();
Copy link
Member

Choose a reason for hiding this comment

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

We should not expose the EventEmitter.

class X {
  private _onRemovableChange = new EventEmitter<boolean>();
  onRemovableChange = this._onRemovableChange.asObservable();
}

});

it('DELETE does not emit the (remove) event', () => {
const DELETE_EVENT: KeyboardEvent = new FakeEvent(DELETE) as KeyboardEvent;
Copy link
Member

Choose a reason for hiding this comment

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

double cast shouldn't be necessary (also above).

TypeScript automatically interferes the types when doing const event = new FakeEvent(DELETE) as KeyboardEvent;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not really a double cast (it's just an explicit type declaration), but it is indeed extra code. I'll remove.

Add events, styling and keyboard controls to allow removable chips.

 - Add basic styling for a user-provided remove icon.
 - Add keyboard controls for backspace/delete.
 - Add `(remove)` event which is emitted when the remove icon or
   one of the delete keys is pressed.
 - Add `md-chip-remove` directive which can be applied to `<md-icon>`
   (or others) to inform the chip of the remove request.

Add new directive `mdChipInput` for controlling:
 - `(chipAdded)` - Event fired when a chip should be added.
 - `[separatorKeys]` - The list of keycodes that will fire the
   `(chipAdded)` event.
 - `[addOnBlur]` - Whether or not to fire the `(chipAdded)` event
   when the input is blurred.

Additionally, fix some issues with dark theme and add styling/support
for usage inside the `md-input-container` and add styling for focused
chips.

BREAKING CHANGE - The `selectable` property of the `md-chip-list` has
now been moved to `md-chip` to maintain consistency with the new
`removable` option.

If you used the following code,

```html
<md-chip-list [selectable]="selectable">
  <md-chip>My Chip</md-chip>
</md-chip-list>
```

you should switch it to

```html
<md-chip-list>
  <md-chip [selectable]="selectable">My Chip</md-chip>
</md-chip-list>
```

References angular#120. Closes angular#3143.

Hat tip to @willshowell for suggestion to use secondary-text
for focus color :-)
@JacobBrandt
Copy link

Works great. Looking forward to this being merged.

@willshowell
Copy link
Contributor

@topherfangio is this waiting for further review? Don't want it to get stuck in limbo!

@topherfangio
Copy link
Contributor Author

@JacobBrandt Thanks for the kind words! I'm glad you like it!

@willshowell It is indeed waiting for a final review and I am nagging the appropriate people to get it done ;-)

<md-chip color="warn" selected="true" *ngIf="visible"
(destroy)="alert('chip destroyed')" (click)="toggleVisible()">
(destroy)="alert('chip destroyed')" (remove)="toggleVisible()">
<md-icon md-chip-remove>cancel</md-icon>
Copy link
Member

Choose a reason for hiding this comment

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

Using the md-icon element directly here feels a little weird to me. Couple of thoughts:

  • I bet we can draw an x with css and avoid the need for the icon entirely.
  • md-icon has its own associated role and aria attributes, which may interfere with the chip's option-ness
  • I originally thought this should be a button element (since it's an action), but now I'm thinking that's probably not the case, since a screen-reader user will probably want to see the chip as an atomic option.

So, what do you think of changing this so that there's two directives:

  1. <md-chip-remove>, which has its own x icon (potentially drawn with css)
  2. [mdChipRemove] as an attribute that you apply when you want to do your own thing (no styles, only behavior)
    ?

@@ -16,34 +16,90 @@

// The spec only provides guidance for light-themed chips. When inside of a dark theme, fall back
// to standard background and foreground colors.
$unselected-background: if($is-dark-theme, mat-color($background, card), #e0e0e0);
$unselected-background: if($is-dark-theme, #656565, #e0e0e0);
Copy link
Member

Choose a reason for hiding this comment

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

Add a comment for the origin of #656565?


&:hover {
opacity: 0.54;
}
Copy link
Member

Choose a reason for hiding this comment

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

Any way to generally reduce the nesting in this file?

* to properly center the icon within the chip.
*/
@Directive({
selector: '[md-chip-remove], [mat-chip-remove], [mdChipRemove], [matChipRemove]',
Copy link
Member

Choose a reason for hiding this comment

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

No need for the dash-case attribute selectors any more.

/** Subscription for our onRemoveChange Observable */
_onRemoveChangeSubscription: Subscription;

constructor(protected _renderer: Renderer, protected _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.

Can be updated to Renderer2 now


set selectable(value: boolean) {
this._selectable = coerceBooleanProperty(value);
registerInput(inputElement: HTMLInputElement) {
Copy link
Member

Choose a reason for hiding this comment

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

Rather than being an HTMLInputElement, can we make this an interface that only defines the specific properties / methods the chip-list needs to interact with?

* Programmatically focus the chip list. This in turn focuses the first non-disabled chip in this
* chip list, or the input if available and there are 0 chips.
*
* TODO: ARIA says this should focus the first `selected` chip if any are selected.
Copy link
Member

Choose a reason for hiding this comment

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

Can you move the TODO to a separate // comment so that it doesn't appear in the API docs?

@@ -148,11 +175,19 @@ export class MdChipList implements AfterContentInit {
*
* @param chips The list of chips to be subscribed.
*/
protected _subscribeChips(chips: QueryList<MdChip>): void {
protected _subscribeChips(chips: QueryList < MdChip >): void {
Copy link
Member

Choose a reason for hiding this comment

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

You can omit the spaces inside the < >

* one.
*/
protected _checkDestroyedFocus() {
let chipsArray = this.chips.toArray();
Copy link
Member

Choose a reason for hiding this comment

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

The toArray shouldn't be necessary; QueryList has a length property

fixture.detectChanges();
});

it('SPACE selects/deselects the currently focused chip', () => {
Copy link
Member

Choose a reason for hiding this comment

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

Tests should start with the word should, e.g.,

it('should toggle selection with the space key', ...

@axtho
Copy link

axtho commented Apr 21, 2017

Any progress on this?

@sabithpocker
Copy link

Are there any versions with chips and remove implemented. I reached here after looking around for a while!

@DmitryEfimenko
Copy link

May I ask if there is any progress on this?
Just trying to figure out if I should wait or look for something else for the feature I'm working on.

@jelbourn
Copy link
Member

jelbourn commented Jun 2, 2017

This PR is being absorbed into #4912

@jelbourn jelbourn closed this Jun 2, 2017
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 6, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
cla: yes PR author has agreed to Google's Contributor License Agreement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

md-chips example doesn't work