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/styles/rose/components/form/_actions.scss b/addons/rose/addon/styles/rose/components/form/_actions.scss
new file mode 100644
index 000000000..4c2d62a0a
--- /dev/null
+++ b/addons/rose/addon/styles/rose/components/form/_actions.scss
@@ -0,0 +1,3 @@
+.rose-form-actions {
+ //display: flex;
+}
diff --git a/addons/rose/addon/styles/rose/components/form/_index.scss b/addons/rose/addon/styles/rose/components/form/_index.scss
index c8c516f8c..29fa5e995 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 'actions';
diff --git a/addons/rose/addon/styles/rose/components/form/_input.scss b/addons/rose/addon/styles/rose/components/form/_input.scss
index 7116c6a76..0895f7f17 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);
diff --git a/addons/rose/addon/styles/rose/components/form/_select.scss b/addons/rose/addon/styles/rose/components/form/_select.scss
index 63cb16911..9effb0b80 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);
diff --git a/addons/rose/addon/styles/rose/utilities/_button.scss b/addons/rose/addon/styles/rose/utilities/_button.scss
index 3bb506024..f286ef5be 100644
--- a/addons/rose/addon/styles/rose/utilities/_button.scss
+++ b/addons/rose/addon/styles/rose/utilities/_button.scss
@@ -4,32 +4,39 @@
@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 {
- margin-left: -(sizing.rems(xs));
- margin-right: sizing.rems(xxs);
- }
+ .rose-button-wrapper {
+ display: flex;
+ flex-direction: row;
- &.has-icon-right > .rose-icon:last-child {
- margin-left: sizing.rems(xxs);
- margin-right: -(sizing.rems(xs));
- }
+ &.has-icon-left > .rose-icon:first-child {
+ margin-left: -(sizing.rems(xs));
+ margin-right: sizing.rems(xxs);
+ }
- &.has-icon-only {
- padding-right: sizing.rems(s);
- padding-left: sizing.rems(s);
- .rose-button-body {
- position: absolute;
- top: -1000px;
- left: -1000px;
- width: 1px;
- height: 1px;
- opacity: 0;
+ &.has-icon-right > .rose-icon:last-child {
+ margin-left: sizing.rems(xxs);
+ margin-right: -(sizing.rems(xs));
+ }
+
+ &.has-icon-only {
+ padding-right: sizing.rems(s);
+ padding-left: sizing.rems(s);
+ .rose-button-body {
+ position: absolute;
+ top: -1000px;
+ left: -1000px;
+ width: 1px;
+ height: 1px;
+ opacity: 0;
+ }
}
}
}
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/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/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/tests/dummy/app/controllers/index.js b/addons/rose/tests/dummy/app/controllers/index.js
index 3b56b102a..ff0929bb6 100644
--- a/addons/rose/tests/dummy/app/controllers/index.js
+++ b/addons/rose/tests/dummy/app/controllers/index.js
@@ -3,7 +3,7 @@ import { action } from '@ember/object';
export default class IndexController extends Controller {
@action
- selectChange(/*value*/) {
+ actionHandler(/*value*/) {
// eslint-disable-next-line no-console
//console.log(value);
}
diff --git a/addons/rose/tests/dummy/app/templates/index.hbs b/addons/rose/tests/dummy/app/templates/index.hbs
index 400f1d6f1..40586ca08 100644
--- a/addons/rose/tests/dummy/app/templates/index.hbs
+++ b/addons/rose/tests/dummy/app/templates/index.hbs
@@ -50,7 +50,7 @@ Dummy app index template
Choose an option
@@ -63,12 +63,18 @@ Dummy app index template
disabled={{true}}
@label="Label"
@helperText="Helper text"
- @onChange={{fn this.selectChange}}
+ @onChange={{fn this.actionHandler}}
as |select|
>
Choose an option
+Form Actions
+
+
{};
+ 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"])');
+ });
+});