Skip to content

Commit 3dc1bcd

Browse files
authored
Fixed Mouseover Pre-popup Causing Typing Error - PR#7 from glenn2223
### Fixed - The mouse being over the popup when it's rendered no longer selects that value whilst typing ### Changes - Added CI based testing for every PR
2 parents bc70f6c + 7d7d1f7 commit 3dc1bcd

File tree

4 files changed

+205
-21
lines changed

4 files changed

+205
-21
lines changed

.github/workflows/test.yml

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Tests
2+
on:
3+
pull_request:
4+
branches:
5+
- '**'
6+
7+
# ALLOW MANUAL RUNS
8+
workflow_dispatch:
9+
10+
jobs:
11+
test:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: '20.x'
18+
- run: npm ci
19+
- run: npm test
20+
- run: npm run prepublishOnly

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ All notable changes to this project will be documented in this file.
2626
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
2727
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
2828

29+
## UNPUBLISHED
30+
31+
### Fixed
32+
33+
- The mouse being over the popup when it's rendered no longer selects that value whilst typing
34+
35+
### Changes
36+
37+
- Developer dependency bumps (no user-facing changes)
38+
- Added CI based testing for every PR
39+
2940
## [1.0.1] - 2023-11-14
3041

3142
<small>[Compare to previous release][comp:1.0.1]</small>

src/index.ts

+86-21
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use strict';
2+
13
import {
24
AutocompleteEventFunction as EventFunction,
35
CloseEventData,
@@ -29,6 +31,7 @@ export class Autocomplete<T = { label: string; value: string }> {
2931
lastTerm: string;
3032
valueStore?: string;
3133
focusValue?: string;
34+
focusPoint?: [number, number];
3235
}
3336
> = {};
3437

@@ -168,8 +171,14 @@ export class Autocomplete<T = { label: string; value: string }> {
168171
this._removeFocus(data.ul);
169172

170173
// Focus on the new one
171-
const liEl = <HTMLLIElement>ev.target,
172-
newVal = liEl.dataset.value || liEl.innerText;
174+
const liEl = (<HTMLElement>ev.target).closest('li');
175+
176+
if (!liEl) {
177+
return;
178+
}
179+
180+
const newVal = liEl.dataset.value || liEl.innerText;
181+
173182
liEl.classList.add('focused');
174183

175184
// Update the input value and store
@@ -200,10 +209,10 @@ export class Autocomplete<T = { label: string; value: string }> {
200209
if (typeof (data.item as { link?: string }).link === 'string') {
201210
window.location.href = (data.item as { link: string }).link;
202211
} else {
203-
const liEl = <HTMLLIElement>ev.target;
212+
const liEl = (<HTMLElement>ev.target).closest('li');
204213

205214
// Set input value
206-
data.input.value = liEl.dataset.value ?? liEl.innerText;
215+
data.input.value = liEl?.dataset.value ?? liEl?.innerText ?? '';
207216
this._stateData[data.ul.id].valueStore = data.input.value;
208217

209218
this._clearFocusStore(data.ul.id);
@@ -229,20 +238,36 @@ export class Autocomplete<T = { label: string; value: string }> {
229238

230239
this.options.onOpen?.(ev, data);
231240

232-
const tL = position({
241+
const { top, left } = position({
233242
target: data.ul,
234243
anchor: <HTMLElement>ev.target,
235244
my: this.options.position.my,
236245
at: this.options.position.at,
237246
collision: this.options.position.collision,
238247
});
239248

240-
data.ul.style.top = tL.top;
241-
data.ul.style.left = tL.left;
249+
data.ul.style.top = top;
250+
data.ul.style.left = left;
242251
data.ul.hidden = false;
243252

244253
if (this.options.autoFocus) {
245254
data.ul.children[0]?.dispatchEvent(new Event('focus'));
255+
} else {
256+
this._stateData[data.ul.id].focusPoint = [-1, -1];
257+
258+
// If they aren't already hovering over it, remove the focusPoint
259+
// so we can trigger mouseover events immediately
260+
setTimeout(() => {
261+
const focusPoint = this._stateData[data.ul.id].focusPoint;
262+
263+
if (
264+
focusPoint &&
265+
focusPoint[0] === -1 &&
266+
focusPoint[1] === -1
267+
) {
268+
this._stateData[data.ul.id].focusPoint = undefined;
269+
}
270+
}, 333);
246271
}
247272

248273
this._traceLog('Opened menu', `Menu id: ${data.ul.id}`);
@@ -279,6 +304,7 @@ export class Autocomplete<T = { label: string; value: string }> {
279304
target.value = vS;
280305

281306
this._stateData[data.ul.id].valueStore = undefined;
307+
this._stateData[data.ul.id].focusValue = undefined;
282308

283309
this._infoLog('Reverted input', `Input ac-id: ${data.ul.id}`);
284310
}
@@ -306,8 +332,8 @@ export class Autocomplete<T = { label: string; value: string }> {
306332
)
307333
).json() as Promise<ListItemType<T>[]>)
308334
: typeof this.options.source === 'function'
309-
? this.options.source({ term: data.term })
310-
: this.options.source,
335+
? this.options.source({ term: data.term })
336+
: this.options.source,
311337
});
312338
} catch {
313339
return;
@@ -491,9 +517,14 @@ export class Autocomplete<T = { label: string; value: string }> {
491517
};
492518

493519
private _itemClickEvent = (ev: MouseEvent) => {
494-
const li = <HTMLLIElement>ev.target,
495-
ul = <HTMLUListElement>li.parentElement,
496-
id = ul.id,
520+
const li = (<HTMLElement>ev.target).closest('li'),
521+
ul = li?.closest('ul');
522+
523+
if (!ul || !li) {
524+
return;
525+
}
526+
527+
const id = ul.id,
497528
item =
498529
this._stateData[id].data[Array.from(ul.children).indexOf(li)],
499530
input = <HTMLInputElement>(
@@ -505,19 +536,53 @@ export class Autocomplete<T = { label: string; value: string }> {
505536
this.itemSelect(ev, { ul, item, input });
506537
};
507538

508-
private _itemFocusEvent = (ev: FocusEvent) => {
509-
const li = <HTMLLIElement>ev.target,
510-
ul = <HTMLUListElement>li.parentElement,
511-
id = ul.id,
539+
private _itemFocusEvent = (ev: FocusEvent | MouseEvent) => {
540+
const li = (<HTMLElement>ev.target).closest('li'),
541+
ul = li?.closest('ul');
542+
543+
if (!ul || !li) {
544+
return;
545+
}
546+
547+
const id = ul.id,
512548
item =
513549
this._stateData[id].data[Array.from(ul.children).indexOf(li)],
514550
input = <HTMLInputElement>(
515551
document.querySelector(`input[data-ac-id='${id}']`)
516-
);
552+
),
553+
that = this;
554+
555+
if (ev instanceof MouseEvent && this._stateData[id].focusPoint) {
556+
const [x, y] = this._stateData[id].focusPoint;
557+
558+
if (x === -1 && y === -1) {
559+
this._stateData[id].focusPoint = [ev.clientX, ev.clientY];
560+
li.addEventListener('mousemove', handlePopHover);
561+
562+
return;
563+
}
564+
565+
this._stateData[id].focusPoint = undefined;
566+
}
517567

518568
this._traceLog('Menu item focused', `Item summary: ${li.innerText}`);
519569

520570
this.itemFocus(ev, { ul, item, input });
571+
572+
function handlePopHover(this: HTMLLIElement, subEv: MouseEvent) {
573+
const focusPoint = that._stateData[id].focusPoint;
574+
575+
if (
576+
focusPoint === undefined ||
577+
Math.abs(focusPoint[0] - subEv.clientX) > 5 ||
578+
Math.abs(focusPoint[1] - subEv.clientY) > 5
579+
) {
580+
that._stateData[id].focusPoint = undefined;
581+
li!.removeEventListener('mousemove', handlePopHover);
582+
583+
li!.dispatchEvent(new MouseEvent('mouseover', subEv));
584+
}
585+
}
521586
};
522587

523588
private _removeFocus = (ul: HTMLUListElement) => {
@@ -555,25 +620,25 @@ export class Autocomplete<T = { label: string; value: string }> {
555620
private _debrottle<F extends { (someEv: Event): void }>(func: F) {
556621
const that = this;
557622
let calledAgain: boolean;
558-
let dTimer: NodeJS.Timer | number | undefined;
623+
let dTimer: ReturnType<typeof setTimeout> | undefined;
559624

560625
return function (this: ThisParameterType<F>, ...args: Parameters<F>) {
561626
if (dTimer) {
562627
calledAgain = true;
563628
} else {
564-
const context = this;
629+
const subThat = this;
565630

566631
dTimer = setTimeout(() => {
567632
if (calledAgain) {
568633
calledAgain = false;
569634

570-
func.apply(context, args);
635+
func.apply(subThat, args);
571636
}
572637

573638
dTimer = undefined;
574639
}, that.options.delay);
575640

576-
func.apply(context, args);
641+
func.apply(subThat, args);
577642
}
578643
};
579644
}

tests/mouseover.test.ts

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { Autocomplete, AutocompleteStatus } from '../src/index';
2+
3+
jest.useFakeTimers();
4+
5+
describe('Mouseover Tests', () => {
6+
let inputEL: HTMLInputElement, autocomplete: Autocomplete;
7+
8+
describe('Test environment:-', () => {
9+
it('has added element', () => {
10+
inputEL = document.createElement('input');
11+
12+
inputEL.classList.add('test');
13+
inputEL = document.body.insertAdjacentElement(
14+
'beforeend',
15+
inputEL,
16+
) as HTMLInputElement;
17+
18+
expect(inputEL).not.toBeNull();
19+
});
20+
21+
it('has created autocomplete', () => {
22+
autocomplete = new Autocomplete('.test', {
23+
source: [
24+
{ label: 'First label', value: 'First Value' },
25+
{ label: 'Second label', value: 'Second Value' },
26+
{ label: 'Third label', value: 'Third Value' },
27+
{ label: 'Final label', value: 'Final Value' },
28+
],
29+
onOpen: (e, data) => {
30+
data.ul.style.width = `${
31+
(e.target as HTMLInputElement).width
32+
}px`;
33+
},
34+
});
35+
36+
expect(autocomplete).not.toBeNull();
37+
});
38+
39+
it('has initial state of "stopped"', () =>
40+
expect(autocomplete.status).toBe(AutocompleteStatus.Stopped));
41+
42+
it('"start" should not throw', () =>
43+
expect(autocomplete.start).not.toThrow());
44+
45+
it('now has "started" state', () =>
46+
expect(autocomplete.status).toBe(AutocompleteStatus.Started));
47+
});
48+
49+
describe('Mouse over', () => {
50+
beforeEach(() => {
51+
inputEL.dispatchEvent(new Event('focusout'));
52+
jest.advanceTimersByTime(251);
53+
});
54+
55+
it('popping up under mouse should not change input', () => {
56+
inputEL.value = 'Test Value';
57+
inputEL.dispatchEvent(new Event('change'));
58+
59+
const ul =
60+
(document.getElementById(
61+
inputEL.dataset.acId ?? '',
62+
) as HTMLUListElement | null) ?? document.createElement('ul');
63+
64+
ul.children[0].dispatchEvent(new Event('mouseover'));
65+
66+
jest.advanceTimersByTime(1);
67+
68+
const point: [number, number] | undefined =
69+
//@ts-ignore
70+
autocomplete._stateData[inputEL.dataset.acId].focusPoint;
71+
72+
expect(point).toBeDefined();
73+
});
74+
75+
it('no initial mouseover should clear the focusPoint', () => {
76+
inputEL.value = 'Test Value';
77+
inputEL.dispatchEvent(new Event('change'));
78+
79+
jest.advanceTimersByTime(334);
80+
81+
const point: [number, number] | undefined =
82+
//@ts-ignore
83+
autocomplete._stateData[inputEL.dataset.acId].focusPoint;
84+
85+
expect(point).toBeUndefined();
86+
});
87+
});
88+
});

0 commit comments

Comments
 (0)