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(material/chips): prevent chips from being deleted when user holds backspace #19700

Conversation

gioragutt
Copy link
Contributor

@gioragutt gioragutt commented Jun 19, 2020

  • By default, while holding backspace when the input is focused and empty, the last chip (if exists) would be focused.
  • This makes is so that when people delete the content they wrote, they will eventually start deleting chips.
  • This PR makes it so that only after the user releases the backspace after deleting all the content, subsequent backspace presses will focus the chips.

Edge Cases:

  • BS on initial empty input should focus last chip.
  • BS on initial non-empty input should not focus last chip (and not created new one due to focus out).
  • BS after changing input (from empty state or otherwise) should not focus last input or create chip.
  • BS after creating new chip should focus last input.

Fixes: #18659

@gioragutt gioragutt requested a review from jelbourn as a code owner June 19, 2020 22:10
@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Jun 19, 2020
@gioragutt
Copy link
Contributor Author

@mmalerba I've come across a pretty annoying edge case.

It might be due to a shitty design (the flag), but maybe not.

Currently, from within the chip-list itself, I don't see a way to actually know when a chip was created (via one of the separator keys, focusout, or any other way).

This makes it so that when a chip is created, I don't flip the flag back to true, and a double backspace press is needed.

I'll illustrate:

  • Focus input
  • Write something
  • Press TAB. This triggers focusout, creates a new chip, and focuses the next element in the page.
  • Press SHIFT + TAB. This focuses back on the input
  • Press BACKSPACE. Nothing happens (this is where the flag is flipped)
  • Press BACKSPACE. Last chip is focused.

I would love for a review of the current method I'm using (the boolean flag).
If you think there is any other way, which is less quirky, I'd love to know (even if you can't think of one, just tell me there's a better way).

I can expose an observable from MatChipTextControl if needed, but at the same time I don't know if this is a smart idea to add it there (idk what else might [be able to] use it).

Currently I don't see this class exposed via public api in the normal chips, but I see it's exposed in the MDC code.

@gioragutt
Copy link
Contributor Author

@mmalerba perhaps this entire behaviour should be moved to and managed by chip-input?

@jelbourn jelbourn requested a review from mmalerba June 22, 2020 20:13
@mmalerba
Copy link
Contributor

I think the flag approach looks fine, but yeah I would suggest moving all of this logic into the input. If that turns out to be too difficult for some reason, it should be ok to add a new observable to MatChipTextControl

@gioragutt
Copy link
Contributor Author

@mmalerba good to hear. I’ll work on that today 👌🏻 Neither of the methods should be hard, I’d rather do the cleanest approach

@gioragutt
Copy link
Contributor Author

gioragutt commented Jun 23, 2020

@mmalerba I have a question, don't quite know how to tackle this.

When the user creates a new chip (via one of the separator keys, blur, etc), it's up to them to reset the input value,

As shown in the chips demo in dev-app:

  add(event: MatChipInputEvent): void {
    const {input, value} = event;

    // Add our person
    if ((value || '').trim()) {
      this.people.push({ name: value.trim() });
    }

    // Reset the input value
    if (input) {
      input.value = '';
    }
  }

I've tried getting notified about when this happens (the input.value = '') but I can't quite make it.
It does not raise an input event, so it does not trigger _onInput(), and I also tried to subscribe to fromEvent(this._inputElement, 'input') and got nothing.

This is a problem we have regardless of whether the code is in chip-input or chip-list, and also if the user just decides to do the same on his own (as in - get reference to the input and mess with the input value).

Do you have any thoughts on how we can do that?

diff --git a/src/material/chips/chip-input.ts b/src/material/chips/chip-input.ts
index cf4812683..116803cf2 100644
--- a/src/material/chips/chip-input.ts
+++ b/src/material/chips/chip-input.ts
@@ -187,6 +187,12 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy, A
     }
     if (!event || this._isSeparatorKey(event)) {
       this.chipEnd.emit({ input: this._inputElement, value: this._inputElement.value });
+      setTimeout(() => {
+        if (this.empty) {
+          console.log('yay that worked');
+          this._focusLastChipOnBackspace = true;
+        }
+      }, 0);
 
       if (event) {
         event.preventDefault();

☝🏻 this works for the normal case (where the user does not mess with the input willingly), and fixes this as long as the change to the input done in the event handling.

Not sure if this is the prettiest fix, and AFAIK this will cause a bunch of change detections due to setTimeout, so maybe I should run it outside the angular zone?

Also I'm not quite sure how the setTimeout affects testability 🤨 nvm

@mmalerba
Copy link
Contributor

Hmmm yeah this is a tricky one. What if in the chip-list you listen to changes in the child chips and call a method on the chip input to reset the flag whenever the children change?

@gioragutt
Copy link
Contributor Author

@mmalerba problem here is that it's not about whether the chip was actually added to the list or not, but whether the input value was changed by messing with the dom element itself.

I'm thinking about all kinds of RxJS flows I can do with the input events and whatnot, but from my testing - this change does not raise an event, as I stated above.

Any chance there's another component in the library that had to deal with a similar issue? Is there someone I can ask, or perhaps you can give me directions to what kind of behaviour I should look for in other components to find that myself?

@mmalerba
Copy link
Contributor

The only way I can think of to detect when the input's value is programatically set would be to use Object.defineProperty to define a setter for the value property. However, this is not a pattern we currently use in our library and I don't think we want to start doing that.

You're right that resetting it when the chips change isn't exactly right, but in practice when the user types a separator character the input will be reset and a chip will be added, so it does cover the most common case.

To cover other edge cases people might have I would just add a clear method that sets the value to '' and resets the flag. You can update our examples to use that method so we guide people down the right path.

Does this sound like a good approach to you?

@gioragutt
Copy link
Contributor Author

Hey @mmalerba, I know it's been a while, hope you're doing fine (Covid and all).

The issue (#18659) got some comments these last few days so I decided to see what was left in the PR and fix it.

@gioragutt gioragutt force-pushed the giorag/chip-list-focus-last-chip-on-backspace-v2 branch from 822f42e to 692580c Compare February 12, 2021 21:59
@gioragutt gioragutt force-pushed the giorag/chip-list-focus-last-chip-on-backspace-v2 branch from 692580c to c1543a3 Compare February 12, 2021 22:00
Copy link
Contributor

@mmalerba mmalerba left a comment

Choose a reason for hiding this comment

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

Thanks for picking this back up, couple small things and it should be good to go.

/** Represents an input event on a `matChipInput`. */
export interface MatChipInputEvent {
/** The native `<input>` element that the event is being fired for. */
input: HTMLInputElement;

/** The value of the input. */
value: string;

/** Call to clear the value of the input */
clearInput(): void;
Copy link
Contributor

Choose a reason for hiding this comment

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

rather than having the clearInput method on the event, how about adding a reference to the MatChipInput and putting the method on the directive instead. Then people would do something like this: event.chipInput.clear()

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 idea! will change it 👍🏻

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What about the input itself? the HTMLInputElement... should it still be part of the event?

Copy link
Contributor

Choose a reason for hiding this comment

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

We can't remove it without going through a deprecation period. You can mark it as deprecated:

/**
 * @deprecated use `chipInput.inputElement`
 * @breaking-change 13.0.0 remove this property.
 */

(You'll need to change _inputElement on MatChipInput to public and remove the underscore)

src/material/chips/chip-input.ts Show resolved Hide resolved
@mmalerba mmalerba added the target: minor This PR is targeted for the next minor release label Feb 12, 2021
@mmalerba mmalerba added the merge: fix commit message When the PR is merged, rewrites/fixups of the commit messages are needed label Feb 16, 2021
Copy link
Contributor

@mmalerba mmalerba left a comment

Choose a reason for hiding this comment

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

Does the MDC-based one need to be updated as well or does it share the same underlying code? I would think at least the tests for the MDC-based chips need to be updated to add the same tests that were added for the non-MDC chips

@gioragutt
Copy link
Contributor Author

Does the MDC-based one need to be updated as well or does it share the same underlying code? I would think at least the tests for the MDC-based chips need to be updated to add the same tests that were added for the non-MDC chips

@mmalerba time I worked on this you (I think) said that it was okay to not deal with the MDC implementation. If it is necessary now I'll take a look.

I'm pretty sure its not the same underlying code, but it's only an assumption. What say you? Should I take a look?

@mmalerba
Copy link
Contributor

Ah, yeah since the time you started we've started to roll out the MDC-based components inside Google as an initial test before eventually moving them out of experimental. We'll need to make sure it works in the MDC one too so teams don't experience a regression as they migrate

@gioragutt
Copy link
Contributor Author

gioragutt commented Feb 16, 2021 via email

@mmalerba
Copy link
Contributor

IIRC MDC didn't have built in support for connecting the chips with an input, so we just built that part on top, in Angular. That should mean that you don't have to worry too much about the MDC stuff going on in the background.

If you do wind up needing to dive deeper into the MDC details, the "advanced approach" described in the MDC docs explains the approach we use to integration. And you can find the code for the MDC chips here.

@gioragutt
Copy link
Contributor Author

IIRC MDC didn't have built in support for connecting the chips with an input, so we just built that part on top, in Angular. That should mean that you don't have to worry too much about the MDC stuff going on in the background.

If you do wind up needing to dive deeper into the MDC details, the "advanced approach" described in the MDC docs explains the approach we use to integration. And you can find the code for the MDC chips here.

I'm looking at the code now, honestly it seems mostly copy-pasted, so it should be somewhat straightforward. I'm looking for any significant differences, so far it looks good.

@mmalerba
Copy link
Contributor

mmalerba commented Mar 1, 2021

It looks like there are a number of failures that will need to be investigated

@gioragutt
Copy link
Contributor Author

gioragutt commented Mar 1, 2021 via email

@mmalerba
Copy link
Contributor

mmalerba commented Mar 3, 2021

So part of the problem appears to be people passing objects like {input: ..., value: ...} to functions that expect MatChipInputEvent. In order to make this change less breaking, I suggest changing MatChipInputEvent like this:

interface MatChipInputEvent {
  /** 
   * The native `<input>` element that the event is being fired for.
   * @deprecated Use `MatChipInputEvent#chipInput.inputElement` instead.
   * @breaking-change 13.0.0 This property will be removed.
   */
  input: HTMLInputElement;

  /** The value of the input. */
  value: string;
  
  /** 
   * Reference to the chip input that emitted the event. 
   * @breaking-change 13.0.0 This property will be made required.
   */
  chipInput?: MatChipInput;
}

In our examples, you can then just assert that chipInput is defined like this: event.chipInput!.clear().

Lets try that and see how the tests go after

@gioragutt
Copy link
Contributor Author

gioragutt commented Mar 4, 2021 via email

@mmalerba
Copy link
Contributor

mmalerba commented Mar 4, 2021

I re-ran it with the fix applied and most of the errors went away. It looks like there's one more issue: When using the MDC chips the typing on MatChipInputEvent#chipInput is wrong because it refers to the non-MDC MatChipInput. The fix should be pretty straightforward. Instead of re-exporting the same interface from the non-MDC chips in the MDC package, just give MDC package its own version of the interface and export that

@gioragutt
Copy link
Contributor Author

gioragutt commented Mar 4, 2021 via email

@gioragutt
Copy link
Contributor Author

gioragutt commented Mar 5, 2021

I re-ran it with the fix applied and most of the errors went away. It looks like there's one more issue: When using the MDC chips the typing on MatChipInputEvent#chipInput is wrong because it refers to the non-MDC MatChipInput. The fix should be pretty straightforward. Instead of re-exporting the same interface from the non-MDC chips in the MDC package, just give MDC package its own version of the interface and export that

Hey man, I looked at the code and I can't seem to find the behavior you're describing.

src/material-experimental/mdc-chips/chip-input.ts exports its own MatChipInputEvent interface.

Perhaps someone that was using the MDC chips accidentally imported the non-MDC interface?

@mmalerba
Copy link
Contributor

mmalerba commented Mar 5, 2021

Sorry about that, you're right. That project must've been doing something weird. I'll see if I can fix their app so we can get this in.

@gioragutt
Copy link
Contributor Author

Hey @mmalerba 👋🏻

And update on that?

@mmalerba
Copy link
Contributor

mmalerba commented Mar 9, 2021

My shift as the team's release manager starts tomorrow, I plan to look into it then

@gioragutt
Copy link
Contributor Author

gioragutt commented Mar 9, 2021 via email

@mmalerba
Copy link
Contributor

I ran the tests again after making some fixes to internal apps and everything passed, so I'll merge this today.

We currently have ~150 PRs that could be merged if they didn't cause failures of some kind in Google's codebase. For some of those cases the tests are finding legitimate issues with the PR, but for others it may just be a matter of the tests needing to be updated. That's why it can sometimes take a long time to merge PRs that look like they're ready to go on Github.

@mmalerba mmalerba merged commit 6230560 into angular:master Mar 10, 2021
@mmalerba
Copy link
Contributor

Thanks for the hard work on this! 🎉

@gioragutt
Copy link
Contributor Author

Thanks for the hard work on this! 🎉

Thank you for helping me get this merged, your time is obviously invaluable, and I appreciate your responsiveness on this.

I've not experienced this in even smaller repos, so what you do is definitely not taken for granted.

Hope I'll find more opportunities to contribute!

@gioragutt gioragutt deleted the giorag/chip-list-focus-last-chip-on-backspace-v2 branch March 11, 2021 10:11
mmalerba added a commit to mmalerba/components that referenced this pull request Mar 12, 2021
@mmalerba
Copy link
Contributor

@gioragutt Unfortunately I had to revert this. It broke a team that was using a custom version of the chips input. I'll ask them to investigate and fix it so we can roll forward

@gioragutt
Copy link
Contributor Author

@mmalerba got it. As always, let me know if there's anything I can help with 👍🏻 I'm available for whatever is needed.

mmalerba added a commit to mmalerba/components that referenced this pull request Mar 15, 2021
…d when user holds backspace (angular#19700)""

This is a roll-forward of the original PR. We will need to resolve the
issues in Google's code base that caused it to be reverted before
merging again.

This reverts commit 4381b8e.
mmalerba added a commit to mmalerba/components that referenced this pull request Mar 15, 2021
mmalerba added a commit to mmalerba/components that referenced this pull request Mar 16, 2021
mmalerba added a commit that referenced this pull request Mar 16, 2021
#22235)

* Revert "Revert "feat(material/chips): prevent chips from being deleted when user holds backspace (#19700)""

This is a roll-forward of the original PR. We will need to resolve the
issues in Google's code base that caused it to be reverted before
merging again.

This reverts commit 4381b8e.

* fixup! Revert "Revert "feat(material/chips): prevent chips from being deleted when user holds backspace (#19700)""

* fixup! Revert "Revert "feat(material/chips): prevent chips from being deleted when user holds backspace (#19700)""
@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 Apr 12, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker cla: yes PR author has agreed to Google's Contributor License Agreement merge: fix commit message When the PR is merged, rewrites/fixups of the commit messages are needed P2 The issue is important to a large percentage of users, with a workaround target: minor This PR is targeted for the next minor release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[MatChipList] Disable last chip focus when pressing BACKSPACE with empty input
4 participants