Skip to content

Commit

Permalink
Merge pull request #2915 from alphagov/character-count-no-maxwords-ma…
Browse files Browse the repository at this point in the history
…xlength
  • Loading branch information
romaricpascal authored Nov 3, 2022
2 parents 4c915e3 + a546fb2 commit 41da3f7
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 66 deletions.
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()
})
})
})
})

0 comments on commit 41da3f7

Please sign in to comment.