diff --git a/packages/components/src/components/form/_form.scss b/packages/components/src/components/form/_form.scss index e1504929e39a..12c2f4f04344 100644 --- a/packages/components/src/components/form/_form.scss +++ b/packages/components/src/components/form/_form.scss @@ -83,6 +83,26 @@ } } + //Fluid Form + .#{$prefix}--form--fluid + .#{$prefix}--text-input__field-wrapper[data-invalid] { + display: block; + } + + .#{$prefix}--form--fluid .#{$prefix}--fieldset { + margin: 0; + } + + .#{$prefix}--form--fluid input[data-invalid] { + outline: none; + } + + .#{$prefix}--form--fluid .#{$prefix}--form-requirement { + margin: 0; + padding: $carbon--spacing-03 rem(40px) $carbon--spacing-03 + $carbon--spacing-05; + } + // Fix for red ring when input is marked required in Firefox, refs #744 input:not(output):not([data-invalid]):-moz-ui-invalid { box-shadow: none; diff --git a/packages/components/src/components/text-input/_text-input.scss b/packages/components/src/components/text-input/_text-input.scss index e10ad33fcbc0..e73495696872 100644 --- a/packages/components/src/components/text-input/_text-input.scss +++ b/packages/components/src/components/text-input/_text-input.scss @@ -170,6 +170,60 @@ right: $carbon--spacing-08; } } + + //----------------------------- + // Fluid Text Input + //----------------------------- + .#{$prefix}--form--fluid .#{$prefix}--text-input-wrapper { + position: relative; + background: $field-01; + transition: background-color $duration--fast-01 motion(standard, productive), + outline $duration--fast-01 motion(standard, productive); + } + + .#{$prefix}--form--fluid .#{$prefix}--label { + position: absolute; + top: rem(13px); + left: $carbon--spacing-05; + z-index: 1; + margin: 0; + } + + .#{$prefix}--form--fluid .#{$prefix}--form__helper-text { + display: none; + } + + .#{$prefix}--form--fluid .#{$prefix}--text-input { + min-height: rem(64px); + padding: rem(32px) $carbon--spacing-05 rem(13px); + } + + .#{$prefix}--text-input__divider, + .#{$prefix}--form--fluid .#{$prefix}--text-input__divider { + display: none; + } + + .#{$prefix}--form--fluid .#{$prefix}--text-input--invalid { + border-bottom: none; + } + + .#{$prefix}--form--fluid + .#{$prefix}--text-input--invalid + + .#{$prefix}--text-input__divider { + display: block; + margin: 0 1rem; + border-style: solid; + border-bottom: none; + border-color: $ui-03; + } + + .#{$prefix}--form--fluid .#{$prefix}--text-input__invalid-icon { + top: rem(80px); + } + + .#{$prefix}--form--fluid .#{$prefix}--text-input-wrapper--light { + background: $field-02; + } } @include exports('text-input') { diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 6675236119c5..d63663db22bf 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -2568,6 +2568,16 @@ Map { }, }, }, + "FluidForm" => Object { + "propTypes": Object { + "children": Object { + "type": "node", + }, + "className": Object { + "type": "string", + }, + }, + }, "FormGroup" => Object { "defaultProps": Object { "invalid": false, diff --git a/packages/react/src/__tests__/index-test.js b/packages/react/src/__tests__/index-test.js index bc3e0c25b891..dfb8ea79ae71 100644 --- a/packages/react/src/__tests__/index-test.js +++ b/packages/react/src/__tests__/index-test.js @@ -52,6 +52,7 @@ describe('Carbon Components React', () => { "FileUploaderItem", "FileUploaderSkeleton", "Filename", + "FluidForm", "Form", "FormGroup", "FormItem", diff --git a/packages/react/src/components/FluidForm/FluidForm-story.js b/packages/react/src/components/FluidForm/FluidForm-story.js new file mode 100644 index 000000000000..7afec1823a1f --- /dev/null +++ b/packages/react/src/components/FluidForm/FluidForm-story.js @@ -0,0 +1,67 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; + +import { withKnobs } from '@storybook/addon-knobs'; +import FluidForm from '../FluidForm'; +import TextInput from '../TextInput'; + +const additionalProps = { + className: 'some-class', + onSubmit: e => { + e.preventDefault(); + action('FormSubmitted')(e); + }, +}; + +const TextInputProps = { + className: 'some-class', + id: 'test2', + labelText: 'Text Input label', + placeholder: 'Placeholder text', +}; + +const InvalidPasswordProps = { + className: 'some-class', + id: 'test4', + labelText: 'Password', + invalid: true, + invalidText: + 'Your password must be at least 6 characters as well as contain at least one uppercase, one lowercase, and one number.', +}; + +storiesOf('FluidForm', module) + .addDecorator(withKnobs) + .add( + 'Default', + () => ( + + + + + + ), + { + info: { + text: ` + Forms are widely used to collect user input. + + Form can have any number of react components enclosed within FormGroup component. FormGroup component + is a wrapper for legend and fieldset component. + + `, + }, + } + ); diff --git a/packages/react/src/components/FluidForm/FluidForm-test.js b/packages/react/src/components/FluidForm/FluidForm-test.js new file mode 100644 index 000000000000..b9cbfabae0d6 --- /dev/null +++ b/packages/react/src/components/FluidForm/FluidForm-test.js @@ -0,0 +1,58 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import FluidForm from '../FluidForm'; +import { shallow, mount } from 'enzyme'; + +describe('FluidForm', () => { + describe('Renders as expected', () => { + const wrapper = mount(); + it('renders children as expected', () => { + expect(wrapper.find('.child').length).toBe(0); + }); + it('renders wrapper as expected', () => { + expect(wrapper.length).toBe(1); + }); + it('renders extra classes passed in via className', () => { + expect(wrapper.hasClass('extra-class')).toEqual(true); + }); + + it('should render wrapper as expected', () => { + const form = shallow( + +
+
+ + ); + expect(form.length).toEqual(1); + }); + it('should render children as expected', () => { + const form1 = shallow( + +
+
+ + ); + expect(form1.find('.test-child').length).toBe(2); + }); + + it('should handle submit events', () => { + const onSubmit = jest.fn(); + const form1 = mount( + + + + ); + const btn = form1.find('button'); + btn.simulate('submit'); + expect(onSubmit).toBeCalled(); + }); + }); +}); diff --git a/packages/react/src/components/FluidForm/FluidForm.js b/packages/react/src/components/FluidForm/FluidForm.js new file mode 100644 index 000000000000..aa7141fe8f49 --- /dev/null +++ b/packages/react/src/components/FluidForm/FluidForm.js @@ -0,0 +1,41 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import PropTypes from 'prop-types'; +import React from 'react'; +import classnames from 'classnames'; +import { settings } from 'carbon-components'; +import Form from '../Form'; +import { FormContext } from './FormContext'; + +const { prefix } = settings; + +function FluidForm({ className, children, ...other }) { + const classNames = classnames(`${prefix}--form--fluid`, className); + + return ( + +
+ {children} +
+
+ ); +} + +FluidForm.propTypes = { + /** + * Provide children to be rendered inside of the
element + */ + children: PropTypes.node, + + /** + * Provide a custom className to be applied on the containing node + */ + className: PropTypes.string, +}; + +export default FluidForm; diff --git a/packages/react/src/components/FluidForm/FormContext.js b/packages/react/src/components/FluidForm/FormContext.js new file mode 100644 index 000000000000..c99698b123e9 --- /dev/null +++ b/packages/react/src/components/FluidForm/FormContext.js @@ -0,0 +1,12 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { createContext } from 'react'; + +export const FormContext = createContext({ + isFluid: false, +}); diff --git a/packages/react/src/components/FluidForm/index.js b/packages/react/src/components/FluidForm/index.js new file mode 100644 index 000000000000..c69d6f65ff7d --- /dev/null +++ b/packages/react/src/components/FluidForm/index.js @@ -0,0 +1,9 @@ +/** + * Copyright IBM Corp. 2016, 2018 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +export default from './FluidForm'; +export { FormContext } from './FormContext'; diff --git a/packages/react/src/components/Form/Form.js b/packages/react/src/components/Form/Form.js index 84d5c731250e..f6b1304a1704 100644 --- a/packages/react/src/components/Form/Form.js +++ b/packages/react/src/components/Form/Form.js @@ -14,7 +14,6 @@ const { prefix } = settings; const Form = ({ className, children, ...other }) => { const classNames = classnames(`${prefix}--form`, className); - return ( {' '} diff --git a/packages/react/src/components/TextInput/TextInput-story.js b/packages/react/src/components/TextInput/TextInput-story.js index 65906aafa38b..646b55cf6dd6 100644 --- a/packages/react/src/components/TextInput/TextInput-story.js +++ b/packages/react/src/components/TextInput/TextInput-story.js @@ -11,6 +11,7 @@ import { action } from '@storybook/addon-actions'; import { withKnobs, boolean, select, text } from '@storybook/addon-knobs'; import TextInput from '../TextInput'; import TextInputSkeleton from '../TextInput/TextInput.Skeleton'; +import FluidForm from '../FluidForm/FluidForm'; const types = { None: '', @@ -114,6 +115,27 @@ storiesOf('TextInput', module) }, } ) + .add( + 'Fluid', + () => ( + + + + ), + { + info: { + text: ` + Text fields enable the user to interact with and input data. A single line + field is used when the input anticipated by the user is a single line of + text as opposed to a paragraph. + The default type is 'text' and its value can be either 'string' or 'number'. + `, + }, + } + ) .add( 'Toggle password visibility', () => { diff --git a/packages/react/src/components/TextInput/TextInput.js b/packages/react/src/components/TextInput/TextInput.js index 0f4f8220cda6..9fca7079da3e 100644 --- a/packages/react/src/components/TextInput/TextInput.js +++ b/packages/react/src/components/TextInput/TextInput.js @@ -6,13 +6,14 @@ */ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useContext } from 'react'; import classNames from 'classnames'; import { settings } from 'carbon-components'; import { WarningFilled16 } from '@carbon/icons-react'; import PasswordInput from './PasswordInput'; import ControlledPasswordInput from './ControlledPasswordInput'; import { textInputProps } from './util'; +import { FormContext } from '../FluidForm'; const { prefix } = settings; const TextInput = React.forwardRef(function TextInput( @@ -59,6 +60,13 @@ const TextInput = React.forwardRef(function TextInput( title: placeholder, ...other, }; + const inputWrapperClasses = classNames( + `${prefix}--form-item`, + `${prefix}--text-input-wrapper`, + { + [`${prefix}--text-input-wrapper--light`]: light, + } + ); const labelClasses = classNames(`${prefix}--label`, { [`${prefix}--visually-hidden`]: hideLabel, [`${prefix}--label--disabled`]: other.disabled, @@ -83,8 +91,10 @@ const TextInput = React.forwardRef(function TextInput(
{helperText}
) : null; + const { isFluid } = useContext(FormContext); + return ( -
+
{label} {helper}
)} {input} + {isFluid &&
} + {/*
*/} + {isFluid ? error : null}
- {error} + {isFluid ? null : error}
); }); diff --git a/packages/react/src/index.js b/packages/react/src/index.js index e2938a6f74b2..686c29db490a 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -58,6 +58,7 @@ export FileUploader, { FileUploaderItem, } from './components/FileUploader'; export Form from './components/Form'; +export FluidForm from './components/FluidForm'; export FormGroup from './components/FormGroup'; export FormItem from './components/FormItem'; export FormLabel from './components/FormLabel';