@atlaskit/form
Version:
A form allows people to input information.
174 lines (160 loc) • 7.42 kB
JavaScript
/* character-counter.tsx generated by @compiled/babel-plugin v3.0.2 */
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
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
var messageContainerStyles = {
root: "_11c8wadc _syaz1tmw _1pfh1b66"
};
// Extracted styles for error icon wrapper, need to use css to override the default height
var errorIconWrapperStyles = null;
// Error icon with wrapper for character count violations
var ErrorIconWithWrapper = function ErrorIconWithWrapper() {
return /*#__PURE__*/React.createElement("span", {
className: ax(["_1e0c1txw _4t3i7vkz _4cvr1h6o"])
}, /*#__PURE__*/React.createElement(ErrorIcon, {
label: "error",
size: "small"
}));
};
// Helper to pluralise "character(s)"
var pluralize = function pluralize(count) {
return "character".concat(count !== 1 ? 's' : '');
};
export var CharacterCounterMotionContext = /*#__PURE__*/createContext(false);
var CounterVisual = function CounterVisual(_ref) {
var shouldDisplayAsError = _ref.shouldDisplayAsError,
displayText = _ref.displayText,
_ref$isExiting = _ref.isExiting,
isExiting = _ref$isExiting === void 0 ? false : _ref$isExiting,
resolvedFieldId = _ref.resolvedFieldId;
return /*#__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 ? "".concat(resolvedFieldId, "-character-counter") : undefined
}, displayText));
};
var AnimatedCounterVisual = function AnimatedCounterVisual(_ref2) {
var shouldDisplayAsError = _ref2.shouldDisplayAsError,
displayText = _ref2.displayText,
transition = _ref2.transition,
resolvedFieldId = _ref2.resolvedFieldId,
testId = _ref2.testId;
var _useMotion = useMotion({
initialState: transition === 'instant' ? 'visible' : undefined
}),
ref = _useMotion.ref,
state = _useMotion.state;
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 var CharacterCounter = function CharacterCounter(_ref3) {
var maxCharacters = _ref3.maxCharacters,
minCharacters = _ref3.minCharacters,
currentValue = _ref3.currentValue,
overMaximumMessage = _ref3.overMaximumMessage,
underMaximumMessage = _ref3.underMaximumMessage,
underMinimumMessage = _ref3.underMinimumMessage,
_ref3$shouldShowAsErr = _ref3.shouldShowAsError,
shouldShowAsError = _ref3$shouldShowAsErr === void 0 ? true : _ref3$shouldShowAsErr,
inputId = _ref3.inputId,
testId = _ref3.testId;
var _useState = useState(''),
_useState2 = _slicedToArray(_useState, 2),
announcementText = _useState2[0],
setAnnouncementText = _useState2[1];
var debounceTimeoutRef = useRef(null);
var isMotionEnabled = useContext(CharacterCounterMotionContext) && fg('platform-dst-motion-uplift-input');
// Resolve the field ID from context (form use) or inputId prop (standalone use)
var contextFieldId = useContext(FieldId);
var resolvedFieldId = contextFieldId || inputId;
var currentLength = (currentValue === null || currentValue === void 0 ? void 0 : currentValue.length) || 0;
// Check if character count violates limits
var isTooShort = minCharacters !== undefined && currentLength < minCharacters;
var isTooLong = maxCharacters !== undefined && currentLength > maxCharacters;
// Determine what to display based on the current value, the maximum and minimum character limits, and any custom messages
var getMessage = function getMessage() {
// Below minimum so show custom message or default
if (isTooShort) {
var needed = minCharacters - currentLength;
return underMinimumMessage || "".concat(needed, " more ").concat(pluralize(needed), " needed");
}
// Over maximum so show custom message or default
if (isTooLong) {
var over = currentLength - maxCharacters;
return overMaximumMessage || "".concat(over, " ").concat(pluralize(over), " too many");
}
// Within limits - show remaining count (if max is defined)
if (maxCharacters) {
var remaining = maxCharacters - currentLength;
return underMaximumMessage || "".concat(remaining, " ").concat(pluralize(remaining), " remaining");
}
// No message to show (min only limit satisfied)
return null;
};
var displayText = getMessage();
// Determine if the current character count violates limits
var displayAsError = (isTooShort || isTooLong) && shouldShowAsError;
// Debounce screen reader announcements so that it only reads the message when it input has settled
useEffect(function () {
// Debounce by 1 second to avoid announcing every keystroke
debounceTimeoutRef.current = setTimeout(function () {
setAnnouncementText(displayText || '');
}, 1000);
// Cleanup function clears the timeout when displayText changes or component unmounts
return function () {
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 && "".concat(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 ? "".concat(testId, "-announcer") : undefined
}, announcementText)));
};