Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
103 changes: 77 additions & 26 deletions README.md

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions assets/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,25 @@
}
}
}

@textarea-prefix-cls: rc-textarea;

.rc-textarea-affix-wrapper {
display: inline-block;
box-sizing: border-box;

textarea {
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 0;
border: 1px solid #1677ff;
}
}

.@{textarea-prefix-cls}-out-of-range {
&,
& textarea {
color: red;
}
}
22 changes: 22 additions & 0 deletions docs/demo/textarea.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: TextArea
nav:
title: Demo
path: /demo
---

## Basic

<code src="../examples/textarea-basic.tsx"></code>

## Auto Size

<code src="../examples/textarea-auto-size.tsx"></code>

## Allow Clear

<code src="../examples/textarea-allow-clear.tsx"></code>

## Show Count

<code src="../examples/textarea-show-count.tsx"></code>
25 changes: 25 additions & 0 deletions docs/examples/textarea-allow-clear.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* eslint-disable no-console */
import Input from '@rc-component/input';
import React, { useState, type ChangeEvent } from 'react';

const TextArea = Input.TextArea;

export default function App() {
const [value, setValue] = useState('hello\nworld');

const onChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
const {
target: { value: currentValue },
} = e;
setValue(currentValue);
};

return (
<div>
<p>Uncontrolled</p>
<TextArea autoSize allowClear />
<p>controlled</p>
<TextArea value={value} onChange={onChange} allowClear />
</div>
);
}
39 changes: 39 additions & 0 deletions docs/examples/textarea-auto-size.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* eslint-disable no-console */
import Input, { type TextAreaProps } from '@rc-component/input';
import React, { useState, type ChangeEvent } from 'react';

const TextArea = Input.TextArea;

export default function App() {
const [value, setValue] = useState('hello\nworld');

const onChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
const {
target: { value: currentValue },
} = e;
setValue(currentValue);
};

const onResize: TextAreaProps['onResize'] = ({ width, height }) => {
console.log(`size is changed, width:${width} height:${height}`);
};

return (
<div>
<p>when set to true</p>
<TextArea
autoSize
onResize={onResize}
value={value}
onChange={onChange}
/>
<p>when set to object of minRows and maxRows</p>
<TextArea
autoSize={{ minRows: 5, maxRows: 15 }}
onResize={onResize}
value={value}
onChange={onChange}
/>
</div>
);
}
40 changes: 40 additions & 0 deletions docs/examples/textarea-basic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* eslint-disable no-console */
import Input, { type TextAreaProps } from '@rc-component/input';
import React, { useState, type ChangeEvent, type KeyboardEvent } from 'react';

const TextArea = Input.TextArea;

export default function App() {
const [value, setValue] = useState('');

const onChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
const {
target: { value: currentValue },
} = e;
console.log(e.target.value);
setValue(currentValue);
};

const onResize: TextAreaProps['onResize'] = ({ width, height }) => {
console.log(`size is changed, width:${width} height:${height}`);
};

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const onPressEnter = (e: KeyboardEvent<HTMLTextAreaElement>) => {
console.log(`enter key is pressed`);
};

return (
<div>
<TextArea
prefixCls="custom-textarea"
onPressEnter={onPressEnter}
onResize={onResize}
value={value}
onChange={onChange}
autoFocus
onFocus={() => console.log('focus')}
/>
</div>
);
}
57 changes: 57 additions & 0 deletions docs/examples/textarea-show-count.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/* eslint-disable no-console */
import Input from '@rc-component/input';
import React, { useState, type ChangeEvent } from 'react';
import '../../assets/index.less';

const TextArea = Input.TextArea;

export default function App() {
const [value, setValue] = useState('hello\nworld');

const onChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
const {
target: { value: currentValue },
} = e;
setValue(currentValue);
};

return (
<div>
<p>Uncontrolled</p>
<TextArea autoSize showCount />
<p>controlled</p>
<TextArea value={value} onChange={onChange} showCount maxLength={100} />
<p>with height</p>
<TextArea
value={value}
onChange={onChange}
showCount
style={{ height: 200, width: '100%', resize: 'vertical' }}
/>
<hr />
<p>Count.exceedFormatter</p>
<TextArea
defaultValue="👨‍👨‍👧‍👦"
count={{
show: true,
max: 5,
}}
/>
<TextArea
defaultValue="🔥"
count={{
show: true,
max: 5,
exceedFormatter: (val, { max }) => {
const segments = [...new Intl.Segmenter().segment(val)];

return segments
.filter((seg) => seg.index + seg.segment.length <= max)
.map((seg) => seg.segment)
.join('');
},
}}
/>
</div>
);
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"prepare": "husky install"
},
"dependencies": {
"@rc-component/resize-observer": "^1.1.1",
"@rc-component/util": "^1.4.0",
"clsx": "^2.1.1"
},
Expand Down
66 changes: 18 additions & 48 deletions src/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { clsx } from 'clsx';
import useControlledState from '@rc-component/util/lib/hooks/useControlledState';
import omit from '@rc-component/util/lib/omit';
import React, {
forwardRef,
Expand All @@ -11,6 +10,9 @@ import React, {
import type { HolderRef } from './BaseInput';
import BaseInput from './BaseInput';
import useCount from './hooks/useCount';
import useCountDisplay from './hooks/useCountDisplay';
import useCountExceed from './hooks/useCountExceed';
import useMergedValue from './hooks/useMergedValue';
import type { ChangeEventInfo, InputProps, InputRef } from './interface';
import { resolveOnChange } from './utils/commonUtils';
import {
Expand Down Expand Up @@ -58,21 +60,21 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
};

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

// =================== Select Range ===================
const [selection, setSelection] = useState<
[start: number, end: number] | null
>(null);
const { setValue, formatValue } = useMergedValue(
props.defaultValue,
props.value,
);

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

const isOutOfRange = !!mergedMax && valueLength > mergedMax;
const { isOutOfRange, dataCount } = useCountDisplay({
countConfig,
value: formatValue,
maxLength,
});
const getExceedValue = useCountExceed({
countConfig,
getTarget: () => inputRef.current,
});

// ======================= Ref ========================
useImperativeHandle(ref, () => ({
Expand Down Expand Up @@ -108,25 +110,9 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
currentValue: string,
info: ChangeEventInfo,
) => {
let cutValue = currentValue;

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

if (currentValue !== cutValue) {
setSelection([
inputRef.current?.selectionStart || 0,
inputRef.current?.selectionEnd || 0,
]);
}
} else if (info.source === 'compositionEnd') {
if (info.source === 'compositionEnd' && currentValue === cutValue) {
// Avoid triggering twice
// https://github.com/ant-design/ant-design/issues/46587
return;
Expand All @@ -138,12 +124,6 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
}
};

useEffect(() => {
if (selection) {
inputRef.current?.setSelectionRange(...selection);
}
}, [selection]);

const onInternalChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
triggerChange(e, e.target.value, {
source: 'change',
Expand Down Expand Up @@ -260,17 +240,7 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {

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

if (suffix || countConfig.show) {
const dataCount = countConfig.showFormatter
? countConfig.showFormatter({
value: formatValue,
count: valueLength,
maxLength: mergedMax,
})
: `${valueLength}${hasMaxLength ? ` / ${mergedMax}` : ''}`;

return (
<>
{countConfig.show && (
Expand Down
Loading