Bug 1515308 - Migrate facet-base bindings to custom element. r=mkmelin
authorArshad Khan <arshdkhn1@gmail.com>
Wed, 19 Dec 2018 18:27:22 +0530
changeset 34160 0a09c6d031d8375f02964bfc217eaf1abeb8b75c
parent 34159 10ad26e5d74d555404b4ad902e898681d6afd756
child 34161 53838da2d1516a9d69c298659936e6393386afa2
push id389
push userclokep@gmail.com
push dateMon, 18 Mar 2019 19:01:53 +0000
reviewersmkmelin
bugs1515308
Bug 1515308 - Migrate facet-base bindings to custom element. r=mkmelin
mail/base/content/glodaFacet.js
mail/base/content/glodaFacetBindings.css
mail/base/content/glodaFacetBindings.xml
mail/base/content/glodaFacetView.css
mail/base/content/glodaFacetView.js
mail/base/content/glodaFacetView.xhtml
--- a/mail/base/content/glodaFacet.js
+++ b/mail/base/content/glodaFacet.js
@@ -1,13 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/* global HTMLElement, DateFacetVis, FacetContext */
+/* global HTMLElement, DateFacetVis, FacetContext, glodaFacetStrings */
 class MozFacetDate extends HTMLElement {
   get build() {
     return this.buildFunc;
   }
 
   get brushItems() {
     return (aItems) => this.vis.hoverItems(aItems);
   }
@@ -55,9 +55,337 @@ class MozFacetDate extends HTMLElement {
         this.vis.build();
       } else {
         this.vis.rebuild();
       }
     }
   }
 }
 
+class MozFacetBoolean extends HTMLElement {
+  constructor() {
+    super();
+
+    this.addEventListener("mouseover", (event) => {
+      FacetContext.hoverFacet(
+        this.faceter,
+        this.faceter.attrDef,
+        true, this.trueValues
+      );
+    });
+
+    this.addEventListener("mouseout", (event) => {
+      FacetContext.unhoverFacet(
+        this.faceter,
+        this.faceter.attrDef,
+        true,
+        this.trueValues
+      );
+    });
+  }
+
+  connectedCallback() {
+    this.addChildren();
+
+    this.canUpdate = true;
+    this.bubble.addEventListener("click", (event) => {
+      return this.bubbleClicked(event);
+    });
+
+    if ("faceter" in this) {
+      this.build(true);
+    }
+  }
+
+  addChildren() {
+    this.bubble = document.createElement("span");
+    this.bubble.classList.add("facet-checkbox-bubble");
+
+    this.checkbox = document.createElement("input");
+    this.checkbox.setAttribute("type", "checkbox");
+
+    this.labelNode = document.createElement("span");
+    this.labelNode.classList.add("facet-checkbox-label");
+
+    this.countNode = document.createElement("span");
+    this.countNode.classList.add("facet-checkbox-count");
+
+    this.bubble.appendChild(this.checkbox);
+    this.bubble.appendChild(this.labelNode);
+    this.bubble.appendChild(this.countNode);
+
+    this.appendChild(this.bubble);
+  }
+
+  set disabled(val) {
+    if (val) {
+      this.setAttribute("disabled", "true");
+      this.checkbox.setAttribute("disabled", "true");
+    } else {
+      this.removeAttribute("disabled");
+      this.checkbox.removeAttribute("disabled");
+    }
+  }
+
+  get disabled() {
+    return this.getAttribute("disabled") == "true";
+  }
+
+  set checked(val) {
+    if (this.checked == val) {
+      return;
+    }
+    this.checkbox.checked = val;
+    if (val) {
+      this.setAttribute("checked", "true");
+      if (!this.disabled) {
+        FacetContext.addFacetConstraint(
+          this.faceter,
+          true,
+          this.trueGroups
+        );
+      }
+    } else {
+      this.removeAttribute("checked");
+      this.checkbox.removeAttribute("checked");
+      if (!this.disabled) {
+        FacetContext.removeFacetConstraint(
+          this.faceter,
+          true,
+          this.trueGroups
+        );
+      }
+    }
+    this.checkStateChanged();
+  }
+
+  get checked() {
+    return this.getAttribute("checked") == "true";
+  }
+
+  extraSetup() { }
+
+  checkStateChanged() { }
+
+  brushItems() { }
+
+  clearBrushedItems() { }
+
+  build(firstTime) {
+    if (firstTime) {
+      this.labelNode.textContent = this.facetDef.strings.facetNameLabel;
+      this.checkbox.setAttribute(
+        "aria-label",
+        this.facetDef.strings.facetNameLabel
+      );
+      this.trueValues = [];
+    }
+
+    // If we do not currently have a constraint applied and there is only
+    //  one (or no) group, then: disable us, but reflect the underlying
+    //  state of the data (checked or non-checked)
+    if (!this.faceter.constraint && (this.orderedGroups.length <= 1)) {
+      this.disabled = true;
+      let count = 0;
+      if (this.orderedGroups.length) {
+        // true case?
+        if (this.orderedGroups[0][0]) {
+          count = this.orderedGroups[0][1].length;
+          this.checked = true;
+        } else {
+          this.checked = false;
+        }
+      }
+      this.countNode.textContent = count.toLocaleString();
+      return;
+    }
+    // if we were disabled checked before, clear ourselves out
+    if (this.disabled && this.checked) {
+      this.checked = false;
+    }
+    this.disabled = false;
+
+    // if we are here, we have our 2 groups, find true...
+    // (note: it is possible to get jerked around by null values
+    //  currently, so leave a reasonable failure case)
+    this.trueValues = [];
+    this.trueGroups = [true];
+    for (let groupPair of this.orderedGroups) {
+      if (groupPair[0]) {
+        this.trueValues = groupPair[1];
+      }
+    }
+
+    this.countNode.textContent = this.trueValues.length.toLocaleString();
+  }
+
+  bubbleClicked(event) {
+    if (!this.disabled) {
+      this.checked = !this.checked;
+    }
+    event.stopPropagation();
+  }
+}
+
+class MozFacetBooleanFiltered extends MozFacetBoolean {
+  static get observedAttributes() {
+    return ["checked", "disabled"];
+  }
+
+  connectedCallback() {
+    super.addChildren();
+
+    this.filterNode = document.createElement("select");
+    this.filterNode.classList.add("facet-filter-list");
+    this.appendChild(this.filterNode);
+
+    this.canUpdate = true;
+    this.bubble.addEventListener("click", (event) => {
+      return super.bubbleClicked(event);
+    });
+
+    this.extraSetup();
+
+    if ("faceter" in this) {
+      this.build(true);
+    }
+
+    this._updateAttributes();
+  }
+
+  attributeChangedCallback() {
+    this._updateAttributes();
+  }
+
+  _updateAttributes() {
+    if (!this.checkbox) {
+      return;
+    }
+
+    if (this.hasAttribute("checked")) {
+      this.checkbox.setAttribute("checked", this.getAttribute("checked"));
+    } else {
+      this.checkbox.removeAttribute("checked");
+    }
+
+    if (this.hasAttribute("disabled")) {
+      this.checkbox.setAttribute("disabled", this.getAttribute("disabled"));
+    } else {
+      this.checkbox.removeAttribute("disabled");
+    }
+  }
+
+  extraSetup() {
+    this.groupDisplayProperty = this.getAttribute("groupDisplayProperty");
+
+    this.filterNode.addEventListener("change", (event) => this.filterChanged(event));
+
+    this.selectedValue = "all";
+  }
+
+  build(firstTime) {
+    if (firstTime) {
+      this.labelNode.textContent = this.facetDef.strings.facetNameLabel;
+      this.checkbox.setAttribute("aria-label", this.facetDef.strings.facetNameLabel);
+      this.trueValues = [];
+    }
+
+    // Only update count if anything other than "all" is selected.
+    // Otherwise we lose the set of attachment types in our select box,
+    // and that makes us sad.  We do want to update on "all" though
+    // because other facets may further reduce the number of attachments
+    // we see.  (Or if this is not just being used for attachments, it
+    // still holds.)
+    if (this.selectedValue != "all") {
+      let count = 0;
+      for (let groupPair of this.orderedGroups) {
+        if (groupPair[0] != null)
+          count += groupPair[1].length;
+      }
+      this.countNode.textContent = count.toLocaleString();
+      return;
+    }
+
+    while (this.filterNode.hasChildNodes()) {
+      this.filterNode.lastChild.remove();
+    }
+
+    let allNode = document.createElement("option");
+    allNode.textContent =
+      glodaFacetStrings.get(
+        "glodaFacetView.facets.filter." + this.attrDef.attributeName + ".allLabel"
+      );
+    allNode.setAttribute("value", "all");
+    if (this.selectedValue == "all") {
+      allNode.setAttribute("selected", "selected");
+    }
+    this.filterNode.appendChild(allNode);
+
+    // if we are here, we have our 2 groups, find true...
+    // (note: it is possible to get jerked around by null values
+    // currently, so leave a reasonable failure case)
+    // empty true groups is for the checkbox
+    this.trueGroups = [];
+    // the real true groups is the actual true values for our explicit
+    // filtering
+    this.realTrueGroups = [];
+    this.trueValues = [];
+    this.falseValues = [];
+    let selectNodes = [];
+    for (let groupPair of this.orderedGroups) {
+      if (groupPair[0] === null) {
+        this.falseValues.push.apply(this.falseValues, groupPair[1]);
+      } else {
+        this.trueValues.push.apply(this.trueValues, groupPair[1]);
+
+        let groupValue = groupPair[0];
+        let selNode = document.createElement("option");
+        selNode.textContent = groupValue[this.groupDisplayProperty];
+        selNode.setAttribute("value", this.realTrueGroups.length);
+        if (this.selectedValue == groupValue.category) {
+          selNode.setAttribute("selected", "selected");
+        }
+        selectNodes.push(selNode);
+
+        this.realTrueGroups.push(groupValue);
+      }
+    }
+    selectNodes.sort((a, b) => {
+      return a.textContent.localeCompare(b.textContent);
+    });
+    selectNodes.forEach((selNode) => { this.filterNode.appendChild(selNode); });
+
+    this.disabled = !this.trueValues.length;
+
+    this.countNode.textContent = this.trueValues.length.toLocaleString();
+  }
+
+  checkStateChanged() {
+    // if they un-check us, revert our value to all.
+    if (!this.checked)
+      this.selectedValue = "all";
+  }
+
+  filterChanged(event) {
+    if (!this.checked) {
+      return;
+    }
+    if (this.filterNode.value == "all") {
+      this.selectedValue = "all";
+      FacetContext.addFacetConstraint(
+        this.faceter,
+        true,
+        this.trueGroups,
+        false,
+        true
+      );
+    } else {
+      let groupValue = this.realTrueGroups[parseInt(this.filterNode.value)];
+      this.selectedValue = groupValue.category;
+      FacetContext.addFacetConstraint(this.faceter, true, [groupValue], false, true);
+    }
+  }
+}
+
+
 customElements.define("facet-date", MozFacetDate);
+customElements.define("facet-boolean", MozFacetBoolean);
+customElements.define("facet-boolean-filtered", MozFacetBooleanFiltered);
--- a/mail/base/content/glodaFacetBindings.css
+++ b/mail/base/content/glodaFacetBindings.css
@@ -1,24 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 .facetious[type="discrete"] {
   -moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#facet-discrete');
 }
 
-.facetious[type="boolean"] {
-  -moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#facet-boolean');
-}
-
-.facetious[type="boolean-filtered"] {
-  -moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#facet-boolean-filtered');
-}
-
 .results[type="message"] {
   -moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#results-message');
 }
 
 .message {
   -moz-binding: url('chrome://messenger/content/glodaFacetBindings.xml#result-message');
 }
 
--- a/mail/base/content/glodaFacetBindings.xml
+++ b/mail/base/content/glodaFacetBindings.xml
@@ -6,321 +6,16 @@
 <!-- import-globals-from glodaFacetView.js -->
 
 <bindings id="glodaFacetBindings"
           xmlns="http://www.mozilla.org/xbl"
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
           xmlns:html="http://www.w3.org/1999/xhtml"
           xmlns:xbl="http://www.mozilla.org/xbl"
           xmlns:svg="http://www.w3.org/2000/svg">
-
-<binding id="facet-base">
-  <implementation>
-    <method name="brushItems">
-      <body><![CDATA[
-      ]]></body>
-    </method>
-    <method name="clearBrushedItems">
-      <body><![CDATA[
-      ]]></body>
-    </method>
-  </implementation>
-</binding>
-
-<!--
-  - A boolean facet; we presume orderedGroups contains at most two entries, and
-  -  that their group values consist of true or false.
-  -
-  - The implication of this UI is that you only want to filter to true values
-  -  and would never want to filter out the false values.  Consistent with this
-  -  assumption we disable the UI if there are no true values.
-  -
-  - This depressingly
-  -->
-<binding id="facet-boolean"
-         extends="chrome://messenger/content/glodaFacetBindings.xml#facet-base">
-  <content>
-    <html:span anonid="bubble" class="facet-checkbox-bubble">
-      <html:input anonid="checkbox" type="checkbox" />
-      <html:span anonid="label" class="facet-checkbox-label" />
-      <html:span anonid="count" class="facet-checkbox-count" />
-    </html:span>
-  </content>
-  <implementation>
-    <constructor><![CDATA[
-      this.bubble = document.getAnonymousElementByAttribute(this, "anonid",
-                                                            "bubble");
-      this.checkbox = document.getAnonymousElementByAttribute(this, "anonid",
-                                                              "checkbox");
-      this.labelNode = document.getAnonymousElementByAttribute(this, "anonid",
-                                                               "label");
-      this.countNode = document.getAnonymousElementByAttribute(this, "anonid",
-                                                               "count");
-
-      let dis = this;
-      this.bubble.addEventListener("click", function(event) {
-        return dis.bubbleClicked(event);
-      }, true);
-
-      this.extraSetup();
-
-      if ("faceter" in this)
-        this.build(true);
-    ]]></constructor>
-    <field name="canUpdate" readonly="true">true</field>
-    <property name="disabled">
-      <getter><![CDATA[
-        return this.getAttribute("disabled") == "true";
-      ]]></getter>
-      <setter><![CDATA[
-        if (val) {
-          this.setAttribute("disabled", "true");
-          this.checkbox.setAttribute("disabled", true);
-        } else {
-          this.removeAttribute("disabled");
-          this.checkbox.removeAttribute("disabled");
-        }
-      ]]></setter>
-    </property>
-    <property name="checked">
-      <getter><![CDATA[
-        return this.getAttribute("checked") == "true";
-      ]]></getter>
-      <setter><![CDATA[
-        if (this.checked == val)
-          return;
-        this.checkbox.checked = val;
-        if (val) {
-          // the XBL inherits magic appears to fail if we explicitly check the
-          //  box itself rather than via our click handler, presumably because
-          //  we unshadow something.  So manually apply changes ourselves.
-          this.setAttribute("checked", "true");
-          this.checkbox.setAttribute("checked", "true");
-          if (!this.disabled)
-            FacetContext.addFacetConstraint(this.faceter, true,
-                                            this.trueGroups);
-        } else {
-          this.removeAttribute("checked");
-          this.checkbox.removeAttribute("checked");
-          if (!this.disabled)
-            FacetContext.removeFacetConstraint(this.faceter, true,
-                                               this.trueGroups);
-        }
-        this.checkStateChanged();
-      ]]></setter>
-    </property>
-    <method name="extraSetup">
-      <body><![CDATA[
-      ]]></body>
-    </method>
-    <method name="checkStateChanged">
-      <body><![CDATA[
-      ]]></body>
-    </method>
-    <method name="build">
-      <parameter name="aFirstTime" />
-      <body><![CDATA[
-        if (aFirstTime) {
-          this.labelNode.textContent = this.facetDef.strings.facetNameLabel;
-          this.checkbox.setAttribute("aria-label",
-                                     this.facetDef.strings.facetNameLabel);
-          this.trueValues = [];
-        }
-
-        // If we do not currently have a constraint applied and there is only
-        //  one (or no) group, then: disable us, but reflect the underlying
-        //  state of the data (checked or non-checked)
-        if (!this.faceter.constraint && (this.orderedGroups.length <= 1)) {
-          this.disabled = true;
-          let count = 0;
-          if (this.orderedGroups.length) {
-            // true case?
-            if (this.orderedGroups[0][0]) {
-              count = this.orderedGroups[0][1].length;
-              this.checked = true;
-            } else {
-              this.checked = false;
-            }
-          }
-          this.countNode.textContent = count.toLocaleString();
-          return;
-        }
-        // if we were disabled checked before, clear ourselves out
-        if (this.disabled && this.checked)
-          this.checked = false;
-        this.disabled = false;
-
-        // if we are here, we have our 2 groups, find true...
-        // (note: it is possible to get jerked around by null values
-        //  currently, so leave a reasonable failure case)
-        this.trueValues = [];
-        this.trueGroups = [true];
-        for (let groupPair of this.orderedGroups) {
-          if (groupPair[0])
-            this.trueValues = groupPair[1];
-        }
-
-        this.countNode.textContent = this.trueValues.length.toLocaleString();
-      ]]></body>
-    </method>
-    <method name="bubbleClicked">
-      <parameter name="event" />
-      <body><![CDATA[
-      if (!this.disabled)
-        this.checked = !this.checked;
-      event.stopPropagation();
-      ]]></body>
-    </method>
-  </implementation>
-  <handlers>
-    <handler event="mouseover"><![CDATA[
-      FacetContext.hoverFacet(this.faceter, this.faceter.attrDef,
-                              true, this.trueValues);
-    ]]></handler>
-    <handler event="mouseout"><![CDATA[
-      FacetContext.unhoverFacet(this.faceter, this.faceter.attrDef,
-                                true, this.trueValues);
-    ]]></handler>
-  </handlers>
-</binding>
-
-<!--
-  - A check-box with filter-box front-end to a standard discrete faceter.  If
-  -  there are no non-null values, we disable the UI.  If there are non-null
-  -  values the checkbox is enabled and the filter is hidden.  Once you check
-  -  the box we apply the facet and show the filtering mechanism.
-  -->
-<binding id="facet-boolean-filtered"
-         extends="chrome://messenger/content/glodaFacetBindings.xml#facet-boolean">
-  <content>
-    <html:span anonid="bubble" class="facet-checkbox-bubble">
-      <html:input anonid="checkbox" type="checkbox"
-                  xbl:inherits="checked,disabled"/>
-      <html:span anonid="label" class="facet-checkbox-label" />
-      <html:span anonid="count" class="facet-checkbox-count" />
-    </html:span>
-    <html:select anonid="filter" class="facet-filter-list" />
-  </content>
-  <implementation>
-    <method name="extraSetup">
-      <body><![CDATA[
-        this.filterNode = document.getAnonymousElementByAttribute(
-                            this, "anonid", "filter");
-        this.groupDisplayProperty = this.getAttribute("groupDisplayProperty");
-
-        let dis = this;
-        this.filterNode.addEventListener("change", function(event) {
-          return dis.filterChanged(event);
-        });
-
-        this.selectedValue = "all";
-      ]]></body>
-    </method>
-    <method name="build">
-      <parameter name="aFirstTime" />
-      <body><![CDATA[
-        if (aFirstTime) {
-          this.labelNode.textContent = this.facetDef.strings.facetNameLabel;
-          this.checkbox.setAttribute("aria-label",
-                                     this.facetDef.strings.facetNameLabel);
-          this.trueValues = [];
-        }
-
-        // Only update count if anything other than "all" is selected.
-        //  Otherwise we lose the set of attachment types in our select box,
-        //  and that makes us sad.  We do want to update on "all" though
-        //  because other facets may further reduce the number of attachments
-        //  we see.  (Or if this is not just being used for attachments, it
-        //  still holds.)
-        if (this.selectedValue != "all") {
-          let count = 0;
-          for (let groupPair of this.orderedGroups) {
-            if (groupPair[0] != null)
-              count += groupPair[1].length;
-          }
-          this.countNode.textContent = count.toLocaleString();
-          return;
-        }
-
-        while (this.filterNode.hasChildNodes())
-          this.filterNode.lastChild.remove();
-        let allNode = document.createElement("option");
-        allNode.textContent =
-          glodaFacetStrings.get("glodaFacetView.facets.filter." +
-                                this.attrDef.attributeName + ".allLabel");
-        allNode.setAttribute("value", "all");
-        if (this.selectedValue == "all")
-          allNode.setAttribute("selected", "selected");
-        this.filterNode.appendChild(allNode);
-
-        // if we are here, we have our 2 groups, find true...
-        // (note: it is possible to get jerked around by null values
-        //  currently, so leave a reasonable failure case)
-        // empty true groups is for the checkbox
-        this.trueGroups = [];
-        // the real true groups is the actual true values for our explicit
-        //  filtering
-        this.realTrueGroups = [];
-        this.trueValues = [];
-        this.falseValues = [];
-        let selectNodes = [];
-        for (let groupPair of this.orderedGroups) {
-          if (groupPair[0] === null) {
-            this.falseValues.push.apply(this.falseValues, groupPair[1]);
-          } else {
-            this.trueValues.push.apply(this.trueValues, groupPair[1]);
-
-            let groupValue = groupPair[0];
-            let selNode = document.createElement("option");
-            selNode.textContent = groupValue[this.groupDisplayProperty];
-            selNode.setAttribute("value", this.realTrueGroups.length);
-            if (this.selectedValue == groupValue.category)
-              selNode.setAttribute("selected", "selected");
-            selectNodes.push(selNode);
-
-            this.realTrueGroups.push(groupValue);
-          }
-        }
-        selectNodes.sort(function(a, b) {
-          return a.textContent.localeCompare(b.textContent);
-        });
-        selectNodes.forEach(function(selNode) { this.filterNode.appendChild(selNode); }, this);
-
-        this.disabled = !this.trueValues.length;
-
-        this.countNode.textContent = this.trueValues.length.toLocaleString();
-      ]]></body>
-    </method>
-    <method name="checkStateChanged">
-      <body><![CDATA[
-        // if they un-check us, revert our value to all.
-        if (!this.checked)
-          this.selectedValue = "all";
-      ]]></body>
-    </method>
-    <method name="filterChanged">
-      <parameter name="event" />
-      <body><![CDATA[
-        if (!this.checked)
-          return;
-        if (this.filterNode.value == "all") {
-          this.selectedValue = "all";
-          FacetContext.addFacetConstraint(this.faceter, true,
-                                          this.trueGroups, false, true);
-        } else {
-          let groupValue = this.realTrueGroups[parseInt(this.filterNode.value)];
-          this.selectedValue = groupValue.category;
-          FacetContext.addFacetConstraint(this.faceter, true,
-                                          [groupValue], false, true);
-        }
-      ]]></body>
-    </method>
-  </implementation>
-</binding>
-
 <binding id="popup-menu">
   <content>
     <html:div anonid="parent" class="parent"
               tabindex="0"
       ><html:div anonid="include-item" class="popup-menuitem top"
         tabindex="0"
         onmouseover="this.focus()"
         onkeypress="if (event.keyCode == event.DOM_VK_RETURN) this.parentNode.parentNode.doInclude()"
--- a/mail/base/content/glodaFacetView.css
+++ b/mail/base/content/glodaFacetView.css
@@ -281,27 +281,27 @@ html[dir="rtl"] .date-vis-frame {
   display: inline-flex;
   padding: 2px;
   padding-inline-end: 6px;
   border-radius: var(--borderRadius);
   cursor: pointer;
   font-size: 90%;
 }
 
-.facetious[type="boolean"][disabled] {
+facet-boolean[disabled] {
   opacity: 0.6;
 }
 
-.facetious[type="boolean"][disabled] > .facet-checkbox-bubble,
-.facetious[type="boolean-filtered"][disabled] > .facet-checkbox-bubble {
+facet-boolean[disabled] > .facet-checkbox-bubble,
+facet-boolean-filtered[disabled] > .facet-checkbox-bubble {
   cursor: default;
 }
 
-.facetious[type="boolean"]:not([disabled]):hover > .facet-checkbox-bubble,
-.facetious[type="boolean-filtered"]:not([disabled]):hover > .facet-checkbox-bubble {
+facet-boolean:not([disabled]):hover > .facet-checkbox-bubble,
+facet-boolean-filtered:not([disabled]):hover > .facet-checkbox-bubble {
   background-color: Highlight;
   color: HighlightText;
 }
 
 .facet-checkbox-label,
 .facet-checkbox-count {
   margin: 3px;
 }
@@ -316,17 +316,17 @@ html[dir="rtl"] .date-vis-frame {
   content: "(";
 }
 .facet-checkbox-count:after {
   content: ")";
 }
 
 /* === Boolean Filtered === */
 
-.facetious[type="boolean-filtered"]:not([checked]) > .facet-filter-list {
+facet-boolean-filtered:not([checked]) > .facet-filter-list {
   display: none
 }
 
 .facet-filter-list {
   display: block;
 }
 
 /* === Discrete Facet === */
--- a/mail/base/content/glodaFacetView.js
+++ b/mail/base/content/glodaFacetView.js
@@ -118,24 +118,35 @@ const UIFacets = {
     return document.getElementById("facets");
   },
   clearFacets() {
     while (this.node.hasChildNodes()) {
       this.node.lastChild.remove();
     }
   },
   addFacet(type, attrDef, args) {
-    let facet = document.createElement("div");
+    let facet;
+
+    if (type === "boolean") {
+      facet = document.createElement("facet-boolean");
+    } else if (type === "boolean-filtered") {
+      facet = document.createElement("facet-boolean-filtered");
+    } else {
+      facet = document.createElement("div");
+      facet.setAttribute("class", "facetious");
+      facet.setAttribute("type", type);
+    }
+
     facet.attrDef = attrDef;
     facet.nounDef = attrDef.objectNounDef;
+
     for (let key in args) {
       facet[key] = args[key];
     }
-    facet.setAttribute("class", "facetious");
-    facet.setAttribute("type", type);
+
     facet.setAttribute("name", attrDef.attributeName);
     this.node.appendChild(facet);
 
     return facet;
   },
 };
 
 /**
--- a/mail/base/content/glodaFacetView.xhtml
+++ b/mail/base/content/glodaFacetView.xhtml
@@ -45,26 +45,23 @@
       onkeypress="if (event.keyCode == event.DOM_VK_ESCAPE) document.getElementById('popup-menu').hide();"
       onmouseup="return clickOnBody(event)">
   <div id="popup-menu" class="popup-menu" variety="invisible"/>
   <div id="table">
     <div>
         <div class="facets facets-sidebar" id="facets">
           <h1 id="filter-header-label">&glodaFacetView.filters.label;</h1>
           <div>
-            <div id="facet-fromMe" class="facetious" type="boolean" attr="fromMe"
-                 uninitialized="true" />
-            <div id="facet-toMe" class="facetious" type="boolean" attr="toMe"
-                 uninitialized="true" />
-            <div id="facet-star" class="facetious" type="boolean" attr="star"
-                 uninitialized="true"/><br />
-            <div id="facet-attachmentTypes" class="facetious" type="boolean-filtered"
-                 attr="attachmentTypes"
-                 groupDisplayProperty="categoryLabel"
-                 uninitialized="true"/>
+            <facet-boolean id="facet-fromMe" attr="fromMe" uninitialized="true"/>
+            <facet-boolean id="facet-toMe" attr="toMe" uninitialized="true"/>
+            <facet-boolean id="facet-star" attr="star" uninitialized="true"/><br/>
+            <facet-boolean-filtered id="facet-attachmentTypes"
+                                    attr="attachmentTypes"
+                                    groupDisplayProperty="categoryLabel"
+                                    uninitialized="true"/>
           </div>
         </div>
         <div id="main-column">
           <div id="header">
             <div id="date-toggle" class="date-toggle" tabindex="0" role="button"
                  onclick="FacetContext.toggleTimeline()"
                  onkeypress="if (event.charCode == KeyEvent.DOM_VK_SPACE) { FacetContext.toggleTimeline(); event.preventDefault() }"/>
             <div id="search-value"/>