Repository: mlaursen/react-md
Pull request #947 (2020-09-07 16:16:08)

Added componentRef and containerRef props to FocusContainer
Hello,

I've enchanced `FocusContainer` to simplify its usage (for example with `Drawer`).

mlaursen: Looks good to me, thanks! I'll release this as 1.17.0 in a little bit.

Patch:
diff --git a/src/js/Helpers/FocusContainer.d.ts b/src/js/Helpers/FocusContainer.d.ts
index a9bf7311fb..c1aa473e6f 100644
--- a/src/js/Helpers/FocusContainer.d.ts
+++ b/src/js/Helpers/FocusContainer.d.ts
@@ -6,6 +6,8 @@ export interface FocusContainerProps extends Props {
   [key: string]: any;
 
   component?: React.ReactType;
+  componentRef?: React.Ref<any>;
+  containerRef?: React.Ref<any>;
   children?: React.ReactNode;
   initialFocus?: string;
   focusOnMount?: boolean;
@@ -13,5 +15,9 @@ export interface FocusContainerProps extends Props {
   containFocus?: boolean;
 }
 
-declare const FocusContainer: React.ComponentClass<FocusContainerProps>;
+interface FocusContainerComponent extends React.ComponentClass<FocusContainerProps> {
+  getContainer(): React.ReactElement<any>;
+}
+
+declare const FocusContainer: FocusContainerComponent;
 export default FocusContainer;
diff --git a/src/js/Helpers/FocusContainer.js b/src/js/Helpers/FocusContainer.js
index 3c1b1a85bd..9dd278ba86 100644
--- a/src/js/Helpers/FocusContainer.js
+++ b/src/js/Helpers/FocusContainer.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import { findDOMNode } from 'react-dom';
 
 import isValidFocusKeypress from '../utils/EventUtils/isValidFocusKeypress';
+import setRef from '../utils/ref';
 
 const BASE_FOCUSABLE_ELEMENTS = '[href],[tabindex]:not([tabindex="-1"]),input:not([disabled]):not([type="hidden"])';
 const FOCUSABLE_QUERY = ['button', 'textarea', 'select']
@@ -27,6 +28,27 @@ export default class FocusContainer extends PureComponent {
       PropTypes.object,
     ]).isRequired,
 
+    /**
+     * An optional ref to get reference to the rendered component.
+     * Just like other refs, this will provide null when it unmounts.
+     */
+    componentRef: PropTypes.oneOfType([
+      PropTypes.func,
+      PropTypes.object,
+    ]),
+
+    /**
+     * An optional ref to get reference to the top-most element of the rendered container.
+     * Just like other refs, this will provide null when it unmounts.
+     *
+     * This is helpful if you'd like access the DOM node for a parent Component without needing to use
+     * `ReactDOM.findDOMNode`.
+     */
+    containerRef: PropTypes.oneOfType([
+      PropTypes.func,
+      PropTypes.object,
+    ]),
+
     /**
      * An optional style to apply.
      */
@@ -105,6 +127,15 @@ export default class FocusContainer extends PureComponent {
     }
   }
 
+  /**
+   * Return reference to the top-most DOM node of the rendered container.
+   *
+   * @return {Element|null|undefined} Reference to the top-most DOM node of the rendered container.
+   */
+  getContainer() {
+    return this._container;
+  }
+
   _enableFocusTrap = () => {
     window.addEventListener('keydown', this._handleKeyDown, true);
   };
@@ -151,14 +182,18 @@ export default class FocusContainer extends PureComponent {
    * if the `focusOnMount` prop is `true`.
    */
   _containFocus = (containerRef) => {
+    const { props } = this;
+    setRef(props.componentRef, containerRef);
     if (containerRef === null) {
       this._container = null;
       this._disableFocusTrap();
+      setRef(props.containerRef, null);
       return;
     }
 
-    const { focusOnMount, containFocus } = this.props;
+    const { focusOnMount, containFocus } = props;
     this._container = findDOMNode(containerRef);
+    setRef(props.containerRef, this._container);
     this._focusables = [].slice.call(this._container.querySelectorAll(FOCUSABLE_QUERY));
 
     if (focusOnMount) {
diff --git a/src/js/Tabs/TabsContainer.js b/src/js/Tabs/TabsContainer.js
index 8f049a47da..38b656d437 100644
--- a/src/js/Tabs/TabsContainer.js
+++ b/src/js/Tabs/TabsContainer.js
@@ -7,6 +7,7 @@ import SwipeableViews from 'react-swipeable-views';
 import getField from '../utils/getField';
 import controlled from '../utils/PropTypes/controlled';
 import between from '../utils/PropTypes/between';
+import setRef from '../utils/ref';
 import ResizeObserver from '../Helpers/ResizeObserver';
 import Paper from '../Papers/Paper';
 import TabPanel from './TabPanel';
@@ -286,16 +287,8 @@ export default class TabsContainer extends PureComponent {
   };
 
   _setContainer = (container) => {
-    const { componentRef } = this.props;
     this._container = findDOMNode(container);
-    if (componentRef) {
-      const refType = typeof componentRef;
-      if (refType === 'function') {
-        componentRef(this._container);
-      } else if (refType === 'object') {
-        componentRef.current = this._container;
-      }
-    }
+    setRef(this.props.componentRef, this._container);
   };
 
   _setTabs = (tabs) => {
diff --git a/src/js/utils/isValued.js b/src/js/utils/isValued.js
index e3b9539141..57a5a41355 100644
--- a/src/js/utils/isValued.js
+++ b/src/js/utils/isValued.js
@@ -1,4 +1,4 @@
-/** @module utils/isValied */
+/** @module utils/isValued */
 
 /**
  * A really simple utility function to check if an input's value is considered "valued".
diff --git a/src/js/utils/ref.js b/src/js/utils/ref.js
new file mode 100644
index 0000000000..2c1174beae
--- /dev/null
+++ b/src/js/utils/ref.js
@@ -0,0 +1,18 @@
+/** @module utils/ref */
+
+/**
+ * A utility function to set/save `ref`.
+ *
+ * @param {function|object} target - a function or an object that is used to save given reference
+ * @param {object} value - a reference value that should be saved
+ */
+export default function setRef(target, value) {
+  if (target) {
+    const targetType = typeof target;
+    if (targetType === 'function') {
+      target(value);
+    } else if (targetType === 'object') {
+      target.current = value;
+    }
+  }
+}


Test patch:
diff --git a/src/js/utils/__tests__/ref.js b/src/js/utils/__tests__/ref.js
new file mode 100644
index 0000000000..68c14963fa
--- /dev/null
+++ b/src/js/utils/__tests__/ref.js
@@ -0,0 +1,20 @@
+/* eslint-env jest */
+import setRef from '../ref';
+
+describe('setRef', () => {
+  const testRef = { a: 1 };
+  it('should call specified function to save ref value', () => {
+    let savedRef;
+    function target(ref) {
+      savedRef = ref;
+    }
+    setRef(target, testRef);
+    expect(savedRef).toBe(testRef);
+  });
+
+  it('should save ref value in \'current\' field of passed object', () => {
+    const target = {};
+    setRef(target, testRef);
+    expect(target.current).toBe(testRef);
+  });
+});