Skip to content

Commit

Permalink
Add tabindex and focus states to code blocks
Browse files Browse the repository at this point in the history
This is required for accessibility reasons. Because some code blocks
have scrollbars they need to be focusable using a keyboard.

I've copied the styles from the design system's documentation (see the
code blocks in the HTML / Nunjucks tabs here: https://design-system.service.gov.uk/components/button/)

The implementation is simple when syntax highlighting is not enabled,
because we can simply implement `block_code` to surround the code with
the markup we want.

When syntax highlighting is enabled it's a bit more complicated.
Middleman already `include`s its own implementation of `block_code`,
which renders the source code highlighted with spans. This method
doesn't look like it would be easy to customise, so instead I've
resorted to post-processing the HTML (i.e. replacing the attributes on
the `<pre>` tag using a regex).

Testing this was a bit tricky, because you need to monkey patch the
class to include the `block_code` method. I had to clone the class and
patch the clone to do avoid affecting other tests.
  • Loading branch information
richardTowers committed Mar 11, 2021
1 parent 3de782e commit 67994d8
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 0 deletions.
9 changes: 9 additions & 0 deletions lib/assets/stylesheets/modules/_technical-documentation.scss
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@
border: 1px solid $code-02;
}

pre.code-block {
border: $govuk-focus-width solid transparent;

&:focus {
border: $govuk-focus-width solid $govuk-focus-text-colour;
outline: $govuk-focus-width solid $govuk-focus-colour;
}
}

pre code {
background: none;
color: inherit;
Expand Down
19 changes: 19 additions & 0 deletions lib/govuk_tech_docs/tech_docs_html_renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,24 @@ def table_row(body)
body_with_row_headers = body.gsub(/<td># ([^<]*)<\/td>/, "<th scope=\"row\">\\1</th>")
"<tr>#{body_with_row_headers}</tr>"
end

def block_code(text, lang)
if defined?(super)
# Post-processing the block_code HTML to implement tabbable code blocks.
#
# Middleman monkey patches the Middleman::Renderers::MiddlemanRedcarpetHTML
# to include Middleman::Syntax::RedcarpetCodeRenderer. This defines its own
# version of `block_code(text, lang)` which we can call with `super`.

highlighted_html = super
highlighted_html
.sub(/<pre class=" *([^"]*)">/, '<pre class="code-block \1" tabindex="0">')
else
# If syntax highlighting with redcarpet isn't enabled, super will not
# be `defined?`, so we can jump straight to rendering HTML ourselves.

"<pre class=\"code-block\" tabindex=\"0\"><code class=\"#{lang}\">#{text}</code></pre>"
end
end
end
end
68 changes: 68 additions & 0 deletions spec/govuk_tech_docs/tech_docs_html_renderer_spec.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require "middleman-syntax/extension"

RSpec.describe GovukTechDocs::TechDocsHTMLRenderer do
describe "#render a table" do
it "renders tables with row headings" do
Expand Down Expand Up @@ -33,3 +35,69 @@
end
end
end

RSpec.describe "code blocks" do
let(:app) { double("app") }
let(:context) { double("context") }

before :each do
allow(context).to receive(:app) { app }
allow(app).to receive(:api)
end

describe "without syntax highlighting" do
let(:processor) {
Redcarpet::Markdown.new(GovukTechDocs::TechDocsHTMLRenderer.new(context: context), fenced_code_blocks: true)
}

describe "#render" do
it "sets tab index to 0" do
output = processor.render <<~MARKDOWN
Hello world:
```ruby
def hello_world
puts "hello world"
end
```
MARKDOWN

expect(output).to include('<pre class="code-block" tabindex="0">')
expect(output).to include("def hello_world")
expect(output).to include('puts "hello world"')
expect(output).to include("end")
end
end
end

describe "with syntax highlighting" do
let(:processor) {
renderer_class = GovukTechDocs::TechDocsHTMLRenderer.clone.tap do |c|
c.send :include, Middleman::Syntax::RedcarpetCodeRenderer
end

Redcarpet::Markdown.new(renderer_class.new(context: context), fenced_code_blocks: true)
}

describe "#render" do
it "sets tab index to 0" do
output = processor.render <<~MARKDOWN
Hello world:
```ruby
def hello_world
puts "hello world"
end
```
MARKDOWN

expect(output).to include('<pre class="code-block ruby" tabindex="0">')
expect(output).to include("def</span>")
expect(output).to include("hello_world</span>")
expect(output).to include("puts</span>")
expect(output).to include('"hello world"</span>')
expect(output).to include("end</span>")
end
end
end
end

0 comments on commit 67994d8

Please sign in to comment.