Skip to content

Commit

Permalink
Gif block: refactor and add test coverage (#19066)
Browse files Browse the repository at this point in the history
* Initial commit

* Focusing forward ref input field

* Added tests

* Added @testing-library/react-hooks library to unit test custom hooks
 Importing regenerator runtime to polyfill transpiled generator functions for react hooks testing library
 Added custom hook to fetch api data
 Added tests for custom hook and updated tests for edit.js

* revert css change as it would be a UI regression

* Removing previous version (kept it for reference while developing)

* Add changelog

* Here we are extracting getSelectedGiphyAttributes to utils. getSelectedGiphyAttributes is not referencing any props or state so we can use it in the useEffect hook and not reference it.

* Using a regex to capture known giphy paths and extract the id. This is a little stricter than testing for the index of the domain.
  • Loading branch information
ramonjd authored Mar 15, 2021
1 parent 1cf67f2 commit 2c1efe3
Show file tree
Hide file tree
Showing 19 changed files with 788 additions and 201 deletions.
4 changes: 4 additions & 0 deletions projects/plugins/jetpack/changelog/refactor-gif-block
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: compat

Git block: Refactored class component into a functional one and added unit tests. No functional/UI changes. Changelog optional.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* External dependencies
*/

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { forwardRef } from '@wordpress/element';
import { Button } from '@wordpress/components';
/**
* Internal dependencies
*/

export function SearchForm( { onSubmit, onChange, value = '' }, ref ) {
return (
<form
className="wp-block-jetpack-gif_input-container"
onSubmit={ onSubmit }
>
<input
type="text"
className="wp-block-jetpack-gif_input components-placeholder__input"
placeholder={ __( 'Enter search terms, e.g. cat…', 'jetpack' ) }
value={ value }
onChange={ onChange }
ref={ ref }
aria-labelledby="wp-block-jetpack-gif_search-button"
/>
<Button id="wp-block-jetpack-gif_search-button" isSecondary type="submit">
{ __( 'Search', 'jetpack' ) }
</Button>
</form>
);
}

export default forwardRef( SearchForm );
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const GIPHY_API_KEY = 't1PkR1Vq0mzHueIFBvZSZErgFs9NBmYW';
19 changes: 19 additions & 0 deletions projects/plugins/jetpack/extensions/blocks/gif/controls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* External dependencies
*/
import { PanelBody, Path, SVG } from '@wordpress/components';
import { InspectorControls } from '@wordpress/block-editor';

export default function Controls() {
return (
<InspectorControls>
<PanelBody className="components-panel__body-gif-branding">
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 202 22">
<Path d="M4.6 5.9H0v10h1.6v-3.1h3c4.8 0 4.8-6.9 0-6.9zm0 5.4h-3v-4h3c2.6.1 2.6 4 0 4zM51.2 12.3c2-.3 2.7-1.7 2.7-3.1 0-1.7-1.2-3.3-3.5-3.3h-4.6v10h1.6v-3.4h2.1l3 3.4h1.9l-.2-.3-3-3.3zM47.4 11V7.4h3c1.3 0 1.9.9 1.9 1.8s-.6 1.8-1.9 1.8h-3zM30.6 13.6L28 5.9h-1.1l-2.5 7.7-2.6-7.7H20l3.7 10H25l1.4-3.5L27.5 9l1.1 3.4 1.3 3.5h1.4l3.5-10h-1.7z" />
<Path d="M14.4 5.7c-3 0-5.1 2.2-5.1 5.2 0 2.6 1.6 5.1 5.1 5.1 3.5 0 5.1-2.5 5.1-5.2-.1-2.6-1.7-5.1-5.1-5.1zm-.1 8.9c-2.5 0-3.5-1.9-3.5-3.7 0-2.2 1.2-3.8 3.5-3.8 2.4 0 3.5 2 3.5 3.8.1 2-1 3.7-3.5 3.7zM57.7 11.6h5.5v-1.5h-5.5V7.4h5.7V5.9h-7.3v10h7.3v-1.6h-5.7zM38 14.3v-2.7h5.5v-1.5H38V7.4h5.7V5.9h-7.3v10h7.3v-1.6zM93 10.3l-2.7-4.4h-1.9V6l3.8 5.8v4.1h1.6v-4.1l4-5.8v-.1h-2zM69.3 5.9h-3.8v10h3.8c3.5 0 5.1-2.5 5-5.1-.1-2.5-1.6-4.9-5-4.9zm0 8.4h-2.2V7.4h2.2c2.3 0 3.4 1.7 3.4 3.4s-1 3.5-3.4 3.5zM86.3 10.7c.9-.4 1.4-1.1 1.4-2 0-2-1.5-2.8-3.4-2.8h-4.6v10h4.6c2 0 3.7-.7 3.7-2.8 0-.8-.5-2-1.7-2.4zm-5-3.4h3c1.2 0 1.8.7 1.8 1.4 0 .8-.6 1.3-1.8 1.3h-3V7.3zm3 7.1h-3v-2.9h3c.9 0 2.1.5 2.1 1.6 0 1-1.2 1.3-2.1 1.3zM113.9 13.3h5.3V16c-1.2.9-2.9 1.1-4 1.1-4.2 0-5.6-3.3-5.6-6 0-4.1 2.2-6.1 5.6-6.1 1.4 0 3.2.4 4.8 1.8l3.4-3.4C120.7.6 118.1 0 115.2 0c-7.8 0-11.4 5.6-11.4 11s3.1 10.9 11.4 10.9c4 0 7.6-1.4 8.9-4.1V8.6h-10.2v4.7zM171.9 8.5h-7.4V.6h-5.9v20.8h5.9v-7.8h7.4v7.8h5.9V.6h-5.9zM195.1.6l-4.5 7.1-4.3-7.1h-6.6v.2l7.9 12.3v8.3h5.9v-8.3L201.8.9V.6zM127.4.6h5.9v20.8h-5.9zM147.6.6h-10.1v20.8h5.9v-5.6h4.2c5.6-.1 8.3-3.4 8.3-7.6.1-4.1-2.7-7.6-8.3-7.6zm0 10.2h-4.2V5.6h4.2c1.6 0 2.5 1.2 2.5 2.6 0 1.4-.9 2.6-2.5 2.6z" />
</SVG>
</PanelBody>
</InspectorControls>
);
}

307 changes: 106 additions & 201 deletions projects/plugins/jetpack/extensions/blocks/gif/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,220 +3,125 @@
*/
import classNames from 'classnames';
import { __ } from '@wordpress/i18n';
import { Component, createRef } from '@wordpress/element';
import { Button, PanelBody, Path, Placeholder, SVG } from '@wordpress/components';
import { InspectorControls, RichText } from '@wordpress/block-editor';
import { createRef, useState, useEffect } from '@wordpress/element';
import { Placeholder } from '@wordpress/components';
import { RichText } from '@wordpress/block-editor';

/**
* Internal dependencies
*/
import { icon, title } from './';

const GIPHY_API_KEY = 't1PkR1Vq0mzHueIFBvZSZErgFs9NBmYW';
const INPUT_PROMPT = __( 'Search for a term or paste a Giphy URL', 'jetpack' );

class GifEdit extends Component {
textControlRef = createRef();

state = {
captionFocus: false,
results: null,
};

onFormSubmit = event => {
event.preventDefault();
this.onSubmit();
};

onSubmit = () => {
const { attributes } = this.props;
const { searchText } = attributes;
this.parseSearch( searchText );
import { getUrl, getSelectedGiphyAttributes } from './utils';
import SearchForm from './components/search-form';
import Controls from './controls';
import useFetchGiphyData from './hooks/use-fetch-giphy-data';

function GifEdit( {
attributes,
setAttributes,
className,
isSelected,
} ) {
const { align, caption, giphyUrl, searchText, paddingTop } = attributes;
const classes = classNames( className, `align${ align }` );
const [ captionFocus, setCaptionFocus ] = useState( false );
const searchFormInputRef = createRef();
const { isFetching, giphyData, fetchGiphyData } = useFetchGiphyData();

const setSearchInputFocus = () => {
searchFormInputRef.current.focus();
setCaptionFocus( false );
};

parseSearch = searchText => {
let giphyID = null;
// If search is hardcoded Giphy URL following this pattern: https://giphy.com/embed/4ZFekt94LMhNK
if ( searchText.indexOf( '//giphy.com/gifs' ) !== -1 ) {
giphyID = this.splitAndLast( this.splitAndLast( searchText, '/' ), '-' );
}
// If search is hardcoded Giphy URL following this patterh: http://i.giphy.com/4ZFekt94LMhNK.gif
if ( searchText.indexOf( '//i.giphy.com' ) !== -1 ) {
giphyID = this.splitAndLast( searchText, '/' ).replace( '.gif', '' );
}
// https://media.giphy.com/media/gt0hYzKlMpfOg/giphy.gif
const match = searchText.match(
/http[s]?:\/\/media.giphy.com\/media\/([A-Za-z0-9\-.]+)\/giphy.gif/
);
if ( match ) {
giphyID = match[ 1 ];
}
if ( giphyID ) {
return this.fetch( this.urlForId( giphyID ) );
useEffect( () => {
if ( giphyData && giphyData[ 0 ] ) {
setAttributes( getSelectedGiphyAttributes( giphyData[ 0 ] ) );
}
}, [ giphyData, setAttributes ] );

return this.fetch( this.urlForSearch( searchText ) );
};

urlForSearch = searchText => {
return `https://api.giphy.com/v1/gifs/search?q=${ encodeURIComponent(
searchText
) }&api_key=${ encodeURIComponent( GIPHY_API_KEY ) }&limit=10`;
};

urlForId = giphyId => {
return `https://api.giphy.com/v1/gifs/${ encodeURIComponent(
giphyId
) }?api_key=${ encodeURIComponent( GIPHY_API_KEY ) }`;
};

splitAndLast = ( array, delimiter ) => {
const split = array.split( delimiter );
return split[ split.length - 1 ];
};

fetch = url => {
const xhr = new XMLHttpRequest();
xhr.open( 'GET', url );
xhr.onload = () => {
if ( xhr.status === 200 ) {
const res = JSON.parse( xhr.responseText );
// If there is only one result, Giphy's API does not return an array.
// The following statement normalizes the data into an array with one member in this case.
const results = typeof res.data.images !== 'undefined' ? [ res.data ] : res.data;
const giphyData = results[ 0 ];
// No results
if ( ! giphyData.images ) {
return;
}
this.setState( { results }, () => {
this.selectGiphy( giphyData );
} );
} else {
// Error handling TK
}
};
xhr.send();
};

selectGiphy = giphy => {
const { setAttributes } = this.props;
const calculatedPaddingTop = Math.floor(
( giphy.images.original.height / giphy.images.original.width ) * 100
);
const paddingTop = `${ calculatedPaddingTop }%`;
const giphyUrl = giphy.embed_url;
setAttributes( { giphyUrl, paddingTop } );
};

setFocus = () => {
this.textControlRef.current.querySelector( 'input' ).focus();
this.setState( { captionFocus: false } );
};
const onSubmit = ( event ) => {
event.preventDefault();

hasSearchText = () => {
const { attributes } = this.props;
const { searchText } = attributes;
return searchText && searchText.length > 0;
};
if ( ! attributes.searchText || isFetching ) {
return;
}

thumbnailClicked = thumbnail => {
this.selectGiphy( thumbnail );
fetchGiphyData( getUrl( attributes.searchText ) );
};

render() {
const { attributes, className, isSelected, setAttributes } = this.props;
const { align, caption, giphyUrl, searchText, paddingTop } = attributes;
const { captionFocus, results } = this.state;
const style = { paddingTop };
const classes = classNames( className, `align${ align }` );
const inputFields = (
<form
className="wp-block-jetpack-gif_input-container"
onSubmit={ this.onFormSubmit }
ref={ this.textControlRef }
>
<input
type="text"
className="wp-block-jetpack-gif_input components-placeholder__input"
placeholder={ __( 'Enter search terms, e.g. cat…', 'jetpack' ) }
onChange={ event => setAttributes( { searchText: event.target.value } ) }
value={ searchText }
/>
<Button isDefault onClick={ this.onSubmit }>
{ __( 'Search', 'jetpack' ) }
</Button>
</form>
);
return (
<div className={ classes }>
<InspectorControls>
<PanelBody className="components-panel__body-gif-branding">
<SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 202 22">
<Path d="M4.6 5.9H0v10h1.6v-3.1h3c4.8 0 4.8-6.9 0-6.9zm0 5.4h-3v-4h3c2.6.1 2.6 4 0 4zM51.2 12.3c2-.3 2.7-1.7 2.7-3.1 0-1.7-1.2-3.3-3.5-3.3h-4.6v10h1.6v-3.4h2.1l3 3.4h1.9l-.2-.3-3-3.3zM47.4 11V7.4h3c1.3 0 1.9.9 1.9 1.8s-.6 1.8-1.9 1.8h-3zM30.6 13.6L28 5.9h-1.1l-2.5 7.7-2.6-7.7H20l3.7 10H25l1.4-3.5L27.5 9l1.1 3.4 1.3 3.5h1.4l3.5-10h-1.7z" />
<Path d="M14.4 5.7c-3 0-5.1 2.2-5.1 5.2 0 2.6 1.6 5.1 5.1 5.1 3.5 0 5.1-2.5 5.1-5.2-.1-2.6-1.7-5.1-5.1-5.1zm-.1 8.9c-2.5 0-3.5-1.9-3.5-3.7 0-2.2 1.2-3.8 3.5-3.8 2.4 0 3.5 2 3.5 3.8.1 2-1 3.7-3.5 3.7zM57.7 11.6h5.5v-1.5h-5.5V7.4h5.7V5.9h-7.3v10h7.3v-1.6h-5.7zM38 14.3v-2.7h5.5v-1.5H38V7.4h5.7V5.9h-7.3v10h7.3v-1.6zM93 10.3l-2.7-4.4h-1.9V6l3.8 5.8v4.1h1.6v-4.1l4-5.8v-.1h-2zM69.3 5.9h-3.8v10h3.8c3.5 0 5.1-2.5 5-5.1-.1-2.5-1.6-4.9-5-4.9zm0 8.4h-2.2V7.4h2.2c2.3 0 3.4 1.7 3.4 3.4s-1 3.5-3.4 3.5zM86.3 10.7c.9-.4 1.4-1.1 1.4-2 0-2-1.5-2.8-3.4-2.8h-4.6v10h4.6c2 0 3.7-.7 3.7-2.8 0-.8-.5-2-1.7-2.4zm-5-3.4h3c1.2 0 1.8.7 1.8 1.4 0 .8-.6 1.3-1.8 1.3h-3V7.3zm3 7.1h-3v-2.9h3c.9 0 2.1.5 2.1 1.6 0 1-1.2 1.3-2.1 1.3zM113.9 13.3h5.3V16c-1.2.9-2.9 1.1-4 1.1-4.2 0-5.6-3.3-5.6-6 0-4.1 2.2-6.1 5.6-6.1 1.4 0 3.2.4 4.8 1.8l3.4-3.4C120.7.6 118.1 0 115.2 0c-7.8 0-11.4 5.6-11.4 11s3.1 10.9 11.4 10.9c4 0 7.6-1.4 8.9-4.1V8.6h-10.2v4.7zM171.9 8.5h-7.4V.6h-5.9v20.8h5.9v-7.8h7.4v7.8h5.9V.6h-5.9zM195.1.6l-4.5 7.1-4.3-7.1h-6.6v.2l7.9 12.3v8.3h5.9v-8.3L201.8.9V.6zM127.4.6h5.9v20.8h-5.9zM147.6.6h-10.1v20.8h5.9v-5.6h4.2c5.6-.1 8.3-3.4 8.3-7.6.1-4.1-2.7-7.6-8.3-7.6zm0 10.2h-4.2V5.6h4.2c1.6 0 2.5 1.2 2.5 2.6 0 1.4-.9 2.6-2.5 2.6z" />
</SVG>
</PanelBody>
</InspectorControls>
{ ! giphyUrl ? (
<Placeholder
className="wp-block-jetpack-gif_placeholder"
icon={ icon }
label={ title }
instructions={ INPUT_PROMPT }
>
{ inputFields }
</Placeholder>
) : (
<figure>
{ isSelected && inputFields }
{ isSelected && results && results.length > 1 && (
<div className="wp-block-jetpack-gif_thumbnails-container">
{ results.map( thumbnail => {
const thumbnailStyle = {
backgroundImage: `url(${ thumbnail.images.downsized_still.url })`,
};
return (
<button
className="wp-block-jetpack-gif_thumbnail-container"
key={ thumbnail.id }
onClick={ () => {
this.thumbnailClicked( thumbnail );
} }
style={ thumbnailStyle }
/>
);
} ) }
</div>
) }
<div className="wp-block-jetpack-gif-wrapper" style={ style }>
<div
className="wp-block-jetpack-gif_cover"
onClick={ this.setFocus }
onKeyDown={ this.setFocus }
role="button"
tabIndex="0"
/>
<iframe src={ giphyUrl } title={ searchText } />
const onChange = ( event ) => setAttributes( { searchText: event.target.value } );
const onSelectThumbnail = ( thumbnail ) => setAttributes( getSelectedGiphyAttributes( thumbnail ) );

return (
<div className={ classes }>
<Controls />
{ ! giphyUrl ? (
<Placeholder
className="wp-block-jetpack-gif_placeholder"
icon={ icon }
label={ title }
instructions={ __( 'Search for a term or paste a Giphy URL', 'jetpack' ) }
>
<SearchForm
onSubmit={ onSubmit }
onChange={ onChange }
value={ searchText }
ref={ searchFormInputRef }
/>
</Placeholder>
) : (
<figure>
{ isSelected && (
<SearchForm
onSubmit={ onSubmit }
onChange={ onChange }
value={ searchText }
ref={ searchFormInputRef }
/>
) }
{ isSelected && giphyData && giphyData.length > 1 && (
<div className="wp-block-jetpack-gif_thumbnails-container">
{ giphyData.map( thumbnail => {
const thumbnailStyle = {
backgroundImage: `url(${ thumbnail.images.downsized_still.url })`,
};
return (
<button
className="wp-block-jetpack-gif_thumbnail-container"
key={ thumbnail.id }
onClick={ () => onSelectThumbnail( thumbnail ) }
style={ thumbnailStyle }
/>
);
} ) }
</div>
{ ( ! RichText.isEmpty( caption ) || isSelected ) && !! giphyUrl && (
<RichText
className="wp-block-jetpack-gif-caption gallery-caption"
inlineToolbar
isSelected={ captionFocus }
unstableOnFocus={ () => {
this.setState( { captionFocus: true } );
} }
onChange={ value => setAttributes( { caption: value } ) }
placeholder={ __( 'Write caption…', 'jetpack' ) }
tagName="figcaption"
value={ caption }
/>
) }
</figure>
) }
</div>
);
}
) }
<div className="wp-block-jetpack-gif-wrapper" style={ { paddingTop } }>
<div
className="wp-block-jetpack-gif_cover"
onClick={ setSearchInputFocus }
onKeyDown={ setSearchInputFocus }
role="button"
tabIndex="0"
/>
<iframe src={ giphyUrl } title={ searchText } />
</div>
{ ( ! RichText.isEmpty( caption ) || isSelected ) && !! giphyUrl && (
<RichText
className="wp-block-jetpack-gif-caption gallery-caption"
inlineToolbar
isSelected={ captionFocus }
unstableOnFocus={ () => setCaptionFocus( false ) }
onChange={ value => setAttributes( { caption: value } ) }
placeholder={ __( 'Write caption…', 'jetpack' ) }
tagName="figcaption"
value={ caption }
/>
) }
</figure>
) }
</div>
);
}

export default GifEdit;
Loading

0 comments on commit 2c1efe3

Please sign in to comment.