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:
parent
ff2360d430
commit
61842beb3b
|
@ -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');
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
@import './skin';
|
||||||
|
@import './layout';
|
||||||
|
%copy-button {
|
||||||
|
@extend %button;
|
||||||
|
}
|
||||||
|
.copy-button button {
|
||||||
|
@extend %copy-button;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
---
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
title: Copy {name} to the clipboard
|
||||||
|
success: Copied {name}
|
||||||
|
error: There was a problem.
|
Loading…
Reference in New Issue