Skip to content

Commit

Permalink
Fixed Mouseover Pre-popup Causing Typing Error - PR#7 from glenn2223
Browse files Browse the repository at this point in the history
### 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
  • Loading branch information
glenn2223 authored Oct 18, 2024
2 parents bc70f6c + 7d7d1f7 commit 3dc1bcd
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 21 deletions.
20 changes: 20 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Tests
on:
pull_request:
branches:
- '**'

# ALLOW MANUAL RUNS
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20.x'
- run: npm ci
- run: npm test
- run: npm run prepublishOnly
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## UNPUBLISHED

### Fixed

- The mouse being over the popup when it's rendered no longer selects that value whilst typing

### Changes

- Developer dependency bumps (no user-facing changes)
- Added CI based testing for every PR

## [1.0.1] - 2023-11-14

<small>[Compare to previous release][comp:1.0.1]</small>
Expand Down
107 changes: 86 additions & 21 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use strict';

import {
AutocompleteEventFunction as EventFunction,
CloseEventData,
Expand Down Expand Up @@ -29,6 +31,7 @@ export class Autocomplete<T = { label: string; value: string }> {
lastTerm: string;
valueStore?: string;
focusValue?: string;
focusPoint?: [number, number];
}
> = {};

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

// Focus on the new one
const liEl = <HTMLLIElement>ev.target,
newVal = liEl.dataset.value || liEl.innerText;
const liEl = (<HTMLElement>ev.target).closest('li');

if (!liEl) {
return;
}

const newVal = liEl.dataset.value || liEl.innerText;

liEl.classList.add('focused');

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

// Set input value
data.input.value = liEl.dataset.value ?? liEl.innerText;
data.input.value = liEl?.dataset.value ?? liEl?.innerText ?? '';
this._stateData[data.ul.id].valueStore = data.input.value;

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

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

const tL = position({
const { top, left } = position({
target: data.ul,
anchor: <HTMLElement>ev.target,
my: this.options.position.my,
at: this.options.position.at,
collision: this.options.position.collision,
});

data.ul.style.top = tL.top;
data.ul.style.left = tL.left;
data.ul.style.top = top;
data.ul.style.left = left;
data.ul.hidden = false;

if (this.options.autoFocus) {
data.ul.children[0]?.dispatchEvent(new Event('focus'));
} else {
this._stateData[data.ul.id].focusPoint = [-1, -1];

// If they aren't already hovering over it, remove the focusPoint
// so we can trigger mouseover events immediately
setTimeout(() => {
const focusPoint = this._stateData[data.ul.id].focusPoint;

if (
focusPoint &&
focusPoint[0] === -1 &&
focusPoint[1] === -1
) {
this._stateData[data.ul.id].focusPoint = undefined;
}
}, 333);
}

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

this._stateData[data.ul.id].valueStore = undefined;
this._stateData[data.ul.id].focusValue = undefined;

this._infoLog('Reverted input', `Input ac-id: ${data.ul.id}`);
}
Expand Down Expand Up @@ -306,8 +332,8 @@ export class Autocomplete<T = { label: string; value: string }> {
)
).json() as Promise<ListItemType<T>[]>)
: typeof this.options.source === 'function'
? this.options.source({ term: data.term })
: this.options.source,
? this.options.source({ term: data.term })
: this.options.source,
});
} catch {
return;
Expand Down Expand Up @@ -491,9 +517,14 @@ export class Autocomplete<T = { label: string; value: string }> {
};

private _itemClickEvent = (ev: MouseEvent) => {
const li = <HTMLLIElement>ev.target,
ul = <HTMLUListElement>li.parentElement,
id = ul.id,
const li = (<HTMLElement>ev.target).closest('li'),
ul = li?.closest('ul');

if (!ul || !li) {
return;
}

const id = ul.id,
item =
this._stateData[id].data[Array.from(ul.children).indexOf(li)],
input = <HTMLInputElement>(
Expand All @@ -505,19 +536,53 @@ export class Autocomplete<T = { label: string; value: string }> {
this.itemSelect(ev, { ul, item, input });
};

private _itemFocusEvent = (ev: FocusEvent) => {
const li = <HTMLLIElement>ev.target,
ul = <HTMLUListElement>li.parentElement,
id = ul.id,
private _itemFocusEvent = (ev: FocusEvent | MouseEvent) => {
const li = (<HTMLElement>ev.target).closest('li'),
ul = li?.closest('ul');

if (!ul || !li) {
return;
}

const id = ul.id,
item =
this._stateData[id].data[Array.from(ul.children).indexOf(li)],
input = <HTMLInputElement>(
document.querySelector(`input[data-ac-id='${id}']`)
);
),
that = this;

if (ev instanceof MouseEvent && this._stateData[id].focusPoint) {
const [x, y] = this._stateData[id].focusPoint;

if (x === -1 && y === -1) {
this._stateData[id].focusPoint = [ev.clientX, ev.clientY];
li.addEventListener('mousemove', handlePopHover);

return;
}

this._stateData[id].focusPoint = undefined;
}

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

this.itemFocus(ev, { ul, item, input });

function handlePopHover(this: HTMLLIElement, subEv: MouseEvent) {
const focusPoint = that._stateData[id].focusPoint;

if (
focusPoint === undefined ||
Math.abs(focusPoint[0] - subEv.clientX) > 5 ||
Math.abs(focusPoint[1] - subEv.clientY) > 5
) {
that._stateData[id].focusPoint = undefined;
li!.removeEventListener('mousemove', handlePopHover);

li!.dispatchEvent(new MouseEvent('mouseover', subEv));
}
}
};

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

return function (this: ThisParameterType<F>, ...args: Parameters<F>) {
if (dTimer) {
calledAgain = true;
} else {
const context = this;
const subThat = this;

dTimer = setTimeout(() => {
if (calledAgain) {
calledAgain = false;

func.apply(context, args);
func.apply(subThat, args);
}

dTimer = undefined;
}, that.options.delay);

func.apply(context, args);
func.apply(subThat, args);
}
};
}
Expand Down
88 changes: 88 additions & 0 deletions tests/mouseover.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Autocomplete, AutocompleteStatus } from '../src/index';

jest.useFakeTimers();

describe('Mouseover Tests', () => {
let inputEL: HTMLInputElement, autocomplete: Autocomplete;

describe('Test environment:-', () => {
it('has added element', () => {
inputEL = document.createElement('input');

inputEL.classList.add('test');
inputEL = document.body.insertAdjacentElement(
'beforeend',
inputEL,
) as HTMLInputElement;

expect(inputEL).not.toBeNull();
});

it('has created autocomplete', () => {
autocomplete = new Autocomplete('.test', {
source: [
{ label: 'First label', value: 'First Value' },
{ label: 'Second label', value: 'Second Value' },
{ label: 'Third label', value: 'Third Value' },
{ label: 'Final label', value: 'Final Value' },
],
onOpen: (e, data) => {
data.ul.style.width = `${
(e.target as HTMLInputElement).width
}px`;
},
});

expect(autocomplete).not.toBeNull();
});

it('has initial state of "stopped"', () =>
expect(autocomplete.status).toBe(AutocompleteStatus.Stopped));

it('"start" should not throw', () =>
expect(autocomplete.start).not.toThrow());

it('now has "started" state', () =>
expect(autocomplete.status).toBe(AutocompleteStatus.Started));
});

describe('Mouse over', () => {
beforeEach(() => {
inputEL.dispatchEvent(new Event('focusout'));
jest.advanceTimersByTime(251);
});

it('popping up under mouse should not change input', () => {
inputEL.value = 'Test Value';
inputEL.dispatchEvent(new Event('change'));

const ul =
(document.getElementById(
inputEL.dataset.acId ?? '',
) as HTMLUListElement | null) ?? document.createElement('ul');

ul.children[0].dispatchEvent(new Event('mouseover'));

jest.advanceTimersByTime(1);

const point: [number, number] | undefined =
//@ts-ignore
autocomplete._stateData[inputEL.dataset.acId].focusPoint;

expect(point).toBeDefined();
});

it('no initial mouseover should clear the focusPoint', () => {
inputEL.value = 'Test Value';
inputEL.dispatchEvent(new Event('change'));

jest.advanceTimersByTime(334);

const point: [number, number] | undefined =
//@ts-ignore
autocomplete._stateData[inputEL.dataset.acId].focusPoint;

expect(point).toBeUndefined();
});
});
});

0 comments on commit 3dc1bcd

Please sign in to comment.