diff --git a/assets/js/autocomplete.ts b/assets/js/autocomplete.ts index 2fecad0d..033176c8 100644 --- a/assets/js/autocomplete.ts +++ b/assets/js/autocomplete.ts @@ -8,6 +8,7 @@ import { getTermContexts } from './match_query'; import store from './utils/store'; import { TermContext } from './query/lex.ts'; import { $, $$, makeEl, removeEl } from './utils/dom.ts'; +import { mouseMoveThenOver } from './utils/events.ts'; type TermSuggestion = { label: string; @@ -125,38 +126,18 @@ function createItem(list: HTMLUListElement, suggestion: TermSuggestion) { className: 'autocomplete__item', }); - let ignoreMouseOver = true; - item.textContent = suggestion.label; item.dataset.value = suggestion.value; - function onItemMouseOver() { - // Prevent selection when mouse entered the element without actually moving. - if (ignoreMouseOver) { - return; - } - + mouseMoveThenOver(item, () => { removeSelected(); item.classList.add('autocomplete__item--selected'); - } - - item.addEventListener('mouseover', onItemMouseOver); + }); item.addEventListener('mouseout', () => { removeSelected(); }); - item.addEventListener( - 'mousemove', - () => { - ignoreMouseOver = false; - onItemMouseOver(); - }, - { - once: true, - }, - ); - item.addEventListener('click', () => { if (!inputField || !item.dataset.value) return; diff --git a/assets/js/utils/__tests__/events.spec.ts b/assets/js/utils/__tests__/events.spec.ts index 575883b7..ab1dbd67 100644 --- a/assets/js/utils/__tests__/events.spec.ts +++ b/assets/js/utils/__tests__/events.spec.ts @@ -1,4 +1,4 @@ -import { delegate, fire, leftClick, on, PhilomenaAvailableEventsMap } from '../events'; +import { delegate, fire, mouseMoveThenOver, leftClick, on, PhilomenaAvailableEventsMap } from '../events'; import { getRandomArrayItem } from '../../../test/randomness'; import { fireEvent } from '@testing-library/dom'; @@ -80,6 +80,55 @@ describe('Event utils', () => { }); }); + describe('mouseMoveThenOver', () => { + it('should NOT fire on first mouseover', () => { + const mockButton = document.createElement('button'); + const mockHandler = vi.fn(); + + mouseMoveThenOver(mockButton, mockHandler); + + fireEvent.mouseOver(mockButton); + + expect(mockHandler).toHaveBeenCalledTimes(0); + }); + + it('should fire on the first mousemove', () => { + const mockButton = document.createElement('button'); + const mockHandler = vi.fn(); + + mouseMoveThenOver(mockButton, mockHandler); + + fireEvent.mouseMove(mockButton); + + expect(mockHandler).toHaveBeenCalledTimes(1); + }); + + it('should fire on subsequent mouseover', () => { + const mockButton = document.createElement('button'); + const mockHandler = vi.fn(); + + mouseMoveThenOver(mockButton, mockHandler); + + fireEvent.mouseMove(mockButton); + fireEvent.mouseOver(mockButton); + + expect(mockHandler).toHaveBeenCalledTimes(2); + }); + + it('should NOT fire on subsequent mousemove', () => { + const mockButton = document.createElement('button'); + const mockHandler = vi.fn(); + + mouseMoveThenOver(mockButton, mockHandler); + + fireEvent.mouseMove(mockButton); + fireEvent.mouseOver(mockButton); + fireEvent.mouseMove(mockButton); + + expect(mockHandler).toHaveBeenCalledTimes(2); + }); + }); + describe('delegate', () => { it('should call the native addEventListener method on the element', () => { const mockElement = document.createElement('div'); diff --git a/assets/js/utils/events.ts b/assets/js/utils/events.ts index 70460bf8..458df039 100644 --- a/assets/js/utils/events.ts +++ b/assets/js/utils/events.ts @@ -43,6 +43,17 @@ export function leftClick(func }; } +export function mouseMoveThenOver(element: El, func: (e: MouseEvent) => void) { + element.addEventListener( + 'mousemove', + (event: MouseEvent) => { + func(event); + element.addEventListener('mouseover', func); + }, + { once: true }, + ); +} + export function delegate( node: PhilomenaEventElement, event: K,