Merge changes I19a0c153,I1037c43d

* changes:
  Fix filter so that it works with shadow DOM
  Consolidate list-view code
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
index 730ff74..ea0136f 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
@@ -18,97 +18,38 @@
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../../styles/shared-styles.html">
 
-<link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-admin-group-list">
   <template>
-    <style include="shared-styles">
-      :host {
-        display: flex;
-        flex-direction: column;
-      }
-      tr.group-table {
-        border-bottom: 1px solid #eee;
-      }
-      #groupList {
-        border-collapse: collapse;
-        width: 100%;
-      }
-      #filterContainer {
-        margin: 1em;
-      }
-      #groupFilter {
-        font-size: 1em;
-        max-width: 25em;
-      }
-      td {
-        flex-shrink: 0;
-        padding: .3em .5em;
-      }
-      th {
-        background-color: #ddd;
-        border-bottom: 1px solid #eee;
-        font-weight: bold;
-        padding: .3em .5em;
-        text-align: left;
-      }
-      a {
-        color: var(--default-text-color);
-        text-decoration: none;
-      }
-      a:hover {
-        text-decoration: underline;
-      }
-      nav {
-        padding: .5em 0;
-        text-align: center;
-      }
-      nav a {
-        display: inline-block;
-      }
-      nav a:first-of-type {
-        margin-right: .5em;
-      }
-      .description {
-        width: 70%;
-      }
-    </style>
-    <div id="filterContainer">
-      <label>Filter:</label>
-      <input is="iron-input"
-          type="text"
-          id="groupFilter"
-          bind-value="{{_filter}}"
-          on-input="_onValueChange">
-    </div>
-    <table id="groupList">
-      <tr class="headerRow">
-        <th class="name topHeader">Group Name</th>
-        <th class="description topHeader">Group Description</th>
-        <th class="visibleToAll topHeader">Visible To All</th>
-      </tr>
-      <template is="dom-repeat" items="[[_shownGroups]]">
-        <tr class="group-table">
-          <td class="name">
-            <a href$="[[_getUrl(item.group_id)]]">[[item.name]]</a>
-          </td>
-          <td class="description">[[item.description]]</td>
-          <td class="visibleToAll">[[_visibleToAll(item)]]</td>
+    <style include="shared-styles"></style>
+    <gr-list-view
+        filter="[[_filter]]"
+        items="[[_groups]]"
+        items-per-page="[[_groupsPerPage]]"
+        loading="[[_loading]]"
+        offset="[[_offset]]"
+        path="/admin/groups">
+      <table id="list">
+        <tr class="headerRow">
+          <th class="name topHeader">Group Name</th>
+          <th class="description topHeader">Group Description</th>
+          <th class="visibleToAll topHeader">Visible To All</th>
         </tr>
-      </template>
-    </table>
-    <nav>
-      <a id="prevArrow"
-          href$="[[_computeNavLink(_offset, -1, _groupsPerPage)]]"
-          hidden$="[[_hidePrevArrow(_offset)]]" hidden>&larr; Prev</a>
-      <a id="nextArrow"
-          href$="[[_computeNavLink(_offset, 1, _groupsPerPage)]]"
-          hidden$="[[_hideNextArrow(_loading, _groups)]]" hidden>
-        Next &rarr;</a>
-    </nav>
+        <template is="dom-repeat" items="[[_shownGroups]]">
+          <tr class="table">
+            <td class="name">
+              <a href$="[[_getUrl(item.group_id)]]">[[item.name]]</a>
+            </td>
+            <td class="description">[[item.description]]</td>
+            <td class="visibleToAll">[[_visibleToAll(item)]]</td>
+          </tr>
+        </template>
+      </table>
+    </gr-list-view>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-admin-group-list.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
index 2586b0c..295aae2 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
@@ -14,8 +14,6 @@
 (function() {
   'use strict';
 
-  const REQUEST_DEBOUNCE_INTERVAL_MS = 200;
-
   Polymer({
     is: 'gr-admin-group-list',
 
@@ -66,16 +64,6 @@
       'previous-page': '_handlePreviousPage',
     },
 
-    _onValueChange(e) {
-      this.debounce('reload', () => {
-        if (e.target.value) {
-          return page.show('/admin/groups/q/filter:' +
-              this.encodeURL(e.target.value, false));
-        }
-        page.show('/admin/groups');
-      }, REQUEST_DEBOUNCE_INTERVAL_MS);
-    },
-
     _paramsChanged(value) {
       this._loading = true;
 
@@ -118,34 +106,8 @@
           this.encodeURL(item, true);
     },
 
-    _computeNavLink(offset, direction, groupsPerPage, filter) {
-      // Offset could be a string when passed from the router.
-      offset = +(offset || 0);
-      const newOffset = Math.max(0, offset + (groupsPerPage * direction));
-      let href = this.getBaseUrl() + '/admin/groups';
-      if (filter) {
-        href += '/q/filter:' + filter;
-      }
-      if (newOffset > 0) {
-        href += ',' + newOffset;
-      }
-      return href;
-    },
-
     _computeShownGroups(groups) {
       return groups.slice(0, 25);
     },
-
-    _hidePrevArrow(offset) {
-      return offset === 0;
-    },
-
-    _hideNextArrow(loading, groups) {
-      let lastPage = false;
-      if (groups.length < this._groupsPerPage + 1) {
-        lastPage = true;
-      }
-      return loading || lastPage || !groups || !groups.length;
-    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
index 5bb6906..8b969f7 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
@@ -87,31 +87,6 @@
         });
       });
 
-      test('test next button', done => {
-        flush(() => {
-          let loading;
-          assert.isFalse(element._hideNextArrow(loading, groups));
-          loading = true;
-          assert.isTrue(element._hideNextArrow(loading, groups));
-          loading = false;
-          assert.isFalse(element._hideNextArrow(loading, groups));
-          element._groups = [];
-          assert.isTrue(element._hideNextArrow(loading, element._groups));
-          groups = _.times(4, groupGenerator);
-          assert.isTrue(element._hideNextArrow(loading, groups));
-          done();
-        });
-      });
-
-      test('test for prev button', () => {
-        flush(() => {
-          let offset = 0;
-          assert.isTrue(element._hidePrevArrow(offset));
-          offset = 5;
-          assert.isFalse(element._hidePrevArrow(offset));
-        });
-      });
-
       test('_shownGroups', () => {
         assert.equal(element._shownGroups.length, 25);
       });
@@ -130,33 +105,12 @@
         element._paramsChanged(value).then(() => { flush(done); });
       });
 
-      test('test next button', done => {
-        flush(() => {
-          let loading;
-          assert.isTrue(element._hideNextArrow(loading, groups));
-          groups = _.times(1, groupGenerator);
-          assert.isTrue(element._hideNextArrow(loading, groups));
-          groups = _.times(26, groupGenerator);
-          assert.isFalse(element._hideNextArrow(loading, groups));
-          done();
-        });
-      });
-
       test('_shownGroups', () => {
         assert.equal(element._shownGroups.length, 25);
       });
     });
 
     suite('filter', () => {
-      test('_onValueChange', done => {
-        sandbox.stub(page, 'show', url => {
-          assert.equal(url, '/admin/groups/q/filter:test');
-          done();
-        });
-        const e = {target: {value: 'test'}};
-        element._onValueChange(e);
-      });
-
       test('_paramsChanged', done => {
         sandbox.stub(element.$.restAPI, 'getGroups', () => {
           return Promise.resolve(groups);
@@ -171,30 +125,6 @@
           done();
         });
       });
-
-      test('_computeNavLink', () => {
-        const offset = 25;
-        const groupsPerPage = 25;
-        const filter = 'test';
-
-        sandbox.stub(element, 'getBaseUrl', () => '');
-
-        assert.equal(
-            element._computeNavLink(offset, 1, groupsPerPage, filter),
-            '/admin/groups/q/filter:test,50');
-
-        assert.equal(
-            element._computeNavLink(offset, -1, groupsPerPage, filter),
-            '/admin/groups/q/filter:test');
-
-        assert.equal(
-            element._computeNavLink(offset, 1, groupsPerPage, null),
-            '/admin/groups,50');
-
-        assert.equal(
-            element._computeNavLink(offset, -1, groupsPerPage, null),
-            '/admin/groups');
-      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
index 1adc739..0d3fe90 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
@@ -18,112 +18,49 @@
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
-<link rel="import" href="../../../styles/gr-form-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 
 <dom-module id="gr-admin-project-list">
   <template>
-    <style include="shared-styles">
-      :host {
-        display: flex;
-        flex-direction: column;
-      }
-      tr.project-table {
-        border-bottom: 1px solid #eee;
-      }
-      #projectList {
-        border-collapse: collapse;
-        width: 100%;
-      }
-      #filterContainer {
-        margin: 1em;
-      }
-      #projectFilter {
-        font-size: 1em;
-        max-width: 25em;
-      }
-      td {
-        flex-shrink: 0;
-        padding: .3em .5em;
-      }
-      th {
-        background-color: #ddd;
-        border-bottom: 1px solid #eee;
-        font-weight: bold;
-        padding: .3em .5em;
-        text-align: left;
-      }
-      a {
-        color: var(--default-text-color);
-        text-decoration: none;
-      }
-      a:hover {
-        text-decoration: underline;
-      }
-      nav {
-        padding: .5em 0;
-        text-align: center;
-      }
-      nav a {
-        display: inline-block;
-      }
-      nav a:first-of-type {
-        margin-right: .5em;
-      }
-      .description {
-        width: 70%;
-      }
-      .loading {
-        color: #666;
-        padding: 1em var(--default-horizontal-margin);
-      }
-    </style>
-    <div id="filterContainer">
-      <label>Filter:</label>
-      <input is="iron-input"
-          type="text"
-          id="projectFilter"
-          bind-value="{{_filter}}"
-          on-input="_onValueChange">
-    </div>
-    <table id="projectList">
-      <tr class="headerRow">
-        <th class="name topHeader">Project Name</th>
-        <th class="description topHeader">Project Description</th>
-        <th class="repositoryBrowser topHeader">Repository Browser</th>
-        <th class="readOnly topHeader">Read only</th>
-      </tr>
-      <tr class="loading" hidden$="[[!_loading]]" hidden><td>Loading...</td></tr>
-      <template is="dom-repeat" items="[[_shownProjects]]" hidden$="[[_loading]]" hidden>
-        <tr class="project-table">
-          <td class="name">
-            <a href$="[[_getUrl(item.name)]]">[[item.name]]</a>
-          </td>
-          <td class="description">[[item.description]]</td>
-          <td class="repositoryBrowser">
-            <template is="dom-repeat"
-                items="[[_computeWeblink(item)]]" as="link">
-              <a href$="[[link.url]]" class="webLink" rel="noopener" target="_blank">
-                ([[link.name]])
-              </a>
-            </template>
-          </td>
-          <td class="readOnly">[[_readOnly(item)]]</td>
+    <style include="shared-styles"></style>
+    <gr-list-view
+        filter="[[_filter]]"
+        items-per-page="[[_projectsPerPage]]"
+        items="[[_projects]]"
+        loading="[[_loading]]"
+        offset="[[_offset]]"
+        path="/admin/projects">
+      <table id="list">
+        <tr class="headerRow">
+          <th class="name topHeader">Project Name</th>
+          <th class="description topHeader">Project Description</th>
+          <th class="repositoryBrowser topHeader">Repository Browser</th>
+          <th class="readOnly topHeader">Read only</th>
         </tr>
-      </template>
-    </table>
-    <nav>
-      <a id="prevArrow"
-          href$="[[_computeNavLink(_offset, -1, _projectsPerPage, _filter)]]"
-          hidden$="[[_hidePrevArrow(_offset)]]" hidden>&larr; Prev</a>
-      <a id="nextArrow"
-          href$="[[_computeNavLink(_offset, 1, _projectsPerPage, _filter)]]"
-          hidden$="[[_hideNextArrow(_loading, _projects)]]" hidden>
-        Next &rarr;</a>
-    </nav>
+        <tr class="loading" hidden$="[[!_loading]]" hidden><td>Loading...</td></tr>
+        <template is="dom-repeat" items="[[_shownProjects]]" hidden$="[[_loading]]" hidden>
+          <tr class="table">
+            <td class="name">
+              <a href$="[[_getUrl(item.name)]]">[[item.name]]</a>
+            </td>
+            <td class="description">[[item.description]]</td>
+            <td class="repositoryBrowser">
+              <template is="dom-repeat"
+                  items="[[_computeWeblink(item)]]" as="link">
+                <a href$="[[link.url]]" class="webLink" rel="noopener" target="_blank">
+                  ([[link.name]])
+                </a>
+              </template>
+            </td>
+            <td class="readOnly">[[_readOnly(item)]]</td>
+          </tr>
+        </template>
+      </table>
+    </gr-list-view>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-admin-project-list.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
index 9aacccf..e7671d5 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
@@ -14,8 +14,6 @@
 (function() {
   'use strict';
 
-  const REQUEST_DEBOUNCE_INTERVAL_MS = 200;
-
   Polymer({
     is: 'gr-admin-project-list',
 
@@ -61,21 +59,6 @@
       Gerrit.URLEncodingBehavior,
     ],
 
-    listeners: {
-      'next-page': '_handleNextPage',
-      'previous-page': '_handlePreviousPage',
-    },
-
-    _onValueChange(e) {
-      this.debounce('reload', () => {
-        if (e.target.value) {
-          return page.show('/admin/projects/q/filter:' +
-              this.encodeURL(e.target.value, false));
-        }
-        page.show('/admin/projects');
-      }, REQUEST_DEBOUNCE_INTERVAL_MS);
-    },
-
     _paramsChanged(value) {
       this._loading = true;
 
@@ -119,7 +102,6 @@
           this.encodeURL(item, true);
     },
 
-
     _computeWeblink(project) {
       if (!project.web_links) {
         return '';
@@ -128,34 +110,8 @@
       return webLinks.length ? webLinks : null;
     },
 
-    _computeNavLink(offset, direction, projectsPerPage, filter) {
-      // Offset could be a string when passed from the router.
-      offset = +(offset || 0);
-      const newOffset = Math.max(0, offset + (projectsPerPage * direction));
-      let href = this.getBaseUrl() + '/admin/projects';
-      if (filter) {
-        href += '/q/filter:' + filter;
-      }
-      if (newOffset > 0) {
-        href += ',' + newOffset;
-      }
-      return href;
-    },
-
     _computeShownProjects(projects) {
       return projects.slice(0, 25);
     },
-
-    _hidePrevArrow(offset) {
-      return offset === 0;
-    },
-
-    _hideNextArrow(loading, projects) {
-      let lastPage = false;
-      if (projects.length < this._projectsPerPage + 1) {
-        lastPage = true;
-      }
-      return loading || lastPage || !projects || !projects.length;
-    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
index 4267055..ff8571b 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
@@ -82,31 +82,6 @@
         });
       });
 
-      test('test next button', done => {
-        flush(() => {
-          let loading;
-          assert.isFalse(element._hideNextArrow(loading, projects));
-          loading = true;
-          assert.isTrue(element._hideNextArrow(loading, projects));
-          loading = false;
-          assert.isFalse(element._hideNextArrow(loading, projects));
-          element._projects = [];
-          assert.isTrue(element._hideNextArrow(loading, element._projects));
-          projects = _.times(4, projectGenerator);
-          assert.isTrue(element._hideNextArrow(loading, projects));
-          done();
-        });
-      });
-
-      test('test for prev button', () => {
-        flush(() => {
-          let offset = 0;
-          assert.isTrue(element._hidePrevArrow(offset));
-          offset = 5;
-          assert.isFalse(element._hidePrevArrow(offset));
-        });
-      });
-
       test('_shownProjects', () => {
         assert.equal(element._shownProjects.length, 25);
       });
@@ -125,33 +100,12 @@
         element._paramsChanged(value).then(() => { flush(done); });
       });
 
-      test('test next button', done => {
-        flush(() => {
-          let loading;
-          assert.isTrue(element._hideNextArrow(loading, projects));
-          projects = _.times(1, projectGenerator);
-          assert.isTrue(element._hideNextArrow(loading, projects));
-          projects = _.times(26, projectGenerator);
-          assert.isFalse(element._hideNextArrow(loading, projects));
-          done();
-        });
-      });
-
       test('_shownProjects', () => {
         assert.equal(element._shownProjects.length, 25);
       });
     });
 
     suite('filter', () => {
-      test('_onValueChange', done => {
-        sandbox.stub(page, 'show', url => {
-          assert.equal(url, '/admin/projects/q/filter:test');
-          done();
-        });
-        const e = {target: {value: 'test'}};
-        element._onValueChange(e);
-      });
-
       test('_paramsChanged', done => {
         sandbox.stub(element.$.restAPI, 'getProjects', () => {
           return Promise.resolve(projects);
@@ -166,30 +120,6 @@
           done();
         });
       });
-
-      test('_computeNavLink', () => {
-        const offset = 25;
-        const projectsPerPage = 25;
-        const filter = 'test';
-
-        sandbox.stub(element, 'getBaseUrl', () => '');
-
-        assert.equal(
-            element._computeNavLink(offset, 1, projectsPerPage, filter),
-            '/admin/projects/q/filter:test,50');
-
-        assert.equal(
-            element._computeNavLink(offset, -1, projectsPerPage, filter),
-            '/admin/projects/q/filter:test');
-
-        assert.equal(
-            element._computeNavLink(offset, 1, projectsPerPage, null),
-            '/admin/projects,50');
-
-        assert.equal(
-            element._computeNavLink(offset, -1, projectsPerPage, null),
-            '/admin/projects');
-      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
new file mode 100644
index 0000000..0e68899
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
@@ -0,0 +1,106 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<link rel="import" href="../../../styles/shared-styles.html">
+
+<dom-module id="gr-list-view">
+  <template>
+    <style include="shared-styles">
+      #filterContainer {
+        margin: 1em;
+      }
+      #filter {
+        font-size: 1em;
+        max-width: 25em;
+      }
+      a {
+        color: var(--default-text-color);
+        text-decoration: none;
+      }
+      a:hover {
+        text-decoration: underline;
+      }
+      nav {
+        padding: .5em 0;
+        text-align: center;
+      }
+      nav a {
+        display: inline-block;
+      }
+      nav a:first-of-type {
+        margin-right: .5em;
+      }
+      ::content {
+        display: flex;
+        flex-direction: column;
+      }
+      ::content tr.table {
+        border-bottom: 1px solid #eee;
+      }
+      ::content #list {
+        border-collapse: collapse;
+        width: 100%;
+      }
+      ::content td {
+        flex-shrink: 0;
+        padding: .3em .5em;
+      }
+      ::content th {
+        background-color: #ddd;
+        border-bottom: 1px solid #eee;
+        font-weight: bold;
+        padding: .3em .5em;
+        text-align: left;
+      }
+      ::content a {
+        color: var(--default-text-color);
+        text-decoration: none;
+      }
+      ::content a:hover {
+        text-decoration: underline;
+      }
+      ::content .description {
+        width: 70%;
+      }
+      ::content .loading {
+        color: #666;
+        padding: 1em var(--default-horizontal-margin);
+      }
+    </style>
+    <div id="filterContainer">
+      <label>Filter:</label>
+      <input is="iron-input"
+          type="text"
+          id="filter"
+          bind-value="{{_filter}}">
+    </div>
+    <content></content>
+    <nav>
+      <a id="prevArrow"
+          href$="[[_computeNavLink(offset, -1, itemsPerPage, filter)]]"
+          hidden$="[[_hidePrevArrow(offset)]]" hidden>&larr; Prev</a>
+      <a id="nextArrow"
+          href$="[[_computeNavLink(offset, 1, itemsPerPage, filter)]]"
+          hidden$="[[_hideNextArrow(loading, items)]]" hidden>
+        Next &rarr;</a>
+    </nav>
+  </template>
+  <script src="gr-list-view.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
new file mode 100644
index 0000000..1188534
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
@@ -0,0 +1,80 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  const REQUEST_DEBOUNCE_INTERVAL_MS = 200;
+
+  Polymer({
+    is: 'gr-list-view',
+
+    properties: {
+      items: Array,
+      itemsPerPage: Number,
+      _filter: {
+        type: String,
+        observer: '_filterChanged',
+      },
+      offset: Number,
+      loading: Boolean,
+      path: String,
+    },
+
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+      Gerrit.URLEncodingBehavior,
+    ],
+
+    listeners: {
+      'next-page': '_handleNextPage',
+      'previous-page': '_handlePreviousPage',
+    },
+
+    _filterChanged(filter) {
+      this.debounce('reload', () => {
+        if (filter) {
+          return page.show(`${this.path}/q/filter:` +
+              this.encodeURL(filter, false));
+        }
+        page.show(this.path);
+      }, REQUEST_DEBOUNCE_INTERVAL_MS);
+    },
+
+    _computeNavLink(offset, direction, projectsPerPage, filter) {
+      // Offset could be a string when passed from the router.
+      offset = +(offset || 0);
+      const newOffset = Math.max(0, offset + (projectsPerPage * direction));
+      let href = this.getBaseUrl() + this.path;
+      if (filter) {
+        href += '/q/filter:' + filter;
+      }
+      if (newOffset > 0) {
+        href += ',' + newOffset;
+      }
+      return href;
+    },
+
+    _hidePrevArrow(offset) {
+      return offset === 0;
+    },
+
+    _hideNextArrow(loading, projects) {
+      let lastPage = false;
+      if (projects.length < this.itemsPerPage + 1) {
+        lastPage = true;
+      }
+      return loading || lastPage || !projects || !projects.length;
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
new file mode 100644
index 0000000..10ef4e1
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-list-view</title>
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-list-view.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-list-view></gr-list-view>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-list-view tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('_computeNavLink', () => {
+      const offset = 25;
+      const projectsPerPage = 25;
+      const filter = 'test';
+      element.path = '/admin/projects';
+
+      sandbox.stub(element, 'getBaseUrl', () => '');
+
+      assert.equal(
+          element._computeNavLink(offset, 1, projectsPerPage, filter),
+          '/admin/projects/q/filter:test,50');
+
+      assert.equal(
+          element._computeNavLink(offset, -1, projectsPerPage, filter),
+          '/admin/projects/q/filter:test');
+
+      assert.equal(
+          element._computeNavLink(offset, 1, projectsPerPage, null),
+          '/admin/projects,50');
+
+      assert.equal(
+          element._computeNavLink(offset, -1, projectsPerPage, null),
+          '/admin/projects');
+    });
+
+    test('_onValueChange', done => {
+      element.path = '/admin/projects';
+      sandbox.stub(page, 'show', url => {
+        assert.equal(url, '/admin/projects/q/filter:test');
+        done();
+      });
+      const e = {target: {value: 'test'}};
+      element._onValueChange(e);
+    });
+
+    test('next button', done => {
+      element.itemsPerPage = 25;
+      projects = new Array(26);
+
+      flush(() => {
+        let loading;
+        assert.isFalse(element._hideNextArrow(loading, projects));
+        loading = true;
+        assert.isTrue(element._hideNextArrow(loading, projects));
+        loading = false;
+        assert.isFalse(element._hideNextArrow(loading, projects));
+        element._projects = [];
+        assert.isTrue(element._hideNextArrow(loading, element._projects));
+        projects = new Array(4);
+        assert.isTrue(element._hideNextArrow(loading, projects));
+        done();
+      });
+    });
+
+    test('prev button', () => {
+      flush(() => {
+        let offset = 0;
+        assert.isTrue(element._hidePrevArrow(offset));
+        offset = 5;
+        assert.isFalse(element._hidePrevArrow(offset));
+      });
+    });
+  });
+</script>