Add clipboard-sk element.

Displays a copy-content icon and when clicked copies the contents of the 'value' attribute into the user's clipboard. Also displays a tooltip letting the user know the value was copied.

If the value to be copied is expensive to calculate then compute the value in the `calculatedValue` function. See `clipboard-sk-demo.ts` for an example.

Bug: skia:13929
Change-Id: I2e385760f289d4184c642316152acdbcefef291e
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/607865
Auto-Submit: Joe Gregorio <jcgregorio@google.com>
Reviewed-by: Ravi Mistry <rmistry@google.com>
Commit-Queue: Joe Gregorio <jcgregorio@google.com>
diff --git a/infra-sk/modules/clipboard-sk/BUILD.bazel b/infra-sk/modules/clipboard-sk/BUILD.bazel
new file mode 100644
index 0000000..c0700be
--- /dev/null
+++ b/infra-sk/modules/clipboard-sk/BUILD.bazel
@@ -0,0 +1,56 @@
+load("//infra-sk:index.bzl", "karma_test", "sk_demo_page_server", "sk_element", "sk_element_puppeteer_test", "sk_page")
+
+sk_demo_page_server(
+    name = "demo_page_server",
+    sk_page = ":clipboard-sk-demo",
+)
+
+sk_element(
+    name = "clipboard-sk",
+    sass_srcs = ["clipboard-sk.scss"],
+    sk_element_deps = ["//infra-sk/modules/tooltip-sk"],
+    ts_deps = [
+        "//infra-sk/modules/ElementSk:index_ts_lib",
+        "@npm//elements-sk",
+        "@npm//lit-html",
+        "@npm//common-sk",
+    ],
+    ts_srcs = [
+        "clipboard-sk.ts",
+        "index.ts",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+sk_page(
+    name = "clipboard-sk-demo",
+    html_file = "clipboard-sk-demo.html",
+    scss_entry_point = "clipboard-sk-demo.scss",
+    sk_element_deps = [":clipboard-sk"],
+    ts_deps = ["@npm//common-sk"],
+    ts_entry_point = "clipboard-sk-demo.ts",
+)
+
+sk_element_puppeteer_test(
+    name = "clipboard-sk_puppeteer_test",
+    src = "clipboard-sk_puppeteer_test.ts",
+    sk_demo_page_server = ":demo_page_server",
+    deps = [
+        "//puppeteer-tests:util_ts_lib",
+        "@npm//@types/chai",
+        "@npm//chai",
+    ],
+)
+
+karma_test(
+    name = "clipboard-sk_test",
+    src = "clipboard-sk_test.ts",
+    deps = [
+        ":clipboard-sk",
+        "//infra-sk/modules:test_util_ts_lib",
+        "//infra-sk/modules/tooltip-sk",
+        "@npm//@types/chai",
+        "@npm//chai",
+        "@npm//common-sk",
+    ],
+)
diff --git a/infra-sk/modules/clipboard-sk/clipboard-sk-demo.html b/infra-sk/modules/clipboard-sk/clipboard-sk-demo.html
new file mode 100644
index 0000000..3bf944e
--- /dev/null
+++ b/infra-sk/modules/clipboard-sk/clipboard-sk-demo.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>clipboard-sk</title>
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  </head>
+  <body class="body-sk darkmode">
+    <h1>clipboard-sk</h1>
+    <div>
+      Hover over the icon
+      <clipboard-sk value="This gets copied to the clipboard"></clipboard-sk>
+    </div>
+    <h1>calculated clipboard-sk value</h1>
+    <div>
+      The value copied to the clipboard is calculated on the fly.
+      <clipboard-sk
+        id="onthefly"
+        value="This gets changed before copying to the clipboard"
+      ></clipboard-sk>
+    </div>
+  </body>
+</html>
diff --git a/infra-sk/modules/clipboard-sk/clipboard-sk-demo.scss b/infra-sk/modules/clipboard-sk/clipboard-sk-demo.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/infra-sk/modules/clipboard-sk/clipboard-sk-demo.scss
diff --git a/infra-sk/modules/clipboard-sk/clipboard-sk-demo.ts b/infra-sk/modules/clipboard-sk/clipboard-sk-demo.ts
new file mode 100644
index 0000000..935a2b5
--- /dev/null
+++ b/infra-sk/modules/clipboard-sk/clipboard-sk-demo.ts
@@ -0,0 +1,9 @@
+import { $$ } from 'common-sk/modules/dom';
+import { ClipboardSk } from './clipboard-sk';
+import './index';
+
+// If a clipboard value is too expensive to calculate all the time, for example,
+// a CSV file, then you can set the `calculatedValue` property on the
+// clipboard-sk element and that will only be called if the user actually clicks
+// on the element.
+$$<ClipboardSk>('#onthefly')!.calculatedValue = async (): Promise<string> => 'This is the altered value.';
diff --git a/infra-sk/modules/clipboard-sk/clipboard-sk.scss b/infra-sk/modules/clipboard-sk/clipboard-sk.scss
new file mode 100644
index 0000000..51fabf8
--- /dev/null
+++ b/infra-sk/modules/clipboard-sk/clipboard-sk.scss
@@ -0,0 +1,6 @@
+clipboard-sk {
+  content-copy-icon-sk svg.icon-sk-svg {
+    width: 16px;
+    height: 16px;
+  }
+}
diff --git a/infra-sk/modules/clipboard-sk/clipboard-sk.ts b/infra-sk/modules/clipboard-sk/clipboard-sk.ts
new file mode 100644
index 0000000..94ecc58
--- /dev/null
+++ b/infra-sk/modules/clipboard-sk/clipboard-sk.ts
@@ -0,0 +1,89 @@
+/**
+ * @module modules/clipboard-sk
+ * @description <h2><code>clipboard-sk</code></h2>
+ *
+ * Displays a copy-content icon and when clicked copies the contents of the
+ * 'value' attribute into the user's clipboard. Also displays a tooltip letting
+ * the user know the value was copied.
+ *
+ * If the value to be copied is expensive to calculate then compute the value in
+ * the `calculatedValue` function. See `clipboard-sk-demo.ts` for an example.
+ *
+ * @attr value - The content to put into the clipboard.
+ *
+ */
+import { define } from 'elements-sk/define';
+import { html } from 'lit-html';
+import { $$ } from 'common-sk/modules/dom';
+import { ElementSk } from '../ElementSk';
+
+import 'elements-sk/icon/content-copy-icon-sk';
+import '../tooltip-sk';
+import { TooltipSk } from '../tooltip-sk/tooltip-sk';
+
+export const defaultToolTipMessage = 'Copy to clipboard';
+
+export const copyCompleteToolTipMessage = 'Copied!';
+
+export const copyFailedToolTipMessage = 'Failed to copy!';
+
+export class ClipboardSk extends ElementSk {
+  // We need to assign an id to the content-copy-icon-sk, so that the tooltip-sk
+  // has something to use as a target.
+  private icon_id: string = `x${`${Math.random()}`.slice(2)}`;
+
+  private tooltip: TooltipSk | null = null;
+
+  /** If the value to be copied is expensive to calculate then compute the value
+    * in the `calculatedValue` function. See `clipboard-sk-demo.ts` for an
+    * example.
+    * */
+  calculatedValue: (()=> Promise<string>) | null = null;
+
+  constructor() {
+    super(ClipboardSk.template);
+  }
+
+  private static template = (ele: ClipboardSk) => html`
+  <content-copy-icon-sk
+    id=${ele.icon_id}
+    @click=${() => ele.copyToClipboard()}>
+    @mouseleave=${() => ele.restoreToolTipMessage()}
+  </content-copy-icon-sk>
+  <tooltip-sk
+    target=${ele.icon_id}
+    value=${defaultToolTipMessage}>
+  </tooltip-sk>`;
+
+  connectedCallback(): void {
+    super.connectedCallback();
+    this._upgradeProperty('value');
+    this._render();
+    this.tooltip = $$('tooltip-sk', this);
+  }
+
+  private async copyToClipboard(): Promise<void> {
+    try {
+      if (this.calculatedValue !== null) {
+        this.value = await this.calculatedValue();
+      }
+      await navigator.clipboard.writeText(this.value);
+      this.tooltip!.value = copyCompleteToolTipMessage;
+    } catch (error) {
+      this.tooltip!.value = copyFailedToolTipMessage;
+    }
+    this._render();
+  }
+
+  private restoreToolTipMessage(): void {
+    this.tooltip!.value = defaultToolTipMessage;
+    this._render();
+  }
+
+  /** @prop value {string} The content to put into the clipboard. */
+  get value(): string { return this.getAttribute('value') || ''; }
+
+  set value(val: string) { this.setAttribute('value', val); }
+}
+
+define('clipboard-sk', ClipboardSk);
diff --git a/infra-sk/modules/clipboard-sk/clipboard-sk_puppeteer_test.ts b/infra-sk/modules/clipboard-sk/clipboard-sk_puppeteer_test.ts
new file mode 100644
index 0000000..8357362
--- /dev/null
+++ b/infra-sk/modules/clipboard-sk/clipboard-sk_puppeteer_test.ts
@@ -0,0 +1,27 @@
+import { expect } from 'chai';
+import {
+  inBazel, loadCachedTestBed, takeScreenshot, TestBed,
+} from '../../../puppeteer-tests/util';
+
+describe('clipboard-sk', () => {
+  let testBed: TestBed;
+  before(async () => {
+    testBed = await loadCachedTestBed();
+  });
+
+  beforeEach(async () => {
+    // Remove the /dist/ below for //infra-sk elements.
+    await testBed.page.goto(inBazel() ? testBed.baseUrl : `${testBed.baseUrl}/clipboard-sk.html`);
+    await testBed.page.setViewport({ width: 400, height: 550 });
+  });
+
+  it('should render the demo page (smoke test)', async () => {
+    expect(await testBed.page.$$('clipboard-sk')).to.have.length(2);
+  });
+
+  describe('screenshots', () => {
+    it('shows the default view', async () => {
+      await takeScreenshot(testBed.page, 'infra-sk', 'clipboard-sk');
+    });
+  });
+});
diff --git a/infra-sk/modules/clipboard-sk/clipboard-sk_test.ts b/infra-sk/modules/clipboard-sk/clipboard-sk_test.ts
new file mode 100644
index 0000000..981edb7
--- /dev/null
+++ b/infra-sk/modules/clipboard-sk/clipboard-sk_test.ts
@@ -0,0 +1,26 @@
+import './index';
+import { assert } from 'chai';
+import { $$ } from 'common-sk/modules/dom';
+import { ClipboardSk, defaultToolTipMessage } from './clipboard-sk';
+
+import { setUpElementUnderTest } from '../test_util';
+import { TooltipSk } from '../tooltip-sk/tooltip-sk';
+
+const testMessage = 'This should end up in the clipboard';
+
+describe('clipboard-sk', () => {
+  const newInstance = setUpElementUnderTest<ClipboardSk>('clipboard-sk');
+
+  let element: ClipboardSk;
+  beforeEach(() => {
+    element = newInstance((el: ClipboardSk) => {
+      el.value = testMessage;
+    });
+  });
+
+  describe('on construction', () => {
+    it('has the right tooltip value', () => {
+      assert.equal($$<TooltipSk>('tooltip-sk', element)!.value, defaultToolTipMessage);
+    });
+  });
+});
diff --git a/infra-sk/modules/clipboard-sk/index.ts b/infra-sk/modules/clipboard-sk/index.ts
new file mode 100644
index 0000000..02e966e
--- /dev/null
+++ b/infra-sk/modules/clipboard-sk/index.ts
@@ -0,0 +1 @@
+import './clipboard-sk';
diff --git a/infra-sk/modules/tooltip-sk/tooltip-sk.ts b/infra-sk/modules/tooltip-sk/tooltip-sk.ts
index c0ee197..3fe586b 100644
--- a/infra-sk/modules/tooltip-sk/tooltip-sk.ts
+++ b/infra-sk/modules/tooltip-sk/tooltip-sk.ts
@@ -36,6 +36,8 @@
 
   connectedCallback(): void {
     super.connectedCallback();
+    this._upgradeProperty('value');
+    this._upgradeProperty('target');
     this._render();
     this.hide();
 
@@ -51,7 +53,7 @@
     // back to this element. We require an id for this to work, so assign a
     // random id if one hasn't been set.
     if (!this.id) {
-      this.id = `x${Math.random()}`;
+      this.id = `x${`${Math.random()}`.slice(2)}`;
     }
 
     this.connectToTarget();