From be40d776a5ee6bbf4c5af4df57889a32e9b8b3bf Mon Sep 17 00:00:00 2001 From: Jeff Yates Date: Tue, 30 Jul 2024 10:33:45 -0500 Subject: [PATCH] Guard against matchMedia access during initial render (#1459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: This PR makes sure that we don't call `matchMedia` during the initial render. This is because `matchMedia` is not available during the initial render, and calling it will throw an error. Issue: FEI-5743 ## Test plan: 1. Put up this PR and await the pre-release packages 2. Update webapp with the pre-release packages 3. Put up a webapp PR and check that the ZND is working on URLs that were previously erroring Author: somewhatabstract Reviewers: somewhatabstract, jeresig, jeremywiebe Required Reviewers: Approved By: jeresig, jeremywiebe Checks: ✅ codecov/project, ✅ codecov/patch, ✅ Upload Coverage (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Jest Coverage (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1459 --- .changeset/quiet-moose-arrive.md | 5 ++++ packages/perseus/src/widgets/explanation.tsx | 15 ++++++++--- packages/perseus/src/widgets/label-image.tsx | 27 ++++++++++++------- .../src/widgets/label-image/marker.tsx | 15 ++++++++++- 4 files changed, 49 insertions(+), 13 deletions(-) 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 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, ];