diff --git a/ui/packages/consul-ui/.docfy-config.js b/ui/packages/consul-ui/.docfy-config.js
index 419e40827d..cf6d67d012 100644
--- a/ui/packages/consul-ui/.docfy-config.js
+++ b/ui/packages/consul-ui/.docfy-config.js
@@ -2,6 +2,7 @@ const path = require('path');
const autolinkHeadings = require('remark-autolink-headings');
const refractor = require('refractor');
+const gherkin = require('refractor/lang/gherkin');
const prism = require('@mapbox/rehype-prism');
const fs = require('fs');
@@ -24,6 +25,7 @@ if($CONSUL_DOCFY_CONFIG.length > 0) {
}
}
+refractor.register(gherkin);
refractor.alias('handlebars', 'hbs');
refractor.alias('shell', 'sh');
diff --git a/ui/packages/consul-ui/app/components/buttons/index.scss b/ui/packages/consul-ui/app/components/buttons/index.scss
index 9bf7723bdf..d7112051d6 100644
--- a/ui/packages/consul-ui/app/components/buttons/index.scss
+++ b/ui/packages/consul-ui/app/components/buttons/index.scss
@@ -16,6 +16,3 @@ button.type-cancel {
%app-view-content form button[type='button'].type-delete {
@extend %dangerous-button;
}
-button.copy-btn {
- @extend %copy-button;
-}
diff --git a/ui/packages/consul-ui/app/components/buttons/layout.scss b/ui/packages/consul-ui/app/components/buttons/layout.scss
index baea95be22..763e95f7a8 100644
--- a/ui/packages/consul-ui/app/components/buttons/layout.scss
+++ b/ui/packages/consul-ui/app/components/buttons/layout.scss
@@ -34,30 +34,6 @@
padding-top: calc(0.4em - 1px) !important;
padding-bottom: calc(0.4em - 1px) !important;
}
-%copy-button:empty {
- padding: 0px !important;
- margin-right: 0;
- top: -1px;
-}
-%copy-button:empty::after {
- content: '';
- display: none;
- position: absolute;
- top: -2px;
- left: -3px;
- width: 20px;
- height: 22px;
-}
-%copy-button:empty:hover::after {
- display: block;
-}
-%copy-button:empty::before {
- position: relative;
- z-index: 1;
-}
-%copy-button:not(:empty)::before {
- margin-right: 4px;
-}
%internal-button {
padding: 0.9em 1em;
text-align: center;
diff --git a/ui/packages/consul-ui/app/components/buttons/skin.scss b/ui/packages/consul-ui/app/components/buttons/skin.scss
index cf554e372c..ad14c1fe27 100644
--- a/ui/packages/consul-ui/app/components/buttons/skin.scss
+++ b/ui/packages/consul-ui/app/components/buttons/skin.scss
@@ -11,20 +11,6 @@
cursor: default;
box-shadow: none;
}
-%copy-button {
- @extend %button;
- min-height: 17px;
-}
-%copy-button::before {
- @extend %with-copy-action-mask, %as-pseudo;
- background-color: var(--gray-500);
-}
-%copy-button::after {
- background-color: var(--gray-050);
-}
-%copy-button:not(:empty)::before {
- margin-right: 10px;
-}
%primary-button,
%secondary-button,
%dangerous-button {
@@ -34,22 +20,6 @@
box-shadow: $decor-elevation-300;
}
/* color */
-%copy-button {
- color: $color-action;
- background-color: $color-transparent;
-}
-%copy-button:hover:not(:disabled):not(:active),
-%copy-button:focus {
- /*frame-grey frame-blue*/
- color: $color-action;
- background-color: $gray-050;
-}
-%copy-button:hover::before {
- background-color: $blue-500;
-}
-%copy-button:active {
- background-color: $gray-200;
-}
%primary-button {
@extend %frame-blue-800;
}
diff --git a/ui/packages/consul-ui/app/components/copy-button/README.mdx b/ui/packages/consul-ui/app/components/copy-button/README.mdx
index e176fc2760..7f45737fdb 100644
--- a/ui/packages/consul-ui/app/components/copy-button/README.mdx
+++ b/ui/packages/consul-ui/app/components/copy-button/README.mdx
@@ -1,35 +1,43 @@
# CopyButton
-```hbs preview-template
-
- Icon Only:
-
-
+Button component used for copy-to-clipboard functionality so the user can easily copy specified text to their clipboard, along with tooltip-like notifications so the user has some sort of feedback to know the value has been copied.
-
- Icon and text:
-
-
- Copy me!
-
+This component is essentially a composition of our `{{with-copyable}}` modifier, our `{{tooltip}}` modifier plus specific Consul-flavored visual treatment. This is all glued together with our ` ` component to manage states.
+
+Can be used inline to render only a small icon for the button with no other text.
+
+```hbs preview-template
+
+ Icon only
+
+
+
+
+
+
+ Icon and text
+
+
+ Copy me!
+
+
```
-### Arguments
+## Arguments
| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `String` | | The string to be copied to the clipboard on click |
-| `name` | `String` | | The 'Name' of the string to be copied. Mainly used for giving feedback to the user |
+| `name` | `String` | | The 'Name' of the string to be copied. Mainly used for accessibility reasons and giving feedback to the user |
-This component renders a simple button, when clicked copies the value (the `@value` attribute) to the users clipboard. A simple piece of feedback is given to the user in the form of a tooltip. When used inline an empty button is rendered.
-### See
+## See
- [Component Source Code](./index.js)
- [Template Source Code](./index.hbs)
diff --git a/ui/packages/consul-ui/app/components/copy-button/index.hbs b/ui/packages/consul-ui/app/components/copy-button/index.hbs
index bebe79c7ca..160e2582c1 100644
--- a/ui/packages/consul-ui/app/components/copy-button/index.hbs
+++ b/ui/packages/consul-ui/app/components/copy-button/index.hbs
@@ -1,29 +1,30 @@
-
-
+
+{{#let (fn dispatch 'SUCCESS') (fn dispatch 'ERROR') (fn dispatch 'RESET') as |success error reset|}}
{{~yield~}}
+{{/let}}
diff --git a/ui/packages/consul-ui/app/components/copy-button/index.js b/ui/packages/consul-ui/app/components/copy-button/index.js
index 77b960d736..d107619178 100644
--- a/ui/packages/consul-ui/app/components/copy-button/index.js
+++ b/ui/packages/consul-ui/app/components/copy-button/index.js
@@ -1,29 +1,9 @@
import Component from '@glimmer/component';
-import { inject as service } from '@ember/service';
-import { action } from '@ember/object';
import chart from './chart.xstate';
export default class CopyButton extends Component {
- @service('clipboard/os') clipboard;
- @service('dom') dom;
-
constructor() {
super(...arguments);
this.chart = chart;
- this.guid = this.dom.guid(this);
- this._listeners = this.dom.listeners();
- }
-
- @action
- connect() {
- this._listeners.add(this.clipboard.execute(`#${this.guid} button`), {
- success: () => this.dispatch('SUCCESS'),
- error: () => this.dispatch('ERROR'),
- });
- }
-
- @action
- disconnect() {
- this._listeners.remove();
}
}
diff --git a/ui/packages/consul-ui/app/components/copy-button/index.scss b/ui/packages/consul-ui/app/components/copy-button/index.scss
new file mode 100644
index 0000000000..12a2b759c9
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/copy-button/index.scss
@@ -0,0 +1,8 @@
+@import './skin';
+@import './layout';
+%copy-button {
+ @extend %button;
+}
+.copy-button button {
+ @extend %copy-button;
+}
diff --git a/ui/packages/consul-ui/app/components/copy-button/layout.scss b/ui/packages/consul-ui/app/components/copy-button/layout.scss
new file mode 100644
index 0000000000..b2b2c9ee6b
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/copy-button/layout.scss
@@ -0,0 +1,29 @@
+%copy-button {
+ min-height: 17px;
+}
+%copy-button:empty {
+ padding: 0px !important;
+ margin-right: 0;
+ top: -1px;
+}
+/* this is used to provide a small background to the icon only buttons */
+/* without knocking out any positioning when you hover over */
+%copy-button:empty::after {
+ content: '';
+ display: none;
+ position: absolute;
+ top: -2px;
+ left: -3px;
+ width: 20px;
+ height: 22px;
+}
+%copy-button:empty:hover::after {
+ display: block;
+}
+%copy-button:empty::before {
+ position: relative;
+ z-index: 1;
+}
+%copy-button:not(:empty)::before {
+ margin-right: 4px;
+}
diff --git a/ui/packages/consul-ui/app/components/copy-button/skin.scss b/ui/packages/consul-ui/app/components/copy-button/skin.scss
new file mode 100644
index 0000000000..836cb2bd8b
--- /dev/null
+++ b/ui/packages/consul-ui/app/components/copy-button/skin.scss
@@ -0,0 +1,22 @@
+%copy-button {
+ color: var(--blue-500);
+ background-color: var(--transparent);
+}
+%copy-button::before {
+ @extend %with-copy-action-mask, %as-pseudo;
+ background-color: var(--gray-500);
+}
+%copy-button::after {
+ background-color: var(--gray-050);
+}
+%copy-button:hover:not(:disabled):not(:active),
+%copy-button:focus {
+ color: var(--blue-500);
+ background-color: var(--gray-050);
+}
+%copy-button:hover::before {
+ background-color: var(--blue-500);
+}
+%copy-button:active {
+ background-color: var(--gray-200);
+}
diff --git a/ui/packages/consul-ui/app/modifiers/with-copyable.js b/ui/packages/consul-ui/app/modifiers/with-copyable.js
new file mode 100644
index 0000000000..562f058755
--- /dev/null
+++ b/ui/packages/consul-ui/app/modifiers/with-copyable.js
@@ -0,0 +1,55 @@
+import Modifier from 'ember-modifier';
+import { inject as service } from '@ember/service';
+import { runInDebug } from '@ember/debug';
+
+const typeAssertion = (type, value, withDefault) => {
+ return typeof value === type ? value : withDefault;
+};
+export default class WithCopyableModifier extends Modifier {
+ @service('clipboard/os') clipboard;
+
+ hash = null;
+ source = null;
+
+ connect([value], _hash) {
+ value = typeAssertion('string', value, this.element.innerText);
+ const hash = {
+ success: e => {
+ runInDebug(_ => console.info(`with-copyable: Copied \`${value}\``));
+ return typeAssertion('function', _hash.success, () => {})(e);
+ },
+ error: e => {
+ runInDebug(_ => console.info(`with-copyable: Error copying \`${value}\``));
+ return typeAssertion('function', _hash.error, () => {})(e);
+ },
+ };
+ this.source = this.clipboard
+ .execute(this.element, {
+ text: _ => value,
+ ...hash.options,
+ })
+ .on('success', hash.success)
+ .on('error', hash.error);
+ this.hash = hash;
+ }
+
+ disconnect() {
+ if (this.source && this.hash) {
+ this.source.off('success', this.hash.success).off('error', this.hash.error);
+
+ this.source.destroy();
+ this.hash = null;
+ this.source = null;
+ }
+ }
+
+ // lifecycle hooks
+ didReceiveArguments() {
+ this.disconnect();
+ this.connect(this.args.positional, this.args.named);
+ }
+
+ willRemove() {
+ this.disconnect();
+ }
+}
diff --git a/ui/packages/consul-ui/app/modifiers/with-copyable.mdx b/ui/packages/consul-ui/app/modifiers/with-copyable.mdx
new file mode 100644
index 0000000000..d050431e82
--- /dev/null
+++ b/ui/packages/consul-ui/app/modifiers/with-copyable.mdx
@@ -0,0 +1,79 @@
+# with-copyable
+
+Modifier for adding copy-to-clipboard functionality to any component to allow
+the user to easily copy specified text to their clipboard by clicking on
+something. Mainly an Ember flavoured wrapper for
+[Clipboard.js](https://clipboardjs.com/) which the modifier uses for all its
+functionality.
+
+You can either explicitly specify the content to be copied to the users
+clipboard using the first (and only) parameter but if this is omitted it will
+use the content (`innerText`) of the DOM element it is attached to.
+
+Usually you will want to provide a `success` and `error` callback which you
+can provide with named parameters. An escape hatch through to Clipboard.js
+options is also provided via the `options` named parameter.
+
+
+```hbs preview-template
+
+ Explicitly specifying the text to be copied as the first parameter
+ Click me
+
+ Clipboard Contents:
+{{this.copied}}
+
+```
+
+```hbs preview-template
+
+ Defaulting to the innerText of the DOM element
+ Click me
+
+ Clipboard Contents:
+{{this.copied}}
+
+```
+
+The Clipboard.js class is provided via a `clipboard/os` Service, also includes
+a `clipboard/local-storage` Service that automatically replaces the OS based
+clipboard during testing to enable you to assert for text that would be copied
+to the clipboard. During acceptance testing there is a specific step
+specifically for this so you don't have to think about it:
+
+```gherkin acceptance-test
+Scenario:
+ When I click copyButton
+ Then I copied "stringToCopy"
+```
+
+## Positional Arguments
+
+| Argument | Type | Default | Description |
+| --- | --- | --- | --- |
+| `value` | `String` | The `innerText` of the element | The string to be copied to the clipboard on click |
+
+## Named Arguments
+
+| Argument | Type | Default | Description |
+| --- | --- | --- | --- |
+| `success` | `Function` | `(e) => {}` | A function to be called when the text has been successfully copied to the users clipboard |
+| `error` | `Function` | `(e) => {}` | A function to be called when there was an error copying text to the users clipboard |
+| `options` | `Object` | `{}` | An object containing any documented Clipboard.js options |
+
+
+
+## See
+
+- [Modifier Source Code](./index.js)
+
+---
diff --git a/ui/packages/consul-ui/app/services/clipboard/local-storage.js b/ui/packages/consul-ui/app/services/clipboard/local-storage.js
index 01eacb74fb..a35774b5a7 100644
--- a/ui/packages/consul-ui/app/services/clipboard/local-storage.js
+++ b/ui/packages/consul-ui/app/services/clipboard/local-storage.js
@@ -1,10 +1,9 @@
-import Service from '@ember/service';
-
+import Service, { inject as service } from '@ember/service';
import Clipboard from 'clipboard';
class ClipboardCallback extends Clipboard {
- constructor(trigger, cb) {
- super(trigger);
+ constructor(trigger, options, cb) {
+ super(trigger, options);
this._cb = cb;
}
onClick(e) {
@@ -17,12 +16,12 @@ class ClipboardCallback extends Clipboard {
}
export default class LocalStorageService extends Service {
- storage = window.localStorage;
+ @service('-document') doc;
key = 'clipboard';
- execute(trigger) {
- return new ClipboardCallback(trigger, val => {
- this.storage.setItem(this.key, val);
+ execute(trigger, options) {
+ return new ClipboardCallback(trigger, options, val => {
+ this.doc.defaultView.localStorage.setItem(this.key, val);
});
}
}
diff --git a/ui/packages/consul-ui/app/services/clipboard/os.js b/ui/packages/consul-ui/app/services/clipboard/os.js
index 10a6dba8ad..99dc5cd41c 100644
--- a/ui/packages/consul-ui/app/services/clipboard/os.js
+++ b/ui/packages/consul-ui/app/services/clipboard/os.js
@@ -3,7 +3,7 @@ import Service from '@ember/service';
import Clipboard from 'clipboard';
export default class OsService extends Service {
- execute(trigger) {
- return new Clipboard(trigger);
+ execute() {
+ return new Clipboard(...arguments);
}
}
diff --git a/ui/packages/consul-ui/app/styles/components.scss b/ui/packages/consul-ui/app/styles/components.scss
index 47757f3fe3..3493ff5bcf 100644
--- a/ui/packages/consul-ui/app/styles/components.scss
+++ b/ui/packages/consul-ui/app/styles/components.scss
@@ -10,6 +10,7 @@
@import 'consul-ui/components/code-editor';
@import 'consul-ui/components/composite-row';
@import 'consul-ui/components/confirmation-dialog';
+@import 'consul-ui/components/copy-button';
@import 'consul-ui/components/definition-table';
@import 'consul-ui/components/display-toggle';
@import 'consul-ui/components/dom-recycling-table';
diff --git a/ui/packages/consul-ui/tests/acceptance/components/copy-button.feature b/ui/packages/consul-ui/tests/acceptance/components/copy-button.feature
index c2c0b96ea6..5000cf9e42 100644
--- a/ui/packages/consul-ui/tests/acceptance/components/copy-button.feature
+++ b/ui/packages/consul-ui/tests/acceptance/components/copy-button.feature
@@ -22,5 +22,5 @@ Feature: components / copy-button
node: node-0
---
Then the url should be /dc-1/nodes/node-0/health-checks
- When I click ".healthcheck-output:nth-child(1) button.copy-btn"
+ When I click ".healthcheck-output:nth-child(1) .copy-button button"
Then I copied "The output"
diff --git a/ui/packages/consul-ui/translations/components/copy-button/en-us.yaml b/ui/packages/consul-ui/translations/components/copy-button/en-us.yaml
new file mode 100644
index 0000000000..2011513de5
--- /dev/null
+++ b/ui/packages/consul-ui/translations/components/copy-button/en-us.yaml
@@ -0,0 +1,3 @@
+title: Copy {name} to the clipboard
+success: Copied {name}
+error: There was a problem.