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 `<Header>` 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.
This commit is contained in:
parent
fac5a4609d
commit
e61548d2c5
|
@ -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 (
|
||||
<div className={ className } ref={ this.headerRef }>
|
||||
<h1 className="woocommerce-layout__header-breadcrumbs">
|
||||
{ _sections.map( ( section, i ) => {
|
||||
const sectionPiece = Array.isArray( section ) ? (
|
||||
<Link
|
||||
href={
|
||||
isEmbedded
|
||||
? getAdminLink( section[ 0 ] )
|
||||
: getNewPath( {}, section[ 0 ], {} )
|
||||
}
|
||||
type={ isEmbedded ? 'wp-admin' : 'wc-admin' }
|
||||
onClick={ this.trackLinkClick }
|
||||
>
|
||||
{ section[ 1 ] }
|
||||
</Link>
|
||||
) : (
|
||||
section
|
||||
);
|
||||
return (
|
||||
<span key={ i }>
|
||||
{ decodeEntities( sectionPiece ) }
|
||||
</span>
|
||||
);
|
||||
} ) }
|
||||
</h1>
|
||||
{ window.wcAdminFeatures[ 'activity-panels' ] && (
|
||||
<ActivityPanel isEmbedded={ isEmbedded } query={ query } />
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
sections: PropTypes.node.isRequired,
|
||||
isEmbedded: PropTypes.bool,
|
||||
return (
|
||||
<div className={ className } ref={ headerElement }>
|
||||
<h1 className="woocommerce-layout__header-breadcrumbs">
|
||||
{ _sections.map( ( section, i ) => {
|
||||
const sectionPiece = Array.isArray( section ) ? (
|
||||
<Link
|
||||
href={
|
||||
isEmbedded
|
||||
? getAdminLink( section[ 0 ] )
|
||||
: getNewPath( {}, section[ 0 ], {} )
|
||||
}
|
||||
type={ isEmbedded ? 'wp-admin' : 'wc-admin' }
|
||||
onClick={ trackLinkClick }
|
||||
>
|
||||
{ section[ 1 ] }
|
||||
</Link>
|
||||
) : (
|
||||
section
|
||||
);
|
||||
return (
|
||||
<span key={ i }>
|
||||
{ decodeEntities( sectionPiece ) }
|
||||
</span>
|
||||
);
|
||||
} ) }
|
||||
</h1>
|
||||
{ window.wcAdminFeatures[ 'activity-panels' ] && (
|
||||
<ActivityPanel isEmbedded={ isEmbedded } query={ query } />
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Header.defaultProps = {
|
||||
isEmbedded: false,
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
|
|
@ -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(
|
||||
<Header sections={ encodedBreadcrumb } isEmbedded={ true } />,
|
||||
{
|
||||
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(
|
||||
<Header sections={ encodedBreadcrumb } isEmbedded={ true } />
|
||||
);
|
||||
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(
|
||||
<Header sections={ encodedBreadcrumb } isEmbedded={ false } />
|
||||
);
|
||||
|
||||
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(
|
||||
<Header sections={ encodedBreadcrumb } isEmbedded={ false } />
|
||||
);
|
||||
|
||||
expect( document.title ).toBe(
|
||||
'Accounts & Privacy ‹ Settings ‹ Fake Site Title — WooCommerce'
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'tracks link clicks with recordEvent', () => {
|
||||
const { queryByRole } = render(
|
||||
<Header sections={ encodedBreadcrumb } isEmbedded={ false } />
|
||||
);
|
||||
|
||||
const firstLink = queryByRole( 'link' );
|
||||
fireEvent.click( firstLink );
|
||||
|
||||
expect( recordEvent ).toBeCalledWith( 'navbar_breadcrumb_click', {
|
||||
href: firstLink.getAttribute( 'href' ),
|
||||
text: firstLink.innerText,
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
Loading…
Reference in New Issue