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: Do not trigger change detection internally on mouseenter #420

Merged
merged 1 commit into from
May 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/lib/picker/category.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import { EmojiFrequentlyService } from './emoji-frequently.service';
[imageUrlFn]="emojiImageUrlFn"
[hideObsolete]="hideObsolete"
[useButton]="emojiUseButton"
(emojiOver)="emojiOver.emit($event)"
(emojiOverOutsideAngular)="emojiOverOutsideAngular.emit($event)"
(emojiLeaveOutsideAngular)="emojiLeaveOutsideAngular.emit($event)"
(emojiClick)="emojiClick.emit($event)"
></ngx-emoji>
Expand Down Expand Up @@ -93,7 +93,7 @@ import { EmojiFrequentlyService } from './emoji-frequently.service';
[imageUrlFn]="emojiImageUrlFn"
[hideObsolete]="hideObsolete"
[useButton]="emojiUseButton"
(emojiOver)="emojiOver.emit($event)"
(emojiOverOutsideAngular)="emojiOverOutsideAngular.emit($event)"
(emojiLeaveOutsideAngular)="emojiLeaveOutsideAngular.emit($event)"
(emojiClick)="emojiClick.emit($event)"
></ngx-emoji>
Expand Down Expand Up @@ -126,12 +126,12 @@ export class CategoryComponent implements OnChanges, OnInit, AfterViewInit {
@Input() emojiBackgroundImageFn?: Emoji['backgroundImageFn'];
@Input() emojiImageUrlFn?: Emoji['imageUrlFn'];
@Input() emojiUseButton?: boolean;
@Output() emojiOver: Emoji['emojiOver'] = new EventEmitter();
@Output() emojiClick: Emoji['emojiClick'] = new EventEmitter();
/**
* Note: the suffix is added explicitly so we know the event is dispatched outside of the Angular zone.
*/
@Output() emojiOverOutsideAngular: Emoji['emojiOver'] = new EventEmitter();
@Output() emojiLeaveOutsideAngular: Emoji['emojiLeave'] = new EventEmitter();
@Output() emojiClick: Emoji['emojiClick'] = new EventEmitter();
@ViewChild('container', { static: true }) container!: ElementRef;
@ViewChild('label', { static: true }) label!: ElementRef;
containerStyles: any = {};
Expand Down
27 changes: 27 additions & 0 deletions src/lib/picker/ngx-emoji/emoji.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@ import { TestBed } from '@angular/core/testing';
import { EmojiModule } from './emoji.module';

describe('EmojiComponent', () => {
it('should trigger change detection whenever `emojiOver` has observers', () => {
@Component({
template: '<ngx-emoji (emojiOver)="onEmojiOver()"></ngx-emoji>',
})
class TestComponent {
onEmojiOver() {}
}

TestBed.configureTestingModule({
imports: [EmojiModule],
declarations: [TestComponent],
});

const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();

const appRef = TestBed.inject(ApplicationRef);
spyOn(appRef, 'tick');
spyOn(fixture.componentInstance, 'onEmojiOver');

const emoji = fixture.nativeElement.querySelector('span.emoji-mart-emoji');
emoji.dispatchEvent(new MouseEvent('mouseenter'));

expect(appRef.tick).toHaveBeenCalledTimes(1);
expect(fixture.componentInstance.onEmojiOver).toHaveBeenCalled();
});

it('should trigger change detection whenever `emojiLeave` has observers', () => {
@Component({
template: '<ngx-emoji (emojiLeave)="onEmojiLeave()"></ngx-emoji>',
Expand Down
66 changes: 37 additions & 29 deletions src/lib/picker/ngx-emoji/emoji.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ export interface EmojiEvent {
#button
type="button"
(click)="handleClick($event)"
(mouseenter)="handleOver($event)"
[attr.title]="title"
[attr.aria-label]="label"
class="emoji-mart-emoji"
Expand All @@ -67,7 +66,6 @@ export interface EmojiEvent {
<span
#button
(click)="handleClick($event)"
(mouseenter)="handleOver($event)"
[attr.title]="title"
[attr.aria-label]="label"
class="emoji-mart-emoji"
Expand Down Expand Up @@ -99,16 +97,19 @@ export class EmojiComponent implements OnChanges, Emoji, OnDestroy {
@Input() sheetRows?: number;
@Input() sheetColumns?: number;
@Input() useButton?: boolean;
@Output() emojiOver: Emoji['emojiOver'] = new EventEmitter();
@Output() emojiClick: Emoji['emojiClick'] = new EventEmitter();
/**
* Note: `emojiLeave` and `emojiLeaveOutsideAngular` are dispatched on the same event, but for different
* purposes. The `emojiLeaveOutsideAngular` would be set up in category component so we don't care
* about zone context the callback is being called in. The `emojiLeave` is for backwards compatibility
* if anyone is listening to this event explicitly in their code.
* Note: `emojiOver` and `emojiOverOutsideAngular` are dispatched on the same event (`mouseenter`), but
* for different purposes. The `emojiOverOutsideAngular` event is listened only in `emoji-category`
* component and the category component doesn't care about zone context the callback is being called in.
* The `emojiOver` is for backwards compatibility if anyone is listening to this event explicitly in their code.
*/
@Output() emojiOver: Emoji['emojiOver'] = new EventEmitter();
@Output() emojiOverOutsideAngular: Emoji['emojiOver'] = new EventEmitter();
/** See comments above, this serves the same purpose. */
@Output() emojiLeave: Emoji['emojiLeave'] = new EventEmitter();
@Output() emojiLeaveOutsideAngular: Emoji['emojiLeave'] = new EventEmitter();
@Output() emojiClick: Emoji['emojiClick'] = new EventEmitter();

style: any;
title?: string = undefined;
label = '';
Expand Down Expand Up @@ -139,7 +140,7 @@ export class EmojiComponent implements OnChanges, Emoji, OnDestroy {
private readonly emojiService = inject(EmojiService);

constructor() {
this.setupMouseLeaveListener();
this.setupMouseListeners();
}

ngOnChanges() {
Expand Down Expand Up @@ -238,27 +239,34 @@ export class EmojiComponent implements OnChanges, Emoji, OnDestroy {
this.emojiClick.emit({ emoji, $event });
}

handleOver($event: Event) {
const emoji = this.getSanitizedData();
this.emojiOver.emit({ emoji, $event });
}

private setupMouseLeaveListener(): void {
this.button$
.pipe(
private setupMouseListeners(): void {
const eventListener$ = (eventName: string) =>
this.button$.pipe(
// Note: `EMPTY` is used to remove event listener once the DOM node is removed.
switchMap(button => (button ? fromEvent(button, 'mouseleave') : EMPTY)),
switchMap(button => (button ? fromEvent(button, eventName) : EMPTY)),
takeUntil(this.destroy$),
)
.subscribe($event => {
const emoji = this.getSanitizedData();
this.emojiLeaveOutsideAngular.emit({ emoji, $event });
// Note: this is done for backwards compatibility. We run change detection if developers
// are listening to `emojiLeave` in their code. For instance:
// `<ngx-emoji (emojiLeave)="..."></ngx-emoji>`.
if (this.emojiLeave.observed) {
this.ngZone.run(() => this.emojiLeave.emit({ emoji, $event }));
}
});
);

eventListener$('mouseenter').subscribe($event => {
const emoji = this.getSanitizedData();
this.emojiOverOutsideAngular.emit({ emoji, $event });
// Note: this is done for backwards compatibility. We run change detection if developers
// are listening to `emojiOver` in their code. For instance:
// `<ngx-emoji (emojiOver)="..."></ngx-emoji>`.
if (this.emojiOver.observed) {
this.ngZone.run(() => this.emojiOver.emit({ emoji, $event }));
}
});

eventListener$('mouseleave').subscribe($event => {
const emoji = this.getSanitizedData();
this.emojiLeaveOutsideAngular.emit({ emoji, $event });
// Note: this is done for backwards compatibility. We run change detection if developers
// are listening to `emojiLeave` in their code. For instance:
// `<ngx-emoji (emojiLeave)="..."></ngx-emoji>`.
if (this.emojiLeave.observed) {
this.ngZone.run(() => this.emojiLeave.emit({ emoji, $event }));
}
});
}
}
2 changes: 1 addition & 1 deletion src/lib/picker/picker.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
[emojiBackgroundImageFn]="backgroundImageFn"
[emojiImageUrlFn]="imageUrlFn"
[emojiUseButton]="useButton"
(emojiOver)="handleEmojiOver($event)"
(emojiOverOutsideAngular)="handleEmojiOver($event)"
(emojiLeaveOutsideAngular)="handleEmojiLeave()"
(emojiClick)="handleEmojiClick($event)"
></emoji-category>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/picker/picker.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('PickerComponent', () => {
fixture.detectChanges();
});

it('should update preview on `mouseleave` but should not trigger change detection', fakeAsync(() => {
it('should update preview on `mouseenter` and `mouseleave` but should not trigger change detection', fakeAsync(() => {
const picker = fixture.debugElement.query(By.directive(PickerComponent));
expect(picker.componentInstance.previewEmoji).toEqual(null);

Expand Down