chore: 🤖 add error messages support to fields, except radios

This commit is contained in:
Randall Morey committed 2020-05-28 14:43:11 -04:00
1 parent aee1bd4015
commit 2135b7b0e2
27 files changed
+363 -121

No files matched your search

@@ -10,58 +10,8 @@ import { text, boolean, select } from '@storybook/addon-knobs';
A component for building accessible forms. Yields contextual fields
and actions.
```htmlbars
<Rose::Form
@onSubmit={{this.submit}}
@cancel={{this.cancel}}
@disabled={{false}}
as |form|>
<form.input
@label="Label"
@helperText="Helper text"
@value="Text"
/>
<form.textarea
@label="Label"
@helperText="Helper text"
@value="Text"
/>
<form.select
@label="Label"
@helperText="Helper text"
@onChange={{fn this.actionHandler}}
as |Field|
>
<Field as |select|>
<select.option>Choose an option</select.option>
<select.option @value="value-1">Value 1</select.option>
<select.option @value="value-2">Value 2</select.option>
<select.option @value="value-3">Value 3</select.option>
</Field>
</form.select>
<form.radioGroup @name="color" @selectedValue="green" as |radioGroup|>
<radioGroup.radio
@label="Red"
@value="red"
/>
<radioGroup.radio
@label="Green"
@value="green"
/>
<radioGroup.radio
@label="Blue"
@value="blue"
/>
</form.radioGroup>
<form.checkbox
@label="Label"
/>
<form.actions @submitText="Save" @cancelText="Cancel" @showCancel={{true}} />
</Rose::Form>
```
<Preview>
<Story name="Form">{{
<Story name="Form" height="35rem">{{
template: hbs`
<Rose::Form
@onSubmit={{submit}}
@@ -82,14 +32,14 @@ and actions.
@label="Label"
@helperText="Helper text"
@onChange={{selectChange}}
as |Field|
as |field|
>
<Field as |select|>
<field.field as |select|>
<select.option>Choose an option</select.option>
<select.option @value="value-1">Value 1</select.option>
<select.option @value="value-2">Value 2</select.option>
<select.option @value="value-3">Value 3</select.option>
</Field>
</field.field>
</form.select>
<form.radioGroup @name="color" @selectedValue="green" as |radioGroup|>
<radioGroup.radio
@@ -120,3 +70,54 @@ and actions.
},
}}</Story>
</Preview>
```htmlbars
<Rose::Form
@onSubmit={{this.submit}}
@cancel={{this.cancel}}
@disabled={{false}}
as |form|>
<form.input
@label="Label"
@helperText="Helper text"
@value="Text"
/>
<form.textarea
@label="Label"
@helperText="Helper text"
@value="Text"
/>
<form.select
@label="Label"
@helperText="Helper text"
@onChange={{fn this.actionHandler}}
as |field|
>
<field.field as |select|>
<select.option>Choose an option</select.option>
<select.option @value="value-1">Value 1</select.option>
<select.option @value="value-2">Value 2</select.option>
<select.option @value="value-3">Value 3</select.option>
</field.field>
</form.select>
<form.radioGroup @name="color" @selectedValue="green" as |radioGroup|>
<radioGroup.radio
@label="Red"
@value="red"
/>
<radioGroup.radio
@label="Green"
@value="green"
/>
<radioGroup.radio
@label="Blue"
@value="blue"
/>
</form.radioGroup>
<form.checkbox
@label="Label"
/>
<form.actions @submitText="Save" @cancelText="Cancel" @showCancel={{true}} />
</Rose::Form>
```
@@ -20,7 +20,7 @@ Renders a form input checkbox.
```
<Preview>
<Story name="Basic" height="10rem">{{
<Story name="Basic Checkbox" height="10rem">{{
template: hbs`
<Rose::Form::Checkbox
name={{name}}
@@ -39,3 +39,28 @@ Renders a form input checkbox.
}
}}</Story>
</Preview>
<Preview>
<Story name="Checkbox with errors" height="10rem">{{
template: hbs`
<Rose::Form::Checkbox
name={{name}}
@label={{label}}
@checked={{checked}}
@error={{true}}
@disabled={{disabled}}
as |field|
>
<field.errors as |errors|>
<errors.message>An error occurred.</errors.message>
</field.errors>
</Rose::Form::Checkbox>
`,
context: {
label: text('text', 'Checkbox 🛎'),
name: text('name', 'checkbox-1'),
checked: boolean('checked', false),
disabled: boolean('disabled', false),
}
}}</Story>
</Preview>
@@ -6,5 +6,5 @@ import layout from '../../../templates/components/rose/form/errors';
*/
export default Component.extend({
layout,
tagName: ''
tagName: '',
});
@@ -6,5 +6,5 @@ import layout from '../../../../templates/components/rose/form/errors/message';
*/
export default Component.extend({
layout,
tagName: ''
tagName: '',
});
@@ -22,14 +22,14 @@ Renders a simple form input.
```
<Preview>
<Story name="Basic">{{
<Story name="Basic Field">{{
template: hbs`
<Rose::Form::Input
@type={{type}}
@value={{value}}
@label={{label}}
@helperText={{helperText}}
@error={{error}}
@error={{false}}
@disabled={{disabled}}
readonly={{read-only}}
/>
@@ -50,7 +50,44 @@ Renders a simple form input.
helperText: text('helperText', 'Helper text'),
disabled: boolean('disabled', false),
'read-only': boolean('readonly', false),
error: boolean('error', false),
}
}}</Story>
</Preview>
<Preview>
<Story name="Field with Errors">{{
template: hbs`
<Rose::Form::Input
@type={{type}}
@value={{value}}
@label={{label}}
@helperText={{helperText}}
@error={{true}}
@disabled={{disabled}}
readonly={{read-only}}
as |field|
>
<field.errors as |errors|>
<errors.message>An error occurred.</errors.message>
</field.errors>
</Rose::Form::Input>
`,
context: {
type: select(
'type',
{
text: 'text',
email: 'email',
number: 'number',
password: 'password',
},
'text'
),
value: text('value', 'Value'),
label: text('label', 'Label'),
helperText: text('helperText', 'Helper text'),
disabled: boolean('disabled', false),
'read-only': boolean('readonly', false),
}
}}</Story>
</Preview>
@@ -17,14 +17,14 @@ Renders a form select element.
@helperText="Set your desired color"
@disabled={{false}}
@error={{false}}
as |Field|
as |field|
>
<Field as |select|>
<field.field as |select|>
<select.option>Choose a color</select.option>
<select.option @value="red">Red</select.option>
<select.option @value="green">Green</select.option>
<select.option @value="blue">Blue</select.option>
</Field>
</field.field>
</Rose::Form::Select>
```
@@ -37,16 +37,60 @@ Renders a form select element.
@label={{label}}
@helperText={{helperText}}
@disabled={{disabled}}
@error={{error}}
@error={{false}}
@onChange={{fn onChange}}
as |Field|
as |field|
>
<Field as |select|>
<field.field as |select|>
<select.option>Choose an option</select.option>
<select.option @value="value-1">Value 1</select.option>
<select.option @value="value-2">Value 2</select.option>
<select.option @value="value-3">Value 3</select.option>
</Field>
</field.field>
</Rose::Form::Select>
`,
context: {
value: select(
'value',
{
null: null,
'value-1': 'value-1',
'value-2': 'value-2',
'value-3': 'value-3',
},
null
),
name: text('name', 'select-name'),
label: text('label', 'Label'),
helperText: text('helperText', 'Helper text'),
disabled: boolean('disabled', false),
onChange: action('changed'),
}
}}</Story>
</Preview>
<Preview>
<Story name="Select with Errors" height="10rem">{{
template: hbs`
<Rose::Form::Select
@name={{name}}
@value={{value}}
@label={{label}}
@helperText={{helperText}}
@disabled={{disabled}}
@error={{true}}
@onChange={{fn onChange}}
as |field|
>
<field.field as |select|>
<select.option>Choose an option</select.option>
<select.option @value="value-1">Value 1</select.option>
<select.option @value="value-2">Value 2</select.option>
<select.option @value="value-3">Value 3</select.option>
</field.field>
<field.errors as |errors|>
<errors.message>An error occurred.</errors.message>
</field.errors>
</Rose::Form::Select>
`,
context: {
@@ -64,7 +108,6 @@ Renders a form select element.
label: text('label', 'Label'),
helperText: text('helperText', 'Helper text'),
disabled: boolean('disabled', false),
error: boolean('error', false),
onChange: action('changed'),
}
}}</Story>
@@ -21,13 +21,13 @@ Renders a simple form textarea.
```
<Preview>
<Story name="Basic" height="10rem">{{
<Story name="Basic Textarea" height="10rem">{{
template: hbs`
<Rose::Form::Textarea
@value={{value}}
@label={{label}}
@helperText={{helperText}}
@error={{error}}
@error={{false}}
@disabled={{disabled}}
readonly={{read-only}}
/>
@@ -38,7 +38,32 @@ Renders a simple form textarea.
helperText: text('helperText', 'Helper text'),
disabled: boolean('disabled', false),
'read-only': boolean('readonly', false),
error: boolean('error', false),
}
}}</Story>
</Preview>
<Preview>
<Story name="Textarea with Errors" height="10rem">{{
template: hbs`
<Rose::Form::Textarea
@value={{value}}
@label={{label}}
@helperText={{helperText}}
@error={{true}}
@disabled={{disabled}}
readonly={{read-only}}
as |field|>
<field.errors as |errors|>
<errors.message>An error occurred.</errors.message>
</field.errors>
</Rose::Form::Textarea>
`,
context: {
value: text('value', 'Value'),
label: text('label', 'Label'),
helperText: text('helperText', 'Helper text'),
disabled: boolean('disabled', false),
'read-only': boolean('readonly', false),
}
}}</Story>
</Preview>
@@ -6,6 +6,11 @@
margin-bottom: sizing.rems(xs);
color: var(--ui-gray);
.rose-form-errors {
margin-top: sizing.rems(xs);
margin-bottom: sizing.rems(xs);
}
.rose-form-checkbox-label {
// label styles
@include type.type(s, semibold);
@@ -19,6 +19,7 @@
.rose-form-input-field {
@include type.type(m);
margin-bottom: sizing.rems(xs);
padding: sizing.rems(xs);
color: var(--stark);
background-color: var(--subtle);
@@ -24,6 +24,7 @@
appearance: none;
vertical-align: middle;
display: inline-block;
margin-bottom: sizing.rems(xs);
padding: sizing.rems(xs) sizing.rems(xxl) sizing.rems(xs) sizing.rems(xs);
border: sizing.rems(xxxxs) solid var(--ui-gray-subtler-2);
border-radius: sizing.rems(xxxs);
@@ -1,6 +1,6 @@
@use '../../../variables/sizing';
@import "message";
@import 'message';
.rose-form-errors {
margin-bottom: sizing.rems(l);
@@ -6,12 +6,15 @@
id={{this.id}}
disabled={{@disabled}}
@type="checkbox"
@checked={{if @checked true false}} />
@checked={{if @checked true false}}
aria-describedby="{{if @error (concat 'errors-' this.id)}}" />
<label class="rose-form-checkbox-label" for={{this.id}}>
{{@label}}
</label>
{{yield}}
{{yield (hash
errors=(component 'rose/form/errors' id=(concat 'errors-' this.id))
)}}
</div>
@@ -1,4 +1,4 @@
<div class="rose-form-errors" ...attributes>
<div class="rose-form-errors" id={{@id}} ...attributes>
{{yield (hash
message=(component 'rose/form/errors/message')
)}}
@@ -19,8 +19,10 @@
disabled={{@disabled}}
@type={{@type}}
@value={{@value}}
aria-describedby="{{if @helperText (concat 'helper-text-' this.id)}}" />
aria-describedby="{{if @helperText (concat 'helper-text-' this.id)}} {{if @error (concat 'errors-' this.id)}}" />
{{yield}}
{{yield (hash
errors=(component 'rose/form/errors' id=(concat 'errors-' this.id))
)}}
</div>
@@ -12,13 +12,17 @@
</span>
{{/if}}
{{yield (component 'rose/form/select/select'
id=this.id
name=@name
disabled=@disabled
value=@value
onChange=@onChange
helperText=@helperText
{{yield (hash
field=(component 'rose/form/select/select'
id=this.id
name=@name
disabled=@disabled
value=@value
onChange=@onChange
helperText=@helperText
error=@error
)
errors=(component 'rose/form/errors' id=(concat 'errors-' this.id))
)}}
</div>
@@ -5,7 +5,7 @@
disabled={{@disabled}}
@value={{@value}}
@onChange={{@onChange}}
aria-describedby="{{if @helperText (concat 'helper-text-' this.id)}}"
aria-describedby="{{if @helperText (concat 'helper-text-' this.id)}} {{if @error (concat 'errors-' this.id)}}"
as |xs|
>
{{yield xs}}
@@ -18,8 +18,10 @@
id={{this.id}}
disabled={{@disabled}}
@value={{@value}}
aria-describedby="{{if @helperText (concat 'helper-text-' this.id)}}" />
aria-describedby="{{if @helperText (concat 'helper-text-' this.id)}} {{if @error (concat 'errors-' this.id)}}" />
{{yield}}
{{yield (hash
errors=(component 'rose/form/errors' id=(concat 'errors-' this.id))
)}}
</div>
@@ -1 +1 @@
export { default } from 'rose/components/rose/form/errors';
export { default } from 'rose/components/rose/form/errors';
@@ -1 +1 @@
export { default } from 'rose/components/rose/form/errors/message';
export { default } from 'rose/components/rose/form/errors/message';
@@ -107,25 +107,25 @@ Dummy app index template
@label="Label"
@helperText="Helper text"
@onChange={{fn this.actionHandler}}
as |Field|
as |field|
>
<Field as |select|>
<field.field as |select|>
<select.option>Choose an option</select.option>
<select.option @value="value-1">Value 1</select.option>
<select.option @value="value-2">Value 2</select.option>
<select.option @value="value-3">Value 3</select.option>
</Field>
</field.field>
</form.select>
<form.select
@label="Label"
@helperText="Helper text"
@onChange={{fn this.actionHandler}}
as |Field|
as |field|
>
<Field as |select|>
<field.field as |select|>
<select.option>Choose an option</select.option>
</Field>
</field.field>
</form.select>
@@ -45,9 +45,9 @@ module('Integration | Component | rose/form', function (hooks) {
@label="Select"
@name="select-field"
@onChange={{this.select}}
as |Field|
as |field|
>
<Field />
<field.field />
</form.select>
<form.checkbox @label="Checkbox" name="checkbox-field" />
<form.radioGroup @name="radio-group-field" @selectedValue="green" as |radioGroup|>
@@ -12,6 +12,23 @@ module('Integration | Component | rose/form/checkbox', function (hooks) {
assert.ok(await find('input'));
});
test('it displays optional errors', async function (assert) {
await render(hbs`
<Rose::Form::Checkbox @error={{true}} as |field|>
<field.errors as |errors|>
<errors.message>An error occurred.</errors.message>
</field.errors>
</Rose::Form::Checkbox>
`);
const fieldEl = find('input');
const id = fieldEl.id;
const errorsId = `errors-${id}`;
const errorMessageEl = find('.rose-form-errors');
assert.equal(errorMessageEl.id, errorsId);
assert.equal(errorMessageEl.textContent.trim(), 'An error occurred.');
assert.equal(fieldEl.getAttribute('aria-describedby').trim(), errorsId);
});
test('it is not checked by default', async function (assert) {
await render(hbs`<Rose::Form::Checkbox />`);
assert.equal(await find('input').checked, false);
@@ -3,10 +3,10 @@ import { setupRenderingTest } from 'ember-qunit';
import { render, find } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | rose/form/errors', function(hooks) {
module('Integration | Component | rose/form/errors', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function(assert) {
test('it renders', async function (assert) {
assert.expect(3);
await render(hbs`
<Rose::Form::Errors as |errors|>
@@ -15,7 +15,13 @@ module('Integration | Component | rose/form/errors', function(hooks) {
<//Rose::Form::Errors>
`);
assert.ok(find('.rose-form-errors'));
assert.equal(find('.rose-form-error-message:first-child').textContent.trim(), 'An error occurred.');
assert.equal(find('.rose-form-error-message:last-child').textContent.trim(), 'Another error occurred.');
assert.equal(
find('.rose-form-error-message:first-child').textContent.trim(),
'An error occurred.'
);
assert.equal(
find('.rose-form-error-message:last-child').textContent.trim(),
'Another error occurred.'
);
});
});
@@ -3,10 +3,10 @@ import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | rose/form/errors/message', function(hooks) {
module('Integration | Component | rose/form/errors/message', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function(assert) {
test('it renders', async function (assert) {
await render(hbs`
<Rose::Form::Errors::Message>
An error occurred.
@@ -20,7 +20,27 @@ module('Integration | Component | rose/form/input', function (hooks) {
const helperTextEl = find('.rose-form-input-helper-text');
assert.equal(helperTextEl.textContent.trim(), 'Help me');
assert.equal(helperTextEl.id, helperId);
assert.equal(fieldEl.getAttribute('aria-describedby'), helperId);
assert.equal(fieldEl.getAttribute('aria-describedby').trim(), helperId);
});
test('it displays optional errors', async function (assert) {
await render(hbs`
<Rose::Form::Input @error={{true}} as |field|>
<field.errors as |errors|>
<errors.message>An error occurred.</errors.message>
</field.errors>
</Rose::Form::Input>
`);
const fieldEl = find('input');
const id = fieldEl.id;
const errorsId = `errors-${id}`;
const errorMessageEl = find('.rose-form-errors');
assert.equal(errorMessageEl.id, errorsId);
assert.equal(errorMessageEl.textContent.trim(), 'An error occurred.');
assert.equal(
fieldEl.getAttribute('aria-describedby').split(' ')[1],
errorsId
);
});
test('it is type="text" by default', async function (assert) {
@@ -12,14 +12,14 @@ module('Integration | Component | rose/form/select', function (hooks) {
<Rose::Form::Select
@label="Label"
@onChange={{fn this.onChange}}
as |Field|
as |field|
>
<Field as |select|>
<field.field as |select|>
<select.option>Choose an option</select.option>
<select.option @value="value-1">Value 1</select.option>
<select.option @value="value-2">Value 2</select.option>
<select.option @value="value-3">Value 3</select.option>
</Field>
</field.field>
</Rose::Form::Select>
`);
assert.equal(find('label').textContent.trim(), 'Label');
@@ -34,11 +34,11 @@ module('Integration | Component | rose/form/select', function (hooks) {
@label="Label"
@helperText="Help me"
@onChange={{fn this.onChange}}
as |Field|
as |field|
>
<Field as |select|>
<field.field as |select|>
<select.option>Choose an option</select.option>
</Field>
</field.field>
</Rose::Form::Select>
`);
const fieldEl = find('select');
@@ -47,7 +47,37 @@ module('Integration | Component | rose/form/select', function (hooks) {
const helperTextEl = find('.rose-form-select-helper-text');
assert.equal(helperTextEl.textContent.trim(), 'Help me');
assert.equal(helperTextEl.id, helperId);
assert.equal(fieldEl.getAttribute('aria-describedby'), helperId);
assert.equal(fieldEl.getAttribute('aria-describedby').trim(), helperId);
});
test('it displays optional errors', async function (assert) {
this.onChange = () => {};
await render(hbs`
<Rose::Form::Select
@label="Label"
@helperText="Help me"
@onChange={{fn this.onChange}}
@error={{true}}
as |field|
>
<field.field as |select|>
<select.option>Choose an option</select.option>
</field.field>
<field.errors as |errors|>
<errors.message>An error occurred.</errors.message>
</field.errors>
</Rose::Form::Select>
`);
const fieldEl = find('select');
const id = fieldEl.id;
const errorsId = `errors-${id}`;
const errorMessageEl = find('.rose-form-errors');
assert.equal(errorMessageEl.id, errorsId);
assert.equal(errorMessageEl.textContent.trim(), 'An error occurred.');
assert.equal(
fieldEl.getAttribute('aria-describedby').split(' ')[1],
errorsId
);
});
test('it supports disabled attribute', async function (assert) {
@@ -56,11 +86,11 @@ module('Integration | Component | rose/form/select', function (hooks) {
<Rose::Form::Select
@onChange={{fn this.onChange}}
@disabled={{true}}
as |Field|
as |field|
>
<Field as |select|>
<field.field as |select|>
<select.option>Choose an option</select.option>
</Field>
</field.field>
</Rose::Form::Select>
`);
assert.ok(find('[disabled]'));
@@ -73,12 +103,12 @@ module('Integration | Component | rose/form/select', function (hooks) {
<Rose::Form::Select
@onChange={{fn this.onChange}}
@value={{value}}
as |Field|
as |field|
>
<Field as |select|>
<field.field as |select|>
<select.option @value="value-1">value-1</select.option>
<select.option @value="value-2">value-2</select.option>
</Field>
</field.field>
</Rose::Form::Select>
`);
assert.equal(find('select').value, 'value-1');
@@ -104,12 +134,12 @@ module('Integration | Component | rose/form/select', function (hooks) {
<Rose::Form::Select
@name="my-select"
@onChange={{fn this.onChange}}
as |Field|
as |field|
>
<Field as |select|>
<field.field as |select|>
<select.option>Choose an option</select.option>
<select.option @value="value-1">value-1</select.option>
</Field>
</field.field>
</Rose::Form::Select>
`);
await fillIn('[name="my-select"]', 'value-1');
@@ -20,7 +20,27 @@ module('Integration | Component | rose/form/textarea', function (hooks) {
const helperTextEl = find('.rose-form-input-helper-text');
assert.equal(helperTextEl.textContent.trim(), 'Help me');
assert.equal(helperTextEl.id, helperId);
assert.equal(fieldEl.getAttribute('aria-describedby'), helperId);
assert.equal(fieldEl.getAttribute('aria-describedby').trim(), helperId);
});
test('it displays optional errors', async function (assert) {
await render(hbs`
<Rose::Form::Textarea @error={{true}} as |field|>
<field.errors as |errors|>
<errors.message>An error occurred.</errors.message>
</field.errors>
</Rose::Form::Textarea>
`);
const fieldEl = find('textarea');
const id = fieldEl.id;
const errorsId = `errors-${id}`;
const errorMessageEl = find('.rose-form-errors');
assert.equal(errorMessageEl.id, errorsId);
assert.equal(errorMessageEl.textContent.trim(), 'An error occurred.');
assert.equal(
fieldEl.getAttribute('aria-describedby').split(' ')[1],
errorsId
);
});
test('it supports disabled attribute', async function (assert) {