Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support mentions onece delete #48389

Open
wants to merge 7 commits into
base: feature
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -395,6 +395,22 @@ exports[`renders components/mentions/demo/form.tsx extend context correctly 1`]

exports[`renders components/mentions/demo/form.tsx extend context correctly 2`] = `[]`;

exports[`renders components/mentions/demo/onceDelete.tsx extend context correctly 1`] = `
<div
class="ant-mentions ant-mentions-outlined"
style="width: 100%;"
>
<textarea
class="rc-textarea"
rows="1"
>
@afc163@zombieJ
</textarea>
</div>
`;

exports[`renders components/mentions/demo/onceDelete.tsx extend context correctly 2`] = `[]`;

exports[`renders components/mentions/demo/placement.tsx extend context correctly 1`] = `
<div
class="ant-mentions ant-mentions-outlined"
Expand Down
14 changes: 14 additions & 0 deletions components/mentions/__tests__/__snapshots__/demo.test.tsx.snap
Expand Up @@ -335,6 +335,20 @@ exports[`renders components/mentions/demo/form.tsx correctly 1`] = `
</form>
`;

exports[`renders components/mentions/demo/onceDelete.tsx correctly 1`] = `
<div
class="ant-mentions ant-mentions-outlined"
style="width:100%"
>
<textarea
class="rc-textarea"
rows="1"
>
@afc163@zombieJ
</textarea>
</div>
`;

exports[`renders components/mentions/demo/placement.tsx correctly 1`] = `
<div
class="ant-mentions ant-mentions-outlined"
Expand Down
30 changes: 30 additions & 0 deletions components/mentions/__tests__/index.test.tsx
Expand Up @@ -5,6 +5,7 @@ import focusTest from '../../../tests/shared/focusTest';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { act, fireEvent, render } from '../../../tests/utils';
import KeyCode from 'rc-util/lib/KeyCode';

const { getMentions } = Mentions;

Expand Down Expand Up @@ -95,6 +96,35 @@ describe('Mentions', () => {
expect(textareaInstance.value).toEqual('');
});

it('should delete last item when Delete key is pressed', () => {
const { getByRole } = render(
<Mentions
style={{ width: '100%' }}
defaultValue="@afc163@zombieJ"
itemOnceDelete
options={[
{
value: 'afc163',
label: 'afc163',
},
{
value: 'zombieJ',
label: 'zombieJ',
},
{
value: 'yesmeck',
label: 'yesmeck',
},
]}
/>,
);
const textarea = getByRole('textbox') as HTMLTextAreaElement;
textarea.selectionStart = textarea.value.length;
textarea.selectionEnd = textarea.value.length;
fireEvent.keyDown(textarea, { key: 'Delete', keyCode: KeyCode.BACKSPACE });
expect(textarea.value).toBe('@afc163 ');
});

it('should support custom clearIcon', () => {
const { container } = render(<Mentions allowClear={{ clearIcon: 'clear' }} />);
expect(container.querySelector('.ant-mentions-clear-icon')?.textContent).toBe('clear');
Expand Down
7 changes: 7 additions & 0 deletions components/mentions/demo/onceDelete.md
@@ -0,0 +1,7 @@
## zh-CN

一次删除按键删除。

## en-US

Once Delete.
26 changes: 26 additions & 0 deletions components/mentions/demo/onceDelete.tsx
@@ -0,0 +1,26 @@
import { Mentions } from 'antd';
import React from 'react';

const App: React.FC = () => (
<Mentions
style={{ width: '100%' }}
defaultValue="@afc163@zombieJ"
itemOnceDelete
options={[
{
value: 'afc163',
label: 'afc163',
},
{
value: 'zombieJ',
label: 'zombieJ',
},
{
value: 'yesmeck',
label: 'yesmeck',
},
]}
/>
);

export default App;
2 changes: 2 additions & 0 deletions components/mentions/index.en-US.md
Expand Up @@ -47,6 +47,7 @@ return (
<code src="./demo/allowClear.tsx">With clear icon</code>
<code src="./demo/autoSize.tsx">autoSize</code>
<code src="./demo/status.tsx">Status</code>
<code src="./demo/onceDelete.tsx">Once Delete</code>
<code src="./demo/render-panel.tsx" debug>_InternalPanelDoNotUseOrYouWillBeFired</code>
<code src="./demo/component-token.tsx" debug>Component Token</code>

Expand Down Expand Up @@ -79,6 +80,7 @@ Common props ref:[Common props](/docs/react/common-props)
| onSearch | Trigger when prefix hit | (text: string, prefix: string) => void | - | |
| onSelect | Trigger when user select the option | (option: OptionProps, prefix: string) => void | - | |
| options | Option Configuration | [Options](#option) | \[] | 5.1.0 |
| itemOnceDelete | Delete Item with Once Delete | boolean | - | |

### Mention methods

Expand Down
68 changes: 67 additions & 1 deletion components/mentions/index.tsx
Expand Up @@ -22,6 +22,8 @@
import useVariant from '../form/hooks/useVariants';
import Spin from '../spin';
import useStyle from './style';
import KeyCode from 'rc-util/lib/KeyCode';
import useMergedState from 'rc-util/lib/hooks/useMergedState';

export const { Option } = RcMentions;

Expand All @@ -45,14 +47,15 @@
status?: InputStatus;
options?: MentionsOptionProps[];
popupClassName?: string;
itemOnceDelete?: boolean;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个 prop 不符合 antd 的 naming standard,可以先看看现有的 API 命名有没有可以复用的

/**
* @since 5.13.0
* @default "outlined"
*/
variant?: Variant;
}

export interface MentionsRef extends RcMentionsRef {}
export interface MentionsRef extends RcMentionsRef { }

interface MentionsConfig {
prefix?: string | string[];
Expand All @@ -75,13 +78,21 @@
children,
notFoundContent,
options,
itemOnceDelete = false,
status: customStatus,
allowClear = false,
popupClassName,
style,
variant: customVariant,
value,
defaultValue,
onChange,
...restProps
} = props;
const [mergedValue, setMergedValue] = useMergedState('', {
defaultValue,
value,
});
const [focused, setFocused] = React.useState(false);
const innerRef = React.useRef<MentionsRef>(null);
const mergedRef = composeRef(ref, innerRef);
Expand Down Expand Up @@ -121,6 +132,58 @@
setFocused(false);
};

const onInternalChange = (data: string) => {
setMergedValue(data)
onChange?.(data);
}

const splitValue = (data: string) => {
if (props.prefix) {
const prefix = Array.isArray(props.prefix) ? props.prefix : [props.prefix];
prefix.forEach((pre) => {
if (pre) {
data = data.split(pre).join(` ${pre}`);

Check warning on line 145 in components/mentions/index.tsx

View check run for this annotation

Codecov / codecov/patch

components/mentions/index.tsx#L142-L145

Added lines #L142 - L145 were not covered by tests
}
});
} else {
data = data.split('@').join(' @');
}
return data.trim();
}

const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.keyCode === KeyCode.BACKSPACE && itemOnceDelete) {
if (mergedValue) {
const data = splitValue(mergedValue)
const cursorPosition = e.currentTarget.selectionStart;
const valueList = data.trimEnd().split(props.split ?? ' ');
let length = 0;
let itemIndex = -1;
for (let i = 0; i < valueList.length; i++) {
length += valueList[i].length + (props.split?.length ?? 1);
if (length >= cursorPosition) {
itemIndex = i;
break;
}
}
const len = valueList.slice(itemIndex + 1, valueList.length).join('').length
valueList.splice(itemIndex, 1);
if (valueList.length !== 0) {
const res = valueList.join(props.split ?? ' ') + (props.split ?? ' ');
setMergedValue(res);
setTimeout(() => {
if (innerRef.current && innerRef.current.textarea) {
innerRef.current.textarea.selectionStart -= len
innerRef.current.textarea.selectionEnd -= len

Check warning on line 177 in components/mentions/index.tsx

View check run for this annotation

Codecov / codecov/patch

components/mentions/index.tsx#L175-L177

Added lines #L175 - L177 were not covered by tests
}
})
} else {
setMergedValue('');

Check warning on line 181 in components/mentions/index.tsx

View check run for this annotation

Codecov / codecov/patch

components/mentions/index.tsx#L180-L181

Added lines #L180 - L181 were not covered by tests
}
}
}
};

const notFoundContentEle = React.useMemo<React.ReactNode>(() => {
if (notFoundContent !== undefined) {
return notFoundContent;
Expand Down Expand Up @@ -181,8 +244,11 @@
allowClear={mergedAllowClear}
direction={direction}
style={{ ...contextMentions?.style, ...style }}
value={mergedValue}
{...restProps}
filterOption={mentionsfilterOption}
onKeyDown={onKeyDown}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

事件丢了,应该还需要透一下 props. onKeyDown?.(event)

onChange={onInternalChange}
onFocus={onFocus}
onBlur={onBlur}
dropdownClassName={classNames(popupClassName, rootClassName, hashId, cssVarCls, rootCls)}
Expand Down
2 changes: 2 additions & 0 deletions components/mentions/index.zh-CN.md
Expand Up @@ -48,6 +48,7 @@ return (
<code src="./demo/allowClear.tsx">带移除图标</code>
<code src="./demo/autoSize.tsx">自动大小</code>
<code src="./demo/status.tsx">自定义状态</code>
<code src="./demo/onceDelete.tsx">一次删除</code>
<code src="./demo/render-panel.tsx" debug>_InternalPanelDoNotUseOrYouWillBeFired</code>
<code src="./demo/component-token.tsx" debug>组件 Token</code>

Expand Down Expand Up @@ -80,6 +81,7 @@ return (
| onSearch | 搜索时触发 | (text: string, prefix: string) => void | - | |
| onSelect | 选择选项时触发 | (option: OptionProps, prefix: string) => void | - | |
| options | 选项配置 | [Options](#option) | [] | 5.1.0 |
| itemOnceDelete | 支持一键清除 | boolean | false | | |

### Mentions 方法

Expand Down