Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
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
30 changes: 28 additions & 2 deletions src/components/text-area/demos/demo1.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from 'react'
import { TextArea } from 'antd-mobile'
import { TextArea, Toast } from 'antd-mobile'
import { DeleteOutline } from 'antd-mobile-icons'
import { DemoBlock } from 'demos'
import React, { useState } from 'react'

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

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

<DemoBlock title='带清除按钮(自定义图标和回调)'>
<TextArea
placeholder='请输入内容'
defaultValue='试试点击清除'
clearable={{
clearIcon: <DeleteOutline />,
onClear: () => {
Toast.show('已清除')
},
}}
/>
</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
17 changes: 16 additions & 1 deletion src/components/text-area/index.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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 feature, supports passing an object for detailed configuration | `boolean \| TextAreaClearableConfig` | `false` |
| defaultValue | Input value by default | `string` | - |
| id | `id` of `textarea` element, often used in conjunction with `label` | `string` | - |
| maxLength | Maximum number of characters | `number` | - |
Expand All @@ -26,7 +27,21 @@ Long text input that requires wrapping.
| 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`
#### TextAreaClearableConfig

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| clearIcon | Custom clear icon | `ReactNode` | `<CloseCircleFill />` |
| onClear | Triggered after clicking the clear button | `() => void` | - |

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

#### Why is there no onlyShowClearWhenFocus property?

The clearable feature of the TextArea component has already implemented similar behavior:

- In non-autoSize scenario, the clear icon only shows when focused and the value is not empty;
- In autoSize scenario, to prevent input area width changes caused by the appearance/disappearance of the clear icon which could trigger height jitters, the space for the clear icon is always reserved.

### CSS Variables

Expand Down
6 changes: 5 additions & 1 deletion src/components/text-area/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import './text-area.less'
import { TextArea } from './text-area'

export type { TextAreaProps, TextAreaRef } from './text-area'
export type {
TextAreaClearableConfig,
TextAreaProps,
TextAreaRef,
} from './text-area'

export default TextArea
17 changes: 16 additions & 1 deletion src/components/text-area/index.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| autoSize | 自适应内容高度 | `boolean \| { minRows?: number, maxRows?: number }` | `false` |
| clearable | 是否启用清除功能,支持传入对象进行详细配置 | `boolean \| TextAreaClearableConfig` | `false` |
| defaultValue | 默认值 | `string` | - |
| id | `textarea` 元素的 `id`,常用来配合 `label` 使用 | `string` | - |
| maxLength | 最大字符数 | `number` | - |
Expand All @@ -26,7 +27,21 @@
| showCount | 显示字数,支持自定义渲染 | `boolean \| ((length: number, maxLength?: number) => ReactNode)` | `false` |
| value | 输入值 | `string` | - |

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

| 属性 | 说明 | 类型 | 默认值 |
| --------- | ------------------ | ------------ | --------------------- |
| clearIcon | 自定义清除图标 | `ReactNode` | `<CloseCircleFill />` |
| onClear | 点击清除按钮后触发 | `() => void` | - |

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

#### 为什么没有 onlyShowClearWhenFocus 属性?

TextArea 组件的 clearable 功能已实现类似行为:

- 在非 autoSize 场景下,输入框有内容且聚焦时,清除图标才会显示;
- 在 autoSize 场景下,为了防止清除图标出现/消失导致输入区域宽度变化进而引发高度抖动,会始终预留清除图标的空间。

### CSS 变量

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ exports[`TextArea should works with \`value={null}\` 1`] = `
<div
class="adm-text-area"
>
<textarea
class="adm-text-area-element"
rows="2"
/>
<div
class="adm-text-area-input-wrapper"
>
<div
class="adm-text-area-textarea-wrapper"
>
<textarea
class="adm-text-area-element"
rows="2"
/>
</div>
</div>
</div>
</div>
`;
198 changes: 197 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,8 @@
import React, { createRef } from 'react'
import { render, fireEvent, act } from 'testing'
import { act, fireEvent, render, screen } from 'testing'
import TextArea, { TextAreaRef } from '..'
import * as validate from '../../../utils/validate'
import ConfigProvider from '../../config-provider'

const classPrefix = 'adm-text-area'
const lineHeight = 25
Expand Down Expand Up @@ -165,4 +167,198 @@ 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

// For non-autoSize, clear button only shows when focused
let clearBtn = container.querySelector(`.${classPrefix}-clear`)
expect(clearBtn).not.toBeInTheDocument()

// Focus the textarea to show clear button
fireEvent.focus(textarea)
clearBtn = container.querySelector(`.${classPrefix}-clear`)
expect(clearBtn).toBeInTheDocument()

fireEvent.click(clearBtn as HTMLElement)
expect(textarea.value).toBe('')
})

test('should works with composition on iOS', async () => {
const spy = jest.spyOn(validate, 'isIOS').mockReturnValue(true)
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()
spy.mockRestore()
})

test('should not blur on non-iOS when click clear button', async () => {
const spy = jest.spyOn(validate, 'isIOS').mockReturnValue(false)
const { container } = render(
<TextArea clearable defaultValue={'testValue'} />
)
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
act(() => {
textarea.focus()
})
fireEvent.compositionStart(textarea)
expect(textarea).toHaveFocus()

const clearBtn = container.querySelector(
`.${classPrefix}-clear`
) as HTMLElement
fireEvent.click(clearBtn)
// On non-iOS, the textarea should keep focus even during composition
expect(textarea).toHaveFocus()
expect(textarea.value).toBe('')
spy.mockRestore()
})

test('onClear callback', () => {
const onClear = jest.fn()
const { container } = render(
<TextArea clearable={{ onClear }} defaultValue={'testValue'} />
)
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 autoSize />
)
expect(baseElement.querySelector('.antd-mobile-icon')).toBeTruthy()
})

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

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

it('props override context', () => {
render(
<ConfigProvider textArea={{ clearIcon: 'little' }}>
<TextArea
value='foobar'
clearable={{ clearIcon: 'bamboo' }}
autoSize
/>
</ConfigProvider>
)
fireEvent.focus(screen.getByRole('textbox'))
expect(screen.getByText('bamboo')).toBeVisible()
})
})

describe('clearable with focus behavior', () => {
test('non-autoSize: clear button only shows when focused and has value', () => {
const { container, rerender } = render(
<TextArea clearable value='test' />
)
let clearBtn = container.querySelector(`.${classPrefix}-clear`)
// Initially not focused, clear button should not be visible
expect(clearBtn).not.toBeInTheDocument()

// Focus but no value
rerender(<TextArea clearable value='' />)
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
fireEvent.focus(textarea)
clearBtn = container.querySelector(`.${classPrefix}-clear`)
expect(clearBtn).not.toBeInTheDocument()

// Focus and has value
rerender(<TextArea clearable value='test' />)
fireEvent.focus(textarea)
clearBtn = container.querySelector(`.${classPrefix}-clear`)
expect(clearBtn).toBeInTheDocument()

// Blur after focus
fireEvent.blur(textarea)
clearBtn = container.querySelector(`.${classPrefix}-clear`)
expect(clearBtn).not.toBeInTheDocument()
})

test('autoSize: clear container exists but hidden when not focused', () => {
const { container } = render(<TextArea clearable value='test' autoSize />)
let clearBtn = container.querySelector(`.${classPrefix}-clear`)
// Initially not focused, clear container should exist but be hidden
expect(clearBtn).toBeInTheDocument()
expect(clearBtn).toHaveClass(`${classPrefix}-clear-hidden`)
expect(clearBtn).toHaveAttribute('aria-hidden', 'true')

// Focus and has value - clear should be visible
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
fireEvent.focus(textarea)
clearBtn = container.querySelector(`.${classPrefix}-clear`)
expect(clearBtn).toBeInTheDocument()
expect(clearBtn).not.toHaveClass(`${classPrefix}-clear-hidden`)
expect(clearBtn).not.toHaveAttribute('aria-hidden', 'true')

// Blur after focus - clear should be hidden again
fireEvent.blur(textarea)
clearBtn = container.querySelector(`.${classPrefix}-clear`)
expect(clearBtn).toBeInTheDocument()
expect(clearBtn).toHaveClass(`${classPrefix}-clear-hidden`)
expect(clearBtn).toHaveAttribute('aria-hidden', 'true')
})

test('disabled or readOnly should not show clear', () => {
const { container } = render(<TextArea clearable value='test' disabled />)
const clearBtn = container.querySelector(`.${classPrefix}-clear`)
expect(clearBtn).not.toBeInTheDocument()

// Test readOnly
const { container: container2 } = render(
<TextArea clearable value='test' readOnly />
)
const clearBtn2 = container2.querySelector(`.${classPrefix}-clear`)
expect(clearBtn2).not.toBeInTheDocument()
})
})
})
Loading
Loading