Skip to content

Commit e36b9ac

Browse files
Add character count support for countType: "characters" using Intl.Segmenter
1 parent 60534e4 commit e36b9ac

2 files changed

Lines changed: 43 additions & 4 deletions

File tree

packages/govuk-frontend/src/govuk/components/character-count/character-count.mjs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
configOverride
66
} from '../../common/configuration.mjs'
77
import { formatErrorMessage } from '../../common/index.mjs'
8-
import { ConfigError, ElementError } from '../../errors/index.mjs'
8+
import { ConfigError, ElementError, SupportError } from '../../errors/index.mjs'
99
import { I18n } from '../../i18n.mjs'
1010

1111
/**
@@ -28,6 +28,11 @@ export class CharacterCount extends ConfigurableComponent {
2828
/** @private */
2929
count = 0
3030

31+
/**
32+
* @type {Intl.Segmenter | null}
33+
*/
34+
segmenter = null
35+
3136
/** @private */
3237
$visibleCountMessage
3338

@@ -126,6 +131,21 @@ export class CharacterCount extends ConfigurableComponent {
126131
locale: closestAttributeValue(this.$root, 'lang')
127132
})
128133

134+
if (this.config.countType === 'characters') {
135+
if (!('Segmenter' in Intl)) {
136+
throw new SupportError(
137+
formatErrorMessage(
138+
CharacterCount,
139+
'Support for "Intl.Segmenter" required'
140+
)
141+
)
142+
}
143+
144+
this.segmenter = new Intl.Segmenter(this.i18n.locale, {
145+
granularity: 'grapheme'
146+
})
147+
}
148+
129149
// Determine the limit attribute (characters or words)
130150
this.maxLength = this.config.maxlength ?? Infinity
131151

@@ -360,8 +380,18 @@ export class CharacterCount extends ConfigurableComponent {
360380
updateCount() {
361381
const text = this.$textarea.value
362382

383+
// Count grapheme clusters (user-perceived characters)
384+
if (this.config.countType === 'characters') {
385+
this.count = this.segmenter
386+
? Array.from(this.segmenter.segment(text)).length
387+
: 0
388+
389+
return
390+
}
391+
392+
// Count consecutive non-whitespace results
363393
if (this.config.countType === 'words') {
364-
const tokens = text.match(/\S+/g) ?? [] // Matches consecutive non-whitespace chars
394+
const tokens = text.match(/\S+/g) ?? []
365395
this.count = tokens.length
366396
return
367397
}
@@ -515,8 +545,8 @@ export class CharacterCount extends ConfigurableComponent {
515545
* @property {number} [threshold=0] - The percentage value of the limit at
516546
* which point the count message is displayed. If this attribute is set, the
517547
* count message will be hidden by default.
518-
* @property {'length' | 'words'} [countType] - The count type (`"length"` or
519-
* `"words"`) used to count the text.
548+
* @property {'characters' | 'length' | 'words'} [countType] - The count type
549+
* (`"characters"`, `"length"` or `"words"`) used to count the text.
520550
* @property {CharacterCountTranslations} [i18n=CharacterCount.defaults.i18n] - Character count translations
521551
*/
522552

packages/govuk-frontend/src/govuk/components/character-count/character-count.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,15 @@ examples:
234234
label:
235235
text: Enter a job description
236236

237+
- name: with count type 'characters'
238+
options:
239+
id: with-count-type-characters
240+
name: example
241+
countType: characters
242+
maxlength: 200
243+
label:
244+
text: Enter a job description
245+
237246
- name: with count type 'words'
238247
options:
239248
id: with-count-type-words

0 commit comments

Comments
 (0)