Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/chops/chops-checkbox/chops-checkbox.js b/static_src/elements/chops/chops-checkbox/chops-checkbox.js
new file mode 100644
index 0000000..d752347
--- /dev/null
+++ b/static_src/elements/chops/chops-checkbox/chops-checkbox.js
@@ -0,0 +1,135 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {LitElement, html, css} from 'lit-element';
+
+/**
+ * `<chops-checkbox>`
+ *
+ * A checkbox component. This component is primarily a wrapper
+ * around a native checkbox to allow easy sharing of styles.
+ *
+ */
+export class ChopsCheckbox extends LitElement {
+  /** @override */
+  static get styles() {
+    return css`
+      :host {
+        --chops-checkbox-color: var(--chops-primary-accent-color);
+        /* A bit brighter than Chrome's default focus color to
+        * avoid blending into the checkbox's blue. */
+        --chops-checkbox-focus-color: hsl(193, 82%, 63%);
+        --chops-checkbox-size: 16px;
+        --chops-checkbox-check-size: 18px;
+      }
+      label {
+        cursor: pointer;
+        display: inline-flex;
+        align-items: center;
+      }
+      input[type="checkbox"] {
+        /* We need the checkbox to be hidden but still accessible. */
+        opacity: 0;
+        width: 0;
+        height: 0;
+        position: absolute;
+        top: -9999;
+        left: -9999;
+      }
+      label::before {
+        width: var(--chops-checkbox-size);
+        height: var(--chops-checkbox-size);
+        margin-right: 8px;
+        box-sizing: border-box;
+        content: "\\2713";
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        border: 2px solid #222;
+        border-radius: 2px;
+        background: #fff;
+        font-size: var(--chops-checkbox-check-size);
+        padding: 0;
+        color: transparent;
+      }
+      input[type="checkbox"]:focus + label::before {
+        /* Make sure an outline shows around this element for
+        * accessibility.
+        */
+        box-shadow: 0 0 5px 1px var(--chops-checkbox-focus-color);
+      }
+      input[type="checkbox"]:checked + label::before {
+        background: var(--chops-checkbox-color);
+        border-color: var(--chops-checkbox-color);
+        color: #fff;
+      }
+    `;
+  }
+
+  /** @override */
+  render() {
+    return html`
+      <!-- Note: Avoiding 2-way data binding to futureproof this code
+        for LitElement. -->
+      <input id="checkbox" type="checkbox"
+        .checked=${this.checked} @change=${this._checkedChangeHandler}>
+      <label for="checkbox">
+        <slot></slot>
+      </label>
+    `;
+  }
+
+  /** @override */
+  static get properties() {
+    return {
+      label: {type: String},
+
+      /**
+       * Note: At the moment, this component does not manage its own
+       * internal checked state. It expects its checked state to come
+       * from its parent, and its parent is expected to update the
+       * chops-checkbox's checked state on a change event.
+       *
+       * This can be generalized in the future to support multiple
+       * ways of managing checked state if needed.
+       **/
+      checked: {type: Boolean},
+    };
+  }
+
+  /**
+   * Clicks the checkbox. Helpful for automated testing.
+   */
+  click() {
+    super.click();
+    /** @type {HTMLInputElement} */ (
+      this.shadowRoot.querySelector('#checkbox')).click();
+  }
+
+  /**
+   * Listens to the native checkbox's change event and runs internal
+   * logic based on changes.
+   * @param {Event} evt
+   * @private
+   */
+  _checkedChangeHandler(evt) {
+    this._checkedChange(evt.target.checked);
+  }
+
+  /**
+   * @param {boolean} checked Whether the box was checked or unchecked.
+   * @fires CustomEvent#checked-change
+   * @private
+   */
+  _checkedChange(checked) {
+    if (checked === this.checked) return;
+    const customEvent = new CustomEvent('checked-change', {
+      detail: {
+        checked: checked,
+      },
+    });
+    this.dispatchEvent(customEvent);
+  }
+}
+customElements.define('chops-checkbox', ChopsCheckbox);
diff --git a/static_src/elements/chops/chops-checkbox/chops-checkbox.test.js b/static_src/elements/chops/chops-checkbox/chops-checkbox.test.js
new file mode 100644
index 0000000..5a11111
--- /dev/null
+++ b/static_src/elements/chops/chops-checkbox/chops-checkbox.test.js
@@ -0,0 +1,86 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {assert} from 'chai';
+import sinon from 'sinon';
+import {ChopsCheckbox} from './chops-checkbox.js';
+
+let element;
+
+describe('chops-checkbox', () => {
+  beforeEach(() => {
+    element = document.createElement('chops-checkbox');
+    document.body.appendChild(element);
+  });
+
+  afterEach(() => {
+    document.body.removeChild(element);
+  });
+
+  it('initializes', () => {
+    assert.instanceOf(element, ChopsCheckbox);
+  });
+
+  it('clicking checkbox dispatches checked-change event', async () => {
+    element.checked = false;
+    sinon.stub(window, 'CustomEvent');
+    sinon.stub(element, 'dispatchEvent');
+
+    await element.updateComplete;
+
+    element.shadowRoot.querySelector('#checkbox').click();
+
+    assert.deepEqual(window.CustomEvent.args[0][0], 'checked-change');
+    assert.deepEqual(window.CustomEvent.args[0][1], {
+      detail: {checked: true},
+    });
+
+    assert.isTrue(window.CustomEvent.calledOnce);
+    assert.isTrue(element.dispatchEvent.calledOnce);
+
+    window.CustomEvent.restore();
+    element.dispatchEvent.restore();
+  });
+
+  it('updating checked property updates native <input>', async () => {
+    element.checked = false;
+
+    await element.updateComplete;
+
+    assert.isFalse(element.checked);
+    assert.isFalse(element.shadowRoot.querySelector('input').checked);
+
+    element.checked = true;
+
+    await element.updateComplete;
+
+    assert.isTrue(element.checked);
+    assert.isTrue(element.shadowRoot.querySelector('input').checked);
+  });
+
+  it('updating checked attribute updates native <input>', async () => {
+    element.setAttribute('checked', true);
+    await element.updateComplete;
+
+    assert.equal(element.getAttribute('checked'), 'true');
+    assert.isTrue(element.shadowRoot.querySelector('input').checked);
+
+    element.click();
+    await element.updateComplete;
+
+    // We expect the 'checked' attribute to remain the same even as the
+    // corresponding property changes when the user clicks the checkbox.
+    assert.equal(element.getAttribute('checked'), 'true');
+    assert.isFalse(element.shadowRoot.querySelector('input').checked);
+
+    element.click();
+    await element.updateComplete;
+    assert.isTrue(element.shadowRoot.querySelector('input').checked);
+
+    element.removeAttribute('checked');
+    await element.updateComplete;
+    assert.isNotTrue(element.getAttribute('checked'));
+    assert.isFalse(element.shadowRoot.querySelector('input').checked);
+  });
+});