Merge pull request #319 from hashicorp/feature-dialog
feat: 🎸 add dialog component with modal capabilities
This commit is contained in:
12 files changed
+361
No files matched your search
@@ -0,0 +1,3 @@
|
||||
<div class="rose-dialog-body" tabindex="0">
|
||||
{{yield}}
|
||||
</div>
|
||||
@@ -0,0 +1,75 @@
|
||||
{{#if @modal}}
|
||||
<div class="rose-dialog-modal-overlay"></div>
|
||||
|
||||
<section
|
||||
...attributes
|
||||
role="dialog"
|
||||
class="rose-dialog{{if @modal ' modal'}}"
|
||||
{{focus-trap}}
|
||||
>
|
||||
|
||||
{{#if @heading}}
|
||||
<header class="rose-dialog-header">
|
||||
|
||||
{{#if @icon}}
|
||||
<Rose::Icon @name={{@icon}} @size="medium" />
|
||||
{{/if}}
|
||||
|
||||
<h2 class="rose-dialog-heading">{{@heading}}</h2>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="rose-dialog-dismiss"
|
||||
title="Dismiss"
|
||||
{{on "click" (fn this.dismiss @dismiss)}}
|
||||
>
|
||||
<Rose::Icon @name="cancel-plain" @size="small" />
|
||||
</button>
|
||||
|
||||
</header>
|
||||
{{/if}}
|
||||
|
||||
{{yield (hash
|
||||
body=(component 'rose/dialog/body')
|
||||
footer=(component 'rose/anonymous' tagName="footer" class="rose-dialog-footer")
|
||||
)}}
|
||||
|
||||
</section>
|
||||
|
||||
{{else}}
|
||||
|
||||
<section
|
||||
...attributes
|
||||
role="dialog"
|
||||
class="rose-dialog{{if @modal ' modal'}}"
|
||||
>
|
||||
|
||||
{{#if @heading}}
|
||||
<header class="rose-dialog-header">
|
||||
|
||||
{{#if @icon}}
|
||||
<Rose::Icon @name={{@icon}} @size="medium" />
|
||||
{{/if}}
|
||||
|
||||
<h2 class="rose-dialog-heading">{{@heading}}</h2>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="rose-dialog-dismiss"
|
||||
title="Dismiss"
|
||||
{{on "click" (fn this.dismiss @dismiss)}}
|
||||
>
|
||||
<Rose::Icon @name="cancel-plain" @size="small" />
|
||||
</button>
|
||||
|
||||
</header>
|
||||
{{/if}}
|
||||
|
||||
{{yield (hash
|
||||
body=(component 'rose/dialog/body')
|
||||
footer=(component 'rose/anonymous' tagName="footer" class="rose-dialog-footer")
|
||||
)}}
|
||||
|
||||
</section>
|
||||
|
||||
{{/if}}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
<Meta title="Rose|Dialog" component="Dialog" />
|
||||
|
||||
# Dialog
|
||||
|
||||
A dialog component with optional modal capabilities.
|
||||
|
||||
<Preview>
|
||||
<Story name="Dialog" height="15rem">{{
|
||||
template: hbs`
|
||||
<Rose::Dialog
|
||||
@modal={{modal}}
|
||||
@dismiss={{dismiss}}
|
||||
@icon={{icon}}
|
||||
@heading={{headingText}}
|
||||
as |dialog|>
|
||||
<dialog.body>
|
||||
Dialog Body
|
||||
</dialog.body>
|
||||
<dialog.footer>
|
||||
Footer
|
||||
</dialog.footer>
|
||||
</Rose::Dialog>
|
||||
`,
|
||||
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'),
|
||||
},
|
||||
}}</Story>
|
||||
</Preview>
|
||||
|
||||
|
||||
```htmlbars
|
||||
<Rose::Dialog
|
||||
@modal={{true}}
|
||||
@dismiss={{@dismissFn}}
|
||||
@icon="check-circle-outline"
|
||||
@heading="Heading"
|
||||
as |dialog|>
|
||||
<dialog.body>
|
||||
Dialog Body
|
||||
</dialog.body>
|
||||
<dialog.footer>
|
||||
Footer
|
||||
</dialog.footer>
|
||||
</Rose::Dialog>
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
@import 'button';
|
||||
@import 'dialog';
|
||||
@import 'dropdown';
|
||||
@import 'form';
|
||||
@import 'header';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'rose/components/rose/dialog';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'rose/components/rose/dialog/body';
|
||||
@@ -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",
|
||||
|
||||
@@ -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`
|
||||
<Rose::Dialog
|
||||
@dismiss={{this.dismiss}}
|
||||
@heading="Heading"
|
||||
as |dialog|>
|
||||
<dialog.body>Dialog</dialog.body>
|
||||
<dialog.footer>Dialog</dialog.footer>
|
||||
</Rose::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');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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`<Rose::Dialog::Body />`);
|
||||
|
||||
assert.equal(this.element.textContent.trim(), '');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
<Rose::Dialog::Body>
|
||||
template block text
|
||||
</Rose::Dialog::Body>
|
||||
`);
|
||||
|
||||
assert.equal(this.element.textContent.trim(), 'template block text');
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user