Skip to content

Commit

Permalink
feat(react): refactor Modal and ModalFooter to functional components (#…
Browse files Browse the repository at this point in the history
…10046)

* chore: check in progress

* chore: check in progress

* chore: check in progress

* feat(react): update modal and modal footer to functional component

* fix(react): add forwardRef to Modal and ModalFooter

* Update packages/react/src/components/Modal/next/Modal.js

Co-authored-by: Josh Black <[email protected]>

* Update packages/react/src/components/Modal/next/Modal.js

Co-authored-by: Josh Black <[email protected]>

* fix(react): pull out secondarybuttonset into local component

* fix(react): update snapshot

* fix(react): update Modal with various style conventions

* fix(react): remove knobs from v11 story and change other to rest

Co-authored-by: Josh Black <[email protected]>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 15, 2021
1 parent 5b4efa9 commit 8c0b660
Show file tree
Hide file tree
Showing 11 changed files with 1,571 additions and 24 deletions.
10 changes: 7 additions & 3 deletions packages/react/src/components/ComposedModal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,26 @@

import * as FeatureFlags from '@carbon/feature-flags';
import { ModalHeader as ModalHeaderNext } from './next/ModalHeader';
import { ModalFooter as ModalFooterNext } from './next/ModalFooter';
import { default as ComposedModalNext } from './next/ComposedModal';
import {
default as ComposedModalClassic,
ModalHeader as ModalHeaderClassic,
ModalBody,
ModalFooter,
ModalFooter as ModalFooterClassic,
} from './ComposedModal';

export const ModalHeader = FeatureFlags.enabled('enable-v11-release')
? ModalHeaderNext
: ModalHeaderClassic;

export const ModalFooter = FeatureFlags.enabled('enable-v11-release')
? ModalFooterNext
: ModalFooterClassic;

export const ComposedModal = FeatureFlags.enabled('enable-v11-release')
? ComposedModalNext
: ComposedModalClassic;

export { ModalBody, ModalFooter };

export { ModalBody };
export default from './ComposedModal';
74 changes: 69 additions & 5 deletions packages/react/src/components/ComposedModal/next/ComposedModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,83 @@ import PropTypes from 'prop-types';
import { ModalHeader } from './ModalHeader';
import { ModalFooter } from '../ComposedModal';

import classNames from 'classnames';
import cx from 'classnames';

import toggleClass from '../../../tools/toggleClass';
import requiredIfGivenPropIsTruthy from '../../../prop-types/requiredIfGivenPropIsTruthy';

import wrapFocus from '../../../internal/wrapFocus';
import { usePrefix } from '../../../internal/usePrefix';

export function ModalBody({
className: customClassName,
children,
hasForm,
hasScrollingContent,
...rest
}) {
const prefix = usePrefix();
const contentClass = cx({
[`${prefix}--modal-content`]: true,
[`${prefix}--modal-content--with-form`]: hasForm,
[`${prefix}--modal-scroll-content`]: hasScrollingContent,
[customClassName]: customClassName,
});
const hasScrollingContentProps = hasScrollingContent
? {
tabIndex: 0,
role: 'region',
}
: {};
return (
<>
<div className={contentClass} {...hasScrollingContentProps} {...rest}>
{children}
</div>
{hasScrollingContent && (
<div className={`${prefix}--modal-content--overflow-indicator`} />
)}
</>
);
}

ModalBody.propTypes = {
/**
* Required props for the accessibility label of the header
*/
['aria-label']: requiredIfGivenPropIsTruthy(
'hasScrollingContent',
PropTypes.string
),

/**
* Specify the content to be placed in the ModalBody
*/
children: PropTypes.node,

/**
* Specify an optional className to be added to the Modal Body node
*/
className: PropTypes.string,

/**
* Provide whether the modal content has a form element.
* If `true` is used here, non-form child content should have `bx--modal-content__regular-content` class.
*/
hasForm: PropTypes.bool,

/**
* Specify whether the modal contains scrolling content
*/
hasScrollingContent: PropTypes.bool,
};

const ComposedModal = React.forwardRef(function ComposedModal(
{
['aria-labelledby']: ariaLabelledBy,
['aria-label']: ariaLabel,
children,
className,
className: customClassName,
containerClassName,
danger,
onClose,
Expand Down Expand Up @@ -88,14 +152,14 @@ const ComposedModal = React.forwardRef(function ComposedModal(
}
}

const modalClass = classNames({
const modalClass = cx({
[`${prefix}--modal`]: true,
'is-visible': isOpen,
[className]: className,
[customClassName]: customClassName,
[`${prefix}--modal--danger`]: danger,
});

const containerClass = classNames({
const containerClass = cx({
[`${prefix}--modal-container`]: true,
[`${prefix}--modal-container--${size}`]: size,
[containerClassName]: containerClassName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import {
text,
withKnobs,
} from '@storybook/addon-knobs';
import { ModalBody, ModalFooter } from '../ComposedModal';
import ComposedModal from './ComposedModal';
import ComposedModal, { ModalBody } from './ComposedModal';
import { ModalHeader } from './ModalHeader';
import { ModalFooter } from './ModalFooter';
import Select from '../../Select';
import SelectItem from '../../SelectItem';
import TextInput from '../../TextInput';
Expand Down Expand Up @@ -120,8 +120,8 @@ const props = {
false
),
...secondaryButtons(numberOfButtons),
onRequestClose: action('onRequestClose'),
onRequestSubmit: action('onRequestSubmit'),
onRequestClose: () => action('onRequestClose')(),
onRequestSubmit: () => action('onRequestSubmit')(),
};
},
};
Expand Down
124 changes: 124 additions & 0 deletions packages/react/src/components/ComposedModal/next/ModalFooter-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React from 'react';
import { shallow, mount } from 'enzyme';
import Button from '../../Button';
import { ModalFooter } from './ModalFooter';
import InlineLoading from '../../InlineLoading';
import { settings } from 'carbon-components';

const { prefix } = settings;

describe('<ModalFooter />', () => {
describe('Renders as expected', () => {
const wrapper = mount(
<ModalFooter className="extra-class">
<p>Test</p>
</ModalFooter>
);

it('renders children as expected', () => {
expect(wrapper.find('p').length).toBe(1);
});

it('renders wrapper as expected', () => {
expect(wrapper.length).toBe(1);
});

it('renders extra classes passed in via className', () => {
expect(wrapper.hasClass('extra-class')).toEqual(true);
});
});

describe('Should render buttons only if appropriate prop passed in', () => {
const wrapper = shallow(
<ModalFooter className="extra-class">
<p>Test</p>
</ModalFooter>
);

const primaryWrapper = shallow(<ModalFooter primaryButtonText="test" />);
const secondaryWrapper = mount(<ModalFooter secondaryButtonText="test" />);
const multipleSecondaryWrapper = mount(
<ModalFooter
secondaryButtons={[
{
buttonText: <InlineLoading />,
onClick: jest.fn(),
},
{
buttonText: 'Cancel',
onClick: jest.fn(),
},
]}
/>
);

it('does not render primary button if no primary text', () => {
expect(wrapper.find(`.${prefix}--btn--primary`).exists()).toBe(false);
});

it('does not render secondary button if no secondary text', () => {
expect(wrapper.find(`.${prefix}--btn--secondary`).exists()).toBe(false);
});

it('renders primary button if primary text', () => {
const buttonComponent = primaryWrapper.find(Button);
expect(buttonComponent.exists()).toBe(true);
expect(buttonComponent.props().kind).toBe('primary');
});

it('renders primary button if secondary text', () => {
const buttonComponent = secondaryWrapper.find(Button);
expect(buttonComponent.exists()).toBe(true);
expect(buttonComponent.props().kind).toBe('secondary');
});

it('correctly renders multiple secondary buttons', () => {
const buttonComponents = multipleSecondaryWrapper.find(Button);
expect(buttonComponents.length).toEqual(2);
expect(buttonComponents.at(0).props().kind).toBe('secondary');
expect(buttonComponents.at(1).props().kind).toBe('secondary');
});
});

describe('Should render the appropriate buttons when `danger` prop is true', () => {
const primaryWrapper = shallow(
<ModalFooter primaryButtonText="test" danger />
);
const secondaryWrapper = mount(
<ModalFooter secondaryButtonText="test" danger />
);
const multipleSecondaryWrapper = mount(
<ModalFooter
secondaryButtons={[
{
buttonText: <InlineLoading />,
onClick: jest.fn(),
},
{
buttonText: 'Cancel',
onClick: jest.fn(),
},
]}
/>
);

it('renders danger button if primary text && danger', () => {
const buttonComponent = primaryWrapper.find(Button);
expect(buttonComponent.exists()).toBe(true);
expect(buttonComponent.props().kind).toBe('danger');
});

it('renders secondary button if secondary text && danger', () => {
const buttonComponent = secondaryWrapper.find(Button);
expect(buttonComponent.exists()).toBe(true);
expect(buttonComponent.prop('kind')).toBe('secondary');
});

it('correctly renders multiple secondary buttons', () => {
const buttonComponents = multipleSecondaryWrapper.find(Button);
expect(buttonComponents.length).toEqual(2);
expect(buttonComponents.at(0).props().kind).toBe('secondary');
expect(buttonComponents.at(1).props().kind).toBe('secondary');
});
});
});
Loading

0 comments on commit 8c0b660

Please sign in to comment.