Skip to content

Commit

Permalink
FOR-469: Add word and character counter components
Browse files Browse the repository at this point in the history
  • Loading branch information
SamChatfield committed Oct 28, 2024
1 parent c0a8d87 commit 4680291
Show file tree
Hide file tree
Showing 12 changed files with 828 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ node_modules
*~
*.sublime-project
*.sublime-workspace
.DS_STORE

# Built javascript
all.js
72 changes: 70 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ hmpoForm(ctx, params)
hmpoErrorGroup(ctx, params)
hmpoAutoSubmit(ctx, params)
hmpoSubmit(ctx, params)
hmpoCharacterCount(ctx, params)
hmpoCheckboxes(ctx, params);
hmpoDate(ctx, params)
hmpoNumber(ctx, params)
Expand All @@ -65,6 +65,7 @@ hmpoRadios(ctx, params)
hmpoSelect(ctx, params)
hmpoText(ctx, params)
hmpoTextarea(ctx, params)
hmpoWordCount(ctx,params)
```

### Field parameters
Expand All @@ -79,7 +80,6 @@ Label, hint, and legend text is loaded from localisation using a default key str

### Other available components:
```
hmpoCharsLeft(params);
hmpoCircleStep(params);
hmpoCircleStepList(params);
hmpoClose(params);
Expand All @@ -93,11 +93,78 @@ hmpoSidebar(params)
hmpoWarningText(params)
```

## Deprecated form wizard components
```
hmpoCharsLeft(ctx, params)
```

### Helper and formatting components:
```
hmpoHtml(obj)
```

## Using hmpoCharacterCount and hmpoWordCount
hmpoCharacterCount will be replacing hmpoCharsLeft however for backwards compatability it will still be remaining. When using hmpoCharacterCount you will need specify a maxlength validator for the component in fields.js whereas for hmpoWordCount you will need to specify a maxwords validator. An example can be found below.

```
'my-character-count': {
...
validate: [
...,
{ type: 'maxlength', arguments: 10 }
]
},
'my-word-count': {
...
validate: [
...,
{ type: 'maxwords', arguments: 10 }
]
},
```
You may also want to add a translation for the component and that can be found below. You will need to keep %{count} as this is used by the govuk frontend component to parse the character/word count:

```
"my-character-count": {
...
"maxlength": "You can only enter up to {{maxlength}} characters" - required by default
(The keys bellow will allow translation of the hint text. %{count} is parsed by gds to show dynamic count)
"textareaDescriptionText": "Enter up to %{count} characters" - shown, instead of dynamic count, to the user if javascript is disabled,
"charactersUnderLimitText": {
"one": "you have one char left" - shown when user has one characters left
"other": "you have %{count} characters left" - shown when user has n characters left
} - shown to user when they have n characters remaining
"charactersAtLimitText": "you have 0 characters remaining" - shown when user has no characters left
"charactersOverLimitText": {
"one": "you have entered 1 character too many " - shown when user has one character over the limit
"other": "you have %{count} characters too many" - shown when user has n. characters over the limit
} - shown to user when they have exceed number of allowed characters
},
"my-word-count": {
...
"maxlength": "You can only enter up to {{maxlength}} words" - required by default
(The keys bellow will allow translation of the hint text. %{count} is parsed by gds to show dynamic count)
"textareaDescriptionText": "Enter up to %{count} chars" - shown, instead of dynamic count, to the user if javascript is disabled,
"wordUnderLimitText": {
"one": "you have one word left" - shown when user has one word left
"other": "you have %{count} wrods left" - shown when user has n words left
} - shown to user when they have n words remaining
"wordsAtLimitText": "you have 0 words remaining" - shown when user has no words left
"wordsOverLimitText": {
"one": "you have entered one word too many " - shown when user has one word over the limit
"other": "you have %{count} words too many" - shown when user has n. words over the limit
} - shown to user when they have exceed number of allowed words
}
```

## Filters

```
Expand All @@ -113,6 +180,7 @@ currency
currencyOrFree
url
filter
add
```

### `date` filter
Expand Down
7 changes: 7 additions & 0 deletions components/hmpo-character-count/macro.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% macro hmpoCharacterCount(ctx, params, base) %}
{%- from "../hmpo-text-count/macro.njk" import hmpoTextCount %}
{{- hmpoTextCount(ctx, params, {
type: "charactercount",
classes: "govuk-!-width-three-quarters"
}, base) }}
{% endmacro %}
175 changes: 175 additions & 0 deletions components/hmpo-character-count/spec.macro.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
'use strict';

describe('hmpoCharacterCount', () => {
let locals;

beforeEach(() => {
locals = {
options: {
fields: {
'my-input': {
validate: 'required'
}
}
},
values: {
'my-input': 'abc123'
}
};
});

it('renders with id', () => {
const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input' }, ctx: true }, locals);
const $component = $('.govuk-textarea');

expect($component.attr('id')).to.equal('my-input');
});

it('renders with label and hint', () => {
const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input' }, ctx: true }, locals);
const $label = $('.govuk-label');
const $hint = $('.govuk-hint');

expect($label.text().trim()).to.equal('[fields.my-input.label]');
expect($label.attr('id')).to.equal('my-input-label');
expect($hint.text().trim()).to.equal('[fields.my-input.hint]');
});

it('does not render extra hint if there is no localisatio, but will render character count hint', () => {
locals.translate = sinon.stub();
locals.translate.returnsArg(0);
locals.translate.withArgs('fields.my-input.hint').returns(undefined);

const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input' }, ctx: true }, locals);
const $label = $('.govuk-label');
const $hint = $('.govuk-hint');

expect($label.text().trim()).to.equal('fields.my-input.label');
expect($label.attr('id')).to.equal('my-input-label');
expect($hint.length).to.equal(1);
});

it('renders with value', () => {
const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input' }, ctx: true }, locals);
const $component = $('.govuk-textarea');

expect($component.text()).to.equal('abc123');
});

it('renders with aria-required=false if validator is not required', () => {
locals.options.fields['my-input'].validate = undefined;

const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input' }, ctx: true }, locals);
const $component = $('.govuk-textarea');

expect($component.attr('aria-required')).to.equal('false');
});

it('renders with no aria-required if validator is required', () => {
const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input' }, ctx: true }, locals);
const $component = $('.govuk-textarea');

expect($component.attr('aria-required')).to.be.undefined;
});

it('renders with no aria-required if validators contains required', () => {
locals.options.fields['my-input'].validate = [ 'required' ];

const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input' }, ctx: true }, locals);
const $component = $('.govuk-textarea');

expect($component.attr('aria-required')).to.be.undefined;
});

it('renders with no aria-required if validators contains required validator object', () => {
locals.options.fields['my-input'].validate = [ { type: 'required' } ];

const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input' }, ctx: true }, locals);
const $component = $('.govuk-textarea');

expect($component.attr('aria-required')).to.be.undefined;
});

it('renders with max-characters from validator', () => {
const maxcharacters = 5;
locals.options.fields['my-input'].validate = [ { type: 'maxlength', arguments: maxcharacters } ];

const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input', maxlength: maxcharacters }, ctx: true }, locals);
const $component = $('.govuk-character-count__message');
const countMessage = $component.text();

expect(countMessage).to.contain(maxcharacters);
expect(countMessage).to.contain('characters');
});

it('renders with max-characters from validator array', () => {
const maxcharacters = 5;
locals.options.fields['my-input'].validate = [ { type: 'maxlength', arguments: [ maxcharacters ] } ];

const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input', maxlength: maxcharacters }, ctx: true }, locals);
const $component = $('.govuk-character-count__message');
const countMessage = $component.text();

expect(countMessage).to.contain(maxcharacters);
expect(countMessage).to.contain('characters');
});


it('renders with errorValue if available', () => {
locals.errorValues = {
'my-input': 'def456'
};

const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input' }, ctx: true }, locals);
const $component = $('.govuk-textarea');

expect($component.text()).to.equal('def456');
});

it('renders error message if available', () => {
locals.errors = {
'my-input': { key: 'my-input', type: 'validator' }
};

const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input' }, ctx: true }, locals);
const $component = $('#my-input-error');

expect($component.text().trim()).to.equal('[govuk.error]: [fields.my-input.validation.validator]');
});

it('renders label as header', () => {
const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input', isPageHeading: true }, ctx: true }, locals);
const $label = $('h1 .govuk-label');

expect($label.attr('class')).to.equal('govuk-label govuk-label--l');
});

it('renders with nopaste', () => {
const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input', isPageHeading: true, noPaste: true }, ctx: true }, locals);
const $label = $('.govuk-textarea');

expect($label.attr('class')).to.equal('govuk-textarea govuk-js-character-count govuk-!-width-three-quarters js-nopaste');
});

it('renders with no extra classes', () => {
const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input', isPageHeading: true }, ctx: true }, locals);
const $label = $('.govuk-textarea');

expect($label.attr('class')).to.equal('govuk-textarea govuk-js-character-count govuk-!-width-three-quarters');
});

it('renders with extra classes', () => {
const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input', isPageHeading: true, classes: 'test' }, ctx: true }, locals);
const $label = $('.govuk-textarea');

expect($label.attr('class')).to.equal('govuk-textarea govuk-js-character-count test');
});

it('renders with extra classes and noPaste', () => {
const $ = render({ component: 'hmpoCharacterCount', params: { id: 'my-input', isPageHeading: true, classes: 'test', noPaste: true }, ctx: true }, locals);
const $label = $('.govuk-textarea');

expect($label.attr('class')).to.equal('govuk-textarea govuk-js-character-count test js-nopaste');
});

});
6 changes: 6 additions & 0 deletions components/hmpo-field/macro.njk
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
{%- elif field.type == "group" %}
{% from "../hmpo-error-group/macro.njk" import hmpoErrorGroup %}
{% set component = hmpoErrorGroup %}
{%- elif field.type == "charactercount"%}
{% from "../hmpo-character-count/macro.njk" import hmpoCharacterCount %}
{% set component = hmpoCharacterCount %}
{%- elif field.type == "wordcount"%}
{% from "../hmpo-word-count/macro.njk" import hmpoWordCount %}
{% set component = hmpoWordCount %}
{%- endif %}

{%- if component %}
Expand Down
79 changes: 79 additions & 0 deletions components/hmpo-text-count/macro.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{% macro hmpoTextCount(ctx, params, base) %}
{%- set params = hmpoGetParams(ctx, params, base) %}

{%- set pageHeading = {
isPageHeading: true,
classes: "govuk-label--l"
} if params.isPageHeading %}
{%- set args = {
id: params.id,
name: params.id,
label: merge(
pageHeading,
{ attributes: { id: params.id + "-label" } },
hmpoGetOptions(ctx, params, "label")
),
spellcheck: hmpoGetOptions(ctx, params, "spellcheck", true),
threshold: params.threshold,
hint: hmpoGetOptions(ctx, params, "hint", true),
value: hmpoGetValue(ctx, params),
errorMessage: hmpoGetError(ctx, params),
inputmode: params.inputmode,
countMessage: params.countMessage,
classes: "" + (params.classes if params.classes else "govuk-!-width-one-half") + (" js-nopaste" if params.noPaste),
formGroup: params.formGroup,
autocomplete: params.autocomplete,
rows: params.rows,
attributes: hmpoGetAttributes(ctx, params, {
"aria-required": hmpoGetValidatorAttribute(ctx, params, "required", null, false)
} | filter(null))
} %}

{% if params.type == 'wordcount' %}

{%- set args = args | add("maxwords", hmpoGetValidatorAttribute(ctx, params, "maxwords", 0)) %}
{%- set wordsUnderLimitText = hmpoTranslateExtraFieldContent(ctx, params, "wordsUnderLimitText", true) %}
{%- set wordsAtLimitText = hmpoTranslateExtraFieldContent(ctx, params, "wordsAtLimitText", true) %}
{%- set wordsOverLimitText = hmpoTranslateExtraFieldContent(ctx, params, "wordsOverLimitText", true) %}

{%- if wordsUnderLimitText != undefined %}
{%- set args = args | add("wordsUnderLimitText", wordsUnderLimitText) %}
{% endif %}

{%- if wordsAtLimitText != undefined %}
{%- set args = args | add("wordsAtLimitText", wordsAtLimitText) %}
{% endif %}

{%- if wordsOverLimitText != undefined %}
{%- set args = args | add("wordsOverLimitText", wordsOverLimitText) %}
{% endif %}

{%- else %}

{%- set args = args | add("maxlength", hmpoGetValidatorAttribute(ctx, params, "maxlength", 0)) %}
{%- set charactersUnderLimitText = hmpoTranslateExtraFieldContent(ctx, params, "charactersUnderLimitText", true) %}
{%- set charactersAtLimitText = hmpoTranslateExtraFieldContent(ctx, params, "charactersAtLimitText", true) %}
{%- set charactersOverLimitText = hmpoTranslateExtraFieldContent(ctx, params, "charactersOverLimitText", true) %}

{%- if charactersUnderLimitText != undefined %}
{%- set args = args | add("charactersUnderLimitText", charactersUnderLimitText) %}
{% endif %}

{%- if charactersAtLimitText != undefined %}
{%- set args = args | add("charactersAtLimitText", charactersAtLimitText) %}
{% endif %}

{%- if charactersOverLimitText != undefined %}
{%- set args = args | add("charactersOverLimitText", charactersOverLimitText) %}
{% endif %}

{% endif %}

{%- set textareaDescriptionText = hmpoTranslateExtraFieldContent(ctx, params, "textareaDescriptionText", true) %}
{%- if textareaDescriptionText != undefined %}
{%- set args = args | add("textareaDescriptionText", textareaDescriptionText) %}
{% endif %}

{%- from "govuk/components/character-count/macro.njk" import govukCharacterCount %}
{{- govukCharacterCount(args) }}
{% endmacro %}
Loading

0 comments on commit 4680291

Please sign in to comment.