diff --git a/src/lib/picker/picker.component.ts b/src/lib/picker/picker.component.ts
index 4edaf69..b013a55 100644
--- a/src/lib/picker/picker.component.ts
+++ b/src/lib/picker/picker.component.ts
@@ -402,6 +402,7 @@ export class PickerComponent implements OnInit, OnDestroy {
this.ref.detectChanges();
}
}
+
handleSearch($emojis: any[] | null) {
this.SEARCH_CATEGORY.emojis = $emojis;
for (const component of this.categoryRefs.toArray()) {
@@ -417,12 +418,18 @@ export class PickerComponent implements OnInit, OnDestroy {
this.handleScroll();
}
- handleEnterKey($event: Event, emoji?: EmojiData) {
+ handleEnterKey($event: Event, emoji?: EmojiData): void {
+ // Note: the `handleEnterKey` is invoked when the search component dispatches the
+ // `enterKeyOutsideAngular` event or when any emoji is clicked thus `emojiClickOutsideAngular`
+ // event is dispatched. Both events are dispatched outside of the Angular zone to prevent
+ // no-op ticks, basically when users outside of the picker component are not listening
+ // to any of these events.
+
if (!emoji) {
if (this.SEARCH_CATEGORY.emojis !== null && this.SEARCH_CATEGORY.emojis.length) {
emoji = this.SEARCH_CATEGORY.emojis[0];
if (emoji) {
- this.emojiSelect.emit({ $event, emoji });
+ dispatchInAngularContextIfObserved(this.emojiSelect, this.ngZone, { $event, emoji });
} else {
return;
}
@@ -435,8 +442,10 @@ export class PickerComponent implements OnInit, OnDestroy {
const component = this.categoryRefs.toArray()[1];
if (component && this.enableFrequentEmojiSort) {
- component.updateRecentEmojis();
- component.ref.markForCheck();
+ this.ngZone.run(() => {
+ component.updateRecentEmojis();
+ component.ref.markForCheck();
+ });
}
}
handleEmojiOver($event: EmojiEvent) {
@@ -455,6 +464,7 @@ export class PickerComponent implements OnInit, OnDestroy {
this.cancelAnimationFrame();
this.ref.detectChanges();
}
+
handleEmojiLeave() {
if (!this.showPreview || !this.previewRef) {
return;
@@ -468,16 +478,21 @@ export class PickerComponent implements OnInit, OnDestroy {
this.ref.detectChanges();
});
}
+
handleEmojiClick($event: EmojiEvent) {
- this.emojiClick.emit($event);
- this.emojiSelect.emit($event);
+ // Note: we're getting back into the Angular zone because click events on emojis are handled
+ // outside of the Angular zone.
+ dispatchInAngularContextIfObserved(this.emojiClick, this.ngZone, $event);
+ dispatchInAngularContextIfObserved(this.emojiSelect, this.ngZone, $event);
this.handleEnterKey($event.$event, $event.emoji);
}
+
handleSkinChange(skin: Emoji['skin']) {
this.skin = skin;
localStorage.setItem(`${this.NAMESPACE}.skin`, String(skin));
this.skinChange.emit(skin);
}
+
getWidth(): string {
if (this.style && this.style.width) {
return this.style.width;
@@ -492,3 +507,16 @@ export class PickerComponent implements OnInit, OnDestroy {
}
}
}
+
+/**
+ * This is only a helper function because the same code is being re-used many times.
+ */
+function dispatchInAngularContextIfObserved
(
+ emitter: EventEmitter,
+ ngZone: NgZone,
+ value: T,
+): void {
+ if (emitter.observed) {
+ ngZone.run(() => emitter.emit(value));
+ }
+}
diff --git a/src/lib/picker/search.component.ts b/src/lib/picker/search.component.ts
index f2225f8..d684a4a 100644
--- a/src/lib/picker/search.component.ts
+++ b/src/lib/picker/search.component.ts
@@ -4,6 +4,8 @@ import {
ElementRef,
EventEmitter,
Input,
+ NgZone,
+ OnDestroy,
OnInit,
Output,
ViewChild,
@@ -11,6 +13,7 @@ import {
import { FormsModule } from '@angular/forms';
import { EmojiSearch } from './emoji-search.service';
+import { Subject, fromEvent, takeUntil } from 'rxjs';
let id = 0;
@@ -22,7 +25,6 @@ let id = 0;
[id]="inputId"
#inputRef
type="search"
- (keyup.enter)="handleEnterKey($event)"
[placeholder]="i18n.search"
[autofocus]="autoFocus"
[(ngModel)]="query"
@@ -59,7 +61,7 @@ let id = 0;
standalone: true,
imports: [FormsModule],
})
-export class SearchComponent implements AfterViewInit, OnInit {
+export class SearchComponent implements AfterViewInit, OnInit, OnDestroy {
@Input() maxResults = 75;
@Input() autoFocus = false;
@Input() i18n: any;
@@ -69,35 +71,38 @@ export class SearchComponent implements AfterViewInit, OnInit {
@Input() icons!: { [key: string]: string };
@Input() emojisToShowFilter?: (x: any) => boolean;
@Output() searchResults = new EventEmitter();
- @Output() enterKey = new EventEmitter();
- @ViewChild('inputRef', { static: true }) private inputRef!: ElementRef;
+ @Output() enterKeyOutsideAngular = new EventEmitter();
+ @ViewChild('inputRef', { static: true }) private inputRef!: ElementRef;
isSearching = false;
icon?: string;
query = '';
inputId = `emoji-mart-search-${++id}`;
- constructor(private emojiSearch: EmojiSearch) {}
+ private destroy$ = new Subject();
+
+ constructor(private ngZone: NgZone, private emojiSearch: EmojiSearch) {}
ngOnInit() {
this.icon = this.icons.search;
+ this.setupKeyupListener();
}
+
ngAfterViewInit() {
if (this.autoFocus) {
this.inputRef.nativeElement.focus();
}
}
+
+ ngOnDestroy(): void {
+ this.destroy$.next();
+ }
+
clear() {
this.query = '';
this.handleSearch('');
this.inputRef.nativeElement.focus();
}
- handleEnterKey($event: Event) {
- if (!this.query) {
- return;
- }
- this.enterKey.emit($event);
- $event.preventDefault();
- }
+
handleSearch(value: string) {
if (value === '') {
this.icon = this.icons.search;
@@ -116,7 +121,22 @@ export class SearchComponent implements AfterViewInit, OnInit {
) as any[];
this.searchResults.emit(emojis);
}
+
handleChange() {
this.handleSearch(this.query);
}
+
+ private setupKeyupListener(): void {
+ this.ngZone.runOutsideAngular(() =>
+ fromEvent(this.inputRef.nativeElement, 'keyup')
+ .pipe(takeUntil(this.destroy$))
+ .subscribe($event => {
+ if (!this.query || $event.key !== 'Enter') {
+ return;
+ }
+ this.enterKeyOutsideAngular.emit($event);
+ $event.preventDefault();
+ }),
+ );
+ }
}