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 5import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm"; 6import React from "react"; 7 8const VISIBLE = "visible"; 9const 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 16export 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 */ 25export class TopSiteImpressionWrapper extends React.PureComponent { 26 _dispatchImpressionStats() { 27 const { tile } = this.props; 28 29 this.props.dispatch( 30 ac.OnlyToMain({ 31 type: at.TOP_SITES_IMPRESSION_STATS, 32 data: { 33 type: "impression", 34 ...tile, 35 }, 36 }) 37 ); 38 } 39 40 setImpressionObserverOrAddListener() { 41 const { props } = this; 42 43 if (!props.dispatch) { 44 return; 45 } 46 47 if (props.document.visibilityState === VISIBLE) { 48 this.setImpressionObserver(); 49 } else { 50 // We should only ever send the latest impression stats ping, so remove any 51 // older listeners. 52 if (this._onVisibilityChange) { 53 props.document.removeEventListener( 54 VISIBILITY_CHANGE_EVENT, 55 this._onVisibilityChange 56 ); 57 } 58 59 this._onVisibilityChange = () => { 60 if (props.document.visibilityState === VISIBLE) { 61 this.setImpressionObserver(); 62 props.document.removeEventListener( 63 VISIBILITY_CHANGE_EVENT, 64 this._onVisibilityChange 65 ); 66 } 67 }; 68 props.document.addEventListener( 69 VISIBILITY_CHANGE_EVENT, 70 this._onVisibilityChange 71 ); 72 } 73 } 74 75 /** 76 * Set an impression observer for the wrapped component. It makes use of 77 * the Intersection Observer API to detect if the wrapped component is 78 * visible with a desired ratio, and only sends impression if that's the case. 79 * 80 * See more details about Intersection Observer API at: 81 * https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API 82 */ 83 setImpressionObserver() { 84 const { props } = this; 85 86 if (!props.tile) { 87 return; 88 } 89 90 this._handleIntersect = entries => { 91 if ( 92 entries.some( 93 entry => 94 entry.isIntersecting && 95 entry.intersectionRatio >= INTERSECTION_RATIO 96 ) 97 ) { 98 this._dispatchImpressionStats(); 99 this.impressionObserver.unobserve(this.refs.topsite_impression_wrapper); 100 } 101 }; 102 103 const options = { threshold: INTERSECTION_RATIO }; 104 this.impressionObserver = new props.IntersectionObserver( 105 this._handleIntersect, 106 options 107 ); 108 this.impressionObserver.observe(this.refs.topsite_impression_wrapper); 109 } 110 111 componentDidMount() { 112 if (this.props.tile) { 113 this.setImpressionObserverOrAddListener(); 114 } 115 } 116 117 componentWillUnmount() { 118 if (this._handleIntersect && this.impressionObserver) { 119 this.impressionObserver.unobserve(this.refs.topsite_impression_wrapper); 120 } 121 if (this._onVisibilityChange) { 122 this.props.document.removeEventListener( 123 VISIBILITY_CHANGE_EVENT, 124 this._onVisibilityChange 125 ); 126 } 127 } 128 129 render() { 130 return ( 131 <div 132 ref={"topsite_impression_wrapper"} 133 className="topsite-impression-observer" 134 > 135 {this.props.children} 136 </div> 137 ); 138 } 139} 140 141TopSiteImpressionWrapper.defaultProps = { 142 IntersectionObserver: global.IntersectionObserver, 143 document: global.document, 144 tile: null, 145}; 146