UNPKG

@atlaskit/form

Version:

A form allows people to input information.

166 lines (152 loc) • 6.35 kB
/* 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))); };