Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add post on improving accessibility of Rails forms #153

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
149 changes: 149 additions & 0 deletions app/posts/finishing-touches-for-forms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
title: Finishing touches for forms
date: 2025-01-05
description: Some tips on making sure forms written in Ruby on Rails are accessible
author:
name: Peter Yates
url: https://github.com/peteryates
image:
src: /assets/posts/finishing-touches-for-forms/illustration.png
alt: Illustration of somebody straightening a picture frame on a wall which says ‘form sweet form’.
opengraphImage: true
---

The [GOV.UK formbuilder](https://govuk-form-builder.netlify.app) makes it easy for Ruby on Rails developers to build forms that follow the rules set out in the [GOV.UK Design System](https://design-system.service.gov.uk/).

It generates the correct HTML, provides a nice API for customising the fields to meet the needs of your service and takes care of clearly displaying any error messages.

There are, however, some suggestions in the Design System guidance that go beyond the remit of the form builder.

They are often forgotten, we'll go over some simple ways to solve them here.

## Positioning the error summary at the top of the page

In addition to making it clear that something is wrong, we also need to tell the user exactly what happened and how to fix it.

[The guidance says](https://design-system.service.gov.uk/components/error-summary#where-to-put-the-error-summary):

> Put the error summary at the top of the main container. If your page includes breadcrumbs or a back link, place it below these, but above the `<h1>`.

This is problematic because the form often isn't the first thing on the page but the form builder needs to be responsible for rendering the error summary to ensure the links and targets are consistent.

We could solve the problem by wrapping the whole page in a `<form>` element, which would allow us to place the error summary wherever we like, but it's a hack. It isn't semantically correct and is likely to lead to other problems.

Thankfully, Rails has our backs. We can use [`content_for`](https://guides.rubyonrails.org/layouts_and_rendering.html#using-the-content-for-method) to insert content into a named block in our layout. If we add a named block called `top_of_main` at the top of our `<main>` element, like this:

```html
<div class="govuk-width-container">
<main class="govuk-main-wrapper" id="main-content">
<%= yield :top_of_main %>
<h1 class="govuk-heading-xl">Default page template</h1>

<!-- the rest of our page -->
```

We can send our error summary from the form to it, like this:

```html
<%= form_with(model: @object, url: some_path) do |form| %>
<%= content_for(:top_of_main) { form.govuk_error_summary } %>

<%= form.govuk_text_field(:how_many_juggling_items) %>
<% end %>
```

And now our error summary is in the right spot.

![Two screenshots side by side showing a form with errors. The left one has the error summary after the h1 and the right one has the error summary first](/assets/posts/finishing-touches-for-forms/error-summary-side-by-side.png)

## Prefixing the page title with 'Error:'

When a form submission results in a validation failure we need to make it clear to the user that something is wrong.

[The guidance says](https://design-system.service.gov.uk/patterns/validation/#how-to-tell-the-user-about-validation-errors):

> Add ‘Error: ’ to the beginning of the page `<title>` so screen readers read it out as soon as possible

This is a little tricky because we need some logic that can determine whether there's an error on the page, and we set the `<title>` in the document `<head>` before we've rendered the form in the `<body>`.

We can use `content_for` here too to place the title where we need it.

```html
<head>
<%= tag.title(yield(:page_title)) %>
<!-- other head content -->
</head>
```
peteryates marked this conversation as resolved.
Show resolved Hide resolved

We can use the [GOV.UK Components](https://govuk-components.netlify.app/) [title with error prefix helper](https://govuk-components.netlify.app/helpers/title-with-error-prefix/) to add the 'Error:' prefix whenever `@object.errors.any?` is `true`, and pass the resulting string into the `page_title` content:

```ruby
<%
content_for(:page_title) do
title_with_error_prefix(
"How many implements can you juggle with?",
error: @object.errors.any?
)
end
%>
```

## Making sure our check boxes and radio button error links work

The form builder comes with two ways to build lists of check boxes and radio buttons.

The simplest is to use `#govuk_collection_check_boxes` and `#govuk_collection_radio_buttons`, which mimic the behaviour of [their Rails counterparts](https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-collection_checkboxes).

You just pass in the list of options and the form builder will render them. This works for simple lists.

Sometimes, however, the list needs to be customised a little further. For example we might need [a conditionally revealed questions](https://design-system.service.gov.uk/components/checkboxes#conditionally-revealing-a-related-question) or [a divider](https://design-system.service.gov.uk/components/checkboxes/#add-an-option-for-none).

To support this the form builder comes with the more powerful `#govuk_check_boxes_fieldset` and `#govuk_radio_buttons_fieldset` methods which let the developer build the form field by field.

This power comes at a cost. For example you could write something like this. Here the `delete` will only be shown to admins:

```ruby
f.govuk_radio_buttons_fieldset(:close_ticket)) do

if @user.admin?
f.govuk_radio_button(
:close_ticket,
'delete'
)
end

f.govuk_radio_button(
:close_ticket,
'archive'
)

# the rest of the options
end
```

This would make it impossible for the error summary to accurately link to the first check box or radio button without repeating the logic --- an approach that's going to lead to bugs when it's updated in one place but not the other.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does --- render?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Em dash.


Instead, we have to help the error summary by 'marking' the field we want the error summary to link to with `link_error: true`. This overrides the ID generation so the link in the error summary will match it.

```ruby
f.govuk_radio_buttons_fieldset(:close_ticket)) do

if @user.admin?
f.govuk_radio_button(
:close_ticket,
'delete',
link_errors: true
)
end

f.govuk_radio_button(
:close_ticket,
'archive',
link_errors: @user.regular_user?
)

# the rest of the options
end
```

Now when there's a validation error, regardless of whether the user is an admin or not the error summary will link to the first option. It's a good idea to write some tests to ensure future changes don't affect it.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need a concluding paragraph? Something about how these finishing touches will make the world of difference to users or something (trite?) like that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is we'd need a heading to make it clear the summary is for the post rather than for the third tip.

Loading