diff --git a/src/components/UIShell/SideNav.js b/src/components/UIShell/SideNav.js
index 8b1720fa2ada..a9df78996fc9 100644
--- a/src/components/UIShell/SideNav.js
+++ b/src/components/UIShell/SideNav.js
@@ -5,131 +5,120 @@
* LICENSE file in the root directory of this source tree.
*/
+import React, { useState, useRef } from 'react';
import { settings } from 'carbon-components';
import cx from 'classnames';
-import React from 'react';
import PropTypes from 'prop-types';
import { AriaLabelPropType } from '../../prop-types/AriaPropTypes';
import SideNavFooter from './SideNavFooter';
const { prefix } = settings;
-const translations = {
- 'carbon.sidenav.state.open': 'Close',
- 'carbon.sidenav.state.closed': 'Open',
-};
-
-function translateById(id) {
- return translations[id];
-}
-
-export default class SideNav extends React.Component {
- static propTypes = {
- /**
- * Specify whether the side navigation is expanded or collapsed
- */
- isExpanded: PropTypes.bool,
- /**
- * Required props for accessibility label on the underlying menu
- */
- ...AriaLabelPropType,
-
- /**
- * Optionally provide a custom class to apply to the underlying
node
- */
- className: PropTypes.string,
-
- /**
- * Provide a custom function for translating all message ids within this
- * component. This function will take in two arguments: the mesasge Id and the
- * state of the component. From this, you should return a string representing
- * the label you want displayed or read by screen readers.
- */
- translateById: PropTypes.func,
+const SideNav = React.forwardRef(function SideNav(props, ref) {
+ const {
+ expanded: expandedProp,
+ defaultExpanded,
+ 'aria-label': ariaLabel,
+ 'aria-labelledby': ariaLabelledBy,
+ children,
+ onToggle,
+ className: customClassName,
+ translateById: t,
+ } = props;
+ const { current: controlled } = useRef(expandedProp !== undefined);
+ const [expandedState, setExpandedState] = useState(defaultExpanded);
+ const expanded = controlled ? expandedProp : expandedState;
+
+ const handleToggle = (event, value = !expanded) => {
+ if (!controlled) {
+ setExpandedState(value);
+ }
+ if (onToggle) {
+ onToggle(event, value);
+ }
};
- static defaultProps = {
- translateById,
- isExpanded: false,
+ const accessibilityLabel = {
+ 'aria-label': ariaLabel,
+ 'aria-labelledby': ariaLabelledBy,
};
- constructor(props) {
- super(props);
-
- this.state = {
- prevIsExpanded: props.isExpanded,
- isExpanded: props.isExpanded,
- isFocused: false,
+ const assistiveText = expanded
+ ? t('carbon.sidenav.state.open')
+ : t('carbon.sidenav.state.closed');
+
+ const className = cx({
+ [`${prefix}--side-nav`]: true,
+ [`${prefix}--side-nav--expanded`]: expanded,
+ [customClassName]: !!customClassName,
+ });
+
+ return (
+
+ );
+});
+
+SideNav.defaultProps = {
+ translateById: id => {
+ const translations = {
+ 'carbon.sidenav.state.open': 'Close',
+ 'carbon.sidenav.state.closed': 'Open',
};
- }
-
- static getDerivedStateFromProps({ isExpanded }, state) {
- return state && state.prevIsExpanded === isExpanded
- ? null
- : {
- prevIsExpanded: isExpanded,
- isExpanded,
- };
- }
-
- handleExpand = () => {
- this.setState(state => ({ isExpanded: !state.isExpanded }));
- };
-
- handleFocus = () => {
- this.setState(state => {
- if (!state.isFocused) {
- return { isFocused: true };
- }
- return null;
- });
- };
+ return translations[id];
+ },
+ defaultExpanded: false,
+};
- handleBlur = () => {
- this.setState(state => {
- if (state.isFocused) {
- return { isFocused: false };
- }
- return null;
- });
- };
+SideNav.propTypes = {
+ /**
+ * If `true`, the SideNav will be expanded, otherwise it will be collapsed.
+ * Using this prop causes SideNav to become a controled component.
+ */
+ expanded: PropTypes.bool,
+
+ /**
+ * If `true`, the SideNav will be open on initial render.
+ */
+ defaultExpanded: PropTypes.bool,
+
+ /**
+ * An optional listener that is called when an event that would cause
+ * toggling the SideNav occurs.
+ *
+ * @param {object} event
+ * @param {boolean} value
+ */
+ onToggle: PropTypes.func,
+
+ /**
+ * Required props for accessibility label on the underlying menu
+ */
+ ...AriaLabelPropType,
+
+ /**
+ * Optionally provide a custom class to apply to the underlying node
+ */
+ className: PropTypes.string,
+
+ /**
+ * Provide a custom function for translating all message ids within this
+ * component. This function will take in two arguments: the mesasge Id and the
+ * state of the component. From this, you should return a string representing
+ * the label you want displayed or read by screen readers.
+ */
+ translateById: PropTypes.func,
+};
- render() {
- const {
- 'aria-label': ariaLabel,
- 'aria-labelledby': ariaLabelledBy,
- children,
- className: customClassName,
- translateById: t,
- } = this.props;
- const { isExpanded, isFocused } = this.state;
- const accessibilityLabel = {
- 'aria-label': ariaLabel,
- 'aria-labelledby': ariaLabelledBy,
- };
- const assistiveText =
- isExpanded || isFocused
- ? t('carbon.sidenav.state.open')
- : t('carbon.sidenav.state.closed');
- const className = cx({
- [`${prefix}--side-nav`]: true,
- [`${prefix}--side-nav--expanded`]: isExpanded || isFocused,
- [customClassName]: !!customClassName,
- });
-
- return (
-
- );
- }
-}
+export default SideNav;
diff --git a/src/components/UIShell/SideNavFooter.js b/src/components/UIShell/SideNavFooter.js
index 15382333d0eb..5162d003a7d4 100644
--- a/src/components/UIShell/SideNavFooter.js
+++ b/src/components/UIShell/SideNavFooter.js
@@ -22,7 +22,7 @@ const { prefix } = settings;
const SideNavFooter = ({
assistiveText,
className: customClassName,
- isExpanded,
+ expanded,
onToggle,
}) => {
const className = cx(`${prefix}--side-nav__footer`, customClassName);
@@ -31,10 +31,10 @@ const SideNavFooter = ({
@@ -52,7 +52,7 @@ SideNavFooter.propTypes = {
/**
* Specify whether the side navigation is expanded or collapsed
*/
- isExpanded: PropTypes.bool.isRequired,
+ expanded: PropTypes.bool.isRequired,
/**
* Provide a function that is called when the toggle button is interacted
diff --git a/src/components/UIShell/__tests__/SideNav-test.js b/src/components/UIShell/__tests__/SideNav-test.js
index f401d5ad0615..95b232b3704c 100644
--- a/src/components/UIShell/__tests__/SideNav-test.js
+++ b/src/components/UIShell/__tests__/SideNav-test.js
@@ -23,37 +23,4 @@ describe('SideNav', () => {
const wrapper = mount();
expect(wrapper).toMatchSnapshot();
});
-
- it('should toggle the menu expansion state when clicking on the footer', () => {
- const wrapper = mount();
- expect(wrapper.state('isExpanded')).toBe(false);
- wrapper.find('button').simulate('click');
- expect(wrapper.state('isExpanded')).toBe(true);
- expect(wrapper).toMatchSnapshot();
- });
-
- it('should be expanded by default', () => {
- const wrapper = mount();
- expect(wrapper.state('isExpanded')).toBe(true);
- });
-
- it('should be collapsed by default', () => {
- const wrapper = mount();
- expect(wrapper.state('isExpanded')).toBe(false);
- });
-
- it('Blur event should trigger a state update of isFocused', () => {
- const wrapper = mount();
- wrapper.simulate('focus');
- expect(wrapper.state('isFocused')).toBe(true);
- wrapper.simulate('blur');
- expect(wrapper.state('isFocused')).toBe(false);
- });
-
- it('Focus event should trigger a state update of isFocused', () => {
- const wrapper = mount();
- expect(wrapper.state('isFocused')).toBe(false);
- wrapper.simulate('focus');
- expect(wrapper.state('isFocused')).toBe(true);
- });
});
diff --git a/src/components/UIShell/__tests__/__snapshots__/SideNav-test.js.snap b/src/components/UIShell/__tests__/__snapshots__/SideNav-test.js.snap
index b0f94520b837..28769425c971 100644
--- a/src/components/UIShell/__tests__/__snapshots__/SideNav-test.js.snap
+++ b/src/components/UIShell/__tests__/__snapshots__/SideNav-test.js.snap
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SideNav should render 1`] = `
-
-
-`;
-
-exports[`SideNav should toggle the menu expansion state when clicking on the footer 1`] = `
-
-
-
+
`;
diff --git a/src/components/UIShell/__tests__/__snapshots__/SideNavFooter-test.js.snap b/src/components/UIShell/__tests__/__snapshots__/SideNavFooter-test.js.snap
index 945406558537..39f35b1d8c6b 100644
--- a/src/components/UIShell/__tests__/__snapshots__/SideNavFooter-test.js.snap
+++ b/src/components/UIShell/__tests__/__snapshots__/SideNavFooter-test.js.snap
@@ -11,7 +11,7 @@ exports[`SideNavFooter should render 1`] = `
>