Skip to content

Commit 84a8f53

Browse files
Align character count with NHS.UK frontend
1 parent 84977a6 commit 84a8f53

File tree

8 files changed

+159
-191
lines changed

8 files changed

+159
-191
lines changed

src/__tests__/index.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ describe('Index', () => {
1515
'Button',
1616
'Card',
1717
'CharacterCount',
18-
'CharacterCountType',
1918
'Checkboxes',
2019
'ChevronRightCircleIcon',
2120
'Clearfix',

src/components/form-elements/character-count/CharacterCount.tsx

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,23 @@
11
'use client';
2-
import React, { FC, useEffect, useRef, useState } from 'react';
2+
import React, { FC, HTMLProps, useEffect, useRef, useState } from 'react';
33
import { CharacterCount } from 'nhsuk-frontend';
4-
import { HTMLAttributesWithData } from '@util/types/NHSUKTypes';
4+
import classNames from 'classnames';
5+
import SingleInputFormGroup from '@components/utils/SingleInputFormGroup';
6+
import { FormElementProps } from '@util/types/FormTypes';
57

6-
export enum CharacterCountType {
7-
Characters,
8-
Words,
8+
export interface CharacterCountProps
9+
extends HTMLProps<HTMLTextAreaElement>,
10+
Omit<FormElementProps, 'fieldsetProps' | 'legend' | 'legendProps'> {
11+
maxLength?: number;
12+
maxWords?: number;
13+
threshold?: number;
914
}
1015

11-
type CharacterCountProps = React.HTMLAttributes<HTMLDivElement> & {
12-
children: React.ReactNode;
13-
maxLength: number;
14-
countType: CharacterCountType;
15-
textAreaId: string;
16-
thresholdPercent?: number;
17-
};
18-
1916
const CharacterCountComponent: FC<CharacterCountProps> = ({
2017
children,
2118
maxLength,
22-
countType,
23-
textAreaId,
24-
thresholdPercent,
19+
maxWords,
20+
threshold,
2521
...rest
2622
}) => {
2723
const moduleRef = useRef<HTMLDivElement>(null);
@@ -35,28 +31,41 @@ const CharacterCountComponent: FC<CharacterCountProps> = ({
3531
setInstance(new CharacterCount(moduleRef.current));
3632
}, [moduleRef, instance]);
3733

38-
const characterCountProps: HTMLAttributesWithData<HTMLDivElement> =
39-
countType === CharacterCountType.Characters
40-
? { ...rest, ['data-maxlength']: maxLength }
41-
: { ...rest, ['data-maxwords']: maxLength };
42-
43-
if (thresholdPercent) {
44-
characterCountProps['data-threshold'] = thresholdPercent;
45-
}
46-
4734
return (
48-
<div
49-
className="nhsuk-character-count"
50-
data-module="nhsuk-character-count"
51-
ref={moduleRef}
52-
{...characterCountProps}
35+
<SingleInputFormGroup<CharacterCountProps>
36+
inputType="textarea"
37+
formGroupProps={{
38+
className: 'nhsuk-character-count',
39+
'data-module': 'nhsuk-character-count',
40+
'data-maxlength': maxLength,
41+
'data-maxwords': maxWords,
42+
'data-threshold': threshold,
43+
ref: moduleRef,
44+
}}
45+
{...rest}
5346
>
54-
<div className="nhsuk-form-group">{children}</div>
55-
56-
<div className="nhsuk-hint nhsuk-character-count__message" id={`${textAreaId}-info`}>
57-
You can enter up to {maxLength} characters
58-
</div>
59-
</div>
47+
{({ className, id, error, 'aria-describedby': ariaDescribedBy, ...rest }) => (
48+
<>
49+
<textarea
50+
className={classNames(
51+
'nhsuk-textarea',
52+
{ 'nhsuk-textarea--error': error },
53+
'nhsuk-js-character-count',
54+
className,
55+
)}
56+
id={id}
57+
aria-describedby={`${id}-info ${ariaDescribedBy}`}
58+
{...rest}
59+
/>
60+
<div className="nhsuk-hint nhsuk-character-count__message" id={`${id}-info`}>
61+
{maxWords
62+
? `You can enter up to ${maxWords} words`
63+
: `You can enter up to ${maxLength} characters`}
64+
</div>
65+
{children}
66+
</>
67+
)}
68+
</SingleInputFormGroup>
6069
);
6170
};
6271

Lines changed: 47 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,19 @@
11
import React from 'react';
22
import { render } from '@testing-library/react';
3-
import CharacterCount, { CharacterCountType } from '../CharacterCount';
4-
import Label from '@components/form-elements/label/Label';
5-
import HintText from '@components/form-elements/hint-text/HintText';
6-
import Textarea from '@components/form-elements/textarea/Textarea';
3+
import CharacterCount from '../CharacterCount';
74

85
describe('Character Count', () => {
9-
const children = (
10-
<>
11-
<Label htmlFor="more-detail">Can you provide more detail?</Label>
12-
<HintText id="more-detail-hint">
13-
Do not include personal information like your name, date of birth or NHS number.
14-
</HintText>
15-
<Textarea
16-
id="more-detail"
17-
className="nhsuk-js-character-count"
18-
name="more-detail"
19-
aria-describedby="more-detail-hint"
20-
rows={5}
21-
/>
22-
</>
23-
);
24-
256
it('Matches snapshot', () => {
267
const { container } = render(
278
<CharacterCount
9+
label="Can you provide more detail?"
10+
labelProps={{ isPageHeading: true, size: 'l' }}
11+
hint="Do not include personal information like your name, date of birth or NHS number"
12+
id="more-detail"
13+
name="more-detail"
2814
maxLength={200}
29-
countType={CharacterCountType.Characters}
30-
textAreaId="more-detail"
31-
>
32-
{children}
33-
</CharacterCount>,
15+
rows={5}
16+
/>,
3417
);
3518

3619
expect(container).toMatchSnapshot();
@@ -39,57 +22,61 @@ describe('Character Count', () => {
3922
it('Sets the data-maxlength attribute when counting characters', () => {
4023
const { container } = render(
4124
<CharacterCount
25+
label="Can you provide more detail?"
26+
labelProps={{ isPageHeading: true, size: 'l' }}
27+
hint="Do not include personal information like your name, date of birth or NHS number"
28+
id="more-detail"
29+
name="more-detail"
4230
maxLength={200}
43-
countType={CharacterCountType.Characters}
44-
textAreaId="more-detail"
45-
>
46-
{children}
47-
</CharacterCount>,
31+
rows={5}
32+
/>,
4833
);
4934

50-
expect(container.querySelector('.nhsuk-character-count')?.getAttribute('data-maxlength')).toBe(
51-
'200',
52-
);
53-
expect(
54-
container.querySelector('.nhsuk-character-count')?.getAttribute('data-maxwords'),
55-
).toBeNull();
56-
expect(
57-
container.querySelector('.nhsuk-character-count')?.getAttribute('data-threshold'),
58-
).toBeNull();
35+
const characterCountEl = container.querySelector('.nhsuk-character-count');
36+
37+
expect(characterCountEl).toHaveAttribute('data-maxlength', '200');
38+
expect(characterCountEl).not.toHaveAttribute('data-maxwords');
39+
expect(characterCountEl).not.toHaveAttribute('data-threshold');
5940
});
6041

6142
it('Sets the data-maxwords attribute when counting words', () => {
6243
const { container } = render(
63-
<CharacterCount maxLength={200} countType={CharacterCountType.Words} textAreaId="more-detail">
64-
{children}
65-
</CharacterCount>,
44+
<CharacterCount
45+
label="Can you provide more detail?"
46+
labelProps={{ isPageHeading: true, size: 'l' }}
47+
hint="Do not include personal information like your name, date of birth or NHS number"
48+
id="more-detail"
49+
name="more-detail"
50+
maxWords={200}
51+
rows={5}
52+
/>,
6653
);
6754

68-
expect(container.querySelector('.nhsuk-character-count')?.getAttribute('data-maxwords')).toBe(
69-
'200',
70-
);
71-
expect(
72-
container.querySelector('.nhsuk-character-count')?.getAttribute('data-maxlength'),
73-
).toBeNull();
74-
expect(
75-
container.querySelector('.nhsuk-character-count')?.getAttribute('data-threshold'),
76-
).toBeNull();
55+
const characterCountEl = container.querySelector('.nhsuk-character-count');
56+
57+
expect(characterCountEl).not.toHaveAttribute('data-maxlength');
58+
expect(characterCountEl).toHaveAttribute('data-maxwords', '200');
59+
expect(characterCountEl).not.toHaveAttribute('data-threshold');
7760
});
7861

7962
it('Sets the data-threshold attribute when threshold is specified', () => {
8063
const { container } = render(
8164
<CharacterCount
65+
label="Can you provide more detail?"
66+
labelProps={{ isPageHeading: true, size: 'l' }}
67+
hint="Do not include personal information like your name, date of birth or NHS number"
68+
id="more-detail"
69+
name="more-detail"
8270
maxLength={200}
83-
countType={CharacterCountType.Characters}
84-
thresholdPercent={50}
85-
textAreaId="more-detail"
86-
>
87-
{children}
88-
</CharacterCount>,
71+
threshold={50}
72+
rows={5}
73+
/>,
8974
);
9075

91-
expect(container.querySelector('.nhsuk-character-count')?.getAttribute('data-threshold')).toBe(
92-
'50',
93-
);
76+
const characterCountEl = container.querySelector('.nhsuk-character-count');
77+
78+
expect(characterCountEl).toHaveAttribute('data-maxlength', '200');
79+
expect(characterCountEl).not.toHaveAttribute('data-maxwords');
80+
expect(characterCountEl).toHaveAttribute('data-threshold', '50');
9481
});
9582
});

src/components/form-elements/character-count/__tests__/__snapshots__/CharacterCount.test.tsx.snap

Lines changed: 36 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,57 +3,52 @@
33
exports[`Character Count Matches snapshot 1`] = `
44
<div>
55
<div
6-
class="nhsuk-character-count"
6+
class="nhsuk-form-group nhsuk-character-count"
77
data-maxlength="200"
88
data-module="nhsuk-character-count"
99
data-nhsuk-character-count-init=""
1010
>
11-
<div
12-
class="nhsuk-form-group"
11+
<h1
12+
class="nhsuk-label-wrapper"
1313
>
1414
<label
15-
class="nhsuk-label"
15+
class="nhsuk-label nhsuk-label--l"
1616
for="more-detail"
17+
id="more-detail--label"
1718
>
1819
Can you provide more detail?
1920
</label>
20-
<div
21-
class="nhsuk-hint"
22-
id="more-detail-hint"
23-
>
24-
Do not include personal information like your name, date of birth or NHS number.
25-
</div>
26-
<div
27-
class="nhsuk-form-group"
28-
>
29-
<textarea
30-
aria-describedby="more-detail-hint"
31-
class="nhsuk-textarea nhsuk-js-character-count"
32-
id="more-detail"
33-
name="more-detail"
34-
rows="5"
35-
/>
36-
<div
37-
class="nhsuk-hint nhsuk-character-count__message nhsuk-u-visually-hidden"
38-
id="more-detail-info"
39-
>
40-
You can enter up to
41-
200
42-
characters
43-
</div>
44-
<div
45-
aria-hidden="true"
46-
class="nhsuk-hint nhsuk-character-count__message nhsuk-character-count__status"
47-
>
48-
You have 200 characters remaining
49-
</div>
50-
<div
51-
aria-live="polite"
52-
class="nhsuk-character-count__sr-status nhsuk-u-visually-hidden"
53-
>
54-
You have 200 characters remaining
55-
</div>
56-
</div>
21+
</h1>
22+
<div
23+
class="nhsuk-hint"
24+
id="more-detail--hint"
25+
>
26+
Do not include personal information like your name, date of birth or NHS number
27+
</div>
28+
<textarea
29+
aria-describedby="more-detail-info more-detail--hint"
30+
class="nhsuk-textarea nhsuk-js-character-count"
31+
id="more-detail"
32+
name="more-detail"
33+
rows="5"
34+
/>
35+
<div
36+
class="nhsuk-hint nhsuk-character-count__message nhsuk-u-visually-hidden"
37+
id="more-detail-info"
38+
>
39+
You can enter up to 200 characters
40+
</div>
41+
<div
42+
aria-hidden="true"
43+
class="nhsuk-hint nhsuk-character-count__message nhsuk-character-count__status"
44+
>
45+
You have 200 characters remaining
46+
</div>
47+
<div
48+
aria-live="polite"
49+
class="nhsuk-character-count__sr-status nhsuk-u-visually-hidden"
50+
>
51+
You have 200 characters remaining
5752
</div>
5853
</div>
5954
</div>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { default, CharacterCountType } from './CharacterCount';
1+
export { default } from './CharacterCount';

src/index.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ export { default as BackLink } from './components/navigation/back-link';
33
export { default as Breadcrumb } from './components/navigation/breadcrumb';
44
export { default as Button } from './components/form-elements/button';
55
export { default as Card } from './components/navigation/card';
6-
export {
7-
default as CharacterCount,
8-
CharacterCountType,
9-
} from './components/form-elements/character-count';
6+
export { default as CharacterCount } from './components/form-elements/character-count';
107
export { default as Checkboxes } from './components/form-elements/checkboxes';
118
export { default as ContentsList } from './components/navigation/contents-list';
129
export { default as DateInput } from './components/form-elements/date-input';

src/util/types/FormTypes.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ export interface FormElementProps {
1010
errorProps?: ErrorMessageProps;
1111
hint?: string;
1212
hintProps?: HintTextProps;
13-
formGroupProps?: HTMLProps<HTMLDivElement>;
13+
formGroupProps?: HTMLProps<HTMLDivElement> & {
14+
'data-module'?: string;
15+
'data-maxlength'?: number;
16+
'data-maxwords'?: number;
17+
'data-threshold'?: number;
18+
};
1419
disableErrorLine?: boolean;
1520
id?: string;
1621
name?: string;

0 commit comments

Comments
 (0)