[Fixed] Hide popup when input is blurred except when popup is clicked

Issue

I have the following use case:

There is an input and a popup should be displayed next to the input whenever it has focus. So while the input has focus, the user can either type into the input or use the popup to select something.

When the user is done, he can unfocus the element either by clicking
somewhere else or by pressing tab and focussing the next element.
However, if he clicks a button inside the popup, the popup should stay
open and the input should stay focused so that the user can continue
typing.

The problem that I have is that angular processes (blur) on the input before it processes (click) in the popup. That means that when the user
clicks in the popup, the input loses focus, it gets hidden, and then the
click of the user won’t be processed anymore.

I have made a stackblitz-demo for the problem:

This is the source code:

app.component.html

<h1>Hello</h1>
<p>
    Type 'one', 'two' or 'three' to change number or select a number
    from popup.
    <br>
    <input type="checkbox" [(ngModel)]="hideHelperOnBlur" /> Hide
    helper on blur (if this is set, then the buttons in the popup don't work
    but if this is not set, the popup doesn't close if the input looses
    focus -> how can I get both? The buttons to work but the popup still
    closing on blur?)
</p>
    Your number: {{selectedNumber}}
<p>
Change number:
<input [ngModel]="formattedNumber" (ngModelChange)="newNumberTyped($event)" (focus)="helperVisible = true"
    (blur)="processBlur()" #mainInput />

<div *ngIf="helperVisible">
    <button *ngFor="let number of numbers" (click)="selectNumber(number.raw)">{{number.formatted}} </button>
</div>

app.component.ts

import { Component, ViewChild, ElementRef } from '@angular/core';

@Component({
    selector: 'my-app',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent {
    name = 'Angular';
    formattedNumber: string = 'three';
    selectedNumber: number = 3;
    helperVisible: boolean = false;
    numbers: any[] = [
        { raw: 1, formatted: 'one' },
        { raw: 2, formatted: 'two' },
        { raw: 3, formatted: 'three' }
    ];
    @ViewChild('mainInput', { static: false })
    mainInput: ElementRef;
    hideHelperOnBlur: boolean;

    newNumberTyped(newFormattedNumber: string) {
        this.numbers.forEach((number) => {
            if (newFormattedNumber == number.formatted) {
                this.formattedNumber = newFormattedNumber;
                this.selectedNumber = number.raw;
            }
        });
    }

    selectNumber(newRawNumber: number) {
        this.numbers.forEach((number) => {
            if (newRawNumber == number.raw) {
                this.formattedNumber = number.formatted;
                this.selectedNumber = newRawNumber;
            }
        });
        this.mainInput.nativeElement.focus();
    }

    processBlur() {
        if (this.hideHelperOnBlur) {
            this.helperVisible = false;
        }
    }
}

I expect the following behavior:

  • When the input gets focus the popup is visible
  • When the user clicks outside anywhere on the page that is not the
    popup or the input, the popup closes
  • When the user presses the tab while the input is focussed, the input
    loses focus and the popup closes
  • When the user clicks a button in the popup, that number is selected

I only seem to be able to get either the second and third or the fourth
criteria to work (see the checkbox in the demo).

What I already tried:

  • Using @HostListener with focusin, focusout or focus or
    (blur)="someFunction(event) to find out which element has the new focus
    to check whether that element is in the popup or the input -> this
    doesn’t seem to work since at the time of focusout-event it is not yet
    clear who gets the new focus
  • using a timeout so that angular already finished processing the
    click event when the timeout is over -> this seems to work but working
    with timeouts is a really clunky solution that may break easily

Does anyone have a solution?

Solution

This is more of a Javascript thing. The blur event takes place before the click event. The click event only takes place once the mouse button is released.

You can use the mousedown event here to your advantage. The mousedown event takes place before the blur event. Simply call preventDefault on mousedown in the popover buttons to prevent the input from losing focus. This would also solve the issue where your input blinks when you click buttons in the popover so you can get rid of this.mainInput.nativeElement.focus(); in your selectNumber function.

<button *ngFor="let number of numbers" (mousedown)="$event.preventDefault()" (click)="selectNumber(number.raw)">{{number.formatted}}</button>

Here is a working example on StackBlitz.

Leave a Reply

(*) Required, Your email will not be published