Skip to content

Commit

Permalink
feat: add Switches (#4268)
Browse files Browse the repository at this point in the history
* feat: add Switches

* clean up
  • Loading branch information
jquense authored Aug 19, 2019
1 parent 668ef09 commit 98297c6
Show file tree
Hide file tree
Showing 14 changed files with 190 additions and 92 deletions.
13 changes: 7 additions & 6 deletions src/Form.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import classNames from 'classnames';
import React from 'react';
import PropTypes from 'prop-types';

import createWithBsPrefix from './utils/createWithBsPrefix';
import { useBootstrapPrefix } from './ThemeProvider';
import FormGroup from './FormGroup';
import FormControl from './FormControl';
import React from 'react';
import FormCheck from './FormCheck';
import FormControl from './FormControl';
import FormGroup from './FormGroup';
import FormLabel from './FormLabel';
import FormText from './FormText';
import Switch from './Switch';
import { useBootstrapPrefix } from './ThemeProvider';
import createWithBsPrefix from './utils/createWithBsPrefix';

const propTypes = {
/**
Expand Down Expand Up @@ -80,6 +80,7 @@ Form.Row = createWithBsPrefix('form-row');
Form.Group = FormGroup;
Form.Control = FormControl;
Form.Check = FormCheck;
Form.Switch = Switch;
Form.Label = FormLabel;
Form.Text = FormText;

Expand Down
47 changes: 34 additions & 13 deletions src/FormCheck.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import classNames from 'classnames';
import React, { useContext, useMemo } from 'react';
import PropTypes from 'prop-types';

import { useBootstrapPrefix } from './ThemeProvider';
import FormContext from './FormContext';
import all from 'prop-types-extra/lib/all';
import React, { useContext, useMemo } from 'react';
import Feedback from './Feedback';
import FormCheckInput from './FormCheckInput';
import FormCheckLabel from './FormCheckLabel';
import FormContext from './FormContext';
import { useBootstrapPrefix } from './ThemeProvider';

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

/**
* A seperate bsPrefix used for custom controls
*
* @default 'custom-control'
*/
bsCustomPrefix: PropTypes.string,

/**
* The FormCheck `ref` will be forwarded to the underlying input element,
* which means it will be a DOM node, when resolved.
Expand All @@ -29,7 +36,7 @@ const propTypes = {
/**
* Provide a function child to manually handle the layout of the FormCheck's inner components.
*
* ````
* ```jsx
* <FormCheck>
* <FormCheck.Input isInvalid type={radio} />
* <FormCheck.Label>Allow us to contact you?</FormCheck.Label>
Expand All @@ -47,8 +54,17 @@ const propTypes = {
/** Use Bootstrap's custom form elements to replace the browser defaults */
custom: PropTypes.bool,

/** The type of checkable. */
type: PropTypes.oneOf(['radio', 'checkbox']).isRequired,
/**
* The type of checkable.
* @type {('radio' | 'checkbox' | 'switch')}
*/
type: all(
PropTypes.oneOf(['radio', 'checkbox', 'switch']).isRequired,
({ type, custom }) =>
type === 'switch' && custom === false
? Error('`custom` cannot be set to `false` when the type is `switch`')
: null,
),

/** Manually style the input as valid */
isValid: PropTypes.bool.isRequired,
Expand All @@ -74,6 +90,7 @@ const FormCheck = React.forwardRef(
{
id,
bsPrefix,
bsCustomPrefix,
inline,
disabled,
isValid,
Expand All @@ -85,12 +102,16 @@ const FormCheck = React.forwardRef(
type,
label,
children,
custom,
custom: propCustom,
...props
},
ref,
) => {
bsPrefix = useBootstrapPrefix(bsPrefix, 'form-check');
const custom = type === 'switch' ? true : propCustom;

bsPrefix = custom
? useBootstrapPrefix(bsCustomPrefix, 'custom-control')
: useBootstrapPrefix(bsPrefix, 'form-check');

const { controlId } = useContext(FormContext);
const innerFormContext = useMemo(
Expand All @@ -106,7 +127,7 @@ const FormCheck = React.forwardRef(
const input = (
<FormCheckInput
{...props}
type={type}
type={type === 'switch' ? 'checkbox' : type}
ref={ref}
isValid={isValid}
isInvalid={isInvalid}
Expand All @@ -121,9 +142,9 @@ const FormCheck = React.forwardRef(
style={style}
className={classNames(
className,
!custom && bsPrefix,
custom && `custom-control custom-${type}`,
inline && `${custom ? 'custom-control' : bsPrefix}-inline`,
bsPrefix,
custom && `custom-${type}`,
inline && `${bsPrefix}-inline`,
)}
>
{children || (
Expand Down
31 changes: 23 additions & 8 deletions src/FormCheckInput.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import classNames from 'classnames';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';

import { useBootstrapPrefix } from './ThemeProvider';
import React, { useContext } from 'react';
import FormContext from './FormContext';
import { useBootstrapPrefix } from './ThemeProvider';

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

/**
* A seperate bsPrefix used for custom controls
*
* @default 'custom-control'
*/
bsCustomPrefix: PropTypes.string,

/** A HTML id attribute, necessary for proper form accessibility. */
id: PropTypes.string,

Expand All @@ -36,12 +42,22 @@ const defaultProps = {

const FormCheckInput = React.forwardRef(
(
{ id, bsPrefix, className, isValid, isInvalid, isStatic, ...props },
{
id,
bsPrefix,
bsCustomPrefix,
className,
isValid,
isInvalid,
isStatic,
...props
},
ref,
) => {
bsPrefix = useBootstrapPrefix(bsPrefix, 'form-check-input');

const { controlId, custom } = useContext(FormContext);
bsPrefix = custom
? useBootstrapPrefix(bsCustomPrefix, 'custom-control-input')
: useBootstrapPrefix(bsPrefix, 'form-check-input');

return (
<input
Expand All @@ -50,8 +66,7 @@ const FormCheckInput = React.forwardRef(
id={id || controlId}
className={classNames(
className,
!custom && bsPrefix,
custom && 'custom-control-input',
bsPrefix,
isValid && 'is-valid',
isInvalid && 'is-invalid',
isStatic && 'position-static',
Expand Down
25 changes: 14 additions & 11 deletions src/FormCheckLabel.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import classNames from 'classnames';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';

import { useBootstrapPrefix } from './ThemeProvider';
import React, { useContext } from 'react';
import FormContext from './FormContext';
import { useBootstrapPrefix } from './ThemeProvider';

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

/**
* A seperate bsPrefix used for custom controls
*
* @default 'custom-control'
*/
bsCustomPrefix: PropTypes.string,

/** The HTML for attribute for associating the label with an input */
htmlFor: PropTypes.string,
};
Expand All @@ -20,21 +26,18 @@ const defaultProps = {
};

const FormCheckLabel = React.forwardRef(
({ bsPrefix, className, htmlFor, ...props }, ref) => {
bsPrefix = useBootstrapPrefix(bsPrefix, 'form-check-label');

({ bsPrefix, bsCustomPrefix, className, htmlFor, ...props }, ref) => {
const { controlId, custom } = useContext(FormContext);
bsPrefix = custom
? useBootstrapPrefix(bsCustomPrefix, 'custom-control-label')
: useBootstrapPrefix(bsPrefix, 'form-check-label');

return (
<label // eslint-disable-line jsx-a11y/label-has-associated-control
{...props}
ref={ref}
htmlFor={htmlFor || controlId}
className={classNames(
className,
!custom && bsPrefix,
custom && 'custom-control-label',
)}
className={classNames(className, bsPrefix)}
/>
);
},
Expand Down
6 changes: 2 additions & 4 deletions src/FormControl.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import classNames from 'classnames';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';

import React, { useContext } from 'react';
import warning from 'warning';

import Feedback from './Feedback';
import FormContext from './FormContext';
import { useBootstrapPrefix } from './ThemeProvider';
Expand Down Expand Up @@ -33,7 +31,7 @@ const propTypes = {
/**
* The underlying HTML element to use when rendering the FormControl.
*
* @type {('input'|'textarea'|elementType)}
* @type {('input'|'textarea'|'select'|elementType)}
*/
as: PropTypes.elementType,

Expand Down
13 changes: 13 additions & 0 deletions src/Switch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import FormCheck from './FormCheck';

const Switch = React.forwardRef((props, ref) => (
<FormCheck {...props} ref={ref} type="switch" />
));

Switch.displayName = 'Switch';

Switch.Input = FormCheck.Input;
Switch.Label = FormCheck.Label;

export default Switch;
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export Fade from './Fade';
export Form from './Form';
export FormControl from './FormControl';
export FormCheck from './FormCheck';
export Switch from './Switch';
export FormGroup from './FormGroup';
export FormLabel from './FormLabel';
export FormText from './FormText';
Expand Down
27 changes: 24 additions & 3 deletions test/FormCheckSpec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { mount } from 'enzyme';

import React from 'react';
import FormCheck from '../src/FormCheck';
import Switch from '../src/Switch';

describe('<FormCheck>', () => {
it('should render correctly', () => {
Expand Down Expand Up @@ -81,7 +81,7 @@ describe('<FormCheck>', () => {
expect(instance.input.tagName).to.equal('INPUT');
});

it('should support custom', () => {
it('should supports custom', () => {
const wrapper = mount(<FormCheck custom label="My label" />);

wrapper
Expand All @@ -96,4 +96,25 @@ describe('<FormCheck>', () => {
const wrapper = mount(<FormCheck custom inline label="My label" />);
wrapper.assertSingle('div.custom-control-inline');
});

it('should supports switches', () => {
let wrapper = mount(<FormCheck type="switch" label="My label" />);

wrapper
.assertSingle('div.custom-control')
.assertSingle('div.custom-switch')
.assertSingle('input[type="checkbox"].custom-control-input');

wrapper.assertSingle('label.custom-control-label');
wrapper.unmount();

wrapper = mount(<Switch label="My label" />);

wrapper
.assertSingle('div.custom-control')
.assertSingle('div.custom-switch')
.assertSingle('input[type="checkbox"].custom-control-input');

wrapper.assertSingle('label.custom-control-label');
});
});
5 changes: 2 additions & 3 deletions types/components/FormCheck.d.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import * as React from 'react';

import FormCheckInput from './FormCheckInput';
import FormCheckLabel from './FormCheckLabel';

import { BsPrefixComponent } from './helpers';

export interface FormCheckProps {
bsCustomPrefix?: string;
innerRef?: React.LegacyRef<this>;
id?: string;
inline?: boolean;
disabled?: boolean;
title?: string;
label?: React.ReactNode;
custom?: boolean;
type?: 'checkbox' | 'radio';
type?: 'checkbox' | 'radio' | 'switch';
isValid?: boolean;
isInvalid?: boolean;
feedback?: React.ReactNode;
Expand Down
6 changes: 2 additions & 4 deletions types/components/FormControl.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import * as React from 'react';

import Feedback from './Feedback';

import { BsPrefixComponent } from './helpers';

export interface FormControlProps {
Expand All @@ -18,10 +16,10 @@ export interface FormControlProps {
isInvalid?: boolean;
}

declare class Form<
declare class FormControl<
As extends React.ElementType = 'input'
> extends BsPrefixComponent<As, FormControlProps> {
static Feedback: typeof Feedback;
}

export default Form;
export default FormControl;
5 changes: 2 additions & 3 deletions types/components/FormGroup.d.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import * as React from 'react';

import { BsPrefixComponent } from './helpers';

export interface FormGroupProps {
innerRef?: React.LegacyRef<this>;
controlId?: string;
}

declare class Form<
declare class FormGroup<
As extends React.ElementType = 'div'
> extends BsPrefixComponent<As, FormGroupProps> {}

export default Form;
export default FormGroup;
Loading

0 comments on commit 98297c6

Please sign in to comment.