Skip to content

Commit 6133193

Browse files
TannerStw15egankodiakhq[bot]
authored
feat(react): added counter to textinput and storybook updates (#12139)
* feat(react): added counter to textinput and storybook updates * chore(react): updated snapshot * chore(react): updated test input styles and stories Co-authored-by: TJ Egan <[email protected]> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 7e30406 commit 6133193

File tree

7 files changed

+201
-6
lines changed

7 files changed

+201
-6
lines changed

packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7835,6 +7835,9 @@ Map {
78357835
"disabled": Object {
78367836
"type": "bool",
78377837
},
7838+
"enableCounter": Object {
7839+
"type": "bool",
7840+
},
78387841
"helperText": Object {
78397842
"type": "node",
78407843
},
@@ -7861,6 +7864,9 @@ Map {
78617864
"light": Object {
78627865
"type": "bool",
78637866
},
7867+
"maxCount": Object {
7868+
"type": "number",
7869+
},
78647870
"onChange": Object {
78657871
"type": "func",
78667872
},

packages/react/src/components/TextArea/TextArea-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,13 @@ describe('TextArea', () => {
136136
<TextArea id="counter2" labelText="someLabel" maxCount={5} />
137137
);
138138

139-
it('should not render element without only enableCounter prop passed in', () => {
139+
it('should not render counter with only enableCounter prop passed in', () => {
140140
expect(
141141
counterTestWrapper1.exists(`${prefix}--text-area__counter`)
142142
).toEqual(false);
143143
});
144144

145-
it('should not render element without only maxCount prop passed in', () => {
145+
it('should not render counter with only maxCount prop passed in', () => {
146146
expect(
147147
counterTestWrapper2.exists(`${prefix}--text-area__counter`)
148148
).toEqual(false);

packages/react/src/components/TextArea/next/TextArea.stories.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,11 @@ export const WithLayer = () => {
6060
};
6161

6262
export const Skeleton = () => <TextAreaSkeleton />;
63+
64+
export const Playground = (args) => (
65+
<TextArea
66+
{...args}
67+
labelText="Text area label"
68+
helperText="Optional helper text."
69+
/>
70+
);

packages/react/src/components/TextInput/TextInput.js

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import PropTypes from 'prop-types';
9-
import React, { useContext } from 'react';
9+
import React, { useContext, useState } from 'react';
1010
import classNames from 'classnames';
1111
import { useNormalizedInputProps } from '../../internal/useNormalizedInputProps';
1212
import PasswordInput from './PasswordInput';
@@ -37,6 +37,8 @@ const TextInput = React.forwardRef(function TextInput(
3737
type = 'text',
3838
warn = false,
3939
warnText,
40+
enableCounter = false,
41+
maxCount,
4042
...rest
4143
},
4244
ref
@@ -45,6 +47,11 @@ const TextInput = React.forwardRef(function TextInput(
4547

4648
const enabled = useFeatureFlag('enable-v11-release');
4749

50+
const { defaultValue, value } = rest;
51+
const [textCount, setTextCount] = useState(
52+
defaultValue?.length || value?.length || 0
53+
);
54+
4855
const normalizedProps = useNormalizedInputProps({
4956
id,
5057
readOnly,
@@ -71,6 +78,7 @@ const TextInput = React.forwardRef(function TextInput(
7178
id,
7279
onChange: (evt) => {
7380
if (!normalizedProps.disabled) {
81+
setTextCount(evt.target.value?.length);
7482
onChange(evt);
7583
}
7684
},
@@ -89,6 +97,11 @@ const TextInput = React.forwardRef(function TextInput(
8997
['aria-describedby']: helperText && normalizedProps.helperId,
9098
...rest,
9199
};
100+
101+
if (enableCounter) {
102+
sharedTextInputProps.maxLength = maxCount;
103+
}
104+
92105
const inputWrapperClasses = classNames(
93106
[
94107
enabled
@@ -131,12 +144,29 @@ const TextInput = React.forwardRef(function TextInput(
131144
[`${prefix}--text-input__readonly-icon`]: readOnly,
132145
});
133146

147+
const counterClasses = classNames(`${prefix}--label`, {
148+
[`${prefix}--label--disabled`]: disabled,
149+
[`${prefix}--text-input__label-counter`]: true,
150+
});
151+
152+
const counter =
153+
enableCounter && maxCount ? (
154+
<div className={counterClasses}>{`${textCount}/${maxCount}`}</div>
155+
) : null;
156+
134157
const label = labelText ? (
135158
<label htmlFor={id} className={labelClasses}>
136159
{labelText}
137160
</label>
138161
) : null;
139162

163+
const labelWrapper = (
164+
<div className={`${prefix}--text-input__label-wrapper`}>
165+
{label}
166+
{counter}
167+
</div>
168+
);
169+
140170
const helper = helperText ? (
141171
<div id={normalizedProps.helperId} className={helperTextClasses}>
142172
{helperText}
@@ -160,10 +190,10 @@ const TextInput = React.forwardRef(function TextInput(
160190
return (
161191
<div className={inputWrapperClasses}>
162192
{!inline ? (
163-
label
193+
labelWrapper
164194
) : (
165195
<div className={`${prefix}--text-input__label-helper-wrapper`}>
166-
{label}
196+
{labelWrapper}
167197
{!isFluid && helper}
168198
</div>
169199
)}
@@ -203,6 +233,11 @@ TextInput.propTypes = {
203233
*/
204234
disabled: PropTypes.bool,
205235

236+
/**
237+
* Specify whether to display the character counter
238+
*/
239+
enableCounter: PropTypes.bool,
240+
206241
/**
207242
* Provide text that is used alongside the control label for additional help
208243
*/
@@ -245,6 +280,11 @@ TextInput.propTypes = {
245280
*/
246281
light: PropTypes.bool,
247282

283+
/**
284+
* Max character count allowed for the textarea. This is needed in order for enableCounter to display
285+
*/
286+
maxCount: PropTypes.number,
287+
248288
/**
249289
* Optionally provide an `onChange` handler that is called whenever `<input>`
250290
* is updated

packages/react/src/components/TextInput/__tests__/TextInput-test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,5 +320,40 @@ describe('TextInput', () => {
320320
);
321321
expect(icon).toBeInTheDocument();
322322
});
323+
324+
it('should not render counter with only enableCounter prop passed in', () => {
325+
render(
326+
<TextInput id="input-1" labelText="TextInput label" enableCounter />
327+
);
328+
329+
const counter = screen.queryByText('0/5');
330+
331+
expect(counter).not.toBeInTheDocument();
332+
});
333+
334+
it('should not render counter with only maxCount prop passed in', () => {
335+
render(
336+
<TextInput id="input-1" labelText="TextInput label" enableCounter />
337+
);
338+
339+
const counter = screen.queryByText('0/5');
340+
341+
expect(counter).not.toBeInTheDocument();
342+
});
343+
344+
it('should have the expected classes for counter', () => {
345+
render(
346+
<TextInput
347+
id="input-1"
348+
labelText="TextInput label"
349+
enableCounter
350+
maxCount={5}
351+
/>
352+
);
353+
354+
const counter = screen.queryByText('0/5');
355+
356+
expect(counter).toBeInTheDocument();
357+
});
323358
});
324359
});

packages/react/src/components/TextInput/next/TextInput.stories.js

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,14 @@ export const Default = () => (
3030

3131
export const Fluid = () => (
3232
<FluidForm>
33-
<TextInput type="text" labelText="Text input label" />
33+
<TextInput type="text" labelText="Text input label" id="text-input-1" />
3434
</FluidForm>
3535
);
3636

3737
export const TogglePasswordVisibility = () => {
3838
return (
3939
<TextInput.PasswordInput
40+
id="text-input-1"
4041
labelText="Text input label"
4142
helperText="Optional help text"
4243
/>
@@ -50,6 +51,7 @@ export const ReadOnly = () => {
5051
helperText="Optional help text"
5152
value="This is read only, you can't type more."
5253
readOnly
54+
id="text-input-1"
5355
/>
5456
);
5557
};
@@ -61,18 +63,21 @@ export const WithLayer = () => {
6163
type="text"
6264
labelText="First layer"
6365
helperText="Optional help text"
66+
id="text-input-1"
6467
/>
6568
<Layer>
6669
<TextInput
6770
type="text"
6871
labelText="Second layer"
6972
helperText="Optional help text"
73+
id="text-input-2"
7074
/>
7175
<Layer>
7276
<TextInput
7377
type="text"
7478
labelText="Third layer"
7579
helperText="Optional help text"
80+
id="text-input-3"
7681
/>
7782
</Layer>
7883
</Layer>
@@ -81,3 +86,94 @@ export const WithLayer = () => {
8186
};
8287

8388
export const Skeleton = () => <TextInputSkeleton />;
89+
90+
export const Playground = (args) => (
91+
<div style={{ width: args.playgroundWidth }}>
92+
<TextInput {...args} id="text-input-1" type="text" />
93+
</div>
94+
);
95+
96+
Playground.argTypes = {
97+
playgroundWidth: {
98+
control: { type: 'range', min: 300, max: 800, step: 50 },
99+
defaultValue: 300,
100+
},
101+
className: {
102+
control: {
103+
type: 'text',
104+
},
105+
defaultValue: 'input-test-class',
106+
},
107+
defaultValue: {
108+
control: {
109+
type: 'text',
110+
},
111+
},
112+
placeholder: {
113+
control: {
114+
type: 'text',
115+
},
116+
defaultValue: 'Placeholder text',
117+
},
118+
invalid: {
119+
control: {
120+
type: 'boolean',
121+
},
122+
defaultValue: false,
123+
},
124+
invalidText: {
125+
control: {
126+
type: 'text',
127+
},
128+
defaultValue: 'Invalid text',
129+
},
130+
disabled: {
131+
control: {
132+
type: 'boolean',
133+
},
134+
defaultValue: false,
135+
},
136+
labelText: {
137+
control: {
138+
type: 'text',
139+
},
140+
defaultValue: 'Label text',
141+
},
142+
helperText: {
143+
control: {
144+
type: 'text',
145+
},
146+
defaultValue: 'Helper text',
147+
},
148+
warn: {
149+
control: {
150+
type: 'boolean',
151+
},
152+
defaultValue: false,
153+
},
154+
warnText: {
155+
control: {
156+
type: 'text',
157+
},
158+
defaultValue:
159+
'Warning message that is really long can wrap to more lines but should not be excessively long.',
160+
},
161+
value: {
162+
control: {
163+
type: 'text',
164+
},
165+
},
166+
onChange: {
167+
action: 'clicked',
168+
},
169+
onClick: {
170+
action: 'clicked',
171+
},
172+
size: {
173+
defaultValue: 'md',
174+
options: ['sm', 'md', 'lg', 'xl'],
175+
control: {
176+
type: 'select',
177+
},
178+
},
179+
};

packages/styles/scss/components/text-input/_text-input.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,4 +408,14 @@
408408
svg {
409409
@include high-contrast-mode('icon-fill');
410410
}
411+
412+
.#{$prefix}--text-input__label-wrapper {
413+
display: flex;
414+
width: 100%;
415+
justify-content: space-between;
416+
417+
.#{$prefix}--text-input__label-counter {
418+
align-self: end;
419+
}
420+
}
411421
}

0 commit comments

Comments
 (0)