-
Notifications
You must be signed in to change notification settings - Fork 50
feat: add count for customize count logic
#47
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
Changes from all commits
aaba78e
8e8416e
acb514c
d790f11
4d1bfc7
3201aec
b268c69
5c95ff5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
|
|
||
| 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) => | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 一定要用专业名词吗,可以简单点吗,比如 getCount
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
其实习惯了就好了 😂
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 词不达意才更麻烦. 词越精确越好, 只不过我们非英语母语而已.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这个和 @MadCcc 讨论下来,
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这个可以参考 echarts,每个配置都有 formatter 自定义返回,对于图表或者组件,自定义方法的使用场景非常多,api 应该有个规范
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
按照 Naming Standard,render 系列命名需要带上
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 感觉这个有问题啊,当
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 用户自己写了 |
||
| [...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 }) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. exceed 只体现了超出,没说明超出哪个,api 应该加上 max
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这个感觉没必要,exceed 本来就是超出,能超出的自然只有
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这个 如果要通过自定义方法的返回控制用户是否可以继续输入,或者右侧显示
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
最早就是一个,后来发现有输入法收不起来,所以拆了一个裁剪方法。 在
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. clone 本地测试了了下,体验来说没问题 |
||
| 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; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
@@ -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); | ||
|
|
||
|
|
@@ -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: () => { | ||
|
|
@@ -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, | ||
| }); | ||
| } | ||
|
Comment on lines
+96
to
105
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这部分没有抽离到 useCount 吗
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 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); | ||
|
|
@@ -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', | ||
|
|
@@ -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`, | ||
|
|
@@ -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()} | ||
|
|
||
| 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]); | ||
| } |

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个不应该默认是 true 吗,因为外层
count={{}}是真值There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
不是的,原本是
showCount默认为真。现在改名叫count是做计数使用。可以配置strategy+max仅做限制而不展示数字。