@availity/mui-textfield 1.0.1 → 1.1.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
@@ -2,6 +2,17 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.1.0](https://github.com/Availity/element/compare/@availity/mui-textfield@1.0.1...@availity/mui-textfield@1.1.0) (2025-03-21)
6
+
7
+ ### Dependency Updates
8
+
9
+ * `mui-form-utils` updated to version `1.0.1`
10
+
11
+ ### Features
12
+
13
+ * **mui-textfield, mui-form-utils:** add placeholder support to select ([3a6b1ef](https://github.com/Availity/element/commit/3a6b1ef2e5ceedca632bd1916734e56402371ffb))
14
+ * **mui-textfield:** add optional character count to textfield ([1c3f0dc](https://github.com/Availity/element/commit/1c3f0dc886e153c79afc81c98b3326ed8990b305))
15
+
5
16
  ## [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
17
 
7
18
  ### Dependency Updates
package/dist/index.d.mts CHANGED
@@ -9,7 +9,9 @@ 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
+ } & Pick<FormLabelProps, 'helpTopicId'> & Omit<TextFieldProps$1, 'fullWidth' | 'variant'>;
13
15
  declare const TextField: react.ForwardRefExoticComponent<Omit<TextFieldProps, "ref"> & react.RefAttributes<HTMLDivElement | HTMLInputElement>>;
14
16
 
15
17
  export { TextField, type TextFieldProps };
package/dist/index.d.ts CHANGED
@@ -9,7 +9,9 @@ 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
+ } & Pick<FormLabelProps, 'helpTopicId'> & Omit<TextFieldProps$1, 'fullWidth' | 'variant'>;
13
15
  declare const TextField: react.ForwardRefExoticComponent<Omit<TextFieldProps, "ref"> & react.RefAttributes<HTMLDivElement | HTMLInputElement>>;
14
16
 
15
17
  export { TextField, type TextFieldProps };
package/dist/index.js CHANGED
@@ -64,27 +64,98 @@ __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_react2 = 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
+ // src/lib/TextField.tsx
95
+ var import_styles = require("@mui/material/styles");
96
+ var import_jsx_runtime5 = require("react/jsx-runtime");
97
+ var SelectPlaceholder = (0, import_styles.styled)("span", {
98
+ name: "MuiTextField",
99
+ slot: "SelectPlaceholder",
100
+ overridesResolver: (props, styles) => styles.avFilled
101
+ })(({ theme }) => ({ opacity: 1, color: theme.palette.grey[400] }));
102
+ var TextField = (0, import_react2.forwardRef)((props, ref) => {
103
+ var _b, _c, _d, _e, _f, _g, _h;
104
+ const _a = props, {
105
+ InputProps: InputProps2,
106
+ helpTopicId,
107
+ InputLabelProps,
108
+ FormHelperTextProps: FormHelperTextProps2,
109
+ required,
110
+ SelectProps: SelectProps2,
111
+ inputProps,
112
+ helperText,
113
+ showCharacterCount = false
114
+ } = _a, rest = __objRest(_a, [
115
+ "InputProps",
116
+ "helpTopicId",
117
+ "InputLabelProps",
118
+ "FormHelperTextProps",
119
+ "required",
120
+ "SelectProps",
121
+ "inputProps",
122
+ "helperText",
123
+ "showCharacterCount"
124
+ ]);
125
+ const [openDetected, setOpenDetected] = (0, import_react2.useState)(false);
126
+ const [charCount, setCharCount] = (0, import_react2.useState)(0);
127
+ const resolvedProps = (props2) => !props2 || Object.keys(props2).length === 0 ? void 0 : props2;
128
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
75
129
  import_TextField.default,
76
130
  __spreadProps(__spreadValues({}, rest), {
131
+ onChange: (event) => {
132
+ setCharCount(event.target.value.length);
133
+ if (rest.onChange) rest.onChange(event);
134
+ },
135
+ helperText: showCharacterCount ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(Grid, { container: true, justifyContent: "space-between", children: [
136
+ helperText,
137
+ " ",
138
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { marginLeft: 4 }, children: [
139
+ charCount || 0,
140
+ "/",
141
+ (inputProps == null ? void 0 : inputProps.maxLength) || ((_c = (_b = rest.slotProps) == null ? void 0 : _b.htmlInput) == null ? void 0 : _c.maxLength)
142
+ ] })
143
+ ] }) : helperText,
144
+ slots: { formHelperText: import_mui_form_utils.FormHelperText },
77
145
  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({
146
+ input: resolvedProps(__spreadValues(__spreadValues(__spreadValues({}, InputProps2), import_mui_form_utils.InputPropOverrides), (_d = rest.slotProps) == null ? void 0 : _d.input)),
147
+ htmlInput: resolvedProps(__spreadValues(__spreadValues({ "aria-required": required }, inputProps), (_e = rest.slotProps) == null ? void 0 : _e.htmlInput)),
148
+ select: resolvedProps(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
149
+ displayEmpty: !!rest.placeholder,
150
+ renderValue: (value) => rest.placeholder && (!value || Array.isArray(value) && value.length === 0) ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SelectPlaceholder, { className: "MuiSelect-placeholder", children: rest.placeholder }) : value
151
+ }, 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)),
152
+ inputLabel: resolvedProps(__spreadValues(__spreadValues({
82
153
  component: import_mui_form_utils.FormLabel,
83
154
  helpTopicId,
84
155
  required,
85
156
  shrink: true
86
- }, InputLabelProps),
87
- formHelperText: __spreadValues({ component: import_mui_form_utils.FormHelperText }, FormHelperTextProps2)
157
+ }, InputLabelProps), (_g = rest.slotProps) == null ? void 0 : _g.inputLabel)),
158
+ formHelperText: resolvedProps(__spreadValues(__spreadValues({ component: "div" }, FormHelperTextProps2), (_h = rest.slotProps) == null ? void 0 : _h.formHelperText))
88
159
  },
89
160
  ref
90
161
  })
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 forwardRef2, useState } from "react";
35
35
  import MuiTextField from "@mui/material/TextField";
36
36
  import {
37
37
  FormHelperText,
@@ -40,24 +40,95 @@ 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
+ // src/lib/TextField.tsx
68
+ import { styled } from "@mui/material/styles";
69
+ import { jsx as jsx5, jsxs } from "react/jsx-runtime";
70
+ var SelectPlaceholder = styled("span", {
71
+ name: "MuiTextField",
72
+ slot: "SelectPlaceholder",
73
+ overridesResolver: (props, styles) => styles.avFilled
74
+ })(({ theme }) => ({ opacity: 1, color: theme.palette.grey[400] }));
75
+ var TextField = forwardRef2((props, ref) => {
76
+ var _b, _c, _d, _e, _f, _g, _h;
77
+ const _a = props, {
78
+ InputProps: InputProps2,
79
+ helpTopicId,
80
+ InputLabelProps,
81
+ FormHelperTextProps: FormHelperTextProps2,
82
+ required,
83
+ SelectProps: SelectProps2,
84
+ inputProps,
85
+ helperText,
86
+ showCharacterCount = false
87
+ } = _a, rest = __objRest(_a, [
88
+ "InputProps",
89
+ "helpTopicId",
90
+ "InputLabelProps",
91
+ "FormHelperTextProps",
92
+ "required",
93
+ "SelectProps",
94
+ "inputProps",
95
+ "helperText",
96
+ "showCharacterCount"
97
+ ]);
46
98
  const [openDetected, setOpenDetected] = useState(false);
47
- return /* @__PURE__ */ jsx(
99
+ const [charCount, setCharCount] = useState(0);
100
+ const resolvedProps = (props2) => !props2 || Object.keys(props2).length === 0 ? void 0 : props2;
101
+ return /* @__PURE__ */ jsx5(
48
102
  MuiTextField,
49
103
  __spreadProps(__spreadValues({}, rest), {
104
+ onChange: (event) => {
105
+ setCharCount(event.target.value.length);
106
+ if (rest.onChange) rest.onChange(event);
107
+ },
108
+ helperText: showCharacterCount ? /* @__PURE__ */ jsxs(Grid, { container: true, justifyContent: "space-between", children: [
109
+ helperText,
110
+ " ",
111
+ /* @__PURE__ */ jsxs("span", { style: { marginLeft: 4 }, children: [
112
+ charCount || 0,
113
+ "/",
114
+ (inputProps == null ? void 0 : inputProps.maxLength) || ((_c = (_b = rest.slotProps) == null ? void 0 : _b.htmlInput) == null ? void 0 : _c.maxLength)
115
+ ] })
116
+ ] }) : helperText,
117
+ slots: { formHelperText: FormHelperText },
50
118
  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({
119
+ input: resolvedProps(__spreadValues(__spreadValues(__spreadValues({}, InputProps2), InputPropOverrides), (_d = rest.slotProps) == null ? void 0 : _d.input)),
120
+ htmlInput: resolvedProps(__spreadValues(__spreadValues({ "aria-required": required }, inputProps), (_e = rest.slotProps) == null ? void 0 : _e.htmlInput)),
121
+ select: resolvedProps(__spreadValues(__spreadValues(__spreadValues(__spreadValues({
122
+ displayEmpty: !!rest.placeholder,
123
+ renderValue: (value) => rest.placeholder && (!value || Array.isArray(value) && value.length === 0) ? /* @__PURE__ */ jsx5(SelectPlaceholder, { className: "MuiSelect-placeholder", children: rest.placeholder }) : value
124
+ }, SelectProps2), SelectPropOverrides), SelectAccessibilityOverrides(openDetected, setOpenDetected, SelectProps2 == null ? void 0 : SelectProps2.open)), (_f = rest.slotProps) == null ? void 0 : _f.select)),
125
+ inputLabel: resolvedProps(__spreadValues(__spreadValues({
55
126
  component: FormLabel,
56
127
  helpTopicId,
57
128
  required,
58
129
  shrink: true
59
- }, InputLabelProps),
60
- formHelperText: __spreadValues({ component: FormHelperText }, FormHelperTextProps2)
130
+ }, InputLabelProps), (_g = rest.slotProps) == null ? void 0 : _g.inputLabel)),
131
+ formHelperText: resolvedProps(__spreadValues(__spreadValues({ component: "div" }, FormHelperTextProps2), (_h = rest.slotProps) == null ? void 0 : _h.formHelperText))
61
132
  },
62
133
  ref
63
134
  })
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.0",
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,25 @@ 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> = {
27
+ render: (args: TextFieldProps) => <TextField {...args} />,
28
+ args: { label: 'Field Label', id: 'test', helpTopicId: '123' },
29
+ };
30
+
31
+ export const _TextFieldCharacterCount: StoryObj<typeof TextField> = {
34
32
  render: (args: TextFieldProps) => <TextField {...args} />,
35
33
  args: {
36
34
  label: 'Field Label',
37
35
  id: 'test',
38
36
  helpTopicId: '123',
37
+ showCharacterCount: true,
38
+ slotProps: { htmlInput: { maxLength: 10 } },
39
39
  },
40
40
  };
41
41
 
@@ -48,9 +48,7 @@ export const _States: StoryObj<typeof TextField> = {
48
48
  <TextField label="Disabled" id="disabled" disabled {...args} />
49
49
  </Stack>
50
50
  ),
51
- args: {
52
- margin: 'normal',
53
- },
51
+ args: { margin: 'normal' },
54
52
  };
55
53
 
56
54
  export const _Sizes: StoryObj<typeof TextField> = {
@@ -60,9 +58,7 @@ export const _Sizes: StoryObj<typeof TextField> = {
60
58
  <TextField label="Medium" id="medium" size="medium" {...args} />
61
59
  </Stack>
62
60
  ),
63
- args: {
64
- margin: 'normal',
65
- },
61
+ args: { margin: 'normal' },
66
62
  };
67
63
 
68
64
  export const _WithIcon: StoryObj<typeof TextField> = {
@@ -158,9 +154,7 @@ const TextMaskCustom = forwardRef<HTMLInputElement, CustomProps>(function TextMa
158
154
  <IMaskInput
159
155
  {...other}
160
156
  mask="(#00) 000-0000"
161
- definitions={{
162
- '#': /[1-9]/,
163
- }}
157
+ definitions={{ '#': /[1-9]/ }}
164
158
  inputRef={ref}
165
159
  onAccept={(value: any) => onChange({ target: { name: props.name, value } })}
166
160
  overwrite
@@ -176,12 +170,7 @@ const NumericFormatCustom = forwardRef<NumericFormatProps, CustomProps>(function
176
170
  {...other}
177
171
  getInputRef={ref}
178
172
  onValueChange={(values) => {
179
- onChange({
180
- target: {
181
- name: props.name,
182
- value: values.value,
183
- },
184
- });
173
+ onChange({ target: { name: props.name, value: values.value } });
185
174
  }}
186
175
  thousandSeparator
187
176
  valueIsNumericString
@@ -251,16 +240,10 @@ export const _InputMasking: StoryObj<typeof TextField> = {
251
240
 
252
241
  // ---------------------------------------
253
242
 
254
- const [values, setValues] = useState({
255
- textmask: '(100) 000-0000',
256
- numberformat: '1320',
257
- });
243
+ const [values, setValues] = useState({ textmask: '(100) 000-0000', numberformat: '1320' });
258
244
 
259
245
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
260
- setValues({
261
- ...values,
262
- [event.target.name]: event.target.value,
263
- });
246
+ setValues({ ...values, [event.target.name]: event.target.value });
264
247
  };
265
248
 
266
249
  return (
@@ -306,9 +289,26 @@ export const _Select: StoryObj<typeof TextField> = {
306
289
  </TextField>
307
290
  );
308
291
  },
309
- args: {
310
- label: 'Select',
292
+ args: { label: 'Select' },
293
+ };
294
+
295
+ export const _SelectPlaceholder: StoryObj<typeof TextField> = {
296
+ render: (args: TextFieldProps) => {
297
+ const [count, setCount] = useState('');
298
+
299
+ const handleChange = (event: SelectChangeEvent) => {
300
+ setCount(event.target.value as string);
301
+ };
302
+
303
+ return (
304
+ <TextField value={count} select SelectProps={{ onChange: handleChange }} {...args}>
305
+ <MenuItem value={10}>10</MenuItem>
306
+ <MenuItem value={20}>20</MenuItem>
307
+ <MenuItem value={30}>30</MenuItem>
308
+ </TextField>
309
+ );
311
310
  },
311
+ args: { label: 'Select', placeholder: 'Select...' },
312
312
  };
313
313
 
314
314
  export const _MultiSelect: StoryObj<typeof TextField> = {
@@ -351,7 +351,5 @@ export const _MultiSelect: StoryObj<typeof TextField> = {
351
351
  </TextField>
352
352
  );
353
353
  },
354
- args: {
355
- label: 'MultiSelect',
356
- },
354
+ args: { label: 'MultiSelect' },
357
355
  };
@@ -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,55 @@ 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/20')).toBeTruthy();
18
+
19
+ const input = getByTestId('testTextField');
20
+ fireEvent.change(input, { target: { value: 'Some Text' } });
21
+
22
+ expect(getByText('9/20')).toBeTruthy();
23
+
24
+ fireEvent.change(input, { target: { value: "Some More Text that doesn't fit" } });
25
+
26
+ expect(getByTitle('Error')).toBeTruthy();
27
+ });
28
+
29
+ test('should render character counter successfully via slotProps', () => {
30
+ const { getByText, getByTestId, getByTitle } = render(
31
+ <TextField
32
+ label="Test"
33
+ showCharacterCount
34
+ slotProps={{ htmlInput: { 'data-testid': 'testTextField', maxLength: 20 } }}
35
+ />
36
+ );
37
+ expect(getByText('0/20')).toBeTruthy();
38
+
39
+ const input = getByTestId('testTextField');
40
+ fireEvent.change(input, { target: { value: 'Some Text' } });
41
+
42
+ expect(getByText('9/20')).toBeTruthy();
43
+
44
+ fireEvent.change(input, { target: { value: "Some More Text that doesn't fit" } });
45
+
46
+ expect(getByTitle('Error')).toBeTruthy();
47
+ });
48
+ });
49
+ describe('TextField select placeholder', () => {
50
+ test('should render select placeholder successfully', () => {
51
+ const { getByText } = render(
52
+ <TextField label="Test" select placeholder="Select...">
53
+ <MenuItem value={10}>10</MenuItem>
54
+ <MenuItem value={20}>20</MenuItem>
55
+ <MenuItem value={30}>30</MenuItem>
56
+ </TextField>
57
+ );
58
+ expect(getByText('Select...')).toBeTruthy();
59
+ });
60
+ });
9
61
  });
@@ -11,6 +11,8 @@ import {
11
11
  SelectPropOverrides,
12
12
  SelectProps,
13
13
  } from '@availity/mui-form-utils';
14
+ import { Grid } from '@availity/mui-layout';
15
+ import { styled } from '@mui/material/styles';
14
16
 
15
17
  export type TextFieldProps = {
16
18
  FormHelperTextProps?: FormHelperTextProps;
@@ -19,33 +21,82 @@ export type TextFieldProps = {
19
21
  SelectProps?: SelectProps;
20
22
  /** If `true`, the input will take up the full width of its container. @default true */
21
23
  fullWidth?: boolean;
24
+ /** if `true`, the character counter will display. The maxLength is taken from the `inputProps.maxLength` prop. @default false */
25
+ showCharacterCount?: boolean;
22
26
  } & Pick<FormLabelProps, 'helpTopicId'> &
23
- Omit<MuiTextFieldProps, 'fullWidth' | 'variant' | 'slotProps'>;
27
+ Omit<MuiTextFieldProps, 'fullWidth' | 'variant'>;
28
+
29
+ const SelectPlaceholder = styled('span', {
30
+ name: 'MuiTextField',
31
+ slot: 'SelectPlaceholder',
32
+ overridesResolver: (props, styles) => styles.avFilled,
33
+ })(({ theme }) => ({ opacity: 1, color: theme.palette.grey[400] }));
24
34
 
25
35
  export const TextField = forwardRef<HTMLDivElement | HTMLInputElement, TextFieldProps>((props, ref) => {
26
- const { InputProps, helpTopicId, InputLabelProps, FormHelperTextProps, required, SelectProps, inputProps, ...rest } =
27
- props;
36
+ const {
37
+ InputProps,
38
+ helpTopicId,
39
+ InputLabelProps,
40
+ FormHelperTextProps,
41
+ required,
42
+ SelectProps,
43
+ inputProps,
44
+ helperText,
45
+ showCharacterCount = false,
46
+ ...rest
47
+ } = props;
28
48
  const [openDetected, setOpenDetected] = useState(false);
49
+ const [charCount, setCharCount] = useState(0);
50
+
51
+ const resolvedProps = (props: Record<string, unknown>) =>
52
+ !props || Object.keys(props).length === 0 ? undefined : props;
29
53
 
30
54
  return (
31
55
  <MuiTextField
32
56
  {...rest}
57
+ onChange={(event) => {
58
+ setCharCount(event.target.value.length);
59
+ if (rest.onChange) rest.onChange(event);
60
+ }}
61
+ helperText={
62
+ showCharacterCount ? (
63
+ <Grid container justifyContent="space-between">
64
+ {helperText}{' '}
65
+ <span style={{ marginLeft: 4 }}>
66
+ {/* @ts-expect-error I'm not sure why maxLength is undefined here, but it works. */}
67
+ {charCount || 0}/{inputProps?.maxLength || rest.slotProps?.htmlInput?.maxLength}
68
+ </span>
69
+ </Grid>
70
+ ) : (
71
+ helperText
72
+ )
73
+ }
74
+ slots={{ formHelperText: FormHelperText }}
33
75
  slotProps={{
34
- input: { ...InputProps, ...InputPropOverrides },
35
- htmlInput: { 'aria-required': required, ...inputProps },
36
- select: {
76
+ input: resolvedProps({ ...InputProps, ...InputPropOverrides, ...rest.slotProps?.input }),
77
+ htmlInput: resolvedProps({ 'aria-required': required, ...inputProps, ...rest.slotProps?.htmlInput }),
78
+ select: resolvedProps({
79
+ displayEmpty: !!rest.placeholder,
80
+ renderValue: (value: unknown) =>
81
+ rest.placeholder && (!value || (Array.isArray(value) && value.length === 0)) ? (
82
+ <SelectPlaceholder className="MuiSelect-placeholder">{rest.placeholder}</SelectPlaceholder>
83
+ ) : (
84
+ value
85
+ ),
37
86
  ...SelectProps,
38
87
  ...SelectPropOverrides,
39
88
  ...SelectAccessibilityOverrides(openDetected, setOpenDetected, SelectProps?.open),
40
- },
41
- inputLabel: {
89
+ ...rest.slotProps?.select,
90
+ }),
91
+ inputLabel: resolvedProps({
42
92
  component: FormLabel,
43
93
  helpTopicId: helpTopicId,
44
94
  required,
45
95
  shrink: true,
46
96
  ...InputLabelProps,
47
- },
48
- formHelperText: { component: FormHelperText, ...FormHelperTextProps },
97
+ ...rest.slotProps?.inputLabel,
98
+ }),
99
+ formHelperText: resolvedProps({ component: 'div', ...FormHelperTextProps, ...rest.slotProps?.formHelperText }),
49
100
  }}
50
101
  ref={ref}
51
102
  />