From 1c5d7fd2bcd15e15a82faabc88d7a6a4f3c8db51 Mon Sep 17 00:00:00 2001 From: Jeff Yates Date: Mon, 29 Jul 2024 16:31:08 -0500 Subject: [PATCH 1/2] Guard against matchMedia access during initial render --- packages/perseus/src/widgets/explanation.tsx | 15 ++++++++--- packages/perseus/src/widgets/label-image.tsx | 27 ++++++++++++------- .../src/widgets/label-image/marker.tsx | 15 ++++++++++- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/packages/perseus/src/widgets/explanation.tsx b/packages/perseus/src/widgets/explanation.tsx index 2a17b6b135..44edc1f183 100644 --- a/packages/perseus/src/widgets/explanation.tsx +++ b/packages/perseus/src/widgets/explanation.tsx @@ -66,6 +66,15 @@ class Explanation extends React.Component { state: State = { expanded: false, }; + _mounted: boolean = false; + + componentDidMount() { + this._mounted = true; + } + + componentWillUnmount() { + this._mounted = false; + } change: (arg1: any, arg2: any, arg3: any) => any = (...args) => { return Changeable.change.apply(this, args); @@ -85,9 +94,9 @@ class Explanation extends React.Component { const caretIcon = this.state.expanded ? caretUp : caretDown; - const allowTransition = mediaQueryIsMatched( - "(prefers-reduced-motion: no-preference)", - ); + const allowTransition = + this._mounted && + mediaQueryIsMatched("(prefers-reduced-motion: no-preference)"); // Special styling is needed to fit the button in a block of text without throwing off the line spacing. // While the button is not normally included in a block of text, it needs to be able to accommodate such a case. diff --git a/packages/perseus/src/widgets/label-image.tsx b/packages/perseus/src/widgets/label-image.tsx index 3dbe0c6220..14fc2477b2 100644 --- a/packages/perseus/src/widgets/label-image.tsx +++ b/packages/perseus/src/widgets/label-image.tsx @@ -109,6 +109,7 @@ export class LabelImage extends React.Component< // The rendered markers on the question image for labeling. _markers: Array; + _mounted: boolean = false; static gradeMarker(marker: InteractiveMarkerType): InteractiveMarkerScore { const score = { @@ -375,6 +376,14 @@ export class LabelImage extends React.Component< }; } + componentDidMount() { + this._mounted = true; + } + + componentWillUnmount() { + this._mounted = false; + } + simpleValidate(rubric: LabelImageProps): PerseusScore { return LabelImage.validate(this.getUserInput(), rubric); } @@ -503,17 +512,17 @@ export class LabelImage extends React.Component< const {activeMarkerIndex, markersInteracted} = this.state; - // Render all markers for widget. - return markers.map((marker, index): React.ReactElement => { - // Determine whether page is rendered in a narrow browser window. - const isNarrowPage = window.matchMedia( - mediaQueries.xsOrSmaller.replace("@media ", ""), - ).matches; + // Determine whether page is rendered in a narrow browser window. + const isNarrowPage = + this._mounted && + window.matchMedia(mediaQueries.xsOrSmaller.replace("@media ", "")) + .matches; - // Determine whether the image is wider than it is tall. - const isWideImage = - this.props.imageWidth / 2 > this.props.imageHeight; + // Determine whether the image is wider than it is tall. + const isWideImage = this.props.imageWidth / 2 > this.props.imageHeight; + // Render all markers for widget. + return markers.map((marker, index): React.ReactElement => { let side: "bottom" | "left" | "right" | "top"; let markerPosition; // Position popup closest to the center, preferring it renders diff --git a/packages/perseus/src/widgets/label-image/marker.tsx b/packages/perseus/src/widgets/label-image/marker.tsx index fec794e2b7..79d4089e9f 100644 --- a/packages/perseus/src/widgets/label-image/marker.tsx +++ b/packages/perseus/src/widgets/label-image/marker.tsx @@ -37,6 +37,10 @@ type Props = InteractiveMarkerType & { }; function shouldReduceMotion(): boolean { + // We cannot use matchMedia during SSR. + if (typeof window.matchMedia !== "function") { + return true; + } const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); return !mediaQuery || mediaQuery.matches; } @@ -49,6 +53,7 @@ export default class Marker extends React.Component { // The marker icon element. _icon: HTMLElement | null | undefined; + _mounted: boolean = false; static defaultProps: { selected: ReadonlyArray; @@ -56,6 +61,14 @@ export default class Marker extends React.Component { selected: [], }; + componentDidMount() { + this._mounted = true; + } + + componentWillUnmount() { + this._mounted = false; + } + renderIcon() { const {selected, showCorrectness, showSelected, showPulsate} = this.props; @@ -106,7 +119,7 @@ export default class Marker extends React.Component { } else if (showPulsate) { iconStyles = [ styles.markerPulsateBase, - shouldReduceMotion() + this._mounted && shouldReduceMotion() ? showPulsate && styles.markerUnfilledPulsateOnce : showPulsate && styles.markerUnfilledPulsateInfinite, ]; From cea126a3a24779b60e2f5f28661b590f2b7ec2b8 Mon Sep 17 00:00:00 2001 From: Jeff Yates Date: Mon, 29 Jul 2024 17:07:40 -0500 Subject: [PATCH 2/2] Add changeset --- .changeset/quiet-moose-arrive.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/quiet-moose-arrive.md diff --git a/.changeset/quiet-moose-arrive.md b/.changeset/quiet-moose-arrive.md new file mode 100644 index 0000000000..9339b7b1b7 --- /dev/null +++ b/.changeset/quiet-moose-arrive.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": patch +--- + +Guard against executing matchMedia during initial render