@v-c/notification 1.0.0 → 2.0.0-beta.1

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 (67) hide show
  1. package/dist/Notification.d.ts +286 -0
  2. package/dist/Notification.js +237 -0
  3. package/dist/NotificationList/Content.d.ts +88 -0
  4. package/dist/NotificationList/Content.js +74 -0
  5. package/dist/NotificationList/index.d.ts +156 -0
  6. package/dist/NotificationList/index.js +204 -0
  7. package/dist/NotificationProvider.d.ts +20 -1
  8. package/dist/NotificationProvider.js +16 -3
  9. package/dist/Notifications.d.ts +136 -8
  10. package/dist/Notifications.js +118 -109
  11. package/dist/Progress.d.ts +8 -0
  12. package/dist/Progress.js +18 -0
  13. package/dist/hooks/useClosable.d.ts +22 -0
  14. package/dist/hooks/useClosable.js +33 -0
  15. package/dist/hooks/useListPosition/index.d.ts +17 -0
  16. package/dist/hooks/useListPosition/index.js +48 -0
  17. package/dist/hooks/useListPosition/useSizes.d.ts +13 -0
  18. package/dist/hooks/useListPosition/useSizes.js +29 -0
  19. package/dist/hooks/useNoticeTimer.d.ts +6 -0
  20. package/dist/hooks/useNoticeTimer.js +71 -0
  21. package/dist/hooks/useNotification.d.ts +8 -24
  22. package/dist/hooks/useNotification.js +33 -22
  23. package/dist/hooks/useStack.d.ts +8 -4
  24. package/dist/hooks/useStack.js +15 -18
  25. package/dist/index.d.ts +7 -5
  26. package/dist/index.js +5 -3
  27. package/docs/context.vue +1 -1
  28. package/docs/hooks.vue +4 -4
  29. package/docs/index.less +62 -143
  30. package/docs/maxCount.vue +1 -1
  31. package/docs/showProgress.vue +2 -2
  32. package/docs/stack.vue +1 -1
  33. package/package.json +5 -4
  34. package/src/Notification.tsx +363 -0
  35. package/src/NotificationList/Content.tsx +84 -0
  36. package/src/NotificationList/index.tsx +298 -0
  37. package/src/NotificationProvider.tsx +23 -3
  38. package/src/Notifications.tsx +103 -87
  39. package/src/Progress.tsx +23 -0
  40. package/src/hooks/useClosable.ts +54 -0
  41. package/src/hooks/useListPosition/index.ts +85 -0
  42. package/src/hooks/useListPosition/useSizes.ts +42 -0
  43. package/src/hooks/useNoticeTimer.ts +96 -0
  44. package/src/hooks/useNotification.tsx +54 -80
  45. package/src/hooks/useStack.ts +26 -18
  46. package/src/index.ts +31 -5
  47. package/tests/index.spec.tsx +200 -0
  48. package/vite.config.ts +4 -3
  49. package/vitest.config.ts +3 -1
  50. package/dist/Notice.cjs +0 -235
  51. package/dist/Notice.d.ts +0 -15
  52. package/dist/Notice.js +0 -227
  53. package/dist/NoticeList.cjs +0 -170
  54. package/dist/NoticeList.d.ts +0 -13
  55. package/dist/NoticeList.js +0 -164
  56. package/dist/NotificationProvider.cjs +0 -14
  57. package/dist/Notifications.cjs +0 -146
  58. package/dist/_virtual/rolldown_runtime.cjs +0 -21
  59. package/dist/hooks/useNotification.cjs +0 -93
  60. package/dist/hooks/useStack.cjs +0 -27
  61. package/dist/index.cjs +0 -7
  62. package/dist/interface.cjs +0 -1
  63. package/dist/interface.d.ts +0 -55
  64. package/dist/interface.js +0 -0
  65. package/src/Notice.tsx +0 -212
  66. package/src/NoticeList.tsx +0 -219
  67. package/src/interface.ts +0 -61
@@ -0,0 +1,363 @@
1
+ import type { VueNode } from '@v-c/util/dist/type'
2
+ import type { Component, CSSProperties, PropType } from 'vue'
3
+ import type { ClosableType } from './hooks/useClosable'
4
+ import type { NotificationProgressProps } from './Progress'
5
+ import { classNames as clsx } from '@v-c/util'
6
+ import { computed, defineComponent, shallowRef, toRef, watch } from 'vue'
7
+ import useClosable from './hooks/useClosable'
8
+ import useNoticeTimer from './hooks/useNoticeTimer'
9
+ import DefaultProgress from './Progress'
10
+
11
+ export interface NotificationClassNames {
12
+ wrapper?: string
13
+ root?: string
14
+ icon?: string
15
+ section?: string
16
+ title?: string
17
+ description?: string
18
+ actions?: string
19
+ close?: string
20
+ progress?: string
21
+ }
22
+
23
+ export interface NotificationStyles {
24
+ wrapper?: CSSProperties
25
+ root?: CSSProperties
26
+ icon?: CSSProperties
27
+ section?: CSSProperties
28
+ title?: CSSProperties
29
+ description?: CSSProperties
30
+ actions?: CSSProperties
31
+ close?: CSSProperties
32
+ progress?: CSSProperties
33
+ }
34
+
35
+ export interface ComponentsType {
36
+ progress?: Component<NotificationProgressProps>
37
+ }
38
+
39
+ export interface NotificationProps {
40
+ // Style
41
+ prefixCls: string
42
+ class?: string
43
+ style?: CSSProperties
44
+ classNames?: NotificationClassNames
45
+ styles?: NotificationStyles
46
+ components?: ComponentsType
47
+
48
+ // UI
49
+ title?: VueNode
50
+ description?: VueNode
51
+ icon?: VueNode
52
+ actions?: VueNode
53
+ role?: string
54
+ closable?: ClosableType
55
+ offset?: number
56
+ notificationIndex?: number
57
+ stackInThreshold?: boolean
58
+ props?: Record<string, any>
59
+
60
+ // Behavior
61
+ duration?: number | false | null
62
+ showProgress?: boolean
63
+ times?: number
64
+ hovering?: boolean
65
+ pauseOnHover?: boolean
66
+
67
+ // Function
68
+ onClick?: (event: MouseEvent) => void
69
+ onMouseEnter?: (event: MouseEvent) => void
70
+ onMouseLeave?: (event: MouseEvent) => void
71
+ /** @deprecated Please use `closable.onClose` instead. */
72
+ onClose?: () => void
73
+ }
74
+
75
+ const Notification = defineComponent({
76
+ name: 'Notification',
77
+ inheritAttrs: false,
78
+ props: {
79
+ prefixCls: { type: String, required: true },
80
+ class: { type: String, default: undefined },
81
+ style: { type: Object as PropType<CSSProperties>, default: undefined },
82
+ classNames: { type: Object as PropType<NotificationClassNames>, default: undefined },
83
+ styles: { type: Object as PropType<NotificationStyles>, default: undefined },
84
+ components: { type: Object as PropType<ComponentsType>, default: undefined },
85
+
86
+ title: { type: null as any, default: undefined },
87
+ description: { type: null as any, default: undefined },
88
+ icon: { type: null as any, default: undefined },
89
+ actions: { type: null as any, default: undefined },
90
+ role: { type: String, default: undefined },
91
+ closable: { type: [Boolean, Object, null] as PropType<ClosableType>, default: undefined },
92
+ offset: { type: Number, default: undefined },
93
+ notificationIndex: { type: Number, default: undefined },
94
+ stackInThreshold: { type: Boolean, default: false },
95
+ props: { type: Object as PropType<Record<string, any>>, default: undefined },
96
+
97
+ duration: { type: [Number, Boolean] as PropType<number | false | null>, default: 4.5 },
98
+ showProgress: { type: Boolean, default: false },
99
+ times: { type: Number, default: undefined },
100
+ hovering: { type: Boolean, default: false },
101
+ pauseOnHover: { type: Boolean, default: true },
102
+
103
+ onClick: { type: Function as PropType<(event: MouseEvent) => void>, default: undefined },
104
+ onMouseEnter: { type: Function as PropType<(event: MouseEvent) => void>, default: undefined },
105
+ onMouseLeave: { type: Function as PropType<(event: MouseEvent) => void>, default: undefined },
106
+ onClose: { type: Function as PropType<() => void>, default: undefined },
107
+ },
108
+ setup(props, { attrs, expose }) {
109
+ const noticeRef = shallowRef<HTMLDivElement | null>(null)
110
+ expose({
111
+ get nativeElement() {
112
+ return noticeRef.value
113
+ },
114
+ })
115
+
116
+ const noticePrefixCls = computed(() => `${props.prefixCls}-notice`)
117
+ const localHovering = shallowRef(false)
118
+ const percent = shallowRef(0)
119
+
120
+ // ========================= Close ==========================
121
+ const [mergedClosable, closableConfig, closeBtnAriaProps] = useClosable(
122
+ toRef(props, 'closable'),
123
+ )
124
+ const onInternalClose = () => {
125
+ closableConfig.value.onClose?.()
126
+ props.onClose?.()
127
+ }
128
+
129
+ // ======================== Duration ========================
130
+ const [onResume, onPause] = useNoticeTimer(
131
+ toRef(props, 'duration'),
132
+ onInternalClose,
133
+ (ptg) => {
134
+ percent.value = ptg
135
+ },
136
+ )
137
+
138
+ watch(
139
+ [
140
+ () => props.pauseOnHover,
141
+ () => props.hovering,
142
+ localHovering,
143
+ ],
144
+ ([pauseOnHover, forcedHovering, hovering]) => {
145
+ if (!pauseOnHover) {
146
+ return
147
+ }
148
+ if (forcedHovering) {
149
+ onPause()
150
+ }
151
+ else if (!hovering) {
152
+ onResume()
153
+ }
154
+ },
155
+ { immediate: true },
156
+ )
157
+
158
+ // ========================= Hover ==========================
159
+ function onInternalMouseEnter(event: MouseEvent) {
160
+ localHovering.value = true
161
+ if (props.pauseOnHover) {
162
+ onPause()
163
+ }
164
+ props.onMouseEnter?.(event)
165
+ }
166
+
167
+ function onInternalMouseLeave(event: MouseEvent) {
168
+ localHovering.value = false
169
+ if (props.pauseOnHover && !props.hovering) {
170
+ onResume()
171
+ }
172
+ props.onMouseLeave?.(event)
173
+ }
174
+
175
+ function onInternalCloseClick(event: MouseEvent) {
176
+ event.preventDefault()
177
+ event.stopPropagation()
178
+ onInternalClose()
179
+ }
180
+
181
+ // ======================== Position ========================
182
+ let lastOffset = props.offset
183
+ let lastIndex = props.notificationIndex
184
+ watch(
185
+ () => props.offset,
186
+ (val) => {
187
+ if (val !== undefined)
188
+ lastOffset = val
189
+ },
190
+ { immediate: true },
191
+ )
192
+ watch(
193
+ () => props.notificationIndex,
194
+ (val) => {
195
+ if (val !== undefined)
196
+ lastIndex = val
197
+ },
198
+ { immediate: true },
199
+ )
200
+
201
+ return () => {
202
+ const {
203
+ title,
204
+ description,
205
+ icon,
206
+ actions,
207
+ role,
208
+ showProgress,
209
+ duration,
210
+ classNames,
211
+ styles,
212
+ components,
213
+ class: className,
214
+ style,
215
+ props: rootProps,
216
+ onClick,
217
+ } = props
218
+
219
+ const validPercent = 100 - Math.min(Math.max(percent.value * 100, 0), 100)
220
+ const ProgressComponent = (components?.progress || DefaultProgress) as any
221
+
222
+ // ======================== Content =========================
223
+ const titleNode
224
+ = title !== undefined && title !== null
225
+ ? (
226
+ <div
227
+ class={clsx(`${noticePrefixCls.value}-title`, classNames?.title)}
228
+ style={styles?.title}
229
+ >
230
+ {title}
231
+ </div>
232
+ )
233
+ : null
234
+
235
+ const descNode
236
+ = description !== undefined && description !== null
237
+ ? (
238
+ <div
239
+ class={clsx(
240
+ `${noticePrefixCls.value}-description`,
241
+ classNames?.description,
242
+ )}
243
+ style={styles?.description}
244
+ >
245
+ {description}
246
+ </div>
247
+ )
248
+ : null
249
+
250
+ const hasTitle = titleNode !== null
251
+ const hasDescription = descNode !== null
252
+ let contentNode: VueNode = null
253
+
254
+ if (hasTitle && hasDescription) {
255
+ contentNode = (
256
+ <div
257
+ class={clsx(`${noticePrefixCls.value}-section`, classNames?.section)}
258
+ style={styles?.section}
259
+ >
260
+ {titleNode}
261
+ {descNode}
262
+ </div>
263
+ )
264
+ }
265
+ else {
266
+ contentNode = titleNode || descNode
267
+ }
268
+
269
+ if (icon !== undefined && icon !== null) {
270
+ contentNode = (
271
+ <div
272
+ class={clsx(`${noticePrefixCls.value}-wrapper`, classNames?.wrapper)}
273
+ style={styles?.wrapper}
274
+ >
275
+ <div
276
+ class={clsx(`${noticePrefixCls.value}-icon`, classNames?.icon)}
277
+ style={styles?.icon}
278
+ >
279
+ {icon}
280
+ </div>
281
+ {contentNode}
282
+ </div>
283
+ )
284
+ }
285
+
286
+ const actionsNode = actions
287
+ ? (
288
+ <div
289
+ class={clsx(`${noticePrefixCls.value}-actions`, classNames?.actions)}
290
+ style={styles?.actions}
291
+ >
292
+ {actions}
293
+ </div>
294
+ )
295
+ : null
296
+
297
+ // ========================= Render =========================
298
+ const mergedOffset = props.offset ?? lastOffset
299
+ const mergedIndex = props.notificationIndex ?? lastIndex ?? 0
300
+
301
+ const mergedStyle: CSSProperties & Record<string, any> = {
302
+ '--notification-index': mergedIndex,
303
+ ...styles?.root,
304
+ ...style,
305
+ }
306
+
307
+ if (mergedOffset !== undefined) {
308
+ mergedStyle['--notification-y'] = `${mergedOffset}px`
309
+ }
310
+
311
+ const mergedRole = role ?? rootProps?.role ?? 'alert'
312
+
313
+ return (
314
+ <div
315
+ {...rootProps}
316
+ ref={noticeRef as any}
317
+ role={mergedRole}
318
+ data-notification-index={mergedIndex}
319
+ class={clsx(
320
+ noticePrefixCls.value,
321
+ className,
322
+ (attrs as any).class,
323
+ classNames?.root,
324
+ {
325
+ [`${noticePrefixCls.value}-closable`]: mergedClosable.value,
326
+ [`${noticePrefixCls.value}-stack-in-threshold`]: props.stackInThreshold,
327
+ },
328
+ )}
329
+ style={mergedStyle}
330
+ onClick={onClick}
331
+ onMouseenter={onInternalMouseEnter}
332
+ onMouseleave={onInternalMouseLeave}
333
+ >
334
+ {contentNode}
335
+ {actionsNode}
336
+
337
+ {mergedClosable.value && (
338
+ <button
339
+ type="button"
340
+ class={clsx(`${noticePrefixCls.value}-close`, classNames?.close)}
341
+ aria-label="Close"
342
+ {...closeBtnAriaProps.value}
343
+ style={styles?.close}
344
+ onClick={onInternalCloseClick}
345
+ >
346
+ {closableConfig.value.closeIcon}
347
+ </button>
348
+ )}
349
+
350
+ {showProgress && typeof duration === 'number' && duration > 0 && (
351
+ <ProgressComponent
352
+ class={clsx(`${noticePrefixCls.value}-progress`, classNames?.progress)}
353
+ percent={validPercent}
354
+ style={styles?.progress}
355
+ />
356
+ )}
357
+ </div>
358
+ )
359
+ }
360
+ },
361
+ })
362
+
363
+ export default Notification
@@ -0,0 +1,84 @@
1
+ import type { CSSProperties, PropType, TransitionGroupProps } from 'vue'
2
+ import { classNames as clsx } from '@v-c/util'
3
+ import { defineComponent, h, shallowRef, TransitionGroup } from 'vue'
4
+
5
+ export interface ContentProps {
6
+ listPrefixCls: string
7
+ height: number
8
+ topNoticeHeight?: number
9
+ topNoticeWidth?: number
10
+ class?: string
11
+ style?: CSSProperties
12
+ motionProps?: TransitionGroupProps
13
+ onAfterLeave?: () => void
14
+ }
15
+
16
+ const Content = defineComponent({
17
+ name: 'NotificationListContent',
18
+ inheritAttrs: false,
19
+ props: {
20
+ listPrefixCls: { type: String, required: true },
21
+ height: { type: Number, required: true },
22
+ topNoticeHeight: { type: Number, default: 0 },
23
+ topNoticeWidth: { type: Number, default: 0 },
24
+ class: { type: String, default: undefined },
25
+ style: { type: Object as PropType<CSSProperties>, default: undefined },
26
+ motionProps: { type: Object as PropType<TransitionGroupProps>, default: undefined },
27
+ onAfterLeave: { type: Function as PropType<() => void>, default: undefined },
28
+ },
29
+ setup(props, { slots, expose }) {
30
+ let prevHeight = props.height
31
+ const innerRef = shallowRef<HTMLElement | null>(null)
32
+ expose({
33
+ get nativeElement() {
34
+ return innerRef.value
35
+ },
36
+ })
37
+
38
+ return () => {
39
+ const {
40
+ listPrefixCls,
41
+ height,
42
+ topNoticeHeight,
43
+ topNoticeWidth,
44
+ class: className,
45
+ style,
46
+ motionProps,
47
+ onAfterLeave,
48
+ } = props
49
+ const heightStatus: 'increase' | 'decrease' = height < prevHeight ? 'decrease' : 'increase'
50
+ prevHeight = height
51
+
52
+ const contentPrefixCls = `${listPrefixCls}-content`
53
+ const contentStyle: CSSProperties & Record<string, any> = {
54
+ ...style,
55
+ 'height': `${height}px`,
56
+ '--top-notificiation-height': `${topNoticeHeight ?? 0}px`,
57
+ '--top-notificiation-width': `${topNoticeWidth ?? 0}px`,
58
+ }
59
+ const contentClass = clsx(
60
+ contentPrefixCls,
61
+ `${contentPrefixCls}-${heightStatus}`,
62
+ className,
63
+ )
64
+
65
+ return h(
66
+ TransitionGroup,
67
+ {
68
+ tag: 'div',
69
+ appear: true,
70
+ ...motionProps,
71
+ ref: (el: any) => {
72
+ innerRef.value = (el?.$el ?? el) as HTMLElement | null
73
+ },
74
+ class: contentClass,
75
+ style: contentStyle,
76
+ onAfterLeave,
77
+ },
78
+ () => slots.default?.(),
79
+ )
80
+ }
81
+ },
82
+ })
83
+
84
+ export default Content