Skip to content

Commit 295e5af

Browse files
committed
feat(Notification): add useNotification hook
1 parent 3479422 commit 295e5af

File tree

9 files changed

+829
-4
lines changed

9 files changed

+829
-4
lines changed

components/notification/PurePanel.tsx

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { computed } from 'vue';
2+
import useStyle from './style';
3+
import useConfigInject from '../config-provider/hooks/useConfigInject';
4+
import type { IconType } from './interface';
5+
import Notice from '../vc-notification/Notice';
6+
import classNames from '../_util/classNames';
7+
import type { NoticeProps } from '../vc-notification/Notice';
8+
import type { VueNode } from '../_util/type';
9+
import {
10+
CheckCircleOutlined,
11+
CloseCircleOutlined,
12+
CloseOutlined,
13+
ExclamationCircleOutlined,
14+
InfoCircleOutlined,
15+
} from '@ant-design/icons-vue';
16+
import { renderHelper } from '../_util/util';
17+
18+
export function getCloseIcon(prefixCls: string, closeIcon?: VueNode) {
19+
return (
20+
closeIcon || (
21+
<span class={`${prefixCls}-close-x`}>
22+
<CloseOutlined class={`${prefixCls}-close-icon`} />
23+
</span>
24+
)
25+
);
26+
}
27+
28+
export interface PureContentProps {
29+
prefixCls: string;
30+
icon?: VueNode;
31+
message?: VueNode;
32+
description?: VueNode;
33+
btn?: VueNode;
34+
type?: IconType;
35+
}
36+
37+
const typeToIcon = {
38+
success: CheckCircleOutlined,
39+
info: InfoCircleOutlined,
40+
error: CloseCircleOutlined,
41+
warning: ExclamationCircleOutlined,
42+
};
43+
44+
export function PureContent({
45+
prefixCls,
46+
icon,
47+
type,
48+
message,
49+
description,
50+
btn,
51+
}: PureContentProps) {
52+
let iconNode = null;
53+
if (icon) {
54+
iconNode = <span class={`${prefixCls}-icon`}>{renderHelper(icon)}</span>;
55+
} else if (type) {
56+
const Icon = typeToIcon[type];
57+
iconNode = <Icon class={`${prefixCls}-icon ${prefixCls}-icon-${type}`} />;
58+
}
59+
60+
return (
61+
<div
62+
class={classNames({
63+
[`${prefixCls}-with-icon`]: iconNode,
64+
})}
65+
role="alert"
66+
>
67+
{iconNode}
68+
<div class={`${prefixCls}-message`}>{message}</div>
69+
<div class={`${prefixCls}-description`}>{description}</div>
70+
{btn && <div class={`${prefixCls}-btn`}>{btn}</div>}
71+
</div>
72+
);
73+
}
74+
75+
export interface PurePanelProps
76+
extends Omit<NoticeProps, 'prefixCls' | 'eventKey'>,
77+
Omit<PureContentProps, 'prefixCls' | 'children'> {
78+
prefixCls?: string;
79+
}
80+
81+
/** @private Internal Component. Do not use in your production. */
82+
export default function PurePanel(props: PurePanelProps) {
83+
const { getPrefixCls } = useConfigInject('notification', props);
84+
const prefixCls = computed(() => props.prefixCls || getPrefixCls('notification'));
85+
const noticePrefixCls = `${prefixCls.value}-notice`;
86+
87+
const [, hashId] = useStyle(prefixCls);
88+
89+
return (
90+
<Notice
91+
{...props}
92+
prefixCls={prefixCls.value}
93+
class={classNames(hashId.value, `${noticePrefixCls}-pure-panel`)}
94+
noticeKey="pure"
95+
duration={null}
96+
closable={props.closable}
97+
closeIcon={getCloseIcon(prefixCls.value, props.closeIcon)}
98+
>
99+
<PureContent
100+
prefixCls={noticePrefixCls}
101+
icon={props.icon}
102+
type={props.type}
103+
message={props.message}
104+
description={props.description}
105+
btn={props.btn}
106+
/>
107+
</Notice>
108+
);
109+
}

components/notification/index.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { globalConfig } from '../config-provider';
1111
import type { NotificationInstance as VCNotificationInstance } from '../vc-notification/Notification';
1212
import classNames from '../_util/classNames';
1313
import useStyle from './style';
14+
import useNotification from './useNotification';
15+
1416
export type NotificationPlacement =
1517
| 'top'
1618
| 'topLeft'
@@ -284,6 +286,7 @@ iconTypes.forEach(type => {
284286
});
285287

286288
api.warn = api.warning;
289+
api.useNotification = useNotification;
287290

288291
export interface NotificationInstance {
289292
success(args: NotificationArgsProps): void;
@@ -298,6 +301,7 @@ export interface NotificationApi extends NotificationInstance {
298301
close(key: string): void;
299302
config(options: ConfigProps): void;
300303
destroy(): void;
304+
useNotification: typeof useNotification;
301305
}
302306

303307
/** @private test Only function. Not work on production */

components/notification/interface.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { CSSProperties } from 'vue';
2+
import type { Key, VueNode } from '../_util/type';
3+
4+
export type NotificationPlacement =
5+
| 'top'
6+
| 'topLeft'
7+
| 'topRight'
8+
| 'bottom'
9+
| 'bottomLeft'
10+
| 'bottomRight';
11+
12+
export type IconType = 'success' | 'info' | 'error' | 'warning';
13+
14+
export interface ArgsProps {
15+
message: VueNode;
16+
description?: VueNode;
17+
btn?: VueNode;
18+
key?: Key;
19+
onClose?: () => void;
20+
duration?: number | null;
21+
icon?: VueNode;
22+
placement?: NotificationPlacement;
23+
style?: CSSProperties;
24+
className?: string;
25+
readonly type?: IconType;
26+
onClick?: () => void;
27+
closeIcon?: VueNode;
28+
}
29+
30+
type StaticFn = (args: ArgsProps) => void;
31+
32+
export interface NotificationInstance {
33+
success: StaticFn;
34+
error: StaticFn;
35+
info: StaticFn;
36+
warning: StaticFn;
37+
open: StaticFn;
38+
destroy(key?: Key): void;
39+
}
40+
41+
export interface GlobalConfigProps {
42+
top?: number;
43+
bottom?: number;
44+
duration?: number;
45+
prefixCls?: string;
46+
getContainer?: () => HTMLElement;
47+
placement?: NotificationPlacement;
48+
closeIcon?: VueNode;
49+
rtl?: boolean;
50+
maxCount?: number;
51+
}
52+
53+
export interface NotificationConfig {
54+
top?: number;
55+
bottom?: number;
56+
prefixCls?: string;
57+
getContainer?: () => HTMLElement;
58+
placement?: NotificationPlacement;
59+
maxCount?: number;
60+
rtl?: boolean;
61+
}
+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import type { VNode } from 'vue';
2+
import { shallowRef, computed, defineComponent } from 'vue';
3+
import { useNotification as useVcNotification } from '../vc-notification';
4+
import type { NotificationAPI } from '../vc-notification';
5+
import type {
6+
NotificationInstance,
7+
ArgsProps,
8+
NotificationPlacement,
9+
NotificationConfig,
10+
} from './interface';
11+
12+
import useStyle from './style';
13+
import { getCloseIcon, PureContent } from './PurePanel';
14+
import { getMotion, getPlacementStyle } from './util';
15+
import useConfigInject from '../config-provider/hooks/useConfigInject';
16+
import classNames from '../_util/classNames';
17+
import type { Key } from '../_util/type';
18+
19+
const DEFAULT_OFFSET = 24;
20+
const DEFAULT_DURATION = 4.5;
21+
22+
// ==============================================================================
23+
// == Holder ==
24+
// ==============================================================================
25+
type HolderProps = NotificationConfig & {
26+
onAllRemoved?: VoidFunction;
27+
getPopupContainer?: () => HTMLElement;
28+
};
29+
30+
interface HolderRef extends NotificationAPI {
31+
prefixCls: string;
32+
hashId: string;
33+
}
34+
35+
const Holder = defineComponent({
36+
name: 'Holder',
37+
inheritAttrs: false,
38+
props: ['prefixCls', 'class', 'type', 'icon', 'content', 'onAllRemoved'],
39+
setup(props: HolderProps, { expose }) {
40+
const { getPrefixCls, getPopupContainer } = useConfigInject('notification', props);
41+
const prefixCls = computed(() => props.prefixCls || getPrefixCls('notification'));
42+
// =============================== Style ===============================
43+
const getStyles = (placement: NotificationPlacement) =>
44+
getPlacementStyle(placement, props.top ?? DEFAULT_OFFSET, props.bottom ?? DEFAULT_OFFSET);
45+
46+
// Style
47+
const [, hashId] = useStyle(prefixCls);
48+
49+
const getClassName = () => classNames(hashId.value, { [`${prefixCls.value}-rtl`]: props.rtl });
50+
51+
// ============================== Motion ===============================
52+
const getNotificationMotion = () => getMotion(prefixCls.value);
53+
54+
// ============================== Origin ===============================
55+
const [api, holder] = useVcNotification({
56+
prefixCls: prefixCls.value,
57+
getStyles,
58+
getClassName,
59+
motion: getNotificationMotion,
60+
closable: true,
61+
closeIcon: getCloseIcon(prefixCls.value),
62+
duration: DEFAULT_DURATION,
63+
getContainer: () =>
64+
props.getPopupContainer?.() || getPopupContainer.value?.() || document.body,
65+
maxCount: props.maxCount,
66+
hashId: hashId.value,
67+
onAllRemoved: props.onAllRemoved,
68+
});
69+
70+
// ================================ Ref ================================
71+
expose({
72+
...api,
73+
prefixCls: prefixCls.value,
74+
hashId,
75+
});
76+
return holder;
77+
},
78+
});
79+
80+
// ==============================================================================
81+
// == Hook ==
82+
// ==============================================================================
83+
export function useInternalNotification(
84+
notificationConfig?: HolderProps,
85+
): readonly [NotificationInstance, () => VNode] {
86+
const holderRef = shallowRef<HolderRef>(null);
87+
88+
// ================================ API ================================
89+
const wrapAPI = computed(() => {
90+
// Wrap with notification content
91+
92+
// >>> Open
93+
const open = (config: ArgsProps) => {
94+
if (!holderRef.value) {
95+
return;
96+
}
97+
const { open: originOpen, prefixCls, hashId } = holderRef.value;
98+
const noticePrefixCls = `${prefixCls}-notice`;
99+
100+
const { message, description, icon, type, btn, className, ...restConfig } = config;
101+
return originOpen({
102+
placement: 'topRight',
103+
...restConfig,
104+
content: (
105+
<PureContent
106+
prefixCls={noticePrefixCls}
107+
icon={icon}
108+
type={type}
109+
message={message}
110+
description={description}
111+
btn={btn}
112+
/>
113+
),
114+
// @ts-ignore
115+
class: classNames(type && `${noticePrefixCls}-${type}`, hashId, className),
116+
});
117+
};
118+
119+
// >>> destroy
120+
const destroy = (key?: Key) => {
121+
if (key !== undefined) {
122+
holderRef.value?.close(key);
123+
} else {
124+
holderRef.value?.destroy();
125+
}
126+
};
127+
128+
const clone = {
129+
open,
130+
destroy,
131+
} as NotificationInstance;
132+
133+
const keys = ['success', 'info', 'warning', 'error'] as const;
134+
keys.forEach(type => {
135+
clone[type] = config =>
136+
open({
137+
...config,
138+
type,
139+
});
140+
});
141+
142+
return clone;
143+
});
144+
145+
// ============================== Return ===============================
146+
return [
147+
wrapAPI.value,
148+
() => <Holder key="notification-holder" {...notificationConfig} ref={holderRef} />,
149+
] as const;
150+
}
151+
152+
export default function useNotification(notificationConfig?: NotificationConfig) {
153+
return useInternalNotification(notificationConfig);
154+
}

0 commit comments

Comments
 (0)