diff --git a/src/hoverifier.test.ts b/src/hoverifier.test.ts index 2249f588..7a63413e 100644 --- a/src/hoverifier.test.ts +++ b/src/hoverifier.test.ts @@ -741,6 +741,80 @@ describe('Hoverifier', () => { } }) + it('keeps the overlay open when the mouse briefly moves over another token on the way to the overlay', () => { + for (const codeView of testcases) { + const scheduler = new TestScheduler((a, b) => chai.assert.deepEqual(a, b)) + + const hover = {} + + scheduler.run(({ cold, expectObservable }) => { + const hoverOverlayElement = document.createElement('div') + + const hoverifier = createHoverifier({ + closeButtonClicks: new Observable(), + hoverOverlayElements: of(hoverOverlayElement), + hoverOverlayRerenders: EMPTY, + getHover: createStubHoverProvider(hover), + getDocumentHighlights: createStubDocumentHighlightProvider(), + getActions: () => of(null), + pinningEnabled: true, + }) + + const positionJumps = new Subject() + + const positionEvents = of(codeView.codeView).pipe(findPositionsFromEvents({ domFunctions: codeView })) + + const subscriptions = new Subscription() + + subscriptions.add(hoverifier) + subscriptions.add( + hoverifier.hoverify({ + dom: codeView, + positionEvents, + positionJumps, + resolveContext: () => codeView.revSpec, + }) + ) + + const hoverAndDefinitionUpdates = hoverifier.hoverStateUpdates.pipe( + filter(propertyIsDefined('hoverOverlayProps')), + map(({ hoverOverlayProps }) => hoverOverlayProps.hoveredToken?.character), + distinctUntilChanged(isEqual) + ) + + const outputDiagram = `${TOOLTIP_DISPLAY_DELAY + MOUSEOVER_DELAY + 1}ms a` + + const outputValues: { [key: string]: number } = { + a: 6, + } + + cold(`a b ${TOOLTIP_DISPLAY_DELAY}ms c d 1ms e`, { + a: ['mouseover', 6], + b: ['mousemove', 6], + c: ['mouseover', 19], + d: ['mousemove', 19], + e: ['mouseover', 'overlay'], + } as Record).subscribe(([eventType, value]) => { + if (value === 'overlay') { + hoverOverlayElement.dispatchEvent( + new MouseEvent(eventType, { + bubbles: true, // Must be true so that React can see it. + }) + ) + } else { + dispatchMouseEventAtPositionImpure(eventType, codeView, { + line: 24, + character: value, + }) + } + }) + + expectObservable(hoverAndDefinitionUpdates).toBe(outputDiagram, outputValues) + }) + break + } + }) + it('dedupes mouseover and mousemove event on same token', () => { for (const codeView of testcases) { const scheduler = new TestScheduler((a, b) => chai.assert.deepEqual(a, b)) diff --git a/src/hoverifier.ts b/src/hoverifier.ts index e8c0399e..1a0f41b3 100644 --- a/src/hoverifier.ts +++ b/src/hoverifier.ts @@ -15,6 +15,8 @@ import { SubscribableOrPromise, Subscription, zip, + race, + MonoTypeOperatorFunction, } from 'rxjs' import { catchError, @@ -30,6 +32,8 @@ import { takeUntil, withLatestFrom, mergeMap, + delay, + startWith, } from 'rxjs/operators' import { Key } from 'ts-key-enum' import { asError, ErrorLike, isErrorLike } from './errors' @@ -406,6 +410,7 @@ export type ContextResolver = (hoveredToken: HoveredToken) => */ export function createHoverifier({ closeButtonClicks, + hoverOverlayElements, hoverOverlayRerenders, getHover, getDocumentHighlights, @@ -433,11 +438,28 @@ export function createHoverifier({ // These Subjects aggregate all events from all hoverified code views const allPositionsFromEvents = new Subject() + // This keeps the overlay open while the mouse moves over another token on the way to the overlay + const suppressWhileOverlayShown = (): MonoTypeOperatorFunction => o => + o.pipe( + withLatestFrom(from(hoverOverlayElements).pipe(startWith(null))), + switchMap(([t, overlayElement]) => + overlayElement === null + ? of(t) + : race( + fromEvent(overlayElement, 'mouseover').pipe(mapTo('suppress')), + of('emit').pipe(delay(MOUSEOVER_DELAY)) + ).pipe( + filter(action => action === 'emit'), + mapTo(t) + ) + ) + ) + const isEventType = (type: T) => ( event: MouseEventTrigger ): event is MouseEventTrigger & { eventType: T } => event.eventType === type - const allCodeMouseMoves = allPositionsFromEvents.pipe(filter(isEventType('mousemove'))) - const allCodeMouseOvers = allPositionsFromEvents.pipe(filter(isEventType('mouseover'))) + const allCodeMouseMoves = allPositionsFromEvents.pipe(filter(isEventType('mousemove')), suppressWhileOverlayShown()) + const allCodeMouseOvers = allPositionsFromEvents.pipe(filter(isEventType('mouseover')), suppressWhileOverlayShown()) const allCodeClicks = allPositionsFromEvents.pipe(filter(isEventType('click'))) const allPositionJumps = new Subject>()