refactor: message & notification (#5113)

* refactor: notification

* refactor: message

* refactor: notification

* test: update message & notification test
refactor-modal
tangjinzhou 2022-01-01 10:34:37 +08:00 committed by GitHub
parent 8a3724ff89
commit ad76bb678d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 939 additions and 575 deletions

View File

@ -114,6 +114,7 @@ export { default as Modal } from './modal';
export type { StatisticProps } from './statistic'; export type { StatisticProps } from './statistic';
export { default as Statistic, StatisticCountdown } from './statistic'; export { default as Statistic, StatisticCountdown } from './statistic';
export type { NotificationPlacement } from './notification';
export { default as notification } from './notification'; export { default as notification } from './notification';
export type { PageHeaderProps } from './page-header'; export type { PageHeaderProps } from './page-header';

View File

@ -9,6 +9,8 @@ import type { TransformCellTextProps } from '../table/interface';
import LocaleReceiver from '../locale-provider/LocaleReceiver'; import LocaleReceiver from '../locale-provider/LocaleReceiver';
import type { RequiredMark } from '../form/Form'; import type { RequiredMark } from '../form/Form';
import type { MaybeRef } from '../_util/type'; import type { MaybeRef } from '../_util/type';
import message from '../message';
import notification from '../notification';
export type SizeType = 'small' | 'middle' | 'large' | undefined; export type SizeType = 'small' | 'middle' | 'large' | undefined;
@ -248,6 +250,17 @@ const ConfigProvider = defineComponent({
); );
}; };
watchEffect(() => {
if (props.direction) {
message.config({
rtl: props.direction === 'rtl',
});
notification.config({
rtl: props.direction === 'rtl',
});
}
});
return () => ( return () => (
<LocaleReceiver children={(_, __, legacyLocale) => renderProvider(legacyLocale as Locale)} /> <LocaleReceiver children={(_, __, legacyLocale) => renderProvider(legacyLocale as Locale)} />
); );

View File

@ -1,5 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/message/demo/custom-style.vue correctly 1`] = `
<button class="ant-btn" type="button">
<!----><span>Customized style</span>
</button>
`;
exports[`renders ./components/message/demo/duration.vue correctly 1`] = ` exports[`renders ./components/message/demo/duration.vue correctly 1`] = `
<button class="ant-btn" type="button"> <button class="ant-btn" type="button">
<!----><span>Customized display duration</span> <!----><span>Customized display duration</span>
@ -36,6 +42,11 @@ exports[`renders ./components/message/demo/thenable.vue correctly 1`] = `
exports[`renders ./components/message/demo/update.vue correctly 1`] = ` exports[`renders ./components/message/demo/update.vue correctly 1`] = `
<button class="ant-btn ant-btn-primary" type="button"> <button class="ant-btn ant-btn-primary" type="button">
<!----><span>Open the message box</span> <!----><span>Open the message box (update by key)</span>
</button>
<br>
<br>
<button class="ant-btn ant-btn-primary" type="button">
<!----><span>Open the message box (update by reactive)</span>
</button> </button>
`; `;

View File

@ -1,9 +1,10 @@
import { asyncExpect } from '../../../tests/utils'; import { asyncExpect } from '../../../tests/utils';
import message from '..'; import message, { getInstance } from '..';
import SmileOutlined from '@ant-design/icons-vue/SmileOutlined'; import SmileOutlined from '@ant-design/icons-vue/SmileOutlined';
describe('message', () => { describe('message', () => {
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers();
document.body.outerHTML = ''; document.body.outerHTML = '';
}); });
@ -11,6 +12,11 @@ describe('message', () => {
message.destroy(); message.destroy();
}); });
afterEach(() => {
message.destroy();
jest.useRealTimers();
});
it('should be able to config top', async () => { it('should be able to config top', async () => {
message.config({ message.config({
top: '100px', top: '100px',
@ -41,53 +47,42 @@ describe('message', () => {
message.info('test'); message.info('test');
} }
message.info('last'); message.info('last');
await asyncExpect(() => { await Promise.resolve();
jest.runAllTimers();
expect(document.querySelectorAll('.ant-message-notice').length).toBe(5); expect(document.querySelectorAll('.ant-message-notice').length).toBe(5);
expect(document.querySelectorAll('.ant-message-notice')[4].textContent).toBe('last'); expect(document.querySelectorAll('.ant-message-notice')[4].textContent).toBe('last');
}, 0);
}); });
it('should be able to hide manually', async () => { it('should be able to hide manually', async () => {
const hide1 = message.info('whatever', 0); const hide1 = message.info('whatever', 0);
const hide2 = message.info('whatever', 0); const hide2 = message.info('whatever', 0);
await asyncExpect(() => { await Promise.resolve();
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2); expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
hide1(); hide1();
}, 0); jest.runAllTimers();
await asyncExpect(() => { expect(getInstance().component.value.notices).toHaveLength(1);
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
hide2(); hide2();
}, 0); jest.runAllTimers();
await asyncExpect(() => { expect(getInstance().component.value.notices).toHaveLength(0);
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
}, 0);
}); });
it('should be able to destroy globally', async () => { it('should be able to destroy globally', async () => {
await asyncExpect(() => {
message.info('whatever', 0); message.info('whatever', 0);
});
await asyncExpect(() => {
message.info('whatever', 0); message.info('whatever', 0);
}); await Promise.resolve();
await asyncExpect(() => {
expect(document.querySelectorAll('.ant-message').length).toBe(1); expect(document.querySelectorAll('.ant-message').length).toBe(1);
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2); expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
});
await asyncExpect(() => {
message.destroy(); message.destroy();
});
await asyncExpect(() => {
expect(document.querySelectorAll('.ant-message').length).toBe(0); expect(document.querySelectorAll('.ant-message').length).toBe(0);
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0); expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
}); });
});
it('should not need to use duration argument when using the onClose arguments', () => { it('should not need to use duration argument when using the onClose arguments', () => {
message.info('whatever', () => {}); message.info('whatever', () => {});
}); });
it('should have the default duration when using the onClose arguments', done => { it('should have the default duration when using the onClose arguments', done => {
jest.useRealTimers();
const defaultDuration = 3; const defaultDuration = 3;
const now = Date.now(); const now = Date.now();
message.info('whatever', () => { message.info('whatever', () => {
@ -99,6 +94,7 @@ describe('message', () => {
}); });
it('should be called like promise', done => { it('should be called like promise', done => {
jest.useRealTimers();
const defaultDuration = 3; const defaultDuration = 3;
const now = Date.now(); const now = Date.now();
message.info('whatever').then(() => { message.info('whatever').then(() => {
@ -112,38 +108,32 @@ describe('message', () => {
// https:// github.com/ant-design/ant-design/issues/8201 // https:// github.com/ant-design/ant-design/issues/8201
it('should hide message correctly', async () => { it('should hide message correctly', async () => {
let hide = message.loading('Action in progress..', 0); let hide = message.loading('Action in progress..', 0);
await asyncExpect(() => { await Promise.resolve();
expect(document.querySelectorAll('.ant-message-notice').length).toBe(1); expect(document.querySelectorAll('.ant-message-notice').length).toBe(1);
hide(); hide();
}, 0); await Promise.resolve();
await asyncExpect(() => { jest.runAllTimers();
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0); expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
}, 0);
}); });
it('should allow custom icon', async () => { it('should allow custom icon', async () => {
message.open({ content: 'Message', icon: <SmileOutlined /> }); message.open({ content: 'Message', icon: <SmileOutlined /> });
await asyncExpect(() => { await Promise.resolve();
expect(document.querySelectorAll('.anticon-smile').length).toBe(1); expect(document.querySelectorAll('.anticon-smile').length).toBe(1);
}, 0);
}); });
it('should have no icon', async () => { it('should have no icon', async () => {
message.open({ content: 'Message' }); message.open({ content: 'Message' });
await asyncExpect(() => { await Promise.resolve();
expect(document.querySelectorAll('.ant-message-notice .anticon').length).toBe(0); expect(document.querySelectorAll('.ant-message-notice .anticon').length).toBe(0);
}, 0);
}); });
// https://github.com/ant-design/ant-design/issues/8201 // https://github.com/ant-design/ant-design/issues/8201
it('should destroy messages correctly', async () => { it('should destroy messages correctly', async () => {
message.loading('Action in progress1..', 0); message.loading('Action in progress1..', 0);
message.loading('Action in progress2..', 0); message.loading('Action in progress2..', 0);
setTimeout(() => message.destroy(), 1000); setTimeout(() => message.destroy(), 1000);
await Promise.resolve();
await asyncExpect(() => {
expect(document.querySelectorAll('.ant-message-notice').length).toBe(2); expect(document.querySelectorAll('.ant-message-notice').length).toBe(2);
}, 0); jest.runAllTimers();
await asyncExpect(() => {
expect(document.querySelectorAll('.ant-message-notice').length).toBe(0); expect(document.querySelectorAll('.ant-message-notice').length).toBe(0);
}, 1500);
}); });
}); });

View File

@ -0,0 +1,46 @@
<docs>
---
order: 6
title:
zh-CN: 自定义样式
en-US: Customized style
---
## zh-CN
使用 `style` `class` 来定义样式
## en-US
The `style` and `class` are available to customize Message.
</docs>
<template>
<a-button @click="success">Customized style</a-button>
</template>
<script lang="ts">
import { message } from 'ant-design-vue';
import { defineComponent } from 'vue';
export default defineComponent({
setup() {
const success = () => {
message.success({
content: () => 'This is a prompt message with custom className and style',
class: 'custom-class',
style: {
marginTop: '20vh',
},
});
};
return {
success,
};
},
});
</script>
<style>
.custom-class {
color: red;
}
</style>

View File

@ -6,6 +6,7 @@
<loading /> <loading />
<thenable /> <thenable />
<update /> <update />
<customStyleVue />
</demo-sort> </demo-sort>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -15,6 +16,7 @@ import Duration from './duration.vue';
import Loading from './loading.vue'; import Loading from './loading.vue';
import Thenable from './thenable.vue'; import Thenable from './thenable.vue';
import Update from './update.vue'; import Update from './update.vue';
import customStyleVue from './custom-style.vue';
import CN from '../index.zh-CN.md'; import CN from '../index.zh-CN.md';
import US from '../index.en-US.md'; import US from '../index.en-US.md';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -28,6 +30,7 @@ export default defineComponent({
Loading, Loading,
Thenable, Thenable,
Update, Update,
customStyleVue,
}, },
setup() { setup() {
return {}; return {};

View File

@ -8,20 +8,25 @@ title:
## zh-CN ## zh-CN
可以通过唯一的 `key` 来更新内容 可以通过唯一的 `key` 来更新内容或者响应式数据
## en-US ## en-US
Update message content with unique `key`. Update message content with unique `key`or use reactive data.
</docs> </docs>
<template> <template>
<a-button type="primary" @click="openMessage">Open the message box</a-button> <a-button type="primary" @click="openMessage">Open the message box (update by key)</a-button>
<br />
<br />
<a-button type="primary" @click="openMessage2">
Open the message box (update by reactive)
</a-button>
</template> </template>
<script lang="ts"> <script lang="ts">
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { defineComponent } from 'vue'; import { defineComponent, ref } from 'vue';
const key = 'updatable'; const key = 'updatable';
export default defineComponent({ export default defineComponent({
setup() { setup() {
@ -31,8 +36,17 @@ export default defineComponent({
message.success({ content: 'Loaded!', key, duration: 2 }); message.success({ content: 'Loaded!', key, duration: 2 });
}, 1000); }, 1000);
}; };
const content = ref('Loading...');
const openMessage2 = () => {
// content must use function
message.loading({ content: () => content.value });
setTimeout(() => {
content.value = 'Loaded!';
}, 1000);
};
return { return {
openMessage, openMessage,
openMessage2,
}; };
}, },
}); });

View File

@ -25,7 +25,7 @@ This components provides some static methods, with usage and arguments as follow
| Argument | Description | Type | Default | | Argument | Description | Type | Default |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| content | content of the message | string\| VNode | - | | content | content of the message | string\| VNode \| () => VNode | - |
| duration | time(seconds) before auto-dismiss, don't dismiss if set to 0 | number | 1.5 | | duration | time(seconds) before auto-dismiss, don't dismiss if set to 0 | number | 1.5 |
| onClose | Specify a function that will be called when the message is closed | Function | - | | onClose | Specify a function that will be called when the message is closed | Function | - |
@ -48,11 +48,15 @@ The properties of config are as follows:
| Property | Description | Type | Default | Version | | Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| content | content of the message | string\| VNode | - | | | class | Customized CSS class | string | - |
| content | content of the message | string\| VNode \| () => VNode | - | |
| duration | time(seconds) before auto-dismiss, don't dismiss if set to 0 | number | 3 | | | duration | time(seconds) before auto-dismiss, don't dismiss if set to 0 | number | 3 | |
| onClose | Specify a function that will be called when the message is closed | function | - | | | onClose | Specify a function that will be called when the message is closed | function | - | |
| icon | Customized Icon | VNode | - | | | icon | Customized Icon | VNode \| ()=> VNode | - | |
| key | The unique identifier of the Message | string\|number | - | | | key | The unique identifier of the Message | string\|number | - | |
| style | Customized inline style | CSSProperties | - | |
| onClick | Specify a function that will be called when the message is clicked | function | - | |
| onClose | Specify a function that will be called when the message is closed | function | - | |
### Global static methods ### Global static methods
@ -68,12 +72,16 @@ message.config({
top: '100px', top: '100px',
duration: 2, duration: 2,
maxCount: 3, maxCount: 3,
rtl: true,
prefixCls: 'my-message',
}); });
``` ```
| Argument | Description | Type | Default | | Argument | Description | Type | Default | Version |
| --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| duration | time before auto-dismiss, in seconds | number | 1.5 | | duration | time before auto-dismiss, in seconds | number | 1.5 | |
| getContainer | Return the mount node for Message | () => HTMLElement | () => document.body | | getContainer | Return the mount node for Message | () => HTMLElement | () => document.body | |
| maxCount | max message show, drop oldest if exceed limit | number | - | | maxCount | max message show, drop oldest if exceed limit | number | - | |
| top | distance from top | string | `24px` | | prefixCls | The prefix className of message node | string | `ant-message` | 3.0 |
| rtl | Whether to enable RTL mode | boolean | false | 3.0 |
| top | distance from top | string | `8px` |

View File

@ -5,18 +5,65 @@ import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFill
import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled'; import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled';
import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled'; import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled';
import InfoCircleFilled from '@ant-design/icons-vue/InfoCircleFilled'; import InfoCircleFilled from '@ant-design/icons-vue/InfoCircleFilled';
import type { VueNode } from '../_util/type'; import type { Key, VueNode } from '../_util/type';
import type { NotificationInstance } from '../vc-notification/Notification';
import classNames from '../_util/classNames';
let defaultDuration = 3; let defaultDuration = 3;
let defaultTop: string; let defaultTop: string;
let messageInstance: any; let messageInstance: NotificationInstance;
let key = 1; let key = 1;
let localPrefixCls = ''; let localPrefixCls = '';
let transitionName = 'move-up'; let transitionName = 'move-up';
let hasTransitionName = false;
let getContainer = () => document.body; let getContainer = () => document.body;
let maxCount: number; let maxCount: number;
let rtl = false;
function getMessageInstance(args: MessageArgsProps, callback: (i: any) => void) { export function getKeyThenIncreaseKey() {
return key++;
}
export interface ConfigOptions {
top?: string;
duration?: number;
prefixCls?: string;
getContainer?: () => HTMLElement;
transitionName?: string;
maxCount?: number;
rtl?: boolean;
}
function setMessageConfig(options: ConfigOptions) {
if (options.top !== undefined) {
defaultTop = options.top;
messageInstance = null; // delete messageInstance for new defaultTop
}
if (options.duration !== undefined) {
defaultDuration = options.duration;
}
if (options.prefixCls !== undefined) {
localPrefixCls = options.prefixCls;
}
if (options.getContainer !== undefined) {
getContainer = options.getContainer;
}
if (options.transitionName !== undefined) {
transitionName = options.transitionName;
messageInstance = null; // delete messageInstance for new transitionName
hasTransitionName = true;
}
if (options.maxCount !== undefined) {
maxCount = options.maxCount;
messageInstance = null;
}
if (options.rtl !== undefined) {
rtl = options.rtl;
}
}
function getMessageInstance(args: MessageArgsProps, callback: (i: NotificationInstance) => void) {
if (messageInstance) { if (messageInstance) {
callback(messageInstance); callback(messageInstance);
return; return;
@ -27,6 +74,7 @@ function getMessageInstance(args: MessageArgsProps, callback: (i: any) => void)
prefixCls: args.prefixCls || localPrefixCls, prefixCls: args.prefixCls || localPrefixCls,
rootPrefixCls: args.rootPrefixCls, rootPrefixCls: args.rootPrefixCls,
transitionName, transitionName,
hasTransitionName,
style: { top: defaultTop }, // style: { top: defaultTop }, //
getContainer, getContainer,
maxCount, maxCount,
@ -49,7 +97,7 @@ export interface ThenableArgument {
(val: any): void; (val: any): void;
} }
const iconMap = { const typeToIcon = {
info: InfoCircleFilled, info: InfoCircleFilled,
success: CheckCircleFilled, success: CheckCircleFilled,
error: CloseCircleFilled, error: CloseCircleFilled,
@ -57,16 +105,14 @@ const iconMap = {
loading: LoadingOutlined, loading: LoadingOutlined,
}; };
export interface MessageType { export interface MessageType extends PromiseLike<any> {
(): void; (): void;
then: (fill: ThenableArgument, reject: ThenableArgument) => Promise<void>;
promise: Promise<void>;
} }
export interface MessageArgsProps { export interface MessageArgsProps {
content: string | (() => VueNode) | VueNode; content: string | (() => VueNode) | VueNode;
duration: number | null; duration?: number;
type: NoticeType; type?: NoticeType;
prefixCls?: string; prefixCls?: string;
rootPrefixCls?: string; rootPrefixCls?: string;
onClose?: () => void; onClose?: () => void;
@ -75,12 +121,13 @@ export interface MessageArgsProps {
style?: CSSProperties; style?: CSSProperties;
class?: string; class?: string;
appContext?: any; appContext?: any;
onClick?: (e: MouseEvent) => void;
} }
function notice(args: MessageArgsProps): MessageType { function notice(args: MessageArgsProps): MessageType {
const duration = args.duration !== undefined ? args.duration : defaultDuration; const duration = args.duration !== undefined ? args.duration : defaultDuration;
const target = args.key || key++; const target = args.key || getKeyThenIncreaseKey();
const closePromise = new Promise(resolve => { const closePromise = new Promise(resolve => {
const callback = () => { const callback = () => {
if (typeof args.onClose === 'function') { if (typeof args.onClose === 'function') {
@ -95,18 +142,21 @@ function notice(args: MessageArgsProps): MessageType {
style: args.style || {}, style: args.style || {},
class: args.class, class: args.class,
content: ({ prefixCls }) => { content: ({ prefixCls }) => {
const Icon = iconMap[args.type]; const Icon = typeToIcon[args.type];
const iconNode = Icon ? <Icon /> : ''; const iconNode = Icon ? <Icon /> : '';
const messageClass = classNames(`${prefixCls}-custom-content`, {
[`${prefixCls}-${args.type}`]: args.type,
[`${prefixCls}-rtl`]: rtl === true,
});
return ( return (
<div <div class={messageClass}>
class={`${prefixCls}-custom-content${args.type ? ` ${prefixCls}-${args.type}` : ''}`} {typeof args.icon === 'function' ? args.icon() : args.icon || iconNode}
>
{typeof args.icon === 'function' ? args.icon : args.icon || iconNode}
<span>{typeof args.content === 'function' ? args.content() : args.content}</span> <span>{typeof args.content === 'function' ? args.content() : args.content}</span>
</div> </div>
); );
}, },
onClose: callback, onClose: callback,
onClick: args.onClick,
}); });
}); });
}); });
@ -121,7 +171,7 @@ function notice(args: MessageArgsProps): MessageType {
return result; return result;
} }
type ConfigDuration = number | (() => void); type ConfigDuration = number;
type JointContent = VueNode | MessageArgsProps; type JointContent = VueNode | MessageArgsProps;
export type ConfigOnClose = () => void; export type ConfigOnClose = () => void;
@ -132,73 +182,64 @@ function isArgsProps(content: JointContent): content is MessageArgsProps {
); );
} }
export interface ConfigOptions {
top?: string;
duration?: number;
prefixCls?: string;
getContainer?: () => HTMLElement;
transitionName?: string;
maxCount?: number;
}
const api: any = { const api: any = {
open: notice, open: notice,
config(options: ConfigOptions) { config: setMessageConfig,
if (options.top !== undefined) { destroy(messageKey?: Key) {
defaultTop = options.top;
messageInstance = null; // delete messageInstance for new defaultTop
}
if (options.duration !== undefined) {
defaultDuration = options.duration;
}
if (options.prefixCls !== undefined) {
localPrefixCls = options.prefixCls;
}
if (options.getContainer !== undefined) {
getContainer = options.getContainer;
}
if (options.transitionName !== undefined) {
transitionName = options.transitionName;
messageInstance = null; // delete messageInstance for new transitionName
}
if (options.maxCount !== undefined) {
maxCount = options.maxCount;
messageInstance = null;
}
},
destroy() {
if (messageInstance) { if (messageInstance) {
messageInstance.destroy(); if (messageKey) {
const { removeNotice } = messageInstance;
removeNotice(messageKey);
} else {
const { destroy } = messageInstance;
destroy();
messageInstance = null; messageInstance = null;
} }
}
}, },
}; };
['success', 'info', 'warning', 'error', 'loading'].forEach(type => { export function attachTypeApi(originalApi: MessageApi, type: NoticeType) {
api[type] = (content: JointContent, duration: ConfigDuration, onClose?: ConfigOnClose) => { originalApi[type] = (
content: JointContent,
duration?: ConfigDuration,
onClose?: ConfigOnClose,
) => {
if (isArgsProps(content)) { if (isArgsProps(content)) {
return api.open({ ...content, type }); return originalApi.open({ ...content, type });
} }
if (typeof duration === 'function') { if (typeof duration === 'function') {
onClose = duration; onClose = duration;
duration = undefined; duration = undefined;
} }
return api.open({ content, duration, type, onClose });
return originalApi.open({ content, duration, type, onClose });
}; };
}); }
(['success', 'info', 'warning', 'error', 'loading'] as NoticeType[]).forEach(type =>
attachTypeApi(api, type),
);
api.warn = api.warning; api.warn = api.warning;
export interface MessageApi { export interface MessageInstance {
info(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType; info(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
success(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType; success(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
error(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType; error(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
warn(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
warning(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType; warning(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
loading(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType; loading(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
open(args: MessageArgsProps): MessageType; open(args: MessageArgsProps): MessageType;
config(options: ConfigOptions): void;
destroy(): void;
} }
export interface MessageApi extends MessageInstance {
warn(content: JointContent, duration?: ConfigDuration, onClose?: ConfigOnClose): MessageType;
config(options: ConfigOptions): void;
destroy(messageKey?: Key): void;
}
/** @private test Only function. Not work on production */
export const getInstance = () => (process.env.NODE_ENV === 'test' ? messageInstance : null);
export default api as MessageApi; export default api as MessageApi;

View File

@ -25,8 +25,8 @@ cover: https://gw.alipayobjects.com/zos/alicdn/hAkKTIW0K/Message.svg
- `message.loading(content, [duration], onClose)` - `message.loading(content, [duration], onClose)`
| 参数 | 说明 | 类型 | 默认值 | | 参数 | 说明 | 类型 | 默认值 |
| -------- | --------------------------------------------- | -------------- | ------ | | --- | --- | --- | --- |
| content | 提示内容 | string\| VNode | - | | content | 提示内容 | string\| VNode \| () => VNode | - |
| duration | 自动关闭的延时,单位秒。设为 0 时不自动关闭。 | number | 3 | | duration | 自动关闭的延时,单位秒。设为 0 时不自动关闭。 | number | 3 |
| onClose | 关闭时触发的回调函数 | Function | - | | onClose | 关闭时触发的回调函数 | Function | - |
@ -47,13 +47,19 @@ cover: https://gw.alipayobjects.com/zos/alicdn/hAkKTIW0K/Message.svg
- `message.warn(config)` // alias of warning - `message.warn(config)` // alias of warning
- `message.loading(config)` - `message.loading(config)`
`config` 对象属性如下:
| 参数 | 说明 | 类型 | 默认值 | 版本 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| -------- | --------------------------------------------- | -------------- | ------ | ---- | | --- | --- | --- | --- | --- |
| content | 提示内容 | string\| VNode | - | | | class | 自定义 CSS class | string | - | |
| content | 提示内容 | string\| VNode \| ()=> VNode | - | |
| duration | 自动关闭的延时,单位秒。设为 0 时不自动关闭。 | number | 3 | | | duration | 自动关闭的延时,单位秒。设为 0 时不自动关闭。 | number | 3 | |
| onClose | 关闭时触发的回调函数 | Function | - | | | onClose | 关闭时触发的回调函数 | Function | - | |
| icon | 自定义图标 | VNode | - | | | icon | 自定义图标 | VNode \| () => VNode | - | |
| key | 当前提示的唯一标志 | string \| number | - | | | key | 当前提示的唯一标志 | string \| number | - | |
| style | 自定义内联样式 | CSSProperties | - | |
| onClick | 点击 message 时触发的回调函数 | function | - | |
| onClose | 关闭时触发的回调函数 | function | - | |
### 全局方法 ### 全局方法
@ -69,12 +75,16 @@ message.config({
top: `100px`, top: `100px`,
duration: 2, duration: 2,
maxCount: 3, maxCount: 3,
rtl: true,
prefixCls: 'my-message',
}); });
``` ```
| 参数 | 说明 | 类型 | 默认值 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| duration | 默认自动关闭延时,单位秒 | number | 3 | | duration | 默认自动关闭延时,单位秒 | number | 3 | |
| getContainer | 配置渲染节点的输出位置 | () => HTMLElement | () => document.body | | getContainer | 配置渲染节点的输出位置 | () => HTMLElement | () => document.body | |
| maxCount | 最大显示数, 超过限制时,最早的消息会被自动关闭 | number | - | | maxCount | 最大显示数, 超过限制时,最早的消息会被自动关闭 | number | - | |
| top | 消息距离顶部的位置 | string | `24px` | | prefixCls | 消息节点的 className 前缀 | string | `ant-message` | 3.0 |
| rtl | 是否开启 RTL 模式 | boolean | false | | |
| top | 消息距离顶部的位置 | string | `8px` | |

View File

@ -7,7 +7,7 @@
.reset-component(); .reset-component();
position: fixed; position: fixed;
top: 16px; top: 8px;
left: 0; left: 0;
z-index: @zindex-message; z-index: @zindex-message;
width: 100%; width: 100%;
@ -16,9 +16,6 @@
&-notice { &-notice {
padding: 8px; padding: 8px;
text-align: center; text-align: center;
&:first-child {
margin-top: -8px;
}
} }
&-notice-content { &-notice-content {
@ -54,8 +51,7 @@
font-size: @font-size-lg; font-size: @font-size-lg;
} }
&-notice.move-up-leave.move-up-leave-active { &-notice.@{ant-prefix}-move-up-leave.@{ant-prefix}-move-up-leave-active {
overflow: hidden;
animation-name: MessageMoveOut; animation-name: MessageMoveOut;
animation-duration: 0.3s; animation-duration: 0.3s;
} }
@ -67,9 +63,12 @@
padding: 8px; padding: 8px;
opacity: 1; opacity: 1;
} }
100% { 100% {
max-height: 0; max-height: 0;
padding: 0; padding: 0;
opacity: 0; opacity: 0;
} }
} }
@import './rtl';

View File

@ -0,0 +1,17 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@message-prefix-cls: ~'@{ant-prefix}-message';
.@{message-prefix-cls}-rtl {
direction: rtl;
span {
direction: rtl;
}
.@{iconfont-css-prefix} {
margin-right: 0;
margin-left: 8px;
}
}

View File

@ -42,7 +42,12 @@ exports[`renders ./components/notification/demo/placement.vue correctly 1`] = `
exports[`renders ./components/notification/demo/update.vue correctly 1`] = ` exports[`renders ./components/notification/demo/update.vue correctly 1`] = `
<button class="ant-btn ant-btn-primary" type="button"> <button class="ant-btn ant-btn-primary" type="button">
<!----><span>Open the notification box</span> <!----><span>Open the notification box (update by key)</span>
</button>
<br>
<br>
<button class="ant-btn ant-btn-primary" type="button">
<!----><span>Open the notification box (update by reactive)</span>
</button> </button>
`; `;

View File

@ -1,13 +1,15 @@
import { asyncExpect } from '../../../tests/utils'; import { asyncExpect } from '../../../tests/utils';
import notification from '..'; import notification, { getInstance } from '..';
import { StepBackwardOutlined } from '@ant-design/icons-vue'; import { StepBackwardOutlined } from '@ant-design/icons-vue';
describe('notification', () => { describe('notification', () => {
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers();
document.body.outerHTML = ''; document.body.outerHTML = '';
}); });
afterEach(() => { afterEach(() => {
jest.useRealTimers();
notification.destroy(); notification.destroy();
}); });
@ -24,17 +26,18 @@ describe('notification', () => {
key: '2', key: '2',
}); });
}); });
await asyncExpect(() => { await Promise.resolve();
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(2); expect(document.querySelectorAll('.ant-notification-notice').length).toBe(2);
notification.close('1'); notification.close('1');
}, 0); jest.runAllTimers();
await asyncExpect(() => { expect(
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(1); (await getInstance('ant-notification-topRight-false')).component.value.notices,
).toHaveLength(1);
notification.close('2'); notification.close('2');
}, 0); jest.runAllTimers();
await asyncExpect(() => { expect(
expect(document.querySelectorAll('.ant-notification-notice').length).toBe(0); (await getInstance('ant-notification-topRight-false')).component.value.notices,
}, 0); ).toHaveLength(0);
}); });
it('should be able to destroy globally', async () => { it('should be able to destroy globally', async () => {

View File

@ -31,7 +31,7 @@ export default defineComponent({
message: 'Notification Title', message: 'Notification Title',
description: description:
'This is the content of the notification. This is the content of the notification. This is the content of the notification.', 'This is the content of the notification. This is the content of the notification. This is the content of the notification.',
icon: h(SmileOutlined, { style: 'color: #108ee9' }), icon: () => h(SmileOutlined, { style: 'color: #108ee9' }),
}); });
}; };

View File

@ -8,11 +8,11 @@ title:
## zh-CN ## zh-CN
使用 style className 来定义样式 使用 `style` `class` 来定义样式
## en-US ## en-US
The style and className are available to customize Notification. The `style` and `class` are available to customize Notification.
</docs> </docs>
@ -33,6 +33,7 @@ export default defineComponent({
width: '600px', width: '600px',
marginLeft: `${335 - 600}px`, marginLeft: `${335 - 600}px`,
}, },
class: 'notification-custom-class',
}); });
}; };
return { return {
@ -41,3 +42,8 @@ export default defineComponent({
}, },
}); });
</script> </script>
<style>
.notification-custom-class {
color: red;
}
</style>

View File

@ -46,6 +46,7 @@ import {
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { notification } from 'ant-design-vue'; import { notification } from 'ant-design-vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import type { NotificationPlacement } from 'ant-design-vue';
export default defineComponent({ export default defineComponent({
components: { components: {
RadiusUpleftOutlined, RadiusUpleftOutlined,
@ -54,7 +55,7 @@ export default defineComponent({
RadiusBottomrightOutlined, RadiusBottomrightOutlined,
}, },
setup() { setup() {
const openNotification = (placement: string) => { const openNotification = (placement: NotificationPlacement) => {
notification.open({ notification.open({
message: `Notification ${placement}`, message: `Notification ${placement}`,
description: description:

View File

@ -8,20 +8,27 @@ title:
## zh-CN ## zh-CN
可以通过唯一的 key 来更新内容 可以通过唯一的 key 来更新内容, 或者通过响应式数据更新
## en-US ## en-US
Update content with unique key. Update content with unique key, or use reactive data.
</docs> </docs>
<template> <template>
<a-button type="primary" @click="openNotification">Open the notification box</a-button> <a-button type="primary" @click="openNotification">
Open the notification box (update by key)
</a-button>
<br />
<br />
<a-button type="primary" @click="openNotification2">
Open the notification box (update by reactive)
</a-button>
</template> </template>
<script lang="ts"> <script lang="ts">
import { notification } from 'ant-design-vue'; import { notification } from 'ant-design-vue';
import { defineComponent } from 'vue'; import { defineComponent, ref } from 'vue';
const key = 'updatable'; const key = 'updatable';
export default defineComponent({ export default defineComponent({
setup() { setup() {
@ -39,8 +46,22 @@ export default defineComponent({
}); });
}, 1000); }, 1000);
}; };
const message = ref('Notification Title');
const description = ref('description');
const openNotification2 = () => {
// content must use function
notification.open({
message: () => message.value,
description: () => description.value,
});
setTimeout(() => {
message.value = 'New Title';
description.value = 'New description.';
}, 1000);
};
return { return {
openNotification, openNotification,
openNotification2,
}; };
}, },
}); });

View File

@ -31,30 +31,35 @@ The properties of config are as follows:
| Property | Description | Type | Default | Version | | Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| bottom | Distance from the bottom of the viewport, when `placement` is `bottomRight` or `bottomLeft` (unit: pixels). | string | `24px` | | | bottom | Distance from the bottom of the viewport, when `placement` is `bottomRight` or `bottomLeft` (unit: pixels). | string | `24px` | |
| btn | Customized close button | VNode | - | | | btn | Customized close button | VNode \| () => VNode | - | |
| class | Customized CSS class | string | - | | | class | Customized CSS class | string | - | |
| description | The content of notification box (required) | string\| VNode | - | | | description | The content of notification box (required) | string\| VNode \| () => VNode | - | |
| duration | Time in seconds before Notification is closed. When set to 0 or null, it will never be closed automatically | number | 4.5 | | | duration | Time in seconds before Notification is closed. When set to 0 or null, it will never be closed automatically | number | 4.5 | |
| getContainer | Return the mount node for Notification | () => HTMLNode | () => document.body | | | getContainer | Return the mount node for Notification | () => HTMLNode | () => document.body | |
| icon | Customized icon | VNode | - | | | icon | Customized icon | VNode \| () => VNode | - | |
| key | The unique identifier of the Notification | string | - | | | key | The unique identifier of the Notification | string | - | |
| message | The title of notification box (required) | string\|VNode | - | | | message | The title of notification box (required) | string\| VNode \| () => VNode | - | |
| placement | Position of Notification, can be one of `topLeft` `topRight` `bottomLeft` `bottomRight` | string | `topRight` | | | placement | Position of Notification, can be one of `topLeft` `topRight` `bottomLeft` `bottomRight` | string | `topRight` | |
| style | Customized inline style | Object \| string | - | | | style | Customized inline style | Object \| string | - | |
| onClose | Specify a function that will be called when the close button is clicked | Function | - | | | onClose | Specify a function that will be called when the close button is clicked | Function | - | |
| onClick | Specify a function that will be called when the notification is clicked | Function | - | | | onClick | Specify a function that will be called when the notification is clicked | Function | - | |
| top | Distance from the top of the viewport, when `placement` is `topRight` or `topLeft` (unit: pixels). | string | `24px` | | | top | Distance from the top of the viewport, when `placement` is `topRight` or `topLeft` (unit: pixels). | string | `24px` | |
| closeIcon | custom close icon | VNode | - | | | closeIcon | custom close icon | VNode \| () => VNode | - | |
`notification` also provides a global `config()` method that can be used for specifying the default options. Once this method is used, all the notification boxes will take into account these globally defined options when displaying. `notification` also provides a global `config()` method that can be used for specifying the default options. Once this method is used, all the notification boxes will take into account these globally defined options when displaying.
- `notification.config(options)` - `notification.config(options)`
> When you use `ConfigProvider` for global configuration, the system will automatically start RTL mode by default.(4.3.0+)
>
> When you want to use it alone, you can start the RTL mode through the following settings.
```js ```js
notification.config({ notification.config({
placement: 'bottomRight', placement: 'bottomRight',
bottom: '50px', bottom: '50px',
duration: 3, duration: 3,
rtl: true,
}); });
``` ```
@ -64,5 +69,7 @@ notification.config({
| duration | Time in seconds before Notification is closed. When set to 0 or null, it will never be closed automatically | number | 4.5 | | | duration | Time in seconds before Notification is closed. When set to 0 or null, it will never be closed automatically | number | 4.5 | |
| getContainer | Return the mount node for Notification | () => HTMLNode | () => document.body | | | getContainer | Return the mount node for Notification | () => HTMLNode | () => document.body | |
| placement | Position of Notification, can be one of `topLeft` `topRight` `bottomLeft` `bottomRight` | string | `topRight` | | | placement | Position of Notification, can be one of `topLeft` `topRight` `bottomLeft` `bottomRight` | string | `topRight` | |
| rtl | Whether to enable RTL mode | boolean | false | |
| top | Distance from the top of the viewport, when `placement` is `topRight` or `topLeft` (unit: pixels). | string | `24px` | | | top | Distance from the top of the viewport, when `placement` is `topRight` or `topLeft` (unit: pixels). | string | `24px` | |
| closeIcon | custom close icon | VNode | - | | | closeIcon | custom close icon | VNode \| () => VNode | - | |
| maxCount | Max Notification show, drop oldest if exceed limit | number | - | 3.0 |

View File

@ -8,6 +8,8 @@ import CloseOutlined from '@ant-design/icons-vue/CloseOutlined';
import type { VueNode } from '../_util/type'; import type { VueNode } from '../_util/type';
import { renderHelper } from '../_util/util'; import { renderHelper } from '../_util/util';
import { globalConfig } from '../config-provider'; import { globalConfig } from '../config-provider';
import type { NotificationInstance as VCNotificationInstance } from '../vc-notification/Notification';
import classNames from '../_util/classNames';
export type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'; export type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
@ -21,9 +23,11 @@ export interface ConfigProps {
placement?: NotificationPlacement; placement?: NotificationPlacement;
getContainer?: () => HTMLElement; getContainer?: () => HTMLElement;
closeIcon?: VueNode | (() => VueNode); closeIcon?: VueNode | (() => VueNode);
rtl?: boolean;
maxCount?: number;
} }
const notificationInstance: { [key: string]: any } = {}; const notificationInstance: { [key: string]: VCNotificationInstance } = {};
let defaultDuration = 4.5; let defaultDuration = 4.5;
let defaultTop = '24px'; let defaultTop = '24px';
let defaultBottom = '24px'; let defaultBottom = '24px';
@ -31,6 +35,8 @@ let defaultPrefixCls = '';
let defaultPlacement: NotificationPlacement = 'topRight'; let defaultPlacement: NotificationPlacement = 'topRight';
let defaultGetContainer = () => document.body; let defaultGetContainer = () => document.body;
let defaultCloseIcon = null; let defaultCloseIcon = null;
let rtl = false;
let maxCount: number;
function setNotificationConfig(options: ConfigProps) { function setNotificationConfig(options: ConfigProps) {
const { duration, placement, bottom, top, getContainer, closeIcon, prefixCls } = options; const { duration, placement, bottom, top, getContainer, closeIcon, prefixCls } = options;
@ -55,6 +61,12 @@ function setNotificationConfig(options: ConfigProps) {
if (closeIcon !== undefined) { if (closeIcon !== undefined) {
defaultCloseIcon = closeIcon; defaultCloseIcon = closeIcon;
} }
if (options.rtl !== undefined) {
rtl = options.rtl;
}
if (options.maxCount !== undefined) {
maxCount = options.maxCount;
}
} }
function getPlacementStyle( function getPlacementStyle(
@ -62,7 +74,7 @@ function getPlacementStyle(
top: string = defaultTop, top: string = defaultTop,
bottom: string = defaultBottom, bottom: string = defaultBottom,
) { ) {
let style; let style: CSSProperties;
switch (placement) { switch (placement) {
case 'topLeft': case 'topLeft':
style = { style = {
@ -106,20 +118,28 @@ function getNotificationInstance(
closeIcon = defaultCloseIcon, closeIcon = defaultCloseIcon,
appContext, appContext,
}: NotificationArgsProps, }: NotificationArgsProps,
callback: (n: any) => void, callback: (n: VCNotificationInstance) => void,
) { ) {
const { getPrefixCls } = globalConfig(); const { getPrefixCls } = globalConfig();
const prefixCls = getPrefixCls('notification', customizePrefixCls || defaultPrefixCls); const prefixCls = getPrefixCls('notification', customizePrefixCls || defaultPrefixCls);
const cacheKey = `${prefixCls}-${placement}`; const cacheKey = `${prefixCls}-${placement}-${rtl}`;
if (notificationInstance[cacheKey]) { const cacheInstance = notificationInstance[cacheKey];
callback(notificationInstance[cacheKey]); if (cacheInstance) {
Promise.resolve(cacheInstance).then(instance => {
callback(instance);
});
return; return;
} }
const notificationClass = classNames(`${prefixCls}-${placement}`, {
[`${prefixCls}-rtl`]: rtl === true,
});
Notification.newInstance( Notification.newInstance(
{ {
name: 'notification', name: 'notification',
prefixCls: customizePrefixCls || defaultPrefixCls, prefixCls: customizePrefixCls || defaultPrefixCls,
class: `${prefixCls}-${placement}`, class: notificationClass,
style: getPlacementStyle(placement, top, bottom), style: getPlacementStyle(placement, top, bottom),
appContext, appContext,
getContainer, getContainer,
@ -131,6 +151,8 @@ function getNotificationInstance(
); );
return closeIconToRender; return closeIconToRender;
}, },
maxCount,
hasTransitionName: true,
}, },
(notification: any) => { (notification: any) => {
notificationInstance[cacheKey] = notification; notificationInstance[cacheKey] = notification;
@ -206,25 +228,26 @@ function notice(args: NotificationArgsProps) {
}); });
} }
const apiBase = { const api: any = {
open: notice, open: notice,
close(key: string) { close(key: string) {
Object.keys(notificationInstance).forEach(cacheKey => Object.keys(notificationInstance).forEach(cacheKey =>
notificationInstance[cacheKey].removeNotice(key), Promise.resolve(notificationInstance[cacheKey]).then(instance => {
instance.removeNotice(key);
}),
); );
}, },
config: setNotificationConfig, config: setNotificationConfig,
destroy() { destroy() {
Object.keys(notificationInstance).forEach(cacheKey => { Object.keys(notificationInstance).forEach(cacheKey => {
notificationInstance[cacheKey].destroy(); Promise.resolve(notificationInstance[cacheKey]).then(instance => {
delete notificationInstance[cacheKey]; instance.destroy();
});
delete notificationInstance[cacheKey]; // lgtm[js/missing-await]
}); });
}, },
}; };
type NotificationApi = typeof apiBase &
Record<IconType | 'warn', (args: Omit<NotificationArgsProps, 'type'>) => void>;
const api = apiBase as any as NotificationApi;
const iconTypes: IconType[] = ['success', 'info', 'warning', 'error']; const iconTypes: IconType[] = ['success', 'info', 'warning', 'error'];
iconTypes.forEach(type => { iconTypes.forEach(type => {
api[type] = args => api[type] = args =>
@ -235,4 +258,24 @@ iconTypes.forEach(type => {
}); });
api.warn = api.warning; api.warn = api.warning;
export default api;
export interface NotificationInstance {
success(args: NotificationArgsProps): void;
error(args: NotificationArgsProps): void;
info(args: NotificationArgsProps): void;
warning(args: NotificationArgsProps): void;
open(args: NotificationArgsProps): void;
}
export interface NotificationApi extends NotificationInstance {
warn(args: NotificationArgsProps): void;
close(key: string): void;
config(options: ConfigProps): void;
destroy(): void;
}
/** @private test Only function. Not work on production */
export const getInstance = async (cacheKey: string) =>
process.env.NODE_ENV === 'test' ? notificationInstance[cacheKey] : null;
export default api as NotificationApi;

View File

@ -31,31 +31,36 @@ config 参数如下:
| 参数 | 说明 | 类型 | 默认值 | 版本 | | 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| btn | 自定义关闭按钮 | VNode | - | | | btn | 自定义关闭按钮 | VNode \| () => VNode | - | |
| bottom | 消息从底部弹出时,距离底部的位置,单位像素。 | string | `24px` | | | bottom | 消息从底部弹出时,距离底部的位置,单位像素。 | string | `24px` | |
| class | 自定义 CSS class | string | - | | | class | 自定义 CSS class | string | - | |
| description | 通知提醒内容,必选 | string \|VNode | - | | | description | 通知提醒内容,必选 | string \| VNode \| () => VNode | - | |
| duration | 默认 4.5 秒后自动关闭,配置为 null 则不自动关闭 | number | 4.5 | | | duration | 默认 4.5 秒后自动关闭,配置为 null 则不自动关闭 | number | 4.5 | |
| getContainer | 配置渲染节点的输出位置 | () => HTMLNode | () => document.body | | | getContainer | 配置渲染节点的输出位置 | () => HTMLNode | () => document.body | |
| icon | 自定义图标 | VNode | - | | | icon | 自定义图标 | VNode \| () => VNode | - | |
| key | 当前通知唯一标志 | string | - | | | key | 当前通知唯一标志 | string | - | |
| message | 通知提醒标题,必选 | string \|VNode | - | | | message | 通知提醒标题,必选 | string \| VNode \| () => VNode | - | |
| placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | topRight | | | placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | topRight | |
| style | 自定义内联样式 | Object \| string | - | | | style | 自定义内联样式 | Object \| string | - | |
| onClose | 点击默认关闭按钮时触发的回调函数 | Function | - | | | onClose | 点击默认关闭按钮时触发的回调函数 | Function | - | |
| onClick | 点击通知时触发的回调函数 | Function | - | | | onClick | 点击通知时触发的回调函数 | Function | - | |
| top | 消息从顶部弹出时,距离顶部的位置,单位像素。 | string | `24px` | | | top | 消息从顶部弹出时,距离顶部的位置,单位像素。 | string | `24px` | |
| closeIcon | 自定义关闭图标 | VNode | - | | | closeIcon | 自定义关闭图标 | VNode \| () => VNode | - | |
还提供了一个全局配置方法,在调用前提前配置,全局一次生效。 还提供了一个全局配置方法,在调用前提前配置,全局一次生效。
- `notification.config(options)` - `notification.config(options)`
> 当你使用 `ConfigProvider` 进行全局化配置时,系统会默认自动开启 RTL 模式。(3.0+)
>
> 当你想单独使用,可通过如下设置开启 RTL 模式。
```js ```js
notification.config({ notification.config({
placement: 'bottomRight', placement: 'bottomRight',
bottom: '50px', bottom: '50px',
duration: 3, duration: 3,
rtl: true,
}); });
``` ```
@ -66,4 +71,6 @@ notification.config({
| getContainer | 配置渲染节点的输出位置 | () => HTMLNode | () => document.body | | | getContainer | 配置渲染节点的输出位置 | () => HTMLNode | () => document.body | |
| placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | topRight | | | placement | 弹出位置,可选 `topLeft` `topRight` `bottomLeft` `bottomRight` | string | topRight | |
| top | 消息从顶部弹出时,距离顶部的位置,单位像素。 | string | `24px` | | | top | 消息从顶部弹出时,距离顶部的位置,单位像素。 | string | `24px` | |
| closeIcon | 自定义关闭图标 | VNode | - | | | closeIcon | 自定义关闭图标 | VNode \| () => VNode | - | |
| rtl | 是否开启 RTL 模式 | boolean | false | 3.0 |
| maxCount | 最大显示数, 超过限制时,最早的消息会被自动关闭 | number | - | 3.0 |

View File

@ -1,26 +1,25 @@
@import '../../style/themes/index'; @import '../../style/themes/index';
@import '../../style/mixins/index'; @import '../../style/mixins/index';
.popover-customize-bg(@notification-prefix-cls, @popover-background);
@notification-prefix-cls: ~'@{ant-prefix}-notification'; @notification-prefix-cls: ~'@{ant-prefix}-notification';
@notification-width: 384px; @notification-width: 384px;
@notification-padding-vertical: 16px;
@notification-padding-horizontal: 24px;
@notification-padding: @notification-padding-vertical @notification-padding-horizontal; @notification-padding: @notification-padding-vertical @notification-padding-horizontal;
@notification-margin-bottom: 16px; @notification-margin-bottom: 16px;
@notification-margin-edge: 24px;
.@{notification-prefix-cls} { .@{notification-prefix-cls} {
.reset-component(); .reset-component();
position: fixed; position: fixed;
z-index: @zindex-notification; z-index: @zindex-notification;
width: @notification-width; margin-right: @notification-margin-edge;
max-width: ~'calc(100vw - 32px)';
margin-right: 24px;
&-topLeft, &-topLeft,
&-bottomLeft { &-bottomLeft {
margin-right: 0; margin-right: 0;
margin-left: 24px; margin-left: @notification-margin-edge;
.@{notification-prefix-cls}-fade-enter.@{notification-prefix-cls}-fade-enter-active, .@{notification-prefix-cls}-fade-enter.@{notification-prefix-cls}-fade-enter-active,
.@{notification-prefix-cls}-fade-appear.@{notification-prefix-cls}-fade-appear-active { .@{notification-prefix-cls}-fade-appear.@{notification-prefix-cls}-fade-appear-active {
@ -33,18 +32,31 @@
cursor: pointer; cursor: pointer;
} }
&-hook-holder {
position: relative;
}
&-notice { &-notice {
position: relative; position: relative;
width: @notification-width;
max-width: ~'calc(100vw - @{notification-margin-edge} * 2)';
margin-bottom: @notification-margin-bottom; margin-bottom: @notification-margin-bottom;
margin-left: auto;
padding: @notification-padding; padding: @notification-padding;
overflow: hidden; overflow: hidden;
line-height: 1.5; line-height: @line-height-base;
word-wrap: break-word;
background: @notification-bg; background: @notification-bg;
border-radius: @border-radius-base; border-radius: @border-radius-base;
box-shadow: @shadow-2; box-shadow: @shadow-2;
.@{notification-prefix-cls}-topLeft &,
.@{notification-prefix-cls}-bottomLeft & {
margin-right: auto;
margin-left: 0;
}
&-message { &-message {
display: inline-block;
margin-bottom: 8px; margin-bottom: 8px;
color: @heading-color; color: @heading-color;
font-size: @font-size-lg; font-size: @font-size-lg;
@ -57,6 +69,7 @@
max-width: 4px; max-width: 4px;
background-color: transparent; background-color: transparent;
pointer-events: none; pointer-events: none;
&::before { &::before {
display: block; display: block;
content: ''; content: '';
@ -97,12 +110,15 @@
&-success { &-success {
color: @success-color; color: @success-color;
} }
&-info { &-info {
color: @info-color; color: @info-color;
} }
&-warning { &-warning {
color: @warning-color; color: @warning-color;
} }
&-error { &-error {
color: @error-color; color: @error-color;
} }
@ -116,9 +132,14 @@
outline: none; outline: none;
&:hover { &:hover {
& when (@theme = dark) {
color: fade(@white, 85%);
}
& when not (@theme = dark) {
color: shade(@text-color-secondary, 40%); color: shade(@text-color-secondary, 40%);
} }
} }
}
&-btn { &-btn {
float: right; float: right;
@ -134,9 +155,9 @@
&-fade-enter, &-fade-enter,
&-fade-appear { &-fade-appear {
opacity: 0;
.notification-fade-effect(); .notification-fade-effect();
opacity: 0;
animation-play-state: paused; animation-play-state: paused;
} }
@ -164,6 +185,7 @@
left: @notification-width; left: @notification-width;
opacity: 0; opacity: 0;
} }
100% { 100% {
left: 0; left: 0;
opacity: 1; opacity: 1;
@ -175,6 +197,7 @@
right: @notification-width; right: @notification-width;
opacity: 0; opacity: 0;
} }
100% { 100% {
right: 0; right: 0;
opacity: 1; opacity: 1;
@ -185,10 +208,9 @@
0% { 0% {
max-height: 150px; max-height: 150px;
margin-bottom: @notification-margin-bottom; margin-bottom: @notification-margin-bottom;
padding-top: @notification-padding;
padding-bottom: @notification-padding;
opacity: 1; opacity: 1;
} }
100% { 100% {
max-height: 0; max-height: 0;
margin-bottom: 0; margin-bottom: 0;
@ -197,3 +219,5 @@
opacity: 0; opacity: 0;
} }
} }
@import './rtl';

View File

@ -0,0 +1,53 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@notification-prefix-cls: ~'@{ant-prefix}-notification';
.@{notification-prefix-cls} {
&-rtl {
direction: rtl;
}
&-notice {
&-closable &-message {
.@{notification-prefix-cls}-rtl & {
padding-right: 0;
padding-left: 24px;
}
}
&-with-icon &-message {
.@{notification-prefix-cls}-rtl & {
margin-right: 48px;
margin-left: 0;
}
}
&-with-icon &-description {
.@{notification-prefix-cls}-rtl & {
margin-right: 48px;
margin-left: 0;
}
}
&-icon {
.@{notification-prefix-cls}-rtl & {
margin-right: 4px;
margin-left: 0;
}
}
&-close {
.@{notification-prefix-cls}-rtl & {
right: auto;
left: 22px;
}
}
&-btn {
.@{notification-prefix-cls}-rtl & {
float: left;
}
}
}
}

View File

@ -1,88 +0,0 @@
import PropTypes from '../_util/vue-types';
import { getComponent, getSlot } from '../_util/props-util';
import BaseMixin from '../_util/BaseMixin';
export default {
mixins: [BaseMixin],
props: {
duration: PropTypes.number.def(1.5),
closable: PropTypes.looseBool,
prefixCls: PropTypes.string,
update: PropTypes.looseBool,
closeIcon: PropTypes.any,
onClose: PropTypes.func,
},
watch: {
duration() {
this.restartCloseTimer();
},
},
mounted() {
this.startCloseTimer();
},
updated() {
if (this.update) {
this.restartCloseTimer();
}
},
beforeUnmount() {
this.clearCloseTimer();
this.willDestroy = true; // beforeUnmountonMouseleave
},
methods: {
close(e) {
if (e) {
e.stopPropagation();
}
this.clearCloseTimer();
this.__emit('close');
},
startCloseTimer() {
this.clearCloseTimer();
if (!this.willDestroy && this.duration) {
this.closeTimer = setTimeout(() => {
this.close();
}, this.duration * 1000);
}
},
clearCloseTimer() {
if (this.closeTimer) {
clearTimeout(this.closeTimer);
this.closeTimer = null;
}
},
restartCloseTimer() {
this.clearCloseTimer();
this.startCloseTimer();
},
},
render() {
const { prefixCls, closable, clearCloseTimer, startCloseTimer, close, $attrs } = this;
const componentClass = `${prefixCls}-notice`;
const className = {
[`${componentClass}`]: 1,
[`${componentClass}-closable`]: closable,
};
const closeIcon = getComponent(this, 'closeIcon');
return (
<div
class={className}
style={$attrs.style || { right: '50%' }}
onMouseenter={clearCloseTimer}
onMouseleave={startCloseTimer}
>
<div class={`${componentClass}-content`}>{getSlot(this)}</div>
{closable ? (
<a tabindex="0" onClick={close} class={`${componentClass}-close`}>
{closeIcon || <span class={`${componentClass}-close-x`} />}
</a>
) : null}
</div>
);
},
};

View File

@ -0,0 +1,138 @@
import type { Key } from '../_util/type';
import { Teleport, computed, defineComponent, onMounted, watch, onUnmounted } from 'vue';
import type { HTMLAttributes } from 'vue';
import type { MouseEventHandler } from '../_util/EventInterface';
import classNames from '../_util/classNames';
interface DivProps extends HTMLAttributes {
// Ideally we would allow all data-* props but this would depend on https://github.com/microsoft/TypeScript/issues/28960
'data-testid'?: string;
}
export interface NoticeProps {
prefixCls: string;
duration?: number | null;
updateMark?: string;
/** Mark as final key since set maxCount may keep the key but user pass key is different */
noticeKey: Key;
closeIcon?: any;
closable?: boolean;
props?: DivProps;
onClick?: MouseEventHandler;
onClose?: (key: Key) => void;
/** @private Only for internal usage. We don't promise that we will refactor this */
holder?: HTMLDivElement;
/** @private Provided by CSSMotionList */
visible?: boolean;
}
export default defineComponent<NoticeProps>({
name: 'Notice',
inheritAttrs: false,
props: [
'prefixCls',
'duration',
'updateMark',
'noticeKey',
'closeIcon',
'closable',
'props',
'onClick',
'onClose',
'holder',
'visible',
] as any,
setup(props, { attrs, slots }) {
let closeTimer: any;
const duration = computed(() => (props.duration === undefined ? 1.5 : props.duration));
const startCloseTimer = () => {
if (duration.value) {
closeTimer = setTimeout(() => {
close();
}, duration.value * 1000);
}
};
const clearCloseTimer = () => {
if (closeTimer) {
clearTimeout(closeTimer);
closeTimer = null;
}
};
const close = (e?: MouseEvent) => {
if (e) {
e.stopPropagation();
}
clearCloseTimer();
const { onClose, noticeKey } = props;
if (onClose) {
onClose(noticeKey);
}
};
const restartCloseTimer = () => {
clearCloseTimer();
startCloseTimer();
};
onMounted(() => {
startCloseTimer();
});
onUnmounted(() => {
clearCloseTimer();
});
watch(
[duration, () => props.updateMark, () => props.visible],
([preDuration, preUpdateMark, preVisible], [newDuration, newUpdateMark, newVisible]) => {
if (
preDuration !== newDuration ||
preUpdateMark !== newUpdateMark ||
(preVisible !== newVisible && newVisible)
) {
restartCloseTimer();
}
},
{ flush: 'post' },
);
return () => {
const { prefixCls, closable, closeIcon = slots.closeIcon?.(), onClick, holder } = props;
const { class: className, style } = attrs;
const componentClass = `${prefixCls}-notice`;
const dataOrAriaAttributeProps = Object.keys(attrs).reduce(
(acc: Record<string, string>, key: string) => {
if (key.substr(0, 5) === 'data-' || key.substr(0, 5) === 'aria-' || key === 'role') {
acc[key] = (attrs as any)[key];
}
return acc;
},
{},
);
const node = (
<div
class={classNames(componentClass, className, {
[`${componentClass}-closable`]: closable,
})}
style={style}
onMouseenter={clearCloseTimer}
onMouseleave={startCloseTimer}
onClick={onClick}
{...dataOrAriaAttributeProps}
>
<div class={`${componentClass}-content`}>{slots.default?.()}</div>
{closable ? (
<a tabindex={0} onClick={close} class={`${componentClass}-close`}>
{closeIcon || <span class={`${componentClass}-close-x`} />}
</a>
) : null}
</div>
);
if (holder) {
return <Teleport to={holder} v-slots={{ default: () => node }}></Teleport>;
}
return node;
};
},
});

View File

@ -1,177 +0,0 @@
import { defineComponent, createVNode, render as vueRender, onMounted, ref } from 'vue';
import PropTypes from '../_util/vue-types';
import { getComponent } from '../_util/props-util';
import BaseMixin from '../_util/BaseMixin';
import createChainedFunction from '../_util/createChainedFunction';
import Notice from './Notice';
import { getTransitionGroupProps, TransitionGroup } from '../_util/transition';
import ConfigProvider, { globalConfigForApi } from '../config-provider';
function noop() {}
let seed = 0;
const now = Date.now();
function getUuid() {
return `rcNotification_${now}_${seed++}`;
}
const Notification = defineComponent({
mixins: [BaseMixin],
props: {
prefixCls: PropTypes.string.def('rc-notification'),
transitionName: PropTypes.string,
animation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).def('fade'),
maxCount: PropTypes.number,
closeIcon: PropTypes.any,
},
data() {
return {
notices: [],
};
},
methods: {
getTransitionName() {
const props = this.$props;
let transitionName = props.transitionName;
if (!transitionName && props.animation) {
transitionName = `${props.prefixCls}-${props.animation}`;
}
return transitionName;
},
add(notice) {
const key = (notice.key = notice.key || getUuid());
const { maxCount } = this.$props;
this.setState(previousState => {
const notices = previousState.notices;
const noticeIndex = notices.map(v => v.key).indexOf(key);
const updatedNotices = notices.concat();
if (noticeIndex !== -1) {
updatedNotices.splice(noticeIndex, 1, notice);
} else {
if (maxCount && notices.length >= maxCount) {
// XXX, use key of first item to update new added (let React to move exsiting
// instead of remove and mount). Same key was used before for both a) external
// manual control and b) internal react 'key' prop , which is not that good.
notice.updateKey = updatedNotices[0].updateKey || updatedNotices[0].key;
updatedNotices.shift();
}
updatedNotices.push(notice);
}
return {
notices: updatedNotices,
};
});
},
remove(key) {
this.setState(previousState => {
return {
notices: previousState.notices.filter(notice => notice.key !== key),
};
});
},
},
render() {
const { prefixCls, notices, remove, getTransitionName, $attrs } = this;
const transitionProps = getTransitionGroupProps(getTransitionName());
const noticeNodes = notices.map((notice, index) => {
const update = Boolean(index === notices.length - 1 && notice.updateKey);
const key = notice.updateKey ? notice.updateKey : notice.key;
const { content, duration, closable, onClose, style, class: className } = notice;
const close = createChainedFunction(remove.bind(this, notice.key), onClose);
const noticeProps = {
prefixCls,
duration,
closable,
update,
closeIcon: getComponent(this, 'closeIcon', { prefixCls }),
onClose: close,
onClick: notice.onClick || noop,
style,
class: className,
key,
};
return (
<Notice {...noticeProps}>
{typeof content === 'function' ? content({ prefixCls }) : content}
</Notice>
);
});
const className = {
[prefixCls]: 1,
};
return (
<div
class={className}
style={
$attrs.style || {
top: '65px',
left: '50%',
}
}
>
<TransitionGroup tag="span" {...transitionProps}>
{noticeNodes}
</TransitionGroup>
</div>
);
},
});
Notification.newInstance = function newNotificationInstance(properties, callback) {
const {
name = 'notification',
getContainer,
appContext,
prefixCls: customizePrefixCls,
rootPrefixCls: customRootPrefixCls,
...props
} = properties || {};
const div = document.createElement('div');
if (getContainer) {
const root = getContainer();
root.appendChild(div);
} else {
document.body.appendChild(div);
}
const Wrapper = defineComponent({
setup(_props, { attrs }) {
const notiRef = ref();
onMounted(() => {
callback({
notice(noticeProps) {
notiRef.value?.add(noticeProps);
},
removeNotice(key) {
notiRef.value?.remove(key);
},
destroy() {
vueRender(null, div);
if (div.parentNode) {
div.parentNode.removeChild(div);
}
},
});
});
return () => {
const global = globalConfigForApi;
const prefixCls = global.getPrefixCls(name, customizePrefixCls);
const rootPrefixCls = global.getRootPrefixCls(customRootPrefixCls, prefixCls);
return (
<ConfigProvider {...global} notUpdateGlobalConfig={true} prefixCls={rootPrefixCls}>
<Notification ref={notiRef} {...attrs} prefixCls={prefixCls} />
</ConfigProvider>
);
};
},
});
const vm = createVNode(Wrapper, props);
vm.appContext = appContext || vm.appContext;
vueRender(vm, div);
};
export default Notification;

View File

@ -0,0 +1,262 @@
import { getTransitionGroupProps } from '../_util/transition';
import type { Key } from '../_util/type';
import type { CSSProperties } from 'vue';
import {
createVNode,
computed,
defineComponent,
ref,
TransitionGroup,
onMounted,
render as vueRender,
} from 'vue';
import type { NoticeProps } from './Notice';
import Notice from './Notice';
import ConfigProvider, { globalConfigForApi } from '../config-provider';
let seed = 0;
const now = Date.now();
function getUuid() {
const id = seed;
seed += 1;
return `rcNotification_${now}_${id}`;
}
export interface NoticeContent extends Omit<NoticeProps, 'prefixCls' | 'noticeKey' | 'onClose'> {
prefixCls?: string;
key?: Key;
updateMark?: string;
content?: any;
onClose?: () => void;
style?: CSSProperties;
class?: String;
}
export type NoticeFunc = (noticeProps: NoticeContent) => void;
export type HolderReadyCallback = (
div: HTMLDivElement,
noticeProps: NoticeProps & { key: Key },
) => void;
export interface NotificationInstance {
notice: NoticeFunc;
removeNotice: (key: Key) => void;
destroy: () => void;
component: Notification;
}
export interface NotificationProps {
prefixCls?: string;
transitionName?: string;
animation?: string | object;
maxCount?: number;
closeIcon?: any;
}
type NotificationState = {
notice: NoticeContent & {
userPassKey?: Key;
};
holderCallback?: HolderReadyCallback;
}[];
const Notification = defineComponent<NotificationProps>({
name: 'Notification',
inheritAttrs: false,
props: ['prefixCls', 'transitionName', 'animation', 'maxCount', 'closeIcon'] as any,
setup(props, { attrs, expose, slots }) {
const hookRefs = new Map<Key, HTMLDivElement>();
const notices = ref<NotificationState>([]);
const transitionProps = computed(() => {
const { prefixCls, animation = 'fade' } = props;
let name = props.transitionName;
if (!name && animation) {
name = `${prefixCls}-${animation}`;
}
return getTransitionGroupProps(name);
});
const add = (originNotice: NoticeContent, holderCallback?: HolderReadyCallback) => {
const key = originNotice.key || getUuid();
const notice: NoticeContent & { key: Key; userPassKey?: Key } = {
...originNotice,
key,
};
const { maxCount } = props;
const noticeIndex = notices.value.map(v => v.notice.key).indexOf(key);
const updatedNotices = notices.value.concat();
if (noticeIndex !== -1) {
updatedNotices.splice(noticeIndex, 1, { notice, holderCallback } as any);
} else {
if (maxCount && notices.value.length >= maxCount) {
// XXX, use key of first item to update new added (let React to move exsiting
// instead of remove and mount). Same key was used before for both a) external
// manual control and b) internal react 'key' prop , which is not that good.
// eslint-disable-next-line no-param-reassign
// zombieJ: Not know why use `updateKey`. This makes Notice infinite loop in jest.
// Change to `updateMark` for compare instead.
// https://github.com/react-component/notification/commit/32299e6be396f94040bfa82517eea940db947ece
notice.key = updatedNotices[0].notice.key as Key;
notice.updateMark = getUuid();
// zombieJ: That's why. User may close by key directly.
// We need record this but not re-render to avoid upper issue
// https://github.com/react-component/notification/issues/129
notice.userPassKey = key;
updatedNotices.shift();
}
updatedNotices.push({ notice, holderCallback } as any);
}
notices.value = updatedNotices;
};
const remove = (removeKey: Key) => {
notices.value = notices.value.filter(({ notice: { key, userPassKey } }) => {
const mergedKey = userPassKey || key;
return mergedKey !== removeKey;
});
};
expose({
add,
remove,
notices,
});
return () => {
const { prefixCls, closeIcon = slots.closeIcon?.({ prefixCls }) } = props;
const noticeNodes = notices.value.map(({ notice, holderCallback }, index) => {
const updateMark = index === notices.value.length - 1 ? notice.updateMark : undefined;
const { key, userPassKey } = notice;
const { content } = notice;
const noticeProps = {
prefixCls,
closeIcon: typeof closeIcon === 'function' ? closeIcon({ prefixCls }) : closeIcon,
...(notice as any),
...notice.props,
key,
noticeKey: userPassKey || key,
updateMark,
onClose: (noticeKey: Key) => {
remove(noticeKey);
notice.onClose?.();
},
onClick: notice.onClick,
};
if (holderCallback) {
return (
<div
key={key}
class={`${prefixCls}-hook-holder`}
ref={(div: HTMLDivElement) => {
if (typeof key === 'undefined') {
return;
}
if (div) {
hookRefs.set(key, div);
holderCallback(div, noticeProps);
} else {
hookRefs.delete(key);
}
}}
/>
);
}
return (
<Notice {...noticeProps}>
{typeof content === 'function' ? content({ prefixCls }) : content}
</Notice>
);
});
const className = {
[prefixCls]: 1,
[attrs.class as string]: !!attrs.class,
};
return (
<div
class={className}
style={
attrs.style || {
top: '65px',
left: '50%',
}
}
>
<TransitionGroup tag="div" {...transitionProps.value}>
{noticeNodes}
</TransitionGroup>
</div>
);
};
},
});
Notification.newInstance = function newNotificationInstance(properties, callback) {
const {
name = 'notification',
getContainer,
appContext,
prefixCls: customizePrefixCls,
rootPrefixCls: customRootPrefixCls,
transitionName: customTransitionName,
hasTransitionName,
...props
} = properties || {};
const div = document.createElement('div');
if (getContainer) {
const root = getContainer();
root.appendChild(div);
} else {
document.body.appendChild(div);
}
const Wrapper = defineComponent({
name: 'NotificationWrapper',
setup(_props, { attrs }) {
const notiRef = ref();
onMounted(() => {
callback({
notice(noticeProps: NoticeContent) {
notiRef.value?.add(noticeProps);
},
removeNotice(key: Key) {
notiRef.value?.remove(key);
},
destroy() {
vueRender(null, div);
if (div.parentNode) {
div.parentNode.removeChild(div);
}
},
component: notiRef,
});
});
return () => {
const global = globalConfigForApi;
const prefixCls = global.getPrefixCls(name, customizePrefixCls);
const rootPrefixCls = global.getRootPrefixCls(customRootPrefixCls, prefixCls);
const transitionName = hasTransitionName
? customTransitionName
: `${rootPrefixCls}-${customTransitionName}`;
return (
<ConfigProvider {...global} notUpdateGlobalConfig={true} prefixCls={rootPrefixCls}>
<Notification
ref={notiRef}
{...attrs}
prefixCls={prefixCls}
transitionName={transitionName}
/>
</ConfigProvider>
);
};
},
});
const vm = createVNode(Wrapper, props);
vm.appContext = appContext || vm.appContext;
vueRender(vm, div);
};
export default Notification;

View File

@ -1,95 +0,0 @@
@notificationPrefixCls: rc-notification;
.@{notificationPrefixCls} {
position: fixed;
z-index: 1000;
&-notice {
padding: 7px 20px 7px 10px;
border-radius: 3px 3px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
border: 0px solid rgba(0, 0, 0, 0);
background: #fff;
display: block;
width: auto;
line-height: 1.5;
vertical-align: middle;
position: relative;
margin: 10px 0;
&-closable {
padding-right: 20px;
}
&-close {
position: absolute;
right: 5px;
top: 3px;
color: #000;
cursor: pointer;
outline: none;
font-size: 16px;
font-weight: 700;
line-height: 1;
text-shadow: 0 1px 0 #fff;
filter: alpha(opacity=20);
opacity: 0.2;
text-decoration: none;
&-x:after {
content: '×';
}
&:hover {
opacity: 1;
filter: alpha(opacity=100);
text-decoration: none;
}
}
}
.fade-effect() {
animation-duration: 0.3s;
animation-fill-mode: both;
animation-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2);
}
&-fade-enter {
opacity: 0;
.fade-effect();
animation-play-state: paused;
}
&-fade-leave {
.fade-effect();
animation-play-state: paused;
}
&-fade-enter&-fade-enter-active {
animation-name: rcNotificationFadeIn;
animation-play-state: running;
}
&-fade-leave&-fade-leave-active {
animation-name: rcDialogFadeOut;
animation-play-state: running;
}
@keyframes rcNotificationFadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes rcDialogFadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
}

View File

@ -1,3 +1,4 @@
// based on rc-notification 3.3.1 // based on rc-notification 4.5.7
import Notification from './Notification'; import Notification from './Notification';
export default Notification; export default Notification;