Skip to content

Commit f7b39e2

Browse files
tangjinzhouajuner
andauthored
Refactor mentions (#4341)
* refactor(mentions): use compositionAPI (#4313) * refactor: mentions * refactor: mentions Co-authored-by: ajuner <[email protected]>
1 parent 8198cab commit f7b39e2

17 files changed

+719
-621
lines changed

components/mentions/__tests__/index.test.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ function $$(className) {
1111
}
1212

1313
function triggerInput(wrapper, text = '') {
14+
const lastChar = text[text.length - 1];
1415
wrapper.find('textarea').element.value = text;
1516
wrapper.find('textarea').element.selectionStart = text.length;
1617
wrapper.find('textarea').trigger('keydown');
1718
wrapper.find('textarea').trigger('change');
18-
wrapper.find('textarea').trigger('keyup');
19+
wrapper.find('textarea').trigger('keyup', { key: lastChar });
1920
}
2021

2122
describe('Mentions', () => {
@@ -69,9 +70,9 @@ describe('Mentions', () => {
6970
},
7071
{ sync: false, attachTo: 'body' },
7172
);
72-
await sleep(500);
73+
await sleep(100);
7374
triggerInput(wrapper, '@');
74-
await sleep(500);
75+
await sleep(100);
7576
expect($$('.ant-mentions-dropdown-menu-item').length).toBeTruthy();
7677
expect($$('.ant-spin')).toBeTruthy();
7778
});

components/mentions/index.tsx

+130-129
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
import type { App, PropType, VNodeTypes, Plugin, ExtractPropTypes } from 'vue';
2-
import { defineComponent, inject, nextTick } from 'vue';
1+
import type { App, PropType, Plugin, ExtractPropTypes } from 'vue';
2+
import { watch } from 'vue';
3+
import { ref, onMounted } from 'vue';
4+
import { defineComponent, nextTick } from 'vue';
35
import classNames from '../_util/classNames';
46
import omit from 'omit.js';
57
import PropTypes from '../_util/vue-types';
68
import VcMentions from '../vc-mentions';
79
import { mentionsProps as baseMentionsProps } from '../vc-mentions/src/mentionsProps';
8-
import Spin from '../spin';
9-
import BaseMixin from '../_util/BaseMixin';
10-
import { defaultConfigProvider } from '../config-provider';
11-
import { getOptionProps, getComponent, getSlot } from '../_util/props-util';
12-
import type { RenderEmptyHandler } from '../config-provider/renderEmpty';
10+
import useConfigInject from '../_util/hooks/useConfigInject';
11+
import { flattenChildren, getOptionProps } from '../_util/props-util';
1312

1413
const { Option } = VcMentions;
1514

@@ -20,23 +19,26 @@ interface MentionsConfig {
2019

2120
export interface MentionsOptionProps {
2221
value: string;
23-
disabled: boolean;
24-
children: VNodeTypes;
22+
disabled?: boolean;
23+
label?: string | number | ((o: MentionsOptionProps) => any);
2524
[key: string]: any;
2625
}
2726

28-
function loadingFilterOption() {
29-
return true;
27+
interface MentionsEntity {
28+
prefix: string;
29+
value: string;
3030
}
3131

32-
function getMentions(value = '', config: MentionsConfig) {
32+
export type MentionPlacement = 'top' | 'bottom';
33+
34+
const getMentions = (value = '', config: MentionsConfig): MentionsEntity[] => {
3335
const { prefix = '@', split = ' ' } = config || {};
34-
const prefixList = Array.isArray(prefix) ? prefix : [prefix];
36+
const prefixList: string[] = Array.isArray(prefix) ? prefix : [prefix];
3537

3638
return value
3739
.split(split)
38-
.map((str = '') => {
39-
let hitPrefix = null;
40+
.map((str = ''): MentionsEntity | null => {
41+
let hitPrefix: string | null = null;
4042

4143
prefixList.some(prefixStr => {
4244
const startStr = str.slice(0, prefixStr.length);
@@ -50,13 +52,13 @@ function getMentions(value = '', config: MentionsConfig) {
5052
if (hitPrefix !== null) {
5153
return {
5254
prefix: hitPrefix,
53-
value: str.slice(hitPrefix.length),
55+
value: str.slice((hitPrefix as string).length),
5456
};
5557
}
5658
return null;
5759
})
58-
.filter(entity => !!entity && !!entity.value);
59-
}
60+
.filter((entity): entity is MentionsEntity => !!entity && !!entity.value);
61+
};
6062

6163
const mentionsProps = {
6264
...baseMentionsProps,
@@ -73,145 +75,144 @@ const mentionsProps = {
7375
onChange: {
7476
type: Function as PropType<(text: string) => void>,
7577
},
78+
notFoundContent: PropTypes.any,
79+
defaultValue: String,
7680
};
7781

7882
export type MentionsProps = Partial<ExtractPropTypes<typeof mentionsProps>>;
7983

8084
const Mentions = defineComponent({
8185
name: 'AMentions',
82-
mixins: [BaseMixin],
8386
inheritAttrs: false,
84-
Option: { ...Option, name: 'AMentionsOption' },
85-
getMentions,
8687
props: mentionsProps,
87-
emits: ['update:value', 'change', 'focus', 'blur', 'select'],
88-
setup() {
89-
return {
90-
configProvider: inject('configProvider', defaultConfigProvider),
88+
getMentions,
89+
Option,
90+
emits: ['update:value', 'change', 'focus', 'blur', 'select', 'pressenter'],
91+
slots: ['notFoundContent', 'option'],
92+
setup(props, { slots, emit, attrs, expose }) {
93+
const { prefixCls, renderEmpty, direction } = useConfigInject('mentions', props);
94+
const focused = ref(false);
95+
const vcMentions = ref(null);
96+
const value = ref(props.value ?? props.defaultValue ?? '');
97+
watch(
98+
() => props.value,
99+
val => {
100+
value.value = val;
101+
},
102+
);
103+
const handleFocus = (e: FocusEvent) => {
104+
focused.value = true;
105+
emit('focus', e);
91106
};
92-
},
93-
data() {
94-
return {
95-
focused: false,
107+
108+
const handleBlur = (e: FocusEvent) => {
109+
focused.value = false;
110+
emit('blur', e);
96111
};
97-
},
98-
mounted() {
99-
nextTick(() => {
100-
if (process.env.NODE_ENV === 'test') {
101-
if (this.autofocus) {
102-
this.focus();
103-
}
112+
113+
const handleSelect = (...args: [MentionsOptionProps, string]) => {
114+
emit('select', ...args);
115+
focused.value = true;
116+
};
117+
118+
const handleChange = (val: string) => {
119+
if (props.value === undefined) {
120+
value.value = val;
104121
}
105-
});
106-
},
107-
methods: {
108-
handleFocus(e: FocusEvent) {
109-
this.$emit('focus', e);
110-
this.setState({
111-
focused: true,
112-
});
113-
},
114-
handleBlur(e: FocusEvent) {
115-
this.$emit('blur', e);
116-
this.setState({
117-
focused: false,
118-
});
119-
},
120-
handleSelect(...args: [MentionsOptionProps, string]) {
121-
this.$emit('select', ...args);
122-
this.setState({
123-
focused: true,
124-
});
125-
},
126-
handleChange(val: string) {
127-
this.$emit('update:value', val);
128-
this.$emit('change', val);
129-
},
130-
getNotFoundContent(renderEmpty: RenderEmptyHandler) {
131-
const notFoundContent = getComponent(this, 'notFoundContent');
122+
emit('update:value', val);
123+
emit('change', val);
124+
};
125+
126+
const getNotFoundContent = () => {
127+
const notFoundContent = props.notFoundContent;
132128
if (notFoundContent !== undefined) {
133129
return notFoundContent;
134130
}
135-
136-
return renderEmpty('Select');
137-
},
138-
getOptions() {
139-
const { loading } = this.$props;
140-
const children = getSlot(this);
141-
142-
if (loading) {
143-
return (
144-
<Option value="ANTD_SEARCHING" disabled>
145-
<Spin size="small" />
146-
</Option>
147-
);
148-
}
149-
return children;
150-
},
151-
getFilterOption() {
152-
const { filterOption, loading } = this.$props;
153-
if (loading) {
154-
return loadingFilterOption;
131+
if (slots.notFoundContent) {
132+
return slots.notFoundContent();
155133
}
156-
return filterOption;
157-
},
158-
focus() {
159-
(this.$refs.vcMentions as HTMLTextAreaElement).focus();
160-
},
161-
blur() {
162-
(this.$refs.vcMentions as HTMLTextAreaElement).blur();
163-
},
164-
},
165-
render() {
166-
const { focused } = this.$data;
167-
const { getPrefixCls, renderEmpty } = this.configProvider;
168-
const {
169-
prefixCls: customizePrefixCls,
170-
disabled,
171-
getPopupContainer,
172-
...restProps
173-
} = getOptionProps(this) as any;
174-
const { class: className, ...otherAttrs } = this.$attrs;
175-
const prefixCls = getPrefixCls('mentions', customizePrefixCls);
176-
const otherProps = omit(restProps, ['loading', 'onUpdate:value']);
177-
178-
const mergedClassName = classNames(className, {
179-
[`${prefixCls}-disabled`]: disabled,
180-
[`${prefixCls}-focused`]: focused,
181-
});
134+
return renderEmpty.value('Select');
135+
};
182136

183-
const mentionsProps = {
184-
prefixCls,
185-
notFoundContent: this.getNotFoundContent(renderEmpty),
186-
...otherProps,
187-
disabled,
188-
filterOption: this.getFilterOption(),
189-
getPopupContainer,
190-
children: this.getOptions(),
191-
class: mergedClassName,
192-
rows: 1,
193-
...otherAttrs,
194-
onChange: this.handleChange,
195-
onSelect: this.handleSelect,
196-
onFocus: this.handleFocus,
197-
onBlur: this.handleBlur,
198-
ref: 'vcMentions',
137+
const getOptions = () => {
138+
return flattenChildren(slots.default?.() || []).map(item => {
139+
return { ...getOptionProps(item), label: (item.children as any)?.default?.() };
140+
});
199141
};
200142

201-
return <VcMentions {...mentionsProps} />;
143+
const focus = () => {
144+
(vcMentions.value as HTMLTextAreaElement).focus();
145+
};
146+
147+
const blur = () => {
148+
(vcMentions.value as HTMLTextAreaElement).blur();
149+
};
150+
151+
expose({ focus, blur });
152+
153+
onMounted(() => {
154+
nextTick(() => {
155+
if (process.env.NODE_ENV === 'test') {
156+
if (props.autofocus) {
157+
focus();
158+
}
159+
}
160+
});
161+
});
162+
163+
return () => {
164+
const { disabled, getPopupContainer, rows = 1, ...restProps } = props;
165+
const { class: className, ...otherAttrs } = attrs;
166+
const otherProps = omit(restProps, ['defaultValue', 'onUpdate:value', 'prefixCls']);
167+
168+
const mergedClassName = classNames(className, {
169+
[`${prefixCls.value}-disabled`]: disabled,
170+
[`${prefixCls.value}-focused`]: focused.value,
171+
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
172+
});
173+
174+
const mentionsProps = {
175+
prefixCls: prefixCls.value,
176+
...otherProps,
177+
disabled,
178+
direction: direction.value,
179+
filterOption: props.filterOption,
180+
getPopupContainer,
181+
options: props.options || getOptions(),
182+
class: mergedClassName,
183+
...otherAttrs,
184+
rows,
185+
onChange: handleChange,
186+
onSelect: handleSelect,
187+
onFocus: handleFocus,
188+
onBlur: handleBlur,
189+
ref: vcMentions,
190+
value: value.value,
191+
};
192+
return (
193+
<VcMentions
194+
{...mentionsProps}
195+
v-slots={{ notFoundContent: getNotFoundContent, option: slots.option }}
196+
></VcMentions>
197+
);
198+
};
202199
},
203200
});
204201

202+
export const MentionsOption = {
203+
...Option,
204+
name: 'AMentionsOption',
205+
};
206+
205207
/* istanbul ignore next */
206208
Mentions.install = function (app: App) {
207209
app.component(Mentions.name, Mentions);
208-
app.component(Mentions.Option.name, Mentions.Option);
210+
app.component(MentionsOption.name, MentionsOption);
209211
return app;
210212
};
211213

212-
export const MentionsOption = Mentions.Option;
213-
214214
export default Mentions as typeof Mentions &
215215
Plugin & {
216+
getMentions: typeof getMentions;
216217
readonly Option: typeof Option;
217218
};

components/mentions/style/index.less

+4-6
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,6 @@
6464
background-color: transparent;
6565
}
6666
.placeholder();
67-
68-
&:read-only {
69-
cursor: default;
70-
}
7167
}
7268

7369
&-measure {
@@ -123,7 +119,7 @@
123119
overflow: hidden;
124120
color: @text-color;
125121
font-weight: normal;
126-
line-height: 22px;
122+
line-height: @line-height-base;
127123
white-space: nowrap;
128124
text-overflow: ellipsis;
129125
cursor: pointer;
@@ -159,9 +155,11 @@
159155
}
160156

161157
&-active {
162-
background-color: @item-active-bg;
158+
background-color: @item-hover-bg;
163159
}
164160
}
165161
}
166162
}
167163
}
164+
165+
@import './rtl';

components/mentions/style/rtl.less

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
@import '../../style/themes/index';
2+
@import '../../style/mixins/index';
3+
4+
@mention-prefix-cls: ~'@{ant-prefix}-mentions';
5+
6+
.@{mention-prefix-cls} {
7+
&-rtl {
8+
direction: rtl;
9+
}
10+
}

0 commit comments

Comments
 (0)