From b5023ee5f55bd854e77eaa2f974f2c8dc1eafc5b Mon Sep 17 00:00:00 2001 From: Scott Dickerson <6663002+scottdickerson@users.noreply.github.com> Date: Sun, 17 Mar 2019 07:19:08 -0500 Subject: [PATCH] feat(ProgressIndicator): add optional interactivity (#1945) * fix(DatePicker): protect flatpickr in shallow render tests * fix(DatePicker): add comment to describe the Null check * fix(DatePicker): protect callbacks against this.cal NPE * test(DatePicker): add testcase to cover shallow behavior * feat(progressindicator): add interactivity if onChange prop is passed * fix(progressindicator): few small style fixes to the focus outline * fix(progress): support experimental mode * fix(progress): typoed the key to check, fix proptypes errors * fix(progress): current step should not be interactive * fix(progress): added reference to the new styles and additional tests * chore(package): update peer dep to latest carbon * fix(progressindicator): remove unnecessary index check --- package.json | 2 +- .../ProgressIndicator-story.js | 46 ++++++++++++- .../ProgressIndicator-test.js | 20 ++++++ .../ProgressIndicator/ProgressIndicator.js | 67 +++++++++++++++---- 4 files changed, 121 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 9f42dbd20a..9308a06b4d 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ }, "peerDependencies": { "carbon-components": "^9.84.3", - "carbon-icons": "^7.0.5", + "carbon-icons": "^7.0.7", "react": "^16.4.0", "react-dom": "^16.4.0" }, diff --git a/src/components/ProgressIndicator/ProgressIndicator-story.js b/src/components/ProgressIndicator/ProgressIndicator-story.js index 7dd9af744c..06118913cd 100644 --- a/src/components/ProgressIndicator/ProgressIndicator-story.js +++ b/src/components/ProgressIndicator/ProgressIndicator-story.js @@ -8,6 +8,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { withKnobs, number } from '@storybook/addon-knobs'; +import { action } from '@storybook/addon-actions'; import { ProgressIndicator, ProgressStep } from '../ProgressIndicator'; import ProgressIndicatorSkeleton from '../ProgressIndicator/ProgressIndicator.Skeleton'; import Tooltip from '../Tooltip'; @@ -74,13 +75,56 @@ storiesOf('Progress Indicator', module) { info: { text: ` - For React usage, ProgressIndicator holds the currentIndex state to indicate which ProgerssStep is the current step. The ProgressIndicator component should always be used with ProgressStep components as its children. Changing currentIndex prop will automatically set the ProgressStep components props (complete, incomplete, current). + For React usage, ProgressIndicator holds the currentIndex state to indicate which ProgressStep is the current step. The ProgressIndicator component should always be used with ProgressStep components as its children. Changing currentIndex prop will automatically set the ProgressStep components props (complete, incomplete, current). For general usage, Progress Indicators display steps in a process. It should indicate when steps have been complete, the active step, and the steps to come. `, }, } ) + .add( + 'interactive', + () => ( + + + + ( + +

+ Lorem ipsum dolor, sit amet consectetur adipisicing elit. Animi + consequuntur hic ratione aliquid cupiditate, nesciunt saepe iste + blanditiis cumque maxime tenetur veniam est illo deserunt sint + quae pariatur. Laboriosam, consequatur. +

+
+ )} + /> +
+ ), + { + info: { + text: ` + If you register an onChange handler, the Progress Indicator will become interactive. Your parent component should update the currentIndex prop within the onChange handler. + `, + }, + } + ) .add('skeleton', () => , { info: { text: ` diff --git a/src/components/ProgressIndicator/ProgressIndicator-test.js b/src/components/ProgressIndicator/ProgressIndicator-test.js index e8b8daba58..e2208f7df6 100644 --- a/src/components/ProgressIndicator/ProgressIndicator-test.js +++ b/src/components/ProgressIndicator/ProgressIndicator-test.js @@ -78,6 +78,18 @@ describe('ProgressIndicator', () => { expect(list.state().currentIndex).toEqual(2); }); + it('should trigger onChange if clicked', () => { + const mockOnChange = jest.fn(); + + mountedList.setProps({ onChange: mockOnChange }); + mountedList + .find(ProgressStep) + .at(0) + .find('[role="button"]') + .simulate('click'); + expect(mockOnChange).toHaveBeenCalledWith(0); + }); + describe('ProgressStep', () => { it('should render with correct base className', () => { expect( @@ -175,6 +187,14 @@ describe('ProgressIndicator', () => { .prop('complete') ).toBe(false); }); + + it('should render any clickable ProgressSteps with correct classname', () => { + mountedList.setProps({ onChange: jest.fn() }); + expect(mountedList.find('.bx--progress-step-button')).toHaveLength(6); // one button for each div + expect( + mountedList.find('.bx--progress-step-button--unclickable') + ).toHaveLength(1); // only the current step should be unclickable + }); }); }); }); diff --git a/src/components/ProgressIndicator/ProgressIndicator.js b/src/components/ProgressIndicator/ProgressIndicator.js index fe7981b378..a0eee84aba 100644 --- a/src/components/ProgressIndicator/ProgressIndicator.js +++ b/src/components/ProgressIndicator/ProgressIndicator.js @@ -12,6 +12,7 @@ import { settings } from 'carbon-components'; import CheckmarkOutline16 from '@carbon/icons-react/lib/checkmark--outline/16'; import Warning16 from '@carbon/icons-react/lib/warning/16'; import { componentsX } from '../../internal/FeatureFlags'; +import { keys, matches } from '../../tools/key'; const { prefix } = settings; const defaultRenderLabel = props =>

; @@ -25,6 +26,7 @@ export const ProgressStep = ({ ...props }) => { invalid, secondaryLabel, disabled, + onClick, renderLabel: ProgressStepLabel, } = props; @@ -37,6 +39,12 @@ export const ProgressStep = ({ ...props }) => { [className]: className, }); + const handleKeyDown = e => { + if (matches(e, [keys.ENTER, keys.SPACE])) { + onClick(); + } + }; + const currentSvg = current && (componentsX ? ( @@ -88,21 +96,35 @@ export const ProgressStep = ({ ...props }) => { return (

  • - {currentSvg || completeSvg || incompleteSvg} - - {label} - - {componentsX && - secondaryLabel !== null && - secondaryLabel !== undefined ? ( -

    {secondaryLabel}

    - ) : null} - +
    + {currentSvg || completeSvg || incompleteSvg} + + {label} + + {componentsX && + secondaryLabel !== null && + secondaryLabel !== undefined ? ( +

    {secondaryLabel}

    + ) : null} + +
  • ); }; ProgressStep.propTypes = { + /** + * Index of the current step within the ProgressIndicator + */ + index: PropTypes.number, + /** * Provide the label for the */ @@ -158,6 +180,11 @@ ProgressStep.propTypes = { * The ID of the tooltip content. */ tooltipId: PropTypes.string, + + /** + * A callback called if the step is clicked or the enter key is pressed + */ + onClick: PropTypes.func, }; ProgressStep.defaultProps = { @@ -183,6 +210,11 @@ export class ProgressIndicator extends Component { * Optionally specify the current step array index */ currentIndex: PropTypes.number, + + /** + * Optional callback called if a ProgressStep is clicked on. Returns the index of the step. + */ + onChange: PropTypes.func, }; static defaultProps = { @@ -199,25 +231,36 @@ export class ProgressIndicator extends Component { }; } - renderSteps = () => - React.Children.map(this.props.children, (child, index) => { + renderSteps = () => { + const { onChange } = this.props; + + return React.Children.map(this.props.children, (child, index) => { + // only setup click handlers if onChange event is passed + const onClick = onChange ? () => onChange(index) : undefined; if (index === this.state.currentIndex) { return React.cloneElement(child, { current: true, + index, + onClick, }); } if (index < this.state.currentIndex) { return React.cloneElement(child, { complete: true, + index, + onClick, }); } if (index > this.state.currentIndex) { return React.cloneElement(child, { complete: false, + index, + onClick, }); } return null; }); + }; render() { const { className, currentIndex, ...other } = this.props; // eslint-disable-line no-unused-vars