diff --git a/ui/core/app/components/copy-button/index.hbs b/ui/core/app/components/copy-button/index.hbs
index fb5c4b157..94dca5ce1 100644
--- a/ui/core/app/components/copy-button/index.hbs
+++ b/ui/core/app/components/copy-button/index.hbs
@@ -1 +1,19 @@
-{{yield}}
\ No newline at end of file
+{{#if this.isClipboardLibrarySupported}}
+
+
+ {{yield}}
+
+
+
+
+{{else}}
+ {{yield}}
+{{/if}}
diff --git a/ui/core/app/components/copy-button/index.js b/ui/core/app/components/copy-button/index.js
index 1e32d2663..33f444ed0 100644
--- a/ui/core/app/components/copy-button/index.js
+++ b/ui/core/app/components/copy-button/index.js
@@ -1,4 +1,76 @@
import Component from '@glimmer/component';
+import ClipboardJS from 'clipboard';
+import { action } from '@ember/object';
+import { tracked } from '@glimmer/tracking';
+import { generateComponentID } from 'rose/utilities/component-auto-id';
+import { task, timeout } from 'ember-concurrency';
+import { set } from '@ember/object';
+import { bind } from '@ember/runloop';
export default class CopyButtonComponent extends Component {
+
+ // =attributes
+
+ id = generateComponentID();
+ targetId = `copy-target-${this.id}`;
+ icon = 'copy-action';
+
+ @tracked clipboard;
+
+ /**
+ * A Ember Concurrency-based task that updates copy icon to a success state
+ * and back after 1 second. This icon flash indicates success on
+ * copy to clipboard action.
+ *
+ * NOTE: tasks are sort of attributes and sort of methods, but they are not
+ * language-level constructs. Thus we annotate this task as if it
+ * is an attribute.
+ * @type {Task}
+ */
+ @task(function * () {
+ set(this, 'icon', 'copy-success');
+ yield timeout(1000);
+ set(this, 'icon', 'copy-action');
+ }).drop() successIconTimer;
+
+ // =methods
+
+ /**
+ * Checks for ClipboardJS support.
+ * @return {boolean}
+ */
+ get isClipboardLibrarySupported() {
+ return ClipboardJS?.isSupported();
+ }
+
+ /**
+ * When copy to clipboard is successful, begin timer to show success icon.
+ */
+ onCopySuccess(clipboardEvent) {
+ clipboardEvent.clearSelection();
+ this.successIconTimer.perform();
+ }
+
+ // =actions
+
+ /**
+ * Initialize ClipboardJS library and register for it's events.
+ */
+ @action
+ register() {
+ if(!this.clipboard) {
+ this.clipboard = new ClipboardJS('.copy-button');
+ this.clipboard.on('success', bind(this, this.onCopySuccess));
+ }
+ }
+
+ /**
+ * Destroy ClipboardJS library instance and cancel icon timer.
+ */
+ @action
+ destroy() {
+ this.clipboard?.destroy();
+ this.successIconTimer.cancelAll();
+ }
+
}
diff --git a/ui/core/package.json b/ui/core/package.json
index b5fac0049..6b6547fbd 100644
--- a/ui/core/package.json
+++ b/ui/core/package.json
@@ -29,6 +29,7 @@
"devDependencies": {
"@babel/core": "^7.9.6",
"@ember/optional-features": "^1.3.0",
+ "@ember/render-modifiers": "^1.0.2",
"@glimmer/component": "^1.0.0",
"@glimmer/tracking": "^1.0.0",
"api": "*",
@@ -36,6 +37,7 @@
"babel-eslint": "^10.0.3",
"babel-loader": "^8.1.0",
"broccoli-asset-rev": "^3.0.0",
+ "clipboard": "^2.0.6",
"doctoc": "^1.4.0",
"ember-a11y-testing": "^3.0.0",
"ember-auto-import": "^1.5.3",
@@ -84,8 +86,7 @@
"qunit-dom": "^1.0.0",
"rose": "*",
"sass": "^1.26.5",
- "sass-lint": "^1.13.1",
- "clipboard": "^2.0.6"
+ "sass-lint": "^1.13.1"
},
"engines": {
"node": "10.* || >= 12"
diff --git a/ui/core/tests/integration/components/copy-button-test.js b/ui/core/tests/integration/components/copy-button-test.js
index 428596788..be5d05a01 100644
--- a/ui/core/tests/integration/components/copy-button-test.js
+++ b/ui/core/tests/integration/components/copy-button-test.js
@@ -1,26 +1,14 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
-import { render } from '@ember/test-helpers';
+import { render, find } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | copy-button', 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');
+ await render(hbs`text`);
+ assert.ok(find('.copy-button'));
+ assert.equal(find('.copy-target').textContent.trim(), 'text');
});
});
diff --git a/ui/core/translations/actions/en-us.yaml b/ui/core/translations/actions/en-us.yaml
index 40a38941d..5598ee942 100644
--- a/ui/core/translations/actions/en-us.yaml
+++ b/ui/core/translations/actions/en-us.yaml
@@ -28,3 +28,4 @@ add-hosts: Add Hosts
set-password: Set Password
change-password: Change Password
edit-form: Edit Form
+copy: Copy
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index e8826e237..f13b0ae16 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2543,6 +2543,14 @@
ember-cli-babel "^6.16.0"
ember-compatibility-helpers "^1.1.1"
+"@ember/render-modifiers@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@ember/render-modifiers/-/render-modifiers-1.0.2.tgz#2e87c48db49d922ce4850d707215caaac60d8444"
+ integrity sha512-6tEnHl5+62NTSAG2mwhGMFPhUrJQjoVqV+slsn+rlTknm2Zik+iwxBQEbwaiQOU1FUYxkS8RWcieovRNMR8inQ==
+ dependencies:
+ ember-cli-babel "^7.10.0"
+ ember-modifier-manager-polyfill "^1.1.0"
+
"@ember/test-helpers@^1.7.0", "@ember/test-helpers@^1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@ember/test-helpers/-/test-helpers-1.7.1.tgz#cc22a954b3b46856518f034bd492a74e0482389f"
@@ -9601,7 +9609,7 @@ ember-maybe-import-regenerator@^0.1.5, ember-maybe-import-regenerator@^0.1.6:
ember-cli-babel "^6.0.0-beta.4"
regenerator-runtime "^0.9.5"
-ember-modifier-manager-polyfill@^1.2.0:
+ember-modifier-manager-polyfill@^1.1.0, ember-modifier-manager-polyfill@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/ember-modifier-manager-polyfill/-/ember-modifier-manager-polyfill-1.2.0.tgz#cf4444e11a42ac84f5c8badd85e635df57565dda"
integrity sha512-bnaKF1LLKMkBNeDoetvIJ4vhwRPKIIumWr6dbVuW6W6p4QV8ZiO+GdF8J7mxDNlog9CeL9Z/7wam4YS86G8BYA==