Skip to content

Commit

Permalink
feat(fieldset): add Fieldset and ValidationMessage (#215)
Browse files Browse the repository at this point in the history
Refactors the invalid-feedback div out of asInput into its own component.

To address PR feedback, Fieldset and ValidationMessage now use Variant and have
a less confusing prop API than asInput.
  • Loading branch information
thallada authored Apr 24, 2018
1 parent b0ab0eb commit ae9dd7e
Show file tree
Hide file tree
Showing 15 changed files with 9,219 additions and 2,971 deletions.
11,457 changes: 8,545 additions & 2,912 deletions .storybook/__snapshots__/Storyshots.test.js.snap

Large diffs are not rendered by default.

72 changes: 58 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"@storybook/addon-actions": "^3.3.10",
"@storybook/addon-centered": "^3.3.13",
"@storybook/addon-console": "^1.0.0",
"@storybook/addon-info": "^3.3.12",
"@storybook/addon-info": "^3.3.14",
"@storybook/addon-options": "^3.3.10",
"@storybook/addon-storyshots": "^3.3.10",
"@storybook/addons": "^3.3.10",
Expand Down
18 changes: 18 additions & 0 deletions src/Fieldset/Fieldset.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@import "~bootstrap/scss/_forms";
@import '~bootstrap/scss/utilities/_borders';
@import "~bootstrap/scss/utilities/_screenreaders.scss";
@import '~bootstrap/scss/utilities/_spacing';

.paragon-fieldset {
@extend .mb-4;

fieldset legend {
width: auto;
margin-bottom: 0;
}
}

.form-control.is-invalid.is-invalid-nodanger {
@include form-control-focus();
border-color: $input-border-color;
}
144 changes: 144 additions & 0 deletions src/Fieldset/Fieldset.stories.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import centered from '@storybook/addon-centered';
import { checkA11y } from '@storybook/addon-a11y';
import { withInfo } from '@storybook/addon-info';

import Fieldset from './index';
import InputText from '../InputText/index';
import README from './README.md';
import Variant from '../utils/constants';


class ValidatedForm extends React.Component {
constructor(props) {
super(props);
this.firstInputRef = null;
this.secondInputRef = null;

this.state = {
isValid: true,
};

this.handleSubmit = this.handleSubmit.bind(this);
}

handleSubmit(event) {
if (this.firstInputRef.value.length === 0 && this.secondInputRef.value.length === 0) {
this.setState({
isValid: false,
});
} else {
this.setState({
isValid: true,
});
}
event.preventDefault();
}

render() {
return (
<form
onSubmit={this.handleSubmit}
>
<Fieldset
legend="Name"
invalidMessage="Please enter at least one name."
isValid={this.state.isValid}
variant={{
status: Variant.status.DANGER,
}}
variantIconDescription="Error"
>
<InputText
name="firstName"
label="First Name"
value=""
inputRef={(ref) => { this.firstInputRef = ref; }}
/>
<InputText
name="lastName"
label="Last Name"
value=""
inputRef={(ref) => { this.secondInputRef = ref; }}
/>
</Fieldset>
<input
type="submit"
className="btn btn-primary"
value="Submit"
/>
</form>
);
}
}

storiesOf('Fieldset', module)
.addDecorator((story, context) => withInfo({}, README)(story)(context))
.addDecorator(centered)
.addDecorator(checkA11y)
.add('basic usage', () => (
<form>
<Fieldset
legend="Name"
>
<InputText
name="firstName"
label="First Name"
value=""
/>
<InputText
name="lastName"
label="Last Name"
value=""
/>
</Fieldset>
</form>
))
.add('invalid', () => (
<form>
<Fieldset
legend="Name"
invalidMessage="This is invalid!"
isValid={false}
>
<InputText
name="firstName"
label="First Name"
value=""
/>
<InputText
name="lastName"
label="Last Name"
value=""
/>
</Fieldset>
</form>
))
.add('invalid with danger theme', () => (
<form>
<Fieldset
legend="Name"
invalidMessage="This is invalid!"
isValid={false}
variant={{
status: Variant.status.DANGER,
}}
variantIconDescription="Error"
>
<InputText
name="firstName"
label="First Name"
value=""
/>
<InputText
name="lastName"
label="Last Name"
value=""
/>
</Fieldset>
</form>
))
.add('client-side validation of fieldset', () => (
<ValidatedForm />
));
105 changes: 105 additions & 0 deletions src/Fieldset/Fieldset.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React from 'react';
import { mount } from 'enzyme';

import Fieldset from './index';
import newId from '../utils/newId';
import ValidationMessage from '../ValidationMessage';
import Variant from '../utils/constants';

const dangerVariant = {
status: Variant.status.DANGER,
};
const id = 'input1';
const legend = 'A Fieldset';
const invalidMessage = 'This is invalid!';
const children = 'Input goes here';
const variant = {
status: Variant.status.INFO,
};
const variantIconDescription = 'Error';

const baseProps = {
className: '',
id,
isValid: true,
legend,
invalidMessage,
variant,
variantIconDescription,
};

const mockedNextId = 'fieldset1';

// Cannot reference variables inside of a jest mock function: https://github.com/facebook/jest/issues/2567
jest.mock('../utils/newId', () => jest.fn().mockReturnValue('fieldset1'));

describe('Fieldset', () => {
let wrapper;

beforeEach(() => {
const props = {
...baseProps,
};
wrapper = mount(<Fieldset {...props}>{children}</Fieldset>);
});
it('renders', () => {
const fieldset = wrapper.find('fieldset.form-control');
expect(fieldset.exists()).toEqual(true);
expect(fieldset.hasClass('is-invalid-nodanger')).toEqual(true);
expect(fieldset.prop('aria-describedby')).toEqual(`error-${id}`);
const legendElem = fieldset.find('legend');
expect(legendElem.text()).toEqual(legend);
expect(fieldset.text()).toEqual(legend + children);
const feedback = wrapper.find(ValidationMessage);
expect(feedback.prop('id')).toEqual(`error-${id}`);
});
it('renders with auto-generated id if not specified', () => {
const props = {
...baseProps,
id: undefined,
};
wrapper = mount(<Fieldset {...props} />);
const feedback = wrapper.find(ValidationMessage);
expect(feedback.prop('id')).toEqual('error-fieldset1');
});
it('renders invalidMessage when isValid is false', () => {
wrapper.setProps({ isValid: false });
const feedback = wrapper.find(ValidationMessage);
expect(feedback.prop('invalidMessage')).toEqual(invalidMessage);
});
it('renders with danger variant when isValid is false and variant is DANGER', () => {
wrapper.setProps({ isValid: false, variant: dangerVariant });
const feedback = wrapper.find(ValidationMessage);
expect(feedback.hasClass('invalid-feedback-nodanger')).toEqual(false);
expect(feedback.prop('variantIconDescription')).toEqual(variantIconDescription);
expect(feedback.prop('invalidMessage')).toEqual(invalidMessage);
expect(feedback.prop('variant')).toEqual(dangerVariant);
});
it('receives new id when a valid one is passed to props', () => {
const nextId = 'new-id';
let fieldset = wrapper.find('fieldset.form-control');
let feedback = wrapper.find(ValidationMessage);
expect(fieldset.prop('aria-describedby')).toEqual(`error-${id}`);
expect(feedback.prop('id')).toEqual(`error-${id}`);

wrapper.setProps({ id: nextId });
fieldset = wrapper.find('fieldset.form-control');
feedback = wrapper.find(ValidationMessage);
expect(fieldset.prop('aria-describedby')).toEqual(`error-${nextId}`);
expect(feedback.prop('id')).toEqual(`error-${nextId}`);
});
it('auto-generates new id when an invalid one is passed to props', () => {
const nextId = '';
let fieldset = wrapper.find('fieldset.form-control');
let feedback = wrapper.find(ValidationMessage);
expect(fieldset.prop('aria-describedby')).toEqual(`error-${id}`);
expect(feedback.prop('id')).toEqual(`error-${id}`);

wrapper.setProps({ id: nextId });
expect(newId).toHaveBeenCalledWith('fieldset');
fieldset = wrapper.find('fieldset.form-control');
feedback = wrapper.find(ValidationMessage);
expect(fieldset.prop('aria-describedby')).toEqual(`error-${mockedNextId}`);
expect(feedback.prop('id')).toEqual(`error-${mockedNextId}`);
});
});
Loading

0 comments on commit ae9dd7e

Please sign in to comment.