diff --git a/addons/rose/.eslintrc.js b/addons/rose/.eslintrc.js index 85a190092..b705ae52d 100644 --- a/addons/rose/.eslintrc.js +++ b/addons/rose/.eslintrc.js @@ -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 diff --git a/addons/rose/addon/components/rose/form.js b/addons/rose/addon/components/rose/form.js new file mode 100644 index 000000000..0805f0348 --- /dev/null +++ b/addons/rose/addon/components/rose/form.js @@ -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; + }, +}); diff --git a/addons/rose/addon/components/rose/form.stories.mdx b/addons/rose/addon/components/rose/form.stories.mdx new file mode 100644 index 000000000..f57f85da2 --- /dev/null +++ b/addons/rose/addon/components/rose/form.stories.mdx @@ -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'; + + + +# Form + +A component for building accessible forms. Yields contextual fields +and actions. + + + {{ + template: hbs` + + + + + + Choose an option + Value 1 + Value 2 + Value 3 + + + + + + + + + + + `, + context: { + disabled: boolean('disabled', false), + submit: action('submit'), + cancel: action('cancel'), + selectChange: action('selectChange'), + }, + }} + + + +```htmlbars + + + + + + Choose an option + Value 1 + Value 2 + Value 3 + + + + + + + + + + +``` diff --git a/addons/rose/addon/components/rose/form/actions.js b/addons/rose/addon/components/rose/form/actions.js new file mode 100644 index 000000000..4bfcbea85 --- /dev/null +++ b/addons/rose/addon/components/rose/form/actions.js @@ -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 + * + */ +export default Component.extend({ + layout, + tagName: '', + + /** + * @type {boolean} + */ + showCancel: true, +}); diff --git a/addons/rose/addon/components/rose/form/actions.stories.mdx b/addons/rose/addon/components/rose/form/actions.stories.mdx new file mode 100644 index 000000000..7517bddb6 --- /dev/null +++ b/addons/rose/addon/components/rose/form/actions.stories.mdx @@ -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'; + + + +# 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 + +``` + + + {{ + template: hbs` + + `, + context: { + submitText: text('submitText', 'Save'), + cancelText: text('cancelText', 'Cancel'), + showCancel: boolean('showCancel', true), + disabled: boolean('disabled', false), + onCancel: action('cancel'), + }, + }} + diff --git a/addons/rose/addon/components/rose/form/checkbox.js b/addons/rose/addon/components/rose/form/checkbox.js index 5b94c0a1d..ad6ad3f38 100644 --- a/addons/rose/addon/components/rose/form/checkbox.js +++ b/addons/rose/addon/components/rose/form/checkbox.js @@ -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 `` element. - * - * @example - * */ -export default Component.extend({ +export default Component.extend(ComponentAutoId, { layout, - tagName: '', }); diff --git a/addons/rose/addon/components/rose/form/checkbox.stories.mdx b/addons/rose/addon/components/rose/form/checkbox.stories.mdx index 3c342c827..95200ce1c 100644 --- a/addons/rose/addon/components/rose/form/checkbox.stories.mdx +++ b/addons/rose/addon/components/rose/form/checkbox.stories.mdx @@ -11,30 +11,27 @@ Renders a form input checkbox. ```htmlbars ``` - {{ + {{ template: hbs` `, 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. } }} + + + {{ + template: hbs` + + + An error occurred. + + + `, + context: { + label: text('text', 'Checkbox 🛎'), + name: text('name', 'checkbox-1'), + checked: boolean('checked', false), + disabled: boolean('disabled', false), + } + }} + diff --git a/addons/rose/addon/components/rose/form/errors.js b/addons/rose/addon/components/rose/form/errors.js new file mode 100644 index 000000000..bc13c3f56 --- /dev/null +++ b/addons/rose/addon/components/rose/form/errors.js @@ -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: '', +}); diff --git a/addons/rose/addon/components/rose/form/errors.stories.mdx b/addons/rose/addon/components/rose/form/errors.stories.mdx new file mode 100644 index 000000000..27bcbb867 --- /dev/null +++ b/addons/rose/addon/components/rose/form/errors.stories.mdx @@ -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'; + + + +# Form Errors + +Use this component to display one or more error messages. + +```html + + An error message goes here. + Another message goes here. + +``` + + + {{ + template: hbs` + + An error message goes here. + Another message goes here. + + `, + context: {}, + }} + diff --git a/addons/rose/addon/components/rose/form/errors/message.js b/addons/rose/addon/components/rose/form/errors/message.js new file mode 100644 index 000000000..0f59c6b06 --- /dev/null +++ b/addons/rose/addon/components/rose/form/errors/message.js @@ -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: '', +}); diff --git a/addons/rose/addon/components/rose/form/errors/message.stories.mdx b/addons/rose/addon/components/rose/form/errors/message.stories.mdx new file mode 100644 index 000000000..161a5dd61 --- /dev/null +++ b/addons/rose/addon/components/rose/form/errors/message.stories.mdx @@ -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'; + + + +# 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 + + An error message goes here. + +``` + + + {{ + template: hbs` + + An error message goes here. + + `, + context: {}, + }} + diff --git a/addons/rose/addon/components/rose/form/input.js b/addons/rose/addon/components/rose/form/input.js index 005ed82e2..60ae3d0f1 100644 --- a/addons/rose/addon/components/rose/form/input.js +++ b/addons/rose/addon/components/rose/form/input.js @@ -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 `` element. - * - * @example - * */ -export default Component.extend({ +export default Component.extend(ComponentAutoId, { layout, tagName: '', }); diff --git a/addons/rose/addon/components/rose/form/input.stories.mdx b/addons/rose/addon/components/rose/form/input.stories.mdx index c62620de7..48d38020a 100644 --- a/addons/rose/addon/components/rose/form/input.stories.mdx +++ b/addons/rose/addon/components/rose/form/input.stories.mdx @@ -16,21 +16,21 @@ Renders a simple form input. @label="Email" @helperText="Enter your email" @error={{false}} - disabled={{false}} + @disabled={{false}} readonly={{false}} /> ``` - {{ + {{ template: hbs` `, @@ -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), + } + }} + + + + {{ + template: hbs` + + + An error occurred. + + + `, + 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), } }} diff --git a/addons/rose/addon/components/rose/form/radio/group.js b/addons/rose/addon/components/rose/form/radio/group.js index a9508a084..80bba183e 100644 --- a/addons/rose/addon/components/rose/form/radio/group.js +++ b/addons/rose/addon/components/rose/form/radio/group.js @@ -3,29 +3,8 @@ import layout from '../../../../templates/components/rose/form/radio/group'; /** * A `` element. - * - * @example - * - * - * - * - * */ - export default Component.extend({ layout, - tagName: '', }); diff --git a/addons/rose/addon/components/rose/form/radio/group.stories.mdx b/addons/rose/addon/components/rose/form/radio/group.stories.mdx index af30e8246..ebdb50560 100644 --- a/addons/rose/addon/components/rose/form/radio/group.stories.mdx +++ b/addons/rose/addon/components/rose/form/radio/group.stories.mdx @@ -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'; @@ -9,19 +9,21 @@ import { text } from '@storybook/addon-knobs'; Renders a form radiofield group. ```htmlbars - + @@ -30,19 +32,21 @@ Renders a form radiofield group. {{ template: hbs` - + @@ -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'), diff --git a/addons/rose/addon/components/rose/form/radio/radio.js b/addons/rose/addon/components/rose/form/radio/radio.js index f32a6835e..196e47951 100644 --- a/addons/rose/addon/components/rose/form/radio/radio.js +++ b/addons/rose/addon/components/rose/form/radio/radio.js @@ -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 `` element. * `value` and `selectedValue` must be equal to select * radiofield. - * - * @example - * */ -export default Component.extend({ +export default Component.extend(ComponentAutoId, { layout, tagName: '', }); diff --git a/addons/rose/addon/components/rose/form/radio/radio.stories.mdx b/addons/rose/addon/components/rose/form/radio/radio.stories.mdx index 79f2704d9..91d256e9a 100644 --- a/addons/rose/addon/components/rose/form/radio/radio.stories.mdx +++ b/addons/rose/addon/components/rose/form/radio/radio.stories.mdx @@ -10,7 +10,6 @@ Renders a form radiofield. ```htmlbars {{ template: hbs` `, 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'), diff --git a/addons/rose/addon/components/rose/form/select.js b/addons/rose/addon/components/rose/form/select.js index 3de2fd273..c71537542 100644 --- a/addons/rose/addon/components/rose/form/select.js +++ b/addons/rose/addon/components/rose/form/select.js @@ -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 `` element context component which yields option components. - * - * @example - * - * Choose an option - * Curret value - * */ -export default Component.extend({ +export default Component.extend(ComponentAutoId, { layout, - tagName: '', }); diff --git a/addons/rose/addon/components/rose/form/select.stories.mdx b/addons/rose/addon/components/rose/form/select.stories.mdx index 9d3ea7b4e..114c205a0 100644 --- a/addons/rose/addon/components/rose/form/select.stories.mdx +++ b/addons/rose/addon/components/rose/form/select.stories.mdx @@ -11,18 +11,20 @@ Renders a form select element. ```htmlbars - Choose a color - Red - Green - Blue + + Choose a color + Red + Green + Blue + ``` @@ -30,19 +32,21 @@ Renders a form select element. {{ template: hbs` - Choose an option - Value 1 - Value 2 - Value 3 + + Choose an option + Value 1 + Value 2 + Value 3 + `, 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'), + } + }} + + + + {{ + template: hbs` + + + Choose an option + Value 1 + Value 2 + Value 3 + + + An error occurred. + + + `, + 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'), } }} diff --git a/addons/rose/addon/components/rose/form/select/select.js b/addons/rose/addon/components/rose/form/select/select.js new file mode 100644 index 000000000..7e55d8c55 --- /dev/null +++ b/addons/rose/addon/components/rose/form/select/select.js @@ -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: '', +}); diff --git a/addons/rose/addon/components/rose/form/textarea.js b/addons/rose/addon/components/rose/form/textarea.js index fd74470f7..8ca045e84 100644 --- a/addons/rose/addon/components/rose/form/textarea.js +++ b/addons/rose/addon/components/rose/form/textarea.js @@ -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 `` element essentially identical to `Rose::Form::Input` except * for the field element. - * - * @example - * */ -export default Component.extend({ +export default Component.extend(ComponentAutoId, { layout, tagName: '', }); diff --git a/addons/rose/addon/components/rose/form/textarea.stories.mdx b/addons/rose/addon/components/rose/form/textarea.stories.mdx index 8caffcdd8..2ddb54819 100644 --- a/addons/rose/addon/components/rose/form/textarea.stories.mdx +++ b/addons/rose/addon/components/rose/form/textarea.stories.mdx @@ -15,20 +15,20 @@ Renders a simple form textarea. @label="Description" @helperText="Enter a description" @error={{false}} - disabled={{false}} + @disabled={{false}} readonly={{false}} /> ``` - {{ + {{ template: hbs` `, @@ -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), + } + }} + + + + {{ + template: hbs` + + + An error occurred. + + + `, + context: { + value: text('value', 'Value'), + label: text('label', 'Label'), + helperText: text('helperText', 'Helper text'), + disabled: boolean('disabled', false), + 'read-only': boolean('readonly', false), } }} diff --git a/addons/rose/addon/components/rose/notification.js b/addons/rose/addon/components/rose/notification.js new file mode 100644 index 000000000..0ff646e2d --- /dev/null +++ b/addons/rose/addon/components/rose/notification.js @@ -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; + } + +}); diff --git a/addons/rose/addon/components/rose/notification.stories.mdx b/addons/rose/addon/components/rose/notification.stories.mdx new file mode 100644 index 000000000..04cd3f533 --- /dev/null +++ b/addons/rose/addon/components/rose/notification.stories.mdx @@ -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'; + + + +# 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 + + An error occurred. + +``` + + + {{ + template: hbs` + + Some useful information. + + `, + context: {} + }} + + + + {{ + template: hbs` + + Some useful information. + + + Good job! + + + Careful now. + + + An error occurred. + + `, + context: { + dismiss: action('dismiss'), + } + }} + diff --git a/addons/rose/addon/mixins/component-auto-id.js b/addons/rose/addon/mixins/component-auto-id.js new file mode 100644 index 000000000..f970284b8 --- /dev/null +++ b/addons/rose/addon/mixins/component-auto-id.js @@ -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); + }, +}); diff --git a/addons/rose/addon/styles/rose/components/_index.scss b/addons/rose/addon/styles/rose/components/_index.scss index d19b6ef0d..e6d3eefd1 100644 --- a/addons/rose/addon/styles/rose/components/_index.scss +++ b/addons/rose/addon/styles/rose/components/_index.scss @@ -2,3 +2,4 @@ @import 'form'; @import 'header'; @import 'layout'; +@import 'notification'; diff --git a/addons/rose/addon/styles/rose/components/_notification.scss b/addons/rose/addon/styles/rose/components/_notification.scss new file mode 100644 index 000000000..71de0adf0 --- /dev/null +++ b/addons/rose/addon/styles/rose/components/_notification.scss @@ -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); + } +} diff --git a/addons/rose/addon/styles/rose/components/form/_checkbox.scss b/addons/rose/addon/styles/rose/components/form/_checkbox.scss index 4b826841e..9e4827527 100644 --- a/addons/rose/addon/styles/rose/components/form/_checkbox.scss +++ b/addons/rose/addon/styles/rose/components/form/_checkbox.scss @@ -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); diff --git a/addons/rose/addon/styles/rose/components/form/_index.scss b/addons/rose/addon/styles/rose/components/form/_index.scss index c8c516f8c..5a65d3912 100644 --- a/addons/rose/addon/styles/rose/components/form/_index.scss +++ b/addons/rose/addon/styles/rose/components/form/_index.scss @@ -2,3 +2,4 @@ @import 'checkbox'; @import 'select'; @import 'radio'; +@import 'errors'; diff --git a/addons/rose/addon/styles/rose/components/form/_input.scss b/addons/rose/addon/styles/rose/components/form/_input.scss index 7116c6a76..c5cd9f8cb 100644 --- a/addons/rose/addon/styles/rose/components/form/_input.scss +++ b/addons/rose/addon/styles/rose/components/form/_input.scss @@ -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); diff --git a/addons/rose/addon/styles/rose/components/form/_select.scss b/addons/rose/addon/styles/rose/components/form/_select.scss index 63cb16911..a6293b4f0 100644 --- a/addons/rose/addon/styles/rose/components/form/_select.scss +++ b/addons/rose/addon/styles/rose/components/form/_select.scss @@ -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); diff --git a/addons/rose/addon/styles/rose/components/form/errors/_index.scss b/addons/rose/addon/styles/rose/components/form/errors/_index.scss new file mode 100644 index 000000000..f422a3ca5 --- /dev/null +++ b/addons/rose/addon/styles/rose/components/form/errors/_index.scss @@ -0,0 +1,7 @@ +@use '../../../variables/sizing'; + +@import 'message'; + +.rose-form-errors { + margin-bottom: sizing.rems(l); +} diff --git a/addons/rose/addon/styles/rose/components/form/errors/_message.scss b/addons/rose/addon/styles/rose/components/form/errors/_message.scss new file mode 100644 index 000000000..09bf0b317 --- /dev/null +++ b/addons/rose/addon/styles/rose/components/form/errors/_message.scss @@ -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); +} diff --git a/addons/rose/addon/styles/rose/utilities/_button.scss b/addons/rose/addon/styles/rose/utilities/_button.scss index 3bb506024..f708bee45 100644 --- a/addons/rose/addon/styles/rose/utilities/_button.scss +++ b/addons/rose/addon/styles/rose/utilities/_button.scss @@ -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)); } diff --git a/addons/rose/addon/templates/components/rose/button.hbs b/addons/rose/addon/templates/components/rose/button.hbs index f109c1407..a8ac2acbd 100644 --- a/addons/rose/addon/templates/components/rose/button.hbs +++ b/addons/rose/addon/templates/components/rose/button.hbs @@ -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}} - - {{/if}} + + {{#if @iconLeft}} + + {{/if}} - - {{yield}} + + {{yield}} + + + {{#if @iconOnly}} + + {{/if}} + + {{#if @iconRight}} + + {{/if}} - - {{#if @iconOnly}} - - {{/if}} - - {{#if @iconRight}} - - {{/if}} - diff --git a/addons/rose/addon/templates/components/rose/form.hbs b/addons/rose/addon/templates/components/rose/form.hbs new file mode 100644 index 000000000..6c74ccae4 --- /dev/null +++ b/addons/rose/addon/templates/components/rose/form.hbs @@ -0,0 +1,16 @@ + + {{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) + )}} + diff --git a/addons/rose/addon/templates/components/rose/form/actions.hbs b/addons/rose/addon/templates/components/rose/form/actions.hbs new file mode 100644 index 000000000..47557fb7e --- /dev/null +++ b/addons/rose/addon/templates/components/rose/form/actions.hbs @@ -0,0 +1,19 @@ + + + {{@submitText}} + + + {{#if this.showCancel}} + + {{@cancelText}} + + {{/if}} + diff --git a/addons/rose/addon/templates/components/rose/form/checkbox.hbs b/addons/rose/addon/templates/components/rose/form/checkbox.hbs index ab2dec73b..f24ed4478 100644 --- a/addons/rose/addon/templates/components/rose/form/checkbox.hbs +++ b/addons/rose/addon/templates/components/rose/form/checkbox.hbs @@ -3,14 +3,18 @@ + @checked={{if @checked true false}} + aria-describedby="{{if @error (concat 'errors-' this.id)}}" /> - + {{@label}} - {{yield}} + {{yield (hash + errors=(component 'rose/form/errors' id=(concat 'errors-' this.id)) + )}} diff --git a/addons/rose/addon/templates/components/rose/form/errors.hbs b/addons/rose/addon/templates/components/rose/form/errors.hbs new file mode 100644 index 000000000..70e523944 --- /dev/null +++ b/addons/rose/addon/templates/components/rose/form/errors.hbs @@ -0,0 +1,5 @@ + + {{yield (hash + message=(component 'rose/form/errors/message') + )}} + diff --git a/addons/rose/addon/templates/components/rose/form/errors/message.hbs b/addons/rose/addon/templates/components/rose/form/errors/message.hbs new file mode 100644 index 000000000..fb4b2ebe0 --- /dev/null +++ b/addons/rose/addon/templates/components/rose/form/errors/message.hbs @@ -0,0 +1,4 @@ + + + {{yield}} + diff --git a/addons/rose/addon/templates/components/rose/form/input.hbs b/addons/rose/addon/templates/components/rose/form/input.hbs index fe8e86eed..273ce0dc2 100644 --- a/addons/rose/addon/templates/components/rose/form/input.hbs +++ b/addons/rose/addon/templates/components/rose/form/input.hbs @@ -1,20 +1,28 @@ - + {{@label}} {{#if @helperText}} - {{@helperText}} + + {{@helperText}} + {{/if}} + @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)) + )}} diff --git a/addons/rose/addon/templates/components/rose/form/radio/group.hbs b/addons/rose/addon/templates/components/rose/form/radio/group.hbs index 6aa52cf50..76cfea93b 100644 --- a/addons/rose/addon/templates/components/rose/form/radio/group.hbs +++ b/addons/rose/addon/templates/components/rose/form/radio/group.hbs @@ -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) )}} diff --git a/addons/rose/addon/templates/components/rose/form/radio/radio.hbs b/addons/rose/addon/templates/components/rose/form/radio/radio.hbs index 088853d5b..4e38ecb4f 100644 --- a/addons/rose/addon/templates/components/rose/form/radio/radio.hbs +++ b/addons/rose/addon/templates/components/rose/form/radio/radio.hbs @@ -3,14 +3,15 @@ - + {{@label}} diff --git a/addons/rose/addon/templates/components/rose/form/select.hbs b/addons/rose/addon/templates/components/rose/form/select.hbs index f7466ba0f..db870f1fd 100644 --- a/addons/rose/addon/templates/components/rose/form/select.hbs +++ b/addons/rose/addon/templates/components/rose/form/select.hbs @@ -1,22 +1,28 @@ - + {{@label}} {{#if @helperText}} - {{@helperText}} + + {{@helperText}} + {{/if}} - - {{yield xs}} - + {{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)) + )}} diff --git a/addons/rose/addon/templates/components/rose/form/select/select.hbs b/addons/rose/addon/templates/components/rose/form/select/select.hbs new file mode 100644 index 000000000..3d780deaa --- /dev/null +++ b/addons/rose/addon/templates/components/rose/form/select/select.hbs @@ -0,0 +1,12 @@ + + {{yield xs}} + diff --git a/addons/rose/addon/templates/components/rose/form/textarea.hbs b/addons/rose/addon/templates/components/rose/form/textarea.hbs index 94017089a..1d01effa4 100644 --- a/addons/rose/addon/templates/components/rose/form/textarea.hbs +++ b/addons/rose/addon/templates/components/rose/form/textarea.hbs @@ -1,19 +1,27 @@ - + {{@label}} {{#if @helperText}} - {{@helperText}} + + {{@helperText}} + {{/if}} + 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)) + )}} diff --git a/addons/rose/addon/templates/components/rose/notification.hbs b/addons/rose/addon/templates/components/rose/notification.hbs new file mode 100644 index 000000000..04d60df5f --- /dev/null +++ b/addons/rose/addon/templates/components/rose/notification.hbs @@ -0,0 +1,37 @@ + + + + + {{@heading}} + + + + {{yield}} + + + {{#if @dismiss}} + + + + {{@dismissText}} + + + {{/if}} + + diff --git a/addons/rose/app/components/rose/form.js b/addons/rose/app/components/rose/form.js new file mode 100644 index 000000000..69c59c497 --- /dev/null +++ b/addons/rose/app/components/rose/form.js @@ -0,0 +1 @@ +export { default } from 'rose/components/rose/form'; diff --git a/addons/rose/app/components/rose/form/actions.js b/addons/rose/app/components/rose/form/actions.js new file mode 100644 index 000000000..debd30e78 --- /dev/null +++ b/addons/rose/app/components/rose/form/actions.js @@ -0,0 +1 @@ +export { default } from 'rose/components/rose/form/actions'; diff --git a/addons/rose/app/components/rose/form/errors.js b/addons/rose/app/components/rose/form/errors.js new file mode 100644 index 000000000..7acb47a9d --- /dev/null +++ b/addons/rose/app/components/rose/form/errors.js @@ -0,0 +1 @@ +export { default } from 'rose/components/rose/form/errors'; diff --git a/addons/rose/app/components/rose/form/errors/message.js b/addons/rose/app/components/rose/form/errors/message.js new file mode 100644 index 000000000..4a4ac97dd --- /dev/null +++ b/addons/rose/app/components/rose/form/errors/message.js @@ -0,0 +1 @@ +export { default } from 'rose/components/rose/form/errors/message'; diff --git a/addons/rose/app/components/rose/form/select/select.js b/addons/rose/app/components/rose/form/select/select.js new file mode 100644 index 000000000..4fdbc3925 --- /dev/null +++ b/addons/rose/app/components/rose/form/select/select.js @@ -0,0 +1 @@ +export { default } from 'rose/components/rose/form/select/select'; diff --git a/addons/rose/app/components/rose/notification.js b/addons/rose/app/components/rose/notification.js new file mode 100644 index 000000000..228bd468c --- /dev/null +++ b/addons/rose/app/components/rose/notification.js @@ -0,0 +1 @@ +export { default } from 'rose/components/rose/notification'; \ No newline at end of file diff --git a/addons/rose/package.json b/addons/rose/package.json index 4ec6dadf1..ec020554e 100644 --- a/addons/rose/package.json +++ b/addons/rose/package.json @@ -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", diff --git a/addons/rose/tests/dummy/app/controllers/index.js b/addons/rose/tests/dummy/app/controllers/index.js index 3b56b102a..45faa7e5a 100644 --- a/addons/rose/tests/dummy/app/controllers/index.js +++ b/addons/rose/tests/dummy/app/controllers/index.js @@ -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'); } } diff --git a/addons/rose/tests/dummy/app/templates/index.hbs b/addons/rose/tests/dummy/app/templates/index.hbs index b839b6e8b..12fac556c 100644 --- a/addons/rose/tests/dummy/app/templates/index.hbs +++ b/addons/rose/tests/dummy/app/templates/index.hbs @@ -1,205 +1,183 @@ - - - - + - - - Menu item - Menu item - Button menu item - - Section - Section - + + + + + Menu item + Menu item + Button menu item + + Section + Section + - - - Menu item - Menu item - Button menu item - - - - - - - - Dummy app index template - - Icon - - - - - Button - Primary - - Primary - - - Secondary - - Secondary - - - Form - + + Menu item + Menu item + Button menu item + + + + + + +Dummy app index template + + + An error occurred. + + +Icon + + + + +Button +Primary + +Primary + + +Secondary + +Secondary + + +Form + + + Inputs + + + + + + Textareas + + + + + + Selects + + Choose an option Value 1 Value 2 Value 3 - + + + + @label="Label" + @helperText="Helper text" + @onChange={{fn this.actionHandler}} + as |field| + > + Choose an option - - - - - - - - - + + - - - - - - - - - Checkbox - - - - - - - Radio field - - - - - Radio Group + + + + - + /> + - Radio field group - - - - - + Checkboxes + + + + + - - + + diff --git a/addons/rose/tests/integration/components/rose/form-test.js b/addons/rose/tests/integration/components/rose/form-test.js new file mode 100644 index 000000000..3469cc830 --- /dev/null +++ b/addons/rose/tests/integration/components/rose/form-test.js @@ -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` + + + + `); + 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` + + + + + + + + + + + + `); + 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"]')); + }); +}); diff --git a/addons/rose/tests/integration/components/rose/form/actions-test.js b/addons/rose/tests/integration/components/rose/form/actions-test.js new file mode 100644 index 000000000..fa018c7c6 --- /dev/null +++ b/addons/rose/tests/integration/components/rose/form/actions-test.js @@ -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"])'); + }); +}); diff --git a/addons/rose/tests/integration/components/rose/form/checkbox-test.js b/addons/rose/tests/integration/components/rose/form/checkbox-test.js index e341dd0db..6ceb4abf9 100644 --- a/addons/rose/tests/integration/components/rose/form/checkbox-test.js +++ b/addons/rose/tests/integration/components/rose/form/checkbox-test.js @@ -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` + + + An error occurred. + + + `); + 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``); 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``); + test('it is disabled when @disabled={{true}}', async function (assert) { + await render(hbs``); assert.equal(await find('input').disabled, true); }); }); diff --git a/addons/rose/tests/integration/components/rose/form/errors-test.js b/addons/rose/tests/integration/components/rose/form/errors-test.js new file mode 100644 index 000000000..b01cd74d1 --- /dev/null +++ b/addons/rose/tests/integration/components/rose/form/errors-test.js @@ -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` + + An error occurred. + Another error occurred. + /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.' + ); + }); +}); diff --git a/addons/rose/tests/integration/components/rose/form/errors/message-test.js b/addons/rose/tests/integration/components/rose/form/errors/message-test.js new file mode 100644 index 000000000..66662d440 --- /dev/null +++ b/addons/rose/tests/integration/components/rose/form/errors/message-test.js @@ -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` + + An error occurred. + + `); + assert.equal(this.element.textContent.trim(), 'An error occurred.'); + }); +}); diff --git a/addons/rose/tests/integration/components/rose/form/input-test.js b/addons/rose/tests/integration/components/rose/form/input-test.js index 85c78eeb7..424a9cba8 100644 --- a/addons/rose/tests/integration/components/rose/form/input-test.js +++ b/addons/rose/tests/integration/components/rose/form/input-test.js @@ -14,9 +14,32 @@ module('Integration | Component | rose/form/input', function (hooks) { test('it displays optional helper text', async function (assert) { await render(hbs``); + 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` + + + An error occurred. + + + `); + 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``); + await render(hbs``); assert.ok(await find('[disabled]')); }); diff --git a/addons/rose/tests/integration/components/rose/form/radio/group-test.js b/addons/rose/tests/integration/components/rose/form/radio/group-test.js index b3aa9d2f9..b8e6f7fc0 100644 --- a/addons/rose/tests/integration/components/rose/form/radio/group-test.js +++ b/addons/rose/tests/integration/components/rose/form/radio/group-test.js @@ -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` @@ -31,37 +31,37 @@ module('Integration | Component | rose/form/radio/group', function (hooks) { await render(hbs` `); - 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` @@ -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` `); - 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); }); }); diff --git a/addons/rose/tests/integration/components/rose/form/select-test.js b/addons/rose/tests/integration/components/rose/form/select-test.js index 2abd4b463..1f30200fb 100644 --- a/addons/rose/tests/integration/components/rose/form/select-test.js +++ b/addons/rose/tests/integration/components/rose/form/select-test.js @@ -12,12 +12,14 @@ module('Integration | Component | rose/form/select', function (hooks) { - Choose an option - Value 1 - Value 2 - Value 3 + + Choose an option + Value 1 + Value 2 + Value 3 + `); assert.equal(find('label').textContent.trim(), 'Label'); @@ -30,16 +32,51 @@ module('Integration | Component | rose/form/select', function (hooks) { await render(hbs` - Choose an option + + Choose an option + `); + 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` + + + Choose an option + + + An error occurred. + + + `); + 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` - Choose an option + + Choose an option + `); assert.ok(find('[disabled]')); @@ -64,10 +103,12 @@ module('Integration | Component | rose/form/select', function (hooks) { - value-1 - value-2 + + value-1 + value-2 + `); 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` - Choose an option - value-1 + + Choose an option + value-1 + `); await fillIn('[name="my-select"]', 'value-1'); diff --git a/addons/rose/tests/integration/components/rose/form/textarea-test.js b/addons/rose/tests/integration/components/rose/form/textarea-test.js index cce85cc32..f1e5f2a6c 100644 --- a/addons/rose/tests/integration/components/rose/form/textarea-test.js +++ b/addons/rose/tests/integration/components/rose/form/textarea-test.js @@ -14,14 +14,37 @@ module('Integration | Component | rose/form/textarea', function (hooks) { test('it displays optional helper text', async function (assert) { await render(hbs``); + 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` + + + An error occurred. + + + `); + 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``); + await render(hbs``); assert.ok(await find('[disabled]')); }); diff --git a/addons/rose/tests/integration/components/rose/notification-test.js b/addons/rose/tests/integration/components/rose/notification-test.js new file mode 100644 index 000000000..6027644d0 --- /dev/null +++ b/addons/rose/tests/integration/components/rose/notification-test.js @@ -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` + + An error occurred. + + `); + 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` + + `); + await click('.rose-notification-dismiss'); + }); +}); diff --git a/addons/rose/tests/unit/mixins/component-auto-id-test.js b/addons/rose/tests/unit/mixins/component-auto-id-test.js new file mode 100644 index 000000000..a047b3e7b --- /dev/null +++ b/addons/rose/tests/unit/mixins/component-auto-id-test.js @@ -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); + }); +}); diff --git a/ui/core/app/components/form/project/index.hbs b/ui/core/app/components/form/project/index.hbs index e3279fd7d..3842fc226 100644 --- a/ui/core/app/components/form/project/index.hbs +++ b/ui/core/app/components/form/project/index.hbs @@ -5,7 +5,6 @@ {{on "submit" @submit}}>
+ + {{yield}} +