Browse Source

feat: Input focus support cursor (#28602)

* feat: Input focus support cursor

* docs: clean up

* test: Fix lint
pull/28613/head
二货机器人 4 years ago
committed by GitHub
parent
commit
6ae70059c9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      components/input/Input.tsx
  2. 26
      components/input/TextArea.tsx
  3. 98
      components/input/__tests__/__snapshots__/demo.test.js.snap
  4. 58
      components/input/__tests__/focus.test.tsx
  5. 84
      components/input/demo/focus.md
  6. 7
      components/input/index.en-US.md
  7. 7
      components/input/index.zh-CN.md

36
components/input/Input.tsx

@ -11,6 +11,10 @@ import { ConfigConsumer, ConfigConsumerProps, DirectionType } from '../config-pr
import SizeContext, { SizeType } from '../config-provider/SizeContext';
import devWarning from '../_util/devWarning';
export interface InputFocusOptions extends FocusOptions {
cursor?: 'start' | 'end' | 'all';
}
export interface InputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix' | 'type'> {
prefixCls?: string;
@ -99,6 +103,34 @@ export function getInputClassName(
});
}
export function triggerFocus(
element?: HTMLInputElement | HTMLTextAreaElement,
option?: InputFocusOptions,
) {
if (!element) return;
element.focus(option);
// Selection content
const { cursor } = option || {};
if (cursor) {
const len = element.value.length;
switch (cursor) {
case 'start':
element.setSelectionRange(0, 0);
break;
case 'end':
element.setSelectionRange(len, len);
break;
default:
element.setSelectionRange(0, len);
}
}
}
export interface InputState {
value: any;
focused: boolean;
@ -171,8 +203,8 @@ class Input extends React.Component<InputProps, InputState> {
}
}
focus = () => {
this.input.focus();
focus = (option?: InputFocusOptions) => {
triggerFocus(this.input, option);
};
blur() {

26
components/input/TextArea.tsx

@ -1,12 +1,12 @@
import * as React from 'react';
import RcTextArea, { TextAreaProps as RcTextAreaProps } from 'rc-textarea';
import ResizableTextArea from 'rc-textarea/lib/ResizableTextArea';
import omit from 'omit.js';
import classNames from 'classnames';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import { composeRef } from 'rc-util/lib/ref';
import ClearableLabeledInput from './ClearableLabeledInput';
import { ConfigContext } from '../config-provider';
import { fixControlledValue, resolveOnChange } from './Input';
import { fixControlledValue, resolveOnChange, triggerFocus, InputFocusOptions } from './Input';
import SizeContext, { SizeType } from '../config-provider/SizeContext';
interface ShowCountProps {
@ -21,8 +21,10 @@ export interface TextAreaProps extends RcTextAreaProps {
size?: SizeType;
}
export interface TextAreaRef extends HTMLTextAreaElement {
resizableTextArea: any;
export interface TextAreaRef {
focus: (options?: InputFocusOptions) => void;
blur: () => void;
resizableTextArea?: ResizableTextArea;
}
const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
@ -42,7 +44,7 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
const { getPrefixCls, direction } = React.useContext(ConfigContext);
const size = React.useContext(SizeContext);
const innerRef = React.useRef<TextAreaRef>();
const innerRef = React.useRef<RcTextArea>();
const clearableInputRef = React.useRef<ClearableLabeledInput>(null);
const [value, setValue] = useMergedState(props.defaultValue, {
@ -67,18 +69,26 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
handleSetValue(e.target.value);
resolveOnChange(innerRef.current!, e, props.onChange);
resolveOnChange(innerRef.current as any, e, props.onChange);
};
const handleReset = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
handleSetValue('', () => {
innerRef.current?.focus();
});
resolveOnChange(innerRef.current!, e, props.onChange);
resolveOnChange(innerRef.current as any, e, props.onChange);
};
const prefixCls = getPrefixCls('input', customizePrefixCls);
React.useImperativeHandle(ref, () => ({
resizableTextArea: innerRef.current?.resizableTextArea,
focus: (option?: InputFocusOptions) => {
triggerFocus(innerRef.current?.resizableTextArea?.textArea, option);
},
blur: () => innerRef.current?.blur(),
}));
const textArea = (
<RcTextArea
{...omit(props, ['allowClear'])}
@ -92,7 +102,7 @@ const TextArea = React.forwardRef<TextAreaRef, TextAreaProps>(
style={showCount ? null : style}
prefixCls={prefixCls}
onChange={handleChange}
ref={composeRef(ref, innerRef)}
ref={innerRef}
/>
);

98
components/input/__tests__/__snapshots__/demo.test.js.snap

@ -1184,6 +1184,104 @@ exports[`renders ./components/input/demo/borderless-debug.md correctly 1`] = `
</div>
`;
exports[`renders ./components/input/demo/focus.md correctly 1`] = `
<div
class="ant-space ant-space-vertical"
style="width:100%"
>
<div
class="ant-space-item"
style="margin-bottom:8px"
>
<div
class="ant-space ant-space-horizontal ant-space-align-center"
>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn"
type="button"
>
<span>
Focus at first
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn"
type="button"
>
<span>
Focus at last
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn"
type="button"
>
<span>
Focus to select all
</span>
</button>
</div>
<div
class="ant-space-item"
style="margin-right:8px"
>
<button
class="ant-btn"
type="button"
>
<span>
Focus prevent scroll
</span>
</button>
</div>
<div
class="ant-space-item"
>
<button
aria-checked="true"
class="ant-switch ant-switch-checked"
role="switch"
type="button"
>
<div
class="ant-switch-handle"
/>
<span
class="ant-switch-inner"
>
Input
</span>
</button>
</div>
</div>
</div>
<div
class="ant-space-item"
>
<input
class="ant-input"
style="width:100%"
type="text"
value="Ant Design love you!"
/>
</div>
</div>
`;
exports[`renders ./components/input/demo/group.md correctly 1`] = `
<div
class="site-input-group-wrapper"

58
components/input/__tests__/focus.test.tsx

@ -0,0 +1,58 @@
import React from 'react';
import { mount } from 'enzyme';
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
import Input from '..';
const { TextArea } = Input;
describe('Input.Focus', () => {
let inputSpy: ReturnType<typeof spyElementPrototypes>;
let textareaSpy: ReturnType<typeof spyElementPrototypes>;
let focus: ReturnType<typeof jest.fn>;
let setSelectionRange: ReturnType<typeof jest.fn>;
beforeEach(() => {
focus = jest.fn();
setSelectionRange = jest.fn();
inputSpy = spyElementPrototypes(HTMLInputElement, {
focus,
setSelectionRange,
});
textareaSpy = spyElementPrototypes(HTMLTextAreaElement, {
focus,
setSelectionRange,
});
});
afterEach(() => {
inputSpy.mockRestore();
textareaSpy.mockRestore();
});
it('start', () => {
const ref = React.createRef<Input>();
mount(<Input ref={ref} defaultValue="light" />);
ref.current!.focus({ cursor: 'start' });
expect(focus).toHaveBeenCalled();
expect(setSelectionRange).toHaveBeenCalledWith(expect.anything(), 0, 0);
});
it('end', () => {
const ref = React.createRef<Input>();
mount(<Input ref={ref} defaultValue="light" />);
ref.current!.focus({ cursor: 'end' });
expect(focus).toHaveBeenCalled();
expect(setSelectionRange).toHaveBeenCalledWith(expect.anything(), 5, 5);
});
it('all', () => {
const ref = React.createRef<any>();
mount(<TextArea ref={ref} defaultValue="light" />);
ref.current!.focus({ cursor: 'all' });
expect(focus).toHaveBeenCalled();
expect(setSelectionRange).toHaveBeenCalledWith(expect.anything(), 0, 5);
});
});

84
components/input/demo/focus.md

@ -0,0 +1,84 @@
---
order: 21
title:
zh-CN: 聚焦
en-US: Focus
---
## zh-CN
聚焦额外配置属性。
## en-US
Focus with additional option.
```tsx
import { Input, Space, Button, Switch } from 'antd';
const Demo = () => {
const inputRef = React.useRef<any>(null);
const [input, setInput] = React.useState(true);
const sharedProps = {
style: { width: '100%' },
defaultValue: 'Ant Design love you!',
ref: inputRef,
};
return (
<Space direction="vertical" style={{ width: '100%' }}>
<Space>
<Button
onClick={() => {
inputRef.current!.focus({
cursor: 'start',
});
}}
>
Focus at first
</Button>
<Button
onClick={() => {
inputRef.current!.focus({
cursor: 'end',
});
}}
>
Focus at last
</Button>
<Button
onClick={() => {
inputRef.current!.focus({
cursor: 'all',
});
}}
>
Focus to select all
</Button>
<Button
onClick={() => {
inputRef.current!.focus({
preventScroll: true,
});
}}
>
Focus prevent scroll
</Button>
<Switch
checked={input}
checkedChildren="Input"
unCheckedChildren="TextArea"
onChange={() => {
setInput(!input);
}}
/>
</Space>
{input ? <Input {...sharedProps} /> : <Input.TextArea {...sharedProps} />}
</Space>
);
};
ReactDOM.render(<Demo />, mountNode);
```

7
components/input/index.en-US.md

@ -85,6 +85,13 @@ Supports all props of `Input`.
| iconRender | Custom toggle button | (visible) => ReactNode | (visible) => (visible ? &lt;EyeOutlined /> : &lt;EyeInvisibleOutlined />) | 4.3.0 |
| visibilityToggle | Whether show toggle button | boolean | true | |
#### Input Methods
| Name | Description | Parameters | Version |
| --- | --- | --- | --- |
| blur | Remove focus | - | |
| focus | Get focus | (option?: { preventScroll?: boolean, cursor?: 'start' \| 'end' \| 'all' }) | option - 4.10.0 |
## FAQ
### Why Input lose focus when change `prefix/suffix`

7
components/input/index.zh-CN.md

@ -87,6 +87,13 @@ Input 的其他属性和 React 自带的 [input](https://facebook.github.io/reac
| iconRender | 自定义切换按钮 | (visible) => ReactNode | (visible) => (visible ? &lt;EyeOutlined /> : &lt;EyeInvisibleOutlined />) | 4.3.0 |
| visibilityToggle | 是否显示切换按钮 | boolean | true | |
#### Input Methods
| 名称 | 说明 | 参数 | 版本 |
| --- | --- | --- | --- |
| blur | 取消焦点 | - | |
| focus | 获取焦点 | (option?: { preventScroll?: boolean, cursor?: 'start' \| 'end' \| 'all' }) | option - 4.10.0 |
## FAQ
### 为什么我动态改变 `prefix/suffix` 时,Input 会失去焦点?

Loading…
Cancel
Save