diff --git a/addons/rose/addon/components/rose/dialog/body/index.hbs b/addons/rose/addon/components/rose/dialog/body/index.hbs
new file mode 100644
index 000000000..143d0758f
--- /dev/null
+++ b/addons/rose/addon/components/rose/dialog/body/index.hbs
@@ -0,0 +1,3 @@
+
+ {{yield}}
+
diff --git a/addons/rose/addon/components/rose/dialog/index.hbs b/addons/rose/addon/components/rose/dialog/index.hbs
new file mode 100644
index 000000000..a80d3fcb9
--- /dev/null
+++ b/addons/rose/addon/components/rose/dialog/index.hbs
@@ -0,0 +1,75 @@
+{{#if @modal}}
+
+
+
+
+ {{#if @heading}}
+
+ {{/if}}
+
+ {{yield (hash
+ body=(component 'rose/dialog/body')
+ footer=(component 'rose/anonymous' tagName="footer" class="rose-dialog-footer")
+ )}}
+
+
+
+{{else}}
+
+
+
+ {{#if @heading}}
+
+ {{/if}}
+
+ {{yield (hash
+ body=(component 'rose/dialog/body')
+ footer=(component 'rose/anonymous' tagName="footer" class="rose-dialog-footer")
+ )}}
+
+
+
+{{/if}}
diff --git a/addons/rose/addon/components/rose/dialog/index.js b/addons/rose/addon/components/rose/dialog/index.js
new file mode 100644
index 000000000..241ed5ba7
--- /dev/null
+++ b/addons/rose/addon/components/rose/dialog/index.js
@@ -0,0 +1,19 @@
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+
+export default class RoseDialogComponent extends Component {
+
+ // =methods
+
+ /**
+ * Calls the passed dismiss function. This approach is convenient
+ * in case an external dismiss function is not passed into the component,
+ * a template error will not be thrown.
+ * @param {Function} fn
+ */
+ @action
+ dismiss(fn) {
+ fn();
+ }
+
+}
diff --git a/addons/rose/addon/components/rose/dialog/index.stories.mdx b/addons/rose/addon/components/rose/dialog/index.stories.mdx
new file mode 100644
index 000000000..2d4e68f6f
--- /dev/null
+++ b/addons/rose/addon/components/rose/dialog/index.stories.mdx
@@ -0,0 +1,63 @@
+
+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';
+
+
+
+# Dialog
+
+A dialog component with optional modal capabilities.
+
+
+ {{
+ template: hbs`
+
+
+ Dialog Body
+
+
+ Footer
+
+
+ `,
+ context: {
+ modal: boolean('modal', false),
+ headingText: text('headingText', 'Dialog Heading Text'),
+ icon: select(
+ 'icon',
+ {
+ 'check-circle-outline': 'check-circle-outline',
+ 'copy-action': 'copy-action',
+ 'unfold-more': 'unfold-more',
+ 'unfold-less': 'unfold-less',
+ },
+ 'check-circle-outline'
+ ),
+ dismiss: action('dismiss'),
+ },
+ }}
+
+
+
+```htmlbars
+
+
+ Dialog Body
+
+
+ Footer
+
+
+```
diff --git a/addons/rose/addon/styles/rose/components/_dialog.scss b/addons/rose/addon/styles/rose/components/_dialog.scss
new file mode 100644
index 000000000..692d2f4f9
--- /dev/null
+++ b/addons/rose/addon/styles/rose/components/_dialog.scss
@@ -0,0 +1,120 @@
+@use '../variables/sizing';
+@use '../utilities/type';
+@use '../variables/media';
+
+$m: sizing.rems(m); // 16
+$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
+
+.rose-dialog {
+ border: $xxxxs solid var(--ui-gray-subtler-3);
+ border-radius: 2px;
+ background-color: var(--subtle);
+ box-shadow: 0 $xs $s rgba(var(--stark-components), var(--opacity-2)),
+ 0 $xxs $xxs rgba(var(--stark-components), var(--opacity-3));
+
+ .rose-dialog-header {
+ display: flex;
+ align-items: center;
+ padding: ($xs + $xxxs) $m;
+ min-height: sizing.rems(l);
+ color: var(--ui-gray-starker-3);
+ background-color: var(--ui-gray-subtler-5);
+ border-bottom: $xxxxs solid var(--ui-gray-subtler-2);
+
+ .rose-icon {
+ color: currentColor;
+ margin-right: $xxs + $xxxs;
+
+ svg {
+ transform: scale(1.25);
+ position: relative;
+ top: $xxxxs;
+ }
+ }
+
+ .rose-dialog-heading {
+ @include type.type(m, semibold);
+ flex-grow: 1;
+ margin: 0;
+ padding: 0;
+ color: currentColor;
+ }
+
+ .rose-dialog-dismiss {
+ border: $xxxxs solid var(--ui-gray-subtler-2);
+ border-radius: $xxxs;
+ color: var(--ui-gray);
+ background-color: transparent;
+ padding: 0;
+ margin: 0;
+ height: sizing.rems(l);
+ width: sizing.rems(l);
+
+ &:hover {
+ color: var(--ui-gray-starker-2);
+ border-color: currentColor;
+ }
+
+ .rose-icon {
+ margin: 0;
+
+ svg {
+ top: $xxxs;
+ }
+ }
+ }
+ }
+
+ .rose-dialog-body {
+ margin: sizing.rems(l);
+ }
+
+ .rose-dialog-footer {
+ padding: $m;
+ border-top: $xxxxs solid var(--ui-gray-subtler-2);
+ background-color: var(--ui-gray-subtler-5);
+
+ button {
+ margin-bottom: 0;
+ }
+ }
+}
+
+@media (min-width: media.width(large)) {
+ .rose-dialog {
+ &.modal {
+ display: flex;
+ flex-direction: column;
+
+ position: fixed;
+
+ left: calc(50% - 300px);
+ top: 5%;
+
+ max-height: 85%;
+ max-width: 675px;
+ width: 100%;
+
+ .rose-dialog-body {
+ overflow-y: auto;
+ }
+ }
+ }
+ // overlay
+ .rose-dialog-modal-overlay {
+ content: " ";
+ background-color: var(--black);
+ width: 100vw;
+ height: 100vh;
+ position: fixed;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ opacity: 0.5;
+ }
+}
diff --git a/addons/rose/addon/styles/rose/components/_index.scss b/addons/rose/addon/styles/rose/components/_index.scss
index 6e6ef8490..a20eeacaf 100644
--- a/addons/rose/addon/styles/rose/components/_index.scss
+++ b/addons/rose/addon/styles/rose/components/_index.scss
@@ -1,4 +1,5 @@
@import 'button';
+@import 'dialog';
@import 'dropdown';
@import 'form';
@import 'header';
diff --git a/addons/rose/app/components/rose/dialog.js b/addons/rose/app/components/rose/dialog.js
new file mode 100644
index 000000000..6d1f33374
--- /dev/null
+++ b/addons/rose/app/components/rose/dialog.js
@@ -0,0 +1 @@
+export { default } from 'rose/components/rose/dialog';
\ No newline at end of file
diff --git a/addons/rose/app/components/rose/dialog/body.js b/addons/rose/app/components/rose/dialog/body.js
new file mode 100644
index 000000000..53ba1e1f2
--- /dev/null
+++ b/addons/rose/app/components/rose/dialog/body.js
@@ -0,0 +1 @@
+export { default } from 'rose/components/rose/dialog/body';
\ No newline at end of file
diff --git a/addons/rose/package.json b/addons/rose/package.json
index ab1cf15e4..3aeed0231 100644
--- a/addons/rose/package.json
+++ b/addons/rose/package.json
@@ -53,6 +53,7 @@
"ember-cli-update": "^0.54.6",
"ember-disable-prototype-extensions": "^1.1.3",
"ember-export-application-global": "^2.0.1",
+ "ember-focus-trap": "^0.4.0",
"ember-inline-svg": "^1.0.0",
"ember-load-initializers": "^2.1.1",
"ember-maybe-import-regenerator": "^0.1.6",
diff --git a/addons/rose/tests/integration/components/rose/dialog-test.js b/addons/rose/tests/integration/components/rose/dialog-test.js
new file mode 100644
index 000000000..a3fe4c8d6
--- /dev/null
+++ b/addons/rose/tests/integration/components/rose/dialog-test.js
@@ -0,0 +1,28 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find, click } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Component | rose/dialog', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders and handles dismiss actions', async function (assert) {
+ assert.expect(5);
+ this.dismiss = () => assert.ok(true, 'dismissed');
+ await render(hbs`
+
+ Dialog
+ Dialog
+
+ `);
+ assert.ok(find('.rose-dialog'));
+ assert.ok(find('.rose-dialog-header'));
+ assert.ok(find('.rose-dialog-body'));
+ assert.ok(find('.rose-dialog-footer'));
+ await click('button');
+ });
+
+});
diff --git a/addons/rose/tests/integration/components/rose/dialog/body-test.js b/addons/rose/tests/integration/components/rose/dialog/body-test.js
new file mode 100644
index 000000000..28376e933
--- /dev/null
+++ b/addons/rose/tests/integration/components/rose/dialog/body-test.js
@@ -0,0 +1,26 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Component | rose/dialog/body', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function(assert) {
+ // Set any properties with this.set('myProperty', 'value');
+ // Handle any actions with this.set('myAction', function(val) { ... });
+
+ await render(hbs``);
+
+ assert.equal(this.element.textContent.trim(), '');
+
+ // Template block usage:
+ await render(hbs`
+
+ template block text
+
+ `);
+
+ assert.equal(this.element.textContent.trim(), 'template block text');
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index f0c68f52c..2a1b696a0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10355,6 +10355,16 @@ ember-fetch@^8.0.1:
node-fetch "^2.6.0"
whatwg-fetch "^3.0.0"
+ember-focus-trap@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/ember-focus-trap/-/ember-focus-trap-0.4.0.tgz#b69a3a65a49784e2b3a19082655a585cfdaad473"
+ integrity sha512-NHKjTS1xoSS+fURDhAp5oyR7ZTTBnC4KVGw/VlUbFolx7jdLCePHuQdhnrhT02B1VP1dlX2RxnNvNiT2IcoA1w==
+ dependencies:
+ ember-auto-import "^1.5.3"
+ ember-cli-babel "^7.18.0"
+ ember-modifier-manager-polyfill "^1.2.0"
+ focus-trap "^5.1.0"
+
ember-get-config@0.2.4, ember-get-config@^0.2.2, ember-get-config@^0.2.4:
version "0.2.4"
resolved "https://registry.yarnpkg.com/ember-get-config/-/ember-get-config-0.2.4.tgz#118492a2a03d73e46004ed777928942021fe1ecd"
@@ -12143,6 +12153,14 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
+focus-trap@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-5.1.0.tgz#64a0bfabd95c382103397dbc96bfef3a3cf8e5ad"
+ integrity sha512-CkB/nrO55069QAUjWFBpX6oc+9V90Qhgpe6fBWApzruMq5gnlh90Oo7iSSDK7pKiV5ugG6OY2AXM5mxcmL3lwQ==
+ dependencies:
+ tabbable "^4.0.0"
+ xtend "^4.0.1"
+
follow-redirects@^1.0.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.11.0.tgz#afa14f08ba12a52963140fe43212658897bc0ecb"
@@ -21067,6 +21085,11 @@ sync-disk-cache@^2.0.0:
rimraf "^3.0.0"
username-sync "^1.0.2"
+tabbable@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-4.0.0.tgz#5bff1d1135df1482cf0f0206434f15eadbeb9261"
+ integrity sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==
+
table@^3.7.8:
version "3.8.3"
resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f"