Last active
August 2, 2019 09:21
-
-
Save shadyvb/c6c2b892377c87dde74816fa48e4dbd0 to your computer and use it in GitHub Desktop.
Reusable React effects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/content/themes/siemens-ingenuity/verbose/js/app/components/header-search.js b/content/themes/siemens-ingenuity/verbose/js/app/components/header-search.js | |
index fcbe02b7..bf12822a 100644 | |
--- a/content/themes/siemens-ingenuity/verbose/js/app/components/header-search.js | |
+++ b/content/themes/siemens-ingenuity/verbose/js/app/components/header-search.js | |
@@ -1,6 +1,7 @@ | |
-import PropTypes from 'prop-types'; | |
-import React from 'react'; | |
-import { __ } from '@wordpress/i18n'; | |
+import PropTypes from 'prop-types'; | |
+import React, { useEffect, useRef, useState } from 'react'; | |
+import { __ } from '@wordpress/i18n'; | |
+import { onClickedOutside, onKeyboardActions } from '../effects'; | |
/** | |
* Component to render the search form and related UI in the site header. | |
@@ -11,6 +12,16 @@ import { __ } from '@wordpress/i18n'; | |
* @constructor | |
*/ | |
const HeaderSearch = ( { keyword, onChange } ) => { | |
+ const nodeRef = useRef(); | |
+ const actions = [ | |
+ [ 'ArrowUp', () => console.log( 'test up' ) ], | |
+ [ 'ArrowDown', () => console.log( 'test down' ) ], | |
+ ]; | |
+ useEffect( onKeyboardActions( nodeRef, actions ), [] ); | |
+ | |
+ const [ isOptionsVisible, setIsOptionsVisible ] = useState( false ); | |
+ useEffect( onClickedOutside( [ nodeRef ], setIsOptionsVisible ), [] ); | |
+ | |
return ( | |
<form | |
id="form-search-input-inline" | |
@@ -30,6 +41,7 @@ const HeaderSearch = ( { keyword, onChange } ) => { | |
placeholder={ __( 'Search', 'siemens' ) } | |
required | |
type="search" | |
+ ref={ nodeRef } | |
value={ keyword } | |
/> | |
<button className="header__searchSubmit" aria-label={ __( 'Search', 'siemens' ) } type="submit" /> | |
diff --git a/content/themes/siemens-ingenuity/verbose/js/app/effects/index.js b/content/themes/siemens-ingenuity/verbose/js/app/effects/index.js | |
index e69de29b..9b5877cb 100644 | |
--- a/content/themes/siemens-ingenuity/verbose/js/app/effects/index.js | |
+++ b/content/themes/siemens-ingenuity/verbose/js/app/effects/index.js | |
@@ -0,0 +1,66 @@ | |
+import { useEffect } from 'react'; | |
+ | |
+/** | |
+ * React effect: Handle clicking outside of an element | |
+ * | |
+ * Usage: | |
+ * `useEffect( onClickedOutside( nodeRefs, setIsVisible ), [] );` | |
+ * Where | |
+ * - `nodeRefs` are refs for elements that do not steal focus off the popover | |
+ * - `setIsVisible` is the callback to set the state, coming from `useState` | |
+ * | |
+ * @param {Array<React.Ref>} nodeRefs Array of nodes to exclude | |
+ * @param {Function} setState Callback to execute | |
+ * | |
+ * @returns {function(): function(): *} | |
+ */ | |
+export const onClickedOutside = ( nodeRefs, setState ) => { | |
+ // Document click handler | |
+ const onClick = e => { | |
+ // See if the click target is within one of the passed refs | |
+ if ( nodeRefs.filter( ref => ref.current && ref.current.contains( e.target ) ).length === 0 ) { | |
+ // If not, set the state to false | |
+ setState( false ); | |
+ } | |
+ }; | |
+ | |
+ // Return a function ( to execute on componentDidMount ) | |
+ // .. that returns a cleaning function ( to execute on componentWillUnmount ) | |
+ return () => { | |
+ document.addEventListener( 'mousedown', onClick ); | |
+ return () => document.removeEventListener( 'mousedown', onClick ); | |
+ } | |
+}; | |
+ | |
+/** | |
+ * React effect: Execute callbacks on keyboard keys pressed | |
+ * | |
+ * Usage: | |
+ * `useEffect( onKeyboardActions( nodeRef, actionMap ), [] );` | |
+ * Where | |
+ * - `nodeRef` is a ref for the tracked element | |
+ * - `actionMap` is an array of objects, each with a `key` and a `callback` | |
+ * | |
+ * @param {Object<{current:String}>} nodeRef | |
+ * @param {Array<Array<String|Function>>} actionMap | |
+ * | |
+ * @returns {function(): function(): *} | |
+ */ | |
+export const onKeyboardActions = ( nodeRef, actionMap ) => { | |
+ /** | |
+ * Keyboard press event handler | |
+ * @param {KeyboardEvent} e | |
+ */ | |
+ const onPress = e => actionMap | |
+ .filter( ( [ key ] ) => ( | |
+ ( typeof key === 'string' && e.key === key ) | |
+ || | |
+ ( typeof key === 'function' && key( e ) ) | |
+ ) ) | |
+ .map( ( [ , callback ] ) => callback() ); | |
+ | |
+ return () => { | |
+ nodeRef.current && nodeRef.current.addEventListener( 'keydown', onPress ); | |
+ return () => nodeRef.current && nodeRef.current.removeEventListener( 'keydown', onPress ); | |
+ } | |
+}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment