Skip to content

Commit

Permalink
feat(textinput): add character counter and maxlength to textInput
Browse files Browse the repository at this point in the history
  • Loading branch information
jakeoien committed Aug 12, 2020
1 parent cd53980 commit b45192b
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 14 deletions.
2 changes: 1 addition & 1 deletion packages/hs-react-ui/src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ const Button = ({
variant,
type,
disabled,
...containerProps
...containerProps,
};

return isLoading ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ storiesOf('TextInput', module)
onFocus={onFocusCallback}
onBlur={onBlurCallback}
multiLineIsResizable={boolean('multiLineIsResizable', false)}
showCharacterCount={boolean('showCharacterCount', true, 'Max length')}
maxLength={select(
'maxLength',
{ 5: 5, 20: 20, 100: 100, none: undefined },
20,
'Max length',
)}
allowTextBeyondMaxLength={boolean('allowTextBeyondMaxLength', false, 'Max length')}
/>
);
},
Expand All @@ -106,8 +114,6 @@ storiesOf('TextInput', module)
return (
<TextInput
onChange={event => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const newValue = event.target.value;
setInputValue(newValue);
action('onChange')(newValue);
Expand Down
54 changes: 43 additions & 11 deletions packages/hs-react-ui/src/components/TextInput/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { ReactNode, SyntheticEvent, useCallback } from 'react';
import styled, { StyledComponentBase } from 'styled-components';
import React, { ChangeEvent, EventHandler, ReactNode, SyntheticEvent, useCallback } from 'react';
import styled, { css, StyledComponentBase } from 'styled-components';
import Icon from '@mdi/react';
import { mdiClose } from '@mdi/js';
import debounce from 'lodash.debounce';
import { Div, TextArea, Input as InputElement } from '../../htmlElements';
import { Div, Input as InputElement, TextArea } from '../../htmlElements';
import { SubcomponentPropsType } from '../commonTypes';
import { useColors } from '../../context';
import { disabledStyles } from '../../utils/color';
Expand Down Expand Up @@ -71,12 +71,24 @@ const IconContainer = styled(Div)`
}}
`;

const CharacterCounter = styled(Div)`
${({ textIsTooLong }) => {
const { grayLight, destructive } = useColors();
return css`
position: absolute;
top: calc(100% + 0.25em);
right: 0.25em;
color: ${textIsTooLong ? destructive : grayLight};
`;
}};
`;

const ErrorContainer = styled(Div)`
${() => {
${({ showCharacterCount }) => {
const { destructive } = useColors();
return `
position: absolute;
top: calc(100% + 0.25em);
top: calc(100% + ${showCharacterCount ? '1' : '0'}.25em);
color: ${destructive};
font-size: 0.75rem;
`;
Expand All @@ -88,8 +100,8 @@ export type TextInputProps = {
placeholder?: string;
iconPrefix?: string | ReactNode;
onClear?: (event: SyntheticEvent) => void;
onChange?: (event: SyntheticEvent) => void;
debouncedOnChange?: (event: SyntheticEvent) => void;
onChange?: EventHandler<ChangeEvent<HTMLInputElement>>;
debouncedOnChange?: EventHandler<ChangeEvent<HTMLInputElement>>;
onKeyPress?: (event: SyntheticEvent) => void;
onKeyDown?: (event: SyntheticEvent) => void;
onKeyUp?: (event: SyntheticEvent) => void;
Expand All @@ -109,6 +121,9 @@ export type TextInputProps = {
type?: string;
debounceInterval?: number;
multiLineIsResizable?: boolean;
maxLength?: number;
allowTextBeyondMaxLength?: boolean;
showCharacterCount?: boolean;

StyledContainer?: string & StyledComponentBase<any, {}>;
StyledInput?: string & StyledComponentBase<any, {}>;
Expand Down Expand Up @@ -154,7 +169,7 @@ const TextInput = ({
cols = 10,
rows = 10,
value,
defaultValue,
defaultValue = '',
isValid,
isMultiline,
errorMessage,
Expand All @@ -163,6 +178,9 @@ const TextInput = ({
disabled = false,
debounceInterval = 8,
multiLineIsResizable,
maxLength,
allowTextBeyondMaxLength = false,
showCharacterCount = false,

StyledContainer = Container,
StyledInput, // Not defaulting here due to the issue with <input as="textarea" />
Expand All @@ -174,7 +192,10 @@ const TextInput = ({
errorContainerProps = {},
}: TextInputProps) => {
// Debounce the change function using useCallback so that the function is not initialized each time it renders
const debouncedChange = useCallback(debounce(debouncedOnChange, debounceInterval), []);
const debouncedChange = useCallback(debounce(debouncedOnChange, debounceInterval), [
debouncedOnChange,
debounceInterval,
]);

// Determine the correct input type. Using a single input and the 'as' keyword
// to display as a text area disables the ability to set cols/rows
Expand All @@ -184,6 +205,7 @@ const TextInput = ({
} else if (isMultiline) {
InputComponent = TextAreaInputContainer;
}
const displayValue = value || defaultValue;

return (
<StyledContainer disabled={disabled} isValid={isValid} {...containerProps}>
Expand All @@ -195,8 +217,13 @@ const TextInput = ({
rows={rows}
aria-label={ariaLabel}
placeholder={placeholder}
onChange={(e: SyntheticEvent) => {
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
e.persist();
if (maxLength && maxLength >= 0) {
e.target.value = allowTextBeyondMaxLength
? e.target.value
: e.target.value.slice(0, maxLength);
}
onChange(e);
debouncedChange(e);
}}
Expand All @@ -207,7 +234,7 @@ const TextInput = ({
onBlur={onBlur}
onReset={onReset}
onInput={onInput}
value={value || defaultValue}
value={displayValue}
id={id}
type={type}
multiLineIsResizable={multiLineIsResizable}
Expand All @@ -218,6 +245,11 @@ const TextInput = ({
<Icon path={mdiClose} size="1em" />
</StyledIconContainer>
)}
{showCharacterCount && maxLength && (
<CharacterCounter textIsTooLong={displayValue.length > maxLength}>
{displayValue.length} / {maxLength}
</CharacterCounter>
)}
{isValid === false && errorMessage && (
<StyledErrorContainer {...errorContainerProps}>{errorMessage}</StyledErrorContainer>
)}
Expand Down

0 comments on commit b45192b

Please sign in to comment.