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`] = ` >