Skip to content
This repository was archived by the owner on Nov 25, 2021. It is now read-only.

feat: delay hiding the overlay #418

Merged
merged 1 commit into from
Nov 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/hoverifier.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MouseEvent>(),
hoverOverlayElements: of(hoverOverlayElement),
hoverOverlayRerenders: EMPTY,
getHover: createStubHoverProvider(hover),
getDocumentHighlights: createStubDocumentHighlightProvider(),
getActions: () => of(null),
pinningEnabled: true,
})

const positionJumps = new Subject<PositionJump>()

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<string, [SupportedMouseEvent, number | 'overlay']>).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))
Expand Down
26 changes: 24 additions & 2 deletions src/hoverifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
SubscribableOrPromise,
Subscription,
zip,
race,
MonoTypeOperatorFunction,
} from 'rxjs'
import {
catchError,
Expand All @@ -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'
Expand Down Expand Up @@ -406,6 +410,7 @@ export type ContextResolver<C extends object> = (hoveredToken: HoveredToken) =>
*/
export function createHoverifier<C extends object, D, A>({
closeButtonClicks,
hoverOverlayElements,
hoverOverlayRerenders,
getHover,
getDocumentHighlights,
Expand Down Expand Up @@ -433,11 +438,28 @@ export function createHoverifier<C extends object, D, A>({
// These Subjects aggregate all events from all hoverified code views
const allPositionsFromEvents = new Subject<MouseEventTrigger>()

// This keeps the overlay open while the mouse moves over another token on the way to the overlay
const suppressWhileOverlayShown = <T>(): MonoTypeOperatorFunction<T> => 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 = <T extends SupportedMouseEvent>(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<PositionJump & EventOptions<C>>()
Expand Down