From 44329b5a6db4a4622b9dc22688aac16471556fd7 Mon Sep 17 00:00:00 2001
From: "beeps (Kim Grey)" <kimberly.grey@digital.cabinet-office.gov.uk>
Date: Wed, 17 May 2023 14:05:40 +0100
Subject: [PATCH 1/2] Spike modifications to pagination component

---
 .../govuk/components/pagination/_index.scss   |  29 ++-
 .../components/pagination/pagination.yaml     | 204 ++++++++++++------
 .../govuk/components/pagination/template.njk  |  65 ++++--
 3 files changed, 216 insertions(+), 82 deletions(-)

diff --git a/packages/govuk-frontend/src/govuk/components/pagination/_index.scss b/packages/govuk-frontend/src/govuk/components/pagination/_index.scss
index 439e46cf3d..99a7590019 100644
--- a/packages/govuk-frontend/src/govuk/components/pagination/_index.scss
+++ b/packages/govuk-frontend/src/govuk/components/pagination/_index.scss
@@ -37,8 +37,9 @@
   }
 
   .govuk-pagination__item {
-    // Hide items on small screens except the prev/next items,
-    // non-link items and the first and last items
+    // Hide everything initially, we then unhide the stuff we actually want
+    // visible below. It's ridiculous but it works and avoids overly-specific
+    // selectors that mess with the cascade.
     display: none;
 
     // Center align pagination links in their parent list item so that they
@@ -50,6 +51,19 @@
     }
   }
 
+  .govuk-pagination__item:first-child,
+  .govuk-pagination__item:last-child,
+  .govuk-pagination__item--current,
+  .govuk-pagination__item--ellipses {
+    display: block;
+  }
+
+  // If there are exactly three items, do not hide the middle item.
+  // Avoids an awkward and pointless '1 | ... | 3' situation.
+  .govuk-pagination__item:nth-child(2):nth-last-child(2) {
+    display: block;
+  }
+
   .govuk-pagination__prev,
   .govuk-pagination__next {
     @include govuk-typography-weight-bold;
@@ -70,12 +84,11 @@
     padding-right: 0;
   }
 
-  // Only show first, last and non-link items on mobile
-  .govuk-pagination__item--current,
-  .govuk-pagination__item--ellipses,
-  .govuk-pagination__item:first-child,
-  .govuk-pagination__item:last-child {
-    display: block;
+  // Show 'collapsed only' items only on mobile
+  .govuk-pagination__item--collapsed-only {
+    @include govuk-media-query($from: tablet) {
+      display: none;
+    }
   }
 
   .govuk-pagination__item--current {
diff --git a/packages/govuk-frontend/src/govuk/components/pagination/pagination.yaml b/packages/govuk-frontend/src/govuk/components/pagination/pagination.yaml
index b8b93d691a..4d300217ae 100644
--- a/packages/govuk-frontend/src/govuk/components/pagination/pagination.yaml
+++ b/packages/govuk-frontend/src/govuk/components/pagination/pagination.yaml
@@ -16,14 +16,6 @@ params:
         type: string
         required: true
         description: The link's URL.
-      - name: current
-        type: boolean
-        required: false
-        description: Set to `true` to indicate the current page the user is on.
-      - name: ellipsis
-        type: boolean
-        required: false
-        description: Use this option if you want to specify an ellipsis at a given point between numbers. If you set this option as `true`, any other options for the item are ignored.
       - name: attributes
         type: object
         required: false
@@ -70,6 +62,14 @@ params:
         type: object
         required: false
         description: The HTML attributes (for example, data attributes) you want to add to the anchor.
+  - name: currentPage
+    type: integer
+    required: true
+    description: The index of the current page within the set of `items`. This is a 1-based index, so that it matches the visible number that the component uses by default.
+  - name: neighbouringPages
+    type: integer
+    required: false
+    description: How many pages neighbouring the current page should be shown to the user. For example, when set to 2, links for up to 2 pages before and 2 pages after the current page will be visible, for a total of 5 pages. Defaults to 2.
   - name: landmarkLabel
     type: string
     required: false
@@ -90,14 +90,11 @@ examples:
         href: '/previous'
       next:
         href: '/next'
+      currentPage: 2
       items:
-        - number: 1
-          href: '/page/1'
-        - number: 2
-          href: '/page/2'
-          current: true
-        - number: 3
-          href: '/page/3'
+        - href: '/page/1'
+        - href: '/page/2'
+        - href: '/page/3'
   - name: with custom navigation landmark
     data:
       previous:
@@ -105,14 +102,11 @@ examples:
       next:
         href: '/next'
       landmarkLabel: 'search'
+      currentPage: 2
       items:
-        - number: 1
-          href: '/page/1'
-        - number: 2
-          href: '/page/2'
-          current: true
-        - number: 3
-          href: '/page/3'
+        - href: '/page/1'
+        - href: '/page/2'
+        - href: '/page/3'
   - name: with custom link and item text
     data:
       previous:
@@ -121,12 +115,12 @@ examples:
       next:
         href: '/next'
         text: 'Next page'
+      currentPage: 2
       items:
         - number: 'one'
           href: '/page/1'
         - number: 'two'
           href: '/page/2'
-          current: true
         - number: 'three'
           href: '/page/3'
   - name: with custom accessible labels on item links
@@ -135,16 +129,13 @@ examples:
         href: '/previous'
       next:
         href: '/next'
+      currentPage: 2
       items:
-        - number: 1
-          href: '/page/1'
+        - href: '/page/1'
           visuallyHiddenText: '1st page'
-        - number: 2
-          href: '/page/2'
-          current: true
+        - href: '/page/2'
           visuallyHiddenText: '2nd page (you are currently on this page)'
-        - number: 3
-          href: '/page/3'
+        - href: '/page/3'
           visuallyHiddenText: '3rd page'
   - name: with many pages
     data:
@@ -152,48 +143,139 @@ examples:
         href: '/previous'
       next:
         href: '/next'
+      currentPage: 10
       items:
-        - number: 1
-          href: '/page/1'
-        - ellipsis: true
-        - number: 8
-          href: '/page/8'
-        - number: 9
-          href: '/page/9'
-        - number: 10
-          href: '/page/10'
-          current: true
-        - number: 11
-          href: '/page/11'
-        - number: 12
-          href: '/page/12'
-        - ellipsis: true
-        - number: 40
-          href: '/page/40'
+        - href: '/page/1'
+        - href: '/page/2'
+        - href: '/page/3'
+        - href: '/page/4'
+        - href: '/page/5'
+        - href: '/page/6'
+        - href: '/page/7'
+        - href: '/page/8'
+        - href: '/page/9'
+        - href: '/page/10'
+        - href: '/page/11'
+        - href: '/page/12'
+        - href: '/page/13'
+        - href: '/page/14'
+        - href: '/page/15'
+        - href: '/page/16'
+        - href: '/page/17'
+        - href: '/page/18'
+        - href: '/page/19'
+        - href: '/page/20'
   - name: first page
     data:
       next:
         href: '/next'
+      currentPage: 1
       items:
-        - number: 1
-          href: '/page/1'
-          current: true
-        - number: 2
-          href: '/page/2'
-        - number: 3
-          href: '/page/3'
+        - href: '/page/1'
+        - href: '/page/2'
+        - href: '/page/3'
   - name: last page
     data:
       previous:
         href: '/previous'
+      currentPage: 3
       items:
-        - number: 1
-          href: '/page/1'
-        - number: 2
-          href: '/page/2'
-        - number: 3
-          href: '/page/3'
-          current: true
+        - href: '/page/1'
+        - href: '/page/2'
+        - href: '/page/3'
+  - name: bordering first page
+    data:
+      previous:
+        href: '/previous'
+      next:
+        href: '/next'
+      currentPage: 4
+      items:
+        - href: '/page/1'
+        - href: '/page/2'
+        - href: '/page/3'
+        - href: '/page/4'
+        - href: '/page/5'
+        - href: '/page/6'
+        - href: '/page/7'
+        - href: '/page/8'
+        - href: '/page/9'
+        - href: '/page/10'
+  - name: bordering last page
+    data:
+      previous:
+        href: '/previous'
+      next:
+        href: '/next'
+      currentPage: 7
+      items:
+        - href: '/page/1'
+        - href: '/page/2'
+        - href: '/page/3'
+        - href: '/page/4'
+        - href: '/page/5'
+        - href: '/page/6'
+        - href: '/page/7'
+        - href: '/page/8'
+        - href: '/page/9'
+        - href: '/page/10'
+  - name: with fewer neighbours
+    data:
+      previous:
+        href: '/previous'
+      next:
+        href: '/next'
+      currentPage: 4
+      neighbouringPages: 1
+      items:
+        - href: '/page/1'
+        - href: '/page/2'
+        - href: '/page/3'
+        - href: '/page/4'
+        - href: '/page/5'
+        - href: '/page/6'
+        - href: '/page/7'
+        - href: '/page/8'
+        - href: '/page/9'
+        - href: '/page/10'
+  - name: with more neighbours
+    data:
+      previous:
+        href: '/previous'
+      next:
+        href: '/next'
+      currentPage: 7
+      neighbouringPages: 4
+      items:
+        - href: '/page/1'
+        - href: '/page/2'
+        - href: '/page/3'
+        - href: '/page/4'
+        - href: '/page/5'
+        - href: '/page/6'
+        - href: '/page/7'
+        - href: '/page/8'
+        - href: '/page/9'
+        - href: '/page/10'
+  - name: with no neighbours
+    data:
+      previous:
+        href: '/previous'
+      next:
+        href: '/next'
+      currentPage: 7
+      neighbouringPages: 0
+      items:
+        - href: '/page/1'
+        - href: '/page/2'
+        - href: '/page/3'
+        - href: '/page/4'
+        - href: '/page/5'
+        - href: '/page/6'
+        - href: '/page/7'
+        - href: '/page/8'
+        - href: '/page/9'
+        - href: '/page/10'
   - name: with prev and next only
     data:
       previous:
diff --git a/packages/govuk-frontend/src/govuk/components/pagination/template.njk b/packages/govuk-frontend/src/govuk/components/pagination/template.njk
index a1bcdb1c10..7e24f3345a 100644
--- a/packages/govuk-frontend/src/govuk/components/pagination/template.njk
+++ b/packages/govuk-frontend/src/govuk/components/pagination/template.njk
@@ -1,5 +1,21 @@
 {% set blockLevel = not params.items and (params.next or params.previous) %}
 
+{% set currentPage = params.currentPage | int %}
+{% set neighbouringPages = params.neighbouringPages | int if params.neighbouringPages !== undefined else 2 %}
+
+{% set visiblePagesLower = (currentPage - neighbouringPages) | int %}
+{% set visiblePagesUpper = (currentPage + neighbouringPages) | int %}
+
+{% macro _paginationItem(index, item) %}
+  {%- set isCurrent = index == currentPage %}
+  {%- set number = item.number | default(index) %}
+  <li class="govuk-pagination__item {{- ' govuk-pagination__item--current' if isCurrent }}">
+    <a class="govuk-link govuk-pagination__link" href="{{ item.href }}" aria-label="{{ item.visuallyHiddenText | default("Page " + number) }}" {%- if isCurrent %} aria-current="page"{% endif %} {%- for attribute, value in item.attributes %} {{ attribute }}="{{ value }}"{% endfor %}>
+      {{- number -}}
+    </a>
+  </li>
+{% endmacro %}
+
 <nav class="govuk-pagination {{- ' ' + params.classes if params.classes }} {{- ' govuk-pagination--block' if blockLevel }}" role="navigation" aria-label="{{ params.landmarkLabel | default("results") }}" {%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}" {%- endfor -%}>
   {%- if params.previous and params.previous.href -%}
     <div class="govuk-pagination__prev">
@@ -18,21 +34,44 @@
     </div>
   {% endif %}
 
-  {%- if params.items -%}
+  {# Only show numbered pagination if params.items is set #}
+  {%- if params.items %}
     <ul class="govuk-pagination__list">
-      {%- for item in params.items -%}
-        {%- if item.ellipsis -%}
-          <li class="govuk-pagination__item govuk-pagination__item--ellipses">&ctdot;</li>
-        {%- elseif item.number -%}
-          <li class="govuk-pagination__item {{- ' govuk-pagination__item--current' if item.current }}">
-            <a class="govuk-link govuk-pagination__link" href="{{ item.href }}" aria-label="{{ item.visuallyHiddenText | default("Page " + item.number) }}" {%- if item.current %} aria-current="page" {%- endif -%} {%- for attribute, value in item.attributes %} {{ attribute }}="{{ value }}" {%- endfor -%}>
-              {{ item.number }}
-            </a>
-          </li>
-        {%- endif -%}
-      {%- endfor -%}
+
+    {# If there are 3 or fewer items in the list, skip the logic and just render them all. #}
+    {%- if params.items | length <= 3 %}
+      {%- for item in params.items %}
+        {{ _paginationItem(loop.index, item) }}
+      {%- endfor %}
+    {%- else %}
+
+      {%- for item in params.items %}
+
+        {# Always show the first and last numbered option #}
+        {%- if loop.first or loop.last %}
+
+          {# If the last item in the loop, and we're likely to need one, prepend an ellipsis #}
+          {%- if loop.last and loop.index > currentPage + 1 %}
+            <li class="govuk-pagination__item govuk-pagination__item--ellipses {{- ' govuk-pagination__item--collapsed-only' if loop.index - 1 <= visiblePagesUpper }}">&ctdot;</li>
+          {%- endif %}
+
+          {{ _paginationItem(loop.index, item) }}
+
+          {# If the first item in the loop, and we're likely to need one, append an ellipsis #}
+          {%- if loop.first and loop.index < currentPage - 1 %}
+            <li class="govuk-pagination__item govuk-pagination__item--ellipses {{- ' govuk-pagination__item--collapsed-only' if loop.index + 1 >= visiblePagesLower }}">&ctdot;</li>
+          {%- endif %}
+
+        {# Is this within neighbourly range of the current page? Show it! #}
+        {%- elif loop.index >= visiblePagesLower and loop.index <= visiblePagesUpper %}
+          {{ _paginationItem(loop.index, item) }}
+        {%- endif %}
+
+      {%- endfor %}
+
+    {%- endif %}
     </ul>
-  {%- endif -%}
+  {%- endif %}
 
   {%- if params.next and params.next.href -%}
     {%- set nextArrow -%}

From 230c7fac0d662cf45ac2c1d1b925a2c8b9d5af3b Mon Sep 17 00:00:00 2001
From: "beeps (Kim Grey)" <kimberly.grey@digital.cabinet-office.gov.uk>
Date: Thu, 18 May 2023 12:08:54 +0100
Subject: [PATCH 2/2] Add pagination example page

---
 .../src/views/examples/pagination/index.njk   | 92 +++++++++++++++++++
 1 file changed, 92 insertions(+)
 create mode 100644 packages/govuk-frontend-review/src/views/examples/pagination/index.njk

diff --git a/packages/govuk-frontend-review/src/views/examples/pagination/index.njk b/packages/govuk-frontend-review/src/views/examples/pagination/index.njk
new file mode 100644
index 0000000000..269a0b6934
--- /dev/null
+++ b/packages/govuk-frontend-review/src/views/examples/pagination/index.njk
@@ -0,0 +1,92 @@
+{% from "back-link/macro.njk" import govukBackLink %}
+{% from "pagination/macro.njk" import govukPagination %}
+
+{% extends "layout.njk" %}
+
+{% block beforeContent %}
+  {{ govukBackLink({
+    href: "/"
+  }) }}
+{% endblock %}
+
+{% set veryShortPaginationItems = [
+  { href: '#' },
+  { href: '#' },
+  { href: '#' }
+] %}
+
+{% set shortPaginationItems = [
+  { href: '#' },
+  { href: '#' },
+  { href: '#' },
+  { href: '#' }
+] %}
+
+{% set mediumPaginationItems = [
+  { href: '#' },
+  { href: '#' },
+  { href: '#' },
+  { href: '#' },
+  { href: '#' },
+  { href: '#' },
+  { href: '#' }
+] %}
+
+{% set longPaginationItems = [
+  { href: '#' },
+  { href: '#' },
+  { href: '#' },
+  { href: '#' },
+  { href: '#' },
+  { href: '#' },
+  { href: '#' },
+  { href: '#' },
+  { href: '#' },
+  { href: '#' },
+  { href: '#' },
+  { href: '#' }
+] %}
+
+{% block content %}
+
+<h2 class="govuk-heading-l">Very short</h2>
+<p class="govuk-body">Very short paginations (3 or fewer items) are small enough to not need to hide anything on any breakpoint.</p>
+
+{%- for i in range(1, (veryShortPaginationItems | length) + 1) %}
+  {{ govukPagination({
+    items: veryShortPaginationItems,
+    currentPage: i
+  }) }}
+{%- endfor %}
+
+<h2 class="govuk-heading-l">Short</h2>
+<p class="govuk-body">Short paginations (4 items, with default settings) can show all items on wide breakpoints, but hide some for narrow breakpoints.</p>
+
+{%- for i in range(1, (shortPaginationItems | length) + 1) %}
+  {{ govukPagination({
+    items: shortPaginationItems,
+    currentPage: i
+  }) }}
+{%- endfor %}
+
+<h2 class="govuk-heading-l">Medium</h2>
+<p class="govuk-body">Medium length paginations (5 to 7 items, with default settings) have the ability to sometimes be within 'neighbourly' range of both the start and end at the same time, showing all items at once on wide breakpoints, but also have enough items to require removing some in certain contexts and on narrow breakpoints.</p>
+
+{%- for i in range(1, (mediumPaginationItems | length) + 1) %}
+  {{ govukPagination({
+    items: mediumPaginationItems,
+    currentPage: i
+  }) }}
+{%- endfor %}
+
+<h2 class="govuk-heading-l">Long</h2>
+<p class="govuk-body">Long paginations (8 or more items with default settings) have enough items that they can never show all items at once. Some amount of truncation is always required, even on wider breakpoints.</p>
+
+{%- for i in range(1, (longPaginationItems | length) + 1) %}
+  {{ govukPagination({
+    items: longPaginationItems,
+    currentPage: i
+  }) }}
+{%- endfor %}
+
+{% endblock %}