Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
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
12 changes: 11 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,26 @@ 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`

#### Why there is 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;
- 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.
Comment thread
viko16 marked this conversation as resolved.
Outdated

### CSS Variables

Expand Down
12 changes: 11 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,26 @@
| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| 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`

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

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

- 在非 autoSize 场景下,清除图标仅在聚焦时显示;
- 在 autoSize 场景下,为了防止清除图标出现/消失导致输入区域宽度变化进而引发高度抖动,会始终预留清除图标的空间。
Comment thread
viko16 marked this conversation as resolved.
Outdated

### 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>
`;
192 changes: 191 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,192 @@ 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 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 autoSize />
)
expect(baseElement.querySelector('.antd-mobile-icon')).toBeTruthy()
})

it('props', () => {
render(<TextArea value='foobar' clearable autoSize clearIcon='bamboo' />)
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 autoSize clearIcon='bamboo' />
</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()
})
})
})
33 changes: 33 additions & 0 deletions src/components/text-area/text-area.less
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@
max-height: 100%;
}

.adm-text-area-input-wrapper {
display: flex;
align-items: flex-start;
}

.adm-text-area-textarea-wrapper {
position: relative;
flex: auto;
display: flex;
width: 100%;
max-width: 100%;
}

.adm-text-area-element {
font-family: var(--adm-font-family);
resize: none;
Expand Down Expand Up @@ -75,3 +88,23 @@
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);
}

&-hidden {
visibility: hidden;
pointer-events: none;
}
}
Loading