Skip to content

Commit

Permalink
Use JS to trigger hover and reposition the flyout (#47238)
Browse files Browse the repository at this point in the history
* Use JS to trigger hover

* Repositions the flyout

* Fixes ul>ul problem

* Opt for curly braces instead of one-liner, add fullstop in comment

* Uses useLayoutEffect instead of useEffect

* Uses styles on ref instead state

* Adds .hovered comment

* Reinstates position after scrolling and there is enough space

* Update client/my-sites/sidebar-unified/style.scss

Co-authored-by: Dave Smith <[email protected]>

* Fixes tests needing isTouch() needing jsdom

Co-authored-by: cpap <[email protected]>
Co-authored-by: Dave Smith <[email protected]>
  • Loading branch information
3 people authored Nov 10, 2020
1 parent 20e31ba commit c0e3167
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 38 deletions.
58 changes: 55 additions & 3 deletions client/layout/sidebar/expandable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import React, { useState, useRef, useLayoutEffect } from 'react';
import { get, uniqueId } from 'lodash';

/**
Expand All @@ -12,6 +12,10 @@ import { get, uniqueId } from 'lodash';
import TranslatableString from 'calypso/components/translatable/proptype';
import ExpandableSidebarHeading from './expandable-heading';
import SidebarMenu from 'calypso/layout/sidebar/menu';
import { hasTouch } from 'calypso/lib/touch-detect';
import config from 'calypso/config';

const isTouch = hasTouch();

function containsSelectedSidebarItem( children ) {
let selectedItemFound = false;
Expand All @@ -35,6 +39,11 @@ function containsSelectedSidebarItem( children ) {
return selectedItemFound;
}

const offScreen = ( submenu ) => {
const rect = submenu.getBoundingClientRect();
return rect.y + rect.height > window.innerHeight;
};

export const ExpandableSidebarMenu = ( {
className,
title,
Expand All @@ -48,6 +57,14 @@ export const ExpandableSidebarMenu = ( {
...props
} ) => {
let { expanded } = props;
const submenu = useRef();
const [ submenuHovered, setSubmenuHovered ] = useState( false );

if ( submenu.current ) {
// Sets flyout to expand towards bottom
submenu.current.style.bottom = 'auto';
submenu.current.style.top = 0;
}

if ( null === expanded ) {
expanded = containsSelectedSidebarItem( children );
Expand All @@ -56,12 +73,41 @@ export const ExpandableSidebarMenu = ( {
const classes = classNames( className, {
'is-toggle-open': !! expanded,
'is-togglable': true,
hovered: submenuHovered,
} );

const onEnter = () => {
if ( expanded || isTouch ) {
return;
}

setSubmenuHovered( true );
};

const onLeave = () => {
if ( expanded || isTouch ) {
return;
}

setSubmenuHovered( false );
};

const menuId = uniqueId( 'menu' );

useLayoutEffect( () => {
if ( submenuHovered && offScreen( submenu.current ) ) {
// Sets flyout to expand towards top
submenu.current.style.bottom = 0;
submenu.current.style.top = 'auto';
}
}, [ submenuHovered ] );

return (
<SidebarMenu className={ classes }>
<SidebarMenu
className={ classes }
onMouseEnter={ config.isEnabled( 'nav-unification' ) ? () => onEnter() : null }
onMouseLeave={ config.isEnabled( 'nav-unification' ) ? () => onLeave() : null }
>
<ExpandableSidebarHeading
title={ title }
count={ count }
Expand All @@ -74,7 +120,13 @@ export const ExpandableSidebarMenu = ( {
menuId={ menuId }
{ ...props }
/>
<li role="region" id={ menuId } className="sidebar__expandable-content" hidden={ ! expanded }>
<li
role="region"
ref={ submenu }
id={ menuId }
className="sidebar__expandable-content"
hidden={ ! expanded }
>
<ul>{ children }</ul>
</li>
</SidebarMenu>
Expand Down
6 changes: 4 additions & 2 deletions client/layout/sidebar/menu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import React from 'react';
import classNames from 'classnames';

const SidebarMenu = ( { children, className } ) => (
<ul className={ classNames( 'sidebar__menu', className ) }>{ children }</ul>
const SidebarMenu = ( { children, className, ...props } ) => (
<ul className={ classNames( 'sidebar__menu', className ) } { ...props }>
{ children }
</ul>
);

export default SidebarMenu;
64 changes: 33 additions & 31 deletions client/my-sites/sidebar-unified/menu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,38 +60,40 @@ export const MySitesSidebarUnifiedMenu = ( {
}, [ selected, childIsSelected, reduxDispatch, sectionId, sidebarCollapsed ] );

return (
<ExpandableSidebarMenu
onClick={ () => {
if ( link ) {
if ( isExternal( link ) ) {
// If the URL is external, page() will fail to replace state between different domains
externalRedirect( link );
return;
<li>
<ExpandableSidebarMenu
onClick={ () => {
if ( link ) {
if ( isExternal( link ) ) {
// If the URL is external, page() will fail to replace state between different domains.
externalRedirect( link );
return;
}
page( link );
}
page( link );
}
if ( ! sidebarCollapsed ) {
reduxDispatch( collapseAllMySitesSidebarSections() );
reduxDispatch( toggleSection( sectionId ) );
}
} }
expanded={ ! sidebarCollapsed && isExpanded }
title={ title }
customIcon={ <SidebarCustomIcon icon={ icon } /> }
className={ ( selected || childIsSelected ) && 'sidebar__menu--selected' }
>
{ children.map( ( item ) => {
const isSelected = selectedMenuItem?.url === item.url;
return (
<MySitesSidebarUnifiedItem
key={ item.slug }
{ ...item }
selected={ isSelected }
isSubItem={ true }
/>
);
} ) }
</ExpandableSidebarMenu>
if ( ! sidebarCollapsed ) {
reduxDispatch( collapseAllMySitesSidebarSections() );
reduxDispatch( toggleSection( sectionId ) );
}
} }
expanded={ ! sidebarCollapsed && isExpanded }
title={ title }
customIcon={ <SidebarCustomIcon icon={ icon } /> }
className={ ( selected || childIsSelected ) && 'sidebar__menu--selected' }
>
{ children.map( ( item ) => {
const isSelected = selectedMenuItem?.url === item.url;
return (
<MySitesSidebarUnifiedItem
key={ item.slug }
{ ...item }
selected={ isSelected }
isSubItem={ true }
/>
);
} ) }
</ExpandableSidebarMenu>
</li>
);
};

Expand Down
6 changes: 4 additions & 2 deletions client/my-sites/sidebar-unified/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,8 @@ $font-size: rem( 14px );
}
}

.sidebar__menu.is-togglable:not( .is-toggle-open ):hover {
.sidebar__menu.is-togglable:not( .is-toggle-open ).hovered {
// .hovered is handled in client/layout/sidebar/expandable.jsx. Needed for repositioning and hover intent.
position: relative;

.sidebar__heading {
Expand All @@ -505,8 +506,9 @@ $font-size: rem( 14px );
// flyout content
.sidebar__expandable-content {
display: block;
position: absolute;
top: 0;
bottom: auto;
position: absolute;
left: var( --sidebar-width-max );
width: 160px;

Expand Down
4 changes: 4 additions & 0 deletions client/my-sites/sidebar/test/sidebar.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* @jest-environment jsdom
*/

/**
* External dependencies
*/
Expand Down

0 comments on commit c0e3167

Please sign in to comment.