Search code examples
javascriptangulareventsfocusblur

Hide popup when input is blurred except when popup is clicked


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.