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}} +
+ + + +{{else}} + + + +{{/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"