Bug 1530503 - [de-xbl] convert calendar-alarm-widget binding to custom element. r=philipp
authorPaul Morris <paul@paulwmorris.com>
Mon, 15 Apr 2019 11:00:14 -0400
changeset 26360 b8523a5ed09c
parent 26359 29de53dcff07
child 26361 e8744607741f
push id15800
push usermozilla@jorgk.com
push dateTue, 16 Apr 2019 08:27:34 +0000
treeherdercomm-central@e8744607741f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersphilipp
bugs1530503, 1528201
Bug 1530503 - [de-xbl] convert calendar-alarm-widget binding to custom element. r=philipp Also fixes: Bug 1528201 - [de-xbl] convert calendar-snooze-popup binding to custom element. r=philipp I've combined two patches (one for each bug) to better retain the history from the removed calendar-alarm-widget.xml file.
calendar/base/content/dialogs/calendar-alarm-dialog.xul
calendar/base/content/widgets/calendar-alarm-widget.js
calendar/base/content/widgets/calendar-alarm-widget.xml
calendar/base/jar.mn
calendar/base/themes/common/dialogs/calendar-alarm-dialog.css
calendar/base/themes/linux/dialogs/calendar-alarm-dialog.css
calendar/base/themes/osx/dialogs/calendar-alarm-dialog.css
calendar/base/themes/windows/dialogs/calendar-alarm-dialog.css
calendar/test/mozmill/eventDialog/testAlarmDialog.js
--- a/calendar/base/content/dialogs/calendar-alarm-dialog.xul
+++ b/calendar/base/content/dialogs/calendar-alarm-dialog.xul
@@ -22,26 +22,26 @@
         onload="setupWindow(); window.arguments[0].wrappedJSObject.window_onLoad();"
         onunload="finishWindow();"
         onfocus="onFocusWindow();"
         onkeypress="if (event.key == 'Escape') { window.close(); }"
         width="600"
         height="300">
   <script type="application/javascript" src="chrome://calendar/content/calendar-alarm-dialog.js"/>
   <script type="application/javascript" src="chrome://calendar/content/calendar-item-editing.js"/>
+  <script type="application/javascript" src="chrome://calendar/content/widgets/calendar-alarm-widget.js"/>
 
   <notificationbox id="readonly-notification" notificationside="top"/>
   <richlistbox id="alarm-richlist" flex="1" onselect="onSelectAlarm(event)"/>
 
   <hbox pack="end" id="alarm-actionbar" align="center">
     <button id="alarm-snooze-all-button"
             type="menu"
             label="&calendar.alarm.snoozeallfor.label;">
-      <menupopup id="alarm-snooze-all-popup"
-                 type="snooze-menupopup"
+      <menupopup is="calendar-snooze-popup" id="alarm-snooze-all-popup"
                  ignorekeys="true"/>
     </button>
     <button id="alarm-dismiss-all-button"
             label="&calendar.alarm.dismissall.label;"
             oncommand="onDismissAllAlarms();"/>
   </hbox>
   <hbox pack="end" class="resizer-box">
     <resizer dir="bottomright"/>
rename from calendar/base/content/widgets/calendar-alarm-widget.xml
rename to calendar/base/content/widgets/calendar-alarm-widget.js
--- a/calendar/base/content/widgets/calendar-alarm-widget.xml
+++ b/calendar/base/content/widgets/calendar-alarm-widget.js
@@ -1,354 +1,399 @@
-<?xml version="1.0"?>
-<!-- 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/. -->
-
-<!-- globals cal -->
+/* 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/. */
 
-<!DOCTYPE bindings
-[
-  <!ENTITY % dtd1 SYSTEM "chrome://calendar/locale/global.dtd" > %dtd1;
-  <!ENTITY % dtd2 SYSTEM "chrome://calendar/locale/calendar.dtd" > %dtd2;
-]>
+"use strict";
 
-<bindings id="calendar-alarms"
-          xmlns="http://www.mozilla.org/xbl"
-          xmlns:xbl="http://www.mozilla.org/xbl"
-          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+/* global cal Cr MozElements MozXULElement PluralForm Services */
 
-  <binding id="calendar-alarm-widget" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
-    <content orient="horizontal">
-      <xul:vbox pack="start">
-        <xul:image class="alarm-calendar-image"/>
-      </xul:vbox>
-      <xul:vbox flex="1">
-        <xul:label anonid="alarm-title-label" class="alarm-title-label" crop="end"/>
-        <xul:vbox class="additional-information-box">
-          <xul:label anonid="alarm-date-label" class="alarm-date-label"/>
-          <xul:hbox>
-            <xul:label anonid="alarm-location-label" class="location-label">&calendar.alarm.location.label;</xul:label>
-            <xul:description anonid="alarm-location-description"
-                             class="alarm-location-description"
-                             crop="end"
-                             flex="1"/>
-          </xul:hbox>
-          <xul:hbox pack="start">
-            <xul:label class="text-link alarm-details-label"
-                       value="&calendar.alarm.details.label;"
-                       onclick="showDetails(event)"
-                       onkeypress="showDetails(event)"/>
-          </xul:hbox>
-        </xul:vbox>
-      </xul:vbox>
-      <xul:spacer flex="1"/>
-      <xul:label anonid="alarm-relative-date-label" class="alarm-relative-date-label"/>
-      <xul:vbox class="alarm-action-buttons" pack="center">
-        <xul:button anonid="alarm-snooze-button"
-                    type="menu"
-                    label="&calendar.alarm.snoozefor.label;">
-          <xul:menupopup type="snooze-menupopup" ignorekeys="true"/>
-        </xul:button>
-        <xul:button anonid="alarm-dismiss-button"
-                    label="&calendar.alarm.dismiss.label;"
-                    oncommand="dismissAlarm()"/>
-      </xul:vbox>
-    </content>
+// Wrap in a block to prevent leaking to window scope.
+{
+    /**
+     * Represents an alarm in the alarms dialog. It appears there when an alarm is fired, and
+     * allows the alarm to be snoozed, dismissed, etc.
+     * @extends MozElements.MozRichlistitem
+     */
+    class MozCalendarAlarmWidget extends MozElements.MozRichlistitem {
+        connectedCallback() {
+            if (this.delayConnectedCallback() || this.hasConnected) {
+                return;
+            }
+            this.hasConnected = true;
+            this.appendChild(MozXULElement.parseXULToFragment(`
+                <vbox pack="start">
+                  <image class="alarm-calendar-image"/>
+                </vbox>
+                <vbox flex="1">
+                  <label class="alarm-title-label" crop="end"/>
+                  <vbox class="additional-information-box">
+                    <label class="alarm-date-label"/>
+                    <hbox>
+                      <label class="alarm-location-label"/>
+                      <description class="alarm-location-description"
+                                   crop="end"
+                                   flex="1"/>
+                    </hbox>
+                    <hbox pack="start">
+                      <label class="text-link alarm-details-label"
+                             value="&calendar.alarm.details.label;"
+                             onclick="showDetails(event)"
+                             onkeypress="showDetails(event)"/>
+                    </hbox>
+                  </vbox>
+                </vbox>
+                <spacer flex="1"/>
+                <label class="alarm-relative-date-label"/>
+                <vbox class="alarm-action-buttons" pack="center">
+                  <button class="alarm-snooze-button"
+                          type="menu"
+                          label="&calendar.alarm.snoozefor.label;">
+                    <menupopup is="calendar-snooze-popup" ignorekeys="true"/>
+                  </button>
+                  <button class="alarm-dismiss-button"
+                          label="&calendar.alarm.dismiss.label;"
+                          oncommand="dismissAlarm()"/>
+                </vbox>
+                `,
+                [
+                    "chrome://calendar/locale/global.dtd",
+                    "chrome://calendar/locale/calendar.dtd"
+                ]
+            ));
+            this.mItem = null;
+            this.mAlarm = null;
+        }
 
-    <implementation>
-      <field name="mItem">null</field>
-      <field name="mAlarm">null</field>
+        set item(val) {
+            this.mItem = val;
+            this.updateLabels();
+            return val;
+        }
+
+        get item() {
+            return this.mItem;
+        }
 
-      <property name="item"
-                onget="return this.mItem;"
-                onset="this.mItem = val; this.updateLabels(); return val;"/>
-      <property name="alarm"
-                onget="return this.mAlarm;"
-                onset="this.mAlarm = val; this.updateLabels(); return val;"/>
+        set alarm(val) {
+            this.mAlarm = val;
+            this.updateLabels();
+            return val;
+        }
 
-      <method name="updateLabels">
-        <body><![CDATA[
+        get alarm() {
+            return this.mAlarm;
+        }
+
+        /**
+         * Refresh UI text (dates, titles, locations) when the data has changed.
+         */
+        updateLabels() {
             if (!this.mItem || !this.mAlarm) {
                 // Setup not complete, do nothing for now.
                 return;
             }
-
-            let formatter = cal.getDateFormatter();
-            let titleLabel = document.getAnonymousElementByAttribute(this, "anonid", "alarm-title-label");
-            let locationDescription = document.getAnonymousElementByAttribute(this, "anonid", "alarm-location-description");
-            let dateLabel = document.getAnonymousElementByAttribute(this, "anonid", "alarm-date-label");
+            const formatter = cal.getDateFormatter();
+            let titleLabel = this.querySelector(".alarm-title-label");
+            let locationDescription = this.querySelector(".alarm-location-description");
+            let dateLabel = this.querySelector(".alarm-date-label");
 
             // Dates
             if (cal.item.isEvent(this.mItem)) {
                 dateLabel.textContent = formatter.formatItemInterval(this.mItem);
             } else if (cal.item.isToDo(this.mItem)) {
                 let startDate = this.mItem.entryDate || this.mItem.dueDate;
                 if (startDate) {
-                    // A Task with a start or due date, show with label
+                    // A task with a start or due date, show with label.
                     startDate = startDate.getInTimezone(cal.dtz.defaultTimezone);
                     dateLabel.textContent = cal.l10n.getCalString(
                         "alarmStarts",
                         [formatter.formatDateTime(startDate)]
                     );
                 } else {
                     // If the task has no start date, then format the alarm date.
-                    dateLabel.textContent = formatter.formatDateTime(this.mAlarm.alarmDate);
+                    dateLabel.textContent = formatter.formatDateTime(
+                        this.mAlarm.alarmDate
+                    );
                 }
             } else {
                 throw Cr.NS_ERROR_ILLEGAL_VALUE;
             }
 
-            // Relative date
+            // Relative Date
             this.updateRelativeDateLabel();
 
-            // Title, location
+            // Title, Location
             titleLabel.textContent = this.mItem.title || "";
             locationDescription.textContent = this.mItem.getProperty("LOCATION") || "";
             locationDescription.hidden = (locationDescription.textContent.length < 1);
 
-            document.getAnonymousElementByAttribute(this, "anonid", "alarm-location-label").hidden =
-                (locationDescription.textContent.length < 1);
+            this.querySelector(".alarm-location-label")
+                .hidden = (locationDescription.textContent.length < 1);
 
-            // hide snooze button if read-only
-            let snoozeButton = document.getAnonymousElementByAttribute(
-                this,
-                "anonid",
-                "alarm-snooze-button"
-            );
+            // Hide snooze button if read-only.
+            let snoozeButton = this.querySelector(".alarm-snooze-button");
             if (!cal.acl.isCalendarWritable(this.mItem.calendar) ||
                 !cal.acl.userCanModifyItem(this.mItem)) {
                 let tooltip = "reminderDisabledSnoozeButtonTooltip";
                 snoozeButton.disabled = true;
-                snoozeButton.setAttribute("tooltiptext",
-                                          cal.l10n.getString("calendar-alarms", tooltip));
+                snoozeButton.setAttribute(
+                    "tooltiptext",
+                    cal.l10n.getString("calendar-alarms", tooltip)
+                );
             } else {
                 snoozeButton.disabled = false;
                 snoozeButton.removeAttribute("tooltiptext");
             }
-        ]]></body>
-      </method>
+        }
 
-      <method name="updateRelativeDateLabel">
-        <body><![CDATA[
-            let formatter = cal.getDateFormatter();
-            let item = this.mItem;
-            let relativeDateLabel = document.getAnonymousElementByAttribute(this, "anonid", "alarm-relative-date-label");
+        /**
+         * Refresh UI text for relative date when the data has changed.
+         */
+        updateRelativeDateLabel() {
+            const formatter = cal.getDateFormatter();
+            const item = this.mItem;
+            let relativeDateLabel = this.querySelector(".alarm-relative-date-label");
             let relativeDateString;
-            let startDate = item[cal.dtz.startDateProp(item)] || item[cal.dtz.endDateProp(item)];
+            let startDate = item[cal.dtz.startDateProp(item)] ||
+                item[cal.dtz.endDateProp(item)];
+
             if (startDate) {
                 startDate = startDate.getInTimezone(cal.dtz.defaultTimezone);
                 let currentDate = cal.dtz.now();
-                let sinceDayStart = (currentDate.hour * 3600) + (currentDate.minute * 60);
+
+                const sinceDayStart = (currentDate.hour * 3600) +
+                    (currentDate.minute * 60);
 
                 currentDate.second = 0;
                 startDate.second = 0;
 
-                let sinceAlarm = currentDate.subtractDate(startDate).inSeconds;
-                this.mAlarmToday = (sinceAlarm < sinceDayStart) && (sinceAlarm > sinceDayStart - 86400);
+                const sinceAlarm = currentDate.subtractDate(startDate).inSeconds;
+
+                this.mAlarmToday = (sinceAlarm < sinceDayStart) &&
+                    (sinceAlarm > sinceDayStart - 86400);
 
                 if (this.mAlarmToday) {
-                    // The alarm is today
+                    // The alarm is today.
                     relativeDateString = cal.l10n.getCalString(
                         "alarmTodayAt",
                         [formatter.formatTime(startDate)]
                     );
-                } else if (sinceAlarm <= sinceDayStart - 86400 && sinceAlarm > sinceDayStart - 172800) {
-                    // The alarm is tomorrow
+                } else if (sinceAlarm <= sinceDayStart - 86400 &&
+                    sinceAlarm > sinceDayStart - 172800) {
+                    // The alarm is tomorrow.
                     relativeDateString = cal.l10n.getCalString(
                         "alarmTomorrowAt",
                         [formatter.formatTime(startDate)]
                     );
-                } else if (sinceAlarm < sinceDayStart + 86400 && sinceAlarm > sinceDayStart) {
-                    // The alarm is yesterday
+                } else if (sinceAlarm < sinceDayStart + 86400 &&
+                    sinceAlarm > sinceDayStart) {
+                    // The alarm is yesterday.
                     relativeDateString = cal.l10n.getCalString(
                         "alarmYesterdayAt",
                         [formatter.formatTime(startDate)]
                     );
                 } else {
-                    // The alarm is way back
+                    // The alarm is way back.
                     relativeDateString = [formatter.formatDateTime(startDate)];
                 }
             } else {
-                // No start or end date, therefore the alarm must be absolute and
-                // have an alarm date.
-                relativeDateString = [formatter.formatDateTime(this.mAlarm.alarmDate)];
+                // No start or end date, therefore the alarm must be absolute
+                // and have an alarm date.
+                relativeDateString = [
+                    formatter.formatDateTime(this.mAlarm.alarmDate)
+                ];
             }
 
             relativeDateLabel.textContent = relativeDateString;
-        ]]></body>
-      </method>
+        }
 
-      <method name="showDetails">
-        <parameter name="event"/>
-        <body><![CDATA[
-            if (event.type == "click" ||
-                (event.type == "keypress" &&
-                 event.key == "Enter")) {
-                let detailsEvent = document.createEvent("Events");
-                detailsEvent.initEvent("itemdetails", true, false);
+        /**
+         * Click/keypress handler for "Details" link. Dispatches an event to open an item dialog.
+         * @param event {Event} The click or keypress event.
+         */
+        showDetails(event) {
+            if (event.type == "click" || (event.type == "keypress" && event.key == "Enter")) {
+                const detailsEvent = new Event("itemdetails", { bubbles: true, cancelable: false });
                 this.dispatchEvent(detailsEvent);
             }
-        ]]></body>
-      </method>
+        }
 
-      <method name="dismissAlarm">
-        <body><![CDATA[
-            let dismissEvent = document.createEvent("Events");
-            dismissEvent.initEvent("dismiss", true, false);
+        /**
+         * Click handler for "Dismiss" button.  Dispatches an event to dismiss the alarm.
+         */
+        dismissAlarm() {
+            const dismissEvent = new Event("dismiss", { bubbles: true, cancelable: false });
             this.dispatchEvent(dismissEvent);
-        ]]></body>
-      </method>
-    </implementation>
-  </binding>
+        }
+    }
+
+    customElements.define("calendar-alarm-widget", MozCalendarAlarmWidget);
 
-  <binding id="calendar-snooze-popup">
-    <content ignorekeys="true">
-      <xul:menuitem label="&calendar.alarm.snooze.5minutes.label;"
-                    value="5"
-                    oncommand="snoozeItem(event)"/>
-      <xul:menuitem label="&calendar.alarm.snooze.10minutes.label;"
-                    value="10"
-                    oncommand="snoozeItem(event)"/>
-      <xul:menuitem label="&calendar.alarm.snooze.15minutes.label;"
-                    value="15"
-                    oncommand="snoozeItem(event)"/>
-      <xul:menuitem label="&calendar.alarm.snooze.30minutes.label;"
-                    value="30"
-                    oncommand="snoozeItem(event)"/>
-      <xul:menuitem label="&calendar.alarm.snooze.45minutes.label;"
-                    value="45"
-                    oncommand="snoozeItem(event)"/>
-      <xul:menuitem label="&calendar.alarm.snooze.1hour.label;"
-                    value="60"
-                    oncommand="snoozeItem(event)"/>
-      <xul:menuitem label="&calendar.alarm.snooze.2hours.label;"
-                    value="120"
-                    oncommand="snoozeItem(event)"/>
-      <xul:menuitem label="&calendar.alarm.snooze.1day.label;"
-                    value="1440"
-                    oncommand="snoozeItem(event)"/>
-      <children/>
-      <xul:menuseparator/>
-      <xul:hbox class="snooze-options-box">
-        <xul:textbox anonid="snooze-value-textbox"
-                     oninput="updateAccessibleName()"
-                     onselect="updateAccessibleName()"
-                     type="number"
-                     size="3"/>
-        <xul:menulist anonid="snooze-unit-menulist"
-                      class="snooze-unit-menulist"
-                      allowevents="true">
-          <xul:menupopup anonid="snooze-unit-menupopup"
-                         position="after_start"
-                         ignorekeys="true"
-                         class="menulist-menupopup">
-            <xul:menuitem closemenu="single" class="unit-menuitem" value="1"/>
-            <xul:menuitem closemenu="single" class="unit-menuitem" value="60"/>
-            <xul:menuitem closemenu="single" class="unit-menuitem" value="1440"/>
-          </xul:menupopup>
-        </xul:menulist>
-        <xul:toolbarbutton anonid="snooze-popup-ok"
-                           class="snooze-popup-button snooze-popup-ok-button"
-                           oncommand="snoozeOk()"/>
-        <xul:toolbarbutton anonid="snooze-popup-cancel"
-                           class="snooze-popup-button snooze-popup-cancel-button"
-                           aria-label="&calendar.alarm.snooze.cancel;"
-                           oncommand="snoozeCancel()"/>
-      </xul:hbox>
-    </content>
-    <implementation>
-      <constructor><![CDATA[
-          const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+    /**
+     * A popup panel for selecting how long to snooze alarms/reminders.
+     * It appears when a snooze button is clicked.
+     * @extends MozElements.MozMenuPopup
+     */
+    class MozCalendarSnoozePopup extends MozElements.MozMenuPopup {
+        connectedCallback() {
+            if (this.delayConnectedCallback() || this.hasConnected) {
+                return;
+            }
+            this.hasConnected = true;
+            this.appendChild(MozXULElement.parseXULToFragment(`
+                <menuitem label="&calendar.alarm.snooze.5minutes.label;"
+                          value="5"
+                          oncommand="snoozeItem(event)"/>
+                <menuitem label="&calendar.alarm.snooze.10minutes.label;"
+                          value="10"
+                          oncommand="snoozeItem(event)"/>
+                <menuitem label="&calendar.alarm.snooze.15minutes.label;"
+                          value="15"
+                          oncommand="snoozeItem(event)"/>
+                <menuitem label="&calendar.alarm.snooze.30minutes.label;"
+                          value="30"
+                          oncommand="snoozeItem(event)"/>
+                <menuitem label="&calendar.alarm.snooze.45minutes.label;"
+                          value="45"
+                          oncommand="snoozeItem(event)"/>
+                <menuitem label="&calendar.alarm.snooze.1hour.label;"
+                          value="60"
+                          oncommand="snoozeItem(event)"/>
+                <menuitem label="&calendar.alarm.snooze.2hours.label;"
+                          value="120"
+                          oncommand="snoozeItem(event)"/>
+                <menuitem label="&calendar.alarm.snooze.1day.label;"
+                          value="1440"
+                          oncommand="snoozeItem(event)"/>
+                <menuseparator/>
+                <hbox class="snooze-options-box">
+                  <textbox class="snooze-value-textbox"
+                           oninput="updateUIText()"
+                           onselect="updateUIText()"
+                           type="number"
+                           size="3"/>
+                  <menulist class="snooze-unit-menulist" allowevents="true">
+                    <menupopup class="snooze-unit-menupopup menulist-menupopup"
+                               position="after_start"
+                               ignorekeys="true">
+                      <menuitem closemenu="single" class="unit-menuitem" value="1"></menuitem>
+                      <menuitem closemenu="single" class="unit-menuitem" value="60"></menuitem>
+                      <menuitem closemenu="single" class="unit-menuitem" value="1440"></menuitem>
+                    </menupopup>
+                  </menulist>
+                  <toolbarbutton class="snooze-popup-button snooze-popup-ok-button"
+                                 oncommand="snoozeOk()"/>
+                  <toolbarbutton class="snooze-popup-button snooze-popup-cancel-button"
+                                 aria-label="&calendar.alarm.snooze.cancel;"
+                                 oncommand="snoozeCancel()"/>
+                </hbox>
+                `,
+                [
+                    "chrome://calendar/locale/global.dtd",
+                    "chrome://calendar/locale/calendar.dtd"
+                ]
+            ));
+            const defaultSnoozeLength = Services.prefs.getIntPref("calendar.alarms.defaultsnoozelength", 0);
+            const snoozeLength = defaultSnoozeLength <= 0 ? 5 : defaultSnoozeLength;
+
+            let unitList = this.querySelector(".snooze-unit-menulist");
+            let unitValue = this.querySelector(".snooze-value-textbox");
 
-          let snoozePref = Services.prefs.getIntPref("calendar.alarms.defaultsnoozelength", 0);
-          if (snoozePref <= 0) {
-              snoozePref = 5;
-          }
-
-          let unitList = document.getAnonymousElementByAttribute(this, "anonid", "snooze-unit-menulist");
-          let unitValue = document.getAnonymousElementByAttribute(this, "anonid", "snooze-value-textbox");
+            if ((snoozeLength / 60) % 24 == 0) {
+                // Days
+                unitValue.value = (snoozeLength / 60) / 24;
+                unitList.selectedIndex = 2;
+            } else if (snoozeLength % 60 == 0) {
+                // Hours
+                unitValue.value = snoozeLength / 60;
+                unitList.selectedIndex = 1;
+            } else {
+                // Minutes
+                unitValue.value = snoozeLength;
+                unitList.selectedIndex = 0;
+            }
 
-          let selectedIndex = 0;
-          if ((snoozePref % 60) == 0) {
-              snoozePref = snoozePref / 60;
-              if ((snoozePref % 24) == 0) {
-                  snoozePref = snoozePref / 24;
-                  selectedIndex = 2; // Days
-              } else {
-                  selectedIndex = 1; // Hours
-              }
-          } else {
-              selectedIndex = 0; // Minutes
-          }
+            this.updateUIText();
+        }
 
-          unitList.selectedIndex = selectedIndex;
-          unitValue.value = snoozePref;
-
-          this.updateAccessibleName();
-      ]]></constructor>
-
-      <method name="snoozeAlarm">
-        <parameter name="minutes"/>
-        <body><![CDATA[
-            let snoozeEvent = document.createEvent("Events");
-            snoozeEvent.initEvent("snooze", true, false);
+        /**
+         * Dispatch a snooze event when an alarm is snoozed.
+         * @param minutes {number|string} The number of minutes to snooze for.
+         */
+        snoozeAlarm(minutes) {
+            let snoozeEvent = new Event("snooze", { bubbles: true, cancelable: false });
             snoozeEvent.detail = minutes;
-            this.dispatchEvent(snoozeEvent);
-        ]]></body>
-      </method>
 
-      <method name="snoozeItem">
-        <parameter name="event"/>
-        <body><![CDATA[
+            // For single alarms the event.target has to be the calendar-alarm-widget element,
+            // (so call dispatchEvent on that). For snoozing all alarms the event.target is not
+            // relevant but the snooze all popup is not inside a calendar-alarm-widget (so call
+            // dispatchEvent on 'this').
+            const eventTarget = this.id == "alarm-snooze-all-popup"
+                ? this
+                : this.closest("calendar-alarm-widget");
+            eventTarget.dispatchEvent(snoozeEvent);
+        }
+
+        /**
+         * Click handler for snooze popup menu items (like "5 Minutes", "1 Hour", etc.).
+         * @param event {Event} The click event.
+         */
+        snoozeItem(event) {
             this.snoozeAlarm(event.target.value);
-        ]]></body>
-      </method>
-
-      <method name="snoozeOk">
-        <body><![CDATA[
-            let unitList = document.getAnonymousElementByAttribute(this, "anonid", "snooze-unit-menulist");
-            let unitValue = document.getAnonymousElementByAttribute(this, "anonid", "snooze-value-textbox");
+        }
 
-            let minutes = (unitList.value || 1) * unitValue.value;
+        /**
+         * Click handler for the "OK" (checkmark) button when snoozing for a custom amount of time.
+         */
+        snoozeOk() {
+            const unitList = this.querySelector(".snooze-unit-menulist");
+            const unitValue = this.querySelector(".snooze-value-textbox");
+            const minutes = (unitList.value || 1) * unitValue.value;
             this.snoozeAlarm(minutes);
-        ]]></body>
-      </method>
+        }
 
-      <method name="snoozeCancel">
-        <body><![CDATA[
+        /**
+         * Click handler for the "cancel" ("X") button for not snoozing a custom amount of time.
+         */
+        snoozeCancel() {
             this.hidePopup();
-        ]]></body>
-      </method>
+        }
 
-      <method name="updateAccessibleName">
-        <body><![CDATA[
-            let unitList = document.getAnonymousElementByAttribute(this, "anonid", "snooze-unit-menulist");
-            let unitPopup = document.getAnonymousElementByAttribute(this, "anonid", "snooze-unit-menupopup");
-            let unitValue = document.getAnonymousElementByAttribute(this, "anonid", "snooze-value-textbox");
-            let okButton = document.getAnonymousElementByAttribute(this, "anonid", "snooze-popup-ok");
+        /**
+         * Initializes and updates the dynamic UI text. This text can change depending on
+         * input, like for plurals, when you change from "[1] [minute]" to "[2] [minutes]".
+         */
+        updateUIText() {
+            const unitList = this.querySelector(".snooze-unit-menulist");
+            const unitPopup = this.querySelector(".snooze-unit-menupopup");
+            const unitValue = this.querySelector(".snooze-value-textbox");
+            let okButton = this.querySelector(".snooze-popup-ok-button");
 
             function unitName(list) {
-                return { 1: "unitMinutes", 60: "unitHours", 1440: "unitDays" }[list.value] || "unitMinutes";
+                return { 1: "unitMinutes", 60: "unitHours", 1440: "unitDays" }[list.value] ||
+                "unitMinutes";
             }
 
-            let { PluralForm } = ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
-
             let pluralString = cal.l10n.getCalString(unitName(unitList));
-            let unitPlural = PluralForm.get(unitValue.value, pluralString)
-                                       .replace("#1", unitValue.value);
+
+            const unitPlural = PluralForm.get(unitValue.value, pluralString)
+                .replace("#1", unitValue.value);
 
-            let accessibleString = cal.l10n.getString("calendar-alarms",
-                                                      "reminderSnoozeOkA11y",
-                                                      [unitPlural]);
-            okButton.setAttribute("aria-label", accessibleString);
+            let okButtonAriaLabel = cal.l10n.getString(
+                "calendar-alarms",
+                "reminderSnoozeOkA11y",
+                [unitPlural]
+            );
+            okButton.setAttribute("aria-label", okButtonAriaLabel);
 
-            let items = unitPopup.getElementsByTagName("xul:menuitem");
+            const items = unitPopup.getElementsByTagName("menuitem");
             for (let menuItem of items) {
                 pluralString = cal.l10n.getCalString(unitName(menuItem));
+
                 menuItem.label = PluralForm.get(unitValue.value, pluralString)
-                                           .replace("#1", "").trim();
+                    .replace("#1", "").trim();
             }
-        ]]></body>
-      </method>
-    </implementation>
-  </binding>
-</bindings>
+        }
+    }
+
+    customElements.define("calendar-snooze-popup", MozCalendarSnoozePopup, { "extends": "menupopup" });
+}
--- a/calendar/base/jar.mn
+++ b/calendar/base/jar.mn
@@ -87,17 +87,17 @@ calendar.jar:
     content/calendar/chooseCalendarDialog.xul              (content/dialogs/chooseCalendarDialog.xul)
     content/calendar/preferences/alarms.js                 (content/preferences/alarms.js)
     content/calendar/preferences/categories.js             (content/preferences/categories.js)
     content/calendar/preferences/editCategory.xul          (content/preferences/editCategory.xul)
     content/calendar/preferences/editCategory.js           (content/preferences/editCategory.js)
     content/calendar/preferences/general.js                (content/preferences/general.js)
     content/calendar/preferences/views.js                  (content/preferences/views.js)
     content/calendar/widgets/minimonth.xml                 (content/widgets/minimonth.xml)
-    content/calendar/widgets/calendar-alarm-widget.xml     (content/widgets/calendar-alarm-widget.xml)
+    content/calendar/widgets/calendar-alarm-widget.js      (content/widgets/calendar-alarm-widget.js)
     content/calendar/widgets/calendar-widgets.xml          (content/widgets/calendar-widgets.xml)
     content/calendar/widgets/calendar-list-tree.xml        (content/widgets/calendar-list-tree.xml)
     content/calendar/calendar-subscriptions-list.xml       (content/widgets/calendar-subscriptions-list.xml)
     content/calendar/widgets/calendar-widget-bindings.css  (content/widgets/calendar-widget-bindings.css)
     content/calendar/calApplicationUtils.js                (src/calApplicationUtils.js)
     content/calendar/calFilter.js                          (src/calFilter.js)
     content/calendar/WindowsNTToZoneInfoTZId.properties    (src/WindowsNTToZoneInfoTZId.properties)
 % skin calendar classic/1.0 chrome/skin/linux/calendar/
--- a/calendar/base/themes/common/dialogs/calendar-alarm-dialog.css
+++ b/calendar/base/themes/common/dialogs/calendar-alarm-dialog.css
@@ -1,17 +1,12 @@
 /* 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/. */
 
-/* Bindings */
-calendar-alarm-widget {
-    -moz-binding: url(chrome://calendar/content/widgets/calendar-alarm-widget.xml#calendar-alarm-widget);
-}
-
 /* Alarm dialog styles */
 #alarm-richlist {
     margin: 10px;
 }
 
 #alarm-actionbar {
     min-width: 1px;
     margin: 0 5px;
--- a/calendar/base/themes/linux/dialogs/calendar-alarm-dialog.css
+++ b/calendar/base/themes/linux/dialogs/calendar-alarm-dialog.css
@@ -1,17 +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/. */
 
 @import url(chrome://calendar-common/skin/dialogs/calendar-alarm-dialog.css);
 
-menupopup[type="snooze-menupopup"] {
-    -moz-binding: url(chrome://calendar/content/widgets/calendar-alarm-widget.xml#calendar-snooze-popup);
-}
-
 .snooze-popup-ok-button:hover {
     background-color: -moz-menuhover;
 }
 
 .snooze-popup-cancel-button:hover {
     background-color: -moz-menuhover;
 }
--- a/calendar/base/themes/osx/dialogs/calendar-alarm-dialog.css
+++ b/calendar/base/themes/osx/dialogs/calendar-alarm-dialog.css
@@ -1,9 +1,5 @@
 /* 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/. */
 
 @import url(chrome://calendar-common/skin/dialogs/calendar-alarm-dialog.css);
-
-menupopup[type="snooze-menupopup"] {
-    -moz-binding: url(chrome://calendar/content/widgets/calendar-alarm-widget.xml#calendar-snooze-popup);
-}
--- a/calendar/base/themes/windows/dialogs/calendar-alarm-dialog.css
+++ b/calendar/base/themes/windows/dialogs/calendar-alarm-dialog.css
@@ -1,17 +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/. */
 
 @import url(chrome://calendar-common/skin/dialogs/calendar-alarm-dialog.css);
 
-menupopup[type="snooze-menupopup"] {
-    -moz-binding: url(chrome://calendar/content/widgets/calendar-alarm-widget.xml#calendar-snooze-popup);
-}
-
 .snooze-popup-ok-button:hover {
     background-color: -moz-menuhover;
 }
 
 .snooze-popup-cancel-button:hover {
     background-color: -moz-menuhover;
 }
--- a/calendar/test/mozmill/eventDialog/testAlarmDialog.js
+++ b/calendar/test/mozmill/eventDialog/testAlarmDialog.js
@@ -80,17 +80,17 @@ function testAlarmDialog() {
         event.click(eventid("button-saveandclose"));
     });
 
     // Snooze the alarm.
     plan_for_modal_dialog("Calendar:AlarmWindow", alarm => {
         let { eid: alarmid } = helpersForController(alarm);
         let snoozeAllButton = alarmid("alarm-snooze-all-button");
         let popup = alarmid("alarm-snooze-all-popup").getNode();
-        let menuitems = alarm.window.document.getAnonymousNodes(popup);
+        let menuitems = popup.querySelectorAll(":scope > menuitem");
 
         alarm.waitThenClick(snoozeAllButton);
         menuitems[5].click();
     });
     wait_for_modal_dialog("Calendar:AlarmWindow", TIMEOUT_MODAL_DIALOG);
 }
 
 function teardownTest(module) {