Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions src/components/config-provider/config-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ type Config = {
searchBar?: {
searchIcon?: ReactNode
}
textArea?: {
clearIcon?: ReactNode
}
}

export const defaultConfigRef: {
Expand Down
14 changes: 13 additions & 1 deletion src/components/text-area/demos/demo1.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from 'react'
import { TextArea } from 'antd-mobile'
import { DemoBlock } from 'demos'
import React, { useState } from 'react'

export default () => {
const [value, setValue] = useState('')
Expand All @@ -20,13 +20,25 @@ export default () => {
<TextArea placeholder='请输入内容' rows={5} />
</DemoBlock>

<DemoBlock title='带清除按钮'>
<TextArea placeholder='请输入内容' clearable />
</DemoBlock>

<DemoBlock title='根据内容自动调整高度'>
<TextArea
placeholder='请输入内容'
autoSize={{ minRows: 3, maxRows: 5 }}
/>
</DemoBlock>

<DemoBlock title='带清除按钮的自动调整高度'>
<TextArea
placeholder='请输入内容'
autoSize={{ minRows: 3, maxRows: 5 }}
clearable
/>
</DemoBlock>

<DemoBlock title='字数统计'>
<TextArea defaultValue={'北极星垂地,\n东山月满川。'} showCount />
</DemoBlock>
Expand Down
5 changes: 4 additions & 1 deletion src/components/text-area/index.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@ Long text input that requires wrapping.
| Name | Description | Type | Default |
| --- | --- | --- | --- |
| autoSize | Adaptive content height | `boolean \| { minRows?: number, maxRows?: number }` | `false` |
| clearable | Whether to enable the clear icon, the input box will be cleared after clicking the clear icon | `boolean` | `false` |
| clearIcon | Custom clear icon | `ReactNode` | `<CloseCircleFill />` |
| defaultValue | Input value by default | `string` | - |
| id | `id` of `textarea` element, often used in conjunction with `label` | `string` | - |
| maxLength | Maximum number of characters | `number` | - |
| onChange | Triggered when the TextArea content changed | `(value: string) => void` | - |
| onClear | Triggered after clicking the clear button | `() => void` | - |
| placeholder | Hint text | `string` | - |
| rows | Number of rows | `number` | `2` |
| showCount | Display the number of words, supports custom render | `boolean \| ((length: number, maxLength?: number) => ReactNode)` | `false` |
| value | Input value | `string` | - |

In addition, the following native attributes are supported: `autoComplete` `autoFocus` `disabled` `readOnly` `onFocus` `onBlur` `onCompositionStart` `onCompositionEnd` `onClick`
In addition, the following native attributes are supported: `autoComplete` `autoFocus` `disabled` `readOnly` `onFocus` `onBlur` `onCompositionStart` `onCompositionEnd` `onClick` `onKeyDown` `enterKeyHint`

### CSS Variables

Expand Down
5 changes: 4 additions & 1 deletion src/components/text-area/index.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@
| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| autoSize | 自适应内容高度 | `boolean \| { minRows?: number, maxRows?: number }` | `false` |
| clearable | 是否启用清除图标,点击清除图标后会清空输入框 | `boolean` | `false` |
| clearIcon | 自定义清除图标 | `ReactNode` | `<CloseCircleFill />` |
| defaultValue | 默认值 | `string` | - |
| id | `textarea` 元素的 `id`,常用来配合 `label` 使用 | `string` | - |
| maxLength | 最大字符数 | `number` | - |
| onChange | 文本域内容变化时触发 | `(value: string) => void` | - |
| onClear | 点击清除按钮后触发 | `() => void` | - |
| placeholder | 提示文本 | `string` | - |
| rows | 行数 | `number` | `2` |
| showCount | 显示字数,支持自定义渲染 | `boolean \| ((length: number, maxLength?: number) => ReactNode)` | `false` |
| value | 输入值 | `string` | - |

此外还支持以下原生属性:`autoComplete` `autoFocus` `disabled` `readOnly` `onFocus` `onBlur` `onCompositionStart` `onCompositionEnd` `onClick`
此外还支持以下原生属性:`autoComplete` `autoFocus` `disabled` `readOnly` `onFocus` `onBlur` `onCompositionStart` `onCompositionEnd` `onClick` `onKeyDown` `enterKeyHint`

### CSS 变量

Expand Down
94 changes: 93 additions & 1 deletion src/components/text-area/tests/text-area.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import React, { createRef } from 'react'
import { render, fireEvent, act } from 'testing'
import { act, fireEvent, render, screen } from 'testing'
import TextArea, { TextAreaRef } from '..'
import ConfigProvider from '../../config-provider'

jest.mock('../../../utils/validate', () => ({
isIOS: function () {
return true
},
}))
Comment thread
viko16 marked this conversation as resolved.
Outdated

const classPrefix = 'adm-text-area'
const lineHeight = 25
Expand Down Expand Up @@ -165,4 +172,89 @@ describe('TextArea', () => {
expect(onEnterPress).toBeCalledTimes(2)
fireEvent.keyUp(textarea, { keyCode: 14 })
})

test('the clear button should works', async () => {
const { container } = render(
<TextArea clearable defaultValue={'testValue'} />
)
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
const clearBtn = container.querySelector(`.${classPrefix}-clear`)
expect(clearBtn).toBeInTheDocument()
fireEvent.click(clearBtn as HTMLElement)
expect(textarea.value).toBe('')
})

test('should works with composition', async () => {
const onCompositionStart = jest.fn()
const onCompositionEnd = jest.fn()
const { container } = render(
<TextArea
clearable
defaultValue={'testValue'}
onCompositionStart={onCompositionStart}
onCompositionEnd={onCompositionEnd}
/>
)
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
act(() => {
textarea.focus()
})
expect(textarea).toHaveFocus()
fireEvent.compositionStart(textarea)
expect(onCompositionStart).toBeCalledTimes(1)
fireEvent.compositionEnd(textarea)
expect(onCompositionEnd).toBeCalledTimes(1)
fireEvent.compositionStart(textarea)
expect(onCompositionStart).toBeCalledTimes(2)

const clearBtn = container.querySelector(
`.${classPrefix}-clear`
) as HTMLElement
expect(clearBtn).toBeInTheDocument()
fireEvent.click(clearBtn)
expect(textarea).not.toHaveFocus()
})

test('onClear callback', () => {
const onClear = jest.fn()
const { container } = render(
<TextArea clearable defaultValue={'testValue'} onClear={onClear} />
)
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
fireEvent.focus(textarea)
const clearBtn = container.querySelector(`.${classPrefix}-clear`)
fireEvent.click(clearBtn as HTMLElement)
expect(onClear).toBeCalledTimes(1)
expect(textarea.value).toBe('')
})

describe('clearIcon', () => {
it('default', () => {
const { baseElement } = render(<TextArea value='foobar' clearable />)
expect(baseElement.querySelector('.antd-mobile-icon')).toBeTruthy()
})

it('props', () => {
render(<TextArea value='foobar' clearable clearIcon='bamboo' />)
expect(screen.getByText('bamboo')).toBeVisible()
})

it('context', () => {
render(
<ConfigProvider textArea={{ clearIcon: 'little' }}>
<TextArea value='foobar' clearable />
</ConfigProvider>
)
expect(screen.getByText('little')).toBeVisible()
})

it('props override context', () => {
render(
<ConfigProvider textArea={{ clearIcon: 'little' }}>
<TextArea value='foobar' clearable clearIcon='bamboo' />
</ConfigProvider>
)
expect(screen.getByText('bamboo')).toBeVisible()
})
})
})
17 changes: 17 additions & 0 deletions src/components/text-area/text-area.less
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
--text-align: left;
--count-text-align: right;
position: relative;
display: flex;
align-items: flex-start;
width: 100%;
max-width: 100%;
max-height: 100%;
Expand Down Expand Up @@ -75,3 +77,18 @@
font-size: var(--adm-font-size-9);
padding-top: 8px;
}

.adm-text-area-clear {
flex: none;
margin-left: 8px;
color: var(--adm-color-light);
&:active {
color: var(--adm-color-weak);
}
padding: 4px;
cursor: pointer;
Comment thread
viko16 marked this conversation as resolved.
.antd-mobile-icon {
display: block;
font-size: var(--adm-font-size-7);
}
}
34 changes: 33 additions & 1 deletion src/components/text-area/text-area.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { useIsomorphicLayoutEffect } from 'ahooks'
import { CloseCircleFill } from 'antd-mobile-icons'
import type { ReactNode } from 'react'
import React, { forwardRef, useImperativeHandle, useRef } from 'react'
import runes from 'runes2'
import useInputHandleKeyDown from '../../components/input/useInputHandleKeyDown'
import { devError } from '../../utils/dev-log'
import { NativeProps, withNativeProps } from '../../utils/native-props'
import { usePropsValue } from '../../utils/use-props-value'
import { isIOS } from '../../utils/validate'
import { mergeProps } from '../../utils/with-default-props'
import { useConfig } from '../config-provider'

const classPrefix = 'adm-text-area'

Expand Down Expand Up @@ -50,6 +53,9 @@ export type TextAreaProps = Pick<
| 'previous'
| 'search'
| 'send'
clearable?: boolean
clearIcon?: ReactNode
onClear?: () => void
} & NativeProps<
| '--font-size'
| '--color'
Expand All @@ -71,11 +77,13 @@ const defaultProps = {
showCount: false as NonNullable<TextAreaProps['showCount']>,
autoSize: false as NonNullable<TextAreaProps['autoSize']>,
defaultValue: '',
clearIcon: <CloseCircleFill />,
}

export const TextArea = forwardRef<TextAreaRef, TextAreaProps>(
(p: TextAreaProps, ref) => {
const props = mergeProps(defaultProps, p)
const { locale, textArea: componentConfig = {} } = useConfig()
const props = mergeProps(defaultProps, componentConfig, p)
const { autoSize, showCount, maxLength } = props
const [value, setValue] = usePropsValue({
...props,
Expand Down Expand Up @@ -137,6 +145,9 @@ export const TextArea = forwardRef<TextAreaRef, TextAreaProps>(

const compositingRef = useRef(false)

const shouldShowClear =
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Input 的设计是聚焦的时候才展示清除按钮。因为在长表单的情况下,手机不同于 PC,清除按钮会过多占据视觉区域。

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

我曾经尝试过添加 onlyShowClearWhenFocus 功能,但问题是如果清除按钮动态出现/消失的话,会导致 TextArea 宽度变化,影响 autoSize 功能。

Input 没问题是因为永远只有单行,改变输入区域宽度不影响高度。

同时也调研过淘宝、拼多多、微信的多行输入框,都是常驻。感觉我们可以在 #7021 上讨论下。

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

我的想法是可以在存在 clear 的时候提供一下边缘 padding,这样宽度就是稳定的了。因为 Input 和 TextArea 混用一个有一个没有,这个是非常古怪的。

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

但是当自定义 clearIcon 的时候,这个 padding 就不固定了

A. 按照默认 <CloseCircleFill /> 固定一个 padding,那如果自定义 clearIcon 更宽就会遮挡
B. 要求自定义 clearIcon 的时候再传入 padding 或者 CSS Var,可行但是更绕了

而且提供边缘 padding 的时候,一样空了一列,视觉上其实也蛮奇怪的
image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

嗯,明白。这个的确是比较讨厌的点。在 Input 里不用考虑换行做的动态 padding。要不然这样,如果有动态高度的,就固定 padding,反之就是动态 padding 如何?做尽可能的保全

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

longshot20260225104727_compressed.mp4

调整成了 clearIcon 会根据 focus 显示消失,按照上面的讨论:

  • 在 autoSize 时,clearIcon 的位置会持续占据一列
  • 非 autoSize 时,clearIcon 仅在聚焦的时候出现

取了一个相对平衡的点,也在文档上补充说明了这种现象。

props.clearable && !!value && !props.readOnly && !props.disabled

let count
const valueLength = runes(value).length
if (typeof showCount === 'function') {
Expand Down Expand Up @@ -201,6 +212,27 @@ export const TextArea = forwardRef<TextAreaRef, TextAreaProps>(
onKeyDown={handleKeydown}
enterKeyHint={props.enterKeyHint}
/>
{shouldShowClear && (
<div
className={`${classPrefix}-clear`}
onMouseDown={e => {
e.preventDefault()
}}
onClick={() => {
setValue('')
props.onClear?.()

// https://github.com/ant-design/ant-design-mobile/issues/5212
if (isIOS() && compositingRef.current) {
compositingRef.current = false
nativeTextAreaRef.current?.blur()
}
}}
aria-label={locale.TextArea.clear}
>
{props.clearIcon}
</div>
)}
{count}

{autoSize && (
Expand Down
1 change: 1 addition & 0 deletions src/locales/ar-SA.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const arSA = mergeLocale(base, {
'Stepper': { 'decrease': 'يقلل', 'increase': 'يزيد' },
'Switch': { 'name': 'زر الفتح والإغلاق' },
'Selector': { 'name': 'اختر مجموعة' },
'TextArea': { 'clear': 'مسح' },
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
})

export default arSA
3 changes: 3 additions & 0 deletions src/locales/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ export const base = {
Selector: {
name: 'Selector',
},
TextArea: {
clear: 'clear',
},
}

export type Locale = typeof base
3 changes: 3 additions & 0 deletions src/locales/cnr-ME.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ const cnrME = mergeLocale(base, {
Selector: {
name: 'Selektor',
},
TextArea: {
clear: 'obriši',
},
})

export default cnrME
3 changes: 3 additions & 0 deletions src/locales/da-DK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ const daDK = mergeLocale(base, {
Selector: {
name: 'Vælger',
},
TextArea: {
clear: 'ryd',
},
})

export default daDK
1 change: 1 addition & 0 deletions src/locales/de-DE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ const deDE = mergeLocale(base, {
'Stepper': { 'decrease': 'Reduzieren', 'increase': 'Erhöhen' },
'Switch': { 'name': 'Schalter' },
'Selector': { 'name': 'Gruppe auswählen' },
'TextArea': { 'clear': 'Löschen' },
})

export default deDE
6 changes: 6 additions & 0 deletions src/locales/es-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ const esES = mergeLocale(base, {
ImageUploader: {
uploading: 'Subiendo...',
},
Input: {
clear: 'Borrar',
},
Mask: {
name: 'Máscara',
},
Expand All @@ -106,6 +109,9 @@ const esES = mergeLocale(base, {
canRelease: 'Suelte para refrescar inmediatamente',
complete: 'Refrescó exitosamente',
},
TextArea: {
clear: 'Borrar',
},
})

export default esES
3 changes: 3 additions & 0 deletions src/locales/fa-IR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ const faIR = mergeLocale(base, {
NumberKeyboard: {
backspace: 'حذف',
},
TextArea: {
clear: 'پاک کردن',
},
})

export default faIR
6 changes: 6 additions & 0 deletions src/locales/fr-FR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ const frFR = mergeLocale(base, {
InfiniteScroll: {
noMore: 'Non, plus maintenant.',
},
Input: {
clear: 'Effacer',
},
Mask: {
name: 'Masques',
},
Expand All @@ -110,6 +113,9 @@ const frFR = mergeLocale(base, {
canRelease: 'Libérez instantanément rafraîchir',
complete: 'Rafraîchir avec succès',
},
TextArea: {
clear: 'Effacer',
},
})

export default frFR
Loading