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 non-broken fallback text when maxlength/maxwords is 0 #2915

Merged
merged 8 commits into from
Nov 3, 2022
103 changes: 64 additions & 39 deletions CHANGELOG.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions app/views/examples/translated/index.njk
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@
hint: {
text: "Peidiwch â chynnwys gwybodaeth bersonol neu ariannol fel eich rhif Yswiriant Gwladol neu fanylion eich cerdyn credyd."
},
fallbackHintText: "Gallwch ddefnyddio hyd at %{count} nod",
textareaDescriptionText: "Gallwch ddefnyddio hyd at %{count} nod",
charactersUnderLimitHtml: {
one: "Mae gennych %{count} nod ar ôl",
two: "Mae gennych %{count} nod ar ôl",
Expand All @@ -163,7 +163,7 @@
classes: "govuk-label--l",
isPageHeading: true
},
fallbackHintText: "Gallwch ddefnyddio hyd at %{count} gair",
textareaDescriptionText: "Gallwch ddefnyddio hyd at %{count} gair",
hint: {
text: "Peidiwch â chynnwys gwybodaeth bersonol neu ariannol fel eich rhif Yswiriant Gwladol neu fanylion eich cerdyn credyd."
},
Expand Down
9 changes: 9 additions & 0 deletions src/govuk/components/character-count/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@
@include govuk-font($size: false, $tabular: true);
margin-top: 0;
margin-bottom: 0;

&:after {
// Zero-width space that will reserve vertical space when no hint is provided
// as:
// - setting a min-height is not possible without a magic number
// because the line-height is set by the `govuk-font` call above
// - using `:empty` is not possible as the hint macro outputs line breaks
content: "\200B";
}
}

.govuk-character-count__message--disabled {
Expand Down
29 changes: 20 additions & 9 deletions src/govuk/components/character-count/character-count.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ var TRANSLATIONS_DEFAULT = {
wordsOverLimit: {
one: 'You have %{count} word too many',
other: 'You have %{count} words too many'
},
textareaDescription: {
other: ''
}
}

Expand Down Expand Up @@ -118,32 +121,39 @@ CharacterCount.prototype.init = function () {
}

var $textarea = this.$textarea
var $fallbackLimitMessage = document.getElementById($textarea.id + '-info')
var $textareaDescription = document.getElementById($textarea.id + '-info')

// Inject a decription for the textarea if none is present already
// for when the component was rendered with no maxlength, maxwords
// nor custom textareaDescriptionText
if ($textareaDescription.textContent.match(/^\s*$/)) {
$textareaDescription.textContent = this.i18n.t('textareaDescription', { count: this.maxLength })
}

// Move the fallback count message to be immediately after the textarea
// Move the textarea description to be immediately after the textarea
// Kept for backwards compatibility
$textarea.insertAdjacentElement('afterend', $fallbackLimitMessage)
$textarea.insertAdjacentElement('afterend', $textareaDescription)

// Create the *screen reader* specific live-updating counter
// This doesn't need any styling classes, as it is never visible
var $screenReaderCountMessage = document.createElement('div')
$screenReaderCountMessage.className = 'govuk-character-count__sr-status govuk-visually-hidden'
$screenReaderCountMessage.setAttribute('aria-live', 'polite')
this.$screenReaderCountMessage = $screenReaderCountMessage
$fallbackLimitMessage.insertAdjacentElement('afterend', $screenReaderCountMessage)
$textareaDescription.insertAdjacentElement('afterend', $screenReaderCountMessage)

// Create our live-updating counter element, copying the classes from the
// fallback element for backwards compatibility as these may have been
// textarea description for backwards compatibility as these may have been
// configured
var $visibleCountMessage = document.createElement('div')
$visibleCountMessage.className = $fallbackLimitMessage.className
$visibleCountMessage.className = $textareaDescription.className
$visibleCountMessage.classList.add('govuk-character-count__status')
$visibleCountMessage.setAttribute('aria-hidden', 'true')
this.$visibleCountMessage = $visibleCountMessage
$fallbackLimitMessage.insertAdjacentElement('afterend', $visibleCountMessage)
$textareaDescription.insertAdjacentElement('afterend', $visibleCountMessage)

// Hide the fallback limit message
$fallbackLimitMessage.classList.add('govuk-visually-hidden')
// Hide the textarea description
$textareaDescription.classList.add('govuk-visually-hidden')

// Remove hard limit if set
$textarea.removeAttribute('maxlength')
Expand Down Expand Up @@ -373,6 +383,7 @@ export default CharacterCount
* @property {PluralisedTranslation} [wordsUnderLimit] - Words under limit
* @property {string} [wordsAtLimit] - Words at limit
* @property {PluralisedTranslation} [wordsOverLimit] - Words over limit
* @property {PluralisedTranslation} [textareaDescription] - Fallback hint
*/

/**
Expand Down
52 changes: 50 additions & 2 deletions src/govuk/components/character-count/character-count.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('Character count', () => {
await page.setJavaScriptEnabled(true)
})

it('shows the fallback message', async () => {
it('shows the textarea description', async () => {
await goToComponent(page, 'character-count')
const message = await page.$eval('.govuk-character-count__message', el => el.innerHTML.trim())

Expand All @@ -55,7 +55,7 @@ describe('Character count', () => {
expect(srMessage).toBeTruthy()
})

it('hides the fallback hint', async () => {
it('hides the textarea description', async () => {
const messageClasses = await page.$eval('.govuk-character-count__message', el => el.className)
expect(messageClasses).toContain('govuk-visually-hidden')
})
Expand Down Expand Up @@ -429,6 +429,32 @@ describe('Character count', () => {
const visibility = await page.$eval('.govuk-character-count__status', el => window.getComputedStyle(el).visibility)
expect(visibility).toEqual('visible')
})

it('configures the description of the textarea', async () => {
// This tests that a description can be provided through JavaScript attributes
// and interpolated with the limit provided to the character count in JS.

await renderAndInitialise(page, 'character-count', {
nunjucksParams:
examples[
'when neither maxlength/maxwords nor textarea description are set'
],
javascriptConfig: {
maxlength: 10,
i18n: {
textareaDescription: {
other: 'No more than %{count} characters'
}
}
}
})

const message = await page.$eval(
'.govuk-character-count__message',
(el) => el.innerHTML.trim()
)
expect(message).toEqual('No more than 10 characters')
})
})

describe('via `initAll`', () => {
Expand Down Expand Up @@ -579,6 +605,28 @@ describe('Character count', () => {
)
expect(message).toEqual('You have 1 word too many')
})

it('interpolates the textarea description in data attributes with the maximum set in JavaScript', async () => {
// This tests that any textarea description provided through data-attributes
// (or the Nunjucks macro), waiting for a maximum to be provided in
// JavaScript config, will lead to the message being injected in the
// element holding the textarea's accessible description
// (and interpolated to replace `%{count}` with the maximum)

await renderAndInitialise(page, 'character-count', {
nunjucksParams:
examples['when neither maxlength nor maxwords are set'],
javascriptConfig: {
maxlength: 10
}
})

const message = await page.$eval(
'.govuk-character-count__message',
(el) => el.innerHTML.trim()
)
expect(message).toEqual('No more than 10 characters')
})
})
})

Expand Down
30 changes: 22 additions & 8 deletions src/govuk/components/character-count/character-count.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ params:
required: false
description: Options for the hint component.
isComponent: true
- name: fallbackHintText
- name: textareaDescriptionText
type: string
required: false
description: Text describing the maximum number of characters you can enter, which is announced to screen readers. The text is displayed as a fallback if the character count JavaScript does not run. Depending on how you configure this component and what parameters you add, instances of `%{count}` are replaced by the value of `maxwords`. If not configured, it uses `maxlength`. By default, fallback text is provided in English.
description: Text describing the maximum number of characters you can enter, which is announced to screen readers. The text is displayed as a fallback if the character count JavaScript does not run. Depending on how you configure this component and what parameters you add, instances of `%{count}` are replaced by the value of `maxwords`. If not configured, it uses `maxlength`. By default, textarea description is provided in English.
- name: errorMessage
type: object
required: false
Expand Down Expand Up @@ -86,13 +86,13 @@ examples:
label:
text: Can you provide more detail?

- name: with custom fallback text
description: with no-js fallback text translated into Welsh
- name: with custom textarea description
description: with textarea description translated into Welsh
data:
name: custom-fallback
id: custom-fallback
name: custom-textarea-description
id: custom-textarea-description
maxlength: 10
fallbackHintText: Gallwch ddefnyddio hyd at %{count} nod
textareaDescriptionText: Gallwch ddefnyddio hyd at %{count} nod

- name: with hint
data:
Expand Down Expand Up @@ -187,7 +187,6 @@ examples:
other: '%{count} words too many'
one: 'One word too many'


# Hidden examples are not shown in the review app, but are used for tests and HTML fixtures
- name: classes
hidden: true
Expand Down Expand Up @@ -290,3 +289,18 @@ examples:
name: address
label:
text: Full address
- name: when neither maxlength nor maxwords are set
hidden: true
data:
id: no-maximum
name: no-maximum
label:
text: Full address
textareaDescriptionText: 'No more than %{count} characters'
- name: when neither maxlength/maxwords nor textarea description are set
hidden: true
data:
id: no-maximum
name: no-maximum
label:
text: Full address
20 changes: 17 additions & 3 deletions src/govuk/components/character-count/template.njk
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@
{% from "../textarea/macro.njk" import govukTextarea %}
{% from "../hint/macro.njk" import govukHint %}

{%- set hasNoLimit = (not params.maxwords and not params.maxlength) %}

<div class="govuk-character-count" data-module="govuk-character-count"
{%- if params.maxlength %} data-maxlength="{{ params.maxlength }}"{% endif %}
{%- if params.threshold %} data-threshold="{{ params.threshold }}"{% endif %}
{%- if params.maxwords %} data-maxwords="{{ params.maxwords }}"{% endif %}
{#
Without maxlength or maxwords, we can't guess if the component will count words or characters.
We can't guess a default textarea description to be interpolated in JavaScript
once the maximum gets configured there.
So we only add the attribute if a textarea description was explicitely provided.
#}
{%- if hasNoLimit and params.textareaDescriptionText %}{{govukPluralisedI18nAttributes('textarea-description', {other: params.textareaDescriptionText})}}{% endif %}
{%- if params.charactersUnderLimitText %}{{govukPluralisedI18nAttributes('characters-under-limit', params.charactersUnderLimitText)}}{% endif %}
{%- if params.charactersAtLimitText %} data-i18n.characters-at-limit="{{ params.charactersAtLimitText | escape}}"{% endif %}
{%- if params.charactersOverLimitText %}{{govukPluralisedI18nAttributes('characters-over-limit', params.charactersOverLimitText)}}{% endif %}
Expand Down Expand Up @@ -34,10 +43,15 @@
errorMessage: params.errorMessage,
attributes: params.attributes
}) }}
{%- set fallbackHintLength = params.maxwords or params.maxlength %}
{%- set fallbackHintDefault = 'You can enter up to %{count} ' + ('words' if params.maxwords else 'characters') %}
{%- set textareaDescriptionLength = params.maxwords or params.maxlength %}
{%- set textareaDescriptionText = params.textareaDescriptionText or 'You can enter up to %{count} ' + ('words' if params.maxwords else 'characters') %}
{#
If the limit is set in JavaScript, we won't be able to interpolate the message
until JavaScript, so we only set a text if the `maxlength` or `maxwords` options
were provided to the macro.
#}
{{ govukHint({
text: (params.fallbackHintText or fallbackHintDefault) | replace('%{count}', fallbackHintLength),
text: ((textareaDescriptionText) | replace('%{count}', textareaDescriptionLength) if not hasNoLimit),
id: params.id + '-info',
classes: 'govuk-character-count__message' + (' ' + params.countMessage.classes if params.countMessage.classes)
}) }}
Expand Down
38 changes: 35 additions & 3 deletions src/govuk/components/character-count/template.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,9 @@ describe('Character count', () => {
})
})

describe('with custom fallback text', () => {
it('allows customisation of the fallback message', () => {
const $ = render('character-count', examples['with custom fallback text'])
describe('with custom textarea description', () => {
it('allows customisation of the textarea description', () => {
const $ = render('character-count', examples['with custom textarea description'])

const message = $('.govuk-character-count__message').text().trim()
expect(message).toEqual('Gallwch ddefnyddio hyd at 10 nod')
Expand Down Expand Up @@ -256,4 +256,36 @@ describe('Character count', () => {
})
})
})

describe('when neither maxlength nor maxwords are set', () => {
describe('with textarea description set', () => {
// If the template has no maxwords or maxlength to go for
// it needs to pass down any textarea description to the JavaScript
// so it can inject the limit it may have received at instantiation
it('renders the textarea description as a data attribute', () => {
const $ = render('character-count', examples['when neither maxlength nor maxwords are set'])

// Fallback hint is passed as data attribute
const $component = $('[data-module]')
expect($component.attr('data-i18n.textarea-description.other')).toEqual('No more than %{count} characters')

// No content is set as the accessible description cannot be interpolated on the backend
// It'll be up to the JavaScript to fill it in
const $countMessage = $('.govuk-character-count__message')
expect($countMessage.html()).toMatch(/^\s*$/) // The macro outputs linebreaks around the hint itself
})
})

describe('without textarea description', () => {
it('does not render a textarea description data attribute', () => {
const $ = render(
'character-count',
examples['when neither maxlength/maxwords nor textarea description are set']
)

const $component = $('[data-module]')
expect($component.attr('data-i18n.textarea-description.other')).toBeFalsy()
})
})
})
})