Repository: LandRegistry/govuk-frontend-jinja (language: python)

Issue:
GOV.UK Frontend v4.2.0 with components changes
Contains my attempt at implementing the component changes specified in the 4.2.0 release of the govuk-frontend.

Happy to receive feedback @matthew-shaw :) 


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index 8f0cd652..2452ddd7 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -46,5 +46,5 @@ jobs:
       run: |
         (cd tests/utils && nohup python -m flask run --port 3000 &)
         wait-for-it localhost:3000
-        ./govuk-frontend-diff http://localhost:3000 --govuk-frontend-version=v4.1.0 --ci
+        ./govuk-frontend-diff http://localhost:3000 --govuk-frontend-version=v4.2.0 --ci
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f28d13c5..371c8108 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,16 @@ All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-## [Unreleased](https://github.com/LandRegistry/govuk-frontend-jinja/compare/2.2.0...main)
+## [Unreleased](https://github.com/LandRegistry/govuk-frontend-jinja/compare/2.3.0...main)
+
+## [2.3.0](https://github.com/LandRegistry/govuk-frontend-jinja/releases/tag/2.3.0) - 22/07/2022
+
+### Added
+
+- [GOV.UK Frontend v4.2.0](https://github.com/alphagov/govuk-frontend/releases/tag/v4.2.0) support
+- [Pagination component](https://design-system.service.gov.uk/components/pagination/)
+
+Thanks to [SentientHousePlant](https://github.com/SentientHousePlant)
 
 ## [2.2.0](https://github.com/LandRegistry/govuk-frontend-jinja/releases/tag/2.2.0) - 18/05/2022
 
diff --git a/README.md b/README.md
index 515773ae..36b0318f 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # GOV.UK Frontend Jinja Macros
 
 [![PyPI version](https://badge.fury.io/py/govuk-frontend-jinja.svg)](https://pypi.org/project/govuk-frontend-jinja/)
-![govuk-frontend 4.1.0](https://img.shields.io/badge/govuk--frontend%20version-4.1.0-005EA5?logo=gov.uk&style=flat)
+![govuk-frontend 4.2.0](https://img.shields.io/badge/govuk--frontend%20version-4.2.0-005EA5?logo=gov.uk&style=flat)
 [![Python package](https://github.com/LandRegistry/govuk-frontend-jinja/actions/workflows/python-package.yml/badge.svg)](https://github.com/LandRegistry/govuk-frontend-jinja/actions/workflows/python-package.yml)
 
 **GOV.UK Frontend Jinja is a [community tool](https://design-system.service.gov.uk/community/resources-and-tools/) of the [GOV.UK Design System](https://design-system.service.gov.uk/). The Design System team is not responsible for it and cannot support you with using it. Contact the [maintainers](#contributors) directly if you need [help](#support) or you want to request a feature.**
@@ -18,6 +18,7 @@ The following table shows the version of GOV.UK Frontend Jinja that you should u
 
 | GOV.UK Frontend Jinja Version | Target GOV.UK Frontend Version |
 | ----------------------------- | ------------------------------ |
+| [2.3.0](https://github.com/LandRegistry/govuk-frontend-jinja/releases/tag/2.3.0) | [4.2.0](https://github.com/alphagov/govuk-frontend/releases/tag/v4.2.0) |
 | [2.2.0](https://github.com/LandRegistry/govuk-frontend-jinja/releases/tag/2.2.0) | [4.1.0](https://github.com/alphagov/govuk-frontend/releases/tag/v4.1.0) |
 | [2.1.0](https://github.com/LandRegistry/govuk-frontend-jinja/releases/tag/2.1.0) | [4.0.1](https://github.com/alphagov/govuk-frontend/releases/tag/v4.0.1) |
 | [2.0.0](https://github.com/LandRegistry/govuk-frontend-jinja/releases/tag/2.0.0) | [4.0.0](https://github.com/alphagov/govuk-frontend/releases/tag/v4.0.0) |
diff --git a/govuk_frontend_jinja/templates/components/checkboxes/macro.html b/govuk_frontend_jinja/templates/components/checkboxes/macro.html
index aec8d762..4983b2ca 100644
--- a/govuk_frontend_jinja/templates/components/checkboxes/macro.html
+++ b/govuk_frontend_jinja/templates/components/checkboxes/macro.html
@@ -5,16 +5,16 @@
 {% from "govuk_frontend_jinja/components/label/macro.html" import govukLabel %}
 
 {#- If an id 'prefix' is not passed, fall back to using the name attribute
-   instead. We need this for error messages and hints as well -#}
+  instead. We need this for error messages and hints as well -#}
 {% set idPrefix = params.idPrefix if params.idPrefix else params.name %}
 
 {% set ns = namespace() %}
 
 {#- a record of other elements that we need to associate with the input using
-   aria-describedby – for example hints or error messages -#}
+  aria-describedby – for example hints or error messages -#}
 {% set ns.describedBy = params.describedBy if params.describedBy else "" %}
 {% if params.fieldset and params.fieldset.describedBy %}
-   {% set ns.describedBy = params.fieldset.describedBy %}
+  {% set ns.describedBy = params.fieldset.describedBy %}
 {% endif %}
 
 {#- fieldset is false by default -#}
@@ -31,7 +31,7 @@
     'attributes': params.hint.attributes,
     'html': params.hint.html,
     'text': params.hint.text
-  }) | trim }}
+  }) | indent(2) | trim }}
 {% endif %}
 {% if params.errorMessage %}
   {% set errorId = idPrefix + '-error' %}
@@ -43,7 +43,7 @@
     'html': params.errorMessage.html,
     'text': params.errorMessage.text,
     'visuallyHiddenText': params.errorMessage.visuallyHiddenText
-  }) | trim }}
+  }) | indent(2) | trim }}
 {% endif %}
   <div class="govuk-checkboxes {%- if params.classes %} {{ params.classes }}{% endif %}"
     {%- for attribute, value in (params.attributes.items() if params.attributes else {}.items()) %} {{ attribute }}="{{ value }}"{% endfor %}
@@ -66,13 +66,14 @@
         {%- if item.divider %}
           <div class="govuk-checkboxes__divider">{{ item.divider }}</div>
         {%- else %}
+          {% set isChecked = item.checked | default(params.get("values", false) and item.value in params.get('values', [])) %}
           {% set hasHint = True if (item.hint.text if item.hint and item.hint.text) or (item.hint.html if item.hint and item.hint.html) %}
           {% set itemHintId = id + "-item-hint" if hasHint else "" %}
           {% set itemDescribedBy = ns.describedBy if not hasFieldset else "" %}
           {% set itemDescribedBy = (itemDescribedBy + " " + itemHintId) | trim %}
           <div class="govuk-checkboxes__item">
             <input class="govuk-checkboxes__input" id="{{ id }}" name="{{ name }}" type="checkbox" value="{{ item.value }}"
-            {{-" checked" if item.checked }}
+            {{-" checked" if isChecked }}
             {{-" disabled" if item.disabled }}
             {%- if item.conditional and item.conditional.html %} data-aria-controls="{{ conditionalId }}"{% endif -%}
             {%- if item.behaviour %} data-behaviour="{{ item.behaviour }}"{% endif -%}
@@ -84,7 +85,7 @@
               'classes': 'govuk-checkboxes__label' + (' ' + item.label.classes if item.label and item.label.classes else ''),
               'attributes': (item.label.attributes if item.label and item.label.attributes),
               'for': id
-            }) | trim }}
+            }) | indent(6) | trim }}
             {% if hasHint %}
             {{ govukHint({
               'id': itemHintId,
@@ -92,11 +93,11 @@
               'attributes': item.hint.attributes,
               'html': item.hint.html,
               'text': item.hint.text
-            }) | trim }}
+            }) | indent(6) | trim }}
             {% endif %}
           </div>
           {% if item.conditional and item.conditional.html %}
-            <div class="govuk-checkboxes__conditional{% if not item.checked %} govuk-checkboxes__conditional--hidden{% endif %}" id="{{ conditionalId }}">
+            <div class="govuk-checkboxes__conditional{% if not isChecked %} govuk-checkboxes__conditional--hidden{% endif %}" id="{{ conditionalId }}">
               {{ item.conditional.html | safe }}
             </div>
           {% endif %}
diff --git a/govuk_frontend_jinja/templates/components/header/macro.html b/govuk_frontend_jinja/templates/components/header/macro.html
index a88ecc4c..756a0bf6 100644
--- a/govuk_frontend_jinja/templates/components/header/macro.html
+++ b/govuk_frontend_jinja/templates/components/header/macro.html
@@ -1,6 +1,6 @@
 {% macro govukHeader(params) %}
-<header class="govuk-header {{ params.classes if params.classes else '' }}" role="banner" data-module="govuk-header"
-  {%- for attribute, value in (params.attributes.items() if params.attributes else {}.items()) %} {{attribute}}="{{value}}"{% endfor %}>
+<header class="govuk-header {{ params.classes if params.classes }}" role="banner" data-module="govuk-header"
+        {%- for attribute, value in (params.attributes.items() if params.attributes else {}.items()) %} {{attribute}}="{{value}}"{% endfor %}>
   <div class="govuk-header__container {{ params.containerClasses | default('govuk-width-container') }}">
     <div class="govuk-header__logo">
       <a href="{{ params.homepageUrl | default('/') }}" class="govuk-header__link govuk-header__link--homepage">
@@ -10,9 +10,11 @@
           currentColor into the crown whilst continuing to support older browsers
           which do not support external SVGs without a Javascript polyfill. This
           adds approximately 1kb to every page load.
+
           We use currentColour so that we can easily invert it when printing and
           when the focus state is applied. This also benefits users who override
           colours in their browser as they will still see the crown.
+
           The SVG needs `focusable="false"` so that Internet Explorer does not
           treat it as an interactive element - without this it will be
           'focusable' when using the keyboard to navigate. #}
@@ -48,13 +50,20 @@
     {% if params.serviceName or params.navigation  %}
     <div class="govuk-header__content">
     {% if params.serviceName %}
-    <a href="{{ params.serviceUrl }}" class="govuk-header__link govuk-header__link--service-name">
-      {{ params.serviceName }}
-    </a>
+    {% if params.serviceUrl %}
+      <a href="{{ params.serviceUrl }}" class="govuk-header__link govuk-header__service-name">
+        {{ params.serviceName }}
+      </a>
+    {% else%}
+      <span class="govuk-header__service-name">
+        {{ params.serviceName }}
+      </span>
+    {% endif %}
     {% endif %}
     {% if params.navigation %}
     <nav aria-label="{{ params.navigationLabel | default('Menu') }}" class="govuk-header__navigation {{ params.navigationClasses if params.navigationClasses }}">
       <button type="button" class="govuk-header__menu-button govuk-js-header-toggle" aria-controls="navigation" aria-label="{{ params.menuButtonLabel | default('Show or hide menu') }}">Menu</button>
+
       <ul id="navigation" class="govuk-header__navigation-list">
         {% for item in params.navigation %}
           {% if item.text or item.html %}
diff --git a/govuk_frontend_jinja/templates/components/pagination/macro.html b/govuk_frontend_jinja/templates/components/pagination/macro.html
new file mode 100644
index 00000000..194a675e
--- /dev/null
+++ b/govuk_frontend_jinja/templates/components/pagination/macro.html
@@ -0,0 +1,69 @@
+{% macro govukPagination(params) %}
+
+{% set has_key_items = ( params.get('items', []) != [] ) %}
+
+{% set blockLevel = not has_key_items and (params.next or params.previous) %}
+
+<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.items() if params.attributes else {}.items()) %} {{ attribute }}="{{ value }}" {%- endfor -%}">
+  {%- if params.previous and params.previous.href -%}
+    <div class="govuk-pagination__prev">
+      <a class="govuk-link govuk-pagination__link" href="{{ params.previous.href }}" rel="prev" {%- for attribute, value in (params.previous.attributes.items() if params.previous.attributes else {}.items()) %} {{ attribute }}="{{ value }}" {%- endfor -%}>
+        <svg class="govuk-pagination__icon govuk-pagination__icon--prev" xmlns="http://www.w3.org/2000/svg" height="13" width="15" aria-hidden="true" focusable="false" viewBox="0 0 15 13">
+          <path d="m6.5938-0.0078125-6.7266 6.7266 6.7441 6.4062 1.377-1.449-4.1856-3.9768h12.896v-2h-12.984l4.2931-4.293-1.414-1.414z"></path>
+        </svg>
+        <span class="govuk-pagination__link-title {{- ' govuk-pagination__link-title--decorated' if blockLevel and not params.previous.labelText }}">
+          {{- params.previous.text | default("Previous") -}}
+        </span>
+        {%- if params.previous.labelText and blockLevel -%}
+          <span class="govuk-visually-hidden">:</span>
+          <span class="govuk-pagination__link-label">{{ params.previous.labelText }}</span>
+        {%- endif -%}
+      </a>
+    </div>
+  {% endif %}
+
+  {%- if has_key_items -%}
+    <ul class="govuk-pagination__list">
+      {%- for item in params.get('items', []) -%}
+        {%- if item.ellipsis -%}
+          <li class="govuk-pagination__item govuk-pagination__item--ellipses">&ctdot;</li>
+        {%- elif 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.items() if item.attributes else {}.items()) %} {{ attribute }}="{{ value }}" {%- endfor -%}>
+              {{ item.number }}
+            </a>
+          </li>
+        {%- endif -%}
+      {%- endfor -%}
+    </ul>
+  {%- endif -%}
+
+  {%- if params.next and params.next.href -%}
+    {%- set nextArrow -%}
+      <svg class="govuk-pagination__icon govuk-pagination__icon--next" xmlns="http://www.w3.org/2000/svg" height="13" width="15" aria-hidden="true" focusable="false" viewBox="0 0 15 13">
+        <path d="m8.107-0.0078125-1.4136 1.414 4.2926 4.293h-12.986v2h12.896l-4.1855 3.9766 1.377 1.4492 6.7441-6.4062-6.7246-6.7266z"></path>
+      </svg>
+    {%- endset -%}
+
+    <div class="govuk-pagination__next">
+      <a class="govuk-link govuk-pagination__link" href="{{ params.next.href }}" rel="next" {%- for attribute, value in (params.next.attributes.items() if params.next.attributes else {}.items()) %} {{ attribute }}="{{ value }}" {%- endfor -%}>
+        {%- if blockLevel -%}
+          {{- nextArrow | safe -}}
+        {%- endif %}
+        <span class="govuk-pagination__link-title {{- ' govuk-pagination__link-title--decorated' if blockLevel and not params.next.labelText }}">
+          {{- params.next.text | default("Next") -}}
+        </span>
+        {%- if params.next.labelText and blockLevel -%}
+          <span class="govuk-visually-hidden">:</span>
+          <span class="govuk-pagination__link-label">{{ params.next.labelText }}</span>
+        {%- endif -%}
+        {%- if not blockLevel -%}
+          {{- nextArrow | safe -}}
+        {%- endif -%}
+      </a>
+    </div>
+  {%- endif -%}
+</nav>
+
+
+{% endmacro %}
\ No newline at end of file
diff --git a/govuk_frontend_jinja/templates/components/radios/macro.html b/govuk_frontend_jinja/templates/components/radios/macro.html
index bd6f69ff..0870b6f2 100644
--- a/govuk_frontend_jinja/templates/components/radios/macro.html
+++ b/govuk_frontend_jinja/templates/components/radios/macro.html
@@ -25,7 +25,7 @@
     'attributes': params.hint.attributes,
     'html': params.hint.html,
     'text': params.hint.text
-  }) | trim }}
+  }) | indent(2) | trim }}
 {% endif %}
 {% if params.errorMessage %}
   {% set errorId = idPrefix + '-error' %}
@@ -37,9 +37,9 @@
     'html': params.errorMessage.html,
     'text': params.errorMessage.text,
     'visuallyHiddenText': params.errorMessage.visuallyHiddenText
-  }) | trim }}
+  }) | indent(2) | trim }}
 {% endif %}
-  <div class="govuk-radios {%- if params.classes %} {{ params.classes }}{% endif %}{%- if ns.isConditional %} govuk-radios--conditional{% endif -%}"
+  <div class="govuk-radios {%- if params.classes %} {{ params.classes }}{% endif %}"
     {%- for attribute, value in (params.attributes.items() if params.attributes else {}.items()) %} {{ attribute }}="{{ value }}"{% endfor %}
     data-module="govuk-radios">
     {% for item in params['items'] %}
@@ -59,11 +59,12 @@
         {%- if item.divider %}
         <div class="govuk-radios__divider">{{ item.divider }}</div>
         {%- else %}
+        {% set isChecked = item.checked | default(params.value and item.value == params.value) %}
         {% set hasHint = True if (item.hint.text if item.hint and item.hint.text) or (item.hint.html if item.hint and item.hint.html) %}
         {% set itemHintId = id + '-item-hint' %}
         <div class="govuk-radios__item">
           <input class="govuk-radios__input" id="{{ id }}" name="{{ params.name }}" type="radio" value="{{ item.value }}"
-          {{-" checked" if item.checked }}
+          {{-" checked" if isChecked }}
           {{-" disabled" if item.disabled }}
           {%- if item.conditional and item.conditional.html %} data-aria-controls="{{ conditionalId }}"{% endif -%}
           {%- if hasHint %} aria-describedby="{{ itemHintId }}"{% endif -%}
@@ -74,7 +75,7 @@
             'classes': 'govuk-radios__label' + (' ' + item.label.classes if item.label and item.label.classes else ''),
             'attributes': (item.label.attributes if item.label and item.label.attributes),
             'for': id
-          }) | trim }}
+          }) | indent(6) | trim }}
           {% if hasHint %}
           {{ govukHint({
             'id': itemHintId,
@@ -82,11 +83,11 @@
             'attributes': item.hint.attributes,
             'html': item.hint.html,
             'text': item.hint.text
-          }) | trim }}
+          }) | indent(6) | trim }}
           {% endif %}
         </div>
         {% if item.conditional and item.conditional.html %}
-          <div class="govuk-radios__conditional{% if not item.checked %} govuk-radios__conditional--hidden{% endif %}" id="{{ conditionalId }}">
+          <div class="govuk-radios__conditional{% if not isChecked %} govuk-radios__conditional--hidden{% endif %}" id="{{ conditionalId }}">
             {{ item.conditional.html | safe }}
           </div>
         {% endif %}
@@ -110,5 +111,4 @@
   {{ innerHtml | trim | safe }}
 {% endif %}
 </div>
-
 {% endmacro %}
diff --git a/govuk_frontend_jinja/templates/components/select/macro.html b/govuk_frontend_jinja/templates/components/select/macro.html
index 5409a08e..90aea06b 100644
--- a/govuk_frontend_jinja/templates/components/select/macro.html
+++ b/govuk_frontend_jinja/templates/components/select/macro.html
@@ -15,7 +15,7 @@
     'isPageHeading': params.label.isPageHeading,
     'attributes': params.label.attributes,
     'for': params.id
-  }) | trim }}
+  }) | indent(2) | trim }}
 {% endif %}
 {% if params.hint %}
   {% set hintId = params.id + '-hint' %}
@@ -26,7 +26,7 @@
     'attributes': params.hint.attributes,
     'html': params.hint.html,
     'text': params.hint.text
-  }) | trim }}
+  }) | indent(2) | trim }}
 {% endif %}
 {% if params.errorMessage %}
   {% set errorId = params.id + '-error' %}
@@ -38,21 +38,20 @@
     'html': params.errorMessage.html,
     'text': params.errorMessage.text,
     'visuallyHiddenText': params.errorMessage.visuallyHiddenText
-  }) | trim }}
+  }) | indent(2) | trim }}
 {% endif %}
   <select class="govuk-select
     {%- if params.classes %} {{ params.classes }}{% endif %}{%- if params.errorMessage %} govuk-select--error{% endif %}" id="{{ params.id }}" name="{{ params.name }}" {%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %} {%- for attribute, value in (params.attributes.items() if params.attributes else {}.items()) %} {{ attribute }}="{{ value }}"{% endfor %}>
-  {% if 'items' in params and params['items'] | length %}
-    {% for item in params['items'] %}
-      {% if item %}
-        <option value="{{ item.value }}"
-          {{-" selected" if item.selected }}
-          {{-" disabled" if item.disabled }}
-          {%- for attribute, value in (item.attributes.items() if item.attributes else {}.items()) %} {{ attribute }}="{{ value }}"{% endfor -%}>{{ item.text }}</option>
-      {% endif %}
-    {% endfor %}
-  {% endif %}
+    {% if 'items' in params and params['items'] | length %}
+      {% for item in params['items'] %}
+        {% if item %}
+          <option value="{{ item.value }}"
+            {{-" selected" if item.selected | default(params.value and item.value == params.value) }}
+            {{-" disabled" if item.disabled }}
+            {%- for attribute, value in (item.attributes.items() if item.attributes else {}.items()) %} {{ attribute }}="{{ value }}"{% endfor -%}>{{ item.text }}</option>
+        {% endif %}
+      {% endfor %}
+    {% endif %}
   </select>
 </div>
-
 {% endmacro %}
diff --git a/setup.py b/setup.py
index b70e4fa4..15578d4f 100644
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,7 @@
 
 setuptools.setup(
     name="govuk-frontend-jinja",
-    version="2.2.0",
+    version="2.3.0",
     author="Matt Shaw",
     author_email="matthew.shaw@landregistry.gov.uk",
     description="GOV.UK Frontend Jinja Macros",


Test patch:
diff --git a/requirements-test.in b/requirements-test.in
index 25bd43de..f84e8087 100644
--- a/requirements-test.in
+++ b/requirements-test.in
@@ -1,3 +1,4 @@
-flask==2.1.2
+flask==2.1.3
+flake8==4.0.1
 # Generated with piprot 0.9.10
 # Looks like you've been keeping up to date, time for a delicious beverage!
\ No newline at end of file
diff --git a/requirements-test.txt b/requirements-test.txt
index 0630f509..3d9b6c57 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,18 +1,30 @@
 #
-# This file is autogenerated by pip-compile with python 3.10
+# This file is autogenerated by pip-compile with python 3.9
 # To update, run:
 #
 #    pip-compile requirements-test.in
 #
 click==8.1.3
     # via flask
-flask==2.1.2
+flake8==4.0.1
     # via -r requirements-test.in
+flask==2.1.3
+    # via -r requirements-test.in
+importlib-metadata==4.12.0
+    # via flask
 itsdangerous==2.1.2
     # via flask
 jinja2==3.1.2
     # via flask
 markupsafe==2.1.1
     # via jinja2
+mccabe==0.6.1
+    # via flake8
+pycodestyle==2.8.0
+    # via flake8
+pyflakes==2.4.0
+    # via flake8
 werkzeug==2.1.2
     # via flask
+zipp==3.8.1
+    # via importlib-metadata
diff --git a/tests/utils/test-entrypoint.sh b/tests/utils/test-entrypoint.sh
index bfb29a84..54bfb3e7 100755
--- a/tests/utils/test-entrypoint.sh
+++ b/tests/utils/test-entrypoint.sh
@@ -5,4 +5,4 @@ set -e
 flake8 .
 (cd tests/utils && nohup python -m flask run --port 3000 &)
 wait-for-it localhost:3000
-./govuk-frontend-diff http://localhost:3000 --govuk-frontend-version=v4.1.0
+./govuk-frontend-diff http://localhost:3000 --govuk-frontend-version=v4.2.0