@v-c/notification 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/bump.config.ts +6 -0
  3. package/dist/Notice.cjs +230 -0
  4. package/dist/Notice.d.ts +15 -0
  5. package/dist/Notice.js +225 -0
  6. package/dist/NoticeList.cjs +157 -0
  7. package/dist/NoticeList.d.ts +13 -0
  8. package/dist/NoticeList.js +154 -0
  9. package/dist/NotificationProvider.cjs +13 -0
  10. package/dist/NotificationProvider.d.ts +10 -0
  11. package/dist/NotificationProvider.js +10 -0
  12. package/dist/Notifications.cjs +146 -0
  13. package/dist/Notifications.d.ts +24 -0
  14. package/dist/Notifications.js +143 -0
  15. package/dist/_virtual/rolldown_runtime.cjs +21 -0
  16. package/dist/hooks/useNotification.cjs +80 -0
  17. package/dist/hooks/useNotification.d.ts +36 -0
  18. package/dist/hooks/useNotification.js +78 -0
  19. package/dist/hooks/useStack.cjs +24 -0
  20. package/dist/hooks/useStack.d.ts +6 -0
  21. package/dist/hooks/useStack.js +22 -0
  22. package/dist/index.cjs +6 -0
  23. package/dist/index.d.ts +6 -0
  24. package/dist/index.js +4 -0
  25. package/dist/interface.cjs +0 -0
  26. package/dist/interface.d.ts +55 -0
  27. package/dist/interface.js +0 -0
  28. package/docs/context.vue +34 -0
  29. package/docs/hooks.vue +89 -0
  30. package/docs/index.less +265 -0
  31. package/docs/maxCount.vue +24 -0
  32. package/docs/motion.ts +33 -0
  33. package/docs/notification.stories.vue +31 -0
  34. package/docs/showProgress.vue +34 -0
  35. package/docs/stack.vue +39 -0
  36. package/package.json +30 -0
  37. package/src/Notice.tsx +212 -0
  38. package/src/NoticeList.tsx +203 -0
  39. package/src/NotificationProvider.tsx +19 -0
  40. package/src/Notifications.tsx +164 -0
  41. package/src/hooks/useNotification.tsx +163 -0
  42. package/src/hooks/useStack.ts +32 -0
  43. package/src/index.ts +9 -0
  44. package/src/interface.ts +61 -0
  45. package/tsconfig.json +7 -0
  46. package/vite.config.ts +18 -0
  47. package/vitest.config.ts +9 -0
@@ -0,0 +1,34 @@
1
+ <script setup lang="ts">
2
+ import { useNotification } from '../src'
3
+ import motion from './motion.ts'
4
+ import './index.less'
5
+
6
+ const [notice, contextHolder] = useNotification({ showProgress: true, motion })
7
+ </script>
8
+
9
+ <template>
10
+ <button
11
+ @click="() => {
12
+ notice.open({
13
+ content: `${new Date().toISOString()}`,
14
+ });
15
+ }"
16
+ >
17
+ Show With Progress
18
+ </button>
19
+ <button
20
+ @click="() => {
21
+ notice.open({
22
+ content: `${new Date().toISOString()}`,
23
+ pauseOnHover: false,
24
+ });
25
+ }"
26
+ >
27
+ Not Pause On Hover
28
+ </button>
29
+ <contextHolder />
30
+ </template>
31
+
32
+ <style scoped>
33
+
34
+ </style>
package/docs/stack.vue ADDED
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ import { useNotification } from '../src'
3
+ import motion from './motion.ts'
4
+ import './index.less'
5
+
6
+ const [{ open }, contextHolder] = useNotification({ motion, stack: true })
7
+
8
+ function getConfig() {
9
+ return {
10
+ content: `${Array.from({ length: Math.round(Math.random() * 5) + 1 })
11
+ .fill(1)
12
+ .map(() => new Date().toISOString())
13
+ .join('\n')}`,
14
+ duration: null,
15
+ }
16
+ }
17
+ </script>
18
+
19
+ <template>
20
+ <button
21
+ @click="() => {
22
+ open(getConfig());
23
+ }"
24
+ >
25
+ Top Right
26
+ </button>
27
+ <button
28
+ @click="() => {
29
+ open({ ...getConfig(), placement: 'bottomRight' });
30
+ }"
31
+ >
32
+ Bottom Right
33
+ </button>
34
+ <contextHolder />
35
+ </template>
36
+
37
+ <style scoped>
38
+
39
+ </style>
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@v-c/notification",
3
+ "type": "module",
4
+ "version": "0.0.2",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ },
14
+ "./dist/*": "./dist/*",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "main": "./dist/index.js",
18
+ "peerDependencies": {
19
+ "vue": "^3.0.0"
20
+ },
21
+ "dependencies": {
22
+ "@v-c/util": "0.0.16"
23
+ },
24
+ "scripts": {
25
+ "build": "vite build",
26
+ "test": "vitest run",
27
+ "prepublish": "pnpm build",
28
+ "bump": "bumpp --release patch"
29
+ }
30
+ }
package/src/Notice.tsx ADDED
@@ -0,0 +1,212 @@
1
+ import type { CSSProperties } from 'vue'
2
+ import type { Key, NoticeConfig } from './interface.ts'
3
+ import { classNames } from '@v-c/util'
4
+ import KeyCode from '@v-c/util/dist/KeyCode'
5
+ import pickAttrs from '@v-c/util/dist/pickAttrs'
6
+ import { computed, defineComponent, shallowRef, watch } from 'vue'
7
+
8
+ export interface NoticeProps extends Omit<NoticeConfig, 'onClose'> {
9
+ prefixCls: string
10
+ eventKey: Key
11
+ onClick?: (event: Event) => void
12
+ onNoticeClose?: (key: Key) => void
13
+ hovering?: boolean
14
+ props?: Record<string, any>
15
+ }
16
+
17
+ const defaults = {
18
+ duration: 4.5,
19
+ pauseOnHover: true,
20
+ closeIcon: 'x',
21
+ } as const
22
+
23
+ const Notify = defineComponent<NoticeProps & { times?: number }>((props, { attrs }) => {
24
+ const hovering = shallowRef(false)
25
+ const percent = shallowRef(0)
26
+ const spentTime = shallowRef(0)
27
+
28
+ const mergedHovering = computed(() => props.hovering || hovering.value)
29
+ const mergedDuration = computed(() => {
30
+ if (typeof props.duration === 'number') {
31
+ return props.duration
32
+ }
33
+ if (props.duration === undefined) {
34
+ return defaults.duration
35
+ }
36
+ return 0
37
+ })
38
+ const mergedPauseOnHover = computed(() =>
39
+ props.pauseOnHover === undefined ? defaults.pauseOnHover : props.pauseOnHover,
40
+ )
41
+ const mergedShowProgress = computed(() => mergedDuration.value > 0 && props.showProgress)
42
+ const mergedCloseIcon = computed(() => props.closeIcon ?? defaults.closeIcon)
43
+
44
+ // ======================== Close =========================
45
+ const onInternalClose = () => {
46
+ props.onNoticeClose?.(props.eventKey)
47
+ }
48
+
49
+ const onCloseKeyDown = (e: KeyboardEvent) => {
50
+ if (e.key === 'Enter' || e.code === 'Enter' || e.keyCode === KeyCode.ENTER) {
51
+ onInternalClose()
52
+ }
53
+ }
54
+
55
+ // ======================== Timing ========================
56
+ watch([
57
+ () => props.times,
58
+ mergedDuration,
59
+ mergedHovering,
60
+ ], (_n, _, onCleanup) => {
61
+ const duration = mergedDuration.value
62
+ const hoveringValue = mergedHovering.value
63
+ const pauseOnHover = mergedPauseOnHover.value
64
+ if (!hoveringValue && duration > 0) {
65
+ const start = Date.now() - spentTime.value
66
+ const timeoutId = window.setTimeout(() => {
67
+ onInternalClose()
68
+ }, duration * 1000 - spentTime.value)
69
+
70
+ onCleanup(() => {
71
+ if (pauseOnHover) {
72
+ clearTimeout(timeoutId)
73
+ }
74
+ spentTime.value = Date.now() - start
75
+ })
76
+ }
77
+ }, {
78
+ immediate: true,
79
+ })
80
+
81
+ // ===================== Progress Bar =====================
82
+ watch([
83
+ () => props.times,
84
+ mergedDuration,
85
+ spentTime,
86
+ mergedHovering,
87
+ mergedShowProgress,
88
+ ], (_n, _, onCleanup) => {
89
+ const hoveringValue = mergedHovering.value
90
+ const showProgress = mergedShowProgress.value
91
+ const pauseOnHover = mergedPauseOnHover.value
92
+ const duration = mergedDuration.value
93
+ const baseSpentTime = spentTime.value
94
+
95
+ if (!hoveringValue && showProgress && (pauseOnHover || baseSpentTime === 0)) {
96
+ const start = performance.now()
97
+ let animationFrame = 0
98
+
99
+ const calculate = () => {
100
+ cancelAnimationFrame(animationFrame)
101
+ animationFrame = requestAnimationFrame((timestamp) => {
102
+ const runtime = timestamp + baseSpentTime - start
103
+ const progress = Math.min(runtime / (duration * 1000), 1)
104
+ percent.value = progress * 100
105
+ if (progress < 1) {
106
+ calculate()
107
+ }
108
+ })
109
+ }
110
+
111
+ calculate()
112
+
113
+ onCleanup(() => {
114
+ if (pauseOnHover) {
115
+ cancelAnimationFrame(animationFrame)
116
+ }
117
+ })
118
+ }
119
+ }, {
120
+ immediate: true,
121
+ })
122
+
123
+ return () => {
124
+ const {
125
+ closable,
126
+ prefixCls,
127
+ props: divProps,
128
+ onClick,
129
+ content,
130
+ className,
131
+ style,
132
+ } = props
133
+
134
+ // ======================== Closable ========================
135
+ const closableConfig
136
+ = typeof closable === 'object' && closable !== null
137
+ ? closable
138
+ : closable
139
+ ? { closeIcon: mergedCloseIcon.value }
140
+ : {}
141
+ const ariaProps = pickAttrs(closableConfig, true)
142
+
143
+ // ======================== Progress ========================
144
+ const safePercent = percent.value <= 0 ? 0 : percent.value > 100 ? 100 : percent.value
145
+ const validPercent = 100 - safePercent
146
+
147
+ // ======================== Render ========================
148
+ const noticePrefixCls = `${prefixCls}-notice`
149
+
150
+ const mergedStyle: CSSProperties = {
151
+ ...(typeof divProps?.style === 'object' && divProps?.style ? divProps.style : {}),
152
+ ...(typeof (attrs as any).style === 'object' && (attrs as any).style ? (attrs as any).style : {}),
153
+ ...(typeof style === 'object' && style ? style : {}),
154
+ }
155
+
156
+ return (
157
+ <div
158
+ {...divProps}
159
+ class={
160
+ classNames(
161
+ noticePrefixCls,
162
+ className,
163
+ (attrs as any).class,
164
+ {
165
+ [`${noticePrefixCls}-closable`]: !!closable,
166
+ },
167
+ )
168
+ }
169
+ style={mergedStyle}
170
+ onMouseenter={(e: MouseEvent) => {
171
+ hovering.value = true
172
+ divProps?.onMouseEnter?.(e)
173
+ }}
174
+ onMouseleave={(e: MouseEvent) => {
175
+ hovering.value = false
176
+ divProps?.onMouseLeave?.(e)
177
+ }}
178
+ onClick={onClick}
179
+ >
180
+ {/* Content */}
181
+ <div class={`${noticePrefixCls}-content`}>{content}</div>
182
+
183
+ {/* Close Icon */}
184
+ {closable && (
185
+ <button
186
+ type="button"
187
+ class={`${noticePrefixCls}-close`}
188
+ onKeydown={onCloseKeyDown}
189
+ aria-label="Close"
190
+ {...ariaProps}
191
+ onClick={(e) => {
192
+ e.preventDefault()
193
+ e.stopPropagation()
194
+ onInternalClose()
195
+ }}
196
+ >
197
+ {closableConfig.closeIcon ?? mergedCloseIcon.value}
198
+ </button>
199
+ )}
200
+
201
+ {/* Progress Bar */}
202
+ {mergedShowProgress.value && (
203
+ <progress class={`${noticePrefixCls}-progress`} max="100" value={validPercent}>
204
+ {`${validPercent}%`}
205
+ </progress>
206
+ )}
207
+ </div>
208
+ )
209
+ }
210
+ })
211
+
212
+ export default Notify
@@ -0,0 +1,203 @@
1
+ import type { CSSProperties, TransitionGroupProps } from 'vue'
2
+ import type { InnerOpenConfig, Key, NoticeConfig, OpenConfig, Placement, StackConfig } from './interface.ts'
3
+ import { classNames as clsx } from '@v-c/util'
4
+ import { getTransitionGroupProps } from '@v-c/util/dist/utils/transition'
5
+ import { unrefElement } from '@v-c/util/dist/vueuse/unref-element'
6
+ import { computed, defineComponent, reactive, ref, shallowRef, toRef, TransitionGroup, watch, watchEffect } from 'vue'
7
+ import useStack from './hooks/useStack.ts'
8
+ import Notice from './Notice.tsx'
9
+ import { useNotificationContext } from './NotificationProvider.tsx'
10
+
11
+ export interface NoticeListProps {
12
+ configList?: OpenConfig[]
13
+ placement?: Placement
14
+ prefixCls?: string
15
+ motion?: TransitionGroupProps | ((placement: Placement) => TransitionGroupProps)
16
+ stack?: StackConfig
17
+
18
+ // Events
19
+ onAllNoticeRemoved?: (placement: Placement) => void
20
+ onNoticeClose?: (key: Key) => void
21
+ }
22
+
23
+ const NoticeList = defineComponent<NoticeListProps>((props, { attrs }) => {
24
+ const ctx = useNotificationContext()
25
+ const dictRef = reactive<Record<string, HTMLDivElement | undefined>>({})
26
+ const keys = computed(() =>
27
+ (props.configList ?? []).map(config => ({
28
+ config,
29
+ key: String(config.key),
30
+ })),
31
+ )
32
+ const latestNotice = shallowRef<HTMLDivElement | null>(null)
33
+ const hoverKeys = ref<string[]>([])
34
+
35
+ const stackConfig = toRef(props, 'stack')
36
+ const [stackEnabled, stackOptions] = useStack(stackConfig)
37
+ const expanded = computed(
38
+ () =>
39
+ stackEnabled.value
40
+ && (hoverKeys.value.length > 0 || keys.value.length <= stackOptions.threshold!.value!),
41
+ )
42
+ const placementMotion = computed(() => {
43
+ if (typeof props.motion === 'function') {
44
+ return props.placement ? props.motion(props.placement) : undefined
45
+ }
46
+ return props.motion
47
+ })
48
+
49
+ // Clean hover key
50
+ watch([hoverKeys, keys, stackEnabled], () => {
51
+ if (stackEnabled.value && hoverKeys.value.length > 1) {
52
+ hoverKeys.value = hoverKeys.value.filter(key =>
53
+ keys.value.some(({ key: dataKey }) => key === dataKey),
54
+ )
55
+ }
56
+ })
57
+
58
+ // Sync latest notice after DOM updates so collapsed stack uses accurate height
59
+ watchEffect(
60
+ () => {
61
+ if (!stackEnabled.value) {
62
+ latestNotice.value = null
63
+ return
64
+ }
65
+
66
+ const lastKey = keys.value[keys.value.length - 1]?.key
67
+ latestNotice.value = lastKey ? dictRef[lastKey] ?? null : null
68
+ },
69
+ { flush: 'post' },
70
+ )
71
+
72
+ const checkAllClosed = () => {
73
+ if (!props.placement) {
74
+ return
75
+ }
76
+ if (keys.value.length === 0) {
77
+ props.onAllNoticeRemoved?.(props.placement)
78
+ }
79
+ }
80
+
81
+ return () => {
82
+ const { prefixCls = '', placement = 'topRight', onNoticeClose } = props
83
+
84
+ const renderNotify = () =>
85
+ keys.value.map(({ config }, motionIndex) => {
86
+ const { key, times } = config as InnerOpenConfig
87
+ const strKey = String(key)
88
+ const {
89
+ className: configClassName,
90
+ style: configStyle,
91
+ classNames: configClassNames,
92
+ styles: configStyles,
93
+ ...restConfig
94
+ } = config as NoticeConfig
95
+ const dataIndex = keys.value.findIndex(item => item.key === strKey)
96
+ const stackStyle: CSSProperties = {}
97
+
98
+ if (stackEnabled.value) {
99
+ const index = keys.value.length - 1 - (dataIndex > -1 ? dataIndex : motionIndex - 1)
100
+ const transformX = placement === 'top' || placement === 'bottom' ? '-50%' : '0'
101
+ if (index > 0) {
102
+ stackStyle.height = expanded.value
103
+ ? dictRef[strKey]?.offsetHeight
104
+ : latestNotice.value?.offsetHeight
105
+ if (stackStyle.height && typeof stackStyle.height === 'number') {
106
+ stackStyle.height = `${stackStyle.height}px`
107
+ }
108
+ let verticalOffset = 0
109
+ for (let i = 0; i < index; i += 1) {
110
+ const targetKey = keys.value[keys.value.length - 1 - i]?.key
111
+ const node = targetKey ? dictRef[targetKey] : null
112
+ verticalOffset += (node?.offsetHeight ?? 0) + stackOptions.gap!.value!
113
+ }
114
+
115
+ const transformY
116
+ = (expanded.value ? verticalOffset : index * stackOptions.offset!.value!)
117
+ * (placement.startsWith('top') ? 1 : -1)
118
+ const currentWidth = dictRef[strKey]?.offsetWidth
119
+ const latestWidth = latestNotice.value?.offsetWidth
120
+ const scaleX
121
+ = !expanded.value && latestWidth && currentWidth
122
+ ? (latestWidth - stackOptions.offset!.value! * 2 * (index < 3 ? index : 3))
123
+ / currentWidth
124
+ : 1
125
+ stackStyle.transform = `translate3d(${transformX}, ${transformY}px, 0) scaleX(${scaleX})`
126
+ }
127
+ else {
128
+ stackStyle.transform = `translate3d(${transformX}, 0, 0)`
129
+ }
130
+ }
131
+ return (
132
+ <div
133
+ key={strKey}
134
+ class={clsx(`${prefixCls}-notice-wrapper`, configClassNames?.wrapper)}
135
+ style={{
136
+ ...stackStyle,
137
+ ...configStyles?.wrapper,
138
+ }}
139
+ onMouseenter={() => {
140
+ hoverKeys.value = hoverKeys.value.includes(strKey)
141
+ ? hoverKeys.value
142
+ : [...hoverKeys.value, strKey]
143
+ }}
144
+ onMouseleave={() => {
145
+ hoverKeys.value = hoverKeys.value.filter(k => k !== strKey)
146
+ }}
147
+ >
148
+ <Notice
149
+ {...restConfig as any}
150
+ ref={(el) => {
151
+ const element = unrefElement<HTMLDivElement>(el as any) ?? undefined
152
+ if (dataIndex > -1) {
153
+ dictRef[strKey] = element
154
+ }
155
+ else {
156
+ delete dictRef[strKey]
157
+ }
158
+ }}
159
+ prefixCls={prefixCls}
160
+ classNames={configClassNames}
161
+ styles={configStyles}
162
+ class={clsx(configClassName, (ctx.value as any)?.classNames?.notice)}
163
+ style={configStyle}
164
+ times={times}
165
+ eventKey={key}
166
+ onNoticeClose={onNoticeClose}
167
+ hovering={stackEnabled.value && hoverKeys.value.length > 0}
168
+ />
169
+ </div>
170
+ )
171
+ })
172
+ let motionGroupProps: TransitionGroupProps = {}
173
+ if (placementMotion.value) {
174
+ motionGroupProps = getTransitionGroupProps(placementMotion.value.name!, placementMotion.value)
175
+ }
176
+ return (
177
+ <TransitionGroup
178
+ key={placement}
179
+ tag="div"
180
+ appear
181
+ {...{
182
+ class: clsx(
183
+ prefixCls,
184
+ `${prefixCls}-${placement}`,
185
+ (ctx.value as any)?.classNames?.list,
186
+ (attrs as any).class,
187
+ {
188
+ [`${prefixCls}-stack-expanded`]: expanded.value,
189
+ [`${prefixCls}-stack`]: stackEnabled.value,
190
+ },
191
+ ),
192
+ style: (attrs as any).style,
193
+ }}
194
+ {...motionGroupProps}
195
+ onAfterLeave={checkAllClosed}
196
+ >
197
+ {renderNotify()}
198
+ </TransitionGroup>
199
+ )
200
+ }
201
+ })
202
+
203
+ export default NoticeList
@@ -0,0 +1,19 @@
1
+ import type { InjectionKey, Ref } from 'vue'
2
+ import { inject, provide, ref } from 'vue'
3
+
4
+ export interface NotificationContextProps {
5
+ classNames?: {
6
+ notice?: string
7
+ list?: string
8
+ }
9
+ }
10
+ export const NotificationContext: InjectionKey<Ref<NotificationContextProps>> = Symbol('NotificationContext')
11
+
12
+ export function useNotificationProvider(props: Ref<NotificationContextProps>) {
13
+ provide(NotificationContext, props)
14
+ return props
15
+ }
16
+
17
+ export function useNotificationContext() {
18
+ return inject(NotificationContext, ref({}))
19
+ }
@@ -0,0 +1,164 @@
1
+ import type { VueNode } from '@v-c/util/dist/type'
2
+ import type { CSSProperties, TransitionGroupProps } from 'vue'
3
+ import type { InnerOpenConfig, Key, OpenConfig, Placement, Placements, StackConfig } from './interface.ts'
4
+ import { defineComponent, shallowRef, Teleport, watch } from 'vue'
5
+ import NoticeList from './NoticeList.tsx'
6
+
7
+ export interface NotificationsProps {
8
+ prefixCls?: string
9
+ motion?: TransitionGroupProps | ((placement: Placement) => TransitionGroupProps)
10
+ container?: HTMLElement | ShadowRoot
11
+ maxCount?: number
12
+ className?: (placement: Placement) => string
13
+ style?: (placement: Placement) => CSSProperties
14
+ onAllRemoved?: VoidFunction
15
+ stack?: StackConfig
16
+ renderNotifications?: (
17
+ node: VueNode,
18
+ info: { prefixCls: string, key: Key },
19
+ ) => VueNode
20
+ }
21
+
22
+ export interface NotificationsRef {
23
+ open: (config: OpenConfig) => void
24
+ close: (key: Key) => void
25
+ destroy: () => void
26
+ }
27
+
28
+ const defaults = {
29
+ prefixCls: 'vc-notification',
30
+ } as NotificationsProps
31
+
32
+ const Notifications = defineComponent<NotificationsProps>(
33
+ (props = defaults, { expose }) => {
34
+ const configList = shallowRef<OpenConfig[]>([])
35
+ // ======================== Close =========================
36
+ const onNoticeClose = (key: Key) => {
37
+ // Trigger close event
38
+ const config = configList.value.find(item => item.key === key)
39
+ const closable = config?.closable
40
+ const closableObj = closable && typeof closable === 'object' ? closable : {}
41
+ closableObj.onClose?.()
42
+ config?.onClose?.()
43
+ configList.value = configList.value.filter(item => item.key !== key)
44
+ }
45
+
46
+ // ========================= Refs =========================
47
+ expose({
48
+ open: (config: OpenConfig) => {
49
+ const list = configList.value
50
+ let clone = [...configList.value]
51
+ // Replace if exist
52
+ const index = clone.findIndex(item => item.key === config.key)
53
+ const innerConfig: InnerOpenConfig = {
54
+ ...config,
55
+ }
56
+ if (index >= 0) {
57
+ innerConfig.times = ((list[index] as InnerOpenConfig)?.times || 0) + 1
58
+ clone[index] = innerConfig
59
+ }
60
+ else {
61
+ innerConfig.times = 0
62
+ clone.push(innerConfig)
63
+ }
64
+ const maxCount = props.maxCount ?? 0
65
+ if (maxCount > 0 && clone.length > maxCount) {
66
+ clone = clone.slice(-maxCount)
67
+ }
68
+ configList.value = clone
69
+ },
70
+ close: onNoticeClose,
71
+ destroy: () => {
72
+ configList.value = []
73
+ },
74
+ })
75
+
76
+ // ====================== Placements ======================
77
+
78
+ const placements = shallowRef<Placements>({})
79
+
80
+ watch(
81
+ configList,
82
+ () => {
83
+ const nextPlacements: Placements = {}
84
+ configList.value.forEach((config) => {
85
+ const { placement = 'topRight' } = config
86
+ if (placement) {
87
+ nextPlacements[placement] = nextPlacements[placement] || []
88
+ nextPlacements[placement].push(config)
89
+ }
90
+ })
91
+ // Fill exist placements to avoid empty list causing remove without motion
92
+ Object.keys(placements.value).forEach((_placement) => {
93
+ const placement = _placement as Placement
94
+ nextPlacements[placement] = nextPlacements[placement] || []
95
+ })
96
+ placements.value = nextPlacements
97
+ },
98
+ )
99
+
100
+ // Clean up container if all notices fade out
101
+ const onAllNoticeRemoved = (placement: Placement) => {
102
+ const clone = { ...placements.value }
103
+ const list = clone[placement] || []
104
+ if (!list.length) {
105
+ delete clone[placement]
106
+ }
107
+ placements.value = clone
108
+ }
109
+
110
+ // Effect tell that placements is empty now
111
+ const emptyRef = shallowRef(false)
112
+
113
+ watch(
114
+ placements,
115
+ () => {
116
+ if (Object.keys(placements.value).length > 0) {
117
+ emptyRef.value = true
118
+ }
119
+ else if (emptyRef.value) {
120
+ // Trigger only when from exist to empty
121
+ props?.onAllRemoved?.()
122
+ emptyRef.value = false
123
+ }
124
+ },
125
+ )
126
+
127
+ return () => {
128
+ const { container } = props
129
+ const prefixCls = props.prefixCls ?? defaults.prefixCls ?? ''
130
+ // ======================== Render ========================
131
+ if (!container) {
132
+ return null
133
+ }
134
+
135
+ return (
136
+ <Teleport to={container}>
137
+ {Object.keys(placements.value).map((placement) => {
138
+ const placementConfigList = placements.value[placement as Placement]
139
+ const list = (
140
+ <NoticeList
141
+ key={placement}
142
+ configList={placementConfigList}
143
+ placement={placement as Placement}
144
+ prefixCls={prefixCls}
145
+ class={props.className?.(placement as Placement)}
146
+ style={props.style?.(placement as Placement)}
147
+ motion={props.motion}
148
+ stack={props.stack}
149
+ onAllNoticeRemoved={() => onAllNoticeRemoved(placement as Placement)}
150
+ onNoticeClose={onNoticeClose}
151
+ />
152
+ )
153
+ return props.renderNotifications ? props.renderNotifications(list, { prefixCls, key: placement }) : list
154
+ })}
155
+ </Teleport>
156
+ )
157
+ }
158
+ },
159
+ {
160
+ name: 'Notifications',
161
+ },
162
+ )
163
+
164
+ export default Notifications