@v-c/notification 0.0.3 → 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,85 @@
1
+ import type { ComputedRef, MaybeRef } from 'vue'
2
+ import type { StackConfig } from '../useStack'
3
+ import { computed, unref } from 'vue'
4
+ import useSizes from './useSizes'
5
+
6
+ type Key = string | number | symbol
7
+
8
+ export interface ConfigItem {
9
+ key: Key
10
+ }
11
+
12
+ /**
13
+ * Calculates each notification's position and the full list height.
14
+ */
15
+ export default function useListPosition(
16
+ configList: MaybeRef<readonly ConfigItem[]>,
17
+ stack: MaybeRef<StackConfig | undefined>,
18
+ gap: MaybeRef<number> = 0,
19
+ ): [
20
+ ComputedRef<Map<string, number>>,
21
+ (key: string, node: HTMLElement | null) => void,
22
+ ComputedRef<number>,
23
+ ComputedRef<number | undefined>,
24
+ ComputedRef<number | undefined>,
25
+ ] {
26
+ const [sizeMap, setNodeSize] = useSizes()
27
+
28
+ const result = computed(() => {
29
+ const list = unref(configList)
30
+ const stackValue = unref(stack)
31
+ const gapValue = unref(gap) ?? 0
32
+
33
+ let offsetY = 0
34
+ let nextTotalHeight = 0
35
+ const stackThreshold = stackValue?.threshold ?? 0
36
+ const positions = new Map<string, number>()
37
+ let topNoticeHeight: number | undefined
38
+ let topNoticeWidth: number | undefined
39
+
40
+ list
41
+ .slice()
42
+ .reverse()
43
+ .forEach((config, index) => {
44
+ const key = String(config.key)
45
+ const height = sizeMap.value[key]?.height ?? 0
46
+ const y
47
+ = stackValue && index > 0
48
+ ? offsetY + (stackValue.offset ?? 0) - height
49
+ : offsetY
50
+
51
+ positions.set(key, y)
52
+
53
+ if (index === 0) {
54
+ topNoticeHeight = height
55
+ topNoticeWidth = sizeMap.value[key]?.width ?? 0
56
+ }
57
+
58
+ if (!stackValue || index < stackThreshold) {
59
+ nextTotalHeight = Math.max(nextTotalHeight, y + height)
60
+ }
61
+
62
+ if (stackValue) {
63
+ offsetY = y + height
64
+ }
65
+ else {
66
+ offsetY += height + gapValue
67
+ }
68
+ })
69
+
70
+ return {
71
+ positions,
72
+ totalHeight: nextTotalHeight,
73
+ topNoticeHeight,
74
+ topNoticeWidth,
75
+ }
76
+ })
77
+
78
+ return [
79
+ computed(() => result.value.positions),
80
+ setNodeSize,
81
+ computed(() => result.value.totalHeight),
82
+ computed(() => result.value.topNoticeHeight),
83
+ computed(() => result.value.topNoticeWidth),
84
+ ]
85
+ }
@@ -0,0 +1,42 @@
1
+ import type { Ref } from 'vue'
2
+ import { ref } from 'vue'
3
+
4
+ export interface NodeSize {
5
+ width: number
6
+ height: number
7
+ }
8
+
9
+ export type NodeSizeMap = Record<string, NodeSize>
10
+
11
+ /**
12
+ * Track measured node sizes by key. Returns the size map ref and a setter callback.
13
+ */
14
+ export default function useSizes(): [
15
+ Ref<NodeSizeMap>,
16
+ (key: string, node: HTMLElement | null) => void,
17
+ ] {
18
+ const sizeMap = ref<NodeSizeMap>({})
19
+
20
+ function setNodeSize(key: string, node: HTMLElement | null) {
21
+ if (!node) {
22
+ if (!(key in sizeMap.value)) {
23
+ return
24
+ }
25
+ const { [key]: _, ...rest } = sizeMap.value
26
+ sizeMap.value = rest
27
+ return
28
+ }
29
+
30
+ const next: NodeSize = {
31
+ width: node.offsetWidth,
32
+ height: node.offsetHeight,
33
+ }
34
+ const prev = sizeMap.value[key]
35
+ if (prev && prev.width === next.width && prev.height === next.height) {
36
+ return
37
+ }
38
+ sizeMap.value = { ...sizeMap.value, [key]: next }
39
+ }
40
+
41
+ return [sizeMap, setNodeSize]
42
+ }
@@ -0,0 +1,96 @@
1
+ import type { ComputedRef, MaybeRef } from 'vue'
2
+ import raf from '@v-c/util/dist/raf'
3
+ import { computed, onBeforeUnmount, shallowRef, unref, watch } from 'vue'
4
+
5
+ /**
6
+ * Run the auto-close timer for a notice and report progress updates.
7
+ * Returns controls to pause and resume the timer.
8
+ */
9
+ export default function useNoticeTimer(
10
+ duration: MaybeRef<number | false | null | undefined>,
11
+ onClose: () => void,
12
+ onUpdate: (ptg: number) => void,
13
+ ): [() => void, () => void, ComputedRef<number>] {
14
+ const durationMs = computed(() => {
15
+ const value = unref(duration)
16
+ const merged = typeof value === 'number' ? value : 0
17
+ return Math.max(merged, 0) * 1000
18
+ })
19
+
20
+ const walking = shallowRef(durationMs.value > 0)
21
+ const passTime = shallowRef(0)
22
+ let lastRafTime: number | null = null
23
+ let rafId: number | null = null
24
+
25
+ function syncPassTime() {
26
+ const now = Date.now()
27
+ if (lastRafTime !== null) {
28
+ passTime.value += now - lastRafTime
29
+ }
30
+ lastRafTime = now
31
+ }
32
+
33
+ function cancelRaf() {
34
+ if (rafId !== null) {
35
+ raf.cancel(rafId)
36
+ rafId = null
37
+ }
38
+ }
39
+
40
+ function onPause() {
41
+ syncPassTime()
42
+ walking.value = false
43
+ }
44
+
45
+ function onResume() {
46
+ if (durationMs.value > 0) {
47
+ lastRafTime = Date.now()
48
+ walking.value = true
49
+ }
50
+ else {
51
+ onUpdate(0)
52
+ }
53
+ }
54
+
55
+ // Reset when durationMs changed.
56
+ watch(durationMs, () => {
57
+ passTime.value = 0
58
+ lastRafTime = null
59
+ walking.value = durationMs.value > 0
60
+ })
61
+
62
+ // Drive raf loop while walking.
63
+ watch(walking, (isWalking) => {
64
+ cancelRaf()
65
+ if (!isWalking) {
66
+ return
67
+ }
68
+
69
+ function step() {
70
+ syncPassTime()
71
+
72
+ if (passTime.value >= durationMs.value) {
73
+ onUpdate(1)
74
+ onClose()
75
+ }
76
+ else {
77
+ onUpdate(Math.min(passTime.value / durationMs.value, 1))
78
+ rafId = raf(step)
79
+ }
80
+ }
81
+
82
+ step()
83
+ }, { immediate: true })
84
+
85
+ onBeforeUnmount(() => {
86
+ cancelRaf()
87
+ })
88
+
89
+ const percent = computed(() => {
90
+ if (durationMs.value <= 0)
91
+ return 0
92
+ return Math.min(passTime.value / durationMs.value, 1)
93
+ })
94
+
95
+ return [onResume, onPause, percent]
96
+ }
@@ -1,36 +1,29 @@
1
1
  import type { VueNode } from '@v-c/util/dist/type'
2
2
  import type { CSSProperties, MaybeRef, TransitionGroupProps } from 'vue'
3
- import type { Key, OpenConfig, Placement, StackConfig } from '../interface'
3
+ import type { NotificationListConfig, Placement, StackInput } from '../NotificationList'
4
4
  import type { NotificationsProps, NotificationsRef } from '../Notifications'
5
5
  import { computed, onMounted, shallowRef, unref, watch } from 'vue'
6
6
  import Notifications from '../Notifications'
7
7
 
8
+ type Key = string | number | symbol
9
+
8
10
  const defaultGetContainer = () => document.body
9
11
 
10
- type OptionalConfig = Partial<OpenConfig>
12
+ type OptionalConfig = Partial<NotificationListConfig>
13
+ type SharedConfig = Pick<
14
+ NotificationListConfig,
15
+ 'placement' | 'closable' | 'duration' | 'showProgress'
16
+ >
11
17
 
12
- export interface NotificationConfig {
13
- prefixCls?: string
14
- /** Customize container. It will repeat call which means you should return same container element. */
18
+ export interface NotificationConfig extends Omit<NotificationsProps, 'container'> {
19
+ // UI
20
+ placement?: Placement
15
21
  getContainer?: () => HTMLElement | ShadowRoot
16
- motion?: TransitionGroupProps | ((placement: Placement) => TransitionGroupProps)
17
- closeIcon?: VueNode
18
- closable?:
19
- | boolean
20
- | ({ closeIcon?: VueNode, onClose?: VoidFunction } & Record<string, any>)
21
- maxCount?: number
22
+
23
+ // Behavior
24
+ closable?: NotificationListConfig['closable']
22
25
  duration?: number | false | null
23
- showProgress?: boolean
24
- pauseOnHover?: boolean
25
- /** @private. Config for notification holder style. Safe to remove if refactor */
26
- className?: (placement: Placement) => string
27
- /** @private. Config for notification holder style. Safe to remove if refactor */
28
- style?: (placement: Placement) => CSSProperties
29
- /** @private Trigger when all the notification closed. */
30
- onAllRemoved?: VoidFunction
31
- stack?: StackConfig
32
- /** @private Slot for style in Notifications */
33
- renderNotifications?: NotificationsProps['renderNotifications']
26
+ showProgress?: NotificationListConfig['showProgress']
34
27
  }
35
28
 
36
29
  export interface NotificationAPI {
@@ -41,7 +34,7 @@ export interface NotificationAPI {
41
34
 
42
35
  interface OpenTask {
43
36
  type: 'open'
44
- config: OpenConfig
37
+ config: NotificationListConfig
45
38
  }
46
39
 
47
40
  interface CloseTask {
@@ -58,46 +51,34 @@ type Task = OpenTask | CloseTask | DestroyTask
58
51
  let uniqueKey = 0
59
52
 
60
53
  function mergeConfig<T>(...objList: Partial<T>[]): T {
61
- const clone: any = {}
62
-
63
- objList.forEach((obj: any) => {
54
+ const clone = {} as T
55
+ objList.forEach((obj) => {
64
56
  if (obj) {
65
57
  Object.keys(obj).forEach((key) => {
66
- const val = obj[key]
67
-
68
- if (val !== undefined) {
69
- clone[key] = val
58
+ const value = (obj as any)[key]
59
+ if (value !== undefined) {
60
+ ;(clone as any)[key] = value
70
61
  }
71
62
  })
72
63
  }
73
64
  })
74
-
75
65
  return clone
76
66
  }
77
67
 
78
- export default function useNotification(rootConfig: MaybeRef<NotificationConfig> = {}) {
68
+ export default function useNotification(
69
+ rootConfig: MaybeRef<NotificationConfig> = {},
70
+ ): [NotificationAPI, () => VueNode] {
79
71
  const configRef = computed(() => unref(rootConfig) || {})
80
72
  const container = shallowRef<HTMLElement | ShadowRoot>()
73
+ const notificationsRef = shallowRef<NotificationsRef>()
74
+ const taskQueue = shallowRef<Task[]>([])
81
75
 
82
- const notificationRef = shallowRef<NotificationsRef>()
83
-
84
- const shareConfig = computed(() => {
85
- const {
86
- getContainer,
87
- motion,
88
- prefixCls,
89
- maxCount,
90
- className,
91
- style,
92
- onAllRemoved,
93
- stack,
94
- renderNotifications,
95
- ...restConfig
96
- } = configRef.value
97
- return restConfig
76
+ const shareConfig = computed<SharedConfig>(() => {
77
+ const { placement, closable, duration, showProgress } = configRef.value
78
+ return { placement, closable, duration, showProgress }
98
79
  })
99
80
 
100
- const resolveContainer = () => {
81
+ function resolveContainer() {
101
82
  const getContainer = configRef.value.getContainer || defaultGetContainer
102
83
  return getContainer()
103
84
  }
@@ -105,31 +86,30 @@ export default function useNotification(rootConfig: MaybeRef<NotificationConfig>
105
86
  const contextHolder = () => (
106
87
  <Notifications
107
88
  container={container.value}
108
- ref={notificationRef}
89
+ ref={notificationsRef as any}
109
90
  prefixCls={configRef.value.prefixCls}
110
- motion={configRef.value.motion}
91
+ motion={configRef.value.motion as TransitionGroupProps | ((p: Placement) => TransitionGroupProps) | undefined}
111
92
  maxCount={configRef.value.maxCount}
93
+ pauseOnHover={configRef.value.pauseOnHover}
94
+ classNames={configRef.value.classNames}
95
+ styles={configRef.value.styles}
96
+ components={configRef.value.components}
112
97
  className={configRef.value.className}
113
- style={configRef.value.style}
98
+ style={configRef.value.style as ((p: Placement) => CSSProperties) | undefined}
114
99
  onAllRemoved={configRef.value.onAllRemoved}
115
- stack={configRef.value.stack}
100
+ stack={configRef.value.stack as StackInput | undefined}
116
101
  renderNotifications={configRef.value.renderNotifications}
117
102
  />
118
103
  )
119
104
 
120
- const taskQueue = shallowRef<Task[]>([])
121
-
122
- // ========================= Refs =========================
123
-
124
105
  const api: NotificationAPI = {
125
106
  open(config) {
126
- const mergedConfig = mergeConfig(shareConfig.value, config)
127
- if (mergedConfig.key === null || mergedConfig.key === undefined) {
128
- mergedConfig.key = `vc-notification-${uniqueKey}`
107
+ const merged = mergeConfig<NotificationListConfig>(shareConfig.value, config as any)
108
+ if (merged.key === null || merged.key === undefined) {
109
+ merged.key = `vc-notification-${uniqueKey}`
129
110
  uniqueKey += 1
130
111
  }
131
-
132
- taskQueue.value = [...taskQueue.value, { type: 'open', config: mergedConfig }]
112
+ taskQueue.value = [...taskQueue.value, { type: 'open', config: merged }]
133
113
  },
134
114
  close(key) {
135
115
  taskQueue.value = [...taskQueue.value, { type: 'close', key }]
@@ -139,41 +119,35 @@ export default function useNotification(rootConfig: MaybeRef<NotificationConfig>
139
119
  },
140
120
  }
141
121
 
142
- // ======================= Container ======================
143
- // React 18 should all in effect that we will check container in each render
144
- // Which means getContainer should be stable.
145
- onMounted(
146
- () => {
147
- container.value = resolveContainer()
148
- },
149
- )
122
+ onMounted(() => {
123
+ container.value = resolveContainer()
124
+ })
150
125
  watch(
151
126
  () => configRef.value.getContainer,
152
127
  () => {
153
128
  container.value = resolveContainer()
154
129
  },
155
130
  )
131
+
156
132
  watch(taskQueue, () => {
157
- if (notificationRef.value && taskQueue.value.length) {
158
- taskQueue.value.forEach((task) => {
133
+ if (notificationsRef.value && taskQueue.value.length) {
134
+ const tasks = taskQueue.value
135
+ tasks.forEach((task) => {
159
136
  switch (task.type) {
160
137
  case 'open':
161
- notificationRef.value?.open(task.config)
138
+ notificationsRef.value?.open(task.config)
162
139
  break
163
140
  case 'close':
164
- notificationRef.value?.close(task.key)
141
+ notificationsRef.value?.close(task.key)
165
142
  break
166
143
  case 'destroy':
167
- notificationRef.value?.destroy()
168
- break
169
- default:
144
+ notificationsRef.value?.destroy()
170
145
  break
171
146
  }
172
147
  })
173
- taskQueue.value = taskQueue.value.filter(task => !taskQueue.value.includes(task))
148
+ taskQueue.value = taskQueue.value.filter(task => !tasks.includes(task))
174
149
  }
175
150
  })
176
151
 
177
- // ======================== Return ========================
178
- return [api, contextHolder] as [NotificationAPI, () => VueNode]
152
+ return [api, contextHolder]
179
153
  }
@@ -1,32 +1,40 @@
1
- import type { ComputedRef, MaybeRef, ToRefs } from 'vue'
2
- import type { StackConfig } from '../interface'
3
- import { computed, reactive, toRefs, unref, watchEffect } from 'vue'
1
+ import type { ComputedRef, MaybeRef } from 'vue'
2
+ import { computed, unref } from 'vue'
3
+
4
+ export interface StackConfig {
5
+ threshold?: number
6
+ offset?: number
7
+ }
4
8
 
5
9
  const DEFAULT_OFFSET = 8
6
10
  const DEFAULT_THRESHOLD = 3
7
- const DEFAULT_GAP = 16
8
11
 
9
- type StackParams = Exclude<StackConfig, boolean>
12
+ type StackParams = Required<StackConfig>
13
+
14
+ export type StackInput = boolean | StackConfig
10
15
 
11
- type UseStack = (config?: MaybeRef<StackConfig | undefined>) => [ComputedRef<boolean>, ToRefs<StackParams>]
16
+ type UseStack = (
17
+ config?: MaybeRef<StackInput | undefined>,
18
+ ) => [ComputedRef<boolean>, ComputedRef<StackParams>]
12
19
 
13
20
  const useStack: UseStack = (config) => {
14
- const result: StackParams = reactive({
15
- offset: DEFAULT_OFFSET,
16
- threshold: DEFAULT_THRESHOLD,
17
- gap: DEFAULT_GAP,
18
- })
21
+ const enabled = computed(() => !!unref(config))
19
22
 
20
- watchEffect(() => {
21
- const _config = unref(config)
22
- if (_config && typeof _config === 'object') {
23
- result.offset = _config.offset ?? DEFAULT_OFFSET
24
- result.threshold = _config.threshold ?? DEFAULT_THRESHOLD
25
- result.gap = _config.gap ?? DEFAULT_GAP
23
+ const params = computed<StackParams>(() => {
24
+ const value = unref(config)
25
+ if (value && typeof value === 'object') {
26
+ return {
27
+ offset: value.offset ?? DEFAULT_OFFSET,
28
+ threshold: value.threshold ?? DEFAULT_THRESHOLD,
29
+ }
30
+ }
31
+ return {
32
+ offset: DEFAULT_OFFSET,
33
+ threshold: DEFAULT_THRESHOLD,
26
34
  }
27
35
  })
28
36
 
29
- return [computed(() => !!unref(config)), toRefs(result)]
37
+ return [enabled, params]
30
38
  }
31
39
 
32
40
  export default useStack
package/src/index.ts CHANGED
@@ -1,9 +1,35 @@
1
1
  import type { NotificationAPI, NotificationConfig } from './hooks/useNotification'
2
+ import type { ComponentsType, NotificationProps } from './Notification'
3
+ import type { NotificationProgressProps } from './Progress'
2
4
  import useNotification from './hooks/useNotification'
3
- import Notice from './Notice'
4
- import { useNotificationProvider } from './NotificationProvider'
5
+ import Notification from './Notification'
6
+ import NotificationList from './NotificationList'
7
+ import NotificationProvider, { useNotificationProvider } from './NotificationProvider'
8
+ import Progress from './Progress'
5
9
 
6
- export { Notice, useNotification, useNotificationProvider }
7
- export type { NotificationAPI, NotificationConfig }
10
+ export {
11
+ Notification,
12
+ NotificationList,
13
+ NotificationProvider,
14
+ Progress,
15
+ useNotification,
16
+ useNotificationProvider,
17
+ }
8
18
 
9
- export type { NoticeProps } from './Notice'
19
+ export type {
20
+ ComponentsType,
21
+ NotificationAPI,
22
+ NotificationConfig,
23
+ NotificationProgressProps,
24
+ NotificationProps,
25
+ }
26
+
27
+ export type {
28
+ NotificationClassNames,
29
+ NotificationListConfig,
30
+ NotificationListProps,
31
+ NotificationStyles,
32
+ Placement,
33
+ StackConfig,
34
+ StackInput,
35
+ } from './NotificationList'