Skip to content

Commit

Permalink
feat: add count for customize count logic (#47)
Browse files Browse the repository at this point in the history
* chore: init of len

* chore: support len of demo

* feat: support cuter

* test: add test case

* test: use rc-test

* test: update snapshot

* test: add test case

* chore: clean up
  • Loading branch information
zombieJ authored Sep 27, 2023
1 parent aa08e91 commit f135900
Show file tree
Hide file tree
Showing 11 changed files with 291 additions and 75 deletions.
4 changes: 4 additions & 0 deletions assets/index.less
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
.rc-input {
&-out-of-range {
color: red;
}

&-affix-wrapper {
padding: 2px 8px;
overflow: hidden;
Expand Down
63 changes: 61 additions & 2 deletions docs/examples/show-count.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,69 @@
import Input from 'rc-input';
import type { FC } from 'react';
import React from 'react';
import '../../assets/index.less';
import Input from 'rc-input';

const sharedHeadStyle: React.CSSProperties = {
margin: 0,
padding: 0,
};

const Demo: FC = () => {
return <Input prefixCls="rc-input" showCount />;
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 16,
alignItems: 'start',
}}
>
<h3 style={sharedHeadStyle}>Native</h3>
<Input prefixCls="rc-input" showCount defaultValue="👨‍👩‍👧‍👦" />
<Input prefixCls="rc-input" showCount defaultValue="👨‍👩‍👧‍👦" maxLength={20} />
<h3 style={sharedHeadStyle}>Count</h3>
<h4 style={sharedHeadStyle}>Only Max</h4>
<Input
placeholder="count.max"
prefixCls="rc-input"
defaultValue="🔥"
count={{
show: true,
max: 5,
}}
/>
<h4 style={sharedHeadStyle}>Customize strategy</h4>
<Input
placeholder="Emoji count 1"
prefixCls="rc-input"
defaultValue="🔥"
count={{
show: true,
max: 5,
strategy: (val) =>
[...new (Intl as any).Segmenter().segment(val)].length,
}}
/>
<h4 style={sharedHeadStyle}>Customize exceedFormatter</h4>
<Input
placeholder="Emoji count 1"
prefixCls="rc-input"
defaultValue="🔥"
count={{
show: true,
max: 5,
exceedFormatter: (val, { max }) => {
const segments = [...new (Intl as any).Segmenter().segment(val)];

return segments
.filter((seg) => seg.index + seg.segment.length <= max)
.map((seg) => seg.segment)
.join('');
},
}}
/>
</div>
);
};

export default Demo;
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@
"prettier": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
"pretty-quick": "pretty-quick",
"lint-staged": "lint-staged",
"test": "umi-test test",
"coverage": "father test --coverage",
"test": "rc-test",
"coverage": "rc-test --coverage",
"prepare": "husky install"
},
"dependencies": {
Expand Down Expand Up @@ -69,10 +69,10 @@
"np": "^7.0.0",
"prettier": "^2.0.5",
"pretty-quick": "^3.0.0",
"rc-test": "^7.0.15",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"typescript": "^4.0.5",
"umi-test": "^1.9.7"
"typescript": "^4.0.5"
},
"peerDependencies": {
"react": ">=16.0.0",
Expand Down
90 changes: 67 additions & 23 deletions src/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,10 @@ import React, {
useState,
} from 'react';
import BaseInput from './BaseInput';
import useCount from './hooks/useCount';
import type { InputProps, InputRef } from './interface';
import type { InputFocusOptions } from './utils/commonUtils';
import {
fixControlledValue,
resolveOnChange,
triggerFocus,
} from './utils/commonUtils';
import { resolveOnChange, triggerFocus } from './utils/commonUtils';

const Input = forwardRef<InputRef, InputProps>((props, ref) => {
const {
Expand All @@ -32,17 +29,16 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
maxLength,
suffix,
showCount,
count,
type = 'text',
classes,
classNames,
styles,
...rest
} = props;

const [value, setValue] = useMergedState(props.defaultValue, {
value: props.value,
});
const [focused, setFocused] = useState<boolean>(false);
const compositionRef = React.useRef(false);

const inputRef = useRef<HTMLInputElement>(null);

Expand All @@ -52,6 +48,21 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
}
};

// ====================== Value =======================
const [value, setValue] = useMergedState(props.defaultValue, {
value: props.value,
});
const formatValue =
value === undefined || value === null ? '' : String(value);

// ====================== Count =======================
const countConfig = useCount(count, showCount);
const mergedMax = countConfig.max || maxLength;
const valueLength = countConfig.strategy(formatValue);

const isOutOfRange = !!mergedMax && valueLength > mergedMax;

// ======================= Ref ========================
useImperativeHandle(ref, () => ({
focus,
blur: () => {
Expand All @@ -74,15 +85,40 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
setFocused((prev) => (prev && disabled ? false : prev));
}, [disabled]);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (props.value === undefined) {
setValue(e.target.value);
const triggerChange = (
e:
| React.ChangeEvent<HTMLInputElement>
| React.CompositionEvent<HTMLInputElement>,
currentValue: string,
) => {
let cutValue = currentValue;

if (
!compositionRef.current &&
countConfig.exceedFormatter &&
countConfig.max &&
countConfig.strategy(currentValue) > countConfig.max
) {
cutValue = countConfig.exceedFormatter(currentValue, {
max: countConfig.max,
});
}
setValue(cutValue);

if (inputRef.current) {
resolveOnChange(inputRef.current, e, onChange);
resolveOnChange(inputRef.current, e, onChange, cutValue);
}
};

const onInternalChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
triggerChange(e, e.target.value);
};

const onCompositionEnd = (e: React.CompositionEvent<HTMLInputElement>) => {
compositionRef.current = false;
triggerChange(e, e.currentTarget.value);
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (onPressEnter && e.key === 'Enter') {
onPressEnter(e);
Expand Down Expand Up @@ -126,6 +162,7 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
// specify either the value prop, or the defaultValue prop, but not both.
'defaultValue',
'showCount',
'count',
'classes',
'htmlSize',
'styles',
Expand All @@ -136,40 +173,46 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
<input
autoComplete={autoComplete}
{...otherProps}
onChange={handleChange}
onChange={onInternalChange}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
className={clsx(
prefixCls,
{
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-out-of-range`]: isOutOfRange,
},
classNames?.input,
)}
style={styles?.input}
ref={inputRef}
size={htmlSize}
type={type}
onCompositionStart={() => {
compositionRef.current = true;
}}
onCompositionEnd={onCompositionEnd}
/>
);
};

const getSuffix = () => {
// Max length value
const hasMaxLength = Number(maxLength) > 0;
const hasMaxLength = Number(mergedMax) > 0;

if (suffix || showCount) {
const val = fixControlledValue(value);
const valueLength = [...val].length;
const dataCount =
typeof showCount === 'object'
? showCount.formatter({ value: val, count: valueLength, maxLength })
: `${valueLength}${hasMaxLength ? ` / ${maxLength}` : ''}`;
if (suffix || countConfig.show) {
const dataCount = countConfig.showFormatter
? countConfig.showFormatter({
value: formatValue,
count: valueLength,
maxLength: mergedMax,
})
: `${valueLength}${hasMaxLength ? ` / ${mergedMax}` : ''}`;

return (
<>
{!!showCount && (
{countConfig.show && (
<span
className={clsx(
`${prefixCls}-show-count-suffix`,
Expand All @@ -192,14 +235,15 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
return null;
};

// ====================== Render ======================
return (
<BaseInput
{...rest}
prefixCls={prefixCls}
className={className}
inputElement={getInputElement()}
handleReset={handleReset}
value={fixControlledValue(value)}
value={formatValue}
focused={focused}
triggerFocus={focus}
suffix={getSuffix()}
Expand Down
48 changes: 48 additions & 0 deletions src/hooks/useCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as React from 'react';
import type { InputProps } from '..';
import type { CountConfig, ShowCountFormatter } from '../interface';

type ForcedCountConfig = Omit<CountConfig, 'show'> &
Pick<Required<CountConfig>, 'strategy'> & {
show: boolean;
showFormatter?: ShowCountFormatter;
};

/**
* Cut `value` by the `count.max` prop.
*/
export function inCountRange(value: string, countConfig: ForcedCountConfig) {
if (!countConfig.max) {
return true;
}

const count = countConfig.strategy(value);
return count <= countConfig.max;
}

export default function useCount(
count?: CountConfig,
showCount?: InputProps['showCount'],
) {
return React.useMemo<ForcedCountConfig>(() => {
let mergedConfig = count;

if (!count) {
mergedConfig = {
show:
typeof showCount === 'object' && showCount.formatter
? showCount.formatter
: !!showCount,
};
}

const { show, ...rest } = mergedConfig!;

return {
...rest,
show: !!show,
showFormatter: typeof show === 'function' ? show : undefined,
strategy: rest.strategy || ((value) => value.length),
};
}, [count, showCount]);
}
31 changes: 24 additions & 7 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,23 @@ export interface BaseInputProps extends CommonInputProps {
};
}

export interface ShowCountProps {
formatter: (args: {
value: string;
count: number;
maxLength?: number;
}) => ReactNode;
export type ShowCountFormatter = (args: {
value: string;
count: number;
maxLength?: number;
}) => ReactNode;

export type ExceedFormatter = (
value: string,
config: { max: number },
) => string;

export interface CountConfig {
max?: number;
strategy?: (value: string) => number;
show?: boolean | ShowCountFormatter;
/** Trigger when content larger than the `max` limitation */
exceedFormatter?: ExceedFormatter;
}

export interface InputProps
Expand Down Expand Up @@ -103,7 +114,12 @@ export interface InputProps
string
>;
onPressEnter?: KeyboardEventHandler<HTMLInputElement>;
showCount?: boolean | ShowCountProps;
/** @deprecated Use `count` instead */
showCount?:
| boolean
| {
formatter: ShowCountFormatter;
};
autoComplete?: string;
htmlSize?: number;
classNames?: CommonInputProps['classNames'] & {
Expand All @@ -114,6 +130,7 @@ export interface InputProps
input?: CSSProperties;
count?: CSSProperties;
};
count?: CountConfig;
}

export interface InputRef {
Expand Down
Loading

0 comments on commit f135900

Please sign in to comment.