Skip to content

Commit

Permalink
feat(v5): Add floating labels (#5567)
Browse files Browse the repository at this point in the history
* feat(v5): Add floating labels

* Add tests

* Clean up

* Clean up

* Fix bad merge
  • Loading branch information
kyletsang committed May 8, 2021
1 parent 3b1a8b7 commit 6802774
Show file tree
Hide file tree
Showing 11 changed files with 233 additions and 0 deletions.
52 changes: 52 additions & 0 deletions src/FloatingLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import * as React from 'react';

import FormGroup, { FormGroupProps } from './FormGroup';
import { BsPrefixProps, BsPrefixRefForwardingComponent } from './helpers';
import { useBootstrapPrefix } from './ThemeProvider';

export interface FloatingLabelProps extends FormGroupProps, BsPrefixProps {
controlId?: string;
label: React.ReactNode;
}

const propTypes = {
as: PropTypes.elementType,

/**
* Sets `id` on `<FormControl>` and `htmlFor` on `<label>`.
*/
controlId: PropTypes.string,

/**
* Form control label.
*/
label: PropTypes.node.isRequired,
};

const FloatingLabel: BsPrefixRefForwardingComponent<
'div',
FloatingLabelProps
> = React.forwardRef(
({ bsPrefix, className, children, controlId, label, ...props }, ref) => {
bsPrefix = useBootstrapPrefix(bsPrefix, 'form-floating');

return (
<FormGroup
ref={ref}
className={classNames(className, bsPrefix)}
controlId={controlId}
{...props}
>
{children}
<label htmlFor={controlId}>{label}</label>
</FormGroup>
);
},
);

FloatingLabel.displayName = 'FloatingLabel';
FloatingLabel.propTypes = propTypes;

export default FloatingLabel;
4 changes: 4 additions & 0 deletions src/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import PropTypes from 'prop-types';
import * as React from 'react';
import FormCheck from './FormCheck';
import FormControl from './FormControl';
import FormFloating from './FormFloating';
import FormGroup from './FormGroup';
import FormLabel from './FormLabel';
import FormRange from './FormRange';
import FormSelect from './FormSelect';
import FormText from './FormText';
import Switch from './Switch';
import FloatingLabel from './FloatingLabel';
import { BsPrefixRefForwardingComponent, AsProp } from './helpers';

export interface FormProps
Expand Down Expand Up @@ -64,10 +66,12 @@ Form.propTypes = propTypes as any;
export default Object.assign(Form, {
Group: FormGroup,
Control: FormControl,
Floating: FormFloating,
Check: FormCheck,
Switch,
Label: FormLabel,
Text: FormText,
Range: FormRange,
Select: FormSelect,
FloatingLabel,
});
3 changes: 3 additions & 0 deletions src/FormFloating.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import createWithBsPrefix from './createWithBsPrefix';

export default createWithBsPrefix('form-floating');
5 changes: 5 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ export type { FormControlProps } from './FormControl';
export { default as FormCheck } from './FormCheck';
export type { FormCheckProps } from './FormCheck';

export { default as FormFloating } from './FormFloating';

export { default as FloatingLabel } from './FloatingLabel';
export type { FloatingLabelProps } from './FloatingLabel';

export { default as FormGroup } from './FormGroup';
export type { FormGroupProps } from './FormGroup';

Expand Down
39 changes: 39 additions & 0 deletions test/FloatingLabelSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { mount } from 'enzyme';

import FloatingLabel from '../src/FloatingLabel';
import Form from '../src/Form';

describe('<FloatingLabel>', () => {
it('should render correctly', () => {
const wrapper = mount(
<FloatingLabel label="MyLabel">
<Form.Control type="text" />
</FloatingLabel>,
);

wrapper
.assertSingle('div.form-floating')
.assertSingle('input[type="text"]');

wrapper.assertSingle('label').text().should.equal('MyLabel');
});

it('should pass controlId to input and label', () => {
const wrapper = mount(
<FloatingLabel label="MyLabel" controlId="MyId">
<Form.Control type="text" />
</FloatingLabel>,
);

wrapper.assertSingle('input[id="MyId"]');
wrapper.assertSingle('label[htmlFor="MyId"]');
});

it('should support "as"', () => {
mount(
<FloatingLabel label="MyLabel" as="span">
<Form.Control type="text" />
</FloatingLabel>,
).assertSingle('span.form-floating');
});
});
12 changes: 12 additions & 0 deletions www/src/examples/Form/FormFloatingBasic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<>
<FloatingLabel
controlId="floatingInput"
label="Email address"
className="mb-3"
>
<Form.Control type="email" placeholder="name@example.com" />
</FloatingLabel>
<FloatingLabel controlId="floatingPassword" label="Password">
<Form.Control type="password" placeholder="Password" />
</FloatingLabel>
</>;
18 changes: 18 additions & 0 deletions www/src/examples/Form/FormFloatingCustom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<>
<Form.Floating className="mb-3">
<Form.Control
id="floatingInputCustom"
type="email"
placeholder="name@example.com"
/>
<label htmlFor="floatingInputCustom">Email address</label>
</Form.Floating>
<Form.Floating>
<Form.Control
id="floatingPasswordCustom"
type="password"
placeholder="Password"
/>
<label htmlFor="floatingPasswordCustom">Password</label>
</Form.Floating>
</>;
17 changes: 17 additions & 0 deletions www/src/examples/Form/FormFloatingLayout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Row className="g-2">
<Col md>
<FloatingLabel controlId="floatingInputGrid" label="Email address">
<Form.Control type="email" placeholder="name@example.com" />
</FloatingLabel>
</Col>
<Col md>
<FloatingLabel controlId="floatingSelectGrid" label="Works with selects">
<Form.Select aria-label="Floating label select example">
<option>Open this select menu</option>
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
</Form.Select>
</FloatingLabel>
</Col>
</Row>;
8 changes: 8 additions & 0 deletions www/src/examples/Form/FormFloatingSelect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<FloatingLabel controlId="floatingSelect" label="Works with selects">
<Form.Select aria-label="Floating label select example">
<option>Open this select menu</option>
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
</Form.Select>
</FloatingLabel>;
12 changes: 12 additions & 0 deletions www/src/examples/Form/FormFloatingTextarea.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<>
<FloatingLabel controlId="floatingTextarea" label="Comments" className="mb-3">
<Form.Control as="textarea" placeholder="Leave a comment here" />
</FloatingLabel>
<FloatingLabel controlId="floatingTextarea2" label="Comments">
<Form.Control
as="textarea"
placeholder="Leave a comment here"
style={{ height: '100px' }}
/>
</FloatingLabel>
</>;
63 changes: 63 additions & 0 deletions www/src/pages/components/forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import ComponentApi from '../../components/ComponentApi';
import LinkedHeading from '../../components/LinkedHeading';
import ReactPlayground from '../../components/ReactPlayground';
import FormBasic from '../../examples/Form/Basic';
import FormFloatingBasic from '../../examples/Form/FormFloatingBasic';
import FormFloatingCustom from '../../examples/Form/FormFloatingCustom';
import FormFloatingLayout from '../../examples/Form/FormFloatingLayout';
import FormFloatingSelect from '../../examples/Form/FormFloatingSelect';
import FormFloatingTextarea from '../../examples/Form/FormFloatingTextarea';
import Check from '../../examples/Form/Check';
import CheckApi from '../../examples/Form/CheckApi';
import CheckInline from '../../examples/Form/CheckInline';
Expand Down Expand Up @@ -185,6 +190,56 @@ export default withLayout(function FormControlsSection({ data }) {
similarly sized text inputs.
</p>
<ReactPlayground codeText={SelectSizes} />
<LinkedHeading h="2" id="forms-floating-labels">
Floating labels
</LinkedHeading>
<p>
Wrap a <code>{'<Form.Control>'}</code> element in{' '}
<code>{'<FloatingLabel>'}</code> to enable floating labels with
Bootstrap’s textual form fields. A <code>placeholder</code> is required
on each <code>{'<Form.Control>'}</code> as our method of CSS-only
floating labels uses the <code>:placeholder-shown</code> pseudo-element.
</p>
<ReactPlayground codeText={FormFloatingBasic} />
<LinkedHeading h="3" id="forms-floating-labels-textarea">
Textareas
</LinkedHeading>
<p>
By default, <code>{'<textarea>'}</code>s will be the same height as{' '}
<code>{'<input>'}</code>s. To set a custom height on your{' '}
<code>{'<textarea>'}</code>, do not use the <code>rows</code> attribute.
Instead, set an explicit <code>height</code> (either inline or via
custom CSS).
</p>
<ReactPlayground codeText={FormFloatingTextarea} />
<LinkedHeading h="3" id="forms-floating-labels-select">
Selects
</LinkedHeading>
<p>
Other than <code>{'<Form.Control>'}</code>, floating labels are only
available on <code>{'<Form.Select>'}</code>s. They work in the same way,
but unlike <code>{'<input>'}</code>s, they’ll always show the{' '}
<code>{'<label>'}</code> in its floated state.
</p>
<ReactPlayground codeText={FormFloatingSelect} />
<LinkedHeading h="3" id="forms-floating-labels-layout">
Layout
</LinkedHeading>
<p>
When working with the Bootstrap grid system, be sure to place form
elements within column classes.
</p>
<ReactPlayground codeText={FormFloatingLayout} />
<LinkedHeading h="3" id="forms-floating-labels-customize">
Customizing rendering
</LinkedHeading>
<p>
If you need greater control over the rendering, use the{' '}
<code>{'<FormFloating>'}</code> component to wrap your input and label.
Also note that the <code>{'<Form.Control>'}</code> must come first so we
can utilize a sibling selector (e.g., ~).
</p>
<ReactPlayground codeText={FormFloatingCustom} />
<LinkedHeading h="2" id="forms-layout">
Layout
</LinkedHeading>
Expand Down Expand Up @@ -419,6 +474,7 @@ export default withLayout(function FormControlsSection({ data }) {
API
</LinkedHeading>
<ComponentApi metadata={data.Form} />
<ComponentApi metadata={data.FormFloating} exportedBy={data.Form} />
<ComponentApi metadata={data.FormGroup} exportedBy={data.Form} />
<ComponentApi metadata={data.FormLabel} exportedBy={data.Form} />
<ComponentApi metadata={data.FormText} exportedBy={data.Form} />
Expand All @@ -435,6 +491,7 @@ export default withLayout(function FormControlsSection({ data }) {
/>
<ComponentApi metadata={data.FormRange} exportedBy={data.Form} />
<ComponentApi metadata={data.FormSelect} exportedBy={data.Form} />
<ComponentApi metadata={data.FloatingLabel} />
</>
);
});
Expand All @@ -444,6 +501,9 @@ export const query = graphql`
Form: componentMetadata(displayName: { eq: "Form" }) {
...ComponentApi_metadata
}
FormFloating: componentMetadata(displayName: { eq: "FormFloating" }) {
...ComponentApi_metadata
}
FormGroup: componentMetadata(displayName: { eq: "FormGroup" }) {
...ComponentApi_metadata
}
Expand Down Expand Up @@ -474,5 +534,8 @@ export const query = graphql`
FormSelect: componentMetadata(displayName: { eq: "FormSelect" }) {
...ComponentApi_metadata
}
FloatingLabel: componentMetadata(displayName: { eq: "FloatingLabel" }) {
...ComponentApi_metadata
}
}
`;

0 comments on commit 6802774

Please sign in to comment.