Merge "Remove event handlers from individual lines in file list"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 0698a0c..74bd9ac 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -232,95 +232,98 @@
         </label>
       </div>
     </header>
-    <template is="dom-repeat"
-        items="[[_shownFiles]]"
-        as="file"
-        initial-count="[[_fileListIncrement]]">
-      <div class="file-row row">
-        <div class="reviewed" hidden$="[[!_loggedIn]]" hidden>
-          <input type="checkbox" checked="[[file.isReviewed]]"
-              data-path$="[[file.__path]]" on-change="_handleReviewedChange"
-              class="reviewed" aria-label="Reviewed checkbox">
-        </div>
-        <div class$="[[_computeClass('status', file.__path)]]"
-            tabindex="0"
-            aria-label$="[[_computeFileStatusLabel(file.status)]]">
-          [[_computeFileStatus(file.status)]]
-        </div>
-        <a class$="[[_computePathClass(file.__path, _expandedFilePaths.*)]]"
-            href$="[[_computeDiffURL(changeNum, patchRange, file.__path)]]"
-            on-tap="_handleFileTap">
-          <div title$="[[_computeFileDisplayName(file.__path)]]"
-              class="fullFileName">
-            [[_computeFileDisplayName(file.__path)]]
-          </div>
-          <div title$="[[_computeFileDisplayName(file.__path)]]"
-              class="truncatedFileName">
-            [[_computeTruncatedFileDisplayName(file.__path)]]
-          </div>
-          <div class="oldPath" hidden$="[[!file.old_path]]" hidden
-              title$="[[file.old_path]]">
-            [[file.old_path]]
-          </div>
-        </a>
-        <div class="comments desktop">
-          <span class="drafts">
-            [[_computeDraftsString(drafts, patchRange.patchNum, file.__path)]]
-          </span>
-          [[_computeCommentsString(comments, patchRange.patchNum, file.__path)]]
-          [[_computeUnresolvedString(comments, drafts, patchRange.patchNum, file.__path)]]
-        </div>
-        <div class="comments mobile">
-          <span class="drafts">
-            [[_computeDraftsStringMobile(drafts, patchRange.patchNum,
-                file.__path)]]
-          </span>
-          [[_computeCommentsStringMobile(comments, patchRange.patchNum,
-              file.__path)]]
-        </div>
-        <div class$="[[_computeClass('stats', file.__path)]]">
-          <span
-              class="added"
-              tabindex="0"
-              aria-label$="[[file.lines_inserted]] lines added"
-              hidden$=[[file.binary]]>
-            +[[file.lines_inserted]]
-          </span>
-          <span
-              class="removed"
-              tabindex="0"
-              aria-label$="[[file.lines_deleted]] lines removed"
-              hidden$=[[file.binary]]>
-            -[[file.lines_deleted]]
-          </span>
-          <span class$="[[_computeBinaryClass(file.size_delta)]]"
-              hidden$=[[!file.binary]]>
-            [[_formatBytes(file.size_delta)]]
-            [[_formatPercentage(file.size, file.size_delta)]]
-          </span>
-        </div>
-        <div class="show-hide" hidden$="[[_userPrefs.expand_inline_diffs]]">
-          <label class="show-hide">
-            <input type="checkbox" class="show-hide"
-                checked$="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]"
+    <div on-tap="_handleFileListTap">
+      <template is="dom-repeat"
+          items="[[_shownFiles]]"
+          id="files"
+          as="file"
+          initial-count="[[_fileListIncrement]]">
+        <div class="file-row row">
+          <div class="reviewed" hidden$="[[!_loggedIn]]" hidden>
+            <input type="checkbox" checked="[[file.isReviewed]]"
                 data-path$="[[file.__path]]"
-                on-change="_handleHiddenChange">
-            [[_computeShowHideText(file.__path, _expandedFilePaths.*)]]
-          </label>
+                class="reviewed" aria-label="Reviewed checkbox">
+          </div>
+          <div class$="[[_computeClass('status', file.__path)]]"
+              tabindex="0"
+              aria-label$="[[_computeFileStatusLabel(file.status)]]">
+            [[_computeFileStatus(file.status)]]
+          </div>
+          <a class$="[[_computePathClass(file.__path, _expandedFilePaths.*)]]"
+              href$="[[_computeDiffURL(changeNum, patchRange, file.__path)]]"
+              data-path$="[[file.__path]]">
+            <div title$="[[_computeFileDisplayName(file.__path)]]"
+                class="fullFileName">
+              [[_computeFileDisplayName(file.__path)]]
+            </div>
+            <div title$="[[_computeFileDisplayName(file.__path)]]"
+                class="truncatedFileName">
+              [[_computeTruncatedFileDisplayName(file.__path)]]
+            </div>
+            <div class="oldPath" hidden$="[[!file.old_path]]" hidden
+                title$="[[file.old_path]]">
+              [[file.old_path]]
+            </div>
+          </a>
+          <div class="comments desktop">
+            <span class="drafts">
+              [[_computeDraftsString(drafts, patchRange.patchNum, file.__path)]]
+            </span>
+            [[_computeCommentsString(comments, patchRange.patchNum, file.__path)]]
+            [[_computeUnresolvedString(comments, drafts, patchRange.patchNum, file.__path)]]
+          </div>
+          <div class="comments mobile">
+            <span class="drafts">
+              [[_computeDraftsStringMobile(drafts, patchRange.patchNum,
+                  file.__path)]]
+            </span>
+            [[_computeCommentsStringMobile(comments, patchRange.patchNum,
+                file.__path)]]
+          </div>
+          <div class$="[[_computeClass('stats', file.__path)]]">
+            <span
+                class="added"
+                tabindex="0"
+                aria-label$="[[file.lines_inserted]] lines added"
+                hidden$=[[file.binary]]>
+              +[[file.lines_inserted]]
+            </span>
+            <span
+                class="removed"
+                tabindex="0"
+                aria-label$="[[file.lines_deleted]] lines removed"
+                hidden$=[[file.binary]]>
+              -[[file.lines_deleted]]
+            </span>
+            <span class$="[[_computeBinaryClass(file.size_delta)]]"
+                hidden$=[[!file.binary]]>
+              [[_formatBytes(file.size_delta)]]
+              [[_formatPercentage(file.size, file.size_delta)]]
+            </span>
+          </div>
+          <div class="show-hide" hidden$="[[_userPrefs.expand_inline_diffs]]">
+            <label class="show-hide" data-path$="[[file.__path]]"
+                data-expand=true>
+              <input type="checkbox" class="show-hide"
+                  checked$="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]"
+                  data-path$="[[file.__path]]" data-expand=true>
+              [[_computeShowHideText(file.__path, _expandedFilePaths.*)]]
+            </label>
+          </div>
         </div>
-      </div>
-      <gr-diff
-          no-auto-render
-          hidden="[[!_isFileExpanded(file.__path, _expandedFilePaths.*)]]"
-          project="[[change.project]]"
-          commit="[[change.current_revision]]"
-          change-num="[[changeNum]]"
-          patch-range="[[patchRange]]"
-          path="[[file.__path]]"
-          prefs="[[_diffPrefs]]"
-          project-config="[[projectConfig]]"
-          view-mode="[[_getDiffViewMode(diffViewMode, _userPrefs)]]"></gr-diff>
-    </template>
+        <gr-diff
+            no-auto-render
+            hidden="[[!_isFileExpanded(file.__path, _expandedFilePaths.*)]]"
+            project="[[change.project]]"
+            commit="[[change.current_revision]]"
+            change-num="[[changeNum]]"
+            patch-range="[[patchRange]]"
+            path="[[file.__path]]"
+            prefs="[[_diffPrefs]]"
+            project-config="[[projectConfig]]"
+            view-mode="[[_getDiffViewMode(diffViewMode, _userPrefs)]]"></gr-diff>
+      </template>
+    </div>
     <div class$="row totalChanges [[_computeExpandInlineClass(_userPrefs)]]">
       <div class="total-stats" hidden$="[[_hideChangeTotals]]">
         <span
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index a0189f2..54b1099 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -226,10 +226,6 @@
       return parseInt(patchNum, 10) >= parseInt(currentPatchNum, 10);
     },
 
-    _handleHiddenChange: function(e) {
-      this._togglePathExpanded(e.model.file.__path);
-    },
-
     _togglePathExpanded: function(path) {
       // Is the path in the list of expanded diffs? IF so remove it, otherwise
       // add it to the list.
@@ -400,15 +396,29 @@
           });
     },
 
-    _handleFileTap: function(e) {
+    /**
+     * Handle all events from the file list dom-repeat so event handleers don't
+     * have to get registered for potentially very long lists.
+     */
+    _handleFileListTap: function(e) {
+      // Handle checkbox mark as reviewed.
+      if (e.target.classList.contains('reviewed')) {
+        return this._handleReviewedChange(e);
+      }
+
+      // Check to see if the file should be expanded.
+      var path = e.target.dataset.path || e.target.parentElement.dataset.path;
+
       // If the user prefers to expand inline diffs rather than opening the diff
       // view, intercept the click event.
-      if (e.detail.sourceEvent.metaKey || e.detail.sourceEvent.ctrlKey) {
+      if (!path || e.detail.sourceEvent.metaKey ||
+          e.detail.sourceEvent.ctrlKey) {
           return;
       }
-      if (this._userPrefs && this._userPrefs.expand_inline_diffs) {
+      if (e.target.dataset.expand ||
+          this._userPrefs && this._userPrefs.expand_inline_diffs) {
         e.preventDefault();
-        this._handleHiddenChange(e);
+        this._togglePathExpanded(path);
       }
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index bd4619f..a582cbb 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -637,10 +637,13 @@
       flushAsynchronousOperations();
       var fileRows =
           Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
+      // Because the label surrounds the input, the tap event is triggered
+      // there first.
+      var showHideLabel = fileRows[0].querySelector('label.show-hide');
       var showHideCheck = fileRows[0].querySelector(
           'input.show-hide[type="checkbox"]');
       assert.isNotOk(showHideCheck.checked);
-      MockInteractions.tap(showHideCheck);
+      MockInteractions.tap(showHideLabel);
       assert.isOk(showHideCheck.checked);
       assert.notEqual(element._expandedFilePaths.indexOf('myfile.txt'), -1);
     });
@@ -763,18 +766,18 @@
 
       // Remove href attribute so the app doesn't route to a diff view
       commitMsgFile.removeAttribute('href');
-      var hiddenChangeSpy = sandbox.spy(element, '_handleHiddenChange');
+      var togglePathSpy = sandbox.spy(element, '_togglePathExpanded');
 
       MockInteractions.tap(commitMsgFile);
       flushAsynchronousOperations();
-      assert(hiddenChangeSpy.notCalled, 'file is opened as diff view');
+      assert(togglePathSpy.notCalled, 'file is opened as diff view');
       assert.isNotOk(element.$$('.expanded'));
 
       element._userPrefs = {expand_inline_diffs: true};
       flushAsynchronousOperations();
       MockInteractions.tap(commitMsgFile);
       flushAsynchronousOperations();
-      assert(hiddenChangeSpy.calledOnce, 'file is expanded');
+      assert(togglePathSpy.calledOnce, 'file is expanded');
       assert.isOk(element.$$('.expanded'));
     });
 
@@ -812,32 +815,37 @@
       element.push('_expandedFilePaths', path);
     });
 
-    suite('_handleFileTap', function() {
+    suite('_handleFileListTap', function() {
       function testForModifier(modifier) {
         var e = {preventDefault: function() {}};
         e.detail = {sourceEvent: {}};
+        e.target = {
+          dataset: {path: '/test'},
+          classList: element.classList,
+        };
+
         e.detail.sourceEvent[modifier] = true;
 
-        var hiddenChangeStub = sandbox.stub(element, '_handleHiddenChange');
+        var togglePathStub = sandbox.stub(element, '_togglePathExpanded');
         element._userPrefs = { expand_inline_diffs: true };
 
-        element._handleFileTap(e);
-        assert.isFalse(hiddenChangeStub.called);
+        element._handleFileListTap(e);
+        assert.isFalse(togglePathStub.called);
 
         e.detail.sourceEvent[modifier] = false;
-        element._handleFileTap(e);
-        assert.equal(hiddenChangeStub.callCount, 1);
+        element._handleFileListTap(e);
+        assert.equal(togglePathStub.callCount, 1);
 
         element._userPrefs = { expand_inline_diffs: false };
-        element._handleFileTap(e);
-        assert.equal(hiddenChangeStub.callCount, 1);
+        element._handleFileListTap(e);
+        assert.equal(togglePathStub.callCount, 1);
       }
 
-      test('_handleFileTap meta', function() {
+      test('_handleFileListTap meta', function() {
         testForModifier('metaKey');
       });
 
-      test('_handleFileTap ctrl', function() {
+      test('_handleFileListTap ctrl', function() {
         testForModifier('ctrlKey');
       });
     });