Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react): added counter to textinput and storybook updates #12139

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7885,6 +7885,9 @@ Map {
"disabled": Object {
"type": "bool",
},
"enableCounter": Object {
"type": "bool",
},
"helperText": Object {
"type": "node",
},
Expand All @@ -7911,6 +7914,9 @@ Map {
"light": Object {
"type": "bool",
},
"maxCount": Object {
"type": "number",
},
"onChange": Object {
"type": "func",
},
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/components/TextArea/TextArea-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,13 @@ describe('TextArea', () => {
<TextArea id="counter2" labelText="someLabel" maxCount={5} />
);

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

it('should not render element without only maxCount prop passed in', () => {
it('should not render counter with only maxCount prop passed in', () => {
Comment on lines +139 to +145
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wording fixes

expect(
counterTestWrapper2.exists(`${prefix}--text-area__counter`)
).toEqual(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,11 @@ export const WithLayer = () => {
};

export const Skeleton = () => <TextAreaSkeleton />;

export const Playground = (args) => (
<TextArea
{...args}
labelText="Text area label"
helperText="Optional helper text."
/>
);
tw15egan marked this conversation as resolved.
Show resolved Hide resolved
46 changes: 43 additions & 3 deletions packages/react/src/components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import PropTypes from 'prop-types';
import React, { useContext } from 'react';
import React, { useContext, useState } from 'react';
import classNames from 'classnames';
import { useNormalizedInputProps } from '../../internal/useNormalizedInputProps';
import PasswordInput from './PasswordInput';
Expand Down Expand Up @@ -37,6 +37,8 @@ const TextInput = React.forwardRef(function TextInput(
type = 'text',
warn = false,
warnText,
enableCounter = false,
maxCount,
...rest
},
ref
Expand All @@ -45,6 +47,11 @@ const TextInput = React.forwardRef(function TextInput(

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

const { defaultValue, value } = rest;
const [textCount, setTextCount] = useState(
defaultValue?.length || value?.length || 0
);

const normalizedProps = useNormalizedInputProps({
id,
readOnly,
Expand All @@ -71,6 +78,7 @@ const TextInput = React.forwardRef(function TextInput(
id,
onChange: (evt) => {
if (!normalizedProps.disabled) {
setTextCount(evt.target.value?.length);
onChange(evt);
}
},
Expand All @@ -89,6 +97,11 @@ const TextInput = React.forwardRef(function TextInput(
['aria-describedby']: helperText && normalizedProps.helperId,
...rest,
};

if (enableCounter) {
sharedTextInputProps.maxLength = maxCount;
}

const inputWrapperClasses = classNames(
[
enabled
Expand Down Expand Up @@ -131,12 +144,29 @@ const TextInput = React.forwardRef(function TextInput(
[`${prefix}--text-input__readonly-icon`]: readOnly,
});

const counterClasses = classNames(`${prefix}--label`, {
[`${prefix}--label--disabled`]: disabled,
[`${prefix}--text-input__label-counter`]: true,
});

const counter =
enableCounter && maxCount ? (
<div className={counterClasses}>{`${textCount}/${maxCount}`}</div>
) : null;

const label = labelText ? (
<label htmlFor={id} className={labelClasses}>
{labelText}
</label>
) : null;

const labelWrapper = (
<div className={`${prefix}--text-input__label-wrapper`}>
{label}
{counter}
</div>
tw15egan marked this conversation as resolved.
Show resolved Hide resolved
);

const helper = helperText ? (
<div id={normalizedProps.helperId} className={helperTextClasses}>
{helperText}
Expand All @@ -160,10 +190,10 @@ const TextInput = React.forwardRef(function TextInput(
return (
<div className={inputWrapperClasses}>
{!inline ? (
label
labelWrapper
) : (
<div className={`${prefix}--text-input__label-helper-wrapper`}>
{label}
{labelWrapper}
{!isFluid && helper}
</div>
)}
Expand Down Expand Up @@ -203,6 +233,11 @@ TextInput.propTypes = {
*/
disabled: PropTypes.bool,

/**
* Specify whether to display the character counter
*/
enableCounter: PropTypes.bool,

/**
* Provide text that is used alongside the control label for additional help
*/
Expand Down Expand Up @@ -245,6 +280,11 @@ TextInput.propTypes = {
*/
light: PropTypes.bool,

/**
* Max character count allowed for the textarea. This is needed in order for enableCounter to display
*/
maxCount: PropTypes.number,

/**
* Optionally provide an `onChange` handler that is called whenever `<input>`
* is updated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,5 +320,40 @@ describe('TextInput', () => {
);
expect(icon).toBeInTheDocument();
});

it('should not render counter with only enableCounter prop passed in', () => {
render(
<TextInput id="input-1" labelText="TextInput label" enableCounter />
);

const counter = screen.queryByText('0/5');

expect(counter).not.toBeInTheDocument();
});

it('should not render counter with only maxCount prop passed in', () => {
render(
<TextInput id="input-1" labelText="TextInput label" enableCounter />
);

const counter = screen.queryByText('0/5');

expect(counter).not.toBeInTheDocument();
});

it('should have the expected classes for counter', () => {
render(
<TextInput
id="input-1"
labelText="TextInput label"
enableCounter
maxCount={5}
/>
);

const counter = screen.queryByText('0/5');

expect(counter).toBeInTheDocument();
});
tw15egan marked this conversation as resolved.
Show resolved Hide resolved
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ export const Default = () => (

export const Fluid = () => (
<FluidForm>
<TextInput type="text" labelText="Text input label" />
<TextInput type="text" labelText="Text input label" id="text-input-1" />
</FluidForm>
);

export const TogglePasswordVisibility = () => {
return (
<TextInput.PasswordInput
id="text-input-1"
labelText="Text input label"
helperText="Optional help text"
/>
Expand All @@ -50,6 +51,7 @@ export const ReadOnly = () => {
helperText="Optional help text"
value="This is read only, you can't type more."
readOnly
id="text-input-1"
/>
);
};
Expand All @@ -61,18 +63,21 @@ export const WithLayer = () => {
type="text"
labelText="First layer"
helperText="Optional help text"
id="text-input-1"
/>
<Layer>
<TextInput
type="text"
labelText="Second layer"
helperText="Optional help text"
id="text-input-2"
/>
<Layer>
<TextInput
type="text"
labelText="Third layer"
helperText="Optional help text"
id="text-input-3"
tw15egan marked this conversation as resolved.
Show resolved Hide resolved
/>
</Layer>
</Layer>
Expand All @@ -81,3 +86,94 @@ export const WithLayer = () => {
};

export const Skeleton = () => <TextInputSkeleton />;

export const Playground = (args) => (
<div style={{ width: args.playgroundWidth }}>
<TextInput {...args} id="text-input-1" type="text" />
</div>
);

Playground.argTypes = {
playgroundWidth: {
control: { type: 'range', min: 300, max: 800, step: 50 },
defaultValue: 300,
},
className: {
control: {
type: 'text',
},
defaultValue: 'input-test-class',
},
defaultValue: {
control: {
type: 'text',
},
},
placeholder: {
control: {
type: 'text',
},
defaultValue: 'Placeholder text',
},
invalid: {
control: {
type: 'boolean',
},
defaultValue: false,
},
invalidText: {
control: {
type: 'text',
},
defaultValue: 'Invalid text',
},
disabled: {
control: {
type: 'boolean',
},
defaultValue: false,
},
labelText: {
control: {
type: 'text',
},
defaultValue: 'Label text',
},
helperText: {
control: {
type: 'text',
},
defaultValue: 'Helper text',
},
warn: {
control: {
type: 'boolean',
},
defaultValue: false,
},
warnText: {
control: {
type: 'text',
},
defaultValue:
'Warning message that is really long can wrap to more lines but should not be excessively long.',
},
value: {
control: {
type: 'text',
},
},
onChange: {
action: 'clicked',
},
onClick: {
action: 'clicked',
},
size: {
defaultValue: 'md',
options: ['sm', 'md', 'lg', 'xl'],
control: {
type: 'select',
},
},
};
10 changes: 10 additions & 0 deletions packages/styles/scss/components/text-input/_text-input.scss
Original file line number Diff line number Diff line change
Expand Up @@ -408,4 +408,14 @@
svg {
@include high-contrast-mode('icon-fill');
}

.#{$prefix}--text-input__label-wrapper {
display: flex;
width: 100%;
justify-content: space-between;

.#{$prefix}--text-input__label-counter {
align-self: end;
}
}
}