diff --git a/.changeset/four-fans-hope.md b/.changeset/four-fans-hope.md new file mode 100644 index 00000000000..84b622f4b4b --- /dev/null +++ b/.changeset/four-fans-hope.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Update Unlimited Graphs to use SVG Coordinate system to solve "add point" issues that arise due to a11y scaling. diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/point.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/point.tsx index 9c38a7a2f50..4cdc00ab0ec 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/point.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/point.tsx @@ -108,6 +108,9 @@ function UnlimitedPointGraph(statefulProps: StatefulProps) { 400, // Safari Webkit has up to a 350ms delay before a click event is fired ); + // Debug state to display diagnostic info on mobile + const [debugInfo, setDebugInfo] = React.useState(""); + const {graphDimensionsInPixels} = graphConfig; const widthPx = graphDimensionsInPixels[0]; @@ -133,19 +136,83 @@ function UnlimitedPointGraph(statefulProps: StatefulProps) { return; } + const svg = event.currentTarget.ownerSVGElement; + const ctm = event.currentTarget.getScreenCTM(); + if (!svg || !ctm) { + return; + } + + const pt = svg.createSVGPoint(); + pt.x = event.clientX; + pt.y = event.clientY; + const svgPoint = pt.matrixTransform(ctm.inverse()); + + // Subtract left/top to get position relative to rect + const x = svgPoint.x - left; + const y = svgPoint.y - top; + + // Use actual rendered dimensions const elementRect = event.currentTarget.getBoundingClientRect(); + const actualDimensions = { + range: graphConfig.range, + width: elementRect.width, + height: elementRect.height, + }; + + // Capture diagnostic info for mobile debugging + const graphCoords = pixelsToVectors( + [[x, y]], + actualDimensions, + ); + + // Get all possible diagnostic values + const scaleX = Math.sqrt(ctm.a * ctm.a + ctm.b * ctm.b); + const scaleY = Math.sqrt(ctm.c * ctm.c + ctm.d * ctm.d); + const visualViewportScale = + window.visualViewport?.scale ?? 1; + const devicePixelRatio = window.devicePixelRatio ?? 1; + const parentSvg = svg.getBoundingClientRect(); - const x = event.clientX - elementRect.x; - const y = event.clientY - elementRect.y; + const info = `Click: ${event.clientX.toFixed(0)}, ${event.clientY.toFixed(0)} +SVG: ${svgPoint.x.toFixed(1)}, ${svgPoint.y.toFixed(1)} +CTM: a=${ctm.a.toFixed(2)} d=${ctm.d.toFixed(2)} +CTM Scale: ${scaleX.toFixed(3)}, ${scaleY.toFixed(3)} +VVP Scale: ${visualViewportScale.toFixed(3)} +DPR: ${devicePixelRatio.toFixed(3)} +Left/Top: ${left.toFixed(1)}, ${top.toFixed(1)} +Relative: ${x.toFixed(1)}, ${y.toFixed(1)} +Expected: ${widthPx}x${heightPx} +Actual: ${elementRect.width.toFixed(0)}x${elementRect.height.toFixed(0)} +Parent SVG: ${parentSvg.width.toFixed(0)}x${parentSvg.height.toFixed(0)} +Range: [${graphConfig.range[0][0]},${graphConfig.range[0][1]}],[${graphConfig.range[1][0]},${graphConfig.range[1][1]}] +Result: [${graphCoords[0][0].toFixed(1)}, ${graphCoords[0][1].toFixed(1)}]`; + setDebugInfo(info); const graphCoordinates = pixelsToVectors( [[x, y]], - graphConfig, + actualDimensions, ); dispatch(actions.pointGraph.addPoint(graphCoordinates[0])); }} /> + {/* Debug text for mobile - displays click diagnostic info */} + {debugInfo && ( + + {debugInfo.split("\n").map((line, i) => ( + + {line} + + ))} + + )} {coords.map((point, i) => ( { 400, // Safari Webkit has up to a 350ms delay before a click event is fired ); + // Debug state to display diagnostic info on mobile + const [debugInfo, setDebugInfo] = useState(""); + const id = React.useId(); const polygonPointsNumId = id + "-points-num"; const polygonPointsId = id + "-points"; @@ -511,19 +514,83 @@ const UnlimitedPolygonGraph = (statefulProps: StatefulProps) => { return; } + const svg = event.currentTarget.ownerSVGElement; + const ctm = event.currentTarget.getScreenCTM(); + if (!svg || !ctm) { + return; + } + + const pt = svg.createSVGPoint(); + pt.x = event.clientX; + pt.y = event.clientY; + const svgPoint = pt.matrixTransform(ctm.inverse()); + + // Subtract left/top to get position relative to rect + const x = svgPoint.x - left; + const y = svgPoint.y - top; + + // Use actual rendered dimensions const elementRect = event.currentTarget.getBoundingClientRect(); + const actualDimensions = { + range: graphConfig.range, + width: elementRect.width, + height: elementRect.height, + }; + + // Capture diagnostic info for mobile debugging + const graphCoords = pixelsToVectors( + [[x, y]], + actualDimensions, + ); - const x = event.clientX - elementRect.x; - const y = event.clientY - elementRect.y; + // Get all possible diagnostic values + const scaleX = Math.sqrt(ctm.a * ctm.a + ctm.b * ctm.b); + const scaleY = Math.sqrt(ctm.c * ctm.c + ctm.d * ctm.d); + const visualViewportScale = + window.visualViewport?.scale ?? 1; + const devicePixelRatio = window.devicePixelRatio ?? 1; + const parentSvg = svg.getBoundingClientRect(); + + const info = `Click: ${event.clientX.toFixed(0)}, ${event.clientY.toFixed(0)} +SVG: ${svgPoint.x.toFixed(1)}, ${svgPoint.y.toFixed(1)} +CTM: a=${ctm.a.toFixed(2)} d=${ctm.d.toFixed(2)} +CTM Scale: ${scaleX.toFixed(3)}, ${scaleY.toFixed(3)} +VVP Scale: ${visualViewportScale.toFixed(3)} +DPR: ${devicePixelRatio.toFixed(3)} +Left/Top: ${left.toFixed(1)}, ${top.toFixed(1)} +Relative: ${x.toFixed(1)}, ${y.toFixed(1)} +Expected: ${widthPx}x${heightPx} +Actual: ${elementRect.width.toFixed(0)}x${elementRect.height.toFixed(0)} +Parent SVG: ${parentSvg.width.toFixed(0)}x${parentSvg.height.toFixed(0)} +Range: [${graphConfig.range[0][0]},${graphConfig.range[0][1]}],[${graphConfig.range[1][0]},${graphConfig.range[1][1]}] +Result: [${graphCoords[0][0].toFixed(1)}, ${graphCoords[0][1].toFixed(1)}]`; + setDebugInfo(info); const graphCoordinates = pixelsToVectors( [[x, y]], - graphConfig, + actualDimensions, ); dispatch(actions.polygon.addPoint(graphCoordinates[0])); }} /> + {/* Debug text for mobile - displays click diagnostic info */} + {debugInfo && ( + + {debugInfo.split("\n").map((line, i) => ( + + {line} + + ))} + + )} {coords.map((point, i) => { const angleId = `${id}-angle-${i}`; let sideIds = "";