-
Notifications
You must be signed in to change notification settings - Fork 358
refactor(interaction): Enhance touch event handling with passive options #4056
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR enhances touch event handling by adding the passive option to touch event listeners for improved scrolling performance. When preventDefault is disabled (default), touch events are registered with {passive: true} to allow smooth scrolling. When preventDefault is enabled (boolean true or number threshold), events use {passive: false} to allow calling preventDefault().
Key changes:
- Added passive option logic to
eventrect.tsbased onpreventDefaultconfiguration - Applied
{passive: true}to touch events in shape-specific handlers (arc, radar, treemap, funnel) - Updated documentation to describe the passive option behavior
- Added comprehensive test coverage for passive option in various chart types
Reviewed Changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
src/ChartInternal/interactions/eventrect.ts |
Core implementation: calculates passiveOption based on preventDefault setting and applies it to touch event listeners |
src/ChartInternal/shape/arc.ts |
Applies {passive: true} to arc chart touch events (pie, donut, gauge) |
src/ChartInternal/shape/radar.ts |
Applies {passive: true} to radar chart touch events |
src/ChartInternal/shape/treemap.ts |
Applies {passive: true} to treemap chart touch events |
src/ChartInternal/shape/funnel.ts |
Applies {passive: true} to funnel chart touch events |
src/ChartInternal/ChartInternal.ts |
Applies {passive: true} to SVG-level touch events when touch input is enabled |
src/config/Options/interaction/interaction.ts |
Documents the passive option behavior in the API documentation |
src/ChartInternal/internals/tooltip.ts |
Removes IE9-specific visibility style (unrelated cleanup) |
test/interactions/interaction-touch-spec.ts |
Adds test suites to verify passive option is correctly applied for various scenarios and chart types |
| describe("passive option for arc types", () => { | ||
| let addEventListenerSpy; | ||
|
|
||
| it("should add touch events with passive: true for pie chart", () => { | ||
| // Spy on addEventListener before chart creation | ||
| const originalAddEventListener = Element.prototype.addEventListener; | ||
| const touchstartCalls = []; | ||
|
|
||
| addEventListenerSpy = sinon.stub(Element.prototype, "addEventListener").callsFake(function(type, listener, options) { | ||
| if (type === "touchstart") { | ||
| touchstartCalls.push({ type, options }); | ||
| } | ||
| return originalAddEventListener.call(this, type, listener, options); | ||
| }); | ||
|
|
||
| // Create pie chart | ||
| args = { | ||
| data: { | ||
| columns: [ | ||
| ["data1", 50], | ||
| ["data2", 50] | ||
| ], | ||
| type: "pie" | ||
| }, | ||
| interaction: { | ||
| inputType: { | ||
| touch: true | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| chart = util.generate(args); | ||
|
|
||
| // Check if touchstart events were added with passive: true | ||
| const hasPassiveTrue = touchstartCalls.some(call => { | ||
| const options = call.options; | ||
| return options && options.passive === true; | ||
| }); | ||
|
|
||
| expect(touchstartCalls.length).to.be.above(0); | ||
| expect(hasPassiveTrue).to.be.true; | ||
|
|
||
| addEventListenerSpy.restore(); | ||
| }); | ||
| }); |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same issues identified for "passive event listener option" apply here:
- Conflict with global
beforeEachcausing double chart generation - Missing
afterEachhook for spy cleanup
Consider the same restructuring approach with beforeAll to skip the global beforeEach and an afterEach hook for proper cleanup.
| /^(touchstart|mouseover)$/.test(event.type) && $$.setOverOut(true, data); | ||
| } | ||
| }) | ||
| }, isTouch ? {passive: true} : undefined) |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing space after {passive:. Should be {passive: true} for consistency with the coding style used elsewhere in the codebase.
|
|
||
| // hide tooltip | ||
| tooltip | ||
| .style("display", "none") |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] This change appears unrelated to the PR's stated purpose of "enhancing touch event handling with passive options". The removal of the IE9-specific visibility style is a separate concern and should ideally be in its own commit/PR for better change tracking and git history clarity.
| describe("passive event listener option", () => { | ||
| let addEventListenerSpy; | ||
|
|
||
| it("should add touch events with passive: true when preventDefault is false", () => { | ||
| // Spy on addEventListener before chart creation | ||
| const originalAddEventListener = Element.prototype.addEventListener; | ||
| const touchstartCalls = []; | ||
|
|
||
| addEventListenerSpy = sinon.stub(Element.prototype, "addEventListener").callsFake(function(type, listener, options) { | ||
| if (type === "touchstart") { | ||
| touchstartCalls.push({ type, options }); | ||
| } | ||
| return originalAddEventListener.call(this, type, listener, options); | ||
| }); | ||
|
|
||
| // Create chart with touch input enabled and preventDefault disabled | ||
| args = { | ||
| data: { | ||
| columns: [ | ||
| ["data1", 10, 20, 30], | ||
| ["data2", 20, 16, 18] | ||
| ], | ||
| type: "line" | ||
| }, | ||
| interaction: { | ||
| inputType: { | ||
| touch: true | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| chart = util.generate(args); | ||
|
|
||
| // Check if touchstart events were added with passive: true | ||
| const hasPassiveTrue = touchstartCalls.some(call => { | ||
| const options = call.options; | ||
| return options && options.passive === true; | ||
| }); | ||
|
|
||
| expect(touchstartCalls.length).to.be.above(0); | ||
| expect(hasPassiveTrue).to.be.true; | ||
|
|
||
| addEventListenerSpy.restore(); | ||
| }); | ||
|
|
||
| it("should add touch events with passive: false when preventDefault is true", () => { | ||
| // Spy on addEventListener before chart creation | ||
| const originalAddEventListener = Element.prototype.addEventListener; | ||
| const touchCalls = []; | ||
|
|
||
| addEventListenerSpy = sinon.stub(Element.prototype, "addEventListener").callsFake(function(type, listener, options) { | ||
| if (type === "touchstart" || type === "touchmove") { | ||
| touchCalls.push({ type, options }); | ||
| } | ||
| return originalAddEventListener.call(this, type, listener, options); | ||
| }); | ||
|
|
||
| // Create chart with preventDefault enabled | ||
| args = { | ||
| data: { | ||
| columns: [ | ||
| ["data1", 10, 20, 30], | ||
| ["data2", 20, 16, 18] | ||
| ], | ||
| type: "line" | ||
| }, | ||
| interaction: { | ||
| inputType: { | ||
| touch: { | ||
| preventDefault: true | ||
| } | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| chart = util.generate(args); | ||
|
|
||
| // Check if touch events were added with passive: false | ||
| const allPassiveFalse = touchCalls.every(call => { | ||
| const options = call.options; | ||
| return options && options.passive === false; | ||
| }); | ||
|
|
||
| expect(touchCalls.length).to.be.above(0); | ||
| expect(allPassiveFalse).to.be.true; | ||
|
|
||
| addEventListenerSpy.restore(); | ||
| }); | ||
|
|
||
| it("should add touch events with passive: false when preventDefault is a number", () => { | ||
| // Spy on addEventListener before chart creation | ||
| const originalAddEventListener = Element.prototype.addEventListener; | ||
| const touchCalls = []; | ||
|
|
||
| addEventListenerSpy = sinon.stub(Element.prototype, "addEventListener").callsFake(function(type, listener, options) { | ||
| if (type === "touchstart" || type === "touchmove") { | ||
| touchCalls.push({ type, options }); | ||
| } | ||
| return originalAddEventListener.call(this, type, listener, options); | ||
| }); | ||
|
|
||
| // Create chart with preventDefault as a number | ||
| args = { | ||
| data: { | ||
| columns: [ | ||
| ["data1", 10, 20, 30], | ||
| ["data2", 20, 16, 18] | ||
| ], | ||
| type: "line" | ||
| }, | ||
| interaction: { | ||
| inputType: { | ||
| touch: { | ||
| preventDefault: 50 | ||
| } | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| chart = util.generate(args); | ||
|
|
||
| // Check if touch events were added with passive: false | ||
| const allPassiveFalse = touchCalls.every(call => { | ||
| const options = call.options; | ||
| return options && options.passive === false; | ||
| }); | ||
|
|
||
| expect(touchCalls.length).to.be.above(0); | ||
| expect(allPassiveFalse).to.be.true; | ||
|
|
||
| addEventListenerSpy.restore(); | ||
| }); | ||
| }); |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The addEventListenerSpy should be cleaned up in an afterEach hook rather than at the end of each test. If a test fails before reaching restore(), the spy will remain stubbed and affect subsequent tests. Consider adding an afterEach hook to ensure cleanup:
describe("passive event listener option", () => {
let addEventListenerSpy;
afterEach(() => {
if (addEventListenerSpy) {
addEventListenerSpy.restore();
addEventListenerSpy = null;
}
});
// ... tests
});| describe("passive option for other shape types", () => { | ||
| let addEventListenerSpy; | ||
|
|
||
| it("should add touch events with passive: true for radar chart", () => { | ||
| // Spy on addEventListener before chart creation | ||
| const originalAddEventListener = Element.prototype.addEventListener; | ||
| const touchstartCalls = []; | ||
|
|
||
| addEventListenerSpy = sinon.stub(Element.prototype, "addEventListener").callsFake(function(type, listener, options) { | ||
| if (type === "touchstart") { | ||
| touchstartCalls.push({ type, options }); | ||
| } | ||
| return originalAddEventListener.call(this, type, listener, options); | ||
| }); | ||
|
|
||
| // Create radar chart | ||
| args = { | ||
| data: { | ||
| columns: [ | ||
| ["data1", 30, 200, 100], | ||
| ["data2", 130, 100, 140] | ||
| ], | ||
| type: "radar" | ||
| }, | ||
| interaction: { | ||
| inputType: { | ||
| touch: true | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| chart = util.generate(args); | ||
|
|
||
| // Check if touchstart events were added with passive: true | ||
| const hasPassiveTrue = touchstartCalls.some(call => { | ||
| const options = call.options; | ||
| return options && options.passive === true; | ||
| }); | ||
|
|
||
| expect(touchstartCalls.length).to.be.above(0); | ||
| expect(hasPassiveTrue).to.be.true; | ||
|
|
||
| addEventListenerSpy.restore(); | ||
| }); | ||
|
|
||
| it("should add touch events with passive: true for treemap chart", () => { | ||
| // Spy on addEventListener before chart creation | ||
| const originalAddEventListener = Element.prototype.addEventListener; | ||
| const touchstartCalls = []; | ||
|
|
||
| addEventListenerSpy = sinon.stub(Element.prototype, "addEventListener").callsFake(function(type, listener, options) { | ||
| if (type === "touchstart") { | ||
| touchstartCalls.push({ type, options }); | ||
| } | ||
| return originalAddEventListener.call(this, type, listener, options); | ||
| }); | ||
|
|
||
| // Create treemap chart | ||
| args = { | ||
| data: { | ||
| columns: [ | ||
| ["data1", 30, 200, 100], | ||
| ["data2", 130, 100, 140] | ||
| ], | ||
| type: "treemap" | ||
| }, | ||
| interaction: { | ||
| inputType: { | ||
| touch: true | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| chart = util.generate(args); | ||
|
|
||
| // Check if touchstart events were added with passive: true | ||
| const hasPassiveTrue = touchstartCalls.some(call => { | ||
| const options = call.options; | ||
| return options && options.passive === true; | ||
| }); | ||
|
|
||
| expect(touchstartCalls.length).to.be.above(0); | ||
| expect(hasPassiveTrue).to.be.true; | ||
|
|
||
| addEventListenerSpy.restore(); | ||
| }); | ||
|
|
||
| it("should add touch events with passive: true for funnel chart", () => { | ||
| // Spy on addEventListener before chart creation | ||
| const originalAddEventListener = Element.prototype.addEventListener; | ||
| const touchstartCalls = []; | ||
|
|
||
| addEventListenerSpy = sinon.stub(Element.prototype, "addEventListener").callsFake(function(type, listener, options) { | ||
| if (type === "touchstart") { | ||
| touchstartCalls.push({ type, options }); | ||
| } | ||
| return originalAddEventListener.call(this, type, listener, options); | ||
| }); | ||
|
|
||
| // Create funnel chart | ||
| args = { | ||
| data: { | ||
| columns: [ | ||
| ["data1", 30, 200, 100], | ||
| ["data2", 130, 100, 140] | ||
| ], | ||
| type: "funnel" | ||
| }, | ||
| interaction: { | ||
| inputType: { | ||
| touch: true | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| chart = util.generate(args); | ||
|
|
||
| // Check if touchstart events were added with passive: true | ||
| const hasPassiveTrue = touchstartCalls.some(call => { | ||
| const options = call.options; | ||
| return options && options.passive === true; | ||
| }); | ||
|
|
||
| expect(touchstartCalls.length).to.be.above(0); | ||
| expect(hasPassiveTrue).to.be.true; | ||
|
|
||
| addEventListenerSpy.restore(); | ||
| }); | ||
| }); |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same issues identified for "passive event listener option" apply here:
- Conflict with global
beforeEachcausing double chart generation - Missing
afterEachhook for spy cleanup
Consider the same restructuring approach with beforeAll to skip the global beforeEach and an afterEach hook for proper cleanup.
- Updated touch event bindings to use passive options for improved performance. Ref naver#4055
ddfd7fd to
ddfb08e
Compare
Issue
#4055
Details
Updated touch event bindings to use passive options for improved performance.