Skip to content

Commit c24b224

Browse files
committed
Use React Context for modifying Button component behaviour when rendered within ButtonGroup component
1 parent 56bf603 commit c24b224

File tree

7 files changed

+146
-90
lines changed

7 files changed

+146
-90
lines changed

src/lib/components/ui/Button/Button.jsx

Lines changed: 85 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import PropTypes from 'prop-types';
2-
import React from 'react';
2+
import React, { useContext } from 'react';
33
import getRootSizeClassName from '../../../helpers/getRootSizeClassName';
44
import getRootColorClassName from '../../../helpers/getRootColorClassName';
5+
import { resolveContextOrProp } from '../../../helpers/resolveContextOrProp';
56
import { withProviderContext } from '../../../provider';
67
import transferProps from '../../../utils/transferProps';
78
import withForwardedRef from '../withForwardedRef';
9+
import { ButtonGroupContext } from '../ButtonGroup';
810
import getRootLabelVisibilityClassName from './helpers/getRootLabelVisibilityClassName';
911
import getRootPriorityClassName from './helpers/getRootPriorityClassName';
1012
import styles from './Button.scss';
@@ -17,7 +19,6 @@ export const Button = ({
1719
disabled,
1820
endCorner,
1921
forwardedRef,
20-
grouped,
2122
id,
2223
label,
2324
labelVisibility,
@@ -28,68 +29,78 @@ export const Button = ({
2829
type,
2930
color,
3031
...restProps
31-
}) => (
32-
/* No worries, `type` is always assigned correctly through props. */
33-
/* eslint-disable react/button-has-type */
34-
<button
35-
{...transferProps(restProps)}
36-
className={
37-
priority === 'link'
38-
? [
39-
styles.root,
40-
getRootPriorityClassName(priority, styles),
41-
].join(' ')
42-
: [
43-
styles.root,
44-
getRootPriorityClassName(priority, styles),
45-
getRootColorClassName(color, styles),
46-
getRootSizeClassName(size, styles),
47-
getRootLabelVisibilityClassName(labelVisibility, styles),
48-
block ? styles.rootBlock : '',
49-
grouped ? styles.rootGrouped : '',
50-
loadingIcon ? styles.isRootLoading : '',
51-
].join(' ')
52-
}
53-
disabled={disabled || !!loadingIcon}
54-
id={id}
55-
onClick={clickHandler}
56-
ref={forwardedRef}
57-
type={type}
58-
>
59-
{priority !== 'link' && startCorner && (
60-
<span className={styles.startCorner}>
61-
{startCorner}
62-
</span>
63-
)}
64-
{beforeLabel && (
65-
<span className={styles.beforeLabel}>
66-
{beforeLabel}
67-
</span>
68-
)}
69-
<span
70-
className={styles.label}
71-
{...(id && { id: `${id}__labelText` })}
32+
}) => {
33+
const context = useContext(ButtonGroupContext);
34+
35+
return (
36+
/* No worries, `type` is always assigned correctly through props. */
37+
/* eslint-disable react/button-has-type */
38+
<button
39+
{...transferProps(restProps)}
40+
className={
41+
priority === 'link'
42+
? [
43+
styles.root,
44+
getRootPriorityClassName(priority, styles),
45+
].join(' ')
46+
: [
47+
styles.root,
48+
getRootPriorityClassName(
49+
resolveContextOrProp(context?.priority, priority),
50+
styles,
51+
),
52+
getRootColorClassName(color, styles),
53+
getRootSizeClassName(
54+
resolveContextOrProp(context?.size, size),
55+
styles,
56+
),
57+
getRootLabelVisibilityClassName(labelVisibility, styles),
58+
resolveContextOrProp(context?.block, block) ? styles.rootBlock : '',
59+
context ? styles.rootGrouped : '',
60+
loadingIcon ? styles.isRootLoading : '',
61+
].join(' ')
62+
}
63+
disabled={resolveContextOrProp(context?.disabled, disabled) || !!loadingIcon}
64+
id={id}
65+
onClick={clickHandler}
66+
ref={forwardedRef}
67+
type={type}
7268
>
73-
{label}
74-
</span>
75-
{afterLabel && (
76-
<span className={styles.afterLabel}>
77-
{afterLabel}
69+
{priority !== 'link' && startCorner && (
70+
<span className={styles.startCorner}>
71+
{startCorner}
72+
</span>
73+
)}
74+
{beforeLabel && (
75+
<span className={styles.beforeLabel}>
76+
{beforeLabel}
77+
</span>
78+
)}
79+
<span
80+
className={styles.label}
81+
{...(id && { id: `${id}__labelText` })}
82+
>
83+
{label}
7884
</span>
79-
)}
80-
{priority !== 'link' && endCorner && (
81-
<span className={styles.endCorner}>
82-
{endCorner}
83-
</span>
84-
)}
85-
{priority !== 'link' && loadingIcon && (
86-
<span className={styles.loadingIcon}>
87-
{loadingIcon}
88-
</span>
89-
)}
90-
</button>
91-
/* eslint-enable react/button-has-type */
92-
);
85+
{afterLabel && (
86+
<span className={styles.afterLabel}>
87+
{afterLabel}
88+
</span>
89+
)}
90+
{priority !== 'link' && endCorner && (
91+
<span className={styles.endCorner}>
92+
{endCorner}
93+
</span>
94+
)}
95+
{priority !== 'link' && loadingIcon && (
96+
<span className={styles.loadingIcon}>
97+
{loadingIcon}
98+
</span>
99+
)}
100+
</button>
101+
/* eslint-enable react/button-has-type */
102+
);
103+
};
93104

94105
Button.defaultProps = {
95106
afterLabel: null,
@@ -100,7 +111,6 @@ Button.defaultProps = {
100111
disabled: false,
101112
endCorner: null,
102113
forwardedRef: undefined,
103-
grouped: false,
104114
id: undefined,
105115
labelVisibility: 'all',
106116
loadingIcon: null,
@@ -121,6 +131,9 @@ Button.propTypes = {
121131
beforeLabel: PropTypes.node,
122132
/**
123133
* If `true`, the button will span the full width of its parent. Only available if `priority` != `link`.
134+
*
135+
* Ignored if the component is rendered within `ButtonGroup` component
136+
* as the value is inherited in such case.
124137
*/
125138
block: PropTypes.bool,
126139
/**
@@ -133,6 +146,9 @@ Button.propTypes = {
133146
color: PropTypes.oneOf(['primary', 'secondary', 'success', 'warning', 'danger', 'help', 'info', 'note', 'light', 'dark']),
134147
/**
135148
* If `true`, the button will be disabled.
149+
*
150+
* Ignored if the component is rendered within `ButtonGroup` component
151+
* as the value is inherited in such case.
136152
*/
137153
disabled: PropTypes.bool,
138154
/**
@@ -147,10 +163,6 @@ Button.propTypes = {
147163
// eslint-disable-next-line react/forbid-prop-types
148164
PropTypes.shape({ current: PropTypes.any }),
149165
]),
150-
/**
151-
* Treat button differently when it's inside `ButtonGroup`. Do not set manually!
152-
*/
153-
grouped: PropTypes.bool,
154166
/**
155167
* ID of the root HTML element.
156168
*
@@ -173,10 +185,16 @@ Button.propTypes = {
173185
loadingIcon: PropTypes.node,
174186
/**
175187
* Visual priority to highlight or suppress the button.
188+
*
189+
* Ignored if the component is rendered within `ButtonGroup` component
190+
* as the value is inherited in such case.
176191
*/
177192
priority: PropTypes.oneOf(['filled', 'outline', 'flat', 'link']),
178193
/**
179194
* Size of the button. Only available if `priority` != `link`.
195+
*
196+
* Ignored if the component is rendered within `ButtonGroup` component
197+
* as the value is inherited in such case.
180198
*/
181199
size: PropTypes.oneOf(['small', 'medium', 'large']),
182200
/**

src/lib/components/ui/Button/__tests__/Button.test.jsx

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,58 @@ import { colorPropTest } from '../../../../../../tests/propTests/colorPropTest';
1010
import { forwardedRefPropTest } from '../../../../../../tests/propTests/forwardedRefPropTest';
1111
import { labelPropTest } from '../../../../../../tests/propTests/labelPropTest';
1212
import { sizePropTest } from '../../../../../../tests/propTests/sizePropTest';
13+
import { ButtonGroupContext } from '../../ButtonGroup';
1314
import { Button } from '../Button';
1415

1516
const mandatoryProps = {
1617
label: 'label',
1718
};
1819

1920
describe('rendering', () => {
21+
it.each([
22+
[
23+
{ block: true },
24+
(rootElement) => expect(rootElement).toHaveClass('rootBlock'),
25+
],
26+
[
27+
{ block: false },
28+
(rootElement) => expect(rootElement).not.toHaveClass('rootBlock'),
29+
],
30+
[
31+
{ disabled: true },
32+
(rootElement) => expect(rootElement).toBeDisabled(),
33+
],
34+
[
35+
{ disabled: false },
36+
(rootElement) => expect(rootElement).not.toBeDisabled(),
37+
],
38+
[
39+
{ priority: 'filled' },
40+
(rootElement) => expect(rootElement).toHaveClass('rootPriorityFilled'),
41+
],
42+
[
43+
{ priority: 'outline' },
44+
(rootElement) => expect(rootElement).toHaveClass('rootPriorityOutline'),
45+
],
46+
[
47+
{ priority: 'flat' },
48+
(rootElement) => expect(rootElement).toHaveClass('rootPriorityFlat'),
49+
],
50+
...sizePropTest,
51+
])('renders with ButtonGroup props: "%s"', (testedProps, assert) => {
52+
const dom = render((
53+
<ButtonGroupContext.Provider
54+
value={{ ...testedProps }}
55+
>
56+
<Button
57+
{...mandatoryProps}
58+
/>
59+
</ButtonGroupContext.Provider>
60+
));
61+
62+
assert(dom.container.firstChild);
63+
});
64+
2065
it.each([
2166
[
2267
{ afterLabel: <div>after label</div> },
@@ -48,15 +93,6 @@ describe('rendering', () => {
4893
(rootElement) => expect(within(rootElement).getByText('corner text')),
4994
],
5095
...forwardedRefPropTest(React.createRef()),
51-
[
52-
{ grouped: true },
53-
(rootElement) => expect(rootElement).toHaveClass('rootGrouped'),
54-
],
55-
[
56-
{ grouped: false },
57-
(rootElement) => expect(rootElement).not.toHaveClass('rootGrouped'),
58-
],
59-
6096
[
6197
{ id: 'id' },
6298
(rootElement) => {

src/lib/components/ui/ButtonGroup/ButtonGroup.jsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
22
import React from 'react';
33
import { withProviderContext } from '../../../provider';
44
import styles from './ButtonGroup.scss';
5+
import { ButtonGroupContext } from './ButtonGroupContext';
56

67
export const ButtonGroup = ({
78
block,
@@ -19,19 +20,16 @@ export const ButtonGroup = ({
1920
role="group"
2021
{...restProps}
2122
>
22-
{React.Children.map(children, (child) => {
23-
if (!React.isValidElement(child)) {
24-
return null;
25-
}
26-
27-
return React.cloneElement(child, {
23+
<ButtonGroupContext.Provider
24+
value={{
2825
block,
2926
disabled,
30-
grouped: true,
3127
priority,
3228
size,
33-
});
34-
})}
29+
}}
30+
>
31+
{children}
32+
</ButtonGroupContext.Provider>
3533
</div>
3634
);
3735

@@ -50,7 +48,7 @@ ButtonGroup.propTypes = {
5048
/**
5149
* Buttons to be grouped.
5250
*/
53-
children: PropTypes.arrayOf(PropTypes.element).isRequired,
51+
children: PropTypes.node.isRequired,
5452
/**
5553
* If `true`, all buttons inside the group will be disabled.
5654
*/
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import React from 'react';
2+
3+
export const ButtonGroupContext = React.createContext(null);

src/lib/components/ui/ButtonGroup/__tests__/ButtonGroup.test.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Button from '../../Button';
77
import { ButtonGroup } from '../ButtonGroup';
88

99
const mandatoryProps = {
10-
children: [<Button label="label" />],
10+
children: <Button label="label" />,
1111
};
1212

1313
describe('rendering', () => {
@@ -21,7 +21,7 @@ describe('rendering', () => {
2121
(rootElement) => expect(rootElement).not.toHaveClass('isRootBlock'),
2222
],
2323
[
24-
{ children: [<Button label="label text" />] },
24+
{ children: <Button label="label text" /> },
2525
(rootElement) => expect(within(rootElement).getByText('label text')),
2626
],
2727
[
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export { default } from './ButtonGroup';
1+
export { default as ButtonGroup } from './ButtonGroup';
2+
export { ButtonGroupContext } from './ButtonGroupContext';

src/lib/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export {
3333
export { default as Alert } from './components/ui/Alert';
3434
export { default as Badge } from './components/ui/Badge';
3535
export { default as Button } from './components/ui/Button';
36-
export { default as ButtonGroup } from './components/ui/ButtonGroup';
36+
export { ButtonGroup } from './components/ui/ButtonGroup';
3737
export {
3838
Card,
3939
CardBody,

0 commit comments

Comments
 (0)