Skip to content

Commit 647ec1e

Browse files
feat(Modal): implement support for 3 button modals (#7775)
* refactor(modal): unnest selectors and reduce specificity * feat(Modal): support 3 button modals * feat(ComposedModal): support 3 button modals * chore: update snapshots * fix(modal): align button text to start in 3 button modals * docs(Modal): update type * docs(Modal): update secondaryButtons definition * fix(Modal): allow nodes in secondary button text * test(Modal): add secondaryButtons test * test(ComposedModal): add secondaryButtons test Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 3f1f0ec commit 647ec1e

File tree

9 files changed

+338
-75
lines changed

9 files changed

+338
-75
lines changed

packages/components/src/components/modal/_modal.scss

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -302,21 +302,25 @@
302302

303303
.#{$prefix}--modal-footer {
304304
display: flex;
305-
306305
grid-row: -1/-1;
307306
grid-column: 1/-1;
308307
justify-content: flex-end;
309-
height: 4rem;
308+
height: rem(64px);
310309
margin-top: auto;
310+
}
311311

312-
button.#{$prefix}--btn {
313-
flex: 0 1 50%;
314-
max-width: none;
315-
height: 4rem;
316-
margin: 0;
317-
padding-top: $spacing-05;
318-
padding-bottom: $spacing-07;
319-
}
312+
.#{$prefix}--modal-footer .#{$prefix}--btn {
313+
flex: 0 1 50%;
314+
max-width: none;
315+
height: rem(64px);
316+
margin: 0;
317+
padding-top: $spacing-05;
318+
padding-bottom: $spacing-07;
319+
}
320+
321+
.#{$prefix}--modal-footer--three-button .#{$prefix}--btn {
322+
flex: 0 1 25%;
323+
align-items: flex-start;
320324
}
321325

322326
.#{$prefix}--modal-footer button.#{$prefix}--btn:focus {

packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,7 @@ Map {
811811
"secondaryButtonText": Object {
812812
"type": "string",
813813
},
814+
"secondaryButtons": [Function],
814815
"secondaryClassName": Object {
815816
"type": "string",
816817
},
@@ -3295,6 +3296,7 @@ Map {
32953296
"secondaryButtonText": Object {
32963297
"type": "node",
32973298
},
3299+
"secondaryButtons": [Function],
32983300
"selectorPrimaryFocus": Object {
32993301
"type": "string",
33003302
},

packages/react/src/components/ComposedModal/ComposedModal-story.js

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@
88
import React, { useState } from 'react';
99
import ReactDOM from 'react-dom';
1010
import { action } from '@storybook/addon-actions';
11-
import { withKnobs, boolean, select, text } from '@storybook/addon-knobs';
11+
import {
12+
boolean,
13+
object,
14+
optionsKnob as options,
15+
select,
16+
text,
17+
withKnobs,
18+
} from '@storybook/addon-knobs';
1219
import ComposedModal, {
1320
ModalHeader,
1421
ModalBody,
@@ -28,14 +35,17 @@ const sizes = {
2835
};
2936

3037
const buttons = {
31-
'None (0)': 0,
32-
'One (1)': 1,
33-
'Two (2)': 2,
38+
'None (0)': '0',
39+
'One (1)': '1',
40+
'Two (2)': '2',
41+
'Three (3)': '3',
3442
};
3543

3644
const props = {
3745
composedModal: ({ titleOnly } = {}) => ({
38-
numberOfButtons: select('Number of Buttons', buttons, 2),
46+
numberOfButtons: options('Number of Buttons', buttons, '2', {
47+
display: 'inline-radio',
48+
}),
3949
open: boolean('Open (open in <ComposedModal>)', true),
4050
onKeyDown: action('onKeyDown'),
4151
selectorPrimaryFocus: text(
@@ -72,6 +82,35 @@ const props = {
7282
'aria-label': text('ARIA label for content', 'Example modal content'),
7383
}),
7484
modalFooter: (numberOfButtons) => {
85+
const secondaryButtons = () => {
86+
switch (numberOfButtons) {
87+
case '2':
88+
return {
89+
secondaryButtonText: text(
90+
'Secondary button text (secondaryButtonText in <ModalFooter>)',
91+
'Secondary button'
92+
),
93+
};
94+
case '3':
95+
return {
96+
secondaryButtons: object(
97+
'Secondary button config array (secondaryButtons)',
98+
[
99+
{
100+
buttonText: 'Keep both',
101+
onClick: action('onClick'),
102+
},
103+
{
104+
buttonText: 'Rename',
105+
onClick: action('onClick'),
106+
},
107+
]
108+
),
109+
};
110+
default:
111+
return null;
112+
}
113+
};
75114
return {
76115
danger: boolean('Primary button danger (danger)', false),
77116
primaryButtonText: text(
@@ -82,13 +121,7 @@ const props = {
82121
'Primary button disabled (primaryButtonDisabled in <ModalFooter>)',
83122
false
84123
),
85-
secondaryButtonText:
86-
numberOfButtons === 2
87-
? text(
88-
'Secondary button text (secondaryButtonText in <ModalFooter>)',
89-
'Secondary button'
90-
)
91-
: null,
124+
...secondaryButtons(numberOfButtons),
92125
onRequestClose: action('onRequestClose'),
93126
onRequestSubmit: action('onRequestSubmit'),
94127
};

packages/react/src/components/ComposedModal/ComposedModal-test.js

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import ComposedModal, {
1313
ModalBody,
1414
ModalFooter,
1515
} from '../ComposedModal';
16+
import InlineLoading from '../InlineLoading';
1617
import { settings } from 'carbon-components';
1718

1819
const { prefix } = settings;
@@ -97,16 +98,28 @@ describe('<ModalFooter />', () => {
9798
});
9899
});
99100

100-
describe('Should render buttons only if appropriate prop passed in in', () => {
101+
describe('Should render buttons only if appropriate prop passed in', () => {
101102
const wrapper = shallow(
102103
<ModalFooter className="extra-class">
103104
<p>Test</p>
104105
</ModalFooter>
105106
);
106107

107108
const primaryWrapper = shallow(<ModalFooter primaryButtonText="test" />);
108-
const secondaryWrapper = shallow(
109-
<ModalFooter secondaryButtonText="test" />
109+
const secondaryWrapper = mount(<ModalFooter secondaryButtonText="test" />);
110+
const multipleSecondaryWrapper = mount(
111+
<ModalFooter
112+
secondaryButtons={[
113+
{
114+
buttonText: <InlineLoading />,
115+
onClick: jest.fn(),
116+
},
117+
{
118+
buttonText: 'Cancel',
119+
onClick: jest.fn(),
120+
},
121+
]}
122+
/>
110123
);
111124

112125
it('does not render primary button if no primary text', () => {
@@ -128,15 +141,36 @@ describe('<ModalFooter />', () => {
128141
expect(buttonComponent.exists()).toBe(true);
129142
expect(buttonComponent.props().kind).toBe('secondary');
130143
});
144+
145+
it('correctly renders multiple secondary buttons', () => {
146+
const buttonComponents = multipleSecondaryWrapper.find(Button);
147+
expect(buttonComponents.length).toEqual(2);
148+
expect(buttonComponents.at(0).props().kind).toBe('secondary');
149+
expect(buttonComponents.at(1).props().kind).toBe('secondary');
150+
});
131151
});
132152

133153
describe('Should render the appropriate buttons when `danger` prop is true', () => {
134154
const primaryWrapper = shallow(
135155
<ModalFooter primaryButtonText="test" danger />
136156
);
137-
const secondaryWrapper = shallow(
157+
const secondaryWrapper = mount(
138158
<ModalFooter secondaryButtonText="test" danger />
139159
);
160+
const multipleSecondaryWrapper = mount(
161+
<ModalFooter
162+
secondaryButtons={[
163+
{
164+
buttonText: <InlineLoading />,
165+
onClick: jest.fn(),
166+
},
167+
{
168+
buttonText: 'Cancel',
169+
onClick: jest.fn(),
170+
},
171+
]}
172+
/>
173+
);
140174

141175
it('renders danger button if primary text && danger', () => {
142176
const buttonComponent = primaryWrapper.find(Button);
@@ -149,6 +183,13 @@ describe('<ModalFooter />', () => {
149183
expect(buttonComponent.exists()).toBe(true);
150184
expect(buttonComponent.prop('kind')).toBe('secondary');
151185
});
186+
187+
it('correctly renders multiple secondary buttons', () => {
188+
const buttonComponents = multipleSecondaryWrapper.find(Button);
189+
expect(buttonComponents.length).toEqual(2);
190+
expect(buttonComponents.at(0).props().kind).toBe('secondary');
191+
expect(buttonComponents.at(1).props().kind).toBe('secondary');
192+
});
152193
});
153194
});
154195

packages/react/src/components/ComposedModal/ComposedModal.js

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,42 @@ export class ModalFooter extends Component {
563563
*/
564564
secondaryButtonText: PropTypes.string,
565565

566+
/**
567+
* Specify an array of config objects for secondary buttons
568+
* (`Array<{
569+
* buttonText: string,
570+
* onClick: function,
571+
* }>`).
572+
*/
573+
secondaryButtons: (props, propName, componentName) => {
574+
if (props.secondaryButtons) {
575+
if (
576+
!Array.isArray(props.secondaryButtons) ||
577+
props.secondaryButtons.length !== 2
578+
) {
579+
return new Error(
580+
`${propName} needs to be an array of two button config objects`
581+
);
582+
}
583+
584+
const shape = {
585+
buttonText: PropTypes.node,
586+
onClick: PropTypes.func,
587+
};
588+
589+
props[propName].forEach((secondaryButton) => {
590+
PropTypes.checkPropTypes(
591+
shape,
592+
secondaryButton,
593+
propName,
594+
componentName
595+
);
596+
});
597+
}
598+
599+
return null;
600+
},
601+
566602
/**
567603
* Specify a custom className to be applied to the secondary button
568604
*/
@@ -583,6 +619,7 @@ export class ModalFooter extends Component {
583619
const {
584620
className,
585621
primaryClassName,
622+
secondaryButtons,
586623
secondaryClassName,
587624
secondaryButtonText,
588625
primaryButtonText,
@@ -599,6 +636,8 @@ export class ModalFooter extends Component {
599636
const footerClass = classNames({
600637
[`${prefix}--modal-footer`]: true,
601638
[className]: className,
639+
[`${prefix}--modal-footer--three-button`]:
640+
Array.isArray(secondaryButtons) && secondaryButtons.length === 2,
602641
});
603642

604643
const primaryClass = classNames({
@@ -609,17 +648,36 @@ export class ModalFooter extends Component {
609648
[secondaryClassName]: secondaryClassName,
610649
});
611650

612-
return (
613-
<ButtonSet className={footerClass} {...other}>
614-
{secondaryButtonText && (
651+
const SecondaryButtonSet = () => {
652+
if (Array.isArray(secondaryButtons) && secondaryButtons.length <= 2) {
653+
return secondaryButtons.map(
654+
({ buttonText, onClick: onButtonClick }, i) => (
655+
<Button
656+
key={`${buttonText}-${i}`}
657+
className={secondaryClass}
658+
kind="secondary"
659+
onClick={onButtonClick || this.handleRequestClose}>
660+
{buttonText}
661+
</Button>
662+
)
663+
);
664+
}
665+
if (secondaryButtonText) {
666+
return (
615667
<Button
616668
className={secondaryClass}
617669
onClick={this.handleRequestClose}
618670
kind="secondary">
619671
{secondaryButtonText}
620672
</Button>
621-
)}
673+
);
674+
}
675+
return null;
676+
};
622677

678+
return (
679+
<ButtonSet className={footerClass} {...other}>
680+
<SecondaryButtonSet />
623681
{primaryButtonText && (
624682
<Button
625683
onClick={onRequestSubmit}

0 commit comments

Comments
 (0)