Merge branch 'develop' into ICU-177-master-layout

This commit is contained in:
Randall Morey authored and GitHub committed 2020-05-29 15:08:16 -04:00
commit f5ec8f2cba
70 files changed
+1487 -438

No files matched your search

+2 -1
View File
@@ -21,7 +21,8 @@ module.exports = {
browser: true
},
rules: {
'ember/no-jquery': 'error'
'ember/no-jquery': 'error',
'ember/no-new-mixins': 0
},
overrides: [
// node files
+27
View File
@@ -0,0 +1,27 @@
import Component from '@ember/component';
import layout from '../../templates/components/rose/form';
import { action } from '@ember/object';
/**
* A component for building accessible forms. Yields contextual fields
* and actions.
*/
export default Component.extend({
layout,
tagName: '',
// =actions
/**
* Calls the passed `onSubmit` function and disabled the default form
* submit behavior.
* @param {Event} e
*/
@action
handleSubmit(e) {
e.preventDefault();
e.stopPropagation();
this.onSubmit();
return false;
},
});
@@ -0,0 +1,123 @@
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { hbs } from 'ember-cli-htmlbars';
import { action } from '@storybook/addon-actions';
import { text, boolean, select } from '@storybook/addon-knobs';
<Meta title="Rose|Form" component="Form" />
# Form
A component for building accessible forms. Yields contextual fields
and actions.
<Preview>
<Story name="Form" height="35rem">{{
template: hbs`
<Rose::Form
@onSubmit={{submit}}
@cancel={{cancel}}
@disabled={{disabled}}
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={{selectChange}}
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"
@checked={{true}}
/>
<form.actions @submitText="Save" @cancelText="Cancel" @showCancel={{true}} />
</Rose::Form>
`,
context: {
disabled: boolean('disabled', false),
submit: action('submit'),
cancel: action('cancel'),
selectChange: action('selectChange'),
},
}}</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>
```
@@ -0,0 +1,24 @@
import Component from '@ember/component';
import layout from '../../../templates/components/rose/form/actions';
/**
* A set of two form action buttons for submit and cancel.
* To handle submit, place a submit handler on the parent form element.
* To handle cancel button click, pass in a function to `@cancel`.
*
* @example
* <Rose::Form::Actions
* @submitText="Save"
* @cancelText="Cancel"
* @showCancel={{true}}
* @cancel=(fn @cancel) />
*/
export default Component.extend({
layout,
tagName: '',
/**
* @type {boolean}
*/
showCancel: true,
});
@@ -0,0 +1,42 @@
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { hbs } from 'ember-cli-htmlbars';
import { action } from '@storybook/addon-actions';
import { text, boolean } from '@storybook/addon-knobs';
<Meta title="Rose|Form/Actions" component="Form/Actions" />
# Form Actions
A simple collection of buttons for performing common actions like submit/save
and cancel. Form actions should always be used in the context of a form element
where the form has a submit action handler. The submit button does not directly
support actions. The cancel button supports a cancel action.
```html
<Rose::Form::Actions
@submitText="Save"
@cancelText="Cancel"
@showCancel={{true}}
@disabled={{false}}
@cancel=(fn @cancel) />
```
<Preview>
<Story name="Basic Form Actions">{{
template: hbs`
<Rose::Form::Actions
@submitText={{submitText}}
@cancelText={{cancelText}}
@showCancel={{showCancel}}
@disabled={{disabled}}
@cancel={{onCancel}} />
`,
context: {
submitText: text('submitText', 'Save'),
cancelText: text('cancelText', 'Cancel'),
showCancel: boolean('showCancel', true),
disabled: boolean('disabled', false),
onCancel: action('cancel'),
},
}}</Story>
</Preview>
@@ -1,21 +1,11 @@
import Component from '@ember/component';
import layout from '../../../templates/components/rose/form/checkbox';
import ComponentAutoId from '../../../mixins/component-auto-id';
/**
* A simple HTML `<input type="checkbox">` element.
*
* @example
* <Rose::Form::Checkbox
* @label="Label"
* @id="fieldId"
* name="fieldName"
* disabled={{true}}
* @error={{true}}
* @checked={{true}}
* />
*/
export default Component.extend({
export default Component.extend(ComponentAutoId, {
layout,
tagName: '',
});
@@ -11,30 +11,27 @@ Renders a form input checkbox.
```htmlbars
<Rose::Form::Checkbox
@id="form-field-1"
name="boolean-value"
@label="Yes?"
@checked={{false}}
@error={{false}}
disabled={{false}}
@disabled={{false}}
/>
```
<Preview>
<Story name="Basic" height="10rem">{{
<Story name="Basic Checkbox" height="10rem">{{
template: hbs`
<Rose::Form::Checkbox
@id={{id}}
name={{name}}
@label={{label}}
@checked={{checked}}
@error={{error}}
disabled={{disabled}}
@disabled={{disabled}}
/>
`,
context: {
label: text('text', 'Checkbox 🛎'),
id: text('id', 'checkbox-1'),
name: text('name', 'checkbox-1'),
checked: boolean('checked', false),
error: boolean('error', false),
@@ -42,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>
@@ -0,0 +1,10 @@
import Component from '@ember/component';
import layout from '../../../templates/components/rose/form/errors';
/**
* A container for error messages. Yields the error message component.
*/
export default Component.extend({
layout,
tagName: '',
});
@@ -0,0 +1,29 @@
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { hbs } from 'ember-cli-htmlbars';
import { action } from '@storybook/addon-actions';
import { text, boolean } from '@storybook/addon-knobs';
<Meta title="Rose|Form/Errors" component="Form/Errors" />
# Form Errors
Use this component to display one or more error messages.
```html
<Rose::Form::Errors as |errors|>
<errors.message>An error message goes here.</errors.message>
<errors.message>Another message goes here.</errors.message>
</Rose::Form::Errors>
```
<Preview>
<Story name="Basic Form Errors">{{
template: hbs`
<Rose::Form::Errors as |errors|>
<errors.message>An error message goes here.</errors.message>
<errors.message>Another message goes here.</errors.message>
</Rose::Form::Errors>
`,
context: {},
}}</Story>
</Preview>
@@ -0,0 +1,10 @@
import Component from '@ember/component';
import layout from '../../../../templates/components/rose/form/errors/message';
/**
* A simple, styled error message. Do not use directly.
*/
export default Component.extend({
layout,
tagName: '',
});
@@ -0,0 +1,29 @@
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { hbs } from 'ember-cli-htmlbars';
import { action } from '@storybook/addon-actions';
import { text, boolean } from '@storybook/addon-knobs';
<Meta title="Rose|Form/Errors/Message" component="Form/Errors/Message" />
# Form Error Message
This component accepts block content describing a single error to the user.
Normally this component would not be used directly, but rather in the context
of the `Rose::Form::Errors` component.
```html
<Rose::Form::Errors::Message>
An error message goes here.
</Rose::Form::Errors::Message>
```
<Preview>
<Story name="Basic Form Error Message">{{
template: hbs`
<Rose::Form::Errors::Message>
An error message goes here.
</Rose::Form::Errors::Message>
`,
context: {},
}}</Story>
</Preview>
@@ -1,22 +1,11 @@
import Component from '@ember/component';
import layout from '../../../templates/components/rose/form/input';
import ComponentAutoId from '../../../mixins/component-auto-id';
/**
* A simple HTML `<input>` element.
*
* @example
* <Rose::Form::Input
* @id="fieldId"
* @type="text"
* @value="Text"
* @label="Label"
* @helperText="Helper text"
* @error={{true}}
* disabled={{true}}
* readonly={{true}}
* />
*/
export default Component.extend({
export default Component.extend(ComponentAutoId, {
layout,
tagName: '',
});
@@ -16,21 +16,21 @@ Renders a simple form input.
@label="Email"
@helperText="Enter your email"
@error={{false}}
disabled={{false}}
@disabled={{false}}
readonly={{false}}
/>
```
<Preview>
<Story name="Basic">{{
<Story name="Basic Field">{{
template: hbs`
<Rose::Form::Input
@type={{type}}
@value={{value}}
@label={{label}}
@helperText={{helperText}}
@error={{error}}
disabled={{disabled}}
@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>
@@ -3,29 +3,8 @@ import layout from '../../../../templates/components/rose/form/radio/group';
/**
* A `<input type="radio" name="radio-group">` element.
*
* @example
* <Rose::Form::Radio::Group @name="color" @selectedValue="green" as |radioGroup|>
* <radioGroup.radio
* @id="red-radio"
* @label="Red"
* @value="red"
* />
* <radioGroup.radio
* @id="green-radio"
* @label="Green"
* @value="green"
* />
* <radioGroup.radio
* @id="blue-radio"
* @label="Blue"
* @value="blue"
* />
* </Rose::Form::Radio::Group>
*/
export default Component.extend({
layout,
tagName: '',
});
@@ -1,6 +1,6 @@
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { hbs } from 'ember-cli-htmlbars';
import { text } from '@storybook/addon-knobs';
import { text, boolean } from '@storybook/addon-knobs';
<Meta title="Rose|Form/Radio/Group" component="Form/Radio/Group" />
@@ -9,19 +9,21 @@ import { text } from '@storybook/addon-knobs';
Renders a form radiofield group.
```htmlbars
<Rose::Form::Radio::Group @name={{name}} @selectedValue="disagree" as |radioGroup|>
<Rose::Form::Radio::Group
@disabled={{false}}
@name={{name}}
@selectedValue="disagree"
as |radioGroup|
>
<radioGroup.radio
@id="agree-radiofield"
@label="Agree"
@value="agree"
/>
<radioGroup.radio
@id="disagree-radiofield"
@label="disagree"
@value="disagree"
/>
<radioGroup.radio
@id="maybe-radiofield"
@label="maybe"
@value="maybe"
/>
@@ -30,19 +32,21 @@ Renders a form radiofield group.
<Preview>
<Story name="Basic" height="10rem">{{
template: hbs`
<Rose::Form::Radio::Group @name={{name}} @selectedValue={{selectedValue}} as |radioGroup|>
<Rose::Form::Radio::Group
@disabled={{disabled}}
@name={{name}}
@selectedValue={{selectedValue}}
as |radioGroup|
>
<radioGroup.radio
@id="firstField"
@label={{firstFieldLabel}}
@value={{firstFieldValue}}
/>
<radioGroup.radio
@id="secondField"
@label={{secondFieldLabel}}
@value={{secondFieldValue}}
/>
<radioGroup.radio
@id="thirdField"
@label={{thirdFieldLabel}}
@value={{thirdFieldValue}}
/>
@@ -50,6 +54,7 @@ Renders a form radiofield group.
`,
context: {
name: text('name', 'mustagree'),
disabled: boolean('disabled', false),
selectedValue: text('selectedValue', 'disagree'),
firstFieldLabel: text('first label', 'Agree'),
firstFieldValue: text('first value', 'agree'),
@@ -1,22 +1,13 @@
import Component from '@ember/component';
import layout from '../../../../templates/components/rose/form/radio/radio';
import ComponentAutoId from '../../../../mixins/component-auto-id';
/**
* A `<input type="radio">` element.
* `value` and `selectedValue` must be equal to select
* radiofield.
*
* @example
* <Rose::Form::Radio::Radio
* @id="fieldId"
* @label="label"
* @error={{true}}
* @value="fieldValue"
* @selectedValue="fieldValue"
* @disabled={{true}}
* />
*/
export default Component.extend({
export default Component.extend(ComponentAutoId, {
layout,
tagName: '',
});
@@ -10,7 +10,6 @@ Renders a form radiofield.
```htmlbars
<Rose::Form::Radio::Radio
@id="yes-radiofield"
@label="Yes"
@error={{false}}
@value="yes-agree"
@@ -23,7 +22,6 @@ Renders a form radiofield.
<Story name="Basic" height="10rem">{{
template: hbs`
<Rose::Form::Radio::Radio
@id={{id}}
@label={{label}}
@error={{error}}
@value={{value}}
@@ -32,7 +30,6 @@ Renders a form radiofield.
/>
`,
context: {
id: text('id', 'radio-1'),
label: text('text', 'Radiofield'),
error: boolean('error', false),
value: text('value', 'value and selectedValue must match to select radiofield'),
@@ -1,27 +1,11 @@
import Component from '@ember/component';
import layout from '../../../templates/components/rose/form/select';
import ComponentAutoId from '../../../mixins/component-auto-id';
/**
* A `<select>` element context component which yields option components.
*
* @example
* <Rose::Form::Select
* @id="select-id-1"
* name="select-field-name"
* @value="current-value"
* @label="Label"
* @helperText="Helper text"
* @error={{true}}
* @onChange={{fn this.selectChange}}
* disabled={{true}}
* as |select|
* >
* <select.option>Choose an option</select.option>
* <select.option @value="current-value">Curret value</select.option>
* </Rose::Form::Select>
*/
export default Component.extend({
export default Component.extend(ComponentAutoId, {
layout,
tagName: '',
});
@@ -11,18 +11,20 @@ Renders a form select element.
```htmlbars
<Rose::Form::Select
@id="select-id"
@name="select-name"
@value="green"
@label="Color"
@helperText="Set your desired color"
disabled={{false}}
@disabled={{false}}
@error={{false}}
as |select|
as |field|
>
<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 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>
</Rose::Form::Select>
```
@@ -30,19 +32,21 @@ Renders a form select element.
<Story name="Basic" height="10rem">{{
template: hbs`
<Rose::Form::Select
@id={{id}}
@name={{name}}
@value={{value}}
@label={{label}}
@helperText={{helperText}}
disabled={{disabled}}
@error={{error}}
@disabled={{disabled}}
@error={{false}}
@onChange={{fn onChange}}
as |select|
as |field|
>
<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 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>
</Rose::Form::Select>
`,
context: {
@@ -56,11 +60,54 @@ Renders a form select element.
},
null
),
id: text('id', 'field-id'),
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: {
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),
error: boolean('error', false),
onChange: action('changed'),
}
}}</Story>
@@ -0,0 +1,11 @@
import Component from '@ember/component';
import layout from '../../../../templates/components/rose/form/select/select';
/**
* This component is used internally by rose/form/select and should not be
* instantiated directly.
*/
export default Component.extend({
layout,
tagName: '',
});
@@ -1,22 +1,12 @@
import Component from '@ember/component';
import layout from '../../../templates/components/rose/form/textarea';
import ComponentAutoId from '../../../mixins/component-auto-id';
/**
* A `<textarea>` element essentially identical to `Rose::Form::Input` except
* for the field element.
*
* @example
* <Rose::Form::Textarea
* @id="fieldId"
* @value="Text"
* @label="Label"
* @helperText="Helper text"
* @error={{true}}
* disabled={{true}}
* readonly={{true}}
* />
*/
export default Component.extend({
export default Component.extend(ComponentAutoId, {
layout,
tagName: '',
});
@@ -15,20 +15,20 @@ Renders a simple form textarea.
@label="Description"
@helperText="Enter a description"
@error={{false}}
disabled={{false}}
@disabled={{false}}
readonly={{false}}
/>
```
<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}}
disabled={{disabled}}
@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>
@@ -0,0 +1,38 @@
import Component from '@ember/component';
import layout from '../../templates/components/rose/notification';
import { computed } from '@ember/object';
/**
* An accessible alert notification component that displays an icon,
* a heading, and optional block content. Notifications may be static or
* dismissible. To make a notification dismissible, pass in a
* `@dismiss` function.
*/
export default Component.extend({
layout,
tagName: '',
/**
* Returns an icon name based on the notification style.
* @type {string}
*/
@computed('style')
get icon() {
let icon = 'info-circle-fill';
switch(this.style) {
case 'failure':
icon = 'cancel-square-fill';
break;
case 'success':
icon = 'check-circle-fill';
break;
case 'warning':
icon = 'alert-triangle';
break;
}
return icon;
}
});
@@ -0,0 +1,84 @@
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { hbs } from 'ember-cli-htmlbars';
import { action } from '@storybook/addon-actions';
import { text, boolean, select } from '@storybook/addon-knobs';
<Meta title="Rose|Notification" component="Notification" />
# Notification
Notifications come in four semantic styles: `info` (default), `success`,
`warning`, and `failure`. Heading text is required. Additional block content
is optional.
Notifications may be made dismissible by passing a `@dismiss` function into
the component. Dismissible notifications _must_ also have `@dismissText`.
This component does not handle dismissal though. Be sure your `@dismiss`
handler takes appropriate action and hides the notification.
```htmlbars
<Rose::Notification
@style="info"
@heading="Alert Notification"
@dismiss={{dismissHandler}}
@dismissText="Dismiss"
>
An error occurred.
</Rose::Notification>
```
<Preview>
<Story name="Basic Notification">{{
template: hbs`
<Rose::Notification
@style="info"
@heading="Notification without dismissal"
>
Some useful information.
</Rose::Notification>
`,
context: {}
}}</Story>
</Preview>
<Preview>
<Story name="Basic Notifications with Dismissal" height="30rem">{{
template: hbs`
<Rose::Notification
@style="info"
@heading="Dismissible Notification"
@dismiss={{dismiss}}
@dismissText="Dismiss"
>
Some useful information.
</Rose::Notification>
<Rose::Notification
@style="success"
@heading="Success"
@dismiss={{dismiss}}
@dismissText="Dismiss"
>
Good job!
</Rose::Notification>
<Rose::Notification
@style="warning"
@heading="Warning"
@dismiss={{dismiss}}
@dismissText="Dismiss"
>
Careful now.
</Rose::Notification>
<Rose::Notification
@style="failure"
@heading="Error"
@dismiss={{dismiss}}
@dismissText="Dismiss"
>
An error occurred.
</Rose::Notification>
`,
context: {
dismiss: action('dismiss'),
}
}}</Story>
</Preview>
@@ -0,0 +1,15 @@
import Mixin from '@ember/object/mixin';
import { v1 } from 'ember-uuid';
/**
* Add this mixin to components for an auto-generated ID.
*/
export default Mixin.create({
/**
* Generates an ID for this component and assigns it to `this.id`.
*/
init() {
this.id = `component-${v1()}`;
return this._super(...arguments);
},
});
@@ -2,3 +2,4 @@
@import 'form';
@import 'header';
@import 'layout';
@import 'notification';
@@ -0,0 +1,105 @@
@use '../variables/sizing';
@use '../utilities/type';
.rose-notification {
$s: sizing.rems(s); // 12
$xs: sizing.rems(xs); // 8
$xxs: sizing.rems(xxs); // 4
$xxxs: sizing.rems(xxxs); // 2
$xxxxs: sizing.rems(xxxxs); // 1
position: relative;
padding: sizing.rems(m);
margin-bottom: sizing.rems(m);
border: sizing.rems(xxxxs) solid;
border-radius: $xxxs;
box-shadow: 0 $xs $s rgba(var(--stark-components), var(--opacity-3)),
0 $xxs $xxs rgba(var(--stark-components), var(--opacity-4));
.rose-notification-icon {
position: relative;
top: $xxxxs;
}
.rose-notification-header {
@include type.type(m, semibold);
}
.rose-notification-body {
@include type.type(s);
// hack alert, because the icons are still hard to work with
margin-left: sizing.rems(m) + $xxs;
}
.rose-notification-dismiss {
position: absolute;
color: currentColor;
background-color: transparent;
border-radius: $xxxs;
border: 0;
padding: 0;
cursor: pointer;
top: sizing.rems(s);
right: sizing.rems(s);
.rose-icon {
display: block;
padding: $xxs;
}
.rose-notification-dismiss-text {
position: absolute;
top: -1000px;
left: -1000px;
width: 1px;
height: 1px;
opacity: 0;
}
&:hover {
background-color: rgba(var(--stark-components), var(--opacity-3))
}
&:active {
background-color: rgba(var(--stark-components), var(--opacity-2))
}
}
--icon-color: var(--info);
--header-color: var(--info-starker-1);
--border-color: var(--info-subtler-1);
--background-color: var(--info-subtler-3);
border-color: var(--border-color);
background-color: var(--background-color);
.rose-notification-icon {
color: var(--icon-color);
}
.rose-notification-header {
color: var(--header-color);
}
&.is-failure {
--icon-color: var(--failure);
--header-color: var(--failure);
--border-color: var(--failure-subtler-1);
--background-color: var(--failure-subtler-2);
}
&.is-success {
--icon-color: var(--success);
--header-color: var(--success-starker-1);
--border-color: var(--success-subtler-1);
--background-color: var(--success-subtler-2);
}
&.is-warning {
--icon-color: var(--warning);
--header-color: var(--warning-starker-1);
--border-color: var(--warning-subtler-1);
--background-color: var(--warning-subtler-2);
}
}
@@ -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);
@@ -2,3 +2,4 @@
@import 'checkbox';
@import 'select';
@import 'radio';
@import 'errors';
@@ -2,7 +2,7 @@
@use '../../utilities/type';
.rose-form-input {
margin-bottom: sizing.rems(m);
margin-bottom: sizing.rems(l);
.rose-form-input-label {
@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);
@@ -2,7 +2,7 @@
@use '../../utilities/type';
.rose-form-select {
margin-bottom: sizing.rems(m);
margin-bottom: sizing.rems(l);
.rose-form-select-label {
@include type.type(s, semibold);
@@ -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);
@@ -0,0 +1,7 @@
@use '../../../variables/sizing';
@import 'message';
.rose-form-errors {
margin-bottom: sizing.rems(l);
}
@@ -0,0 +1,8 @@
@use '../../../variables/sizing';
@use '../../../utilities/type';
.rose-form-error-message {
@include type.type(xs);
color: var(--failure);
margin-bottom: sizing.rems(xs);
}
@@ -4,18 +4,25 @@
@mixin button {
@include typography.type(button, semibold);
display: flex;
flex-direction: row;
display: inline-block;
margin-bottom: sizing.rems(l);
margin-right: sizing.rems(xs);
padding: sizing.rems(off-xs) sizing.rems(l);
border-radius: sizing.rems(xxxs);
border: sizing.rems(xxxxs) solid;
background: none;
&.has-icon-left > .rose-icon:first-child {
.rose-button-wrapper {
display: flex;
flex-direction: row;
}
&.has-icon-left .rose-icon:first-child {
margin-left: -(sizing.rems(xs));
margin-right: sizing.rems(xxs);
}
&.has-icon-right > .rose-icon:last-child {
&.has-icon-right .rose-icon:last-child {
margin-left: sizing.rems(xxs);
margin-right: -(sizing.rems(xs));
}
@@ -5,29 +5,30 @@
class="{{if @style (concat "rose-button-" @style)}} {{if @iconLeft "has-icon-left"}} {{if @iconRight "has-icon-right"}} {{if @iconOnly "has-icon-only"}}"
>
{{#if @iconLeft}}
<Rose::Icon
@name={{@iconLeft}}
@size="medium"
/>
{{/if}}
<span class="rose-button-wrapper">
{{#if @iconLeft}}
<Rose::Icon
@name={{@iconLeft}}
@size="medium"
/>
{{/if}}
<span class="rose-button-body">
{{yield}}
<span class="rose-button-body">
{{yield}}
</span>
{{#if @iconOnly}}
<Rose::Icon
@name={{@iconOnly}}
@size="medium"
/>
{{/if}}
{{#if @iconRight}}
<Rose::Icon
@name={{@iconRight}}
@size="medium"
/>
{{/if}}
</span>
{{#if @iconOnly}}
<Rose::Icon
@name={{@iconOnly}}
@size="medium"
/>
{{/if}}
{{#if @iconRight}}
<Rose::Icon
@name={{@iconRight}}
@size="medium"
/>
{{/if}}
</button>
@@ -0,0 +1,16 @@
<form
...attributes
novalidate
method="post"
class="rose-form"
{{on "submit" this.handleSubmit}}
>
{{yield (hash
radioGroup=(component 'rose/form/radio/group' disabled=@disabled)
checkbox=(component 'rose/form/checkbox' disabled=@disabled)
input=(component 'rose/form/input' disabled=@disabled)
select=(component 'rose/form/select' disabled=@disabled)
textarea=(component 'rose/form/textarea' disabled=@disabled)
actions=(component 'rose/form/actions' disabled=@disabled cancel=@cancel)
)}}
</form>
@@ -0,0 +1,19 @@
<div class="rose-form-actions" ...attributes>
<Rose::Button
@disabled={{@disabled}}
@submit={{true}}
@style="primary"
>
{{@submitText}}
</Rose::Button>
{{#if this.showCancel}}
<Rose::Button
@disabled={{@disabled}}
@style="secondary"
{{on "click" @cancel}}
>
{{@cancelText}}
</Rose::Button>
{{/if}}
</div>
@@ -3,14 +3,18 @@
<Input
...attributes
class="rose-form-checkbox-field"
id={{@id}}
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={{@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>
@@ -0,0 +1,5 @@
<div class="rose-form-errors" id={{@id}} ...attributes>
{{yield (hash
message=(component 'rose/form/errors/message')
)}}
</div>
@@ -0,0 +1,4 @@
<p class="rose-form-error-message" ...attributes>
<Rose::Icon @name="cancel-square-fill" @size="medium" />
{{yield}}
</p>
@@ -1,20 +1,28 @@
<div class="rose-form-input{{if @error " error"}}">
<label class="rose-form-input-label" for={{@id}}>
<label class="rose-form-input-label" for={{this.id}}>
{{@label}}
</label>
{{#if @helperText}}
<span class="rose-form-input-helper-text">{{@helperText}}</span>
<span
class="rose-form-input-helper-text"
id="helper-text-{{this.id}}">
{{@helperText}}
</span>
{{/if}}
<Input
...attributes
class="rose-form-input-field"
id={{@id}}
id={{this.id}}
disabled={{@disabled}}
@type={{@type}}
@value={{@value}} />
@value={{@value}}
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,3 +1,3 @@
{{yield (hash
radio=(component 'rose/form/radio/radio' name=@name selectedValue=@selectedValue)
radio=(component 'rose/form/radio/radio' name=@name selectedValue=@selectedValue disabled=@disabled)
)}}
@@ -3,14 +3,15 @@
<RadioButton
...attributes
@radioClass="rose-form-radio-field"
@radioId={{@id}}
@radioId={{this.id}}
type="radio"
disabled={{@disabled}}
@name={{@name}}
@value={{@value}}
@groupValue={{@selectedValue}}
@disabled={{@disabled}}/>
<label class="rose-form-radio-label" for={{@id}}>
<label class="rose-form-radio-label" for={{this.id}}>
{{@label}}
</label>
@@ -1,22 +1,28 @@
<div class="rose-form-select{{if @error " error"}}">
<label class="rose-form-select-label" for={{@id}}>
<label class="rose-form-select-label" for={{this.id}}>
{{@label}}
</label>
{{#if @helperText}}
<span class="rose-form-select-helper-text">{{@helperText}}</span>
<span
class="rose-form-select-helper-text"
id="helper-text-{{this.id}}">
{{@helperText}}
</span>
{{/if}}
<XSelect
...attributes
class="rose-form-select-field"
id={{@id}}
@value={{@value}}
@onChange={{@onChange}}
as |xs|
>
{{yield xs}}
</XSelect>
{{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>
@@ -0,0 +1,12 @@
<XSelect
class="rose-form-select-field"
id={{@id}}
name={{@name}}
disabled={{@disabled}}
@value={{@value}}
@onChange={{@onChange}}
aria-describedby="{{if @helperText (concat 'helper-text-' this.id)}} {{if @error (concat 'errors-' this.id)}}"
as |xs|
>
{{yield xs}}
</XSelect>
@@ -1,19 +1,27 @@
<div class="rose-form-input{{if @error " error"}}">
<label class="rose-form-input-label" for={{@id}}>
<label class="rose-form-input-label" for={{this.id}}>
{{@label}}
</label>
{{#if @helperText}}
<span class="rose-form-input-helper-text">{{@helperText}}</span>
<span
class="rose-form-input-helper-text"
id="helper-text-{{this.id}}">
{{@helperText}}
</span>
{{/if}}
<Textarea
...attributes
id={{@id}}
class="rose-form-input-field"
@value={{@value}} />
id={{this.id}}
disabled={{@disabled}}
@value={{@value}}
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>
@@ -0,0 +1,37 @@
<section
...attributes
class="rose-notification is-{{@style}}{{if @dismiss " is-dismissible"}}"
role="alert"
aria-live="{{if @dismiss "assertive"}}"
aria-atomic="true"
>
<header class="rose-notification-header">
<Rose::Icon
class="rose-notification-icon"
@name={{this.icon}}
@size="medium" />
{{@heading}}
</header>
<div class="rose-notification-body">
{{yield}}
</div>
{{#if @dismiss}}
<button
type="button"
class="rose-notification-dismiss"
title={{@dismissText}}
{{on "click" @dismiss}}
>
<Rose::Icon
@name="cancel-plain"
@size="medium" />
<span class="rose-notification-dismiss-text">
{{@dismissText}}
</span>
</button>
{{/if}}
</section>
+1
View File
@@ -0,0 +1 @@
export { default } from 'rose/components/rose/form';
@@ -0,0 +1 @@
export { default } from 'rose/components/rose/form/actions';
@@ -0,0 +1 @@
export { default } from 'rose/components/rose/form/errors';
@@ -0,0 +1 @@
export { default } from 'rose/components/rose/form/errors/message';
@@ -0,0 +1 @@
export { default } from 'rose/components/rose/form/select/select';
@@ -0,0 +1 @@
export { default } from 'rose/components/rose/notification';
+1
View File
@@ -64,6 +64,7 @@
"ember-source": "~3.12.0",
"ember-source-channel-url": "^2.0.1",
"ember-try": "^1.4.0",
"ember-uuid": "^2.1.0",
"emberx-select": "github:adopted-ember-addons/emberx-select#bc5f774",
"eslint-plugin-ember": "^7.7.2",
"eslint-plugin-node": "^11.0.0",
@@ -2,9 +2,11 @@ import Controller from '@ember/controller';
import { action } from '@ember/object';
export default class IndexController extends Controller {
color = 'green';
@action
selectChange(/*value*/) {
actionHandler(/*value*/) {
// eslint-disable-next-line no-console
//console.log(value);
//console.log('action handled');
}
}
+168 -190
View File
@@ -1,205 +1,183 @@
<Rose::Layout::Page as |page|>
<page.header>
<Rose::Header as |header|>
<header.brand @logo="logo" @text="Product Name" />
<Rose::Header as |header|>
<header.nav as |nav|>
<nav.dropdown @text="org-name" as |dropdown|>
<dropdown.link @route="index">Menu item</dropdown.link>
<dropdown.link @route="about">Menu item</dropdown.link>
<dropdown.button>Button menu item</dropdown.button>
</nav.dropdown>
<nav.link @route="index">Section</nav.link>
<nav.link @route="about">Section</nav.link>
</header.nav>
<header.brand @logo="logo" @text="Product Name" />
<header.nav as |nav|>
<nav.dropdown @text="org-name" as |dropdown|>
<dropdown.link @route="index">Menu item</dropdown.link>
<dropdown.link @route="about">Menu item</dropdown.link>
<dropdown.button>Button menu item</dropdown.button>
</nav.dropdown>
<nav.link @route="index">Section</nav.link>
<nav.link @route="about">Section</nav.link>
</header.nav>
<header.utilities as |utilities|>
<utilities.dropdown
@text="user@example.net"
@iconOnly={{true}}
@icon="user-square-fill" as |dropdown|
>
<dropdown.link @route="index">Menu item</dropdown.link>
<dropdown.link @route="about">Menu item</dropdown.link>
<dropdown.button>Button menu item</dropdown.button>
</utilities.dropdown>
</header.utilities>
</Rose::Header>
</page.header>
<page.sidebar />
<page.main>
Dummy app index template
<h2>Icon</h2>
<Rose::Icon @name="alert-circle-fill" />
<Rose::Icon @name="alert-circle-fill" @size="medium" />
<Rose::Icon @name="alert-circle-fill" @size="small" />
<h2>Button</h2>
<Rose::Button @style="primary">Primary</Rose::Button>
<br>
<Rose::Button @style="primary" @disabled={{true}}>Primary</Rose::Button>
<br>
<Rose::Button @style="secondary">Secondary</Rose::Button>
<br>
<Rose::Button @style="secondary" @disabled={{true}}>Secondary</Rose::Button>
<br>
<h2>Form</h2>
<Rose::Form::Select
@label="Label"
@helperText="Helper text"
@onChange={{fn this.selectChange}}
as |select|
<header.utilities as |utilities|>
<utilities.dropdown
@text="user@example.net"
@iconOnly={{true}}
@icon="user-square-fill" as |dropdown|
>
<dropdown.link @route="index">Menu item</dropdown.link>
<dropdown.link @route="about">Menu item</dropdown.link>
<dropdown.button>Button menu item</dropdown.button>
</utilities.dropdown>
</header.utilities>
</Rose::Header>
Dummy app index template
<Rose::Notification
@style="failure"
@heading="Error"
@dismiss={{this.actionHandler}}
@dismissText="Dismiss"
>
An error occurred.
</Rose::Notification>
<h2>Icon</h2>
<Rose::Icon @name="alert-circle-fill" />
<Rose::Icon @name="alert-circle-fill" @size="medium" />
<Rose::Icon @name="alert-circle-fill" @size="small" />
<h2>Button</h2>
<Rose::Button @style="primary">Primary</Rose::Button>
<br>
<Rose::Button @style="primary" @disabled={{true}}>Primary</Rose::Button>
<br>
<Rose::Button @style="secondary">Secondary</Rose::Button>
<br>
<Rose::Button @style="secondary" @disabled={{true}}>Secondary</Rose::Button>
<br>
<h2>Form</h2>
<Rose::Form
@onSubmit={{this.actionHandler}}
@cancel={{this.actionHandler}}
@disabled={{false}}
as |form|
>
<h3>Inputs</h3>
<form.input
@label="Label"
@value="Read only"
readonly={{true}}
/>
<form.input
@label="Label"
@helperText="Helper text"
@value="Text"
/>
<form.input
@label="Label"
@helperText="Helper text"
@value="Text"
@error={{true}}
/>
<form.input
@label="Label"
@helperText="Helper text"
@value="Text"
disabled={{true}}
/>
<h3>Textareas</h3>
<form.textarea
@label="Label"
@value="Read only"
readonly={{true}}
/>
<form.textarea
@label="Label"
@helperText="Helper text"
@value="Text"
/>
<form.textarea
@label="Label"
@helperText="Helper text"
@value="Text"
@error={{true}}
/>
<form.textarea
@label="Label"
@helperText="Helper text"
@value="Text"
disabled={{true}}
/>
<h3>Selects</h3>
<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>
</Rose::Form::Select>
</field.field>
</form.select>
<form.select
<Rose::Form::Select
disabled={{true}}
@label="Label"
@helperText="Helper text"
@onChange={{fn this.selectChange}}
as |select|
>
@label="Label"
@helperText="Helper text"
@onChange={{fn this.actionHandler}}
as |field|
>
<field.field as |select|>
<select.option>Choose an option</select.option>
</Rose::Form::Select>
<Rose::Form::Input
@label="Label"
@value="Read only"
readonly={{true}}
/>
<Rose::Form::Input
@label="Label"
@helperText="Helper text"
@value="Text"
/>
<Rose::Form::Input
@label="Label"
@helperText="Helper text"
@value="Text"
@error={{true}}
/>
<Rose::Form::Input
@label="Label"
@helperText="Helper text"
@value="Text"
disabled={{true}}
/>
</field.field>
</form.select>
<Rose::Form::Textarea
@label="Label"
@value="Read only"
readonly={{true}}
/>
<Rose::Form::Textarea
@label="Label"
@helperText="Helper text"
@value="Text"
/>
<Rose::Form::Textarea
@label="Label"
@helperText="Helper text"
@value="Text"
@error={{true}}
/>
<Rose::Form::Textarea
@label="Label"
@helperText="Helper text"
@value="Text"
disabled={{true}}
/>
<h2>Checkbox</h2>
<Rose::Form::Checkbox
@id="checkbox-1"
@label="Label"
/>
<Rose::Form::Checkbox
@id="checkbox-2"
@error={{true}}
@label="Error"
/>
<Rose::Form::Checkbox
@id="checkbox-3"
@checked={{true}}
@label="Checked"
/>
<Rose::Form::Checkbox
@id="checkbox-4"
@label="Disabled"
disabled={{true}}
/>
<Rose::Form::Checkbox
@id="checkbox-5"
@checked={{true}}
@label="Disabled + Checked"
disabled={{true}}
/>
<h2>Radio field</h2>
<Rose::Form::Radio::Radio
@id="radio-1"
@label="Label"
@value="radio-1"
/>
<Rose::Form::Radio::Radio
@id="radio-2"
@error={{true}}
@label="Error"
@value="radio-2"
/>
<Rose::Form::Radio::Radio
@id="radio-3"
@label="Checked"
/>
<Rose::Form::Radio::Radio
@id="radio-4"
@label="Disabled"
<h3>Radio Group</h3>
<form.radioGroup @name="color" @selectedValue={{this.color}} as |radioGroup|>
<radioGroup.radio
@label="Red"
@value="red"
/>
<radioGroup.radio
@label="Green"
@value="green"
/>
<radioGroup.radio
@label="Blue"
@value="blue"
@disabled={{true}}
@value="radio-4"
/>
<Rose::Form::Radio::Radio
@id="radio-5"
@label="Disabled + Checked"
@value="radio-5"
@variable="radio-5"
@disabled={{true}}
/>
/>
</form.radioGroup>
<h2>Radio field group</h2>
<Rose::Form::Radio::Group @name="color" @variable="green" as |radioGroup|>
<radioGroup.radio
@id="red-radio"
@label="Red"
@value="red"
/>
<radioGroup.radio
@id="green-radio"
@label="Green"
@value="green"
/>
<radioGroup.radio
@id="blue-radio"
@label="Blue"
@value="blue"
/>
</Rose::Form::Radio::Group>
<h3>Checkboxes</h3>
<form.checkbox
@label="Label"
/>
<form.checkbox
@error={{true}}
@label="Error"
/>
<form.checkbox
@checked={{true}}
@label="Checked"
/>
<form.checkbox
@label="Disabled"
disabled={{true}}
/>
<form.checkbox
@checked={{true}}
@label="Disabled + Checked"
disabled={{true}}
/>
</page.main>
</Rose::Layout::Page>
<form.actions
@submitText="Save"
@cancelText="Cancel" />
</Rose::Form>
@@ -0,0 +1,67 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find, click } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | rose/form', function (hooks) {
setupRenderingTest(hooks);
test('it renders and handles submit/cancel actions', async function (assert) {
assert.expect(3);
this.submit = () => assert.ok(true, 'submitted');
this.cancel = () => assert.ok(true, 'cancelled');
await render(hbs`
<Rose::Form
@onSubmit={{this.submit}}
@cancel={{this.cancel}}
@disabled={{false}}
as |form|
>
<form.actions
@submitText="Save"
@cancelText="Cancel" />
</Rose::Form>
`);
assert.ok(find('.rose-form'));
await click('[type="submit"]');
await click('button:not([type="submit"])');
});
test('it can render contextual form components', async function (assert) {
assert.expect(5);
this.submit = () => {};
this.cancel = () => {};
this.select = () => {};
await render(hbs`
<Rose::Form
@onSubmit={{this.submit}}
@cancel={{this.cancel}}
@disabled={{false}}
as |form|
>
<form.input @label="Input" name="input-field" />
<form.textarea @label="Textarea" name="textarea-field" />
<form.select
@label="Select"
@name="select-field"
@onChange={{this.select}}
as |field|
>
<field.field />
</form.select>
<form.checkbox @label="Checkbox" name="checkbox-field" />
<form.radioGroup @name="radio-group-field" @selectedValue="green" as |radioGroup|>
<radioGroup.radio
@label="Red"
@value="red"
/>
</form.radioGroup>
</Rose::Form>
`);
assert.ok(find('[name="input-field"]'));
assert.ok(find('[name="textarea-field"]'));
assert.ok(find('[name="select-field"]'));
assert.ok(find('[name="checkbox-field"]'));
assert.ok(find('[name="radio-group-field"]'));
});
});
@@ -0,0 +1,38 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find, click } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | rose/form/actions', function (hooks) {
setupRenderingTest(hooks);
test('it renders a submit button and a regular button', async function (assert) {
assert.expect(2);
this.cancel = () => {};
await render(hbs`{{rose/form/actions cancel=this.cancel}}`);
assert.ok(find('[type="submit"]'));
assert.ok(find('button:not([type="submit"])'));
});
test('it can disabled the buttons', async function (assert) {
assert.expect(2);
this.cancel = () => {};
await render(hbs`{{rose/form/actions cancel=this.cancel disabled=true}}`);
assert.ok(find('[type="submit"]:disabled'));
assert.ok(find('button:not([type="submit"]):disabled'));
});
test('it optionally does not render the cancel button', async function (assert) {
assert.expect(2);
await render(hbs`{{rose/form/actions showCancel=false}}`);
assert.ok(find('[type="submit"]'));
assert.notOk(find('button:not([type="submit"])'));
});
test('it executes a function on cancel click', async function (assert) {
assert.expect(1);
this.cancel = () => assert.ok(true, 'cancel was clicked');
await render(hbs`{{rose/form/actions cancel=this.cancel}}`);
await click('button:not([type="submit"])');
});
});
@@ -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);
@@ -32,8 +49,8 @@ module('Integration | Component | rose/form/checkbox', function (hooks) {
assert.ok(await find('.error'));
});
test('it is disabled when disabled={{true}}', async function (assert) {
await render(hbs`<Rose::Form::Checkbox disabled={{true}} />`);
test('it is disabled when @disabled={{true}}', async function (assert) {
await render(hbs`<Rose::Form::Checkbox @disabled={{true}} />`);
assert.equal(await find('input').disabled, true);
});
});
@@ -0,0 +1,27 @@
import { module, test } from 'qunit';
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) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
assert.expect(3);
await render(hbs`
<Rose::Form::Errors as |errors|>
<errors.message>An error occurred.</errors.message>
<errors.message>Another error occurred.</errors.message>
<//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.'
);
});
});
@@ -0,0 +1,17 @@
import { module, test } from 'qunit';
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) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
await render(hbs`
<Rose::Form::Errors::Message>
An error occurred.
</Rose::Form::Errors::Message>
`);
assert.equal(this.element.textContent.trim(), 'An error occurred.');
});
});
@@ -14,9 +14,32 @@ module('Integration | Component | rose/form/input', function (hooks) {
test('it displays optional helper text', async function (assert) {
await render(hbs`<Rose::Form::Input @helperText="Help me" />`);
const fieldEl = find('input');
const id = fieldEl.id;
const helperId = `helper-text-${id}`;
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').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(
await find('.rose-form-input-helper-text').textContent.trim(),
'Help me'
fieldEl.getAttribute('aria-describedby').split(' ')[1],
errorsId
);
});
@@ -31,7 +54,7 @@ module('Integration | Component | rose/form/input', function (hooks) {
});
test('it supports disabled attribute', async function (assert) {
await render(hbs`<Rose::Form::Input disabled={{true}} />`);
await render(hbs`<Rose::Form::Input @disabled={{true}} />`);
assert.ok(await find('[disabled]'));
});
@@ -1,6 +1,6 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click, find, findAll } from '@ember/test-helpers';
import { render, click, findAll } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | rose/form/radio/group', function (hooks) {
@@ -10,12 +10,12 @@ module('Integration | Component | rose/form/radio/group', function (hooks) {
await render(hbs`
<Rose::Form::Radio::Group @name="bird" as |radioGroup|>
<radioGroup.radio
@id="bird-1"
@name="bird"
@label="Pegion"
@value="pegion"
/>
<radioGroup.radio
@id="bird-2"
@name="bird"
@label="flamingo"
@value="flamingo"
/>
@@ -31,37 +31,37 @@ module('Integration | Component | rose/form/radio/group', function (hooks) {
await render(hbs`
<Rose::Form::Radio::Group @name="bird" @selectedValue="flamingo" as |radioGroup|>
<radioGroup.radio
@id="bird-1"
@name="bird"
@label="Pegion"
@value="pegion"
/>
<radioGroup.radio
@id="bird-2"
@name="bird"
@label="flamingo"
@value="flamingo"
/>
</Rose::Form::Radio::Group>
`);
assert.equal(find('#bird-1').checked, false);
assert.equal(find('#bird-2').checked, true);
assert.equal(findAll('[type="radio"]')[0].checked, false);
assert.equal(findAll('[type="radio"]')[1].checked, true);
await click(find('#bird-1'));
await click(findAll('[type="radio"]')[0]);
assert.equal(find('#bird-1').checked, true);
assert.equal(find('#bird-2').checked, false);
assert.equal(findAll('[type="radio"]')[0].checked, true);
assert.equal(findAll('[type="radio"]')[1].checked, false);
});
test('it reflects active radiofield value in @selectedValue', async function (assert) {
await render(hbs`
<Rose::Form::Radio::Group @name="bird" @selectedValue={{this.selectedValue}} as |radioGroup|>
<radioGroup.radio
@id="bird-1"
@name="bird"
@label="Pegion"
@value="pegion"
/>
<radioGroup.radio
@id="bird-2"
@name="bird"
@label="flamingo"
@value="flamingo"
/>
@@ -70,10 +70,10 @@ module('Integration | Component | rose/form/radio/group', function (hooks) {
assert.notOk(this.selectedValue);
await click(find('#bird-1'));
await click(findAll('[type="radio"]')[0]);
assert.equal(this.selectedValue, 'pegion');
await click(find('#bird-2'));
await click(findAll('[type="radio"]')[1]);
assert.equal(this.selectedValue, 'flamingo');
});
@@ -82,31 +82,31 @@ module('Integration | Component | rose/form/radio/group', function (hooks) {
await render(hbs`
<Rose::Form::Radio::Group @name="bird" @selectedValue={{this.selectedValue}} as |radioGroup|>
<radioGroup.radio
@id="bird-1"
@name="bird"
@label="Pegion"
@value="pegion"
/>
<radioGroup.radio
@id="bird-2"
@name="bird"
@label="flamingo"
@value="flamingo"
/>
</Rose::Form::Radio::Group>
`);
assert.equal(find('#bird-1').checked, false);
assert.equal(find('#bird-2').checked, false);
assert.equal(findAll('[type="radio"]')[0].checked, false);
assert.equal(findAll('[type="radio"]')[1].checked, false);
this.set('selectedValue', 'flamingo');
assert.equal(find('#bird-1').checked, false);
assert.equal(find('#bird-2').checked, true);
assert.equal(findAll('[type="radio"]')[0].checked, false);
assert.equal(findAll('[type="radio"]')[1].checked, true);
this.set('selectedValue', 'pegion');
assert.equal(find('#bird-1').checked, true);
assert.equal(find('#bird-2').checked, false);
assert.equal(findAll('[type="radio"]')[0].checked, true);
assert.equal(findAll('[type="radio"]')[1].checked, false);
this.set('selectedValue', 'dog');
assert.equal(find('#bird-1').checked, false);
assert.equal(find('#bird-2').checked, false);
assert.equal(findAll('[type="radio"]')[0].checked, false);
assert.equal(findAll('[type="radio"]')[1].checked, false);
});
});
@@ -12,12 +12,14 @@ module('Integration | Component | rose/form/select', function (hooks) {
<Rose::Form::Select
@label="Label"
@onChange={{fn this.onChange}}
as |select|
as |field|
>
<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 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>
</Rose::Form::Select>
`);
assert.equal(find('label').textContent.trim(), 'Label');
@@ -30,16 +32,51 @@ module('Integration | Component | rose/form/select', function (hooks) {
await render(hbs`
<Rose::Form::Select
@label="Label"
@helperText="Helper text"
@helperText="Help me"
@onChange={{fn this.onChange}}
as |select|
as |field|
>
<select.option>Choose an option</select.option>
<field.field as |select|>
<select.option>Choose an option</select.option>
</field.field>
</Rose::Form::Select>
`);
const fieldEl = find('select');
const id = fieldEl.id;
const helperId = `helper-text-${id}`;
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').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(
find('.rose-form-select-helper-text').textContent.trim(),
'Helper text'
fieldEl.getAttribute('aria-describedby').split(' ')[1],
errorsId
);
});
@@ -48,10 +85,12 @@ module('Integration | Component | rose/form/select', function (hooks) {
await render(hbs`
<Rose::Form::Select
@onChange={{fn this.onChange}}
disabled={{true}}
as |select|
@disabled={{true}}
as |field|
>
<select.option>Choose an option</select.option>
<field.field as |select|>
<select.option>Choose an option</select.option>
</field.field>
</Rose::Form::Select>
`);
assert.ok(find('[disabled]'));
@@ -64,10 +103,12 @@ module('Integration | Component | rose/form/select', function (hooks) {
<Rose::Form::Select
@onChange={{fn this.onChange}}
@value={{value}}
as |select|
as |field|
>
<select.option @value="value-1">value-1</select.option>
<select.option @value="value-2">value-2</select.option>
<field.field as |select|>
<select.option @value="value-1">value-1</select.option>
<select.option @value="value-2">value-2</select.option>
</field.field>
</Rose::Form::Select>
`);
assert.equal(find('select').value, 'value-1');
@@ -91,12 +132,14 @@ module('Integration | Component | rose/form/select', function (hooks) {
this.onChange = this.onChange1;
await render(hbs`
<Rose::Form::Select
name="my-select"
@name="my-select"
@onChange={{fn this.onChange}}
as |select|
as |field|
>
<select.option>Choose an option</select.option>
<select.option @value="value-1">value-1</select.option>
<field.field as |select|>
<select.option>Choose an option</select.option>
<select.option @value="value-1">value-1</select.option>
</field.field>
</Rose::Form::Select>
`);
await fillIn('[name="my-select"]', 'value-1');
@@ -14,14 +14,37 @@ module('Integration | Component | rose/form/textarea', function (hooks) {
test('it displays optional helper text', async function (assert) {
await render(hbs`<Rose::Form::Textarea @helperText="Help me" />`);
const fieldEl = find('textarea');
const id = fieldEl.id;
const helperId = `helper-text-${id}`;
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').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(
await find('.rose-form-input-helper-text').textContent.trim(),
'Help me'
fieldEl.getAttribute('aria-describedby').split(' ')[1],
errorsId
);
});
test('it supports disabled attribute', async function (assert) {
await render(hbs`<Rose::Form::Textarea disabled={{true}} />`);
await render(hbs`<Rose::Form::Textarea @disabled={{true}} />`);
assert.ok(await find('[disabled]'));
});
@@ -0,0 +1,33 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find, click } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | rose/notification', function(hooks) {
setupRenderingTest(hooks);
test('it renders', async function(assert) {
await render(hbs`
<Rose::Notification
@style="info"
@heading="Alert Notification"
>
An error occurred.
</Rose::Notification>
`);
assert.ok(find('.rose-notification'));
assert.equal(find('.rose-notification-body').textContent.trim(), 'An error occurred.');
});
test('it has optional dismiss button which triggers a function', async function(assert) {
assert.expect(1);
this.dismiss = () => assert.ok(true, 'dismiss clicked');
await render(hbs`
<Rose::Notification
@style="info"
@heading="Alert Notification"
@dismiss={{this.dismiss}} />
`);
await click('.rose-notification-dismiss');
});
});
@@ -0,0 +1,11 @@
import EmberObject from '@ember/object';
import ComponentAutoIdMixin from 'rose/mixins/component-auto-id';
import { module, test } from 'qunit';
module('Unit | Mixin | component-auto-id', function () {
test('it generates an ID and assigns it to the object', function (assert) {
let ComponentAutoIdObject = EmberObject.extend(ComponentAutoIdMixin);
let subject = ComponentAutoIdObject.create();
assert.ok(subject.id);
});
});
@@ -5,7 +5,6 @@
{{on "submit" @submit}}>
<Rose::Form::Input
@id="name"
name="name"
@type="name"
@value={{@model.name}}
@@ -16,7 +15,6 @@
/>
<Rose::Form::Textarea
@id="description"
name="description"
@type="description"
@value={{@model.description}}
+2 -1
View File
@@ -22,7 +22,7 @@
"format:sass": "prettier --write app/**/*.scss",
"start": "ember serve",
"clean:coverage": "rm -rf coverage_*",
"pretest": "yarn clean:coverage && yarn lint",
"pretest": "yarn clean:coverage",
"test": "COVERAGE=true ember exam --random",
"posttest": "ember coverage-merge && yarn clean:coverage"
},
@@ -65,6 +65,7 @@
"ember-resolver": "^7.0.0",
"ember-route-action-helper": "^2.0.8",
"ember-source": "~3.16.0",
"ember-uuid": "^2.1.0",
"ember-welcome-page": "^4.0.0",
"emberx-select": "github:adopted-ember-addons/emberx-select#bc5f774",
"eslint-plugin-ember": "^7.7.2",
+8
View File
@@ -8002,6 +8002,14 @@ ember-try@^1.4.0:
rsvp "^4.7.0"
walk-sync "^1.1.3"
ember-uuid@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ember-uuid/-/ember-uuid-2.1.0.tgz#a65d030534d47ced1ea22be9b7d3821eff71f99c"
integrity sha512-oiUNIBPCyJtzkcRUAi3qCRwU+yq5r9gzlPLKRG1pQ2WA209lphmZQRR+TyaeNwOijs9bjttzFV6YOyaTO40B/A==
dependencies:
ember-cli-babel "^7.11.1"
ember-cli-htmlbars "^4.0.0"
ember-welcome-page@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/ember-welcome-page/-/ember-welcome-page-4.0.0.tgz#14f5ef8b477f9480c39548d3ebd51f5c4ffb0a14"