Bug 1076767 - Add a spinner to the Import Contacts button whilst importing. r=jaws a=loop-only
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Tue, 11 Nov 2014 14:07:11 +0000
changeset 235236 c59cf7402c10947062dc240e104c04d5ec0345d7
parent 235235 18a2ba5eacbb086343a1bc3cb799fb5138bafe8e
child 235237 d382e4762f9bfafa01c237fbf4d8dee275e1f147
push id611
push userraliiev@mozilla.com
push dateMon, 05 Jan 2015 23:23:16 +0000
treeherdermozilla-release@345cd3b9c445 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws, loop-only
bugs1076767
milestone35.0a2
Bug 1076767 - Add a spinner to the Import Contacts button whilst importing. r=jaws a=loop-only
browser/components/loop/content/js/contacts.js
browser/components/loop/content/js/contacts.jsx
browser/components/loop/content/js/panel.js
browser/components/loop/content/js/panel.jsx
browser/components/loop/content/shared/css/contacts.css
browser/components/loop/content/shared/css/panel.css
browser/components/loop/content/shared/img/loading-icon.gif
browser/components/loop/content/shared/img/spinner.png
browser/components/loop/content/shared/img/spinner@2x.png
browser/components/loop/content/shared/js/views.js
browser/components/loop/content/shared/js/views.jsx
browser/components/loop/jar.mn
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -412,16 +412,18 @@ loop.contacts = (function(_, mozL10n) {
         return comp;
       }
       // If names are equal, compare against unique ids to make sure we have
       // consistent ordering.
       return contact1._guid - contact2._guid;
     },
 
     render: function() {
+      let cx = React.addons.classSet;
+
       let viewForItem = item => {
         return ContactDetail({key: item._guid, contact: item, 
                               handleContactAction: this.handleContactAction})
       };
 
       let shownContacts = _.groupBy(this.contacts, function(contact) {
         return contact.blocked ? "blocked" : "available";
       });
@@ -439,26 +441,29 @@ loop.contacts = (function(_, mozL10n) {
             shownContacts.available = shownContacts.available.filter(filterFn);
           }
           if (shownContacts.blocked) {
             shownContacts.blocked = shownContacts.blocked.filter(filterFn);
           }
         }
       }
 
-      // TODO: bug 1076767 - add a spinner whilst importing contacts.
       return (
         React.DOM.div(null, 
           React.DOM.div({className: "content-area"}, 
             ButtonGroup(null, 
               Button({caption: this.state.importBusy
                                ? mozL10n.get("importing_contacts_progress_button")
                                : mozL10n.get("import_contacts_button"), 
                       disabled: this.state.importBusy, 
-                      onClick: this.handleImportButtonClick}), 
+                      onClick: this.handleImportButtonClick}, 
+                React.DOM.div({className: cx({"contact-import-spinner": true,
+                                    spinner: true,
+                                    busy: this.state.importBusy})})
+              ), 
               Button({caption: mozL10n.get("new_contact_button"), 
                       onClick: this.handleAddContactButtonClick})
             ), 
             showFilter ?
             React.DOM.input({className: "contact-filter", 
                    placeholder: mozL10n.get("contacts_search_placesholder"), 
                    valueLink: this.linkState("filter")})
             : null
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -412,16 +412,18 @@ loop.contacts = (function(_, mozL10n) {
         return comp;
       }
       // If names are equal, compare against unique ids to make sure we have
       // consistent ordering.
       return contact1._guid - contact2._guid;
     },
 
     render: function() {
+      let cx = React.addons.classSet;
+
       let viewForItem = item => {
         return <ContactDetail key={item._guid} contact={item}
                               handleContactAction={this.handleContactAction} />
       };
 
       let shownContacts = _.groupBy(this.contacts, function(contact) {
         return contact.blocked ? "blocked" : "available";
       });
@@ -439,26 +441,29 @@ loop.contacts = (function(_, mozL10n) {
             shownContacts.available = shownContacts.available.filter(filterFn);
           }
           if (shownContacts.blocked) {
             shownContacts.blocked = shownContacts.blocked.filter(filterFn);
           }
         }
       }
 
-      // TODO: bug 1076767 - add a spinner whilst importing contacts.
       return (
         <div>
           <div className="content-area">
             <ButtonGroup>
               <Button caption={this.state.importBusy
                                ? mozL10n.get("importing_contacts_progress_button")
                                : mozL10n.get("import_contacts_button")}
                       disabled={this.state.importBusy}
-                      onClick={this.handleImportButtonClick} />
+                      onClick={this.handleImportButtonClick}>
+                <div className={cx({"contact-import-spinner": true,
+                                    spinner: true,
+                                    busy: this.state.importBusy})} />
+              </Button>
               <Button caption={mozL10n.get("new_contact_button")}
                       onClick={this.handleAddContactButtonClick} />
             </ButtonGroup>
             {showFilter ?
             <input className="contact-filter"
                    placeholder={mozL10n.get("contacts_search_placesholder")}
                    valueLink={this.linkState("filter")} />
             : null }
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -394,28 +394,31 @@ loop.panel = (function(_, mozL10n) {
     },
 
     render: function() {
       // XXX setting elem value from a state (in the callUrl input)
       // makes it immutable ie read only but that is fine in our case.
       // readOnly attr will suppress a warning regarding this issue
       // from the react lib.
       var cx = React.addons.classSet;
-      var inputCSSClass = cx({
-        "pending": this.state.pending,
-        // Used in functional testing, signals that
-        // call url was received from loop server
-        "callUrl": !this.state.pending
-      });
       return (
         React.DOM.div({className: "generate-url"}, 
           React.DOM.header(null, __("share_link_header_text")), 
-          React.DOM.input({type: "url", value: this.state.callUrl, readOnly: "true", 
-                 onCopy: this.handleLinkExfiltration, 
-                 className: inputCSSClass}), 
+          React.DOM.div({className: "generate-url-stack"}, 
+            React.DOM.input({type: "url", value: this.state.callUrl, readOnly: "true", 
+                   onCopy: this.handleLinkExfiltration, 
+                   className: cx({"generate-url-input": true,
+                                  pending: this.state.pending,
+                                  // Used in functional testing, signals that
+                                  // call url was received from loop server
+                                  callUrl: !this.state.pending})}), 
+            React.DOM.div({className: cx({"generate-url-spinner": true,
+                                spinner: true,
+                                busy: this.state.pending})})
+          ), 
           ButtonGroup({additionalClass: "url-actions"}, 
             Button({additionalClass: "button-email", 
                     disabled: !this.state.callUrl, 
                     onClick: this.handleEmailButtonClick, 
                     caption: mozL10n.get("share_button")}), 
             Button({additionalClass: "button-copy", 
                     disabled: !this.state.callUrl, 
                     onClick: this.handleCopyButtonClick, 
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -394,28 +394,31 @@ loop.panel = (function(_, mozL10n) {
     },
 
     render: function() {
       // XXX setting elem value from a state (in the callUrl input)
       // makes it immutable ie read only but that is fine in our case.
       // readOnly attr will suppress a warning regarding this issue
       // from the react lib.
       var cx = React.addons.classSet;
-      var inputCSSClass = cx({
-        "pending": this.state.pending,
-        // Used in functional testing, signals that
-        // call url was received from loop server
-        "callUrl": !this.state.pending
-      });
       return (
         <div className="generate-url">
           <header>{__("share_link_header_text")}</header>
-          <input type="url" value={this.state.callUrl} readOnly="true"
-                 onCopy={this.handleLinkExfiltration}
-                 className={inputCSSClass} />
+          <div className="generate-url-stack">
+            <input type="url" value={this.state.callUrl} readOnly="true"
+                   onCopy={this.handleLinkExfiltration}
+                   className={cx({"generate-url-input": true,
+                                  pending: this.state.pending,
+                                  // Used in functional testing, signals that
+                                  // call url was received from loop server
+                                  callUrl: !this.state.pending})} />
+            <div className={cx({"generate-url-spinner": true,
+                                spinner: true,
+                                busy: this.state.pending})} />
+          </div>
           <ButtonGroup additionalClass="url-actions">
             <Button additionalClass="button-email"
                     disabled={!this.state.callUrl}
                     onClick={this.handleEmailButtonClick}
                     caption={mozL10n.get("share_button")} />
             <Button additionalClass="button-copy"
                     disabled={!this.state.callUrl}
                     onClick={this.handleCopyButtonClick}
--- a/browser/components/loop/content/shared/css/contacts.css
+++ b/browser/components/loop/content/shared/css/contacts.css
@@ -1,12 +1,22 @@
 /* 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/. */
 
+.contact-import-spinner {
+  display: none;
+}
+
+.contact-import-spinner.busy {
+  display: inline-block;
+  vertical-align: middle;
+  -moz-margin-start: 10px;
+}
+
 .content-area input.contact-filter {
   margin-top: 14px;
   border-radius: 10000px;
 }
 
 .contact-list {
   border-top: 1px solid #ccc;
   overflow-x: hidden;
--- a/browser/components/loop/content/shared/css/panel.css
+++ b/browser/components/loop/content/shared/css/panel.css
@@ -41,27 +41,27 @@ body {
   border-top-left-radius: 2px;
   list-style: none;
 }
 
 .tab-view > li {
   flex: 1;
   text-align: center;
   color: #ccc;
-  border-right: 1px solid #ccc;
+  -moz-border-end: 1px solid #ccc;
   padding: 0 10px;
   height: 16px;
   cursor: pointer;
   background-repeat: no-repeat;
   background-size: 16px 16px;
   background-position: center;
 }
 
 .tab-view > li:last-child {
-  border-right-style: none;
+  -moz-border-end-style: none;
 }
 
 .tab-view > li[data-tab-name="call"],
 .tab-view > li[data-tab-name="rooms"] {
   background-image: url("../img/icons-16x16.svg#precall");
 }
 
 .tab-view > li[data-tab-name="call"]:hover,
@@ -302,16 +302,20 @@ body {
   background-color: #fbfbfb;
   color: #333;
   border: 1px solid #c1c1c1;
   border-radius: 2px;
   height: 26px;
   font-size: 12px;
 }
 
+.button > .button-caption {
+  vertical-align: middle;
+}
+
 .button:hover {
   background-color: #ebebeb;
 }
 
 .button:hover:active {
   background-color: #ccc;
   color: #111;
 }
@@ -367,34 +371,73 @@ body[dir=rtl] .dropdown-menu-item {
   white-space: nowrap;
 }
 
 .dropdown-menu-item:hover {
   border: 1px solid #ccc;
   background-color: #eee;
 }
 
+/* Spinner */
+
+@keyframes spinnerRotate {
+  to { transform: rotate(360deg); }
+}
+
+.spinner {
+  width: 16px;
+  height: 16px;
+  background-repeat: no-repeat;
+  background-size: 16px 16px;
+}
+
+.spinner.busy {
+  background-image: url(../img/spinner.png);
+  animation-name: spinnerRotate;
+  animation-duration: 1s;
+  animation-timing-function: linear;
+  animation-iteration-count: infinite;
+}
+
+@media (min-resolution: 2dppx) {
+  .spinner.busy {
+    background-image: url(../img/spinner@2x.png);
+  }
+}
+
 /* Share tab */
 
-.generate-url input {
+.generate-url-stack {
   margin: 14px 0;
+  position: relative;
+}
+
+.generate-url-input {
   outline: 0;
   border: 1px solid #ccc; /* Overriding background style for a text input (see
                              below) resets its borders to a weird beveled style;
                              defining a default 1px border solves the issue. */
   border-radius: 2px;
   height: 26px;
   padding: 0 10px;
   font-size: 1em;
 }
 
-.generate-url input.pending {
-  background-image: url(../img/loading-icon.gif);
-  background-repeat: no-repeat;
-  background-position: right;
+.generate-url-spinner {
+  position: absolute;
+  pointer-events: none;
+  z-index: 1;
+  top: 4px;
+  left: auto;
+  right: 4px;
+}
+
+body[dir=rtl] .generate-url-spinner {
+  left: 4px;
+  right: auto;
 }
 
 .generate-url .button {
   background-color: #0096dd;
   border-color: #0096dd;
   color: #fff;
 }
 
deleted file mode 100644
index 1c72ebb554be018511ae972c3f2361dff02dce02..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..11134bcccac7b69d8364104c4701caeccd99a4c1
GIT binary patch
literal 724
zc$@*$0xSKAP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0007;Nkl<Zcmajb
zO^X~=6o>KuIp^N$u9xm<9mkm@GeZ*N5OpOe;>L)ef*>wk2rfmkb0G*q7UI&KA3^*8
zq8kxhC;?G)kzq7sWs4|b#-t~msp_hG?>SbxkVZm$;73t#?x`2Tf7-e7-D5GN*CXT^
zHEfxgL$bdF=?CZd69Ct+5}kVQDwY+aB5T|3o#^rxk&V6j+#7qZ30_0zbWwZMGx2)z
zrStqQfVv0T>K9a#$}bk-BQ4jTK=FVF4<sovRm)O>GbO*gx3+Os-*T?Gxj*!H6+r7?
zK#b`!t!+Lk)p|4%>Ib85Prk7ff7WCY%{`r{e92tlQ#l}2ev;ciKL+p?o%`^o3tCJ*
zptY?EB%;myNW#e#08^|~-%We9m7fN;c9voknoYIhJ1?`bydu>kiq8T<)A(@beMXx0
zu&SzXBag>#P<x_r`HA_4?-ISul;w}RK@kZlNSI%R^S}3SFQbk8hVN!yS&UA7={gk8
zF|vHj-gT5h;MD!n!+pj2M}&naJVzRAvp5ON9E7Z6XzbyB);GuBQgfWJm=8Ur>yBZE
zOiB=vM7)dtX=iUYj2@g{p3Um*(>srR&!pV}X$s4qD1+fehqTedy_`CCz5sA$JQ>c<
zUf8^r4YEI&O~0nF=vX+qa%ANEn@m{mVbyr!;VLfbO8^`@9{`xC{zjKROSpY>_!^g!
zXIUg!TSsl?4Ptk|EWfgo<b3v!4DvIQ4d|bl{wT9t2e9vm8A$O9ZSHPzIog(Ck;HpO
zY%fCfJPU{A6WM2hM4|z(ln%jifIfQubHlRvts@7P7i5_4v3O-dpdJ%-L=*{#{WSoX
z_Lhg!zYA77KDv1tEKl{XyUQ%)atWE<HjPuL?E^T(N`C|MPg9nE`M}Kp0000<MNUMn
GLSTZO!C2V<
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..62ccb6c0712ac06da5a781e61d7c7bf807ded845
GIT binary patch
literal 1792
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
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -723,17 +723,18 @@ loop.shared.views = (function(_, OT, l10
       var classObject = { button: true, disabled: this.props.disabled };
       if (this.props.additionalClass) {
         classObject[this.props.additionalClass] = true;
       }
       return (
         React.DOM.button({onClick: this.props.onClick, 
                 disabled: this.props.disabled, 
                 className: cx(classObject)}, 
-          this.props.caption
+          React.DOM.span({className: "button-caption"}, this.props.caption), 
+          this.props.children
         )
       )
     }
   });
 
   var ButtonGroup = React.createClass({displayName: 'ButtonGroup',
     PropTypes: {
       additionalClass: React.PropTypes.string
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -723,17 +723,18 @@ loop.shared.views = (function(_, OT, l10
       var classObject = { button: true, disabled: this.props.disabled };
       if (this.props.additionalClass) {
         classObject[this.props.additionalClass] = true;
       }
       return (
         <button onClick={this.props.onClick}
                 disabled={this.props.disabled}
                 className={cx(classObject)}>
-          {this.props.caption}
+          <span className="button-caption">{this.props.caption}</span>
+          {this.props.children}
         </button>
       )
     }
   });
 
   var ButtonGroup = React.createClass({
     PropTypes: {
       additionalClass: React.PropTypes.string
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -27,17 +27,18 @@ browser.jar:
   content/browser/loop/shared/css/conversation.css  (content/shared/css/conversation.css)
   content/browser/loop/shared/css/contacts.css      (content/shared/css/contacts.css)
 
   # Shared images
   content/browser/loop/shared/img/happy.png                     (content/shared/img/happy.png)
   content/browser/loop/shared/img/sad.png                       (content/shared/img/sad.png)
   content/browser/loop/shared/img/icon_32.png                   (content/shared/img/icon_32.png)
   content/browser/loop/shared/img/icon_64.png                   (content/shared/img/icon_64.png)
-  content/browser/loop/shared/img/loading-icon.gif              (content/shared/img/loading-icon.gif)
+  content/browser/loop/shared/img/spinner.png                   (content/shared/img/spinner.png)
+  content/browser/loop/shared/img/spinner@2x.png                (content/shared/img/spinner@2x.png)
   content/browser/loop/shared/img/audio-inverse-14x14.png       (content/shared/img/audio-inverse-14x14.png)
   content/browser/loop/shared/img/audio-inverse-14x14@2x.png    (content/shared/img/audio-inverse-14x14@2x.png)
   content/browser/loop/shared/img/facemute-14x14.png            (content/shared/img/facemute-14x14.png)
   content/browser/loop/shared/img/facemute-14x14@2x.png         (content/shared/img/facemute-14x14@2x.png)
   content/browser/loop/shared/img/hangup-inverse-14x14.png      (content/shared/img/hangup-inverse-14x14.png)
   content/browser/loop/shared/img/hangup-inverse-14x14@2x.png   (content/shared/img/hangup-inverse-14x14@2x.png)
   content/browser/loop/shared/img/mute-inverse-14x14.png        (content/shared/img/mute-inverse-14x14.png)
   content/browser/loop/shared/img/mute-inverse-14x14@2x.png     (content/shared/img/mute-inverse-14x14@2x.png)