Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hooks and HOCs to lib/viewport #31081

Merged
merged 23 commits into from
Mar 4, 2019
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d5c9d6d
Fix removeWithinBreakpointListener.
sgomes Feb 27, 2019
add4f10
Add tests for lib/viewport.
sgomes Feb 27, 2019
369b741
Add helper hooks and higher order components to lib/viewport.
sgomes Feb 27, 2019
cd000ec
Add tests for lib/viewport hooks and HOCs.
sgomes Feb 27, 2019
80cf057
Safely disable and restore console.warn for lib/viewport tests.
sgomes Feb 27, 2019
3aba4e0
Add missing JSDoc to lib/viewport.
sgomes Feb 27, 2019
4a89ba9
Improve displayNames for lib/viewport HOCs
sgomes Feb 27, 2019
f5b063d
Change lib/viewport listener API to use subscription tokens.
sgomes Feb 27, 2019
a5f339a
Change lib/viewport helper tests to create multiple instances.
sgomes Feb 27, 2019
fc9e5a8
Small cleanups to lib/viewport.
sgomes Feb 27, 2019
c195fbf
Move afterAll closer to other methods in lib/viewport tests.
sgomes Feb 28, 2019
8cd491a
Change the lib/viewport registration API to return the unsub fn.
sgomes Mar 1, 2019
a4a1060
Remove noop default from Tooltip.
sgomes Mar 1, 2019
ac545b8
Improvements to viewport hooks after review.
sgomes Mar 1, 2019
6fbfe01
Change withBreakpoint API to add extra indirection.
sgomes Mar 1, 2019
b1ca878
Further improvements to viewport API after review.
sgomes Mar 1, 2019
b2ffccb
Solve edge case with useBreakpoint when breakpoint changes.
sgomes Mar 1, 2019
2e0a939
Fix some typos in lib/viewport README.
sgomes Mar 4, 2019
90b6901
Tweak useBreakpoint again to ensure correctness at all times.
sgomes Mar 4, 2019
7488b26
Rename a parameter in useBreakpoint.
sgomes Mar 4, 2019
a94f99e
Use createHigherOrderComponent for lib/viewport HOCs.
sgomes Mar 4, 2019
f63f191
Rename viewport/react-helpers to viewport/react.
sgomes Mar 4, 2019
32c4dfe
Attach viewport react tests to the DOM.
sgomes Mar 4, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 45 additions & 51 deletions client/components/tooltip/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,69 +4,63 @@
* External dependencies
*/

import React, { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';

/**
* Internal dependencies
*/
import Popover from 'components/popover';
import { isMobile } from 'lib/viewport';
import { useMobileBreakpoint } from 'lib/viewport/react-helpers';
sgomes marked this conversation as resolved.
Show resolved Hide resolved

/**
* Module variables
*/
const noop = () => {};
function Tooltip( props ) {
sgomes marked this conversation as resolved.
Show resolved Hide resolved
const isMobile = useMobileBreakpoint();

class Tooltip extends Component {
static propTypes = {
autoPosition: PropTypes.bool,
className: PropTypes.string,
id: PropTypes.string,
isVisible: PropTypes.bool,
position: PropTypes.string,
rootClassName: PropTypes.string,
status: PropTypes.string,
showDelay: PropTypes.number,
showOnMobile: PropTypes.bool,
};
if ( ! props.showOnMobile && isMobile ) {
return null;
}

static defaultProps = {
showDelay: 100,
position: 'top',
showOnMobile: false,
};
const classes = classnames(
'popover',
'tooltip',
`is-${ props.status }`,
`is-${ props.position }`,
props.className
);

render() {
if ( ! this.props.showOnMobile && isMobile() ) {
return null;
}
return (
<Popover
autoPosition={ props.autoPosition }
className={ classes }
rootClassName={ props.rootClassName }
context={ props.context }
id={ props.id }
isVisible={ props.isVisible }
position={ props.position }
showDelay={ props.showDelay }
>
{ props.children }
</Popover>
);
}

const classes = classnames(
'popover',
'tooltip',
`is-${ this.props.status }`,
`is-${ this.props.position }`,
this.props.className
);
Tooltip.propTypes = {
autoPosition: PropTypes.bool,
className: PropTypes.string,
id: PropTypes.string,
isVisible: PropTypes.bool,
position: PropTypes.string,
rootClassName: PropTypes.string,
status: PropTypes.string,
showDelay: PropTypes.number,
showOnMobile: PropTypes.bool,
};

return (
<Popover
autoPosition={ this.props.autoPosition }
className={ classes }
rootClassName={ this.props.rootClassName }
context={ this.props.context }
id={ this.props.id }
isVisible={ this.props.isVisible }
onClose={ noop }
position={ this.props.position }
showDelay={ this.props.showDelay }
>
{ this.props.children }
</Popover>
);
}
}
Tooltip.defaultProps = {
showDelay: 100,
position: 'top',
showOnMobile: false,
};
sgomes marked this conversation as resolved.
Show resolved Hide resolved

export default Tooltip;
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import { PLAN_PREMIUM, PLAN_BUSINESS } from 'lib/plans/constants';
import { getSitePlan } from 'state/sites/selectors';
import { getSelectedSiteId } from 'state/ui/selectors';

import { isDesktop } from 'lib/viewport';

// NOTE: selector moved here because tour is no longer active and serves as example only
// to use in a tour, move back to 'state/ui/guided-tours/contexts' (see commented out import above)
/**
Expand Down
67 changes: 52 additions & 15 deletions client/lib/viewport/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ This module contains functions to identify the current viewport. This can be use
Simple usage:

```js
import { isDesktop, isMobile } from 'viewport';
import { isDesktop, isMobile } from 'lib/viewport';

if ( isDesktop() ) {
// Render a component optimized for desktop view
} else ( isMobile() ) {
} else if ( isMobile() ) {
// Render a component optimized for mobile view
}
```

Using one of the other breakpoints:

```js
import { isWithinBreakpoint } from 'viewport';
import { isWithinBreakpoint } from 'lib/viewport';

if ( isWithinBreakpoint( '>1400px' ) ) {
// Render a component optimized for a very large screen
Expand All @@ -32,7 +32,7 @@ if ( isWithinBreakpoint( '>1400px' ) ) {
Registering to listen to changes:

```js
import { addIsDesktopListener, removeIsDesktopListener } from 'viewport';
import { subscribeIsDesktop } from 'lib/viewport';

class MyComponent extends React.Component {
sizeChanged = matches => {
Expand All @@ -42,28 +42,65 @@ class MyComponent extends React.Component {
};

componentDidMount() {
addIsDesktopListener( this.sizeChanged );
this.unsubscribe = subscribeIsDesktop( this.sizeChanged );
}

componentWillUnmount() {
removeIsDesktopListener( this.sizeChanged );
this.unsubscribe();
}
}
```

It also comes with React helpers that help in registering a component to breakpoint changes.

Using a hook:

```js
import { useMobileBreakpoint } from 'lib/viewport/react-helpers';

export default function MyComponent( props ) {
const isMobile = useMobileBreakpoint();
return <div>Screen size: { isMobile ? 'mobile' : 'not mobile' }</div>;
}
```

Using a higher-order component:

```js
import { withMobileBreakpoint } from 'lib/viewport/react-helpers';

class MyComponent extends React.Component {
render() {
const { isBreakpointActive: isMobile } = this.props;
return <div>Screen size: { isMobile ? 'mobile' : 'not mobile' }</div>;
}
}

export default withMobileBreakpoint( MyComponent );
```

### Supported methods

- `isWithinBreakpoint( breakpoint )`: Whether the current screen size matches the breakpoint
- `isMobile()`: Whether the current screen size matches a mobile breakpoint (<480px)
- `isDesktop()`: Whether the current screen size matches a desktop breakpoint (>960px)
- `addWithinBreakpointListener( breakpoint, listener )`: Register a listener for size changes that affect the breakpoint
- `removeWithinBreakpointListener( breakpoint, listener )`: Unregister a previously registered listener
- `addIsMobileListener( breakpoint, listener )`: Register a listener for size changes that affect the mobile breakpoint (<480px)
- `removeIsMobileListener( breakpoint, listener )`: Unregister a previously registered listener
- `addIsDesktopListener( breakpoint, listener )`: Register a listener for size changes that affect the desktop breakpoint (>960px)
- `removeIsDesktopListener( breakpoint, listener )`: Unregister a previously registered listener
- `isWithinBreakpoint( breakpoint )`: Whether the current screen size matches the breakpoint.
- `isMobile()`: Whether the current screen size matches a mobile breakpoint (<480px).
- `isDesktop()`: Whether the current screen size matches a desktop breakpoint (>960px).
- `subscribeIsWithinBreakpoint( breakpoint, listener )`: Register a listener for size changes that affect the breakpoint. Returns the unsubscribe function.
- `subscribeIsMobile( listener )`: Register a listener for size changes that affect the mobile breakpoint (<480px). Returns the unsubscribe function.
- `subscribeIsDesktop( listener )`: Register a listener for size changes that affect the desktop breakpoint (>960px). Returns the unsubscribe function.
- `getWindowInnerWidth()`: Get the inner width for the browser window. **Warning**: This method triggers a layout recalc, potentially resulting in performance issues. Please use a breakpoint instead wherever possible.

### Supported hooks

- `useBreakpoint( breakpoint )`: Returns the current status for a breakpoint, and keeps it updated.
- `useMobileBreakpoint()`: Returns the current status for the mobile breakpoint, and keeps it updated.
- `useDesktopBreakpoint()`: Returns the current status for the desktop breakpoint, and keeps it updated.

### Supported higher-order components

- `withBreakpoint( breakpoint )( WrappedComponent )`: Returns a wrapped component with the current status for a breakpoint as the `isBreakpointActive` prop.
- `withMobileBreakpoint( WrappedComponent )`: Returns a wrapped component with the current status for the mobile breakpoint as the `isBreakpointActive` prop.
- `withDesktopBreakpoint( WrappedComponent )`: Returns a wrapped component with the current status for the desktop breakpoint as the `isBreakpointActive` prop.

### Supported breakpoints

- '<480px'
Expand Down
79 changes: 57 additions & 22 deletions client/lib/viewport/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@
// use 769, which is just above the general maximum mobile screen width.
const SERVER_WIDTH = 769;

const MOBILE_BREAKPOINT = '<480px';
const DESKTOP_BREAKPOINT = '>960px';
export const MOBILE_BREAKPOINT = '<480px';
export const DESKTOP_BREAKPOINT = '>960px';

const isServer = typeof window === 'undefined' || ! window.matchMedia;

const noop = () => null;

function createMediaQueryList( { min, max } = {} ) {
if ( min !== undefined && max !== undefined ) {
return isServer
Expand Down Expand Up @@ -100,51 +102,84 @@ function getMediaQueryList( breakpoint ) {
return mediaQueryLists[ breakpoint ];
}

/**
* Returns whether the current window width matches a breakpoint.
* @param {String} breakpoint The breakpoint to consider.
*
* @returns {Boolean} Whether the provided breakpoint is matched.
*/
export function isWithinBreakpoint( breakpoint ) {
const mediaQueryList = getMediaQueryList( breakpoint );
return mediaQueryList ? mediaQueryList.matches : undefined;
}

export function addWithinBreakpointListener( breakpoint, listener ) {
const mediaQueryList = getMediaQueryList( breakpoint );

if ( mediaQueryList && ! isServer ) {
mediaQueryList.addListener( evt => listener( evt.matches ) );
/**
* Registers a listener to be notified of changes to breakpoint matching status.
* @param {String} breakpoint The breakpoint to consider.
* @param {Function} listener The listener to be called on change.
*
* @returns {Function} The function to be called when unsubscribing.
*/
export function subscribeIsWithinBreakpoint( breakpoint, listener ) {
if ( ! listener ) {
return noop;
}
}

export function removeWithinBreakpointListener( breakpoint, listener ) {
const mediaQueryList = getMediaQueryList( breakpoint );

if ( mediaQueryList && ! isServer ) {
mediaQueryList.removeListener( listener );
const wrappedListener = evt => listener( evt.matches );
mediaQueryList.addListener( wrappedListener );
// Return unsubscribe function.
return () => mediaQueryList.removeListener( wrappedListener );
}

return noop;
}

/**
* Returns whether the current window width matches the mobile breakpoint.
*
* @returns {Boolean} Whether the mobile breakpoint is matched.
*/
export function isMobile() {
return isWithinBreakpoint( MOBILE_BREAKPOINT );
}

export function addIsMobileListener( listener ) {
return addWithinBreakpointListener( MOBILE_BREAKPOINT, listener );
}

export function removeIsMobileListener( listener ) {
return removeWithinBreakpointListener( MOBILE_BREAKPOINT, listener );
/**
* Registers a listener to be notified of changes to mobile breakpoint matching status.
* @param {Function} listener The listener to be called on change.
*
* @returns {Function} The registered subscription; undefined if none.
*/
export function subscribeIsMobile( listener ) {
return subscribeIsWithinBreakpoint( MOBILE_BREAKPOINT, listener );
}

/**
* Returns whether the current window width matches the desktop breakpoint.
*
* @returns {Boolean} Whether the desktop breakpoint is matched.
*/
export function isDesktop() {
return isWithinBreakpoint( DESKTOP_BREAKPOINT );
}

export function addIsDesktopListener( listener ) {
return addWithinBreakpointListener( DESKTOP_BREAKPOINT, listener );
}

export function removeIsDesktopListener( listener ) {
return removeWithinBreakpointListener( DESKTOP_BREAKPOINT, listener );
/**
* Registers a listener to be notified of changes to desktop breakpoint matching status.
* @param {Function} listener The listener to be called on change.
*
* @returns {Function} The registered subscription; undefined if none.
*/
export function subscribeIsDesktop( listener ) {
return subscribeIsWithinBreakpoint( DESKTOP_BREAKPOINT, listener );
}

/**
* Returns the current window width.
* Avoid using this method, as it triggers a layout recalc.
* @returns {Number} The current window width, in pixels.
*/
export function getWindowInnerWidth() {
return isServer ? SERVER_WIDTH : window.innerWidth;
}
Loading