Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Switches #4268

Merged
merged 2 commits into from
Aug 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this intended to be used? I don't see it getting hit at all.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's used below in the useBootstrap thing

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i mean i don't see this ever getting overridden or anything demonstrating this getting overridden

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O it's more an escape hatch for a user, same as bsPrefix, since we are using custom-control in the same way it seemed good to also expose it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be good to document/demo

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It documented i guess. I didn't want to make a big deal out of it and confuse normal users i guess!


/**
* 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