@atlaskit/form
Version:
A form allows people to input information.
166 lines (152 loc) • 6.35 kB
JavaScript
/* character-counter.tsx generated by @compiled/babel-plugin v3.0.2 */
import "./character-counter.compiled.css";
import * as React from 'react';
import { ax, ix } from "@compiled/react/runtime";
import { createContext, useContext, useEffect, useRef, useState } from 'react';
import ErrorIcon from '@atlaskit/icon/core/status-error';
import { useMotion } from '@atlaskit/motion/entering/use-motion';
import ExitingPersistence from '@atlaskit/motion/exiting-persistence';
import { fg } from '@atlaskit/platform-feature-flags/fg';
import { Flex, Text } from '@atlaskit/primitives/compiled';
import VisuallyHidden from '@atlaskit/visually-hidden/visually-hidden';
import { FieldId } from './field-id-context';
import MessageTrack from './message-track';
// Extracted styles for character counter message container
const messageContainerStyles = {
root: "_11c8wadc _syaz1tmw _1pfh1b66"
};
// Extracted styles for error icon wrapper, need to use css to override the default height
const errorIconWrapperStyles = null;
// Error icon with wrapper for character count violations
const ErrorIconWithWrapper = () => /*#__PURE__*/React.createElement("span", {
className: ax(["_1e0c1txw _4t3i7vkz _4cvr1h6o"])
}, /*#__PURE__*/React.createElement(ErrorIcon, {
label: "error",
size: "small"
}));
// Helper to pluralise "character(s)"
const pluralize = count => `character${count !== 1 ? 's' : ''}`;
export const CharacterCounterMotionContext = /*#__PURE__*/createContext(false);
const CounterVisual = ({
shouldDisplayAsError,
displayText,
isExiting = false,
resolvedFieldId
}) => /*#__PURE__*/React.createElement(Flex, {
gap: "space.075",
xcss: messageContainerStyles.root
}, shouldDisplayAsError && /*#__PURE__*/React.createElement(ErrorIconWithWrapper, null), /*#__PURE__*/React.createElement(Text, {
color: shouldDisplayAsError ? 'color.text.danger' : 'color.text.subtlest',
size: "small",
id: resolvedFieldId && !isExiting ? `${resolvedFieldId}-character-counter` : undefined
}, displayText));
const AnimatedCounterVisual = ({
shouldDisplayAsError,
displayText,
transition,
resolvedFieldId,
testId
}) => {
const {
ref,
state
} = useMotion({
initialState: transition === 'instant' ? 'visible' : undefined
});
return /*#__PURE__*/React.createElement(MessageTrack, {
motionRef: ref,
state: state,
transition: transition,
testId: testId
}, /*#__PURE__*/React.createElement(CounterVisual, {
shouldDisplayAsError: shouldDisplayAsError,
displayText: displayText,
isExiting: state === 'exiting',
resolvedFieldId: resolvedFieldId
}));
};
/**
* __Character Counter__
*
* A character counter component that displays remaining characters for text input.
* Displays messages for over or under the maximum or minimum character limits.
*/
export const CharacterCounter = ({
maxCharacters,
minCharacters,
currentValue,
overMaximumMessage,
underMaximumMessage,
underMinimumMessage,
shouldShowAsError = true,
inputId,
testId
}) => {
const [announcementText, setAnnouncementText] = useState('');
const debounceTimeoutRef = useRef(null);
const isMotionEnabled = useContext(CharacterCounterMotionContext) && fg('platform-dst-motion-uplift-input');
// Resolve the field ID from context (form use) or inputId prop (standalone use)
const contextFieldId = useContext(FieldId);
const resolvedFieldId = contextFieldId || inputId;
const currentLength = (currentValue === null || currentValue === void 0 ? void 0 : currentValue.length) || 0;
// Check if character count violates limits
const isTooShort = minCharacters !== undefined && currentLength < minCharacters;
const isTooLong = maxCharacters !== undefined && currentLength > maxCharacters;
// Determine what to display based on the current value, the maximum and minimum character limits, and any custom messages
const getMessage = () => {
// Below minimum so show custom message or default
if (isTooShort) {
const needed = minCharacters - currentLength;
return underMinimumMessage || `${needed} more ${pluralize(needed)} needed`;
}
// Over maximum so show custom message or default
if (isTooLong) {
const over = currentLength - maxCharacters;
return overMaximumMessage || `${over} ${pluralize(over)} too many`;
}
// Within limits - show remaining count (if max is defined)
if (maxCharacters) {
const remaining = maxCharacters - currentLength;
return underMaximumMessage || `${remaining} ${pluralize(remaining)} remaining`;
}
// No message to show (min only limit satisfied)
return null;
};
const displayText = getMessage();
// Determine if the current character count violates limits
const displayAsError = (isTooShort || isTooLong) && shouldShowAsError;
// Debounce screen reader announcements so that it only reads the message when it input has settled
useEffect(() => {
// Debounce by 1 second to avoid announcing every keystroke
debounceTimeoutRef.current = setTimeout(() => {
setAnnouncementText(displayText || '');
}, 1000);
// Cleanup function clears the timeout when displayText changes or component unmounts
return () => {
clearTimeout(debounceTimeoutRef.current);
};
}, [displayText]);
// Don't render if there's no message to display (min only limit satisfied)
if (!displayText && !isMotionEnabled) {
return null;
}
return /*#__PURE__*/React.createElement(Flex, {
testId: testId
}, isMotionEnabled ? /*#__PURE__*/React.createElement(ExitingPersistence, {
appear: true
}, displayText && /*#__PURE__*/React.createElement(AnimatedCounterVisual, {
key: displayAsError ? 'error' : 'default',
shouldDisplayAsError: displayAsError,
displayText: displayText,
transition: maxCharacters !== undefined ? 'instant' : 'row',
resolvedFieldId: resolvedFieldId,
testId: testId && `${testId}-motion`
})) : /*#__PURE__*/React.createElement(CounterVisual, {
shouldDisplayAsError: displayAsError,
displayText: displayText,
resolvedFieldId: resolvedFieldId
}), /*#__PURE__*/React.createElement(VisuallyHidden, null, /*#__PURE__*/React.createElement("div", {
"aria-live": "polite",
"data-testid": isMotionEnabled && testId ? `${testId}-announcer` : undefined
}, announcementText)));
};