From e61548d2c5294a1eb6a838ac51a5479c53ed2a44 Mon Sep 17 00:00:00 2001 From: Sam Seay Date: Mon, 24 Aug 2020 10:46:18 +1200 Subject: [PATCH] Refactor the Header component from class to function. (https://github.com/woocommerce/woocommerce-admin/pull/5023) Working towards woocommerce/woocommerce-admin#4654 this refactors the `
` component to be functional so that it can use hooks. The plan is to use the `useUserPreferences` hook there to determine if the mobile banner should be rendered or not. --- .../woocommerce-admin/client/header/index.js | 219 ++++++++---------- .../client/header/test/index.js | 79 ++++++- .../woocommerce-admin/client/layout/index.js | 2 +- 3 files changed, 165 insertions(+), 135 deletions(-) diff --git a/plugins/woocommerce-admin/client/header/index.js b/plugins/woocommerce-admin/client/header/index.js index 7fffc2209f9..dfae3123272 100644 --- a/plugins/woocommerce-admin/client/header/index.js +++ b/plugins/woocommerce-admin/client/header/index.js @@ -2,10 +2,9 @@ * External dependencies */ import { __, sprintf } from '@wordpress/i18n'; -import { Component, createRef } from '@wordpress/element'; +import { useEffect, useRef, useState } from '@wordpress/element'; import classnames from 'classnames'; import { decodeEntities } from '@wordpress/html-entities'; -import PropTypes from 'prop-types'; import { getNewPath } from '@woocommerce/navigation'; import { Link } from '@woocommerce/components'; import { getAdminLink, getSetting } from '@woocommerce/wc-admin-settings'; @@ -17,135 +16,107 @@ import { recordEvent } from '@woocommerce/tracks'; import './style.scss'; import ActivityPanel from './activity-panel'; -class Header extends Component { - constructor( props ) { - super( props ); - this.state = { - isScrolled: false, - }; - - this.headerRef = createRef(); - - this.onWindowScroll = this.onWindowScroll.bind( this ); - this.updateIsScrolled = this.updateIsScrolled.bind( this ); - this.trackLinkClick = this.trackLinkClick.bind( this ); - this.updateDocumentTitle = this.updateDocumentTitle.bind( this ); - } - - componentDidMount() { - this.threshold = this.headerRef.current.offsetTop; - window.addEventListener( 'scroll', this.onWindowScroll ); - this.updateIsScrolled(); - } - - componentWillUnmount() { - window.removeEventListener( 'scroll', this.onWindowScroll ); - window.cancelAnimationFrame( this.handle ); - } - - onWindowScroll() { - this.handle = window.requestAnimationFrame( this.updateIsScrolled ); - } - - updateIsScrolled() { - const isScrolled = window.pageYOffset > this.threshold - 20; - if ( isScrolled !== this.state.isScrolled ) { - this.setState( { - isScrolled, - } ); - } - } - - trackLinkClick( event ) { - const href = event.target.closest( 'a' ).getAttribute( 'href' ); +const trackLinkClick = ( event ) => { + const target = event.target.closest( 'a' ); + const href = target.getAttribute( 'href' ); + if ( href ) { recordEvent( 'navbar_breadcrumb_click', { href, - text: event.target.innerText, + text: target.innerText, } ); } +}; - updateDocumentTitle() { - const { sections, isEmbedded } = this.props; +export const Header = ( { sections, isEmbedded = false, query } ) => { + const headerElement = useRef( null ); + const rafHandle = useRef( null ); + const threshold = useRef( null ); + const siteTitle = getSetting( 'siteTitle', '' ); + const _sections = Array.isArray( sections ) ? sections : [ sections ]; + const [ isScrolled, setIsScrolled ] = useState( false ); - // Don't modify the document title on existing WooCommerce pages. - if ( isEmbedded ) { - return; + const className = classnames( 'woocommerce-layout__header', { + 'is-scrolled': isScrolled, + } ); + + useEffect( () => { + threshold.current = headerElement.current.offsetTop; + + const updateIsScrolled = () => { + setIsScrolled( window.pageYOffset > threshold.current - 20 ); + }; + + const scrollListener = () => { + rafHandle.current = window.requestAnimationFrame( + updateIsScrolled + ); + }; + + window.addEventListener( 'scroll', scrollListener ); + + return () => { + window.removeEventListener( 'scroll', scrollListener ); + window.cancelAnimationFrame( rafHandle.current ); + }; + }, [] ); + + useEffect( () => { + if ( ! isEmbedded ) { + const documentTitle = _sections + .map( ( section ) => { + return Array.isArray( section ) ? section[ 1 ] : section; + } ) + .reverse() + .join( ' ‹ ' ); + + const decodedTitle = decodeEntities( + sprintf( + /* translators: 1: document title. 2: page title */ + __( + '%1$s ‹ %2$s — WooCommerce', + 'woocommerce-admin' + ), + documentTitle, + siteTitle + ) + ); + + if ( document.title !== decodedTitle ) { + document.title = decodedTitle; + } } + }, [ isEmbedded, _sections, siteTitle ] ); - const _sections = Array.isArray( sections ) ? sections : [ sections ]; - - const documentTitle = _sections - .map( ( section ) => { - return Array.isArray( section ) ? section[ 1 ] : section; - } ) - .reverse() - .join( ' ‹ ' ); - - document.title = decodeEntities( - sprintf( - __( - '%1$s ‹ %2$s — WooCommerce', - 'woocommerce-admin' - ), - documentTitle, - getSetting( 'siteTitle', '' ) - ) - ); - } - - render() { - const { sections, isEmbedded, query } = this.props; - const { isScrolled } = this.state; - const _sections = Array.isArray( sections ) ? sections : [ sections ]; - - this.updateDocumentTitle(); - - const className = classnames( 'woocommerce-layout__header', { - 'is-scrolled': isScrolled, - } ); - - return ( -
-

- { _sections.map( ( section, i ) => { - const sectionPiece = Array.isArray( section ) ? ( - - { section[ 1 ] } - - ) : ( - section - ); - return ( - - { decodeEntities( sectionPiece ) } - - ); - } ) } -

- { window.wcAdminFeatures[ 'activity-panels' ] && ( - - ) } -
- ); - } -} - -Header.propTypes = { - sections: PropTypes.node.isRequired, - isEmbedded: PropTypes.bool, + return ( +
+

+ { _sections.map( ( section, i ) => { + const sectionPiece = Array.isArray( section ) ? ( + + { section[ 1 ] } + + ) : ( + section + ); + return ( + + { decodeEntities( sectionPiece ) } + + ); + } ) } +

+ { window.wcAdminFeatures[ 'activity-panels' ] && ( + + ) } +
+ ); }; - -Header.defaultProps = { - isEmbedded: false, -}; - -export default Header; diff --git a/plugins/woocommerce-admin/client/header/test/index.js b/plugins/woocommerce-admin/client/header/test/index.js index c1daf269f9b..4105235b7e9 100644 --- a/plugins/woocommerce-admin/client/header/test/index.js +++ b/plugins/woocommerce-admin/client/header/test/index.js @@ -1,12 +1,25 @@ +jest.mock( '@woocommerce/wc-admin-settings', () => ( { + ...jest.requireActual( '@woocommerce/wc-admin-settings' ), + getSetting() { + return 'Fake Site Title'; + }, +} ) ); + +jest.mock( '@woocommerce/tracks', () => ( { + ...jest.requireActual( '@woocommerce/tracks' ), + recordEvent: jest.fn(), +} ) ); + /** * External dependencies */ -import { shallow } from 'enzyme'; +import { render, fireEvent } from '@testing-library/react'; +import { recordEvent } from '@woocommerce/tracks'; /** * Internal dependencies */ -import Header from '../index.js'; +import { Header } from '../index.js'; const encodedBreadcrumb = [ [ 'admin.php?page=wc-settings', 'Settings' ], @@ -14,16 +27,62 @@ const encodedBreadcrumb = [ ]; describe( 'Header', () => { - test( 'should render decoded breadcrumb name', () => { - const header = shallow( -
, - { - disableLifecycleMethods: true, + beforeEach( () => { + // Mock RAF to be synchronous for testing + jest.spyOn( window, 'requestAnimationFrame' ).mockImplementation( + ( cb ) => { + cb(); } ); - expect( header.text().includes( 'Accounts & Privacy' ) ).toBe( - false + + // Disable the ActivityPanel so it isn't tested here + window.wcAdminFeatures[ 'activity-panels' ] = false; + } ); + + afterEach( () => { + window.requestAnimationFrame.mockRestore(); + } ); + + it( 'should render decoded breadcrumb name', () => { + const { queryByText } = render( +
); - expect( header.text().includes( 'Accounts & Privacy' ) ).toBe( true ); + expect( queryByText( 'Accounts & Privacy' ) ).toBe( null ); + expect( queryByText( 'Accounts & Privacy' ) ).not.toBe( null ); + } ); + + it( 'should only have the is-scrolled class if the page is scrolled', () => { + const { container } = render( +
+ ); + + const topLevelElement = container.firstChild; + expect( topLevelElement.classList ).not.toContain( 'is-scrolled' ); + fireEvent.scroll( window, { target: { scrollY: 200 } } ); + expect( topLevelElement.classList ).toContain( 'is-scrolled' ); + } ); + + it( 'correctly updates the document title to reflect the navigation state', () => { + render( +
+ ); + + expect( document.title ).toBe( + 'Accounts & Privacy ‹ Settings ‹ Fake Site Title — WooCommerce' + ); + } ); + + it( 'tracks link clicks with recordEvent', () => { + const { queryByRole } = render( +
+ ); + + const firstLink = queryByRole( 'link' ); + fireEvent.click( firstLink ); + + expect( recordEvent ).toBeCalledWith( 'navbar_breadcrumb_click', { + href: firstLink.getAttribute( 'href' ), + text: firstLink.innerText, + } ); } ); } ); diff --git a/plugins/woocommerce-admin/client/layout/index.js b/plugins/woocommerce-admin/client/layout/index.js index 806259fd4bb..793d1197f48 100644 --- a/plugins/woocommerce-admin/client/layout/index.js +++ b/plugins/woocommerce-admin/client/layout/index.js @@ -23,7 +23,7 @@ import { recordPageView } from '@woocommerce/tracks'; */ import './style.scss'; import { Controller, getPages } from './controller'; -import Header from '../header'; +import { Header } from '../header'; import Notices from './notices'; import TransientNotices from './transient-notices';