@availity/mui-textfield 1.0.1 → 1.1.1

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
@@ -2,6 +2,19 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.1.1](https://github.com/Availity/element/compare/@availity/mui-textfield@1.1.0...@availity/mui-textfield@1.1.1) (2025-03-27)
6
+
7
+ ## [1.1.0](https://github.com/Availity/element/compare/@availity/mui-textfield@1.0.1...@availity/mui-textfield@1.1.0) (2025-03-21)
8
+
9
+ ### Dependency Updates
10
+
11
+ * `mui-form-utils` updated to version `1.0.1`
12
+
13
+ ### Features
14
+
15
+ * **mui-textfield, mui-form-utils:** add placeholder support to select ([3a6b1ef](https://github.com/Availity/element/commit/3a6b1ef2e5ceedca632bd1916734e56402371ffb))
16
+ * **mui-textfield:** add optional character count to textfield ([1c3f0dc](https://github.com/Availity/element/commit/1c3f0dc886e153c79afc81c98b3326ed8990b305))
17
+
5
18
  ## [1.0.1](https://github.com/Availity/element/compare/@availity/mui-textfield@1.0.0...@availity/mui-textfield@1.0.1) (2025-03-07)
6
19
 
7
20
  ### Dependency Updates
package/dist/index.d.mts CHANGED
@@ -9,7 +9,17 @@ type TextFieldProps = {
9
9
  SelectProps?: SelectProps;
10
10
  /** If `true`, the input will take up the full width of its container. @default true */
11
11
  fullWidth?: boolean;
12
- } & Pick<FormLabelProps, 'helpTopicId'> & Omit<TextFieldProps$1, 'fullWidth' | 'variant' | 'slotProps'>;
12
+ /** if `true`, the character counter will display. The maxLength is taken from the `inputProps.maxLength` prop. @default false */
13
+ showCharacterCount?: boolean;
14
+ /** If `true`, the input maxLength can be exceeded. If validation is required, you'll have to do it manually. @default false */
15
+ displayOverflowMaxLength?: boolean;
16
+ } & Pick<FormLabelProps, 'helpTopicId'> & Omit<TextFieldProps$1, 'fullWidth' | 'variant'>;
17
+ type TextFieldFormHelperTextProps = {
18
+ charCount: string;
19
+ helperText: string;
20
+ maxLength: string;
21
+ showCharacterCount: boolean;
22
+ };
13
23
  declare const TextField: react.ForwardRefExoticComponent<Omit<TextFieldProps, "ref"> & react.RefAttributes<HTMLDivElement | HTMLInputElement>>;
14
24
 
15
- export { TextField, type TextFieldProps };
25
+ export { TextField, type TextFieldFormHelperTextProps, type TextFieldProps };
package/dist/index.d.ts CHANGED
@@ -9,7 +9,17 @@ type TextFieldProps = {
9
9
  SelectProps?: SelectProps;
10
10
  /** If `true`, the input will take up the full width of its container. @default true */
11
11
  fullWidth?: boolean;
12
- } & Pick<FormLabelProps, 'helpTopicId'> & Omit<TextFieldProps$1, 'fullWidth' | 'variant' | 'slotProps'>;
12
+ /** if `true`, the character counter will display. The maxLength is taken from the `inputProps.maxLength` prop. @default false */
13
+ showCharacterCount?: boolean;
14
+ /** If `true`, the input maxLength can be exceeded. If validation is required, you'll have to do it manually. @default false */
15
+ displayOverflowMaxLength?: boolean;
16
+ } & Pick<FormLabelProps, 'helpTopicId'> & Omit<TextFieldProps$1, 'fullWidth' | 'variant'>;
17
+ type TextFieldFormHelperTextProps = {
18
+ charCount: string;
19
+ helperText: string;
20
+ maxLength: string;
21
+ showCharacterCount: boolean;
22
+ };
13
23
  declare const TextField: react.ForwardRefExoticComponent<Omit<TextFieldProps, "ref"> & react.RefAttributes<HTMLDivElement | HTMLInputElement>>;
14
24
 
15
- export { TextField, type TextFieldProps };
25
+ export { TextField, type TextFieldFormHelperTextProps, type TextFieldProps };
package/dist/index.js CHANGED
@@ -64,27 +64,134 @@ __export(index_exports, {
64
64
  module.exports = __toCommonJS(index_exports);
65
65
 
66
66
  // src/lib/TextField.tsx
67
- var import_react = require("react");
67
+ var import_react3 = require("react");
68
68
  var import_TextField = __toESM(require("@mui/material/TextField"));
69
69
  var import_mui_form_utils = require("@availity/mui-form-utils");
70
+
71
+ // ../layout/src/lib/Box.tsx
72
+ var import_react = require("react");
73
+ var import_Box = __toESM(require("@mui/material/Box"));
70
74
  var import_jsx_runtime = require("react/jsx-runtime");
71
- var TextField = (0, import_react.forwardRef)((props, ref) => {
72
- const _a = props, { InputProps: InputProps2, helpTopicId, InputLabelProps, FormHelperTextProps: FormHelperTextProps2, required, SelectProps: SelectProps2, inputProps } = _a, rest = __objRest(_a, ["InputProps", "helpTopicId", "InputLabelProps", "FormHelperTextProps", "required", "SelectProps", "inputProps"]);
73
- const [openDetected, setOpenDetected] = (0, import_react.useState)(false);
74
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
75
+ var Box = (0, import_react.forwardRef)((props, ref) => {
76
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_Box.default, __spreadProps(__spreadValues({}, props), { ref }));
77
+ });
78
+
79
+ // ../layout/src/lib/Container.tsx
80
+ var import_Container = __toESM(require("@mui/material/Container"));
81
+ var import_jsx_runtime2 = require("react/jsx-runtime");
82
+
83
+ // ../layout/src/lib/Grid.tsx
84
+ var import_Grid2 = __toESM(require("@mui/material/Grid2"));
85
+ var import_jsx_runtime3 = require("react/jsx-runtime");
86
+ var Grid = (args) => {
87
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_Grid2.default, __spreadValues({}, args));
88
+ };
89
+
90
+ // ../layout/src/lib/Stack.tsx
91
+ var import_Stack = __toESM(require("@mui/material/Stack"));
92
+ var import_jsx_runtime4 = require("react/jsx-runtime");
93
+
94
+ // ../typography/src/lib/Typography.tsx
95
+ var import_react2 = require("react");
96
+ var import_Typography = __toESM(require("@mui/material/Typography"));
97
+ var import_jsx_runtime5 = require("react/jsx-runtime");
98
+ var Typography = (0, import_react2.forwardRef)(
99
+ (_a, ref) => {
100
+ var _b = _a, { children } = _b, rest = __objRest(_b, ["children"]);
101
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_Typography.default, __spreadProps(__spreadValues({}, rest), { ref, children }));
102
+ }
103
+ );
104
+
105
+ // src/lib/TextField.tsx
106
+ var import_styles = require("@mui/material/styles");
107
+ var import_jsx_runtime6 = require("react/jsx-runtime");
108
+ var SelectPlaceholder = (0, import_styles.styled)("span", {
109
+ name: "MuiTextField",
110
+ slot: "SelectPlaceholder",
111
+ overridesResolver: (props, styles) => styles.avFilled
112
+ })(({ theme }) => ({ opacity: 1, color: theme.palette.grey[400] }));
113
+ var TextFieldFormHelperText = ({
114
+ charCount,
115
+ helperText,
116
+ maxLength,
117
+ showCharacterCount
118
+ }) => {
119
+ if (showCharacterCount) {
120
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Grid, { container: true, justifyContent: "space-between", flexWrap: "nowrap", children: [
121
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_mui_form_utils.FormHelperText, { sx: { marginRight: "12px" }, children: helperText }),
122
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(Typography, { variant: "caption", marginTop: "4px", lineHeight: "1.25rem", children: [
123
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Typography, { component: "span", variant: "inherit", color: charCount > maxLength ? "error" : "textPrimary", children: charCount || 0 }),
124
+ "/",
125
+ maxLength
126
+ ] })
127
+ ] });
128
+ }
129
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_mui_form_utils.FormHelperText, { children: helperText });
130
+ };
131
+ var TextField = (0, import_react3.forwardRef)((props, ref) => {
132
+ var _b, _c, _d, _e, _f, _g, _h;
133
+ const _a = props, {
134
+ InputProps: InputProps2,
135
+ helpTopicId,
136
+ InputLabelProps,
137
+ FormHelperTextProps: FormHelperTextProps2,
138
+ required,
139
+ SelectProps: SelectProps2,
140
+ inputProps,
141
+ helperText,
142
+ showCharacterCount = false,
143
+ displayOverflowMaxLength = false
144
+ } = _a, rest = __objRest(_a, [
145
+ "InputProps",
146
+ "helpTopicId",
147
+ "InputLabelProps",
148
+ "FormHelperTextProps",
149
+ "required",
150
+ "SelectProps",
151
+ "inputProps",
152
+ "helperText",
153
+ "showCharacterCount",
154
+ "displayOverflowMaxLength"
155
+ ]);
156
+ const [openDetected, setOpenDetected] = (0, import_react3.useState)(false);
157
+ const [charCount, setCharCount] = (0, import_react3.useState)(0);
158
+ const maxLength = (inputProps == null ? void 0 : inputProps.maxLength) || ((_c = (_b = rest.slotProps) == null ? void 0 : _b.htmlInput) == null ? void 0 : _c.maxLength);
159
+ const resolvedProps = (props2) => !props2 || Object.keys(props2).length === 0 ? void 0 : props2;
160
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
75
161
  import_TextField.default,
76
162
  __spreadProps(__spreadValues({}, rest), {
163
+ onChange: (event) => {
164
+ setCharCount(event.target.value.length);
165
+ if (rest.onChange) rest.onChange(event);
166
+ },
167
+ helperText: helperText || /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_jsx_runtime6.Fragment, {}),
168
+ slots: { formHelperText: TextFieldFormHelperText },
77
169
  slotProps: {
78
- input: __spreadValues(__spreadValues({}, InputProps2), import_mui_form_utils.InputPropOverrides),
79
- htmlInput: __spreadValues({ "aria-required": required }, inputProps),
80
- select: __spreadValues(__spreadValues(__spreadValues({}, SelectProps2), import_mui_form_utils.SelectPropOverrides), (0, import_mui_form_utils.SelectAccessibilityOverrides)(openDetected, setOpenDetected, SelectProps2 == null ? void 0 : SelectProps2.open)),
81
- inputLabel: __spreadValues({
170
+ input: resolvedProps(__spreadValues(__spreadValues(__spreadValues({}, InputProps2), import_mui_form_utils.InputPropOverrides), (_d = rest.slotProps) == null ? void 0 : _d.input)),
171
+ htmlInput: resolvedProps(__spreadProps(__spreadValues(__spreadValues({
172
+ "aria-required": required
173
+ }, inputProps), (_e = rest.slotProps) == null ? void 0 : _e.htmlInput), {
174
+ maxLength: !displayOverflowMaxLength ? maxLength : void 0
175
+ })),
176
+ select: resolvedProps(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
177
+ displayEmpty: !!rest.placeholder,
178
+ renderValue: (value) => rest.placeholder && (!value || Array.isArray(value) && value.length === 0) ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SelectPlaceholder, { className: "MuiSelect-placeholder", children: rest.placeholder }) : value
179
+ }, SelectProps2), import_mui_form_utils.SelectPropOverrides), (0, import_mui_form_utils.SelectAccessibilityOverrides)(openDetected, setOpenDetected, SelectProps2 == null ? void 0 : SelectProps2.open)), (_f = rest.slotProps) == null ? void 0 : _f.select)),
180
+ inputLabel: resolvedProps(__spreadValues(__spreadValues({
82
181
  component: import_mui_form_utils.FormLabel,
83
182
  helpTopicId,
84
183
  required,
85
184
  shrink: true
86
- }, InputLabelProps),
87
- formHelperText: __spreadValues({ component: import_mui_form_utils.FormHelperText }, FormHelperTextProps2)
185
+ }, InputLabelProps), (_g = rest.slotProps) == null ? void 0 : _g.inputLabel)),
186
+ formHelperText: resolvedProps(__spreadProps(__spreadValues(__spreadValues({
187
+ component: "div"
188
+ }, FormHelperTextProps2), (_h = rest.slotProps) == null ? void 0 : _h.formHelperText), {
189
+ charCount,
190
+ helperText,
191
+ maxLength,
192
+ displayOverflowMaxLength,
193
+ showCharacterCount
194
+ }))
88
195
  },
89
196
  ref
90
197
  })
package/dist/index.mjs CHANGED
@@ -31,7 +31,7 @@ var __objRest = (source, exclude) => {
31
31
  };
32
32
 
33
33
  // src/lib/TextField.tsx
34
- import { forwardRef, useState } from "react";
34
+ import { forwardRef as forwardRef3, useState } from "react";
35
35
  import MuiTextField from "@mui/material/TextField";
36
36
  import {
37
37
  FormHelperText,
@@ -40,24 +40,131 @@ import {
40
40
  SelectAccessibilityOverrides,
41
41
  SelectPropOverrides
42
42
  } from "@availity/mui-form-utils";
43
+
44
+ // ../layout/src/lib/Box.tsx
45
+ import { forwardRef } from "react";
46
+ import MuiBox from "@mui/material/Box";
43
47
  import { jsx } from "react/jsx-runtime";
44
- var TextField = forwardRef((props, ref) => {
45
- const _a = props, { InputProps: InputProps2, helpTopicId, InputLabelProps, FormHelperTextProps: FormHelperTextProps2, required, SelectProps: SelectProps2, inputProps } = _a, rest = __objRest(_a, ["InputProps", "helpTopicId", "InputLabelProps", "FormHelperTextProps", "required", "SelectProps", "inputProps"]);
48
+ var Box = forwardRef((props, ref) => {
49
+ return /* @__PURE__ */ jsx(MuiBox, __spreadProps(__spreadValues({}, props), { ref }));
50
+ });
51
+
52
+ // ../layout/src/lib/Container.tsx
53
+ import MuiContainer from "@mui/material/Container";
54
+ import { jsx as jsx2 } from "react/jsx-runtime";
55
+
56
+ // ../layout/src/lib/Grid.tsx
57
+ import MuiGrid2 from "@mui/material/Grid2";
58
+ import { jsx as jsx3 } from "react/jsx-runtime";
59
+ var Grid = (args) => {
60
+ return /* @__PURE__ */ jsx3(MuiGrid2, __spreadValues({}, args));
61
+ };
62
+
63
+ // ../layout/src/lib/Stack.tsx
64
+ import MuiStack from "@mui/material/Stack";
65
+ import { jsx as jsx4 } from "react/jsx-runtime";
66
+
67
+ // ../typography/src/lib/Typography.tsx
68
+ import { forwardRef as forwardRef2 } from "react";
69
+ import { default as MuiTypography } from "@mui/material/Typography";
70
+ import { jsx as jsx5 } from "react/jsx-runtime";
71
+ var Typography = forwardRef2(
72
+ (_a, ref) => {
73
+ var _b = _a, { children } = _b, rest = __objRest(_b, ["children"]);
74
+ return /* @__PURE__ */ jsx5(MuiTypography, __spreadProps(__spreadValues({}, rest), { ref, children }));
75
+ }
76
+ );
77
+
78
+ // src/lib/TextField.tsx
79
+ import { styled } from "@mui/material/styles";
80
+ import { Fragment, jsx as jsx6, jsxs } from "react/jsx-runtime";
81
+ var SelectPlaceholder = styled("span", {
82
+ name: "MuiTextField",
83
+ slot: "SelectPlaceholder",
84
+ overridesResolver: (props, styles) => styles.avFilled
85
+ })(({ theme }) => ({ opacity: 1, color: theme.palette.grey[400] }));
86
+ var TextFieldFormHelperText = ({
87
+ charCount,
88
+ helperText,
89
+ maxLength,
90
+ showCharacterCount
91
+ }) => {
92
+ if (showCharacterCount) {
93
+ return /* @__PURE__ */ jsxs(Grid, { container: true, justifyContent: "space-between", flexWrap: "nowrap", children: [
94
+ /* @__PURE__ */ jsx6(FormHelperText, { sx: { marginRight: "12px" }, children: helperText }),
95
+ /* @__PURE__ */ jsxs(Typography, { variant: "caption", marginTop: "4px", lineHeight: "1.25rem", children: [
96
+ /* @__PURE__ */ jsx6(Typography, { component: "span", variant: "inherit", color: charCount > maxLength ? "error" : "textPrimary", children: charCount || 0 }),
97
+ "/",
98
+ maxLength
99
+ ] })
100
+ ] });
101
+ }
102
+ return /* @__PURE__ */ jsx6(FormHelperText, { children: helperText });
103
+ };
104
+ var TextField = forwardRef3((props, ref) => {
105
+ var _b, _c, _d, _e, _f, _g, _h;
106
+ const _a = props, {
107
+ InputProps: InputProps2,
108
+ helpTopicId,
109
+ InputLabelProps,
110
+ FormHelperTextProps: FormHelperTextProps2,
111
+ required,
112
+ SelectProps: SelectProps2,
113
+ inputProps,
114
+ helperText,
115
+ showCharacterCount = false,
116
+ displayOverflowMaxLength = false
117
+ } = _a, rest = __objRest(_a, [
118
+ "InputProps",
119
+ "helpTopicId",
120
+ "InputLabelProps",
121
+ "FormHelperTextProps",
122
+ "required",
123
+ "SelectProps",
124
+ "inputProps",
125
+ "helperText",
126
+ "showCharacterCount",
127
+ "displayOverflowMaxLength"
128
+ ]);
46
129
  const [openDetected, setOpenDetected] = useState(false);
47
- return /* @__PURE__ */ jsx(
130
+ const [charCount, setCharCount] = useState(0);
131
+ const maxLength = (inputProps == null ? void 0 : inputProps.maxLength) || ((_c = (_b = rest.slotProps) == null ? void 0 : _b.htmlInput) == null ? void 0 : _c.maxLength);
132
+ const resolvedProps = (props2) => !props2 || Object.keys(props2).length === 0 ? void 0 : props2;
133
+ return /* @__PURE__ */ jsx6(
48
134
  MuiTextField,
49
135
  __spreadProps(__spreadValues({}, rest), {
136
+ onChange: (event) => {
137
+ setCharCount(event.target.value.length);
138
+ if (rest.onChange) rest.onChange(event);
139
+ },
140
+ helperText: helperText || /* @__PURE__ */ jsx6(Fragment, {}),
141
+ slots: { formHelperText: TextFieldFormHelperText },
50
142
  slotProps: {
51
- input: __spreadValues(__spreadValues({}, InputProps2), InputPropOverrides),
52
- htmlInput: __spreadValues({ "aria-required": required }, inputProps),
53
- select: __spreadValues(__spreadValues(__spreadValues({}, SelectProps2), SelectPropOverrides), SelectAccessibilityOverrides(openDetected, setOpenDetected, SelectProps2 == null ? void 0 : SelectProps2.open)),
54
- inputLabel: __spreadValues({
143
+ input: resolvedProps(__spreadValues(__spreadValues(__spreadValues({}, InputProps2), InputPropOverrides), (_d = rest.slotProps) == null ? void 0 : _d.input)),
144
+ htmlInput: resolvedProps(__spreadProps(__spreadValues(__spreadValues({
145
+ "aria-required": required
146
+ }, inputProps), (_e = rest.slotProps) == null ? void 0 : _e.htmlInput), {
147
+ maxLength: !displayOverflowMaxLength ? maxLength : void 0
148
+ })),
149
+ select: resolvedProps(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
150
+ displayEmpty: !!rest.placeholder,
151
+ renderValue: (value) => rest.placeholder && (!value || Array.isArray(value) && value.length === 0) ? /* @__PURE__ */ jsx6(SelectPlaceholder, { className: "MuiSelect-placeholder", children: rest.placeholder }) : value
152
+ }, SelectProps2), SelectPropOverrides), SelectAccessibilityOverrides(openDetected, setOpenDetected, SelectProps2 == null ? void 0 : SelectProps2.open)), (_f = rest.slotProps) == null ? void 0 : _f.select)),
153
+ inputLabel: resolvedProps(__spreadValues(__spreadValues({
55
154
  component: FormLabel,
56
155
  helpTopicId,
57
156
  required,
58
157
  shrink: true
59
- }, InputLabelProps),
60
- formHelperText: __spreadValues({ component: FormHelperText }, FormHelperTextProps2)
158
+ }, InputLabelProps), (_g = rest.slotProps) == null ? void 0 : _g.inputLabel)),
159
+ formHelperText: resolvedProps(__spreadProps(__spreadValues(__spreadValues({
160
+ component: "div"
161
+ }, FormHelperTextProps2), (_h = rest.slotProps) == null ? void 0 : _h.formHelperText), {
162
+ charCount,
163
+ helperText,
164
+ maxLength,
165
+ displayOverflowMaxLength,
166
+ showCharacterCount
167
+ }))
61
168
  },
62
169
  ref
63
170
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@availity/mui-textfield",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "Availity MUI Textfield Component - part of the @availity/element design system",
5
5
  "keywords": [
6
6
  "react",
@@ -40,7 +40,7 @@
40
40
  "publish:canary": "yarn npm publish --access public --tag canary"
41
41
  },
42
42
  "dependencies": {
43
- "@availity/mui-form-utils": "1.0.1",
43
+ "@availity/mui-form-utils": "1.1.0",
44
44
  "@availity/mui-icon": "1.0.1"
45
45
  },
46
46
  "devDependencies": {
@@ -17,25 +17,37 @@ const meta: Meta<typeof TextField> = {
17
17
  title: 'Form Components/TextField/TextField',
18
18
  component: TextField,
19
19
  tags: ['autodocs'],
20
- args: {
21
- helperText: 'Helper Text',
22
- fullWidth: false,
23
- },
24
- argTypes: {
25
- helperText: {
26
- type: 'string',
27
- },
28
- },
20
+ args: { helperText: 'Helper Text', fullWidth: false },
21
+ argTypes: { helperText: { type: 'string' } },
29
22
  };
30
23
 
31
24
  export default meta;
32
25
 
33
26
  export const _TextField: StoryObj<typeof TextField> = {
34
27
  render: (args: TextFieldProps) => <TextField {...args} />,
28
+ args: { label: 'Field Label', id: 'test', helpTopicId: '123' },
29
+ };
30
+
31
+ export const _TextFieldCharacterCount: StoryObj<typeof TextField> = {
32
+ render: (args: TextFieldProps) => <TextField sx={{ width: 'min-content' }} {...args} />,
33
+ args: {
34
+ label: 'Field Label',
35
+ id: 'test',
36
+ helpTopicId: '123',
37
+ showCharacterCount: true,
38
+ slotProps: { htmlInput: { maxLength: 10 } },
39
+ },
40
+ };
41
+
42
+ export const _TextFieldCharacterCountOverflow: StoryObj<typeof TextField> = {
43
+ render: (args: TextFieldProps) => <TextField sx={{ width: 'min-content' }} {...args} />,
35
44
  args: {
36
45
  label: 'Field Label',
37
46
  id: 'test',
38
47
  helpTopicId: '123',
48
+ showCharacterCount: true,
49
+ displayOverflowMaxLength: true,
50
+ slotProps: { htmlInput: { maxLength: 10 } },
39
51
  },
40
52
  };
41
53
 
@@ -48,9 +60,7 @@ export const _States: StoryObj<typeof TextField> = {
48
60
  <TextField label="Disabled" id="disabled" disabled {...args} />
49
61
  </Stack>
50
62
  ),
51
- args: {
52
- margin: 'normal',
53
- },
63
+ args: { margin: 'normal' },
54
64
  };
55
65
 
56
66
  export const _Sizes: StoryObj<typeof TextField> = {
@@ -60,9 +70,7 @@ export const _Sizes: StoryObj<typeof TextField> = {
60
70
  <TextField label="Medium" id="medium" size="medium" {...args} />
61
71
  </Stack>
62
72
  ),
63
- args: {
64
- margin: 'normal',
65
- },
73
+ args: { margin: 'normal' },
66
74
  };
67
75
 
68
76
  export const _WithIcon: StoryObj<typeof TextField> = {
@@ -158,9 +166,7 @@ const TextMaskCustom = forwardRef<HTMLInputElement, CustomProps>(function TextMa
158
166
  <IMaskInput
159
167
  {...other}
160
168
  mask="(#00) 000-0000"
161
- definitions={{
162
- '#': /[1-9]/,
163
- }}
169
+ definitions={{ '#': /[1-9]/ }}
164
170
  inputRef={ref}
165
171
  onAccept={(value: any) => onChange({ target: { name: props.name, value } })}
166
172
  overwrite
@@ -176,12 +182,7 @@ const NumericFormatCustom = forwardRef<NumericFormatProps, CustomProps>(function
176
182
  {...other}
177
183
  getInputRef={ref}
178
184
  onValueChange={(values) => {
179
- onChange({
180
- target: {
181
- name: props.name,
182
- value: values.value,
183
- },
184
- });
185
+ onChange({ target: { name: props.name, value: values.value } });
185
186
  }}
186
187
  thousandSeparator
187
188
  valueIsNumericString
@@ -251,16 +252,10 @@ export const _InputMasking: StoryObj<typeof TextField> = {
251
252
 
252
253
  // ---------------------------------------
253
254
 
254
- const [values, setValues] = useState({
255
- textmask: '(100) 000-0000',
256
- numberformat: '1320',
257
- });
255
+ const [values, setValues] = useState({ textmask: '(100) 000-0000', numberformat: '1320' });
258
256
 
259
257
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
260
- setValues({
261
- ...values,
262
- [event.target.name]: event.target.value,
263
- });
258
+ setValues({ ...values, [event.target.name]: event.target.value });
264
259
  };
265
260
 
266
261
  return (
@@ -306,9 +301,26 @@ export const _Select: StoryObj<typeof TextField> = {
306
301
  </TextField>
307
302
  );
308
303
  },
309
- args: {
310
- label: 'Select',
304
+ args: { label: 'Select' },
305
+ };
306
+
307
+ export const _SelectPlaceholder: StoryObj<typeof TextField> = {
308
+ render: (args: TextFieldProps) => {
309
+ const [count, setCount] = useState('');
310
+
311
+ const handleChange = (event: SelectChangeEvent) => {
312
+ setCount(event.target.value as string);
313
+ };
314
+
315
+ return (
316
+ <TextField value={count} select SelectProps={{ onChange: handleChange }} {...args}>
317
+ <MenuItem value={10}>10</MenuItem>
318
+ <MenuItem value={20}>20</MenuItem>
319
+ <MenuItem value={30}>30</MenuItem>
320
+ </TextField>
321
+ );
311
322
  },
323
+ args: { label: 'Select', placeholder: 'Select...' },
312
324
  };
313
325
 
314
326
  export const _MultiSelect: StoryObj<typeof TextField> = {
@@ -351,7 +363,5 @@ export const _MultiSelect: StoryObj<typeof TextField> = {
351
363
  </TextField>
352
364
  );
353
365
  },
354
- args: {
355
- label: 'MultiSelect',
356
- },
366
+ args: { label: 'MultiSelect' },
357
367
  };
@@ -1,4 +1,5 @@
1
- import { render } from '@testing-library/react';
1
+ import { render, fireEvent } from '@testing-library/react';
2
+ import { MenuItem } from '@availity/mui-menu';
2
3
  import { TextField } from './TextField';
3
4
 
4
5
  describe('TextField', () => {
@@ -6,4 +7,59 @@ describe('TextField', () => {
6
7
  const { getByLabelText } = render(<TextField label="Test" />);
7
8
  expect(getByLabelText('Test')).toBeTruthy();
8
9
  });
10
+
11
+ describe('TextField character counter', () => {
12
+ test('should render character counter successfully via inputProps', () => {
13
+ const { getByText, getByTestId, getByTitle } = render(
14
+ <TextField label="Test" showCharacterCount inputProps={{ 'data-testid': 'testTextField', maxLength: 20 }} />
15
+ );
16
+
17
+ expect(getByText('0')).toBeTruthy();
18
+ expect(getByText('/20')).toBeTruthy();
19
+
20
+ const input = getByTestId('testTextField');
21
+ fireEvent.change(input, { target: { value: 'Some Text' } });
22
+
23
+ expect(getByText('9')).toBeTruthy();
24
+ expect(getByText('/20')).toBeTruthy();
25
+
26
+ fireEvent.change(input, { target: { value: "Some More Text that doesn't fit" } });
27
+
28
+ expect(getByTitle('Error')).toBeTruthy();
29
+ });
30
+
31
+ test('should render character counter successfully via slotProps', () => {
32
+ const { getByText, getByTestId, getByTitle } = render(
33
+ <TextField
34
+ label="Test"
35
+ showCharacterCount
36
+ slotProps={{ htmlInput: { 'data-testid': 'testTextField', maxLength: 20 } }}
37
+ />
38
+ );
39
+ expect(getByText('0')).toBeTruthy();
40
+ expect(getByText('/20')).toBeTruthy();
41
+
42
+ const input = getByTestId('testTextField');
43
+ fireEvent.change(input, { target: { value: 'Some Text' } });
44
+
45
+ expect(getByText('9')).toBeTruthy();
46
+ expect(getByText('/20')).toBeTruthy();
47
+
48
+ fireEvent.change(input, { target: { value: "Some More Text that doesn't fit" } });
49
+
50
+ expect(getByTitle('Error')).toBeTruthy();
51
+ });
52
+ });
53
+ describe('TextField select placeholder', () => {
54
+ test('should render select placeholder successfully', () => {
55
+ const { getByText } = render(
56
+ <TextField label="Test" select placeholder="Select...">
57
+ <MenuItem value={10}>10</MenuItem>
58
+ <MenuItem value={20}>20</MenuItem>
59
+ <MenuItem value={30}>30</MenuItem>
60
+ </TextField>
61
+ );
62
+ expect(getByText('Select...')).toBeTruthy();
63
+ });
64
+ });
9
65
  });
@@ -11,6 +11,9 @@ import {
11
11
  SelectPropOverrides,
12
12
  SelectProps,
13
13
  } from '@availity/mui-form-utils';
14
+ import { Grid } from '@availity/mui-layout';
15
+ import { Typography } from '@availity/mui-typography';
16
+ import { styled } from '@mui/material/styles';
14
17
 
15
18
  export type TextFieldProps = {
16
19
  FormHelperTextProps?: FormHelperTextProps;
@@ -19,33 +22,120 @@ export type TextFieldProps = {
19
22
  SelectProps?: SelectProps;
20
23
  /** If `true`, the input will take up the full width of its container. @default true */
21
24
  fullWidth?: boolean;
25
+ /** if `true`, the character counter will display. The maxLength is taken from the `inputProps.maxLength` prop. @default false */
26
+ showCharacterCount?: boolean;
27
+ /** If `true`, the input maxLength can be exceeded. If validation is required, you'll have to do it manually. @default false */
28
+ displayOverflowMaxLength?: boolean;
22
29
  } & Pick<FormLabelProps, 'helpTopicId'> &
23
- Omit<MuiTextFieldProps, 'fullWidth' | 'variant' | 'slotProps'>;
30
+ Omit<MuiTextFieldProps, 'fullWidth' | 'variant'>;
31
+
32
+ const SelectPlaceholder = styled('span', {
33
+ name: 'MuiTextField',
34
+ slot: 'SelectPlaceholder',
35
+ overridesResolver: (props, styles) => styles.avFilled,
36
+ })(({ theme }) => ({ opacity: 1, color: theme.palette.grey[400] }));
37
+
38
+ export type TextFieldFormHelperTextProps = {
39
+ charCount: string;
40
+ helperText: string;
41
+ maxLength: string;
42
+ showCharacterCount: boolean;
43
+ };
44
+
45
+ const TextFieldFormHelperText = ({
46
+ charCount,
47
+ helperText,
48
+ maxLength,
49
+ showCharacterCount,
50
+ }: TextFieldFormHelperTextProps) => {
51
+ if (showCharacterCount) {
52
+ return (
53
+ <Grid container justifyContent="space-between" flexWrap="nowrap">
54
+ <FormHelperText sx={{ marginRight: '12px' }}>{helperText}</FormHelperText>
55
+ <Typography variant="caption" marginTop="4px" lineHeight="1.25rem">
56
+ <Typography component="span" variant="inherit" color={charCount > maxLength ? 'error' : 'textPrimary'}>
57
+ {charCount || 0}
58
+ </Typography>
59
+ /{maxLength}
60
+ </Typography>
61
+ </Grid>
62
+ );
63
+ }
64
+
65
+ return <FormHelperText>{helperText}</FormHelperText>;
66
+ };
24
67
 
25
68
  export const TextField = forwardRef<HTMLDivElement | HTMLInputElement, TextFieldProps>((props, ref) => {
26
- const { InputProps, helpTopicId, InputLabelProps, FormHelperTextProps, required, SelectProps, inputProps, ...rest } =
27
- props;
69
+ const {
70
+ InputProps,
71
+ helpTopicId,
72
+ InputLabelProps,
73
+ FormHelperTextProps,
74
+ required,
75
+ SelectProps,
76
+ inputProps,
77
+ helperText,
78
+ showCharacterCount = false,
79
+ displayOverflowMaxLength = false,
80
+ ...rest
81
+ } = props;
28
82
  const [openDetected, setOpenDetected] = useState(false);
83
+ const [charCount, setCharCount] = useState(0);
84
+
85
+ // @ts-expect-error I'm not sure why maxLength is undefined in htmlInput, but it works. There's something weird with the type.
86
+ const maxLength = inputProps?.maxLength || rest.slotProps?.htmlInput?.maxLength;
87
+
88
+ const resolvedProps = (props: Record<string, unknown>) =>
89
+ !props || Object.keys(props).length === 0 ? undefined : props;
29
90
 
30
91
  return (
31
92
  <MuiTextField
32
93
  {...rest}
94
+ onChange={(event) => {
95
+ setCharCount(event.target.value.length);
96
+ if (rest.onChange) rest.onChange(event);
97
+ }}
98
+ helperText={helperText || <></>}
99
+ slots={{ formHelperText: TextFieldFormHelperText }}
33
100
  slotProps={{
34
- input: { ...InputProps, ...InputPropOverrides },
35
- htmlInput: { 'aria-required': required, ...inputProps },
36
- select: {
101
+ input: resolvedProps({ ...InputProps, ...InputPropOverrides, ...rest.slotProps?.input }),
102
+ htmlInput: resolvedProps({
103
+ 'aria-required': required,
104
+ ...inputProps,
105
+ ...rest.slotProps?.htmlInput,
106
+ maxLength: !displayOverflowMaxLength ? maxLength : undefined,
107
+ }),
108
+ select: resolvedProps({
109
+ displayEmpty: !!rest.placeholder,
110
+ renderValue: (value: unknown) =>
111
+ rest.placeholder && (!value || (Array.isArray(value) && value.length === 0)) ? (
112
+ <SelectPlaceholder className="MuiSelect-placeholder">{rest.placeholder}</SelectPlaceholder>
113
+ ) : (
114
+ value
115
+ ),
37
116
  ...SelectProps,
38
117
  ...SelectPropOverrides,
39
118
  ...SelectAccessibilityOverrides(openDetected, setOpenDetected, SelectProps?.open),
40
- },
41
- inputLabel: {
119
+ ...rest.slotProps?.select,
120
+ }),
121
+ inputLabel: resolvedProps({
42
122
  component: FormLabel,
43
123
  helpTopicId: helpTopicId,
44
124
  required,
45
125
  shrink: true,
46
126
  ...InputLabelProps,
47
- },
48
- formHelperText: { component: FormHelperText, ...FormHelperTextProps },
127
+ ...rest.slotProps?.inputLabel,
128
+ }),
129
+ formHelperText: resolvedProps({
130
+ component: 'div',
131
+ ...FormHelperTextProps,
132
+ ...rest.slotProps?.formHelperText,
133
+ charCount,
134
+ helperText,
135
+ maxLength,
136
+ displayOverflowMaxLength,
137
+ showCharacterCount,
138
+ }),
49
139
  }}
50
140
  ref={ref}
51
141
  />