@primer/react
Version:
An implementation of GitHub's Primer Design System using React
67 lines (60 loc) • 1.97 kB
JavaScript
/**
* Shared character counting functionality for text inputs with character limits.
* Handles real-time character count updates, validation, and aria-live announcements.
*/
const SCREEN_READER_DELAY = 500;
class CharacterCounter {
announceTimeout = null;
isInitialLoad = true;
constructor(callbacks) {
this.callbacks = callbacks;
}
/**
* Update the character count based on current input value
*/
updateCharacterCount(currentLength, maxLength) {
const charactersRemaining = maxLength - currentLength;
// eslint-disable-next-line no-useless-assignment
let message = '';
if (charactersRemaining >= 0) {
const characterText = charactersRemaining === 1 ? 'character' : 'characters';
message = `${charactersRemaining} ${characterText} remaining`;
this.callbacks.onCountUpdate(charactersRemaining, false, message);
} else {
const charactersOver = -charactersRemaining;
const characterText = charactersOver === 1 ? 'character' : 'characters';
message = `${charactersOver} ${characterText} over`;
this.callbacks.onCountUpdate(charactersOver, true, message);
}
if (!this.isInitialLoad) {
this.announceToScreenReader(message);
}
// After first update, set isInitialLoad to false
if (this.isInitialLoad) {
this.isInitialLoad = false;
}
}
/**
* Announce character count to screen readers with debouncing
*/
announceToScreenReader(message) {
if (this.announceTimeout) {
clearTimeout(this.announceTimeout);
}
if (typeof window === 'undefined' || typeof window.setTimeout !== 'function') {
return;
}
this.announceTimeout = window.setTimeout(() => {
this.callbacks.onScreenReaderAnnounce(message);
}, SCREEN_READER_DELAY);
}
/**
* Clean up any pending timeouts
*/
cleanup() {
if (this.announceTimeout) {
clearTimeout(this.announceTimeout);
}
}
}
export { CharacterCounter };