diff --git a/.gitignore b/.gitignore index f023c5a..8932a4e 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ node_modules *~ *.sublime-project *.sublime-workspace +.DS_STORE # Built javascript all.js diff --git a/README.md b/README.md index d4b7396..0921547 100644 --- a/README.md +++ b/README.md @@ -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) @@ -65,6 +65,7 @@ hmpoRadios(ctx, params) hmpoSelect(ctx, params) hmpoText(ctx, params) hmpoTextarea(ctx, params) +hmpoWordCount(ctx,params) ``` ### Field parameters @@ -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); @@ -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 ``` @@ -113,6 +180,7 @@ currency currencyOrFree url filter +add ``` ### `date` filter diff --git a/components/hmpo-character-count/macro.njk b/components/hmpo-character-count/macro.njk new file mode 100644 index 0000000..48cbe80 --- /dev/null +++ b/components/hmpo-character-count/macro.njk @@ -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 %} \ No newline at end of file diff --git a/components/hmpo-character-count/spec.macro.js b/components/hmpo-character-count/spec.macro.js new file mode 100644 index 0000000..008f88b --- /dev/null +++ b/components/hmpo-character-count/spec.macro.js @@ -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'); + }); + +}); diff --git a/components/hmpo-field/macro.njk b/components/hmpo-field/macro.njk index 52642bc..a908260 100644 --- a/components/hmpo-field/macro.njk +++ b/components/hmpo-field/macro.njk @@ -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 %} diff --git a/components/hmpo-text-count/macro.njk b/components/hmpo-text-count/macro.njk new file mode 100644 index 0000000..3fed8fd --- /dev/null +++ b/components/hmpo-text-count/macro.njk @@ -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 %} \ No newline at end of file diff --git a/components/hmpo-text-count/script.js b/components/hmpo-text-count/script.js new file mode 100644 index 0000000..ad31979 --- /dev/null +++ b/components/hmpo-text-count/script.js @@ -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); diff --git a/components/hmpo-text-count/spec.macro.js b/components/hmpo-text-count/spec.macro.js new file mode 100644 index 0000000..6a8037d --- /dev/null +++ b/components/hmpo-text-count/spec.macro.js @@ -0,0 +1,205 @@ +'use strict'; + +describe('hmpoTextCount', () => { + let locals; + + beforeEach(() => { + locals = { + options: { + fields: { + 'my-input': { + validate: 'required' + } + } + }, + values: { + 'my-input': 'abc123' + } + }; + }); + + it('renders with id', () => { + const $ = render({ component: 'hmpoTextCount', 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: 'hmpoTextCount', 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: 'hmpoTextCount', 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: 'hmpoTextCount', 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: 'hmpoTextCount', 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: 'hmpoTextCount', 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: 'hmpoTextCount', 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: 'hmpoTextCount', params: { id: 'my-input' }, ctx: true }, locals); + const $component = $('.govuk-textarea'); + + expect($component.attr('aria-required')).to.be.undefined; + }); + + it('renders with max-words from validator', () => { + const maxwords = 5; + locals.options.fields['my-input'].validate = [ { type: 'maxwords', arguments: maxwords } ]; + + const $ = render({ component: 'hmpoTextCount', params: { id: 'my-input', type: 'wordcount', maxwords: maxwords }, ctx: true }, locals); + const $component = $('.govuk-character-count__message'); + const countMessage = $component.text(); + + expect(countMessage).to.contain(maxwords); + expect(countMessage).to.contain('words'); + }); + + it('renders with max-words from validator array', () => { + const maxwords = 5; + locals.options.fields['my-input'].validate = [ { type: 'maxwords', arguments: [ maxwords ] } ]; + + const $ = render({ component: 'hmpoTextCount', params: { id: 'my-input', type: 'wordcount', maxwords: maxwords }, ctx: true }, locals); + const $component = $('.govuk-character-count__message'); + const countMessage = $component.text(); + + expect(countMessage).to.contain(maxwords); + expect(countMessage).to.contain('words'); + }); + + it('renders with max-characters from validator', () => { + const maxchars = 5; + locals.options.fields['my-input'].textAreaDescriptionText = 'sadhsahjashjv'; + locals.options.fields['my-input'].validate = [ { type: 'maxlength', arguments: maxchars } ]; + + const $ = render({ component: 'hmpoTextCount', params: { id: 'my-input', maxlength: maxchars }, ctx: true }, locals); + const $component = $('.govuk-character-count__message'); + const countMessage = $component.text(); + + expect(countMessage).to.contain(maxchars); + expect(countMessage).to.contain('characters'); + }); + + it('renders with max-characters from validator array', () => { + const maxchars = 5; + locals.options.fields['my-input'].validate = [ { type: 'maxlength', arguments: [ maxchars ] } ]; + + const $ = render({ component: 'hmpoTextCount', params: { id: 'my-input', maxlength: maxchars }, ctx: true }, locals); + const $component = $('.govuk-character-count__message'); + const countMessage = $component.text(); + + expect(countMessage).to.contain(maxchars); + expect(countMessage).to.contain('characters'); + }); + + it('renders with errorValue if available', () => { + locals.errorValues = { + 'my-input': 'def456' + }; + + const $ = render({ component: 'hmpoTextCount', 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: 'hmpoTextCount', 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: 'hmpoTextCount', 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: 'hmpoTextCount', 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-one-half js-nopaste'); + }); + + it('renders with no extra classes', () => { + const $ = render({ component: 'hmpoTextCount', 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-one-half'); + }); + + it('renders with extra classes', () => { + const $ = render({ component: 'hmpoTextCount', 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: 'hmpoTextCount', 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'); + }); + + it('renders with noPaste set to false', () => { + const $ = render({ component: 'hmpoTextCount', params: { id: 'my-input', isPageHeading: true, noPaste: false }, ctx: true }, locals); + const $label = $('.govuk-textarea'); + + expect($label.attr('class')).to.equal('govuk-textarea govuk-js-character-count govuk-!-width-one-half'); + }); + +}); diff --git a/components/hmpo-word-count/macro.njk b/components/hmpo-word-count/macro.njk new file mode 100644 index 0000000..c431db4 --- /dev/null +++ b/components/hmpo-word-count/macro.njk @@ -0,0 +1,7 @@ +{% macro hmpoWordCount(ctx, params, base) %} + {%- from "../hmpo-text-count/macro.njk" import hmpoTextCount %} + {{- hmpoTextCount(ctx, params, { + type: "wordcount", + classes: "govuk-!-width-three-quarters" + }, base) }} +{% endmacro %} \ No newline at end of file diff --git a/components/hmpo-word-count/spec.macro.js b/components/hmpo-word-count/spec.macro.js new file mode 100644 index 0000000..c23227c --- /dev/null +++ b/components/hmpo-word-count/spec.macro.js @@ -0,0 +1,175 @@ +'use strict'; + +describe('hmpoWordCount', () => { + let locals; + + beforeEach(() => { + locals = { + options: { + fields: { + 'my-input': { + validate: 'required' + } + } + }, + values: { + 'my-input': 'abc123' + } + }; + }); + + it('renders with id', () => { + const $ = render({ component: 'hmpoWordCount', 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: 'hmpoWordCount', 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: 'hmpoWordCount', 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: 'hmpoWordCount', 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: 'hmpoWordCount', 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: 'hmpoWordCount', 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: 'hmpoWordCount', 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: 'hmpoWordCount', params: { id: 'my-input' }, ctx: true }, locals); + const $component = $('.govuk-textarea'); + + expect($component.attr('aria-required')).to.be.undefined; + }); + + it('renders with max-words from validator', () => { + const maxwords = 5; + locals.options.fields['my-input'].validate = [ { type: 'maxwords', arguments: maxwords } ]; + + const $ = render({ component: 'hmpoWordCount', params: { id: 'my-input', maxwords: maxwords }, ctx: true }, locals); + const $component = $('.govuk-character-count__message'); + const countMessage = $component.text(); + + expect(countMessage).to.contain(maxwords); + expect(countMessage).to.contain('words'); + }); + + it('renders with max-words from validator array', () => { + const maxwords = 5; + locals.options.fields['my-input'].validate = [ { type: 'maxwords', arguments: [ maxwords ] } ]; + + const $ = render({ component: 'hmpoWordCount', params: { id: 'my-input', maxwords: maxwords }, ctx: true }, locals); + const $component = $('.govuk-character-count__message'); + const countMessage = $component.text(); + + expect(countMessage).to.contain(maxwords); + expect(countMessage).to.contain('words'); + }); + + + it('renders with errorValue if available', () => { + locals.errorValues = { + 'my-input': 'def456' + }; + + const $ = render({ component: 'hmpoWordCount', 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: 'hmpoWordCount', 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: 'hmpoWordCount', 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: 'hmpoWordCount', 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: 'hmpoWordCount', 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: 'hmpoWordCount', 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: 'hmpoWordCount', 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'); + }); + +}); diff --git a/lib/globals.js b/lib/globals.js index f8b15a9..a01d53f 100644 --- a/lib/globals.js +++ b/lib/globals.js @@ -193,6 +193,15 @@ let globals = { return options; }, + hmpoTranslateExtraFieldContent(ctx, params, fieldKey, optional = false) { + let translate = ctx('translate'); + let contentKey = 'fields.' + (params.contentKey || params.id); + let key = contentKey + '.' + fieldKey; + const translation = translate(key, { self: !optional }); + debug('hmpoTranslateExtraFieldContent', params, fieldKey, translation); + return translation == `[${key}]`? undefined: translation; + }, + hmpoGetValue(ctx, params) { let errorValue = ctx('errorValues.' + params.id); return errorValue !== undefined ? errorValue :ctx('values.' + params.id); diff --git a/test/lib/spec.globals.js b/test/lib/spec.globals.js index 0bc2df4..dca3a04 100644 --- a/test/lib/spec.globals.js +++ b/test/lib/spec.globals.js @@ -143,6 +143,32 @@ describe('Globals', () => { }); }); + describe('hmpoTranslateExtraFieldContent returns translation when key exists', () => { + const context = sinon.stub(); + context.withArgs('translate').returns((key) => { + return key; + }); + const fieldKey = 'test'; + const params = {id: 'id'}; + + const translation = globals.globals.hmpoTranslateExtraFieldContent(context, params, fieldKey); + + translation.should.equal('fields.id.test'); + }); + + describe('hmpoTranslateExtraFieldContent returns undefined when locale doesn\'t exist', () => { + const context = sinon.stub(); + + context.withArgs('translate').returns((key) => { + return `[${key}]`; + }); + const fieldKey = 'test'; + const params = {id: 'id'}; + + const translation = globals.globals.hmpoTranslateExtraFieldContent(context, params, fieldKey); + + expect(translation).to.be.undefined; + }); }); });