Skip to content

Commit 16e0755

Browse files
authored
feat: add mentions (#1790)
* feat: mentions style * feat: theme default * feat: add mentions component * feat: mentions API * feat: add unit test for mentions * feat: update mentions demo * perf: model and inheritAttrs for mentions * perf: use getComponentFromProp instead of this.$props * perf: mentions rm defaultProps * feat: rm rows in mentionsProps * fix: mentions keyDown didn't work * docs: update mentions api * perf: mentions code
1 parent a04b35f commit 16e0755

30 files changed

+1583
-0
lines changed

components/index.js

+4
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ import { default as message } from './message';
7373

7474
import { default as Menu } from './menu';
7575

76+
import { default as Mentions } from './mentions';
77+
7678
import { default as Modal } from './modal';
7779

7880
import { default as notification } from './notification';
@@ -171,6 +173,7 @@ const components = [
171173
List,
172174
LocaleProvider,
173175
Menu,
176+
Mentions,
174177
Modal,
175178
Pagination,
176179
Popconfirm,
@@ -258,6 +261,7 @@ export {
258261
List,
259262
LocaleProvider,
260263
Menu,
264+
Mentions,
261265
Modal,
262266
Pagination,
263267
Popconfirm,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`renders ./components/mentions/demo/async.md correctly 1`] = `<div class="ant-mentions"><textarea rows="1"></textarea></div>`;
4+
5+
exports[`renders ./components/mentions/demo/basic.md correctly 1`] = `<div class="ant-mentions"><textarea rows="1"></textarea></div>`;
6+
7+
exports[`renders ./components/mentions/demo/form.md correctly 1`] = `
8+
<form class="ant-form ant-form-horizontal">
9+
<div class="ant-row ant-form-item">
10+
<div class="ant-col-5 ant-form-item-label"><label for="mentions_coders" title="Top coders" class="">Top coders</label></div>
11+
<div class="ant-col-12 ant-form-item-control-wrapper">
12+
<div class="ant-form-item-control"><span class="ant-form-item-children"><div class="ant-mentions"><textarea rows="1" data-__meta="[object Object]" data-__field="[object Object]" id="mentions_coders"></textarea></div></span>
13+
<!---->
14+
</div>
15+
</div>
16+
</div>
17+
<div class="ant-row ant-form-item">
18+
<div class="ant-col-5 ant-form-item-label"><label for="mentions_bio" title="Bio" class="ant-form-item-required">Bio</label></div>
19+
<div class="ant-col-12 ant-form-item-control-wrapper">
20+
<div class="ant-form-item-control"><span class="ant-form-item-children"><div class="ant-mentions"><textarea rows="3" placeholder="You can use @ to ref user here" data-__meta="[object Object]" data-__field="[object Object]" id="mentions_bio"></textarea></div></span>
21+
<!---->
22+
</div>
23+
</div>
24+
</div>
25+
<div class="ant-row ant-form-item">
26+
<div class="ant-col-12 ant-col-offset-5 ant-form-item-control-wrapper">
27+
<div class="ant-form-item-control"><span class="ant-form-item-children"><button type="button" class="ant-btn ant-btn-primary"><span>Submit</span></button><button type="button" class="ant-btn" style="margin-left: 8px;"><span>Reset</span></button></span>
28+
<!---->
29+
</div>
30+
</div>
31+
</div>
32+
</form>
33+
`;
34+
35+
exports[`renders ./components/mentions/demo/placement.md correctly 1`] = `<div class="ant-mentions"><textarea rows="1"></textarea></div>`;
36+
37+
exports[`renders ./components/mentions/demo/prefix.md correctly 1`] = `<div class="ant-mentions"><textarea rows="1" placeholder="input @ to mention people, # to mention tag"></textarea></div>`;
38+
39+
exports[`renders ./components/mentions/demo/readonly.md correctly 1`] = `
40+
<div>
41+
<div style="margin-bottom: 10px;">
42+
<div class="ant-mentions ant-mentions-disabled"><textarea disabled="disabled" rows="1" placeholder="this is disabled Mentions"></textarea></div>
43+
</div>
44+
<div class="ant-mentions"><textarea rows="1" placeholder="this is readOnly a-mentions" readonly=""></textarea></div>
45+
</div>
46+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import demoTest from '../../../tests/shared/demoTest';
2+
3+
demoTest('mentions');
+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { mount } from '@vue/test-utils';
2+
import Vue from 'vue';
3+
import Mentions from '..';
4+
import { asyncExpect } from '@/tests/utils';
5+
import focusTest from '../../../tests/shared/focusTest';
6+
7+
const { getMentions } = Mentions;
8+
9+
function $$(className) {
10+
return document.body.querySelectorAll(className);
11+
}
12+
13+
function triggerInput(wrapper, text = '') {
14+
wrapper.find('textarea').element.value = text;
15+
wrapper.find('textarea').element.selectionStart = text.length;
16+
wrapper.find('textarea').trigger('keydown');
17+
wrapper.find('textarea').trigger('change');
18+
wrapper.find('textarea').trigger('keyup');
19+
}
20+
21+
describe('Mentions', () => {
22+
beforeAll(() => {
23+
jest.useFakeTimers();
24+
});
25+
26+
afterAll(() => {
27+
jest.useRealTimers();
28+
});
29+
30+
it('getMentions', () => {
31+
const mentions = getMentions('@light #bamboo cat', { prefix: ['@', '#'] });
32+
expect(mentions).toEqual([
33+
{
34+
prefix: '@',
35+
value: 'light',
36+
},
37+
{
38+
prefix: '#',
39+
value: 'bamboo',
40+
},
41+
]);
42+
});
43+
44+
it('focus', () => {
45+
const onFocus = jest.fn();
46+
const onBlur = jest.fn();
47+
48+
const wrapper = mount({
49+
render() {
50+
return <Mentions onFocus={onFocus} onBlur={onBlur} />;
51+
},
52+
});
53+
wrapper.find('textarea').trigger('focus');
54+
expect(wrapper.find('.ant-mentions').classes('ant-mentions-focused')).toBeTruthy();
55+
expect(onFocus).toHaveBeenCalled();
56+
57+
wrapper.find('textarea').trigger('blur');
58+
jest.runAllTimers();
59+
expect(wrapper.classes()).not.toContain('ant-mentions-focused');
60+
expect(onBlur).toHaveBeenCalled();
61+
});
62+
63+
it('loading', done => {
64+
const wrapper = mount(
65+
{
66+
render() {
67+
return <Mentions loading />;
68+
},
69+
},
70+
{ sync: false },
71+
);
72+
triggerInput(wrapper, '@');
73+
Vue.nextTick(() => {
74+
mount(
75+
{
76+
render() {
77+
return wrapper.find({ name: 'Trigger' }).vm.getComponent();
78+
},
79+
},
80+
{ sync: false },
81+
);
82+
Vue.nextTick(() => {
83+
expect($$('.ant-mentions-dropdown-menu-item').length).toBeTruthy();
84+
expect($$('.ant-spin')).toBeTruthy();
85+
done();
86+
});
87+
});
88+
});
89+
90+
focusTest(Mentions);
91+
});

components/mentions/demo/async.md

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<cn>
2+
#### 异步加载
3+
匹配内容列表为异步返回时。
4+
</cn>
5+
6+
<us>
7+
#### Asynchronous loading
8+
async.
9+
</us>
10+
11+
```tpl
12+
<template>
13+
<a-mentions @search="onSearch" :loading="loading">
14+
<a-mentions-option
15+
v-for="({ login, avatar_url: avatar }) in users"
16+
:key="login"
17+
:value="login"
18+
>
19+
<img :src="avatar" :alt="login" style="width: 20px; margin-right: 8px;">
20+
<span>{{login}}</span>
21+
</a-mentions-option>
22+
</a-mentions>
23+
</template>
24+
25+
<script>
26+
import debounce from 'lodash/debounce';
27+
export default {
28+
data() {
29+
return {
30+
loading: false,
31+
users: []
32+
}
33+
},
34+
mounted() {
35+
this.loadGithubUsers = debounce(this.loadGithubUsers, 800);
36+
},
37+
methods: {
38+
onSearch(search) {
39+
this.search = search;
40+
this.loading = !!search;
41+
console.log(!!search)
42+
this.users = [];
43+
console.log('Search:', search);
44+
this.loadGithubUsers(search);
45+
},
46+
loadGithubUsers(key) {
47+
if (!key) {
48+
this.users = [];
49+
return;
50+
}
51+
fetch(`https://api.github.com/search/users?q=${key}`)
52+
.then(res => res.json())
53+
.then(({ items = [] }) => {
54+
const { search } = this;
55+
if (search !== key) return;
56+
57+
this.users = items.slice(0, 10);
58+
this.loading = false;
59+
});
60+
}
61+
}
62+
}
63+
</script>
64+
<style>
65+
```

components/mentions/demo/basic.md

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<cn>
2+
#### 基础列表
3+
基本使用。
4+
</cn>
5+
6+
<us>
7+
#### Basic usage
8+
Basic usage.
9+
</us>
10+
11+
```tpl
12+
<template>
13+
<a-mentions
14+
defaultValue="@afc163"
15+
@change="onChange"
16+
@select="onSelect"
17+
>
18+
<a-mentions-option value="afc163">afc163</a-mentions-option>
19+
<a-mentions-option value="zombieJ">zombieJ</a-mentions-option>
20+
<a-mentions-option value="yesmeck">yesmeck</a-mentions-option>
21+
</a-mentions>
22+
</template>
23+
<script>
24+
export default {
25+
methods: {
26+
onSelect(option) {
27+
console.log('select', option);
28+
},
29+
onChange(value) {
30+
console.log('Change:', value);
31+
}
32+
}
33+
}
34+
</script>
35+
```

components/mentions/demo/form.md

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<cn>
2+
#### 配合 Form 使用
3+
受控模式,例如配合 Form 使用。
4+
</cn>
5+
6+
<us>
7+
#### With Form
8+
Controlled mode, for example, to work with `Form`.
9+
</us>
10+
11+
```tpl
12+
<template>
13+
<a-form :form="form" layout="horizontal">
14+
<a-form-item label="Top coders" :label-col="{ span: 5 }" :wrapper-col="{ span: 12 }">
15+
<a-mentions
16+
rows="1"
17+
v-decorator="[
18+
'coders',
19+
{
20+
rules: [{ validator: checkMention }],
21+
},
22+
]"
23+
>
24+
<a-mentions-option value="afc163">afc163</a-mentions-option>
25+
<a-mentions-option value="zombieJ">zombieJ</a-mentions-option>
26+
<a-mentions-option value="yesmeck">yesmeck</a-mentions-option>
27+
</a-mentions>
28+
</a-form-item>
29+
<a-form-item label="Bio" :label-col="{ span: 5 }" :wrapper-col="{ span: 12 }">
30+
<a-mentions
31+
rows="3"
32+
placeholder="You can use @ to ref user here"
33+
v-decorator="[
34+
'bio',
35+
{
36+
rules: [{ required: true }],
37+
},
38+
]"
39+
>
40+
<a-mentions-option value="afc163">afc163</a-mentions-option>
41+
<a-mentions-option value="zombieJ">zombieJ</a-mentions-option>
42+
<a-mentions-option value="yesmeck">yesmeck</a-mentions-option>
43+
</a-mentions>
44+
</a-form-item>
45+
<a-form-item :wrapper-col="{ span: 12, offset: 5 }">
46+
<a-button type="primary" @click="handleSubmit">
47+
Submit
48+
</a-button>
49+
<a-button style="margin-left: 8px;" @click="handleReset">Reset</a-button>
50+
</a-form-item>
51+
</a-form>
52+
</template>
53+
<script>
54+
import { Mentions } from 'ant-design-vue';
55+
const { getMentions } = Mentions;
56+
export default {
57+
data() {
58+
return {
59+
form: this.$form.createForm(this, { name: 'mentions' })
60+
}
61+
},
62+
methods: {
63+
handleReset(e) {
64+
e.preventDefault();
65+
this.form.resetFields();
66+
},
67+
handleSubmit(e) {
68+
e.preventDefault();
69+
this.form.validateFields((errors, values) => {
70+
if (errors) {
71+
console.log('Errors in the form!!!');
72+
return;
73+
}
74+
console.log('Submit!!!');
75+
console.log(values);
76+
});
77+
},
78+
checkMention(rule, value, callback) {
79+
const mentions = getMentions(value);
80+
if (mentions.length < 2) {
81+
callback(new Error('More than one must be selected!'));
82+
} else {
83+
callback();
84+
}
85+
}
86+
}
87+
}
88+
</script>
89+
```

0 commit comments

Comments
 (0)