Skip to content

Commit

Permalink
feat(v5): Refactor ToggleButton
Browse files Browse the repository at this point in the history
  • Loading branch information
kyletsang committed Aug 7, 2020
1 parent 64b78a5 commit 4daed01
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 76 deletions.
57 changes: 29 additions & 28 deletions src/ToggleButton.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { useCallback, useState } from 'react';

import React from 'react';
import { useBootstrapPrefix } from './ThemeProvider';
import Button, { ButtonProps } from './Button';
import {
BsPrefixAndClassNameOnlyProps,
Expand All @@ -25,6 +25,11 @@ type ToggleButton = BsPrefixComponentClass<'button', ToggleButtonProps>;
const noop = () => undefined;

const propTypes = {
/**
* @default 'btn-check'
*/
bsPrefix: PropTypes.string,

/**
* The `<input>` element `type`
*/
Expand All @@ -46,6 +51,11 @@ const propTypes = {
*/
disabled: PropTypes.bool,

/**
* `id` is required for button clicks to toggle input.
*/
id: PropTypes.string.isRequired,

/**
* A callback fired when the underlying input element changes. This is passed
* directly to the `<input>` so shares the same signature as a native `onChange` event.
Expand All @@ -68,6 +78,7 @@ const propTypes = {
const ToggleButton = React.forwardRef<any, ToggleButtonProps>(
(
{
bsPrefix,
children,
name,
className,
Expand All @@ -76,49 +87,39 @@ const ToggleButton = React.forwardRef<any, ToggleButtonProps>(
onChange,
value,
disabled,
id,
inputRef,
...props
}: ToggleButtonProps,
ref,
) => {
const [focused, setFocused] = useState(false);

const handleFocus = useCallback((e) => {
if (e.target.tagName === 'INPUT') setFocused(true);
}, []);

const handleBlur = useCallback((e) => {
if (e.target.tagName === 'INPUT') setFocused(false);
}, []);
bsPrefix = useBootstrapPrefix(bsPrefix, 'btn-check');

return (
<Button
{...props}
ref={ref}
className={classNames(
className,
focused && 'focus',
disabled && 'disabled',
)}
type={undefined}
active={!!checked}
as="label"
>
<>
<input
className={bsPrefix}
name={name}
type={type}
value={value as any}
ref={inputRef as any}
autoComplete="off"
checked={!!checked}
disabled={!!disabled}
onFocus={handleFocus}
onBlur={handleBlur}
onChange={onChange || noop}
id={id}
/>

{children}
</Button>
<Button
{...props}
ref={ref}
className={classNames(className, disabled && 'disabled')}
type={undefined}
as="label"
htmlFor={id}
>
{children}
</Button>
</>
);
},
);
Expand Down
97 changes: 58 additions & 39 deletions test/ToggleButtonGroupSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('ToggleButton', () => {
const ref = React.createRef();
mount(
<div>
<ToggleButtonGroup.Button ref={ref} value={3}>
<ToggleButtonGroup.Button id="id" ref={ref} value={3}>
Option 3
</ToggleButtonGroup.Button>
</div>,
Expand All @@ -20,39 +20,28 @@ describe('ToggleButton', () => {
it('should add an inputRef', () => {
const ref = React.createRef();
mount(
<ToggleButtonGroup.Button inputRef={ref} value={3}>
<ToggleButtonGroup.Button id="id" inputRef={ref} value={3}>
Option 3
</ToggleButtonGroup.Button>,
);

ref.current.tagName.should.equal('INPUT');
});

it('should set focus state', () => {
const wrapper = mount(
<ToggleButtonGroup.Button value={3}>Option 3</ToggleButtonGroup.Button>,
);

wrapper.find('input').simulate('focus');
wrapper.find('Button').hasClass('focus').should.equal(true);
});

it('should set blur state', () => {
const wrapper = mount(
<ToggleButtonGroup.Button value={3}>Option 3</ToggleButtonGroup.Button>,
);
wrapper.find('input').simulate('blur');
wrapper.find('Button').hasClass('focus').should.equal(false);
});
});

describe('ToggleButtonGroup', () => {
it('should render checkboxes', () => {
mount(
<ToggleButtonGroup type="checkbox">
<ToggleButtonGroup.Button value={1}>Option 1</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button value={2}>Option 2</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button value={3}>Option 3</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id1" value={1}>
Option 1
</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id2" value={2}>
Option 2
</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id3" value={3}>
Option 3
</ToggleButtonGroup.Button>
</ToggleButtonGroup>,
)
.find('input[type="checkbox"]')
Expand All @@ -62,9 +51,15 @@ describe('ToggleButtonGroup', () => {
it('should render radios', () => {
mount(
<ToggleButtonGroup type="radio" name="items">
<ToggleButtonGroup.Button value={1}>Option 1</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button value={2}>Option 2</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button value={3}>Option 3</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id1" value={1}>
Option 1
</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id2" value={2}>
Option 2
</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id3" value={3}>
Option 3
</ToggleButtonGroup.Button>
</ToggleButtonGroup>,
)
.find('input[type="radio"]')
Expand All @@ -74,9 +69,15 @@ describe('ToggleButtonGroup', () => {
it('should select initial values', () => {
mount(
<ToggleButtonGroup type="checkbox" defaultValue={[1, 3]}>
<ToggleButtonGroup.Button value={1}>Option 1</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button value={2}>Option 2</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button value={3}>Option 3</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id1" value={1}>
Option 1
</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id2" value={2}>
Option 2
</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id3" value={3}>
Option 3
</ToggleButtonGroup.Button>
</ToggleButtonGroup>,
)
.find('input[checked=true]')
Expand All @@ -86,13 +87,15 @@ describe('ToggleButtonGroup', () => {
it('should disable radios', () => {
const wrapper = mount(
<ToggleButtonGroup type="radio" name="items">
<ToggleButtonGroup.Button value={1} disabled>
<ToggleButtonGroup.Button id="id1" value={1} disabled>
Option 1
</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button value={2} disabled>
<ToggleButtonGroup.Button id="id2" value={2} disabled>
Option 2
</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button value={3}>Option 3</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id3" value={3}>
Option 3
</ToggleButtonGroup.Button>
</ToggleButtonGroup>,
);

Expand All @@ -105,9 +108,15 @@ describe('ToggleButtonGroup', () => {
const spy = sinon.spy();
mount(
<ToggleButtonGroup type="checkbox" onChange={spy}>
<ToggleButtonGroup.Button value={1}>Option 1</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button value={2}>Option 2</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button value={3}>Option 3</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id1" value={1}>
Option 1
</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id2" value={2}>
Option 2
</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id3" value={3}>
Option 3
</ToggleButtonGroup.Button>
</ToggleButtonGroup>,
)
.find('input[type="checkbox"]')
Expand All @@ -121,9 +130,15 @@ describe('ToggleButtonGroup', () => {
const spy = sinon.spy();
mount(
<ToggleButtonGroup type="radio" name="items" onChange={spy}>
<ToggleButtonGroup.Button value={1}>Option 1</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button value={2}>Option 2</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button value={3}>Option 3</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id1" value={1}>
Option 1
</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id2" value={2}>
Option 2
</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id3" value={3}>
Option 3
</ToggleButtonGroup.Button>
</ToggleButtonGroup>,
)
.find('input[type="radio"]')
Expand All @@ -142,8 +157,12 @@ describe('ToggleButtonGroup', () => {
defaultValue={[1, 2]}
onChange={spy}
>
<ToggleButtonGroup.Button value={1}>Option 1</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button value={2}>Option 2</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id2" value={1}>
Option 1
</ToggleButtonGroup.Button>
<ToggleButtonGroup.Button id="id3" value={2}>
Option 2
</ToggleButtonGroup.Button>
</ToggleButtonGroup>,
)
.find('input[type="checkbox"]')
Expand Down
35 changes: 33 additions & 2 deletions www/src/examples/Button/ToggleButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ function ToggleButtonExample() {

return (
<>
<ButtonGroup toggle className="mb-2">
<ButtonGroup className="mb-2">
<ToggleButton
id="toggle-check"
type="checkbox"
variant="secondary"
checked={checked}
Expand All @@ -22,10 +23,11 @@ function ToggleButtonExample() {
</ToggleButton>
</ButtonGroup>
<br />
<ButtonGroup toggle>
<ButtonGroup className="mb-2">
{radios.map((radio, idx) => (
<ToggleButton
key={idx}
id={`radio-${idx}`}
type="radio"
variant="secondary"
name="radio"
Expand All @@ -37,6 +39,35 @@ function ToggleButtonExample() {
</ToggleButton>
))}
</ButtonGroup>
<br />
<ToggleButton
className="mb-2"
id="toggle-check"
type="checkbox"
variant="outline-primary"
checked={checked}
value="1"
onChange={(e) => setChecked(e.currentTarget.checked)}
>
Checked
</ToggleButton>
<br />
<ButtonGroup>
{radios.map((radio, idx) => (
<ToggleButton
key={idx}
id={`radio-${idx}`}
type="radio"
variant={idx % 2 ? 'outline-success' : 'outline-danger'}
name="radio"
value={radio.value}
checked={radioValue === radio.value}
onChange={(e) => setRadioValue(e.currentTarget.value)}
>
{radio.name}
</ToggleButton>
))}
</ButtonGroup>
</>
);
}
Expand Down
24 changes: 18 additions & 6 deletions www/src/examples/Button/ToggleButtonGroupUncontrolled.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
<>
<ToggleButtonGroup type="checkbox" defaultValue={[1, 3]} className="mb-2">
<ToggleButton value={1}>Checkbox 1 (pre-checked)</ToggleButton>
<ToggleButton value={2}>Checkbox 2</ToggleButton>
<ToggleButton value={3}>Checkbox 3 (pre-checked)</ToggleButton>
<ToggleButton id="tbg-check-1" value={1}>
Checkbox 1 (pre-checked)
</ToggleButton>
<ToggleButton id="tbg-check-2" value={2}>
Checkbox 2
</ToggleButton>
<ToggleButton id="tbg-check-3" value={3}>
Checkbox 3 (pre-checked)
</ToggleButton>
</ToggleButtonGroup>
<br />
<ToggleButtonGroup type="radio" name="options" defaultValue={1}>
<ToggleButton value={1}>Radio 1 (pre-checked)</ToggleButton>
<ToggleButton value={2}>Radio 2</ToggleButton>
<ToggleButton value={3}>Radio 3</ToggleButton>
<ToggleButton id="tbg-radio-1" value={1}>
Radio 1 (pre-checked)
</ToggleButton>
<ToggleButton id="tbg-radio-2" value={2}>
Radio 2
</ToggleButton>
<ToggleButton id="tbg-radio-3" value={3}>
Radio 3
</ToggleButton>
</ToggleButtonGroup>
</>;
7 changes: 6 additions & 1 deletion www/src/pages/migrating.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,9 @@ spacing, use margin utilities instead.

### InputGroup

- dropped `InputGroupPrepend` and `InputGroupAppend`. Buttons and `InputGroupText` can now be added as direct children.
- dropped `InputGroupPrepend` and `InputGroupAppend`. Buttons and `InputGroupText` can now be added as direct children.

### ToggleButton

- add `bsPrefix`.
- `id` is now required. This is to allow toggling of the input due to markup changes in Bootstrap.

0 comments on commit 4daed01

Please sign in to comment.