Skip to content

Commit

Permalink
feat: ignore contentEditable elements
Browse files Browse the repository at this point in the history
  • Loading branch information
roennibus committed Jun 10, 2022
1 parent 6ce43b8 commit 4185015
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 10 deletions.
30 changes: 20 additions & 10 deletions projects/ngneat/hotkeys/src/lib/hotkeys.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { debounceTime, filter, finalize, mergeMap, takeUntil, tap } from 'rxjs/o
import { coerceArray } from './utils/array';
import { hostPlatform, normalizeKeys } from './utils/platform';

export type AllowInElement = 'INPUT' | 'TEXTAREA' | 'SELECT';
export type AllowInElement = 'INPUT' | 'TEXTAREA' | 'SELECT' | 'CONTENTEDITABLE';
interface Options {
group: string;
element: HTMLElement;
Expand Down Expand Up @@ -54,7 +54,7 @@ export class HotkeysService {
private sequenceMaps = new Map<HTMLElement, SequenceSummary>();
private sequenceDebounce: number = 250;

constructor(private eventManager: EventManager, @Inject(DOCUMENT) private document) {}
constructor(private eventManager: EventManager, @Inject(DOCUMENT) private document: Document) {}

getHotkeys(): Hotkey[] {
const sequenceKeys = Array.from(this.sequenceMaps.values())
Expand Down Expand Up @@ -145,10 +145,7 @@ export class HotkeysService {

return getSequenceCompleteObserver().pipe(
takeUntil<Hotkey>(this.dispose.pipe(filter(v => v === normalizedKeys))),
filter(hotkey => {
const excludedTargets = this.getExcludedTargets(hotkey.allowIn || []);
return !excludedTargets?.includes(document.activeElement.nodeName);
}),
filter(hotkey => !this.targetIsExcluded(hotkey.allowIn)),
tap(hotkey => this.callbacks.forEach(cb => cb(hotkey, normalizedKeys, hotkey.element))),
finalize(() => this.removeShortcuts(normalizedKeys))
);
Expand All @@ -169,9 +166,8 @@ export class HotkeysService {
return new Observable(observer => {
const handler = (e: KeyboardEvent) => {
const hotkey = this.hotkeys.get(normalizedKeys);
const excludedTargets = this.getExcludedTargets(hotkey.allowIn || []);
const skipShortcutTrigger = this.targetIsExcluded(hotkey.allowIn);

const skipShortcutTrigger = excludedTargets && excludedTargets.includes(document.activeElement.nodeName);
if (skipShortcutTrigger) {
return;
}
Expand Down Expand Up @@ -237,7 +233,21 @@ export class HotkeysService {
});
}

private getExcludedTargets(allowIn: AllowInElement[]) {
return ['INPUT', 'SELECT', 'TEXTAREA'].filter(t => !allowIn.includes(t as AllowInElement));
private targetIsExcluded(allowIn?: AllowInElement[]) {
const activeElement = this.document.activeElement;
const elementName = activeElement.nodeName;
const elementIsContentEditable = (activeElement as HTMLElement).isContentEditable;
let isExcluded = ['INPUT', 'SELECT', 'TEXTAREA'].includes(elementName) || elementIsContentEditable;

if (isExcluded && allowIn?.length) {
for (let t of allowIn) {
if (activeElement.nodeName === t || (t === 'CONTENTEDITABLE' && elementIsContentEditable)) {
isExcluded = false;
break;
}
}
}

return isExcluded;
}
}
64 changes: 64 additions & 0 deletions projects/ngneat/hotkeys/src/lib/tests/hotkeys.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,30 @@ describe('Directive: Hotkeys', () => {
expect(spyFcn).toHaveBeenCalled();
});

it('should ignore hotkey when typing in a contentEditable element', () => {
const spyFcn = createSpy('subscribe', (...args) => {});
spectator = createDirective(`<div [hotkeys]="'a'"><div contenteditable="true"></div></div>`);
spectator.output('hotkey').subscribe(spyFcn);
spyOnProperty(document.activeElement, 'nodeName', 'get').and.returnValue('DIV');
spyOnProperty(document.activeElement as HTMLElement, 'isContentEditable', 'get').and.returnValue(true);
spectator.dispatchKeyboardEvent(spectator.element.firstElementChild, 'keydown', 'a');
spectator.fixture.detectChanges();
expect(spyFcn).not.toHaveBeenCalled();
});

it('should trigger hotkey when typing in a contentEditable element', () => {
const spyFcn = createSpy('subscribe', (...args) => {});
spectator = createDirective(
`<div [hotkeys]="'a'" [hotkeysOptions]="{allowIn: ['CONTENTEDITABLE']}"><div contenteditable="true"></div></div>`
);
spectator.output('hotkey').subscribe(spyFcn);
spyOnProperty(document.activeElement, 'nodeName', 'get').and.returnValue('DIV');
spyOnProperty(document.activeElement as HTMLElement, 'isContentEditable', 'get').and.returnValue(true);
spectator.dispatchKeyboardEvent(spectator.element.firstElementChild, 'keydown', 'a');
spectator.fixture.detectChanges();
expect(spyFcn).toHaveBeenCalled();
});

it('should register hotkey', () => {
spectator = createDirective(`<div [hotkeys]="'a'"></div>`);
spectator.fixture.detectChanges();
Expand Down Expand Up @@ -148,6 +172,46 @@ describe('Directive: Sequence Hotkeys', () => {
return run();
});

it('should ignore hotkey when typing in a contentEditable element', () => {
const run = async () => {
// * Need to space out time to prevent other test keystrokes from interfering with sequence
await sleep(250);
const spyFcn = createSpy('subscribe', (...args) => {});
spectator = createDirective(`<div [hotkeys]="'g>n'" [isSequence]="true"><div contenteditable="true"></div>`);
spectator.output('hotkey').subscribe(spyFcn);
spyOnProperty(document.activeElement, 'nodeName', 'get').and.returnValue('DIV');
spyOnProperty(document.activeElement as HTMLElement, 'isContentEditable', 'get').and.returnValue(true);
spectator.dispatchKeyboardEvent(spectator.element.firstElementChild, 'keydown', 'g', spectator.element);
spectator.dispatchKeyboardEvent(spectator.element.firstElementChild, 'keydown', 'n', spectator.element);
await sleep(250);
spectator.fixture.detectChanges();
expect(spyFcn).not.toHaveBeenCalled();
};

return run();
});

it('should trigger hotkey when typing in a contentEditable element', () => {
const run = async () => {
// * Need to space out time to prevent other test keystrokes from interfering with sequence
await sleep(250);
const spyFcn = createSpy('subscribe', (...args) => {});
spectator = createDirective(
`<div [hotkeys]="'g>n'" [isSequence]="true" [hotkeysOptions]="{allowIn: ['CONTENTEDITABLE']}"><div contenteditable="true"></div>`
);
spectator.output('hotkey').subscribe(spyFcn);
spyOnProperty(document.activeElement, 'nodeName', 'get').and.returnValue('DIV');
spyOnProperty(document.activeElement as HTMLElement, 'isContentEditable', 'get').and.returnValue(true);
spectator.dispatchKeyboardEvent(spectator.element.firstElementChild, 'keydown', 'g', spectator.element);
spectator.dispatchKeyboardEvent(spectator.element.firstElementChild, 'keydown', 'n', spectator.element);
await sleep(250);
spectator.fixture.detectChanges();
expect(spyFcn).toHaveBeenCalled();
};

return run();
});

it('should register hotkey', () => {
spectator = createDirective(`<div [hotkeys]="'g>p'" [isSequence]="true"></div>`);
spectator.fixture.detectChanges();
Expand Down

0 comments on commit 4185015

Please sign in to comment.