Skip to content

Commit

Permalink
Add reorderable list component
Browse files Browse the repository at this point in the history
A list of items that can be reordered

List items can be reordered by drag and drop or by using the up/down buttons. On small viewports the drag and drop feature is disabled to prevent being triggered when scrolling on touch devices.

This component uses SortableJS - a JavaScript library for drag and drop interactions. When JavaScript is disabled a set of inputs will be shown allowing users to provide an order index for each item.

This is used by publishing tools to allow reordering of lists such as steps in step by step, entries in timelines and order of visual presentation of attachments.
  • Loading branch information
Dilwoar Hussain committed Mar 9, 2021
1 parent c8e5670 commit cf44d25
Show file tree
Hide file tree
Showing 11 changed files with 637 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* Remove lists from summary action links ([PR #1956](https://github.com/alphagov/govuk_publishing_components/pull/1956))
* Fix GOV.UK Frontend deprecation warning for component-guide print stylesheet ([PR #1961](https://github.com/alphagov/govuk_publishing_components/pull/1961))
* Update search box button ([PR #1957](https://github.com/alphagov/govuk_publishing_components/pull/1957))
* Adds Reorderable lists component ([PR #1905](https://github.com/alphagov/govuk_publishing_components/pull/1905))

## 24.4.1

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//= require sortablejs/Sortable.js
window.GOVUK = window.GOVUK || {}
window.GOVUK.Modules = window.GOVUK.Modules || {};

(function (Modules) {
function ReorderableList () { }

ReorderableList.prototype.start = function ($module) {
this.$module = $module[0]
this.$upButtons = this.$module.querySelectorAll('.js-reorderable-list-up')
this.$downButtons = this.$module.querySelectorAll('.js-reorderable-list-down')

this.sortable = window.Sortable.create(this.$module, { // eslint-disable-line new-cap
chosenClass: 'gem-c-reorderable-list__item--chosen',
dragClass: 'gem-c-reorderable-list__item--drag',
onSort: function () {
this.updateOrderIndexes()
this.triggerEvent(this.$module, 'reorder-drag')
}.bind(this)
})

if (typeof window.matchMedia === 'function') {
this.setupResponsiveChecks()
} else {
this.sortable.option('disabled', true)
}

var boundOnClickUpButton = this.onClickUpButton.bind(this)
this.$upButtons.forEach(function (button) {
button.addEventListener('click', boundOnClickUpButton)
})

var boundOnClickDownButton = this.onClickDownButton.bind(this)
this.$downButtons.forEach(function (button) {
button.addEventListener('click', boundOnClickDownButton)
})
}

ReorderableList.prototype.setupResponsiveChecks = function () {
var tabletBreakpoint = '40.0625em' // ~640px
this.mediaQueryList = window.matchMedia('(min-width: ' + tabletBreakpoint + ')')
this.mediaQueryList.addListener(this.checkMode.bind(this))
this.checkMode()
}

ReorderableList.prototype.checkMode = function () {
this.sortable.option('disabled', !this.mediaQueryList.matches)
}

ReorderableList.prototype.onClickUpButton = function (e) {
var item = e.target.closest('.gem-c-reorderable-list__item')
var previousItem = item.previousElementSibling
if (item && previousItem) {
item.parentNode.insertBefore(item, previousItem)
this.updateOrderIndexes()
}
// if triggered by keyboard preserve focus on button
if (e.detail === 0) {
if (item !== item.parentNode.firstElementChild) {
e.target.focus()
} else {
e.target.nextElementSibling.focus()
}
}
this.triggerEvent(e.target, 'reorder-move-up')
}

ReorderableList.prototype.onClickDownButton = function (e) {
var item = e.target.closest('.gem-c-reorderable-list__item')
var nextItem = item.nextElementSibling
if (item && nextItem) {
item.parentNode.insertBefore(item, nextItem.nextElementSibling)
this.updateOrderIndexes()
}
// if triggered by keyboard preserve focus on button
if (e.detail === 0) {
if (item !== item.parentNode.lastElementChild) {
e.target.focus()
} else {
e.target.previousElementSibling.focus()
}
}
this.triggerEvent(e.target, 'reorder-move-down')
}

ReorderableList.prototype.updateOrderIndexes = function () {
var $orderInputs = this.$module.querySelectorAll('.gem-c-reorderable-list__actions input')
$orderInputs.forEach(function (input, index) {
input.setAttribute('value', index + 1)
})
}

ReorderableList.prototype.triggerEvent = function (element, eventName) {
var params = { bubbles: true, cancelable: true }
var event

if (typeof window.CustomEvent === 'function') {
event = new window.CustomEvent(eventName, params)
} else {
event = document.createEvent('CustomEvent')
event.initCustomEvent(eventName, params.bubbles, params.cancelable)
}

element.dispatchEvent(event)
}

Modules.ReorderableList = ReorderableList
})(window.GOVUK.Modules)
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
@import "components/print-link";
@import "components/radio";
@import "components/related-navigation";
@import "components/reorderable-list";
@import "components/search";
@import "components/select";
@import "components/share-links";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
.gem-c-reorderable-list {
@include govuk-font(19, bold);

list-style-type: none;
margin-bottom: govuk-spacing(6);
margin-top: 0;
padding-left: 0;
position: relative;

.govuk-form-group {
margin-bottom: 0;
}
}

.gem-c-reorderable-list__item {
margin-bottom: govuk-spacing(3);
border: 1px solid $govuk-border-colour;
padding: govuk-spacing(3);
}

.gem-c-reorderable-list__item--chosen {
background-color: govuk-colour('light-grey');
outline: 2px dotted $govuk-border-colour;
}

.gem-c-reorderable-list__item--drag {
background-color: govuk-colour('white');
list-style-type: none;

.gem-c-reorderable-list__actions {
visibility: hidden;
}
}

.gem-c-reorderable-list__wrapper {
display: block;

@include govuk-media-query($from: desktop) {
display: inline-flex;
width: 100%;
}
}

.gem-c-reorderable-list__content {
margin-bottom: govuk-spacing(2);
@include govuk-media-query($from: desktop) {
margin-bottom: 0;
flex: 0 1 auto;
min-width: 65%;
}
}

.gem-c-reorderable-list__title {
margin: 0;
}

.gem-c-reorderable-list__description {
@include govuk-font(16, regular);
margin: 0;
}

.gem-c-reorderable-list__actions {
display: block;

@include govuk-media-query($from: desktop) {
flex: 1 0 auto;
text-align: right;
}

.gem-c-button {
display: none;
}
}

.js-enabled {
.gem-c-reorderable-list__item {
@include govuk-media-query($from: desktop) {
cursor: move;
}
}

.gem-c-reorderable-list__actions .govuk-form-group {
display: none;
}

.gem-c-reorderable-list__actions .gem-c-button {
display: inline-block;
margin-left: govuk-spacing(3);
width: 80px;
}

.gem-c-reorderable-list__actions .gem-c-button:first-of-type {
margin-left: 0;

@include govuk-media-query($from: desktop) {
margin-left: govuk-spacing(3);
}
}

.gem-c-reorderable-list__item:first-child .gem-c-button:first-of-type,
.gem-c-reorderable-list__item:last-child .gem-c-button:last-of-type {
display: none;

@include govuk-media-query($from: desktop) {
display: inline-block;
visibility: hidden;
}
}

.gem-c-reorderable-list__item:first-child .gem-c-button:last-of-type {
margin-left: 0;

@include govuk-media-query($from: desktop) {
margin-left: govuk-spacing(3);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<%
items ||= []
input_name ||= "ordering"
data_attributes ||= {}
data_attributes[:module] = "reorderable-list"
%>

<%= tag.ol class: "gem-c-reorderable-list", data: data_attributes do %>
<% items.each_with_index do |item, index| %>
<%= tag.li class: "gem-c-reorderable-list__item" do %>
<%= tag.div class: "gem-c-reorderable-list__wrapper" do %>
<%= tag.div class: "gem-c-reorderable-list__content" do %>
<%= tag.p item[:title], class: "gem-c-reorderable-list__title" %>
<%= tag.p(item[:description], class: "gem-c-reorderable-list__description") if item[:description].present? %>
<% end %>
<%= tag.div class: "gem-c-reorderable-list__actions" do %>
<% label_text = capture do %>
Position<span class='govuk-visually-hidden'> for <%= item[:title] %></span>
<% end %>
<%= render "govuk_publishing_components/components/input", {
label: { text: label_text },
name: "#{input_name}[#{item[:id]}]",
type: "number",
value: index + 1,
width: 2
} %>
<%= render "govuk_publishing_components/components/button", {
text: "Up",
type: "button",
aria_label: "Move \"#{item[:title]}\" up",
classes: "js-reorderable-list-up",
secondary_solid: true
} %>
<%= render "govuk_publishing_components/components/button", {
text: "Down",
type: "button",
aria_label: "Move \"#{item[:title]}\" down",
classes: "js-reorderable-list-down",
secondary_solid: true
} %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Reorderable list
description: A list of items that can be reordered
body: |
List items can be reordered by drag and drop or by using the up/down buttons.
On small viewports the drag and drop feature is disabled to prevent being triggered
when scrolling on touch devices.
This component uses SortableJS - a JavaScript library for drag and drop interactions.
When JavaScript is disabled a set of inputs will be shown allowing users to provide
an order index for each item.
When this component is embedded into a form and that form is submit you will receive a
parameter of `ordering` (which can be customised with the `input_name` option).
This will contain item ids and ordering positions in a hash.
For example, for two items with id "a" and "b" that are ordered accordingly,
you'd receive a submission of `ordering[a]=1&ordering[b]=2`, which Rails can
translate to `"ordering" => { "a" => "1", "b" => "2" }`.
accessibility_criteria: |
Buttons in this component must:
* be keyboard focusable
* inform the user about which item they operate on
* preserve focus after interacting with them
examples:
default:
data:
items:
- id: "ce99dd60-67dc-11eb-ae93-0242ac130002"
title: "Budget 2018"
- id: "d321cb86-67dc-11eb-ae93-0242ac130002"
title: "Budget 2018 (web)"
- id: "63a6d29e-6b6d-4157-9067-84c1a390e352"
title: "Impact on households: distributional analysis to accompany Budget 2018"
- id: "0a4d377d-68f4-472f-b2e3-ef71dc750f85"
title: "Table 2.1: Budget 2018 policy decisions"
- id: "5ebd75d7-6c37-4b93-b444-1b7c49757fb9"
title: "Table 2.2: Measures announced at Autumn Budget 2017 or earlier that will take effect from November 2018 or later (£ million)"
with_description:
data:
items:
- id: "ce99dd60-67dc-11eb-ae93-0242ac130002"
title: "Budget 2018"
description: "PDF, 2.56MB, 48 pages"
- id: "d321cb86-67dc-11eb-ae93-0242ac130002"
title: "Budget 2018 (web)"
description: "HTML attachment"
- id: "63a6d29e-6b6d-4157-9067-84c1a390e352"
title: "Impact on households: distributional analysis to accompany Budget 2018"
description: "PDF, 592KB, 48 pages"
- id: "0a4d377d-68f4-472f-b2e3-ef71dc750f85"
title: "Table 2.1: Budget 2018 policy decisions"
description: "MS Excel Spreadsheet, 248KB"
- id: "5ebd75d7-6c37-4b93-b444-1b7c49757fb9"
title: "Table 2.2: Measures announced at Autumn Budget 2017 or earlier that will take effect from November 2018 or later (£ million)"
description: "MS Excel Spreadsheet, 248KB"
within_form:
embed: |
<form>
<%= component %>
<button class="govuk-button" type="submit">Save order</button>
</form>
data:
items:
- id: "ce99dd60-67dc-11eb-ae93-0242ac130002"
title: "Budget 2018"
- id: "d321cb86-67dc-11eb-ae93-0242ac130002"
title: "Budget 2018 (web)"
- id: "63a6d29e-6b6d-4157-9067-84c1a390e352"
title: "Impact on households: distributional analysis to accompany Budget 2018"
- id: "0a4d377d-68f4-472f-b2e3-ef71dc750f85"
title: "Table 2.1: Budget 2018 policy decisions"
- id: "5ebd75d7-6c37-4b93-b444-1b7c49757fb9"
title: "Table 2.2: Measures announced at Autumn Budget 2017 or earlier that will take effect from November 2018 or later (£ million)"
with_custom_input_name:
data:
input_name: "attachments[ordering]"
items:
- id: "5ebd75d7-6c37-4b93-b444-1b7c49757fb9"
title: "Budget 2018"
description: "PDF, 2.56MB, 48 pages"
- id: "0a4d377d-68f4-472f-b2e3-ef71dc750f85"
title: "Budget 2020"
description: "PDF, 2.56MB, 48 pages"
2 changes: 1 addition & 1 deletion govuk_publishing_components.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Gem::Specification.new do |s|
s.license = "MIT"
s.required_ruby_version = ">= 2.6"

s.files = Dir["{node_modules/govuk-frontend,node_modules/axe-core,node_modules/jquery,app,config,db,lib}/**/*", "LICENCE.md", "README.md"]
s.files = Dir["{node_modules/govuk-frontend,node_modules/axe-core,node_modules/jquery,node_modules/sortablejs,app,config,db,lib}/**/*", "LICENCE.md", "README.md"]

s.add_dependency "govuk_app_config"
s.add_dependency "kramdown"
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"dependencies": {
"axe-core": "^3.5.4",
"govuk-frontend": "^3.11.0",
"jquery": "1.12.4"
"jquery": "1.12.4",
"sortablejs": "^1.13.0"
},
"devDependencies": {
"standardx": "^7.0.0",
Expand Down
Loading

0 comments on commit cf44d25

Please sign in to comment.