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|}} +{{/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
+ + +
Clipboard Contents:
+{{this.copied}}
+
+``` + +```hbs preview-template +
+
Defaulting to the innerText of the DOM element
+ + +
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.