Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion .github/workflows/pa11y/wcag2aa.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
const urls = require('./urls.json')

// Pages where axe reports a false-positive color-contrast issue: the NHS radio/checkbox
// control is drawn with a ::before/::after pseudo element on the label, so axe cannot
// determine the label's background and bails out. The label text itself passes AA.
// Revisit this ignore whenever the nhsuk/govuk frontend gems are bumped (see the tracking
// ticket) in case a newer axe/component version resolves it.
const contrastFalsePositivePaths = [
'/assistants/new',
'/components/checkboxes',
'/components/radios'
]

const urlsWithIgnores = urls.map((url) =>
contrastFalsePositivePaths.some((path) => url.endsWith(path))
? { url, ignore: ['color-contrast'] }
: url
)

module.exports = {
defaults: {
chromeLaunchConfig: {
Expand All @@ -15,5 +32,5 @@ module.exports = {
],
standard: 'WCAG2AA'
},
urls
urls: urlsWithIgnores
}
19 changes: 18 additions & 1 deletion .github/workflows/pa11y/wcag2aaa.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
const urls = require('./urls.json')

// Pages where axe reports a false-positive contrast issue: the NHS radio/checkbox control
// is drawn with a ::before/::after pseudo element on the label, so axe cannot determine the
// label's background and bails out. The label text itself passes. Revisit this ignore
// whenever the nhsuk/govuk frontend gems are bumped (see the tracking ticket) in case a
// newer axe/component version resolves it.
const contrastFalsePositivePaths = [
'/assistants/new',
'/components/checkboxes',
'/components/radios'
]

const urlsWithIgnores = urls.map((url) =>
contrastFalsePositivePaths.some((path) => url.endsWith(path))
? { url, ignore: ['color-contrast', 'color-contrast-enhanced'] }
: url
)

module.exports = {
defaults: {
chromeLaunchConfig: {
Expand All @@ -15,5 +32,5 @@ module.exports = {
],
standard: 'WCAG2AAA'
},
urls
urls: urlsWithIgnores
}
7 changes: 4 additions & 3 deletions lib/design_system/generic/builders/pagination_renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@ def link_with_title(text, target, title, icon, aria_label = nil)
'aria-label': aria_label)
end

def disabled_link_with_title(text, title, aria_label = nil)
def disabled_link_with_title(text, title, _aria_label = nil)
# No aria-label here: it is prohibited on a role-less <span>. The visible
# title plus the visually-hidden text already provide an accessible name.
tag(:span,
tag(:span, title, class: "#{brand}-pagination__title") +
tag(:span, text, class: "#{brand}-u-visually-hidden"),
class: "#{brand}-pagination__link disabled",
'aria-label': aria_label)
class: "#{brand}-pagination__link disabled")
end

# SVG for the "Previous" icon
Expand Down
56 changes: 56 additions & 0 deletions test/dummy/app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ def component_preview(key = nil, html: nil, fragment: nil, &block)
html = extract_html_fragment(html, fragment) if fragment
pretty_html = pretty_print(html)

# The rendered preview is what pa11y/axe scans. Because a page can preview the same
# component more than once, prefix element ids (and their references) per preview so the
# live DOM stays free of duplicate ids. pretty_html above keeps the original ids so the
# displayed code sample is unchanged.
html = uniquify_ids(html, id)

safe_buffer << render_input(display_source, id)
safe_buffer << render_output(html, pretty_html, id)
safe_buffer
Expand Down Expand Up @@ -85,6 +91,56 @@ def pretty_print(html)
fragment.children.map { |child| child.to_xml(indent: 2) }.join("\n")
end

# Prefix every element id in +html+ with +prefix+ and update the attributes that reference
# those ids, so labels and aria-* associations keep resolving. Keeps a page free of duplicate
# ids when the same component is previewed more than once.
def uniquify_ids(html, prefix)
doc = Nokogiri::HTML.fragment(html)
id_map = prefix_element_ids(doc, prefix)
return html if id_map.empty?

remap_single_idrefs(doc, id_map)
remap_idref_lists(doc, id_map)
remap_anchor_hrefs(doc, id_map)

# Returned as a plain String; render_output marks it html_safe when it embeds the preview.
doc.to_html
end

# Rewrite every id to be prefixed and return { old_id => new_id }.
def prefix_element_ids(doc, prefix)
doc.css('[id]').each_with_object({}) do |node, map|
new_id = "#{prefix}-#{node['id']}"
map[node['id']] = new_id
node['id'] = new_id
end
end

# Single-idref attributes: the whole value is one id (a demo value like "Jacket potato"
# can even contain spaces), so map it as a whole rather than splitting.
def remap_single_idrefs(doc, id_map)
Comment thread
FilisLiu marked this conversation as resolved.
%w[for list aria-activedescendant].each do |attr|
doc.css("[#{attr}]").each { |node| node[attr] = id_map[node[attr]] || node[attr] }
end
end

# Space-separated idref lists (aria-labelledby, aria-describedby, ...).
def remap_idref_lists(doc, id_map)
%w[aria-labelledby aria-describedby aria-controls aria-owns aria-details headers].each do |attr|
doc.css("[#{attr}]").each do |node|
node[attr] = node[attr].split(/\s+/).map { |ref| id_map[ref] || ref }.join(' ')
end
end
end

# In-page anchors (e.g. tab links, whose aria-controls is derived from the href by JS).
def remap_anchor_hrefs(doc, id_map)
doc.css('a[href^="#"]').each do |node|
target = node['href'].delete_prefix('#')
node['href'] = "##{id_map[target]}" if id_map.key?(target)
end
end

def render_input(display_source, id)
ds_heading('Input', level: 4) +
ds_tab do |tab|
Expand Down
4 changes: 4 additions & 0 deletions test/dummy/app/models/assistant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ class Assistant < ApplicationRecord
Colour.new('blue', 'Blue', 'Violets are... purple?')
].freeze

# Virtual attribute used only to demo ds_hidden_field with show_text, so it has its own
# element id instead of colliding with the :title text field on the same form.
attr_accessor :reference_code

serialize :desired_filling, coder: YAML

# Rails now adds presence validation to associations automatically but usually govuk-form-builder set relationships by assigning values to the foreign key column.
Expand Down
2 changes: 1 addition & 1 deletion test/dummy/app/views/assistants/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<%= form.ds_date_field :date_of_birth, hint: "Demo for ds_date_field", date_of_birth: true, legend: { size: nil }%>
<% end %>

<%= form.ds_hidden_field :title, value: 'You should not see this', show_text: "This is a readonly text field", label: { size: 'm', text: 'This is a label for a hidden field' } %>
<%= form.ds_hidden_field :reference_code, value: 'You should not see this', show_text: "This is a readonly text field", label: { size: 'm', text: 'This is a label for a hidden field' } %>

<%= form.ds_field_set_tag "Checkboxes" do %>
<%= form.ds_collection_check_boxes :desired_filling, Assistant::FILLINGS, :id, :title, hint_method: :description, hint: "Demo for ds_collection_check_boxes" %>
Expand Down
6 changes: 5 additions & 1 deletion test/nhsuk/builders/pagination_renderer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ class PaginationRendererTest < ActionView::TestCase
assert_select('nav.nhsuk-pagination') do
assert_select('ul.nhsuk-list.nhsuk-pagination__list') do
assert_select('li.nhsuk-pagination-item--previous') do
assert_select '[aria-label=?]', 'Previous Page' do
# Disabled prev/next is a role-less <span>, so it carries no aria-label
# (prohibited there); its accessible name comes from the title text.
assert_select 'span.nhsuk-pagination__link.disabled' do
assert_select 'span.nhsuk-pagination__title', text: 'Previous'
end
Comment thread
shilpigoeldev marked this conversation as resolved.
# Regression guard: the disabled control must not carry an aria-label.
assert_select 'span.nhsuk-pagination__link.disabled[aria-label]', false
end
end
end
Expand Down
Loading