ui: CopyButton amends (#10511)

* ui: Add with-copyable modifier

* Use with-copyable modifier for our own CopyButton

* Move copy-button styling and remove most of `copy-btn`
This commit is contained in:
John Cowen 2021-07-06 16:56:36 +01:00 committed by GitHub
parent ff2360d430
commit 61842beb3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 248 additions and 118 deletions

View File

@ -2,6 +2,7 @@ const path = require('path');
const autolinkHeadings = require('remark-autolink-headings'); const autolinkHeadings = require('remark-autolink-headings');
const refractor = require('refractor'); const refractor = require('refractor');
const gherkin = require('refractor/lang/gherkin');
const prism = require('@mapbox/rehype-prism'); const prism = require('@mapbox/rehype-prism');
const fs = require('fs'); const fs = require('fs');
@ -24,6 +25,7 @@ if($CONSUL_DOCFY_CONFIG.length > 0) {
} }
} }
refractor.register(gherkin);
refractor.alias('handlebars', 'hbs'); refractor.alias('handlebars', 'hbs');
refractor.alias('shell', 'sh'); refractor.alias('shell', 'sh');

View File

@ -16,6 +16,3 @@ button.type-cancel {
%app-view-content form button[type='button'].type-delete { %app-view-content form button[type='button'].type-delete {
@extend %dangerous-button; @extend %dangerous-button;
} }
button.copy-btn {
@extend %copy-button;
}

View File

@ -34,30 +34,6 @@
padding-top: calc(0.4em - 1px) !important; padding-top: calc(0.4em - 1px) !important;
padding-bottom: 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 { %internal-button {
padding: 0.9em 1em; padding: 0.9em 1em;
text-align: center; text-align: center;

View File

@ -11,20 +11,6 @@
cursor: default; cursor: default;
box-shadow: none; 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, %primary-button,
%secondary-button, %secondary-button,
%dangerous-button { %dangerous-button {
@ -34,22 +20,6 @@
box-shadow: $decor-elevation-300; box-shadow: $decor-elevation-300;
} }
/* color */ /* 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 { %primary-button {
@extend %frame-blue-800; @extend %frame-blue-800;
} }

View File

@ -1,35 +1,43 @@
# CopyButton # CopyButton
```hbs preview-template 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.
<p>
Icon Only:
</p>
<CopyButton
@value={{stringToCopy}}
@name="Thing"
/>
<p> 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 `<StateChart />` component to manage states.
Icon and text:
</p> Can be used inline to render only a small icon for the button with no other text.
<CopyButton
@value={{stringToCopy}} ```hbs preview-template
@name="Thing" <figure>
> <figcaption>Icon only</figcaption>
Copy me!
</CopyButton> <CopyButton
@value={{'stringToCopy'}}
@name="Thing"
/>
</figure>
<figure>
<figcaption>Icon and text</figcaption>
<CopyButton
@value={{'stringToCopy'}}
@name="Thing"
>
Copy me!
</CopyButton>
</figure>
``` ```
### Arguments ## Arguments
| Argument | Type | Default | Description | | Argument | Type | Default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `value` | `String` | | The string to be copied to the clipboard on click | | `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) - [Component Source Code](./index.js)
- [Template Source Code](./index.hbs) - [Template Source Code](./index.hbs)

View File

@ -1,29 +1,30 @@
<StateChart @src={{chart}} as |State Guard Action dispatch state|> <StateChart
<Ref @target={{this}} @name="dispatch" @value={{dispatch}} /> @src={{this.chart}}
as |State Guard Action dispatch state|
>
<div <div
{{did-insert this.connect}}
{{will-destroy this.disconnect}}
class="copy-button" class="copy-button"
id={{this.guid}}
...attributes ...attributes
> >
{{#let (fn dispatch 'SUCCESS') (fn dispatch 'ERROR') (fn dispatch 'RESET') as |success error reset|}}
<button <button
title={{concat "Copy " @name " to the clipboard"}} {{with-copyable @value success=success error=error}}
aria-label={{t 'components.copy-button.title' name=@name}}
type="button" type="button"
class="copy-btn" class="copy-btn"
data-clipboard-text={{@value}}
...attributes ...attributes
{{tooltip {{tooltip
(if (state-matches state 'success') (concat 'Copied ' @name '!!') 'There was a problem!') (if (state-matches state 'success') (t 'components.copy-button.success' name=@name) (t 'components.copy-button.error'))
options=(hash options=(hash
trigger='manual' trigger='manual'
showOnCreate=(not (state-matches state 'idle')) showOnCreate=(not (state-matches state 'idle'))
delay=(array 0 3000) delay=(array 0 3000)
onHidden=(action dispatch 'RESET') onHidden=reset
) )
}} }}
> >
{{~yield~}} {{~yield~}}
</button> </button>
{{/let}}
</div> </div>
</StateChart> </StateChart>

View File

@ -1,29 +1,9 @@
import Component from '@glimmer/component'; import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import chart from './chart.xstate'; import chart from './chart.xstate';
export default class CopyButton extends Component { export default class CopyButton extends Component {
@service('clipboard/os') clipboard;
@service('dom') dom;
constructor() { constructor() {
super(...arguments); super(...arguments);
this.chart = chart; 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();
} }
} }

View File

@ -0,0 +1,8 @@
@import './skin';
@import './layout';
%copy-button {
@extend %button;
}
.copy-button button {
@extend %copy-button;
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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
<figure>
<figcaption>Explicitly specifying the text to be copied as the first parameter</figcaption>
<button
{{with-copyable "Copied text"
success=(action (mut this.copied) value="text")
error=(noop)
}}
type="button"
>Click me</button>
<pre>Clipboard Contents:
{{this.copied}}</pre>
</figure>
```
```hbs preview-template
<figure>
<figcaption>Defaulting to the innerText of the DOM element</figcaption>
<button
{{with-copyable success=(action (mut this.copied) value="text")}}
type="button"
>Click <span><br />me</span></button>
<pre>Clipboard Contents:
{{this.copied}}</pre>
</figure>
```
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)
---

View File

@ -1,10 +1,9 @@
import Service from '@ember/service'; import Service, { inject as service } from '@ember/service';
import Clipboard from 'clipboard'; import Clipboard from 'clipboard';
class ClipboardCallback extends Clipboard { class ClipboardCallback extends Clipboard {
constructor(trigger, cb) { constructor(trigger, options, cb) {
super(trigger); super(trigger, options);
this._cb = cb; this._cb = cb;
} }
onClick(e) { onClick(e) {
@ -17,12 +16,12 @@ class ClipboardCallback extends Clipboard {
} }
export default class LocalStorageService extends Service { export default class LocalStorageService extends Service {
storage = window.localStorage; @service('-document') doc;
key = 'clipboard'; key = 'clipboard';
execute(trigger) { execute(trigger, options) {
return new ClipboardCallback(trigger, val => { return new ClipboardCallback(trigger, options, val => {
this.storage.setItem(this.key, val); this.doc.defaultView.localStorage.setItem(this.key, val);
}); });
} }
} }

View File

@ -3,7 +3,7 @@ import Service from '@ember/service';
import Clipboard from 'clipboard'; import Clipboard from 'clipboard';
export default class OsService extends Service { export default class OsService extends Service {
execute(trigger) { execute() {
return new Clipboard(trigger); return new Clipboard(...arguments);
} }
} }

View File

@ -10,6 +10,7 @@
@import 'consul-ui/components/code-editor'; @import 'consul-ui/components/code-editor';
@import 'consul-ui/components/composite-row'; @import 'consul-ui/components/composite-row';
@import 'consul-ui/components/confirmation-dialog'; @import 'consul-ui/components/confirmation-dialog';
@import 'consul-ui/components/copy-button';
@import 'consul-ui/components/definition-table'; @import 'consul-ui/components/definition-table';
@import 'consul-ui/components/display-toggle'; @import 'consul-ui/components/display-toggle';
@import 'consul-ui/components/dom-recycling-table'; @import 'consul-ui/components/dom-recycling-table';

View File

@ -22,5 +22,5 @@ Feature: components / copy-button
node: node-0 node: node-0
--- ---
Then the url should be /dc-1/nodes/node-0/health-checks 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" Then I copied "The output"

View File

@ -0,0 +1,3 @@
title: Copy {name} to the clipboard
success: Copied {name}
error: There was a problem.