From d48e5faba248bb0e573d45ab6c68b00a4b98613d Mon Sep 17 00:00:00 2001 From: Josh Black Date: Mon, 7 Oct 2019 16:19:50 -0500 Subject: [PATCH] Revert "refactor(react): update ContentSwitcher and Switch to hooks (#3302)" (#4250) This reverts commit 954ac48c9a0f657ced44a0b23e2a2b623ffaff2f. --- .../ContentSwitcher/ContentSwitcher-story.js | 18 +- .../ContentSwitcher/ContentSwitcher-test.js | 167 +++++++++++++ .../ContentSwitcher/ContentSwitcher.js | 230 +++++++++--------- .../src/components/ContentSwitcher/Switch.js | 96 -------- .../__tests__/ContentSwitcher-test.js | 187 -------------- .../ContentSwitcher/__tests__/Switch-test.js | 75 ------ .../ContentSwitcher-test.js.snap | 86 ------- .../__snapshots__/Switch-test.js.snap | 55 ----- .../src/components/ContentSwitcher/index.js | 1 - .../src/components/Switch/Switch-test.js | 98 ++++++++ .../react/src/components/Switch/Switch.js | 103 +++++++- packages/react/src/index.js | 3 +- 12 files changed, 487 insertions(+), 632 deletions(-) create mode 100644 packages/react/src/components/ContentSwitcher/ContentSwitcher-test.js delete mode 100644 packages/react/src/components/ContentSwitcher/Switch.js delete mode 100644 packages/react/src/components/ContentSwitcher/__tests__/ContentSwitcher-test.js delete mode 100644 packages/react/src/components/ContentSwitcher/__tests__/Switch-test.js delete mode 100644 packages/react/src/components/ContentSwitcher/__tests__/__snapshots__/ContentSwitcher-test.js.snap delete mode 100644 packages/react/src/components/ContentSwitcher/__tests__/__snapshots__/Switch-test.js.snap create mode 100644 packages/react/src/components/Switch/Switch-test.js diff --git a/packages/react/src/components/ContentSwitcher/ContentSwitcher-story.js b/packages/react/src/components/ContentSwitcher/ContentSwitcher-story.js index 9263ff1b9463..c2c54d2226bf 100644 --- a/packages/react/src/components/ContentSwitcher/ContentSwitcher-story.js +++ b/packages/react/src/components/ContentSwitcher/ContentSwitcher-story.js @@ -8,7 +8,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; -import { withKnobs, boolean, number } from '@storybook/addon-knobs'; +import { withKnobs, boolean } from '@storybook/addon-knobs'; import ContentSwitcher from '../ContentSwitcher'; import Switch from '../Switch'; @@ -29,12 +29,10 @@ storiesOf('ContentSwitcher', module) () => { const switchProps = props.switch(); return ( - - - - + + + + ); }, @@ -53,9 +51,9 @@ storiesOf('ContentSwitcher', module) const switchProps = props.switch(); return ( - - - + + + ); }, diff --git a/packages/react/src/components/ContentSwitcher/ContentSwitcher-test.js b/packages/react/src/components/ContentSwitcher/ContentSwitcher-test.js new file mode 100644 index 000000000000..a9ffda23398f --- /dev/null +++ b/packages/react/src/components/ContentSwitcher/ContentSwitcher-test.js @@ -0,0 +1,167 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import ContentSwitcher from '../ContentSwitcher'; +import Switch from '../Switch'; +import { mount, shallow } from 'enzyme'; +import { settings } from 'carbon-components'; + +const { prefix } = settings; + +describe('ContentSwitcher', () => { + describe('component initial rendering', () => { + const wrapper = shallow( + {}} className="extra-class"> + + + + ); + + const children = wrapper.find(Switch); + + it('should have the correct class', () => { + expect(wrapper.hasClass(`${prefix}--content-switcher`)).toEqual(true); + }); + + it('should render children as expected', () => { + expect(children.length).toEqual(2); + }); + + it('should default "selected" property to true on first child', () => { + expect(children.first().props().selected).toEqual(true); + expect(children.last().props().selected).toEqual(false); + }); + + it('should apply extra classes passed to it', () => { + expect(wrapper.hasClass('extra-class')).toEqual(true); + }); + }); + + describe('Allow initial state to draw from props', () => { + const onChange = jest.fn(); + const mockData = { + index: 0, + }; + + const wrapper = mount( + + + + + ); + + const children = wrapper.find(Switch); + + it('Should apply the selected property on the selected child', () => { + expect(children.first().props().selected).toEqual(false); + expect(children.last().props().selected).toEqual(true); + }); + + it('should avoid change the selected index upon setting props, unless there the value actually changes', () => { + wrapper.setProps({ selectedIndex: 1 }); + // Turns `state.selectedIndex` to `0` + children + .first() + .props() + .onClick(mockData); + wrapper.setProps({ selectedIndex: 1 }); // No change in `selectedIndex` prop + const clonedChildren = wrapper.find(Switch); + expect(clonedChildren.first().props().selected).toEqual(true); + expect(clonedChildren.last().props().selected).toEqual(false); + }); + + it('should change the selected index upon change in props', () => { + wrapper.setProps({ selectedIndex: 0 }); + children + .first() + .props() + .onClick(mockData); + wrapper.setProps({ selectedIndex: 1 }); + const clonedChildren = wrapper.find(Switch); + expect(clonedChildren.first().props().selected).toEqual(false); + expect(clonedChildren.last().props().selected).toEqual(true); + }); + }); + + describe('when child component onClick is invoked', () => { + const onChange = jest.fn(); + const mockData = { + index: 1, + }; + + const wrapper = mount( + + + + + ); + + const children = wrapper.find(Switch); + + children + .first() + .props() + .onClick(mockData); + + it('should invoke onChange', () => { + expect(onChange).toBeCalledWith(mockData); + }); + + it('should set the correct selectedIndex', () => { + expect(wrapper.state('selectedIndex')).toEqual(mockData.index); + }); + + it('should set selected to true on the correct child', () => { + wrapper.update(); + const firstChild = wrapper.find(Switch).first(); + const secondChild = wrapper.find(Switch).last(); + expect(firstChild.props().selected).toEqual(false); + expect(secondChild.props().selected).toEqual(true); + }); + }); + + describe('when child component onKeyDown is invoked', () => { + const onChange = jest.fn(); + const mockData = { + index: 1, + }; + + const wrapper = mount( + + + + + ); + + const children = wrapper.find(Switch); + + children + .first() + .props() + .onKeyDown(mockData); + + it('should invoke onChange', () => { + expect(onChange).toBeCalledWith(mockData); + }); + + it('should set the correct selectedIndex', () => { + expect(wrapper.state('selectedIndex')).toEqual(mockData.index); + }); + + it('should set selected to true on the correct child', () => { + wrapper.update(); + const firstChild = wrapper.find(Switch).first(); + const secondChild = wrapper.find(Switch).last(); + expect(firstChild.props().selected).toEqual(false); + expect(secondChild.props().selected).toEqual(true); + }); + }); +}); diff --git a/packages/react/src/components/ContentSwitcher/ContentSwitcher.js b/packages/react/src/components/ContentSwitcher/ContentSwitcher.js index 8b43fee77897..bb27fbab35ef 100644 --- a/packages/react/src/components/ContentSwitcher/ContentSwitcher.js +++ b/packages/react/src/components/ContentSwitcher/ContentSwitcher.js @@ -5,135 +5,125 @@ * LICENSE file in the root directory of this source tree. */ -import { settings } from 'carbon-components'; -import cx from 'classnames'; import PropTypes from 'prop-types'; -import React, { - Children, - cloneElement, - useEffect, - useRef, - useState, -} from 'react'; +import React from 'react'; +import classNames from 'classnames'; +import { settings } from 'carbon-components'; import { composeEventHandlers } from '../../tools/events'; import { getNextIndex, matches, keys } from '../../internal/keyboard'; const { prefix } = settings; -function ContentSwitcher({ - children, - className: customClassName, - onChange, - selectedIndex: controlledSelectedIndex = 0, - ...rest -}) { - const switchRefs = []; - const className = cx(`${prefix}--content-switcher`, customClassName); - const savedOnChange = useRef(null); - const [selectedIndex, setSelectedIndex] = useState(controlledSelectedIndex); - const [prevControlledIndex, setPrevControlledIndex] = useState( - controlledSelectedIndex - ); - const [shouldFocus, setShouldFocus] = useState(false); - - if (controlledSelectedIndex !== prevControlledIndex) { - setSelectedIndex(controlledSelectedIndex); - setPrevControlledIndex(controlledSelectedIndex); - setShouldFocus(false); - } - - // Always keep track of the latest `onChange` prop to use in our focus effect - // handler - useEffect(() => { - savedOnChange.current = onChange; - }, [onChange]); - - // If our selectedIndex has changed from an event handler, meaning that - // `shouldFocus` is set to true, then call the saved `onChange` handler if it - // exists - useEffect(() => { - if (shouldFocus && savedOnChange.current) { - savedOnChange.current(selectedIndex); - } - }, [shouldFocus, selectedIndex]); - - // We have a couple of scenarios we want to keep track of when managing focus: - // 1) Don't focus the ref at the selectedIndex if its the first render, focus - // should only come from a user action - // 2) Don't focus if selectedIndex has changed because of a change in props - // 3) Trigger focus if triggered by click or key down. Both of these handlers - // should set `shouldFocus` to true - useEffect(() => { - if (!shouldFocus) { - return; - } - - const ref = switchRefs[selectedIndex]; - if (ref && document.activeElement !== ref) { - ref.focus && ref.focus(); - } - }, [shouldFocus, switchRefs, selectedIndex]); - - function handleItemRef(index) { - return ref => { - switchRefs[index] = ref; - }; - } - - function onClick(event, index) { - if (selectedIndex !== index) { - setSelectedIndex(index); - if (shouldFocus !== true) { - setShouldFocus(true); - } - } +export default class ContentSwitcher extends React.Component { + /** + * The DOM references of child ``. + * @type {Array} + * @private + */ + _switchRefs = []; + + state = {}; + + static propTypes = { + /** + * Pass in Switch components to be rendered in the ContentSwitcher + */ + children: PropTypes.node, + + /** + * Specify an optional className to be added to the container node + */ + className: PropTypes.string, + + /** + * Specify an `onChange` handler that is called whenever the ContentSwitcher + * changes which item is selected + */ + onChange: PropTypes.func.isRequired, + + /** + * Specify a selected index for the initially selected content + */ + selectedIndex: PropTypes.number, + }; + + static defaultProps = { + selectedIndex: 0, + }; + + static getDerivedStateFromProps({ selectedIndex }, state) { + const { prevSelectedIndex } = state; + return prevSelectedIndex === selectedIndex + ? null + : { + selectedIndex, + prevSelectedIndex: selectedIndex, + }; } - function onKeyDown(event) { - if (matches(event, [keys.ArrowRight, keys.ArrowLeft])) { - setSelectedIndex(getNextIndex(event, selectedIndex, children.length)); - if (shouldFocus !== true) { - setShouldFocus(true); + handleItemRef = index => ref => { + this._switchRefs[index] = ref; + }; + + handleChildChange = data => { + // the currently selected child index + const { selectedIndex } = this.state; + // the newly selected child index + const { index } = data; + const { key } = data; + + if (matches(data, [keys.ArrowRight, keys.ArrowLeft])) { + const nextIndex = getNextIndex( + key, + selectedIndex, + this.props.children.length + ); + this.setState( + { + selectedIndex: nextIndex, + }, + () => { + const switchRef = this._switchRefs[nextIndex]; + switchRef && switchRef.focus(); + this.props.onChange(data); + } + ); + } else { + if (selectedIndex !== index) { + this.setState({ selectedIndex: index }, () => { + const switchRef = this._switchRefs[index]; + switchRef && switchRef.focus(); + this.props.onChange(data); + }); } } + }; + + render() { + const { + children, + className, + selectedIndex, // eslint-disable-line no-unused-vars + ...other + } = this.props; + + const classes = classNames(`${prefix}--content-switcher`, className); + + return ( +
+ {React.Children.map(children, (child, index) => + React.cloneElement(child, { + index, + onClick: composeEventHandlers([ + this.handleChildChange, + child.props.onClick, + ]), + onKeyDown: this.handleChildChange, + selected: index === this.state.selectedIndex, + ref: this.handleItemRef(index), + }) + )} +
+ ); } - - return ( -
- {Children.map(children, (child, index) => - cloneElement(child, { - index, - onClick: composeEventHandlers([onClick, child.props.onClick]), - onKeyDown: composeEventHandlers([onKeyDown, child.props.onKeyDown]), - selected: index === selectedIndex, - ref: handleItemRef(index), - }) - )} -
- ); } - -ContentSwitcher.propTypes = { - /** - * Pass in Switch components to be rendered in the ContentSwitcher - */ - children: PropTypes.node, - - /** - * Specify an optional className to be added to the container node - */ - className: PropTypes.string, - - /** - * Specify an `onChange` handler that is called whenever the ContentSwitcher - * changes which item is selected - */ - onChange: PropTypes.func.isRequired, - - /** - * Specify a selected index for the initially selected content - */ - selectedIndex: PropTypes.number, -}; - -export default ContentSwitcher; diff --git a/packages/react/src/components/ContentSwitcher/Switch.js b/packages/react/src/components/ContentSwitcher/Switch.js deleted file mode 100644 index c2ca09737c58..000000000000 --- a/packages/react/src/components/ContentSwitcher/Switch.js +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2018 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { settings } from 'carbon-components'; -import cx from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import deprecate from '../../prop-types/deprecate'; - -const { prefix } = settings; - -const Switch = React.forwardRef(function Switch( - { - className: customClassName, - index, - onClick, - // We no longer need the `name` prop as we instead use the `index` of the - // Switch. However, since we spread `...rest` onto the `button` node we need - // to handle it so folks don't see an error for an unrecognized DOM prop - // eslint-disable-next-line no-unused-vars - name, - selected = false, - text, - ...rest - }, - ref -) { - const className = cx({ - [`${prefix}--content-switcher-btn`]: true, - [`${prefix}--content-switcher--selected`]: selected, - [customClassName]: !!customClassName, - }); - - function handleOnClick(event) { - onClick(event, index); - } - - return ( - - ); -}); - -Switch.displayName = 'Switch'; -Switch.propTypes = { - /** - * Specify an optional className to be added to your Switch - */ - className: PropTypes.string, - - /** - * Provide the name of your Switch that is used for event handlers - */ - name: deprecate(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), - - /** - * The index of your Switch in your ContentSwitcher that is used for event handlers. - * Reserved for usage in ContentSwitcher - */ - index: PropTypes.number, - - /** - * A handler that is invoked when a user clicks on the control. - */ - onClick: PropTypes.func, - - /** - * A handler that is invoked on the key down event for the control. - */ - onKeyDown: PropTypes.func, - - /** - * Whether your Switch is selected. Reserved for usage in ContentSwitcher - */ - selected: PropTypes.bool, - - /** - * Provide the contents of your Switch - */ - text: PropTypes.string.isRequired, -}; - -export default Switch; diff --git a/packages/react/src/components/ContentSwitcher/__tests__/ContentSwitcher-test.js b/packages/react/src/components/ContentSwitcher/__tests__/ContentSwitcher-test.js deleted file mode 100644 index 5a6f6378f8aa..000000000000 --- a/packages/react/src/components/ContentSwitcher/__tests__/ContentSwitcher-test.js +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2018 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { mount } from 'enzyme'; -import React from 'react'; -import ContentSwitcher, { Switch } from '../../ContentSwitcher'; -import { ArrowLeft, ArrowRight } from '../../../internal/keyboard/keys'; - -describe('ContentSwitcher', () => { - let mockProps; - - beforeEach(() => { - mockProps = { - className: 'custom-class', - onChange: jest.fn(), - selectedIndex: 0, - }; - }); - - it('should render', () => { - const wrapper = mount( - - - - - - ); - expect(wrapper).toMatchSnapshot(); - }); - - it('should support a custom class name', () => { - const wrapper = mount( - - - - - - ); - expect(wrapper.children().find(`.${mockProps.className}`).length).toBe(1); - }); - - it('should match the selected tab with `selectedIndex`', () => { - const wrapper = mount( - - - - - - ); - expect(wrapper.find({ selected: true }).prop('index')).toBe( - mockProps.selectedIndex - ); - }); - - it('should update the selected index when a `Switch` is clicked', () => { - const children = 3; - const wrapper = mount( - - {Array.from({ length: children }).map((_, index) => ( - - ))} - - ); - - for (let i = children - 1; i >= 0; i--) { - wrapper - .find(Switch) - .at(i) - .simulate('click'); - expect(wrapper.find({ selected: true }).prop('index')).toBe(i); - } - }); - - it('should update the selected index when `selectedIndex` is updated', () => { - const children = 3; - const wrapper = mount( - - - - - - ); - - expect(wrapper.find({ selected: true }).prop('index')).toBe(0); - - for (let i = children - 1; i >= 0; i--) { - wrapper.setProps({ selectedIndex: i }); - expect(wrapper.find({ selected: true }).prop('index')).toBe(i); - } - }); - - it('should not update focus if `selectedIndex` is updated', () => { - const wrapper = mount( - - - - - - ); - - const secondSwitchTrigger = wrapper - .find(Switch) - .at(1) - .find('button'); - secondSwitchTrigger.simulate('click'); - - expect(document.activeElement === secondSwitchTrigger.getDOMNode()).toBe( - true - ); - - wrapper.setProps({ selectedIndex: 1 }); - - expect(document.activeElement === secondSwitchTrigger.getDOMNode()).toBe( - true - ); - }); - - it('should call `onChange` when the selected index changes', () => { - const children = 3; - const wrapper = mount( - - - - - - ); - - // Simulate click to change selected index - for (let i = children - 1; i >= 0; i--) { - wrapper - .find(Switch) - .at(i) - .find('button') - .simulate('click'); - expect(mockProps.onChange).toHaveBeenLastCalledWith(i); - } - }); - - it('should change the selected index when ArrowLeft and ArrowRight are used', () => { - const children = 3; - const wrapper = mount( - - {Array.from({ length: children }).map((_, index) => ( - - ))} - - ); - - wrapper - .find(Switch) - .at(0) - .find('button') - .simulate('focus'); - - for (let i = 0; i < children; i++) { - wrapper - .find(Switch) - .at(i) - .find('button') - .simulate('keydown', ArrowRight); - expect(wrapper.find({ selected: true }).prop('index')).toBe( - // The index will wrap to the beginning of the list so we'll use a ring - // buffer to check the selected index - (i + 1) % children - ); - } - - wrapper.setProps({ selectedIndex: 2 }); - - for (let i = children - 1; i >= 0; i--) { - wrapper - .find(Switch) - .at(i) - .find('button') - .simulate('keydown', ArrowLeft); - expect(wrapper.find({ selected: true }).prop('index')).toBe( - // The index will wrap to the end of the list so we'll use a ring - // buffer to check the selected index - (i + children - 1) % children - ); - } - }); -}); diff --git a/packages/react/src/components/ContentSwitcher/__tests__/Switch-test.js b/packages/react/src/components/ContentSwitcher/__tests__/Switch-test.js deleted file mode 100644 index 20fcced64578..000000000000 --- a/packages/react/src/components/ContentSwitcher/__tests__/Switch-test.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright IBM Corp. 2016, 2018 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -import { mount } from 'enzyme'; -import React from 'react'; -import { Switch } from '../../ContentSwitcher'; - -describe('Switch', () => { - let mockProps; - - beforeEach(() => { - mockProps = { - className: 'custom-class', - index: 0, - onClick: jest.fn(), - onKeyDown: jest.fn(), - ref: jest.fn(), - selected: false, - text: 'mock-text', - }; - }); - - it('should render', () => { - const wrapper = mount(); - expect(wrapper).toMatchSnapshot(); - - const selected = mount(); - expect(selected).toMatchSnapshot(); - }); - - it('should support a custom class name', () => { - const wrapper = mount(); - expect(wrapper.children().find(`.${mockProps.className}`).length).toBe(1); - }); - - it('should call `onClick` with the native event and given index', () => { - const wrapper = mount(); - wrapper.find('button').simulate('click'); - - expect(mockProps.onClick).toHaveBeenCalledTimes(1); - expect(mockProps.onClick).toHaveBeenCalledWith( - expect.objectContaining({ - target: expect.any(HTMLElement), - }), - mockProps.index - ); - }); - - it('should set `tabIndex` to 0 or -1 depending on if it is selected', () => { - const wrapper = mount(); - expect(wrapper.find('button').prop('tabIndex')).toBe('-1'); - wrapper.setProps({ selected: true }); - expect(wrapper.find('button').prop('tabIndex')).toBe('0'); - }); - - it('should set `aria-selected` depending on if it is selected', () => { - const wrapper = mount(); - expect(wrapper.find('button').prop('aria-selected')).toBe( - mockProps.selected - ); - wrapper.setProps({ selected: true }); - expect(wrapper.find('button').prop('aria-selected')).toBe(true); - }); - - it('should set the forwarded ref to the button element', () => { - mount(); - // It seems like enzyme does not call the `ref` with the underlying - // HTMLElement, so we are asserting that it's being called as a rough test - expect(mockProps.ref).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/react/src/components/ContentSwitcher/__tests__/__snapshots__/ContentSwitcher-test.js.snap b/packages/react/src/components/ContentSwitcher/__tests__/__snapshots__/ContentSwitcher-test.js.snap deleted file mode 100644 index c888329da1ac..000000000000 --- a/packages/react/src/components/ContentSwitcher/__tests__/__snapshots__/ContentSwitcher-test.js.snap +++ /dev/null @@ -1,86 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ContentSwitcher should render 1`] = ` - -
- - - - - - - - - -
-
-`; diff --git a/packages/react/src/components/ContentSwitcher/__tests__/__snapshots__/Switch-test.js.snap b/packages/react/src/components/ContentSwitcher/__tests__/__snapshots__/Switch-test.js.snap deleted file mode 100644 index 305b09fc3966..000000000000 --- a/packages/react/src/components/ContentSwitcher/__tests__/__snapshots__/Switch-test.js.snap +++ /dev/null @@ -1,55 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Switch should render 1`] = ` - - - -`; - -exports[`Switch should render 2`] = ` - - - -`; diff --git a/packages/react/src/components/ContentSwitcher/index.js b/packages/react/src/components/ContentSwitcher/index.js index 9d6d768e47d6..ad71bdd957ed 100644 --- a/packages/react/src/components/ContentSwitcher/index.js +++ b/packages/react/src/components/ContentSwitcher/index.js @@ -5,5 +5,4 @@ * LICENSE file in the root directory of this source tree. */ -export { default as Switch } from './Switch'; export default from './ContentSwitcher'; diff --git a/packages/react/src/components/Switch/Switch-test.js b/packages/react/src/components/Switch/Switch-test.js new file mode 100644 index 000000000000..876f2fdd1c77 --- /dev/null +++ b/packages/react/src/components/Switch/Switch-test.js @@ -0,0 +1,98 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import Switch from '../Switch'; +import { shallow } from 'enzyme'; +import { settings } from 'carbon-components'; + +const { prefix } = settings; + +describe('Switch', () => { + describe('component rendering', () => { + const buttonWrapper = shallow( + } text="test" /> + ); + + it('should render a button when kind is button', () => { + expect(buttonWrapper.is('button')).toEqual(true); + }); + + it('should have the expected text', () => { + expect(buttonWrapper.text()).toEqual('test'); + }); + + it('label should have the expected class', () => { + const className = `${prefix}--content-switcher__label`; + expect(buttonWrapper.find('span').hasClass(className)).toEqual(true); + }); + + it('should have the expected class', () => { + const cls = `${prefix}--content-switcher-btn`; + + expect(buttonWrapper.hasClass(cls)).toEqual(true); + }); + + it('should not have selected class', () => { + const selectedClass = `${prefix}--content-switcher--selected`; + + expect(buttonWrapper.hasClass(selectedClass)).toEqual(false); + }); + + it('should have a selected class when selected is set to true', () => { + const selected = true; + + buttonWrapper.setProps({ selected }); + + expect( + buttonWrapper.hasClass(`${prefix}--content-switcher--selected`) + ).toEqual(true); + }); + }); + + describe('events', () => { + const buttonOnClick = jest.fn(); + const linkOnClick = jest.fn(); + const buttonOnKey = jest.fn(); + const linkOnKey = jest.fn(); + const index = 1; + const name = 'first'; + const text = 'test'; + + const buttonWrapper = shallow( + + ); + + const linkWrapper = shallow( + + ); + + it('should invoke button onClick handler', () => { + buttonWrapper.simulate('click', { preventDefault() {} }); + expect(buttonOnClick).toBeCalledWith({ index, name, text }); + }); + + it('should invoke link onClick handler', () => { + linkWrapper.simulate('click', { preventDefault() {} }); + expect(buttonOnClick).toBeCalledWith({ index, name, text }); + }); + }); +}); diff --git a/packages/react/src/components/Switch/Switch.js b/packages/react/src/components/Switch/Switch.js index deb4ab67c5d3..57ce145a4653 100644 --- a/packages/react/src/components/Switch/Switch.js +++ b/packages/react/src/components/Switch/Switch.js @@ -5,5 +5,106 @@ * LICENSE file in the root directory of this source tree. */ -import { Switch } from '../ContentSwitcher'; +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import { settings } from 'carbon-components'; + +const { prefix } = settings; + +const Switch = React.forwardRef(function Switch(props, tabRef) { + const { + className, + index, + name, + onClick, + onKeyDown, + selected, + text, + ...other + } = props; + + const handleClick = e => { + e.preventDefault(); + onClick({ index, name, text }); + }; + + const handleKeyDown = event => { + const key = event.key || event.which; + + onKeyDown({ index, name, text, key }); + }; + + const classes = classNames(className, `${prefix}--content-switcher-btn`, { + [`${prefix}--content-switcher--selected`]: selected, + }); + + const commonProps = { + onClick: handleClick, + onKeyDown: handleKeyDown, + className: classes, + }; + + return ( + + ); +}); + +Switch.displayName = 'Switch'; + +Switch.propTypes = { + /** + * Specify an optional className to be added to your Switch + */ + className: PropTypes.string, + + /** + * The index of your Switch in your ContentSwitcher that is used for event handlers. + * Reserved for usage in ContentSwitcher + */ + index: PropTypes.number, + + /** + * Provide the name of your Switch that is used for event handlers + */ + name: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + + /** + * A handler that is invoked when a user clicks on the control. + * Reserved for usage in ContentSwitcher + */ + onClick: PropTypes.func, + + /** + * A handler that is invoked on the key down event for the control. + * Reserved for usage in ContentSwitcher + */ + onKeyDown: PropTypes.func, + + /** + * Whether your Switch is selected. Reserved for usage in ContentSwitcher + */ + selected: PropTypes.bool, + + /** + * Provide the contents of your Switch + */ + text: PropTypes.string.isRequired, +}; + +Switch.defaultProps = { + selected: false, + text: 'Provide text', + onClick: () => {}, + onKeyDown: () => {}, +}; + export default Switch; diff --git a/packages/react/src/index.js b/packages/react/src/index.js index 7d2324abbe03..cc2d274d0d74 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -17,7 +17,7 @@ export ComposedModal, { ModalBody, ModalFooter, } from './components/ComposedModal'; -export ContentSwitcher, { Switch } from './components/ContentSwitcher'; +export ContentSwitcher from './components/ContentSwitcher'; export Copy from './components/Copy'; export CopyButton from './components/CopyButton'; export DangerButton from './components/DangerButton'; @@ -91,6 +91,7 @@ export SecondaryButton from './components/SecondaryButton'; export Select from './components/Select'; export SelectItem from './components/SelectItem'; export SelectItemGroup from './components/SelectItemGroup'; +export Switch from './components/Switch'; export Slider from './components/Slider'; export { StructuredListWrapper,