Skip to content

Commit

Permalink
FOR-469: Add character and word count components
Browse files Browse the repository at this point in the history
* Update git ignore to ignore ds_store
* Add hmpo-text-counter as base for new word and character counter; it implements gds character counter
* Add hmpo-word-counter to count words, based on hmpo-text-counter
* Add hmpo-character-counter to count characters, based on hmpo-text-counter
* Add unit tests for each component
* Add new global function to translate extra field keys
* Add unit test for new global function
  • Loading branch information
Rhyad-Zergane-HO committed Oct 25, 2024
1 parent bf69b2f commit ba60064
Show file tree
Hide file tree
Showing 10 changed files with 728 additions and 0 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
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 %}
167 changes: 167 additions & 0 deletions components/hmpo-character-count/spec.macro.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
'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');
expect($label.text().trim()).to.equal('[fields.my-input.label]');
expect($label.attr('id')).to.equal('my-input-label');
const $hint = $('.govuk-hint');
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');
expect($label.text().trim()).to.equal('fields.my-input.label');
expect($label.attr('id')).to.equal('my-input-label');
const $hint = $('.govuk-hint');
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');
});

});
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 %}
68 changes: 68 additions & 0 deletions components/hmpo-text-count/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
(function (scope, window) {

documentReady(noPaste);

function documentReady(callback) {
addEvent(document, 'DOMContentLoaded', callback);
addEvent(window, 'load', callback);
}

function each(a, cb) {
a = [].slice.call(a);
for (var i = 0; i < a.length; i++) cb(a[i], i, a);
}


var prevent = function (e) {
e.preventDefault ? e.preventDefault() : e.returnValue = true;
return false;
};

function hasClass(el, className) {
return el.className.split(/\s/).indexOf(className) !== -1;
}

function getElementsByClass(parent, tag, className) {
if (parent.getElementsByClassName) {
return parent.getElementsByClassName(className);
} else {
var elems = [];
each(parent.getElementsByTagName(tag), function (t) {
if (hasClass(t, className)) {
elems.push(t);
}
});
return elems;
}
}

function noPaste() {
var elements = getElementsByClass(document, ['input'], 'js-nopaste');
each(elements, function (element) {
once(element, 'js-nopaste', function () {
addEvent(element, 'paste', prevent);
addEvent(element, 'dragdrop', prevent);
addEvent(element, 'drop', prevent);
});
});
}

function once(elem, key, callback) {
if (!elem) {
return;
}
elem.started = elem.started || {};
if (!elem.started[key]) {
elem.started[key] = true;
callback(elem);
}
}

function addEvent(el, type, callback) {
if (el.addEventListener) {
el.addEventListener(type, callback, false);
} else if (el.attachEvent) {
el.attachEvent('on' + type, callback);
}
}
})(document, window);
Loading

0 comments on commit ba60064

Please sign in to comment.