mirror of
https://github.com/ant-design/ant-design.git
synced 2024-11-27 20:49:53 +08:00
feat: Add Mentions component (#16532)
* init * first demo * support empty * add loading support * add form sample * update form sample * omit value & defaultValue * add 2 rest demo * placement support * update docs * fix test * update docs * add test case * fix lint * follow textarea style * update docs style
This commit is contained in:
parent
1635a1b018
commit
ecb2eb6ede
@ -34,6 +34,7 @@ Array [
|
||||
"LocaleProvider",
|
||||
"message",
|
||||
"Menu",
|
||||
"Mentions",
|
||||
"Modal",
|
||||
"Statistic",
|
||||
"notification",
|
||||
|
@ -16,6 +16,7 @@ const renderEmpty = (componentName?: string): React.ReactNode => (
|
||||
case 'TreeSelect':
|
||||
case 'Cascader':
|
||||
case 'Transfer':
|
||||
case 'Mentions':
|
||||
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} className={`${prefix}-small`} />;
|
||||
default:
|
||||
return <Empty />;
|
||||
|
@ -216,6 +216,7 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
.@{ant-prefix}-mentions,
|
||||
textarea.@{ant-prefix}-input {
|
||||
height: auto;
|
||||
margin-bottom: 4px;
|
||||
|
@ -79,6 +79,8 @@ export { default as message } from './message';
|
||||
|
||||
export { default as Menu } from './menu';
|
||||
|
||||
export { default as Mentions } from './mentions';
|
||||
|
||||
export { default as Modal } from './modal';
|
||||
|
||||
export { default as Statistic } from './statistic';
|
||||
|
@ -1,14 +1,18 @@
|
||||
---
|
||||
category: Components
|
||||
type: Data Entry
|
||||
title: Mention
|
||||
type: Deprecated
|
||||
title: Mention (Deprecated)
|
||||
---
|
||||
|
||||
Mention component.
|
||||
Mention component. Deprecated, please use [Mentions](/components/mentions) instead.
|
||||
|
||||
## When To Use
|
||||
## Why deprecated?
|
||||
|
||||
When need to mention someone or something.
|
||||
<div class="ant-alert ant-alert-error ant-alert-no-icon">
|
||||
Mention use
|
||||
<a href="https://www.npmjs.com/package/draft-js" target="_blank" rel="noopener noreferrer">Draft.js</a>
|
||||
to measure tips position, which use nearly 11.6% package size. We hope to reduce bundle size by using lightweight solution to handle this.
|
||||
</div>
|
||||
|
||||
## API
|
||||
|
||||
|
@ -3,6 +3,7 @@ import RcMention, { Nav, toString, toEditorState, getMentions } from 'rc-editor-
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
import classNames from 'classnames';
|
||||
import Icon from '../icon';
|
||||
import warning from '../_util/warning';
|
||||
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
|
||||
|
||||
export type MentionPlacement = 'top' | 'bottom';
|
||||
@ -56,6 +57,12 @@ class Mention extends React.Component<MentionProps, MentionState> {
|
||||
filteredSuggestions: props.defaultSuggestions,
|
||||
focus: false,
|
||||
};
|
||||
|
||||
warning(
|
||||
false,
|
||||
'Mention',
|
||||
'Mention component is deprecated. Please use Mentions component instead.',
|
||||
);
|
||||
}
|
||||
|
||||
onSearchChange = (value: string, prefix: string) => {
|
||||
|
@ -1,15 +1,19 @@
|
||||
---
|
||||
category: Components
|
||||
subtitle: 提及
|
||||
type: 数据录入
|
||||
type: 废弃
|
||||
subtitle: 提及(废弃)
|
||||
title: Mention
|
||||
---
|
||||
|
||||
提及组件。
|
||||
提及组件。已废弃,请使用 [Mentions](/components/mentions) 代替。
|
||||
|
||||
## 何时使用
|
||||
## 为何废弃?
|
||||
|
||||
用于在输入中提及某人或某事,常用于发布、聊天或评论功能。
|
||||
<div class="ant-alert ant-alert-error ant-alert-no-icon">
|
||||
Mention 组件使用了
|
||||
<a href="https://www.npmjs.com/package/draft-js" target="_blank" rel="noopener noreferrer">Draft.js</a>
|
||||
进行提示定位,占用了约 11.6% 的包大小。因而我们决定使用更轻量级的解决方案以便于在未来降低整个包的大小。
|
||||
</div>
|
||||
|
||||
## API
|
||||
|
||||
|
154
components/mentions/__tests__/__snapshots__/demo.test.js.snap
Normal file
154
components/mentions/__tests__/__snapshots__/demo.test.js.snap
Normal file
@ -0,0 +1,154 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders ./components/mentions/demo/async.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-mentions"
|
||||
style="width:100%"
|
||||
>
|
||||
<textarea
|
||||
rows="1"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/mentions/demo/basic.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-mentions"
|
||||
style="width:100%"
|
||||
>
|
||||
<textarea
|
||||
rows="1"
|
||||
>
|
||||
@afc163
|
||||
</textarea>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/mentions/demo/form.md correctly 1`] = `
|
||||
<form
|
||||
class="ant-form ant-form-horizontal"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-col-6 ant-form-item-label"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
for="mention"
|
||||
title="Top coders"
|
||||
>
|
||||
Top coders
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-col-16 ant-form-item-control-wrapper"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control"
|
||||
>
|
||||
<span
|
||||
class="ant-form-item-children"
|
||||
>
|
||||
<div
|
||||
class="ant-mentions"
|
||||
>
|
||||
<textarea
|
||||
data-__field="[object Object]"
|
||||
data-__meta="[object Object]"
|
||||
id="mention"
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-row ant-form-item"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-col-14 ant-col-offset-6 ant-form-item-control-wrapper"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control"
|
||||
>
|
||||
<span
|
||||
class="ant-form-item-children"
|
||||
>
|
||||
<button
|
||||
class="ant-btn ant-btn-primary"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Submit
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="ant-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Reset
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/mentions/demo/placement.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-mentions"
|
||||
style="width:100%"
|
||||
>
|
||||
<textarea
|
||||
rows="1"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/mentions/demo/prefix.md correctly 1`] = `
|
||||
<div
|
||||
class="ant-mentions"
|
||||
style="width:100%"
|
||||
>
|
||||
<textarea
|
||||
placeholder="input @ to mention people, # to mention tag"
|
||||
rows="1"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders ./components/mentions/demo/readonly.md correctly 1`] = `
|
||||
<div>
|
||||
<div
|
||||
style="margin-bottom:10px"
|
||||
>
|
||||
<div
|
||||
class="ant-mentions ant-mentions-disabled"
|
||||
style="width:100%"
|
||||
>
|
||||
<textarea
|
||||
disabled=""
|
||||
placeholder="this is disabled Mentions"
|
||||
rows="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-mentions"
|
||||
style="width:100%"
|
||||
>
|
||||
<textarea
|
||||
placeholder="this is readOnly Mentions"
|
||||
readonly=""
|
||||
rows="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
3
components/mentions/__tests__/demo.test.js
Normal file
3
components/mentions/__tests__/demo.test.js
Normal file
@ -0,0 +1,3 @@
|
||||
import demoTest from '../../../tests/shared/demoTest';
|
||||
|
||||
demoTest('mentions');
|
83
components/mentions/__tests__/index.test.js
Normal file
83
components/mentions/__tests__/index.test.js
Normal file
@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import Mentions from '..';
|
||||
|
||||
const { getMentions } = Mentions;
|
||||
|
||||
function simulateInput(wrapper, text = '', keyEvent) {
|
||||
const lastChar = text[text.length - 1];
|
||||
const myKeyEvent = keyEvent || {
|
||||
which: lastChar.charCodeAt(0),
|
||||
key: lastChar,
|
||||
};
|
||||
|
||||
wrapper.find('textarea').simulate('keyDown', myKeyEvent);
|
||||
|
||||
const textareaInstance = wrapper.find('textarea').instance();
|
||||
textareaInstance.value = text;
|
||||
textareaInstance.selectionStart = text.length;
|
||||
textareaInstance.selectionStart = text.length;
|
||||
|
||||
if (!keyEvent) {
|
||||
wrapper.find('textarea').simulate('change', {
|
||||
target: { value: text },
|
||||
});
|
||||
}
|
||||
|
||||
wrapper.find('textarea').simulate('keyUp', myKeyEvent);
|
||||
wrapper.update();
|
||||
}
|
||||
|
||||
describe('Mentions', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('getMentions', () => {
|
||||
const mentions = getMentions('@light #bamboo cat', { prefix: ['@', '#'] });
|
||||
expect(mentions).toEqual([
|
||||
{
|
||||
prefix: '@',
|
||||
value: 'light',
|
||||
},
|
||||
{
|
||||
prefix: '#',
|
||||
value: 'bamboo',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('focus', () => {
|
||||
const onFocus = jest.fn();
|
||||
const onBlur = jest.fn();
|
||||
|
||||
const wrapper = mount(<Mentions onFocus={onFocus} onBlur={onBlur} />);
|
||||
wrapper.find('textarea').simulate('focus');
|
||||
expect(wrapper.find('.ant-mentions').hasClass('ant-mentions-focused')).toBeTruthy();
|
||||
expect(onFocus).toHaveBeenCalled();
|
||||
|
||||
wrapper.find('textarea').simulate('blur');
|
||||
jest.runAllTimers();
|
||||
wrapper.update();
|
||||
expect(wrapper.find('.ant-mentions').hasClass('ant-mentions-focused')).toBeFalsy();
|
||||
expect(onBlur).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('loading', () => {
|
||||
const wrapper = mount(<Mentions loading />);
|
||||
simulateInput(wrapper, '@');
|
||||
expect(wrapper.find('.ant-mentions-dropdown-menu-item').length).toBe(1);
|
||||
expect(wrapper.find('.ant-spin').length).toBeTruthy();
|
||||
});
|
||||
|
||||
it('notFoundContent', () => {
|
||||
const wrapper = mount(<Mentions notFoundContent={<span className="bamboo-light" />} />);
|
||||
simulateInput(wrapper, '@');
|
||||
expect(wrapper.find('.ant-mentions-dropdown-menu-item').length).toBe(1);
|
||||
expect(wrapper.find('.bamboo-light').length).toBeTruthy();
|
||||
});
|
||||
});
|
87
components/mentions/demo/async.md
Normal file
87
components/mentions/demo/async.md
Normal file
@ -0,0 +1,87 @@
|
||||
---
|
||||
order: 1
|
||||
title:
|
||||
zh-CN: 异步加载
|
||||
en-US: Asynchronous loading
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
匹配内容列表为异步返回时。
|
||||
|
||||
## en-US
|
||||
|
||||
async
|
||||
|
||||
```jsx
|
||||
import { Mentions } from 'antd';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
const { Option } = Mentions;
|
||||
|
||||
class AsyncMention extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.loadGithubUsers = debounce(this.loadGithubUsers, 800);
|
||||
}
|
||||
|
||||
state = {
|
||||
search: '',
|
||||
loading: false,
|
||||
users: [],
|
||||
};
|
||||
|
||||
onSearch = search => {
|
||||
this.setState({ search, loading: !!search, users: [] });
|
||||
console.log('Search:', search);
|
||||
this.loadGithubUsers(search);
|
||||
};
|
||||
|
||||
loadGithubUsers(key) {
|
||||
if (!key) {
|
||||
this.setState({
|
||||
users: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`https://api.github.com/search/users?q=${key}`)
|
||||
.then(res => res.json())
|
||||
.then(({ items = [] }) => {
|
||||
const { search } = this.state;
|
||||
if (search !== key) return;
|
||||
|
||||
this.setState({
|
||||
users: items.slice(0, 10),
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { users, loading } = this.state;
|
||||
|
||||
return (
|
||||
<Mentions style={{ width: '100%' }} loading={loading} onSearch={this.onSearch}>
|
||||
{users.map(({ login, avatar_url: avatar }) => (
|
||||
<Option key={login} value={login} className="antd-demo-dynamic-option">
|
||||
<img src={avatar} alt={login} />
|
||||
<span>{login}</span>
|
||||
</Option>
|
||||
))}
|
||||
</Mentions>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<AsyncMention />, mountNode);
|
||||
```
|
||||
|
||||
<style>
|
||||
.antd-demo-dynamic-option img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
42
components/mentions/demo/basic.md
Normal file
42
components/mentions/demo/basic.md
Normal file
@ -0,0 +1,42 @@
|
||||
---
|
||||
order: 0
|
||||
title:
|
||||
zh-CN: 基本使用
|
||||
en-US: Basic
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
基本使用
|
||||
|
||||
## en-US
|
||||
|
||||
Basic usage.
|
||||
|
||||
```jsx
|
||||
import { Mentions } from 'antd';
|
||||
|
||||
const { Option } = Mentions;
|
||||
|
||||
function onChange(value) {
|
||||
console.log('Change:', value);
|
||||
}
|
||||
|
||||
function onSelect(option) {
|
||||
console.log('select', option);
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Mentions
|
||||
style={{ width: '100%' }}
|
||||
onChange={onChange}
|
||||
onSelect={onSelect}
|
||||
defaultValue="@afc163"
|
||||
>
|
||||
<Option value="afc163">afc163</Option>
|
||||
<Option value="zombieJ">zombieJ</Option>
|
||||
<Option value="yesmeck">yesmeck</Option>
|
||||
</Mentions>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
81
components/mentions/demo/form.md
Normal file
81
components/mentions/demo/form.md
Normal file
@ -0,0 +1,81 @@
|
||||
---
|
||||
order: 2
|
||||
title:
|
||||
zh-CN: 配合 Form 使用
|
||||
en-US: With Form
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
受控模式,例如配合 Form 使用。
|
||||
|
||||
## en-US
|
||||
|
||||
Controlled mode, for example, to work with `Form`.
|
||||
|
||||
```jsx
|
||||
import { Mentions, Form, Button } from 'antd';
|
||||
|
||||
const { Option, getMentions } = Mentions;
|
||||
|
||||
class App extends React.Component {
|
||||
handleReset = e => {
|
||||
e.preventDefault();
|
||||
this.props.form.resetFields();
|
||||
};
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.form.validateFields((errors, values) => {
|
||||
if (errors) {
|
||||
console.log('Errors in the form!!!');
|
||||
return;
|
||||
}
|
||||
console.log('Submit!!!');
|
||||
console.log(values);
|
||||
});
|
||||
};
|
||||
|
||||
checkMention = (rule, value, callback) => {
|
||||
const mentions = getMentions(value);
|
||||
if (mentions.length < 2) {
|
||||
callback(new Error('More than one must be selected!'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
form: { getFieldDecorator },
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Form layout="horizontal">
|
||||
<Form.Item label="Top coders" labelCol={{ span: 6 }} wrapperCol={{ span: 16 }}>
|
||||
{getFieldDecorator('mention', {
|
||||
rules: [{ validator: this.checkMention }],
|
||||
})(
|
||||
<Mentions rows="3">
|
||||
<Option value="afc163">afc163</Option>
|
||||
<Option value="zombieJ">zombieJ</Option>
|
||||
<Option value="yesmeck">yesmeck</Option>
|
||||
</Mentions>,
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{ span: 14, offset: 6 }}>
|
||||
<Button type="primary" onClick={this.handleSubmit}>
|
||||
Submit
|
||||
</Button>
|
||||
|
||||
<Button onClick={this.handleReset}>Reset</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const FormDemo = Form.create()(App);
|
||||
|
||||
ReactDOM.render(<FormDemo />, mountNode);
|
||||
```
|
29
components/mentions/demo/placement.md
Normal file
29
components/mentions/demo/placement.md
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
order: 5
|
||||
title:
|
||||
zh-CN: 向上展开
|
||||
en-US: Placement
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
向上展开建议。
|
||||
|
||||
## en-US
|
||||
|
||||
Change the suggestions placement.
|
||||
|
||||
```jsx
|
||||
import { Mentions } from 'antd';
|
||||
|
||||
const { Option } = Mentions;
|
||||
|
||||
ReactDOM.render(
|
||||
<Mentions style={{ width: '100%' }} placement="top">
|
||||
<Option value="afc163">afc163</Option>
|
||||
<Option value="zombieJ">zombieJ</Option>
|
||||
<Option value="yesmeck">yesmeck</Option>
|
||||
</Mentions>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
56
components/mentions/demo/prefix.md
Normal file
56
components/mentions/demo/prefix.md
Normal file
@ -0,0 +1,56 @@
|
||||
---
|
||||
order: 3
|
||||
title:
|
||||
zh-CN: 自定义触发字符
|
||||
en-US: Customize Trigger Token
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
通过 `prefix` 属性自定义触发字符。默认为 `@`, 可以定义为数组。
|
||||
|
||||
## en-US
|
||||
|
||||
Customize Trigger Token by `prefix` props. Default to `@`, `Array<string>` also supported.
|
||||
|
||||
```jsx
|
||||
import { Mentions } from 'antd';
|
||||
|
||||
const { Option } = Mentions;
|
||||
|
||||
const MOCK_DATA = {
|
||||
'@': ['afc163', 'zombiej', 'yesmeck'],
|
||||
'#': ['1.0', '2.0', '3.0'],
|
||||
};
|
||||
|
||||
class App extends React.Component {
|
||||
state = {
|
||||
prefix: '@',
|
||||
};
|
||||
|
||||
onSearch = (_, prefix) => {
|
||||
this.setState({ prefix });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { prefix } = this.state;
|
||||
|
||||
return (
|
||||
<Mentions
|
||||
style={{ width: '100%' }}
|
||||
placeholder="input @ to mention people, # to mention tag"
|
||||
prefix={['@', '#']}
|
||||
onSearch={this.onSearch}
|
||||
>
|
||||
{(MOCK_DATA[prefix] || []).map(value => (
|
||||
<Option key={value} value={value}>
|
||||
{value}
|
||||
</Option>
|
||||
))}
|
||||
</Mentions>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, mountNode);
|
||||
```
|
45
components/mentions/demo/readonly.md
Normal file
45
components/mentions/demo/readonly.md
Normal file
@ -0,0 +1,45 @@
|
||||
---
|
||||
order: 4
|
||||
title:
|
||||
zh-CN: 无效或只读
|
||||
en-US: disabled or readOnly
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
通过 `disabled` 属性设置是否生效。通过 `readOnly` 属性设置是否只读。
|
||||
|
||||
## en-US
|
||||
|
||||
Configurate `disabled` and `readOnly`.
|
||||
|
||||
```jsx
|
||||
import { Mentions } from 'antd';
|
||||
|
||||
const { Option } = Mentions;
|
||||
|
||||
function getOptions() {
|
||||
return ['afc163', 'zombiej', 'yesmeck'].map(value => (
|
||||
<Option key={value} value={value}>
|
||||
{value}
|
||||
</Option>
|
||||
));
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<Mentions style={{ width: '100%' }} placeholder="this is disabled Mentions" disabled>
|
||||
{getOptions()}
|
||||
</Mentions>
|
||||
</div>
|
||||
<Mentions style={{ width: '100%' }} placeholder="this is readOnly Mentions" readOnly>
|
||||
{getOptions()}
|
||||
</Mentions>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<App />, mountNode);
|
||||
```
|
54
components/mentions/index.en-US.md
Normal file
54
components/mentions/index.en-US.md
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
category: Components
|
||||
type: Data Entry
|
||||
title: Mentions
|
||||
---
|
||||
|
||||
Mention component.
|
||||
|
||||
> Mention component is deprecated. Please click [here](/components/mention) to view old document.
|
||||
|
||||
## When To Use
|
||||
|
||||
When need to mention someone or something.
|
||||
|
||||
## API
|
||||
|
||||
```jsx
|
||||
<Mentions onChange={onChange}>
|
||||
<Mentions.Option value="sample">Sample</Mentions.Option>
|
||||
</Mentions>
|
||||
```
|
||||
|
||||
### Mention
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| autoFocus | Auto get focus when component mounted | boolean | `false` |
|
||||
| defaultValue | Default value | string | - |
|
||||
| filterOption | Customize filter option logic | false \| (input: string, option: OptionProps) => boolean | - |
|
||||
| notFoundContent | Set mentions content when not match | ReactNode | 'Not Found' |
|
||||
| placement | Set popup placement | 'top' \| 'bottom' | 'bottom' |
|
||||
| prefix | Set trigger prefix keyword | string \| string[] | '@' |
|
||||
| split | Set split string before and after selected mention | string | ' ' |
|
||||
| validateSearch | Customize trigger search logic | (text: string, props: MentionsProps) => void | - |
|
||||
| value | Set value of mentions | string | - |
|
||||
| onChange | Trigger when value changed | (text: string) => void | - |
|
||||
| onSelect | Trigger when user select the option | (option: OptionProps, prefix: string) => void | - |
|
||||
| onSearch | Trigger when prefix hit | (text: string, prefix: string) => void | - |
|
||||
| onFocus | Trigger when mentions get focus | () => void | - |
|
||||
| onBlur | Trigger when mentions lose focus | () => void | - |
|
||||
|
||||
### Mention methods
|
||||
|
||||
| Name | Description |
|
||||
| ------- | ------------ |
|
||||
| blur() | remove focus |
|
||||
| focus() | get focus |
|
||||
|
||||
### Option
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| children | suggestion content | ReactNode | - |
|
||||
| value | value of suggestion, the value will insert into input filed while selected | string | '' |
|
164
components/mentions/index.tsx
Normal file
164
components/mentions/index.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import classNames from 'classnames';
|
||||
import omit from 'omit.js';
|
||||
import * as React from 'react';
|
||||
import { polyfill } from 'react-lifecycles-compat';
|
||||
import RcMentions from 'rc-mentions';
|
||||
import { MentionsProps as RcMentionsProps } from 'rc-mentions/lib/Mentions';
|
||||
import Spin from '../spin';
|
||||
import { ConfigConsumer, ConfigConsumerProps, RenderEmptyHandler } from '../config-provider';
|
||||
|
||||
const { Option } = RcMentions;
|
||||
|
||||
function loadingFilterOption() {
|
||||
return true;
|
||||
}
|
||||
|
||||
export type MentionPlacement = 'top' | 'bottom';
|
||||
|
||||
export interface OptionProps {
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface MentionProps extends RcMentionsProps {
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export interface MentionState {
|
||||
focused: boolean;
|
||||
}
|
||||
|
||||
interface MentionsConfig {
|
||||
prefix?: string | string[];
|
||||
split?: string;
|
||||
}
|
||||
|
||||
interface MentionsEntity {
|
||||
prefix: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
class Mentions extends React.Component<MentionProps, MentionState> {
|
||||
static Option = Option;
|
||||
|
||||
static getMentions = (value: string = '', config?: MentionsConfig): MentionsEntity[] => {
|
||||
const { prefix = '@', split = ' ' } = config || {};
|
||||
const prefixList: string[] = Array.isArray(prefix) ? prefix : [prefix];
|
||||
|
||||
return value
|
||||
.split(split)
|
||||
.map(
|
||||
(str = ''): MentionsEntity | null => {
|
||||
let hitPrefix: string | null = null;
|
||||
|
||||
prefixList.some(prefixStr => {
|
||||
const startStr = str.slice(0, prefixStr.length);
|
||||
if (startStr === prefixStr) {
|
||||
hitPrefix = prefixStr;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hitPrefix !== null) {
|
||||
return {
|
||||
prefix: hitPrefix,
|
||||
value: str.slice(hitPrefix!.length),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
)
|
||||
.filter((entity): entity is MentionsEntity => !!entity && !!entity.value);
|
||||
};
|
||||
|
||||
state = {
|
||||
focused: false,
|
||||
};
|
||||
|
||||
onFocus: React.FocusEventHandler<HTMLTextAreaElement> = (...args) => {
|
||||
const { onFocus } = this.props;
|
||||
if (onFocus) {
|
||||
onFocus(...args);
|
||||
}
|
||||
this.setState({
|
||||
focused: true,
|
||||
});
|
||||
};
|
||||
|
||||
onBlur: React.FocusEventHandler<HTMLTextAreaElement> = (...args) => {
|
||||
const { onBlur } = this.props;
|
||||
if (onBlur) {
|
||||
onBlur(...args);
|
||||
}
|
||||
this.setState({
|
||||
focused: false,
|
||||
});
|
||||
};
|
||||
|
||||
getNotFoundContent(renderEmpty: RenderEmptyHandler) {
|
||||
const { notFoundContent } = this.props;
|
||||
if (notFoundContent !== undefined) {
|
||||
return notFoundContent;
|
||||
}
|
||||
|
||||
return renderEmpty('Select');
|
||||
}
|
||||
|
||||
getOptions = () => {
|
||||
const { children, loading } = this.props;
|
||||
if (loading) {
|
||||
return (
|
||||
<Option value={'ANTD_SEARCHING'} disabled>
|
||||
<Spin size="small" />
|
||||
</Option>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
getFilterOption = () => {
|
||||
const { filterOption, loading } = this.props;
|
||||
if (loading) {
|
||||
return loadingFilterOption;
|
||||
}
|
||||
return filterOption;
|
||||
};
|
||||
|
||||
renderMentions = ({ getPrefixCls, renderEmpty }: ConfigConsumerProps) => {
|
||||
const { focused } = this.state;
|
||||
const { prefixCls: customizePrefixCls, className, disabled, ...restProps } = this.props;
|
||||
const prefixCls = getPrefixCls('mentions', customizePrefixCls);
|
||||
const mentionsProps = omit(restProps, ['loading']);
|
||||
|
||||
const mergedClassName = classNames(className, {
|
||||
[`${prefixCls}-disabled`]: disabled,
|
||||
[`${prefixCls}-focused`]: focused,
|
||||
});
|
||||
|
||||
return (
|
||||
<RcMentions
|
||||
prefixCls={prefixCls}
|
||||
notFoundContent={this.getNotFoundContent(renderEmpty)}
|
||||
className={mergedClassName}
|
||||
disabled={disabled}
|
||||
{...mentionsProps}
|
||||
filterOption={this.getFilterOption()}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
>
|
||||
{this.getOptions()}
|
||||
</RcMentions>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <ConfigConsumer>{this.renderMentions}</ConfigConsumer>;
|
||||
}
|
||||
}
|
||||
|
||||
polyfill(Mentions);
|
||||
|
||||
export default Mentions;
|
64
components/mentions/index.zh-CN.md
Normal file
64
components/mentions/index.zh-CN.md
Normal file
@ -0,0 +1,64 @@
|
||||
---
|
||||
category: Components
|
||||
subtitle: 提及
|
||||
type: 数据录入
|
||||
title: Mentions
|
||||
---
|
||||
|
||||
提及组件。
|
||||
|
||||
> 原 Mention 组件已废弃,原文档请点击[这里](/components/mention)。
|
||||
|
||||
## 何时使用
|
||||
|
||||
用于在输入中提及某人或某事,常用于发布、聊天或评论功能。
|
||||
|
||||
## API
|
||||
|
||||
```jsx
|
||||
<Mention
|
||||
onChange={onChange}
|
||||
suggestions={['afc163', 'benjycui', 'yiminghe', 'jljsj33', 'dqaria', 'RaoHai']}
|
||||
/>
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
```jsx
|
||||
<Mentions onChange={onChange}>
|
||||
<Mentions.Option value="sample">Sample</Mentions.Option>
|
||||
</Mentions>
|
||||
```
|
||||
|
||||
### Mention
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| autoFocus | 自动获得焦点 | boolean | `false` |
|
||||
| defaultValue | 默认值 | string | - |
|
||||
| filterOption | 自定义过滤逻辑 | false \| (input: string, option: OptionProps) => boolean | - |
|
||||
| notFoundContent | 当下拉列表为空时显示的内容 | ReactNode | 'Not Found' |
|
||||
| placement | 弹出层展示位置 | 'top' \| 'bottom' | 'bottom' |
|
||||
| prefix | 设置触发关键字 | string \| string[] | '@' |
|
||||
| split | 设置选中项前后分隔符 | string | ' ' |
|
||||
| validateSearch | 自定义触发验证逻辑 | (text: string, props: MentionsProps) => void | - |
|
||||
| value | 设置值 | string | - |
|
||||
| onChange | 值改变时触发 | (text: string) => void | - |
|
||||
| onSelect | 选择选项时触发 | (option: OptionProps, prefix: string) => void | - |
|
||||
| onSearch | 搜索时触发 | (text: string, prefix: string) => void | - |
|
||||
| onFocus | 获得焦点时触发 | () => void | - |
|
||||
| onBlur | 失去焦点时触发 | () => void | - |
|
||||
|
||||
### Mention 方法
|
||||
|
||||
| 名称 | 描述 |
|
||||
| ------- | -------- |
|
||||
| blur() | 移除焦点 |
|
||||
| focus() | 获取焦点 |
|
||||
|
||||
### Option
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| -------- | -------------- | --------- | ------ |
|
||||
| children | 选项内容 | ReactNode | - |
|
||||
| value | 选择时填充的值 | string | '' |
|
156
components/mentions/style/index.less
Normal file
156
components/mentions/style/index.less
Normal file
@ -0,0 +1,156 @@
|
||||
@import '../../style/themes/index';
|
||||
@import '../../style/mixins/index';
|
||||
@import '../../input/style/mixin';
|
||||
|
||||
@mention-prefix-cls: ~'@{ant-prefix}-mentions';
|
||||
|
||||
.@{mention-prefix-cls} {
|
||||
.reset-component;
|
||||
.input;
|
||||
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: auto;
|
||||
white-space: pre-wrap;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
vertical-align: bottom;
|
||||
|
||||
// =================== Status ===================
|
||||
&-disabled {
|
||||
> textarea {
|
||||
.disabled();
|
||||
}
|
||||
}
|
||||
|
||||
&-focused {
|
||||
.active();
|
||||
}
|
||||
|
||||
// ================= Input Area =================
|
||||
> textarea,
|
||||
&-measure {
|
||||
margin: 0;
|
||||
padding: @input-padding-vertical-base @input-padding-horizontal-base;
|
||||
overflow: inherit;
|
||||
overflow-x: initial;
|
||||
overflow-y: auto;
|
||||
font-weight: inherit;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-style: inherit;
|
||||
font-variant: inherit;
|
||||
font-size-adjust: inherit;
|
||||
font-stretch: inherit;
|
||||
line-height: inherit;
|
||||
direction: inherit;
|
||||
letter-spacing: inherit;
|
||||
white-space: inherit;
|
||||
text-align: inherit;
|
||||
vertical-align: top;
|
||||
word-wrap: break-word;
|
||||
word-break: inherit;
|
||||
tab-size: inherit;
|
||||
}
|
||||
|
||||
> textarea {
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
|
||||
&:read-only {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
&-measure {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// ================== Dropdown ==================
|
||||
&-dropdown {
|
||||
// Ref select dropdown style
|
||||
.reset-component;
|
||||
|
||||
position: absolute;
|
||||
top: -9999px;
|
||||
left: -9999px;
|
||||
z-index: @zindex-dropdown;
|
||||
box-sizing: border-box;
|
||||
font-size: @font-size-base;
|
||||
font-variant: initial;
|
||||
background-color: @component-background;
|
||||
border-radius: @border-radius-base;
|
||||
outline: none;
|
||||
box-shadow: @box-shadow-base;
|
||||
|
||||
&-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-menu {
|
||||
max-height: 250px;
|
||||
margin-bottom: 0;
|
||||
padding-left: 0; // Override default ul/ol
|
||||
overflow: auto;
|
||||
list-style: none;
|
||||
outline: none;
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 5px @control-padding-horizontal;
|
||||
overflow: hidden;
|
||||
color: @text-color;
|
||||
font-weight: normal;
|
||||
line-height: 22px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
min-width: 100px;
|
||||
transition: background 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: @item-hover-bg;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-radius: @border-radius-base @border-radius-base 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 @border-radius-base @border-radius-base;
|
||||
}
|
||||
|
||||
&-disabled {
|
||||
color: @disabled-color;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
color: @disabled-color;
|
||||
background-color: @component-background;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&-selected {
|
||||
color: @text-color;
|
||||
font-weight: @select-item-selected-font-weight;
|
||||
background-color: @background-color-light;
|
||||
}
|
||||
|
||||
&-active {
|
||||
background-color: @item-active-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
components/mentions/style/index.tsx
Normal file
5
components/mentions/style/index.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import './index.less';
|
||||
|
||||
// style dependencies
|
||||
import '../../empty/style';
|
||||
import '../../spin/style';
|
@ -42,6 +42,7 @@
|
||||
"react-dom": ">=16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/create-react-context": "^0.2.4",
|
||||
"@ant-design/icons": "~2.0.0",
|
||||
"@ant-design/icons-react": "~2.0.0",
|
||||
"@types/react-slick": "^0.23.3",
|
||||
@ -49,7 +50,6 @@
|
||||
"babel-runtime": "6.x",
|
||||
"classnames": "~2.2.6",
|
||||
"copy-to-clipboard": "^3.0.8",
|
||||
"@ant-design/create-react-context": "^0.2.4",
|
||||
"css-animation": "^1.5.0",
|
||||
"dom-closest": "^0.2.0",
|
||||
"enquire.js": "^2.1.6",
|
||||
@ -69,6 +69,7 @@
|
||||
"rc-editor-mention": "^1.1.7",
|
||||
"rc-form": "^2.4.0",
|
||||
"rc-input-number": "~4.4.0",
|
||||
"rc-mentions": "~0.3.1",
|
||||
"rc-menu": "~7.4.12",
|
||||
"rc-notification": "~3.3.0",
|
||||
"rc-pagination": "~1.18.0",
|
||||
|
@ -54,6 +54,7 @@ module.exports = {
|
||||
'Data Display': 4,
|
||||
Feedback: 5,
|
||||
Other: 6,
|
||||
Deprecated: 7,
|
||||
通用: 0,
|
||||
布局: 1,
|
||||
导航: 2,
|
||||
@ -61,6 +62,7 @@ module.exports = {
|
||||
数据展示: 4,
|
||||
反馈: 5,
|
||||
其他: 6,
|
||||
废弃: 7,
|
||||
},
|
||||
docVersions: {
|
||||
'0.9.x': 'http://09x.ant.design',
|
||||
|
@ -34,6 +34,7 @@ Array [
|
||||
"LocaleProvider",
|
||||
"message",
|
||||
"Menu",
|
||||
"Mentions",
|
||||
"Modal",
|
||||
"Statistic",
|
||||
"notification",
|
||||
|
Loading…
Reference in New Issue
Block a user