Skip to content

Commit

Permalink
feat: [ASL-4632] Word count component (#352)
Browse files Browse the repository at this point in the history
  • Loading branch information
kiran-varma-home-office authored Nov 25, 2024
1 parent 2ff068c commit 1f81475
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 4 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ukhomeoffice/asl-components",
"version": "13.6.3",
"version": "13.6.4",
"description": "React components for ASL layouts and elements",
"main": "src/index.jsx",
"styles": "styles/index.scss",
Expand Down
4 changes: 3 additions & 1 deletion src/fieldset/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
MultiInput,
DurationField,
SelectMany,
Inset
Inset,
TextAreaWithWordCount
} from '../';

function getLabel(opt, name, type = 'label') {
Expand Down Expand Up @@ -70,6 +71,7 @@ const fields = {
declaration: props => <ApplicationConfirm { ...props } />,
inputDate: props => <DateInput { ...props } onChange={value => props.onChange({ target: { value } })} />,
textarea: props => <TextArea { ...omit(props, ['meta']) } autoExpand={true} />,
textAreaWithWordCount: props => <TextAreaWithWordCount { ...omit(props, ['meta']) } />,
radioGroup: props => {
if (!props.options) {
throw new Error(`radioGroup '${props.name}' has undefined options`);
Expand Down
1 change: 1 addition & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,6 @@ export { default as Tasklist } from './tasklist';
export { default as TrainingSummary } from './training-summary';
export { default as WidthContainer } from './width-container';
export { default as Wrapper } from './wrapper';
export { default as TextAreaWithWordCount } from './text-area-word-count';

export * from './layouts';
29 changes: 29 additions & 0 deletions src/text-area-word-count/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { useState } from 'react';
import { TextArea } from '@ukhomeoffice/react-components';
import classNames from 'classnames';
import WordCountHintMessage from './wordcount-hint-message';
import omit from 'lodash/omit';

export default function TextAreaWithWordCount(props) {

const { value, maxWordCount, error, values, name } = props;

const [content, setContent] = useState(value || '');

const formErrorClass = classNames({
'govuk-form-group': true,
'govuk-character-count': true,
'govuk-form-group--error': error
});

return (
<div className={formErrorClass} id={`${name}-form-group`}>
<TextArea
{...omit(props, 'maxWordCount')}
value={content}
onChange={e => setContent(e.target.value)}
/>
<WordCountHintMessage content={content} id={values.id} maxWordCount={maxWordCount} />
</div>
);
}
28 changes: 28 additions & 0 deletions src/text-area-word-count/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Added from govuk-frotnend

.govuk-character-count {
@include govuk-responsive-margin(6, "bottom");

.govuk-form-group,
.govuk-textarea {
margin-bottom: govuk-spacing(1);
}
}

.govuk-character-count__message {
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 {
visibility: hidden;
}
39 changes: 39 additions & 0 deletions src/text-area-word-count/index.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import { shallow } from 'enzyme';
import WordCountHintMessage from './wordcount-hint-message';
import { describe, test, expect } from '@jest/globals';

describe('<WordCountHintMessage />', () => {
const id = 'applicantTrainingUseAtWork';
const wordCountHintId = '#applicantTrainingUseAtWork-wordcount-hint';

test('displays max words remaining when wordCount is not provided', () => {
const wrapper = shallow(<WordCountHintMessage content='' maxWordCount={10} id={id} />);
expect(wrapper.find(wordCountHintId).text()).toContain('You have 10 words remaining');
});

test('displays remaining words when wordCount is less than maxWordCount', () => {
const wrapper = shallow(<WordCountHintMessage content='This is a sentence with 7 words' maxWordCount={10} id={id} />);
expect(wrapper.find(wordCountHintId).text()).toContain('You have 3 words remaining');
});

test('displays no remaining words when wordCount is equal to maxWordCount', () => {
const wrapper = shallow(<WordCountHintMessage content='This is a sentence with 10 words - 2 more' maxWordCount={10} id={id} />);
expect(wrapper.find(wordCountHintId).text()).toContain('You have 0 words remaining');
});

test('displays too many words when wordCount is greater than maxWordCount', () => {
const wrapper = shallow(<WordCountHintMessage content='This is a sentence with 12 words - 2 more plus 2' maxWordCount={10} id={id} />);
expect(wrapper.find(wordCountHintId).text()).toContain('You have 2 words too many');
});

test('displays singular word when there is only one word remaining', () => {
const wrapper = shallow(<WordCountHintMessage content='This is a sentence with 9 words i think' maxWordCount={10} id={id} />);
expect(wrapper.find(wordCountHintId).text()).toContain('You have 1 word remaining');
});

test('displays singular word when there is only one word too many', () => {
const wrapper = shallow(<WordCountHintMessage content='This is a sentence with 11 words - 3 more words' maxWordCount={10} id={id} />);
expect(wrapper.find(wordCountHintId).text()).toContain('You have 1 word too many');
});
});
27 changes: 27 additions & 0 deletions src/text-area-word-count/wordcount-hint-message.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';

const WordCountHintMessage = ({ content, id, maxWordCount = 0 }) => {

const wordCount = content?.split(/\s+/).filter(Boolean).length ?? 0;
const hintId = `${id}-wordcount-hint`;

let hintText = '';

const wordCountText = count => count === 1 ? 'word' : 'words';

if (wordCount > maxWordCount) {
const count = wordCount - maxWordCount;
hintText = `You have ${count} ${wordCountText(count)} too many`;
} else {
const count = maxWordCount - wordCount;
hintText = `You have ${count} ${wordCountText(count)} remaining`;
}

return (
<div id={hintId} aria-live="polite" className="govuk-hint govuk-character-count__message">
{hintText}
</div>
);
};

export default WordCountHintMessage;
1 change: 1 addition & 0 deletions styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ $highlight-colour: govuk-colour('grey-4');
@import '../src/sticky-nav-anchor/index';
@import '../src/tabs/index';
@import '../src/licence-status-banner/index';
@import '../src/text-area-word-count/index';

.hidden {
display: none;
Expand Down

0 comments on commit 1f81475

Please sign in to comment.