Skip to content

Instantly share code, notes, and snippets.

@shadyvb
Last active August 2, 2019 09:21
Show Gist options
  • Save shadyvb/c6c2b892377c87dde74816fa48e4dbd0 to your computer and use it in GitHub Desktop.
Save shadyvb/c6c2b892377c87dde74816fa48e4dbd0 to your computer and use it in GitHub Desktop.
Reusable React effects
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