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 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');
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,35 +1,43 @@
|
|||
# CopyButton
|
||||
|
||||
```hbs preview-template
|
||||
<p>
|
||||
Icon Only:
|
||||
</p>
|
||||
<CopyButton
|
||||
@value={{stringToCopy}}
|
||||
@name="Thing"
|
||||
/>
|
||||
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 and text:
|
||||
</p>
|
||||
<CopyButton
|
||||
@value={{stringToCopy}}
|
||||
@name="Thing"
|
||||
>
|
||||
Copy me!
|
||||
</CopyButton>
|
||||
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.
|
||||
|
||||
Can be used inline to render only a small icon for the button with no other text.
|
||||
|
||||
```hbs preview-template
|
||||
<figure>
|
||||
<figcaption>Icon only</figcaption>
|
||||
|
||||
<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 |
|
||||
| --- | --- | --- | --- |
|
||||
| `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)
|
||||
|
|
|
@ -1,29 +1,30 @@
|
|||
<StateChart @src={{chart}} as |State Guard Action dispatch state|>
|
||||
<Ref @target={{this}} @name="dispatch" @value={{dispatch}} />
|
||||
<StateChart
|
||||
@src={{this.chart}}
|
||||
as |State Guard Action dispatch state|
|
||||
>
|
||||
<div
|
||||
{{did-insert this.connect}}
|
||||
{{will-destroy this.disconnect}}
|
||||
class="copy-button"
|
||||
id={{this.guid}}
|
||||
...attributes
|
||||
>
|
||||
{{#let (fn dispatch 'SUCCESS') (fn dispatch 'ERROR') (fn dispatch 'RESET') as |success error reset|}}
|
||||
<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"
|
||||
class="copy-btn"
|
||||
data-clipboard-text={{@value}}
|
||||
...attributes
|
||||
{{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
|
||||
trigger='manual'
|
||||
showOnCreate=(not (state-matches state 'idle'))
|
||||
delay=(array 0 3000)
|
||||
onHidden=(action dispatch 'RESET')
|
||||
onHidden=reset
|
||||
)
|
||||
}}
|
||||
>
|
||||
{{~yield~}}
|
||||
</button>
|
||||
{{/let}}
|
||||
</div>
|
||||
</StateChart>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
title: Copy {name} to the clipboard
|
||||
success: Copied {name}
|
||||
error: There was a problem.
|
Loading…
Reference in New Issue