Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
16 changes: 15 additions & 1 deletion src/chartLibraries/dygraph/navigation/generic.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,15 @@ export default chartUI => {
let dygraphLastTouchMove = 0
let dygraphLastTouchPageX = 0

const hasValidTouchTargets = touches =>
Array.from(touches).every(touch => typeof touch.target?.getBoundingClientRect === "function")

const touchStart = (event, dygraph, context) => {
if (!hasValidTouchTargets(event.touches)) {
context.initialTouches = null
return
}

Dygraph.defaultInteractionModel.touchstart(event, dygraph, context)

context.touchDirections = { x: true, y: false }
Expand All @@ -111,6 +119,8 @@ export default chartUI => {
}

const touchMove = (event, dygraph, context) => {
if (!context.initialTouches) return

Dygraph.defaultInteractionModel.touchmove(event, dygraph, context)

if (!dygraphLastTouchMove) chartUI.sdk.trigger("panStart", chartUI.chart)
Expand All @@ -129,7 +139,11 @@ export default chartUI => {

lastTouchEndTime = now

Dygraph.defaultInteractionModel.touchend(event, dygraph, context)
if (event.touches.length > 0 && !hasValidTouchTargets(event.touches)) {
context.initialTouches = null
} else {
Dygraph.defaultInteractionModel.touchend(event, dygraph, context)
}

if (dygraphLastTouchMove === 0 && dygraphLastTouchPageX !== 0) {
chartUI.chart.updateAttribute("clickX", [context.initialTouches?.[0]?.dataX, null])
Expand Down
147 changes: 147 additions & 0 deletions src/chartLibraries/dygraph/navigation/generic.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import genericNavigation from "./generic"

const getHandler = (chartUI, eventName) =>
chartUI.on.mock.calls.find(([name]) => name === eventName)[1]

const makeChartUI = () => {
const chartUI = {
sdk: {
trigger: jest.fn(),
},
chart: {
getAttribute: jest.fn(),
updateAttributes: jest.fn(),
updateAttribute: jest.fn(),
resetNavigation: jest.fn(),
getPropertiesForSeries: jest.fn(),
},
trigger: jest.fn(),
on: jest.fn(() => chartUI),
off: jest.fn(() => chartUI),
}
return chartUI
}

const makeDygraph = () => ({
toDataXCoord: jest.fn(() => 0),
toDataYCoord: jest.fn(() => 0),
xAxisRange: jest.fn(() => [0, 1]),
yAxisRange: jest.fn(() => [0, 1]),
plotter_: { area: { w: 100, h: 100 } },
axes_: [{}],
attributes_: { getForAxis: jest.fn(() => false) },
drawGraph_: jest.fn(),
getFunctionOption: jest.fn(),
})

const makeTouchEvent = (touches, changedTouches = []) => ({
touches,
changedTouches,
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
})

describe("generic navigation touch handling", () => {
let chartUI
let dygraph
let context
let touchStart
let touchMove
let touchEnd

beforeEach(() => {
chartUI = makeChartUI()
genericNavigation(chartUI)
touchStart = getHandler(chartUI, "touchstart")
touchMove = getHandler(chartUI, "touchmove")
touchEnd = getHandler(chartUI, "touchend")
dygraph = makeDygraph()
context = {}
})

it("does not throw on touchstart when a touch target has no getBoundingClientRect", () => {
const event = makeTouchEvent([
{ target: document.createTextNode("x"), pageX: 10, pageY: 10, clientX: 10, clientY: 10 },
])

expect(() => touchStart(event, dygraph, context)).not.toThrow()
})

it("does not throw on touchmove after a touchstart that failed to record initialTouches", () => {
const startEvent = makeTouchEvent([
{ target: document.createTextNode("x"), pageX: 10, pageY: 10, clientX: 10, clientY: 10 },
])
try {
touchStart(startEvent, dygraph, context)
} catch {}

const moveEvent = makeTouchEvent([{ pageX: 20, pageY: 20 }])

expect(() => touchMove(moveEvent, dygraph, context)).not.toThrow()
})

it("still delegates to dygraph's default interaction model for an Element target", () => {
const element = document.createElement("div")
const startEvent = makeTouchEvent([
{ target: element, pageX: 10, pageY: 10, clientX: 10, clientY: 10 },
])

touchStart(startEvent, dygraph, context)

expect(context.initialTouches).toEqual([{ pageX: 10, pageY: 10, dataX: 0, dataY: 0 }])

const moveEvent = makeTouchEvent([{ pageX: 15, pageY: 15 }])

expect(() => touchMove(moveEvent, dygraph, context)).not.toThrow()
expect(dygraph.drawGraph_).toHaveBeenCalledWith(false)
})

it("does not throw on touchmove after a valid one-finger touchstart is followed by an invalid two-finger touchstart", () => {
const element = document.createElement("div")
const oneFingerStart = makeTouchEvent([
{ target: element, pageX: 10, pageY: 10, clientX: 10, clientY: 10 },
])
touchStart(oneFingerStart, dygraph, context)

const twoFingerStart = makeTouchEvent([
{ target: element, pageX: 10, pageY: 10, clientX: 10, clientY: 10 },
{ target: document.createTextNode("x"), pageX: 20, pageY: 20, clientX: 20, clientY: 20 },
])
touchStart(twoFingerStart, dygraph, context)

const twoFingerMove = makeTouchEvent([
{ pageX: 12, pageY: 12 },
{ pageX: 22, pageY: 22 },
])

expect(() => touchMove(twoFingerMove, dygraph, context)).not.toThrow()
})

it("does not throw on touchend re-entry when a remaining touch has an invalid target", () => {
const element = document.createElement("div")
const startEvent = makeTouchEvent([
{ target: element, pageX: 10, pageY: 10, clientX: 10, clientY: 10 },
])
touchStart(startEvent, dygraph, context)

const endEvent = makeTouchEvent([
{ target: document.createTextNode("x"), pageX: 10, pageY: 10 },
])

expect(() => touchEnd(endEvent, dygraph, context)).not.toThrow()
})

it("still delegates the normal touchend path (clickX) when the gesture is valid", () => {
const element = document.createElement("div")
const startEvent = makeTouchEvent([
{ target: element, pageX: 10, pageY: 10, clientX: 10, clientY: 10 },
])
touchStart(startEvent, dygraph, context)

const endEvent = makeTouchEvent([], [{ screenX: 10, screenY: 10 }])

touchEnd(endEvent, dygraph, context)

expect(chartUI.chart.updateAttribute).toHaveBeenCalledWith("clickX", [0, null])
})
})