@atlaskit/form 14.3.2 → 14.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @atlaskit/form
2
2
 
3
+ ## 14.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`c19181795ec37`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/c19181795ec37) -
8
+ Introduces `CharacterCounterField`, a new field component that provides automatic character count
9
+ validation and display. This component wraps the standard `Field` with built-in support for
10
+ minimum and maximum character limits, displaying a real-time character counter to users.
11
+
12
+ **Key features:**
13
+ - Automatic validation for `minCharacters` and `maxCharacters` limits
14
+ - Integrated character counter display with error states
15
+ - Customizable messaging for character limit violations via `overMaximumMessage`,
16
+ `underMaximumMessage`, and `underMinimumMessage` props
17
+ - Seamless integration with existing Field validation through the `validate` prop
18
+ - Full accessibility support with proper ARIA announcements for screen readers
19
+ - Supports both `TextField` and `TextArea` components via render prop pattern
20
+
21
+ **Example usage:**
22
+
23
+ ```javascript
24
+ <CharacterCounterField
25
+ name="description"
26
+ label="Description"
27
+ maxCharacters={200}
28
+ minCharacters={10}
29
+ helperMessage="Provide a brief description"
30
+ >
31
+ {({ fieldProps }) => <TextArea {...fieldProps} />}
32
+ </CharacterCounterField>
33
+ ```
34
+
35
+ This component simplifies the implementation of character-limited inputs by combining validation,
36
+ error handling, and counter display in a single, accessible component.
37
+
3
38
  ## 14.3.2
4
39
 
5
40
  ### Patch Changes
@@ -0,0 +1,3 @@
1
+ ._14uxze3t label[id$=-label]{margin-block-end:var(--ds-space-0,0)}
2
+ ._6rth1b66{margin-block-end:var(--ds-space-050,4px)}
3
+ ._6ul5ze3t [id$=-helper]{margin-block-start:var(--ds-space-0,0)}
@@ -0,0 +1,142 @@
1
+ /* character-counter-field.tsx generated by @compiled/babel-plugin v0.38.1 */
2
+ "use strict";
3
+
4
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
5
+ var _typeof = require("@babel/runtime/helpers/typeof");
6
+ Object.defineProperty(exports, "__esModule", {
7
+ value: true
8
+ });
9
+ exports.default = CharacterCounterField;
10
+ require("./character-counter-field.compiled.css");
11
+ var _react = _interopRequireWildcard(require("react"));
12
+ var React = _react;
13
+ var _runtime = require("@compiled/react/runtime");
14
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
15
+ var _characterCounter = require("./character-counter");
16
+ var _field = _interopRequireDefault(require("./field"));
17
+ var _messages = require("./messages");
18
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
19
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
20
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
21
+ // Override label specific margin block end to fix double spacing issue
22
+ var fieldWrapperStyles = {
23
+ root: "_14uxze3t"
24
+ };
25
+
26
+ // Override helper message margins to fix inconsistent spacing issue
27
+ var helperMessageWrapperStyles = {
28
+ root: "_6rth1b66 _6ul5ze3t"
29
+ };
30
+ /**
31
+ * __Character Counter Field__
32
+ *
33
+ * A field component that wraps the standard Field with automatic character count validation.
34
+ * Validates minimum and maximum character limits and displays a character counter.
35
+ */
36
+ function CharacterCounterField(_ref) {
37
+ var maxCharacters = _ref.maxCharacters,
38
+ minCharacters = _ref.minCharacters,
39
+ children = _ref.children,
40
+ userValidate = _ref.validate,
41
+ overMaximumMessage = _ref.overMaximumMessage,
42
+ underMaximumMessage = _ref.underMaximumMessage,
43
+ underMinimumMessage = _ref.underMinimumMessage,
44
+ helperMessage = _ref.helperMessage,
45
+ defaultValue = _ref.defaultValue,
46
+ id = _ref.id,
47
+ isRequired = _ref.isRequired,
48
+ isDisabled = _ref.isDisabled,
49
+ label = _ref.label,
50
+ elementAfterLabel = _ref.elementAfterLabel,
51
+ name = _ref.name,
52
+ testId = _ref.testId;
53
+ // Default validation function for character limits
54
+ // __TOO_SHORT__ and __TOO_LONG__ are default error codes recognised by the CharacterCounter component
55
+ var validateCharacterCount = function validateCharacterCount(value) {
56
+ var stringValue = String(value || '');
57
+ var length = stringValue.length;
58
+
59
+ // Check minimum length
60
+ if (minCharacters !== undefined && length < minCharacters) {
61
+ return '__TOO_SHORT__';
62
+ }
63
+
64
+ // Check maximum length
65
+ if (maxCharacters !== undefined && length > maxCharacters) {
66
+ return '__TOO_LONG__';
67
+ }
68
+ return undefined;
69
+ };
70
+
71
+ // Combine user validation and character validation
72
+ // Any user defined validation takes priority over character validation
73
+ // If there is no user defined validation for character limits e.g. used maxLength prior to CharacterCounterField,
74
+ // use the default error codes and display the appropriate error message
75
+ var combinedValidate = function combinedValidate(value, formState, fieldState) {
76
+ // First run character validation
77
+ var characterError = validateCharacterCount(value);
78
+
79
+ // Then run user's custom validation if provided
80
+ var userError = userValidate === null || userValidate === void 0 ? void 0 : userValidate(value, formState, fieldState);
81
+
82
+ // If user validation returns a promise, handle it
83
+ if (userError instanceof Promise) {
84
+ return userError.then(function (error) {
85
+ // User error takes priority over character validation
86
+ return error || characterError;
87
+ });
88
+ }
89
+
90
+ // User error takes priority over character validation
91
+ return userError || characterError;
92
+ };
93
+ return /*#__PURE__*/React.createElement("div", {
94
+ className: (0, _runtime.ax)([fieldWrapperStyles.root])
95
+ }, /*#__PURE__*/React.createElement(_field.default, {
96
+ defaultValue: defaultValue,
97
+ id: id,
98
+ isRequired: isRequired,
99
+ isDisabled: isDisabled,
100
+ label: label,
101
+ elementAfterLabel: elementAfterLabel,
102
+ name: name,
103
+ testId: testId,
104
+ validate: combinedValidate
105
+ }, function (_ref2) {
106
+ var extendedFieldProps = _ref2.fieldProps,
107
+ error = _ref2.error,
108
+ valid = _ref2.valid,
109
+ meta = _ref2.meta;
110
+ // Determine if error is a character count violation (handled by CharacterCounter)
111
+ // or an external validation error (needs ErrorMessage)
112
+ var isCharacterCountViolation = error === '__TOO_SHORT__' || error === '__TOO_LONG__';
113
+ var showExternalError = error && !isCharacterCountViolation;
114
+ var showCharacterCounter = (maxCharacters !== undefined || minCharacters !== undefined) && !showExternalError;
115
+
116
+ // Extend aria-describedby to reference the appropriate message component
117
+ var fieldPropsWithCounter = _objectSpread(_objectSpread({}, extendedFieldProps), {}, {
118
+ 'aria-describedby': showCharacterCounter ? "".concat(extendedFieldProps['aria-describedby'], " ").concat(extendedFieldProps.id, "-character-counter").trim() : extendedFieldProps['aria-describedby']
119
+ });
120
+ return /*#__PURE__*/React.createElement(_react.Fragment, null, /*#__PURE__*/React.createElement(_messages.MessageWrapper, null, helperMessage && /*#__PURE__*/React.createElement("div", {
121
+ className: (0, _runtime.ax)([helperMessageWrapperStyles.root])
122
+ }, /*#__PURE__*/React.createElement(_messages.HelperMessage, {
123
+ testId: "".concat(testId, "-helper")
124
+ }, helperMessage))), children({
125
+ fieldProps: fieldPropsWithCounter,
126
+ error: error,
127
+ valid: valid,
128
+ meta: meta
129
+ }), /*#__PURE__*/React.createElement(_messages.MessageWrapper, null, showExternalError && /*#__PURE__*/React.createElement(_messages.ErrorMessage, {
130
+ testId: "".concat(testId, "-error")
131
+ }, error)), showCharacterCounter && /*#__PURE__*/React.createElement(_characterCounter.CharacterCounter, {
132
+ maxCharacters: maxCharacters,
133
+ minCharacters: minCharacters,
134
+ currentValue: String(extendedFieldProps.value || ''),
135
+ shouldShowAsError: isCharacterCountViolation,
136
+ overMaximumMessage: overMaximumMessage,
137
+ underMaximumMessage: underMaximumMessage,
138
+ underMinimumMessage: underMinimumMessage,
139
+ testId: "".concat(testId, "-character-counter")
140
+ }));
141
+ }));
142
+ }
@@ -0,0 +1,8 @@
1
+ ._11c8dcr7{font:var(--ds-font-body-UNSAFE_small,normal 400 9pt/1pc ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",Ubuntu,"Helvetica Neue",sans-serif)}
2
+ ._zulp12x7{gap:var(--ds-space-075,6px)}
3
+ ._1bah1q9y{justify-content:baseline}
4
+ ._1e0c1txw{display:flex}
5
+ ._1pfh1b66{margin-block-start:var(--ds-space-050,4px)}
6
+ ._4cvr1h6o{align-items:center}
7
+ ._4t3i7vkz{height:1pc}
8
+ ._syaze6sf{color:var(--ds-text-danger,#ae2a19)}
@@ -0,0 +1,130 @@
1
+ /* character-counter.tsx generated by @compiled/babel-plugin v0.38.1 */
2
+ "use strict";
3
+
4
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
5
+ var _typeof = require("@babel/runtime/helpers/typeof");
6
+ Object.defineProperty(exports, "__esModule", {
7
+ value: true
8
+ });
9
+ exports.CharacterCounter = void 0;
10
+ require("./character-counter.compiled.css");
11
+ var _react = _interopRequireWildcard(require("react"));
12
+ var React = _react;
13
+ var _runtime = require("@compiled/react/runtime");
14
+ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
15
+ var _statusErrorError = _interopRequireDefault(require("@atlaskit/icon/core/migration/status-error--error"));
16
+ var _compiled = require("@atlaskit/primitives/compiled");
17
+ var _visuallyHidden = _interopRequireDefault(require("@atlaskit/visually-hidden"));
18
+ var _fieldIdContext = require("./field-id-context");
19
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
20
+ // Extracted styles for character counter message container
21
+ var messageContainerStyles = null;
22
+
23
+ // Extracted styles for error icon wrapper
24
+ var errorIconWrapperStyles = null;
25
+
26
+ // Error icon with wrapper for character count violations
27
+ var ErrorIconWithWrapper = function ErrorIconWithWrapper() {
28
+ return /*#__PURE__*/React.createElement("span", {
29
+ className: (0, _runtime.ax)(["_1e0c1txw _4t3i7vkz _4cvr1h6o"])
30
+ }, /*#__PURE__*/React.createElement(_statusErrorError.default, {
31
+ LEGACY_margin: "0 -2px 0 0",
32
+ LEGACY_size: "small",
33
+ label: "error",
34
+ size: "small"
35
+ }));
36
+ };
37
+ // Helper to pluralise "character(s)"
38
+ var pluralize = function pluralize(count) {
39
+ return "character".concat(count !== 1 ? 's' : '');
40
+ };
41
+
42
+ /**
43
+ * __Character Counter__
44
+ *
45
+ * A character counter component that displays remaining characters for text input.
46
+ * Displays messages for over or under the maximum or minimum character limits.
47
+ */
48
+ var CharacterCounter = exports.CharacterCounter = function CharacterCounter(_ref) {
49
+ var maxCharacters = _ref.maxCharacters,
50
+ minCharacters = _ref.minCharacters,
51
+ currentValue = _ref.currentValue,
52
+ overMaximumMessage = _ref.overMaximumMessage,
53
+ underMaximumMessage = _ref.underMaximumMessage,
54
+ underMinimumMessage = _ref.underMinimumMessage,
55
+ _ref$shouldShowAsErro = _ref.shouldShowAsError,
56
+ shouldShowAsError = _ref$shouldShowAsErro === void 0 ? true : _ref$shouldShowAsErro,
57
+ inputId = _ref.inputId,
58
+ testId = _ref.testId;
59
+ var _useState = (0, _react.useState)(''),
60
+ _useState2 = (0, _slicedToArray2.default)(_useState, 2),
61
+ announcementText = _useState2[0],
62
+ setAnnouncementText = _useState2[1];
63
+ var debounceTimeoutRef = (0, _react.useRef)(null);
64
+
65
+ // Resolve the field ID from context (form use) or inputId prop (standalone use)
66
+ var contextFieldId = (0, _react.useContext)(_fieldIdContext.FieldId);
67
+ var resolvedFieldId = contextFieldId || inputId;
68
+ var currentLength = (currentValue === null || currentValue === void 0 ? void 0 : currentValue.length) || 0;
69
+
70
+ // Check if character count violates limits
71
+ var isTooShort = minCharacters !== undefined && currentLength < minCharacters;
72
+ var isTooLong = maxCharacters !== undefined && currentLength > maxCharacters;
73
+
74
+ // Determine what to display based on the current value, the maximum and minimum character limits, and any custom messages
75
+ var getMessage = function getMessage() {
76
+ // Below minimum so show custom message or default
77
+ if (isTooShort) {
78
+ var needed = minCharacters - currentLength;
79
+ return underMinimumMessage || "".concat(needed, " more ").concat(pluralize(needed), " needed");
80
+ }
81
+
82
+ // Over maximum so show custom message or default
83
+ if (isTooLong) {
84
+ var over = currentLength - maxCharacters;
85
+ return overMaximumMessage || "".concat(over, " ").concat(pluralize(over), " too many");
86
+ }
87
+
88
+ // Within limits - show remaining count (if max is defined)
89
+ if (maxCharacters) {
90
+ var remaining = maxCharacters - currentLength;
91
+ return underMaximumMessage || "".concat(remaining, " ").concat(pluralize(remaining), " remaining");
92
+ }
93
+
94
+ // No message to show (min only limit satisfied)
95
+ return null;
96
+ };
97
+ var displayText = getMessage();
98
+
99
+ // Determine if the current character count violates limits
100
+ var displayAsError = (isTooShort || isTooLong) && shouldShowAsError;
101
+
102
+ // Debounce screen reader announcements so that it only reads the message when it input has settled
103
+ (0, _react.useEffect)(function () {
104
+ // Debounce by 1 second to avoid announcing every keystroke
105
+ debounceTimeoutRef.current = setTimeout(function () {
106
+ setAnnouncementText(displayText || '');
107
+ }, 1000);
108
+
109
+ // Cleanup function clears the timeout when displayText changes or component unmounts
110
+ return function () {
111
+ clearTimeout(debounceTimeoutRef.current);
112
+ };
113
+ }, [displayText]);
114
+
115
+ // Don't render if there's no message to display (min only limit satisfied)
116
+ if (!displayText) {
117
+ return null;
118
+ }
119
+ return /*#__PURE__*/React.createElement("div", {
120
+ "data-testid": testId
121
+ }, /*#__PURE__*/React.createElement("div", {
122
+ className: (0, _runtime.ax)(["_zulp12x7 _11c8dcr7 _1e0c1txw _1bah1q9y _syaze6sf _1pfh1b66"])
123
+ }, displayAsError && /*#__PURE__*/React.createElement(ErrorIconWithWrapper, null), /*#__PURE__*/React.createElement(_compiled.Text, {
124
+ color: displayAsError ? 'color.text.danger' : 'color.text.subtlest',
125
+ size: "small",
126
+ id: resolvedFieldId ? "".concat(resolvedFieldId, "-character-counter") : undefined
127
+ }, displayText)), /*#__PURE__*/React.createElement(_visuallyHidden.default, null, /*#__PURE__*/React.createElement("div", {
128
+ "aria-live": "polite"
129
+ }, announcementText)));
130
+ };
package/dist/cjs/index.js CHANGED
@@ -4,6 +4,12 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
+ Object.defineProperty(exports, "CharacterCounterField", {
8
+ enumerable: true,
9
+ get: function get() {
10
+ return _characterCounterField.default;
11
+ }
12
+ });
7
13
  Object.defineProperty(exports, "CheckboxField", {
8
14
  enumerable: true,
9
15
  get: function get() {
@@ -111,4 +117,5 @@ var _label = require("./label");
111
117
  var _messages = require("./messages");
112
118
  var _fieldset = _interopRequireDefault(require("./fieldset"));
113
119
  var _requiredAsterisk = _interopRequireDefault(require("./required-asterisk"));
114
- var _useFormState = require("./use-form-state");
120
+ var _useFormState = require("./use-form-state");
121
+ var _characterCounterField = _interopRequireDefault(require("./character-counter-field"));
@@ -0,0 +1,3 @@
1
+ ._14uxze3t label[id$=-label]{margin-block-end:var(--ds-space-0,0)}
2
+ ._6rth1b66{margin-block-end:var(--ds-space-050,4px)}
3
+ ._6ul5ze3t [id$=-helper]{margin-block-start:var(--ds-space-0,0)}
@@ -0,0 +1,133 @@
1
+ /* character-counter-field.tsx generated by @compiled/babel-plugin v0.38.1 */
2
+ import "./character-counter-field.compiled.css";
3
+ import * as React from 'react';
4
+ import { ax, ix } from "@compiled/react/runtime";
5
+ import { Fragment } from 'react';
6
+ import { CharacterCounter } from './character-counter';
7
+ import Field from './field';
8
+ import { ErrorMessage, HelperMessage, MessageWrapper } from './messages';
9
+ // Override label specific margin block end to fix double spacing issue
10
+ const fieldWrapperStyles = {
11
+ root: "_14uxze3t"
12
+ };
13
+
14
+ // Override helper message margins to fix inconsistent spacing issue
15
+ const helperMessageWrapperStyles = {
16
+ root: "_6rth1b66 _6ul5ze3t"
17
+ };
18
+ /**
19
+ * __Character Counter Field__
20
+ *
21
+ * A field component that wraps the standard Field with automatic character count validation.
22
+ * Validates minimum and maximum character limits and displays a character counter.
23
+ */
24
+ export default function CharacterCounterField({
25
+ maxCharacters,
26
+ minCharacters,
27
+ children,
28
+ validate: userValidate,
29
+ overMaximumMessage,
30
+ underMaximumMessage,
31
+ underMinimumMessage,
32
+ helperMessage,
33
+ defaultValue,
34
+ id,
35
+ isRequired,
36
+ isDisabled,
37
+ label,
38
+ elementAfterLabel,
39
+ name,
40
+ testId
41
+ }) {
42
+ // Default validation function for character limits
43
+ // __TOO_SHORT__ and __TOO_LONG__ are default error codes recognised by the CharacterCounter component
44
+ const validateCharacterCount = value => {
45
+ const stringValue = String(value || '');
46
+ const length = stringValue.length;
47
+
48
+ // Check minimum length
49
+ if (minCharacters !== undefined && length < minCharacters) {
50
+ return '__TOO_SHORT__';
51
+ }
52
+
53
+ // Check maximum length
54
+ if (maxCharacters !== undefined && length > maxCharacters) {
55
+ return '__TOO_LONG__';
56
+ }
57
+ return undefined;
58
+ };
59
+
60
+ // Combine user validation and character validation
61
+ // Any user defined validation takes priority over character validation
62
+ // If there is no user defined validation for character limits e.g. used maxLength prior to CharacterCounterField,
63
+ // use the default error codes and display the appropriate error message
64
+ const combinedValidate = (value, formState, fieldState) => {
65
+ // First run character validation
66
+ const characterError = validateCharacterCount(value);
67
+
68
+ // Then run user's custom validation if provided
69
+ const userError = userValidate === null || userValidate === void 0 ? void 0 : userValidate(value, formState, fieldState);
70
+
71
+ // If user validation returns a promise, handle it
72
+ if (userError instanceof Promise) {
73
+ return userError.then(error => {
74
+ // User error takes priority over character validation
75
+ return error || characterError;
76
+ });
77
+ }
78
+
79
+ // User error takes priority over character validation
80
+ return userError || characterError;
81
+ };
82
+ return /*#__PURE__*/React.createElement("div", {
83
+ className: ax([fieldWrapperStyles.root])
84
+ }, /*#__PURE__*/React.createElement(Field, {
85
+ defaultValue: defaultValue,
86
+ id: id,
87
+ isRequired: isRequired,
88
+ isDisabled: isDisabled,
89
+ label: label,
90
+ elementAfterLabel: elementAfterLabel,
91
+ name: name,
92
+ testId: testId,
93
+ validate: combinedValidate
94
+ }, ({
95
+ fieldProps: extendedFieldProps,
96
+ error,
97
+ valid,
98
+ meta
99
+ }) => {
100
+ // Determine if error is a character count violation (handled by CharacterCounter)
101
+ // or an external validation error (needs ErrorMessage)
102
+ const isCharacterCountViolation = error === '__TOO_SHORT__' || error === '__TOO_LONG__';
103
+ const showExternalError = error && !isCharacterCountViolation;
104
+ const showCharacterCounter = (maxCharacters !== undefined || minCharacters !== undefined) && !showExternalError;
105
+
106
+ // Extend aria-describedby to reference the appropriate message component
107
+ const fieldPropsWithCounter = {
108
+ ...extendedFieldProps,
109
+ 'aria-describedby': showCharacterCounter ? `${extendedFieldProps['aria-describedby']} ${extendedFieldProps.id}-character-counter`.trim() : extendedFieldProps['aria-describedby']
110
+ };
111
+ return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(MessageWrapper, null, helperMessage && /*#__PURE__*/React.createElement("div", {
112
+ className: ax([helperMessageWrapperStyles.root])
113
+ }, /*#__PURE__*/React.createElement(HelperMessage, {
114
+ testId: `${testId}-helper`
115
+ }, helperMessage))), children({
116
+ fieldProps: fieldPropsWithCounter,
117
+ error,
118
+ valid,
119
+ meta
120
+ }), /*#__PURE__*/React.createElement(MessageWrapper, null, showExternalError && /*#__PURE__*/React.createElement(ErrorMessage, {
121
+ testId: `${testId}-error`
122
+ }, error)), showCharacterCounter && /*#__PURE__*/React.createElement(CharacterCounter, {
123
+ maxCharacters: maxCharacters,
124
+ minCharacters: minCharacters,
125
+ currentValue: String(extendedFieldProps.value || ''),
126
+ shouldShowAsError: isCharacterCountViolation,
127
+ overMaximumMessage: overMaximumMessage,
128
+ underMaximumMessage: underMaximumMessage,
129
+ underMinimumMessage: underMinimumMessage,
130
+ testId: `${testId}-character-counter`
131
+ }));
132
+ }));
133
+ }
@@ -0,0 +1,8 @@
1
+ ._11c8dcr7{font:var(--ds-font-body-UNSAFE_small,normal 400 9pt/1pc ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",Ubuntu,"Helvetica Neue",sans-serif)}
2
+ ._zulp12x7{gap:var(--ds-space-075,6px)}
3
+ ._1bah1q9y{justify-content:baseline}
4
+ ._1e0c1txw{display:flex}
5
+ ._1pfh1b66{margin-block-start:var(--ds-space-050,4px)}
6
+ ._4cvr1h6o{align-items:center}
7
+ ._4t3i7vkz{height:1pc}
8
+ ._syaze6sf{color:var(--ds-text-danger,#ae2a19)}
@@ -0,0 +1,114 @@
1
+ /* character-counter.tsx generated by @compiled/babel-plugin v0.38.1 */
2
+ import "./character-counter.compiled.css";
3
+ import * as React from 'react';
4
+ import { ax, ix } from "@compiled/react/runtime";
5
+ import { useContext, useEffect, useRef, useState } from 'react';
6
+ import ErrorIcon from '@atlaskit/icon/core/migration/status-error--error';
7
+ import { Text } from '@atlaskit/primitives/compiled';
8
+ import VisuallyHidden from '@atlaskit/visually-hidden';
9
+ import { FieldId } from './field-id-context';
10
+
11
+ // Extracted styles for character counter message container
12
+ const messageContainerStyles = null;
13
+
14
+ // Extracted styles for error icon wrapper
15
+ const errorIconWrapperStyles = null;
16
+
17
+ // Error icon with wrapper for character count violations
18
+ const ErrorIconWithWrapper = () => /*#__PURE__*/React.createElement("span", {
19
+ className: ax(["_1e0c1txw _4t3i7vkz _4cvr1h6o"])
20
+ }, /*#__PURE__*/React.createElement(ErrorIcon, {
21
+ LEGACY_margin: "0 -2px 0 0",
22
+ LEGACY_size: "small",
23
+ label: "error",
24
+ size: "small"
25
+ }));
26
+ // Helper to pluralise "character(s)"
27
+ const pluralize = count => `character${count !== 1 ? 's' : ''}`;
28
+
29
+ /**
30
+ * __Character Counter__
31
+ *
32
+ * A character counter component that displays remaining characters for text input.
33
+ * Displays messages for over or under the maximum or minimum character limits.
34
+ */
35
+ export const CharacterCounter = ({
36
+ maxCharacters,
37
+ minCharacters,
38
+ currentValue,
39
+ overMaximumMessage,
40
+ underMaximumMessage,
41
+ underMinimumMessage,
42
+ shouldShowAsError = true,
43
+ inputId,
44
+ testId
45
+ }) => {
46
+ const [announcementText, setAnnouncementText] = useState('');
47
+ const debounceTimeoutRef = useRef(null);
48
+
49
+ // Resolve the field ID from context (form use) or inputId prop (standalone use)
50
+ const contextFieldId = useContext(FieldId);
51
+ const resolvedFieldId = contextFieldId || inputId;
52
+ const currentLength = (currentValue === null || currentValue === void 0 ? void 0 : currentValue.length) || 0;
53
+
54
+ // Check if character count violates limits
55
+ const isTooShort = minCharacters !== undefined && currentLength < minCharacters;
56
+ const isTooLong = maxCharacters !== undefined && currentLength > maxCharacters;
57
+
58
+ // Determine what to display based on the current value, the maximum and minimum character limits, and any custom messages
59
+ const getMessage = () => {
60
+ // Below minimum so show custom message or default
61
+ if (isTooShort) {
62
+ const needed = minCharacters - currentLength;
63
+ return underMinimumMessage || `${needed} more ${pluralize(needed)} needed`;
64
+ }
65
+
66
+ // Over maximum so show custom message or default
67
+ if (isTooLong) {
68
+ const over = currentLength - maxCharacters;
69
+ return overMaximumMessage || `${over} ${pluralize(over)} too many`;
70
+ }
71
+
72
+ // Within limits - show remaining count (if max is defined)
73
+ if (maxCharacters) {
74
+ const remaining = maxCharacters - currentLength;
75
+ return underMaximumMessage || `${remaining} ${pluralize(remaining)} remaining`;
76
+ }
77
+
78
+ // No message to show (min only limit satisfied)
79
+ return null;
80
+ };
81
+ const displayText = getMessage();
82
+
83
+ // Determine if the current character count violates limits
84
+ const displayAsError = (isTooShort || isTooLong) && shouldShowAsError;
85
+
86
+ // Debounce screen reader announcements so that it only reads the message when it input has settled
87
+ useEffect(() => {
88
+ // Debounce by 1 second to avoid announcing every keystroke
89
+ debounceTimeoutRef.current = setTimeout(() => {
90
+ setAnnouncementText(displayText || '');
91
+ }, 1000);
92
+
93
+ // Cleanup function clears the timeout when displayText changes or component unmounts
94
+ return () => {
95
+ clearTimeout(debounceTimeoutRef.current);
96
+ };
97
+ }, [displayText]);
98
+
99
+ // Don't render if there's no message to display (min only limit satisfied)
100
+ if (!displayText) {
101
+ return null;
102
+ }
103
+ return /*#__PURE__*/React.createElement("div", {
104
+ "data-testid": testId
105
+ }, /*#__PURE__*/React.createElement("div", {
106
+ className: ax(["_zulp12x7 _11c8dcr7 _1e0c1txw _1bah1q9y _syaze6sf _1pfh1b66"])
107
+ }, displayAsError && /*#__PURE__*/React.createElement(ErrorIconWithWrapper, null), /*#__PURE__*/React.createElement(Text, {
108
+ color: displayAsError ? 'color.text.danger' : 'color.text.subtlest',
109
+ size: "small",
110
+ id: resolvedFieldId ? `${resolvedFieldId}-character-counter` : undefined
111
+ }, displayText)), /*#__PURE__*/React.createElement(VisuallyHidden, null, /*#__PURE__*/React.createElement("div", {
112
+ "aria-live": "polite"
113
+ }, announcementText)));
114
+ };
@@ -9,4 +9,5 @@ export { Label, Legend } from './label';
9
9
  export { HelperMessage, ErrorMessage, MessageWrapper, ValidMessage } from './messages';
10
10
  export { default as Fieldset } from './fieldset';
11
11
  export { default as RequiredAsterisk } from './required-asterisk';
12
- export { useFormState } from './use-form-state';
12
+ export { useFormState } from './use-form-state';
13
+ export { default as CharacterCounterField } from './character-counter-field';
@@ -0,0 +1,3 @@
1
+ ._14uxze3t label[id$=-label]{margin-block-end:var(--ds-space-0,0)}
2
+ ._6rth1b66{margin-block-end:var(--ds-space-050,4px)}
3
+ ._6ul5ze3t [id$=-helper]{margin-block-start:var(--ds-space-0,0)}
@@ -0,0 +1,133 @@
1
+ /* character-counter-field.tsx generated by @compiled/babel-plugin v0.38.1 */
2
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
3
+ import "./character-counter-field.compiled.css";
4
+ import * as React from 'react';
5
+ import { ax, ix } from "@compiled/react/runtime";
6
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
7
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
8
+ import { Fragment } from 'react';
9
+ import { CharacterCounter } from './character-counter';
10
+ import Field from './field';
11
+ import { ErrorMessage, HelperMessage, MessageWrapper } from './messages';
12
+ // Override label specific margin block end to fix double spacing issue
13
+ var fieldWrapperStyles = {
14
+ root: "_14uxze3t"
15
+ };
16
+
17
+ // Override helper message margins to fix inconsistent spacing issue
18
+ var helperMessageWrapperStyles = {
19
+ root: "_6rth1b66 _6ul5ze3t"
20
+ };
21
+ /**
22
+ * __Character Counter Field__
23
+ *
24
+ * A field component that wraps the standard Field with automatic character count validation.
25
+ * Validates minimum and maximum character limits and displays a character counter.
26
+ */
27
+ export default function CharacterCounterField(_ref) {
28
+ var maxCharacters = _ref.maxCharacters,
29
+ minCharacters = _ref.minCharacters,
30
+ children = _ref.children,
31
+ userValidate = _ref.validate,
32
+ overMaximumMessage = _ref.overMaximumMessage,
33
+ underMaximumMessage = _ref.underMaximumMessage,
34
+ underMinimumMessage = _ref.underMinimumMessage,
35
+ helperMessage = _ref.helperMessage,
36
+ defaultValue = _ref.defaultValue,
37
+ id = _ref.id,
38
+ isRequired = _ref.isRequired,
39
+ isDisabled = _ref.isDisabled,
40
+ label = _ref.label,
41
+ elementAfterLabel = _ref.elementAfterLabel,
42
+ name = _ref.name,
43
+ testId = _ref.testId;
44
+ // Default validation function for character limits
45
+ // __TOO_SHORT__ and __TOO_LONG__ are default error codes recognised by the CharacterCounter component
46
+ var validateCharacterCount = function validateCharacterCount(value) {
47
+ var stringValue = String(value || '');
48
+ var length = stringValue.length;
49
+
50
+ // Check minimum length
51
+ if (minCharacters !== undefined && length < minCharacters) {
52
+ return '__TOO_SHORT__';
53
+ }
54
+
55
+ // Check maximum length
56
+ if (maxCharacters !== undefined && length > maxCharacters) {
57
+ return '__TOO_LONG__';
58
+ }
59
+ return undefined;
60
+ };
61
+
62
+ // Combine user validation and character validation
63
+ // Any user defined validation takes priority over character validation
64
+ // If there is no user defined validation for character limits e.g. used maxLength prior to CharacterCounterField,
65
+ // use the default error codes and display the appropriate error message
66
+ var combinedValidate = function combinedValidate(value, formState, fieldState) {
67
+ // First run character validation
68
+ var characterError = validateCharacterCount(value);
69
+
70
+ // Then run user's custom validation if provided
71
+ var userError = userValidate === null || userValidate === void 0 ? void 0 : userValidate(value, formState, fieldState);
72
+
73
+ // If user validation returns a promise, handle it
74
+ if (userError instanceof Promise) {
75
+ return userError.then(function (error) {
76
+ // User error takes priority over character validation
77
+ return error || characterError;
78
+ });
79
+ }
80
+
81
+ // User error takes priority over character validation
82
+ return userError || characterError;
83
+ };
84
+ return /*#__PURE__*/React.createElement("div", {
85
+ className: ax([fieldWrapperStyles.root])
86
+ }, /*#__PURE__*/React.createElement(Field, {
87
+ defaultValue: defaultValue,
88
+ id: id,
89
+ isRequired: isRequired,
90
+ isDisabled: isDisabled,
91
+ label: label,
92
+ elementAfterLabel: elementAfterLabel,
93
+ name: name,
94
+ testId: testId,
95
+ validate: combinedValidate
96
+ }, function (_ref2) {
97
+ var extendedFieldProps = _ref2.fieldProps,
98
+ error = _ref2.error,
99
+ valid = _ref2.valid,
100
+ meta = _ref2.meta;
101
+ // Determine if error is a character count violation (handled by CharacterCounter)
102
+ // or an external validation error (needs ErrorMessage)
103
+ var isCharacterCountViolation = error === '__TOO_SHORT__' || error === '__TOO_LONG__';
104
+ var showExternalError = error && !isCharacterCountViolation;
105
+ var showCharacterCounter = (maxCharacters !== undefined || minCharacters !== undefined) && !showExternalError;
106
+
107
+ // Extend aria-describedby to reference the appropriate message component
108
+ var fieldPropsWithCounter = _objectSpread(_objectSpread({}, extendedFieldProps), {}, {
109
+ 'aria-describedby': showCharacterCounter ? "".concat(extendedFieldProps['aria-describedby'], " ").concat(extendedFieldProps.id, "-character-counter").trim() : extendedFieldProps['aria-describedby']
110
+ });
111
+ return /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(MessageWrapper, null, helperMessage && /*#__PURE__*/React.createElement("div", {
112
+ className: ax([helperMessageWrapperStyles.root])
113
+ }, /*#__PURE__*/React.createElement(HelperMessage, {
114
+ testId: "".concat(testId, "-helper")
115
+ }, helperMessage))), children({
116
+ fieldProps: fieldPropsWithCounter,
117
+ error: error,
118
+ valid: valid,
119
+ meta: meta
120
+ }), /*#__PURE__*/React.createElement(MessageWrapper, null, showExternalError && /*#__PURE__*/React.createElement(ErrorMessage, {
121
+ testId: "".concat(testId, "-error")
122
+ }, error)), showCharacterCounter && /*#__PURE__*/React.createElement(CharacterCounter, {
123
+ maxCharacters: maxCharacters,
124
+ minCharacters: minCharacters,
125
+ currentValue: String(extendedFieldProps.value || ''),
126
+ shouldShowAsError: isCharacterCountViolation,
127
+ overMaximumMessage: overMaximumMessage,
128
+ underMaximumMessage: underMaximumMessage,
129
+ underMinimumMessage: underMinimumMessage,
130
+ testId: "".concat(testId, "-character-counter")
131
+ }));
132
+ }));
133
+ }
@@ -0,0 +1,8 @@
1
+ ._11c8dcr7{font:var(--ds-font-body-UNSAFE_small,normal 400 9pt/1pc ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",Ubuntu,"Helvetica Neue",sans-serif)}
2
+ ._zulp12x7{gap:var(--ds-space-075,6px)}
3
+ ._1bah1q9y{justify-content:baseline}
4
+ ._1e0c1txw{display:flex}
5
+ ._1pfh1b66{margin-block-start:var(--ds-space-050,4px)}
6
+ ._4cvr1h6o{align-items:center}
7
+ ._4t3i7vkz{height:1pc}
8
+ ._syaze6sf{color:var(--ds-text-danger,#ae2a19)}
@@ -0,0 +1,122 @@
1
+ /* character-counter.tsx generated by @compiled/babel-plugin v0.38.1 */
2
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
+ import "./character-counter.compiled.css";
4
+ import * as React from 'react';
5
+ import { ax, ix } from "@compiled/react/runtime";
6
+ import { useContext, useEffect, useRef, useState } from 'react';
7
+ import ErrorIcon from '@atlaskit/icon/core/migration/status-error--error';
8
+ import { Text } from '@atlaskit/primitives/compiled';
9
+ import VisuallyHidden from '@atlaskit/visually-hidden';
10
+ import { FieldId } from './field-id-context';
11
+
12
+ // Extracted styles for character counter message container
13
+ var messageContainerStyles = null;
14
+
15
+ // Extracted styles for error icon wrapper
16
+ var errorIconWrapperStyles = null;
17
+
18
+ // Error icon with wrapper for character count violations
19
+ var ErrorIconWithWrapper = function ErrorIconWithWrapper() {
20
+ return /*#__PURE__*/React.createElement("span", {
21
+ className: ax(["_1e0c1txw _4t3i7vkz _4cvr1h6o"])
22
+ }, /*#__PURE__*/React.createElement(ErrorIcon, {
23
+ LEGACY_margin: "0 -2px 0 0",
24
+ LEGACY_size: "small",
25
+ label: "error",
26
+ size: "small"
27
+ }));
28
+ };
29
+ // Helper to pluralise "character(s)"
30
+ var pluralize = function pluralize(count) {
31
+ return "character".concat(count !== 1 ? 's' : '');
32
+ };
33
+
34
+ /**
35
+ * __Character Counter__
36
+ *
37
+ * A character counter component that displays remaining characters for text input.
38
+ * Displays messages for over or under the maximum or minimum character limits.
39
+ */
40
+ export var CharacterCounter = function CharacterCounter(_ref) {
41
+ var maxCharacters = _ref.maxCharacters,
42
+ minCharacters = _ref.minCharacters,
43
+ currentValue = _ref.currentValue,
44
+ overMaximumMessage = _ref.overMaximumMessage,
45
+ underMaximumMessage = _ref.underMaximumMessage,
46
+ underMinimumMessage = _ref.underMinimumMessage,
47
+ _ref$shouldShowAsErro = _ref.shouldShowAsError,
48
+ shouldShowAsError = _ref$shouldShowAsErro === void 0 ? true : _ref$shouldShowAsErro,
49
+ inputId = _ref.inputId,
50
+ testId = _ref.testId;
51
+ var _useState = useState(''),
52
+ _useState2 = _slicedToArray(_useState, 2),
53
+ announcementText = _useState2[0],
54
+ setAnnouncementText = _useState2[1];
55
+ var debounceTimeoutRef = useRef(null);
56
+
57
+ // Resolve the field ID from context (form use) or inputId prop (standalone use)
58
+ var contextFieldId = useContext(FieldId);
59
+ var resolvedFieldId = contextFieldId || inputId;
60
+ var currentLength = (currentValue === null || currentValue === void 0 ? void 0 : currentValue.length) || 0;
61
+
62
+ // Check if character count violates limits
63
+ var isTooShort = minCharacters !== undefined && currentLength < minCharacters;
64
+ var isTooLong = maxCharacters !== undefined && currentLength > maxCharacters;
65
+
66
+ // Determine what to display based on the current value, the maximum and minimum character limits, and any custom messages
67
+ var getMessage = function getMessage() {
68
+ // Below minimum so show custom message or default
69
+ if (isTooShort) {
70
+ var needed = minCharacters - currentLength;
71
+ return underMinimumMessage || "".concat(needed, " more ").concat(pluralize(needed), " needed");
72
+ }
73
+
74
+ // Over maximum so show custom message or default
75
+ if (isTooLong) {
76
+ var over = currentLength - maxCharacters;
77
+ return overMaximumMessage || "".concat(over, " ").concat(pluralize(over), " too many");
78
+ }
79
+
80
+ // Within limits - show remaining count (if max is defined)
81
+ if (maxCharacters) {
82
+ var remaining = maxCharacters - currentLength;
83
+ return underMaximumMessage || "".concat(remaining, " ").concat(pluralize(remaining), " remaining");
84
+ }
85
+
86
+ // No message to show (min only limit satisfied)
87
+ return null;
88
+ };
89
+ var displayText = getMessage();
90
+
91
+ // Determine if the current character count violates limits
92
+ var displayAsError = (isTooShort || isTooLong) && shouldShowAsError;
93
+
94
+ // Debounce screen reader announcements so that it only reads the message when it input has settled
95
+ useEffect(function () {
96
+ // Debounce by 1 second to avoid announcing every keystroke
97
+ debounceTimeoutRef.current = setTimeout(function () {
98
+ setAnnouncementText(displayText || '');
99
+ }, 1000);
100
+
101
+ // Cleanup function clears the timeout when displayText changes or component unmounts
102
+ return function () {
103
+ clearTimeout(debounceTimeoutRef.current);
104
+ };
105
+ }, [displayText]);
106
+
107
+ // Don't render if there's no message to display (min only limit satisfied)
108
+ if (!displayText) {
109
+ return null;
110
+ }
111
+ return /*#__PURE__*/React.createElement("div", {
112
+ "data-testid": testId
113
+ }, /*#__PURE__*/React.createElement("div", {
114
+ className: ax(["_zulp12x7 _11c8dcr7 _1e0c1txw _1bah1q9y _syaze6sf _1pfh1b66"])
115
+ }, displayAsError && /*#__PURE__*/React.createElement(ErrorIconWithWrapper, null), /*#__PURE__*/React.createElement(Text, {
116
+ color: displayAsError ? 'color.text.danger' : 'color.text.subtlest',
117
+ size: "small",
118
+ id: resolvedFieldId ? "".concat(resolvedFieldId, "-character-counter") : undefined
119
+ }, displayText)), /*#__PURE__*/React.createElement(VisuallyHidden, null, /*#__PURE__*/React.createElement("div", {
120
+ "aria-live": "polite"
121
+ }, announcementText)));
122
+ };
package/dist/esm/index.js CHANGED
@@ -9,4 +9,5 @@ export { Label, Legend } from './label';
9
9
  export { HelperMessage, ErrorMessage, MessageWrapper, ValidMessage } from './messages';
10
10
  export { default as Fieldset } from './fieldset';
11
11
  export { default as RequiredAsterisk } from './required-asterisk';
12
- export { useFormState } from './use-form-state';
12
+ export { useFormState } from './use-form-state';
13
+ export { default as CharacterCounterField } from './character-counter-field';
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @jsxRuntime classic
3
+ * @jsx jsx
4
+ */
5
+ import { type ReactNode } from 'react';
6
+ import { type FieldComponentProps, type FieldProps, type Meta } from './field';
7
+ type SupportedElements = HTMLInputElement | HTMLTextAreaElement;
8
+ export interface CharacterCounterFieldProps<FieldValue = string, Element extends SupportedElements = HTMLInputElement> extends Omit<FieldComponentProps<FieldValue, Element>, 'children' | 'component' | 'helperMessage' | 'errorMessage' | 'validMessage' | 'transform'> {
9
+ /**
10
+ * The input component to render. Use a render function that receives `fieldProps`, `error`, `valid`, and `meta` state.
11
+ * Spread `fieldProps` onto your input element (such as `TextField` or `TextArea`).
12
+ */
13
+ children: (args: {
14
+ fieldProps: FieldProps<FieldValue, Element>;
15
+ error?: string;
16
+ valid: boolean;
17
+ meta: Meta;
18
+ }) => ReactNode;
19
+ /**
20
+ * Helper text displayed above the input to provide additional context or instructions.
21
+ */
22
+ helperMessage?: ReactNode;
23
+ /**
24
+ * Maximum number of characters allowed. When exceeded, the field displays an error message or the message provided by `overMaximumMessage`.
25
+ */
26
+ maxCharacters?: number;
27
+ /**
28
+ * Minimum number of characters required. When not met, the character counter displays an error message or the message provided by `underMinimumMessage`.
29
+ */
30
+ minCharacters?: number;
31
+ /**
32
+ * Custom message displayed when input exceeds the maximum character limit. Use this to provide context-specific guidance or localized messages. Overrides the default "X characters too many" message.
33
+ */
34
+ overMaximumMessage?: string;
35
+ /**
36
+ * Custom message displayed when input is under the maximum limit. Use this to provide context-specific guidance or localized messages. Overrides the default "X characters remaining" message.
37
+ */
38
+ underMaximumMessage?: string;
39
+ /**
40
+ * Custom message displayed when input is under the minimum requirement. Use this to guide users on how much more they need to type. Overrides the default "Minimum of X characters required" message.
41
+ */
42
+ underMinimumMessage?: string;
43
+ }
44
+ /**
45
+ * __Character Counter Field__
46
+ *
47
+ * A field component that wraps the standard Field with automatic character count validation.
48
+ * Validates minimum and maximum character limits and displays a character counter.
49
+ */
50
+ export default function CharacterCounterField<FieldValue = string, Element extends SupportedElements = HTMLInputElement>({ maxCharacters, minCharacters, children, validate: userValidate, overMaximumMessage, underMaximumMessage, underMinimumMessage, helperMessage, defaultValue, id, isRequired, isDisabled, label, elementAfterLabel, name, testId, }: CharacterCounterFieldProps<FieldValue, Element>): JSX.Element;
51
+ export {};
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @jsxRuntime classic
3
+ * @jsx jsx
4
+ */
5
+ export interface CharacterCounterProps {
6
+ /**
7
+ * Maximum number of characters allowed (optional)
8
+ */
9
+ maxCharacters?: number;
10
+ /**
11
+ * Minimum number of characters required (optional)
12
+ */
13
+ minCharacters?: number;
14
+ /**
15
+ * Current value of the input field
16
+ */
17
+ currentValue?: string;
18
+ /**
19
+ * Optional custom message to display when character limit is exceeded
20
+ */
21
+ overMaximumMessage?: string;
22
+ /**
23
+ * Optional custom message to display when character limit is not exceeded
24
+ */
25
+ underMaximumMessage?: string;
26
+ /**
27
+ * Optional custom message to display when minimum character requirement is not met
28
+ */
29
+ underMinimumMessage?: string;
30
+ /**
31
+ * Whether to style violations as errors (red text + icon).
32
+ * By default, violations are automatically styled as errors.
33
+ *
34
+ * In forms, set this to false to suppress error styling when
35
+ * the form hasn't flagged an error yet (e.g., field not touched).
36
+ *
37
+ * // Standalone: smart default (violations = errors)
38
+ * <CharacterCounter currentValue={value} maxCharacters={100} />
39
+ *
40
+ * // Form: align with final-form error state
41
+ * <CharacterCounter
42
+ * currentValue={value}
43
+ * maxCharacters={100}
44
+ * shouldShowAsError={isCharacterCountViolation}
45
+ * />
46
+ */
47
+ shouldShowAsError?: boolean;
48
+ /**
49
+ * ID of the associated input for accessibility.
50
+ * Not needed if the character counter is used within CharacterCounterField.
51
+ * When provided, the character counter will have an ID of `${inputId}-character-counter`
52
+ * which should be referenced in the input's `aria-describedby` attribute.
53
+ * If not provided, will attempt to use InputId context from Form.
54
+ */
55
+ inputId?: string;
56
+ /**
57
+ * A testId prop is provided for specified elements, which is a unique string
58
+ * that appears as a data attribute data-testid in the rendered code,
59
+ * serving as a hook for automated tests
60
+ */
61
+ testId?: string;
62
+ }
63
+ /**
64
+ * __Character Counter__
65
+ *
66
+ * A character counter component that displays remaining characters for text input.
67
+ * Displays messages for over or under the maximum or minimum character limits.
68
+ */
69
+ export declare const CharacterCounter: ({ maxCharacters, minCharacters, currentValue, overMaximumMessage, underMaximumMessage, underMinimumMessage, shouldShowAsError, inputId, testId, }: CharacterCounterProps) => JSX.Element | null;
@@ -16,3 +16,5 @@ export { default as Fieldset } from './fieldset';
16
16
  export { default as RequiredAsterisk } from './required-asterisk';
17
17
  export type { OnSubmitHandler, FormApi } from './types';
18
18
  export { useFormState } from './use-form-state';
19
+ export { default as CharacterCounterField } from './character-counter-field';
20
+ export type { CharacterCounterFieldProps } from './character-counter-field';
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @jsxRuntime classic
3
+ * @jsx jsx
4
+ */
5
+ import { type ReactNode } from 'react';
6
+ import { type FieldComponentProps, type FieldProps, type Meta } from './field';
7
+ type SupportedElements = HTMLInputElement | HTMLTextAreaElement;
8
+ export interface CharacterCounterFieldProps<FieldValue = string, Element extends SupportedElements = HTMLInputElement> extends Omit<FieldComponentProps<FieldValue, Element>, 'children' | 'component' | 'helperMessage' | 'errorMessage' | 'validMessage' | 'transform'> {
9
+ /**
10
+ * The input component to render. Use a render function that receives `fieldProps`, `error`, `valid`, and `meta` state.
11
+ * Spread `fieldProps` onto your input element (such as `TextField` or `TextArea`).
12
+ */
13
+ children: (args: {
14
+ fieldProps: FieldProps<FieldValue, Element>;
15
+ error?: string;
16
+ valid: boolean;
17
+ meta: Meta;
18
+ }) => ReactNode;
19
+ /**
20
+ * Helper text displayed above the input to provide additional context or instructions.
21
+ */
22
+ helperMessage?: ReactNode;
23
+ /**
24
+ * Maximum number of characters allowed. When exceeded, the field displays an error message or the message provided by `overMaximumMessage`.
25
+ */
26
+ maxCharacters?: number;
27
+ /**
28
+ * Minimum number of characters required. When not met, the character counter displays an error message or the message provided by `underMinimumMessage`.
29
+ */
30
+ minCharacters?: number;
31
+ /**
32
+ * Custom message displayed when input exceeds the maximum character limit. Use this to provide context-specific guidance or localized messages. Overrides the default "X characters too many" message.
33
+ */
34
+ overMaximumMessage?: string;
35
+ /**
36
+ * Custom message displayed when input is under the maximum limit. Use this to provide context-specific guidance or localized messages. Overrides the default "X characters remaining" message.
37
+ */
38
+ underMaximumMessage?: string;
39
+ /**
40
+ * Custom message displayed when input is under the minimum requirement. Use this to guide users on how much more they need to type. Overrides the default "Minimum of X characters required" message.
41
+ */
42
+ underMinimumMessage?: string;
43
+ }
44
+ /**
45
+ * __Character Counter Field__
46
+ *
47
+ * A field component that wraps the standard Field with automatic character count validation.
48
+ * Validates minimum and maximum character limits and displays a character counter.
49
+ */
50
+ export default function CharacterCounterField<FieldValue = string, Element extends SupportedElements = HTMLInputElement>({ maxCharacters, minCharacters, children, validate: userValidate, overMaximumMessage, underMaximumMessage, underMinimumMessage, helperMessage, defaultValue, id, isRequired, isDisabled, label, elementAfterLabel, name, testId, }: CharacterCounterFieldProps<FieldValue, Element>): JSX.Element;
51
+ export {};
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @jsxRuntime classic
3
+ * @jsx jsx
4
+ */
5
+ export interface CharacterCounterProps {
6
+ /**
7
+ * Maximum number of characters allowed (optional)
8
+ */
9
+ maxCharacters?: number;
10
+ /**
11
+ * Minimum number of characters required (optional)
12
+ */
13
+ minCharacters?: number;
14
+ /**
15
+ * Current value of the input field
16
+ */
17
+ currentValue?: string;
18
+ /**
19
+ * Optional custom message to display when character limit is exceeded
20
+ */
21
+ overMaximumMessage?: string;
22
+ /**
23
+ * Optional custom message to display when character limit is not exceeded
24
+ */
25
+ underMaximumMessage?: string;
26
+ /**
27
+ * Optional custom message to display when minimum character requirement is not met
28
+ */
29
+ underMinimumMessage?: string;
30
+ /**
31
+ * Whether to style violations as errors (red text + icon).
32
+ * By default, violations are automatically styled as errors.
33
+ *
34
+ * In forms, set this to false to suppress error styling when
35
+ * the form hasn't flagged an error yet (e.g., field not touched).
36
+ *
37
+ * // Standalone: smart default (violations = errors)
38
+ * <CharacterCounter currentValue={value} maxCharacters={100} />
39
+ *
40
+ * // Form: align with final-form error state
41
+ * <CharacterCounter
42
+ * currentValue={value}
43
+ * maxCharacters={100}
44
+ * shouldShowAsError={isCharacterCountViolation}
45
+ * />
46
+ */
47
+ shouldShowAsError?: boolean;
48
+ /**
49
+ * ID of the associated input for accessibility.
50
+ * Not needed if the character counter is used within CharacterCounterField.
51
+ * When provided, the character counter will have an ID of `${inputId}-character-counter`
52
+ * which should be referenced in the input's `aria-describedby` attribute.
53
+ * If not provided, will attempt to use InputId context from Form.
54
+ */
55
+ inputId?: string;
56
+ /**
57
+ * A testId prop is provided for specified elements, which is a unique string
58
+ * that appears as a data attribute data-testid in the rendered code,
59
+ * serving as a hook for automated tests
60
+ */
61
+ testId?: string;
62
+ }
63
+ /**
64
+ * __Character Counter__
65
+ *
66
+ * A character counter component that displays remaining characters for text input.
67
+ * Displays messages for over or under the maximum or minimum character limits.
68
+ */
69
+ export declare const CharacterCounter: ({ maxCharacters, minCharacters, currentValue, overMaximumMessage, underMaximumMessage, underMinimumMessage, shouldShowAsError, inputId, testId, }: CharacterCounterProps) => JSX.Element | null;
@@ -16,3 +16,5 @@ export { default as Fieldset } from './fieldset';
16
16
  export { default as RequiredAsterisk } from './required-asterisk';
17
17
  export type { OnSubmitHandler, FormApi } from './types';
18
18
  export { useFormState } from './use-form-state';
19
+ export { default as CharacterCounterField } from './character-counter-field';
20
+ export type { CharacterCounterFieldProps } from './character-counter-field';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/form",
3
- "version": "14.3.2",
3
+ "version": "14.4.0",
4
4
  "description": "A form allows users to input information.",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -32,7 +32,9 @@
32
32
  "@atlaskit/platform-feature-flags": "^1.1.0",
33
33
  "@atlaskit/primitives": "^16.4.0",
34
34
  "@atlaskit/tokens": "^8.4.0",
35
+ "@atlaskit/visually-hidden": "^3.0.0",
35
36
  "@babel/runtime": "^7.0.0",
37
+ "@compiled/react": "^0.18.6",
36
38
  "final-form": "^4.20.3",
37
39
  "final-form-focus": "^1.1.2",
38
40
  "lodash": "^4.17.21"
@@ -45,21 +47,21 @@
45
47
  "@af/integration-testing": "workspace:^",
46
48
  "@af/visual-regression": "workspace:^",
47
49
  "@atlaskit/banner": "^14.0.0",
48
- "@atlaskit/button": "^23.6.0",
49
- "@atlaskit/checkbox": "^17.1.0",
50
+ "@atlaskit/button": "^23.7.0",
51
+ "@atlaskit/checkbox": "^17.2.0",
50
52
  "@atlaskit/codemod-utils": "^4.2.0",
51
- "@atlaskit/datetime-picker": "^17.1.0",
53
+ "@atlaskit/datetime-picker": "^17.2.0",
52
54
  "@atlaskit/docs": "^11.2.0",
53
55
  "@atlaskit/link": "^3.2.0",
54
56
  "@atlaskit/lozenge": "^13.1.0",
55
- "@atlaskit/modal-dialog": "^14.7.0",
57
+ "@atlaskit/modal-dialog": "^14.8.0",
56
58
  "@atlaskit/radio": "^8.3.0",
57
- "@atlaskit/range": "^9.2.0",
58
- "@atlaskit/section-message": "^8.9.0",
59
- "@atlaskit/select": "^21.4.0",
60
- "@atlaskit/textarea": "^8.1.0",
61
- "@atlaskit/textfield": "^8.1.0",
62
- "@atlaskit/toggle": "^15.1.0",
59
+ "@atlaskit/range": "^9.3.0",
60
+ "@atlaskit/section-message": "^8.10.0",
61
+ "@atlaskit/select": "^21.5.0",
62
+ "@atlaskit/textarea": "^8.2.0",
63
+ "@atlaskit/textfield": "^8.2.0",
64
+ "@atlaskit/toggle": "^15.2.0",
63
65
  "@atlassian/feature-flags-test-utils": "^1.0.0",
64
66
  "@atlassian/ssr-tests": "workspace:^",
65
67
  "@testing-library/react": "^13.4.0",