Skip to content

Commit 4daed01

Browse files
committed
feat(v5): Refactor ToggleButton
1 parent 64b78a5 commit 4daed01

File tree

5 files changed

+144
-76
lines changed

5 files changed

+144
-76
lines changed

src/ToggleButton.tsx

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import classNames from 'classnames';
22
import PropTypes from 'prop-types';
3-
import React, { useCallback, useState } from 'react';
4-
3+
import React from 'react';
4+
import { useBootstrapPrefix } from './ThemeProvider';
55
import Button, { ButtonProps } from './Button';
66
import {
77
BsPrefixAndClassNameOnlyProps,
@@ -25,6 +25,11 @@ type ToggleButton = BsPrefixComponentClass<'button', ToggleButtonProps>;
2525
const noop = () => undefined;
2626

2727
const propTypes = {
28+
/**
29+
* @default 'btn-check'
30+
*/
31+
bsPrefix: PropTypes.string,
32+
2833
/**
2934
* The `<input>` element `type`
3035
*/
@@ -46,6 +51,11 @@ const propTypes = {
4651
*/
4752
disabled: PropTypes.bool,
4853

54+
/**
55+
* `id` is required for button clicks to toggle input.
56+
*/
57+
id: PropTypes.string.isRequired,
58+
4959
/**
5060
* A callback fired when the underlying input element changes. This is passed
5161
* directly to the `<input>` so shares the same signature as a native `onChange` event.
@@ -68,6 +78,7 @@ const propTypes = {
6878
const ToggleButton = React.forwardRef<any, ToggleButtonProps>(
6979
(
7080
{
81+
bsPrefix,
7182
children,
7283
name,
7384
className,
@@ -76,49 +87,39 @@ const ToggleButton = React.forwardRef<any, ToggleButtonProps>(
7687
onChange,
7788
value,
7889
disabled,
90+
id,
7991
inputRef,
8092
...props
8193
}: ToggleButtonProps,
8294
ref,
8395
) => {
84-
const [focused, setFocused] = useState(false);
85-
86-
const handleFocus = useCallback((e) => {
87-
if (e.target.tagName === 'INPUT') setFocused(true);
88-
}, []);
89-
90-
const handleBlur = useCallback((e) => {
91-
if (e.target.tagName === 'INPUT') setFocused(false);
92-
}, []);
96+
bsPrefix = useBootstrapPrefix(bsPrefix, 'btn-check');
9397

9498
return (
95-
<Button
96-
{...props}
97-
ref={ref}
98-
className={classNames(
99-
className,
100-
focused && 'focus',
101-
disabled && 'disabled',
102-
)}
103-
type={undefined}
104-
active={!!checked}
105-
as="label"
106-
>
99+
<>
107100
<input
101+
className={bsPrefix}
108102
name={name}
109103
type={type}
110104
value={value as any}
111105
ref={inputRef as any}
112106
autoComplete="off"
113107
checked={!!checked}
114108
disabled={!!disabled}
115-
onFocus={handleFocus}
116-
onBlur={handleBlur}
117109
onChange={onChange || noop}
110+
id={id}
118111
/>
119-
120-
{children}
121-
</Button>
112+
<Button
113+
{...props}
114+
ref={ref}
115+
className={classNames(className, disabled && 'disabled')}
116+
type={undefined}
117+
as="label"
118+
htmlFor={id}
119+
>
120+
{children}
121+
</Button>
122+
</>
122123
);
123124
},
124125
);

test/ToggleButtonGroupSpec.js

Lines changed: 58 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describe('ToggleButton', () => {
88
const ref = React.createRef();
99
mount(
1010
<div>
11-
<ToggleButtonGroup.Button ref={ref} value={3}>
11+
<ToggleButtonGroup.Button id="id" ref={ref} value={3}>
1212
Option 3
1313
</ToggleButtonGroup.Button>
1414
</div>,
@@ -20,39 +20,28 @@ describe('ToggleButton', () => {
2020
it('should add an inputRef', () => {
2121
const ref = React.createRef();
2222
mount(
23-
<ToggleButtonGroup.Button inputRef={ref} value={3}>
23+
<ToggleButtonGroup.Button id="id" inputRef={ref} value={3}>
2424
Option 3
2525
</ToggleButtonGroup.Button>,
2626
);
2727

2828
ref.current.tagName.should.equal('INPUT');
2929
});
30-
31-
it('should set focus state', () => {
32-
const wrapper = mount(
33-
<ToggleButtonGroup.Button value={3}>Option 3</ToggleButtonGroup.Button>,
34-
);
35-
36-
wrapper.find('input').simulate('focus');
37-
wrapper.find('Button').hasClass('focus').should.equal(true);
38-
});
39-
40-
it('should set blur state', () => {
41-
const wrapper = mount(
42-
<ToggleButtonGroup.Button value={3}>Option 3</ToggleButtonGroup.Button>,
43-
);
44-
wrapper.find('input').simulate('blur');
45-
wrapper.find('Button').hasClass('focus').should.equal(false);
46-
});
4730
});
4831

4932
describe('ToggleButtonGroup', () => {
5033
it('should render checkboxes', () => {
5134
mount(
5235
<ToggleButtonGroup type="checkbox">
53-
<ToggleButtonGroup.Button value={1}>Option 1</ToggleButtonGroup.Button>
54-
<ToggleButtonGroup.Button value={2}>Option 2</ToggleButtonGroup.Button>
55-
<ToggleButtonGroup.Button value={3}>Option 3</ToggleButtonGroup.Button>
36+
<ToggleButtonGroup.Button id="id1" value={1}>
37+
Option 1
38+
</ToggleButtonGroup.Button>
39+
<ToggleButtonGroup.Button id="id2" value={2}>
40+
Option 2
41+
</ToggleButtonGroup.Button>
42+
<ToggleButtonGroup.Button id="id3" value={3}>
43+
Option 3
44+
</ToggleButtonGroup.Button>
5645
</ToggleButtonGroup>,
5746
)
5847
.find('input[type="checkbox"]')
@@ -62,9 +51,15 @@ describe('ToggleButtonGroup', () => {
6251
it('should render radios', () => {
6352
mount(
6453
<ToggleButtonGroup type="radio" name="items">
65-
<ToggleButtonGroup.Button value={1}>Option 1</ToggleButtonGroup.Button>
66-
<ToggleButtonGroup.Button value={2}>Option 2</ToggleButtonGroup.Button>
67-
<ToggleButtonGroup.Button value={3}>Option 3</ToggleButtonGroup.Button>
54+
<ToggleButtonGroup.Button id="id1" value={1}>
55+
Option 1
56+
</ToggleButtonGroup.Button>
57+
<ToggleButtonGroup.Button id="id2" value={2}>
58+
Option 2
59+
</ToggleButtonGroup.Button>
60+
<ToggleButtonGroup.Button id="id3" value={3}>
61+
Option 3
62+
</ToggleButtonGroup.Button>
6863
</ToggleButtonGroup>,
6964
)
7065
.find('input[type="radio"]')
@@ -74,9 +69,15 @@ describe('ToggleButtonGroup', () => {
7469
it('should select initial values', () => {
7570
mount(
7671
<ToggleButtonGroup type="checkbox" defaultValue={[1, 3]}>
77-
<ToggleButtonGroup.Button value={1}>Option 1</ToggleButtonGroup.Button>
78-
<ToggleButtonGroup.Button value={2}>Option 2</ToggleButtonGroup.Button>
79-
<ToggleButtonGroup.Button value={3}>Option 3</ToggleButtonGroup.Button>
72+
<ToggleButtonGroup.Button id="id1" value={1}>
73+
Option 1
74+
</ToggleButtonGroup.Button>
75+
<ToggleButtonGroup.Button id="id2" value={2}>
76+
Option 2
77+
</ToggleButtonGroup.Button>
78+
<ToggleButtonGroup.Button id="id3" value={3}>
79+
Option 3
80+
</ToggleButtonGroup.Button>
8081
</ToggleButtonGroup>,
8182
)
8283
.find('input[checked=true]')
@@ -86,13 +87,15 @@ describe('ToggleButtonGroup', () => {
8687
it('should disable radios', () => {
8788
const wrapper = mount(
8889
<ToggleButtonGroup type="radio" name="items">
89-
<ToggleButtonGroup.Button value={1} disabled>
90+
<ToggleButtonGroup.Button id="id1" value={1} disabled>
9091
Option 1
9192
</ToggleButtonGroup.Button>
92-
<ToggleButtonGroup.Button value={2} disabled>
93+
<ToggleButtonGroup.Button id="id2" value={2} disabled>
9394
Option 2
9495
</ToggleButtonGroup.Button>
95-
<ToggleButtonGroup.Button value={3}>Option 3</ToggleButtonGroup.Button>
96+
<ToggleButtonGroup.Button id="id3" value={3}>
97+
Option 3
98+
</ToggleButtonGroup.Button>
9699
</ToggleButtonGroup>,
97100
);
98101

@@ -105,9 +108,15 @@ describe('ToggleButtonGroup', () => {
105108
const spy = sinon.spy();
106109
mount(
107110
<ToggleButtonGroup type="checkbox" onChange={spy}>
108-
<ToggleButtonGroup.Button value={1}>Option 1</ToggleButtonGroup.Button>
109-
<ToggleButtonGroup.Button value={2}>Option 2</ToggleButtonGroup.Button>
110-
<ToggleButtonGroup.Button value={3}>Option 3</ToggleButtonGroup.Button>
111+
<ToggleButtonGroup.Button id="id1" value={1}>
112+
Option 1
113+
</ToggleButtonGroup.Button>
114+
<ToggleButtonGroup.Button id="id2" value={2}>
115+
Option 2
116+
</ToggleButtonGroup.Button>
117+
<ToggleButtonGroup.Button id="id3" value={3}>
118+
Option 3
119+
</ToggleButtonGroup.Button>
111120
</ToggleButtonGroup>,
112121
)
113122
.find('input[type="checkbox"]')
@@ -121,9 +130,15 @@ describe('ToggleButtonGroup', () => {
121130
const spy = sinon.spy();
122131
mount(
123132
<ToggleButtonGroup type="radio" name="items" onChange={spy}>
124-
<ToggleButtonGroup.Button value={1}>Option 1</ToggleButtonGroup.Button>
125-
<ToggleButtonGroup.Button value={2}>Option 2</ToggleButtonGroup.Button>
126-
<ToggleButtonGroup.Button value={3}>Option 3</ToggleButtonGroup.Button>
133+
<ToggleButtonGroup.Button id="id1" value={1}>
134+
Option 1
135+
</ToggleButtonGroup.Button>
136+
<ToggleButtonGroup.Button id="id2" value={2}>
137+
Option 2
138+
</ToggleButtonGroup.Button>
139+
<ToggleButtonGroup.Button id="id3" value={3}>
140+
Option 3
141+
</ToggleButtonGroup.Button>
127142
</ToggleButtonGroup>,
128143
)
129144
.find('input[type="radio"]')
@@ -142,8 +157,12 @@ describe('ToggleButtonGroup', () => {
142157
defaultValue={[1, 2]}
143158
onChange={spy}
144159
>
145-
<ToggleButtonGroup.Button value={1}>Option 1</ToggleButtonGroup.Button>
146-
<ToggleButtonGroup.Button value={2}>Option 2</ToggleButtonGroup.Button>
160+
<ToggleButtonGroup.Button id="id2" value={1}>
161+
Option 1
162+
</ToggleButtonGroup.Button>
163+
<ToggleButtonGroup.Button id="id3" value={2}>
164+
Option 2
165+
</ToggleButtonGroup.Button>
147166
</ToggleButtonGroup>,
148167
)
149168
.find('input[type="checkbox"]')

www/src/examples/Button/ToggleButton.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ function ToggleButtonExample() {
1010

1111
return (
1212
<>
13-
<ButtonGroup toggle className="mb-2">
13+
<ButtonGroup className="mb-2">
1414
<ToggleButton
15+
id="toggle-check"
1516
type="checkbox"
1617
variant="secondary"
1718
checked={checked}
@@ -22,10 +23,11 @@ function ToggleButtonExample() {
2223
</ToggleButton>
2324
</ButtonGroup>
2425
<br />
25-
<ButtonGroup toggle>
26+
<ButtonGroup className="mb-2">
2627
{radios.map((radio, idx) => (
2728
<ToggleButton
2829
key={idx}
30+
id={`radio-${idx}`}
2931
type="radio"
3032
variant="secondary"
3133
name="radio"
@@ -37,6 +39,35 @@ function ToggleButtonExample() {
3739
</ToggleButton>
3840
))}
3941
</ButtonGroup>
42+
<br />
43+
<ToggleButton
44+
className="mb-2"
45+
id="toggle-check"
46+
type="checkbox"
47+
variant="outline-primary"
48+
checked={checked}
49+
value="1"
50+
onChange={(e) => setChecked(e.currentTarget.checked)}
51+
>
52+
Checked
53+
</ToggleButton>
54+
<br />
55+
<ButtonGroup>
56+
{radios.map((radio, idx) => (
57+
<ToggleButton
58+
key={idx}
59+
id={`radio-${idx}`}
60+
type="radio"
61+
variant={idx % 2 ? 'outline-success' : 'outline-danger'}
62+
name="radio"
63+
value={radio.value}
64+
checked={radioValue === radio.value}
65+
onChange={(e) => setRadioValue(e.currentTarget.value)}
66+
>
67+
{radio.name}
68+
</ToggleButton>
69+
))}
70+
</ButtonGroup>
4071
</>
4172
);
4273
}
Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
11
<>
22
<ToggleButtonGroup type="checkbox" defaultValue={[1, 3]} className="mb-2">
3-
<ToggleButton value={1}>Checkbox 1 (pre-checked)</ToggleButton>
4-
<ToggleButton value={2}>Checkbox 2</ToggleButton>
5-
<ToggleButton value={3}>Checkbox 3 (pre-checked)</ToggleButton>
3+
<ToggleButton id="tbg-check-1" value={1}>
4+
Checkbox 1 (pre-checked)
5+
</ToggleButton>
6+
<ToggleButton id="tbg-check-2" value={2}>
7+
Checkbox 2
8+
</ToggleButton>
9+
<ToggleButton id="tbg-check-3" value={3}>
10+
Checkbox 3 (pre-checked)
11+
</ToggleButton>
612
</ToggleButtonGroup>
713
<br />
814
<ToggleButtonGroup type="radio" name="options" defaultValue={1}>
9-
<ToggleButton value={1}>Radio 1 (pre-checked)</ToggleButton>
10-
<ToggleButton value={2}>Radio 2</ToggleButton>
11-
<ToggleButton value={3}>Radio 3</ToggleButton>
15+
<ToggleButton id="tbg-radio-1" value={1}>
16+
Radio 1 (pre-checked)
17+
</ToggleButton>
18+
<ToggleButton id="tbg-radio-2" value={2}>
19+
Radio 2
20+
</ToggleButton>
21+
<ToggleButton id="tbg-radio-3" value={3}>
22+
Radio 3
23+
</ToggleButton>
1224
</ToggleButtonGroup>
1325
</>;

www/src/pages/migrating.mdx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,9 @@ spacing, use margin utilities instead.
7171

7272
### InputGroup
7373

74-
- dropped `InputGroupPrepend` and `InputGroupAppend`. Buttons and `InputGroupText` can now be added as direct children.
74+
- dropped `InputGroupPrepend` and `InputGroupAppend`. Buttons and `InputGroupText` can now be added as direct children.
75+
76+
### ToggleButton
77+
78+
- add `bsPrefix`.
79+
- `id` is now required. This is to allow toggling of the input due to markup changes in Bootstrap.

0 commit comments

Comments
 (0)