TopSiteImpressionWrapper.jsx (4041B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { actionCreators as ac } from "common/Actions.mjs"; 6 import React from "react"; 7 8 const VISIBLE = "visible"; 9 const VISIBILITY_CHANGE_EVENT = "visibilitychange"; 10 11 // Per analytical requirement, we set the minimal intersection ratio to 12 // 0.5, and an impression is identified when the wrapped item has at least 13 // 50% visibility. 14 // 15 // This constant is exported for unit test 16 export const INTERSECTION_RATIO = 0.5; 17 18 /** 19 * Impression wrapper for a TopSite tile. 20 * 21 * It makses use of the Intersection Observer API to detect the visibility, 22 * and relies on page visibility to ensure the impression is reported 23 * only when the component is visible on the page. 24 */ 25 export class TopSiteImpressionWrapper extends React.PureComponent { 26 _dispatchImpressionStats() { 27 const { actionType, tile } = this.props; 28 if (!actionType) { 29 return; 30 } 31 32 this.props.dispatch( 33 ac.OnlyToMain({ 34 type: actionType, 35 data: { 36 type: "impression", 37 ...tile, 38 }, 39 }) 40 ); 41 } 42 43 setImpressionObserverOrAddListener() { 44 const { props } = this; 45 46 if (!props.dispatch) { 47 return; 48 } 49 50 if (props.document.visibilityState === VISIBLE) { 51 this.setImpressionObserver(); 52 } else { 53 // We should only ever send the latest impression stats ping, so remove any 54 // older listeners. 55 if (this._onVisibilityChange) { 56 props.document.removeEventListener( 57 VISIBILITY_CHANGE_EVENT, 58 this._onVisibilityChange 59 ); 60 } 61 62 this._onVisibilityChange = () => { 63 if (props.document.visibilityState === VISIBLE) { 64 this.setImpressionObserver(); 65 props.document.removeEventListener( 66 VISIBILITY_CHANGE_EVENT, 67 this._onVisibilityChange 68 ); 69 } 70 }; 71 props.document.addEventListener( 72 VISIBILITY_CHANGE_EVENT, 73 this._onVisibilityChange 74 ); 75 } 76 } 77 78 /** 79 * Set an impression observer for the wrapped component. It makes use of 80 * the Intersection Observer API to detect if the wrapped component is 81 * visible with a desired ratio, and only sends impression if that's the case. 82 * 83 * See more details about Intersection Observer API at: 84 * https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API 85 */ 86 setImpressionObserver() { 87 const { props } = this; 88 89 if (!props.tile) { 90 return; 91 } 92 93 this._handleIntersect = entries => { 94 if ( 95 entries.some( 96 entry => 97 entry.isIntersecting && 98 entry.intersectionRatio >= INTERSECTION_RATIO 99 ) 100 ) { 101 this._dispatchImpressionStats(); 102 this.impressionObserver.unobserve(this.refs.topsite_impression_wrapper); 103 } 104 }; 105 106 const options = { threshold: INTERSECTION_RATIO }; 107 this.impressionObserver = new props.IntersectionObserver( 108 this._handleIntersect, 109 options 110 ); 111 this.impressionObserver.observe(this.refs.topsite_impression_wrapper); 112 } 113 114 componentDidMount() { 115 if (this.props.tile) { 116 this.setImpressionObserverOrAddListener(); 117 } 118 } 119 120 componentWillUnmount() { 121 if (this._handleIntersect && this.impressionObserver) { 122 this.impressionObserver.unobserve(this.refs.topsite_impression_wrapper); 123 } 124 if (this._onVisibilityChange) { 125 this.props.document.removeEventListener( 126 VISIBILITY_CHANGE_EVENT, 127 this._onVisibilityChange 128 ); 129 } 130 } 131 132 render() { 133 return ( 134 <div 135 ref={"topsite_impression_wrapper"} 136 className="topsite-impression-observer" 137 > 138 {this.props.children} 139 </div> 140 ); 141 } 142 } 143 144 TopSiteImpressionWrapper.defaultProps = { 145 IntersectionObserver: globalThis.IntersectionObserver, 146 document: globalThis.document, 147 actionType: null, 148 tile: null, 149 };