@v-c/notification 2.0.0 → 2.0.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.
@@ -1,322 +0,0 @@
1
- import type { CSSProperties, TransitionGroupProps } from 'vue'
2
- import type { Key, StackConfig } from '../interface'
3
- import type {
4
- ComponentsType,
5
- NotificationClassNames as NoticeClassNames,
6
- NotificationProps,
7
- NotificationStyles as NoticeStyles,
8
- } from '../Notification'
9
- import { clsx } from '@v-c/util'
10
- import { unrefElement } from '@v-c/util/dist/vueuse/unref-element'
11
- import { computed, defineComponent, ref, shallowRef, toRef, TransitionGroup, watch, watchEffect } from 'vue'
12
-
13
- /**
14
- * Map Vue's TransitionGroup enter/leave class hooks onto the rc-motion
15
- * style class names that antdv-next 6.4.0 notification styles target
16
- * (-enter-start / -enter-active / -leave-start / -leave-active).
17
- *
18
- * The shared @v-c/util getTransitionGroupProps puts -leave-active in the
19
- * leaveActiveClass, which means the notice jumps straight to opacity:0
20
- * on leave without animating. Wire each phase to the correct rc-motion
21
- * suffix so the opacity/transform transition runs.
22
- */
23
- function buildMotionGroupProps(name: string, override?: Partial<TransitionGroupProps>): TransitionGroupProps {
24
- return {
25
- name,
26
- appear: true,
27
- // ENTER: from = opacity 0 (-enter-start / -appear-start)
28
- // to = opacity 1 (-enter-active / -appear-active)
29
- enterFromClass: `${name} ${name}-enter ${name}-appear ${name}-enter-start ${name}-appear-start`,
30
- enterActiveClass: `${name} ${name}-enter ${name}-appear`,
31
- enterToClass: `${name} ${name}-enter ${name}-appear ${name}-enter-active ${name}-appear-active`,
32
- // LEAVE: from = opacity 1 (-leave-start)
33
- // to = opacity 0 (-leave-active)
34
- leaveFromClass: `${name} ${name}-leave ${name}-leave-start`,
35
- leaveActiveClass: `${name} ${name}-leave`,
36
- leaveToClass: `${name} ${name}-leave ${name}-leave-active`,
37
- moveClass: `${name} ${name}-move`,
38
- ...override,
39
- }
40
- }
41
- import useListPosition from '../hooks/useListPosition'
42
- import useStack from '../hooks/useStack'
43
- import Notification from '../Notification'
44
- import { useNotificationContext } from '../NotificationProvider'
45
- import Content from './Content'
46
-
47
- export type Placement = 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight'
48
-
49
- export type { StackConfig }
50
-
51
- export interface NotificationListConfig extends Omit<NotificationProps, 'prefixCls'> {
52
- key: Key
53
- placement?: Placement
54
- times?: number
55
- }
56
-
57
- export interface NotificationClassNames extends NoticeClassNames {
58
- list?: string
59
- listContent?: string
60
- }
61
-
62
- export interface NotificationStyles extends NoticeStyles {
63
- list?: CSSProperties
64
- listContent?: CSSProperties
65
- }
66
-
67
- export interface NotificationListProps {
68
- configList?: NotificationListConfig[]
69
- prefixCls?: string
70
- placement: Placement
71
- pauseOnHover?: boolean
72
- classNames?: NotificationClassNames
73
- styles?: NotificationStyles
74
- components?: ComponentsType
75
- stack?: StackConfig
76
- motion?: TransitionGroupProps | ((placement: Placement) => TransitionGroupProps)
77
- className?: string
78
- style?: CSSProperties
79
- onNoticeClose?: (key: Key) => void
80
- onAllRemoved?: (placement: Placement) => void
81
- }
82
-
83
- const noticeSlotKeys: (keyof NoticeClassNames)[] = [
84
- 'wrapper',
85
- 'root',
86
- 'icon',
87
- 'section',
88
- 'title',
89
- 'description',
90
- 'actions',
91
- 'close',
92
- 'progress',
93
- ]
94
-
95
- function fillClassNames(
96
- list: (NotificationClassNames | undefined)[],
97
- ): NotificationClassNames {
98
- return noticeSlotKeys.reduce<NotificationClassNames>((merged, key) => {
99
- merged[key] = clsx(...list.map(item => item?.[key]))
100
- return merged
101
- }, {})
102
- }
103
-
104
- function fillStyles(
105
- list: (NotificationStyles | undefined)[],
106
- ): NotificationStyles {
107
- return noticeSlotKeys.reduce<NotificationStyles>((merged, key) => {
108
- merged[key] = Object.assign({}, ...list.map(item => item?.[key]))
109
- return merged
110
- }, {})
111
- }
112
-
113
- function getIndex(keys: { key: Key }[], key: Key): number | undefined {
114
- const strKey = String(key)
115
- const index = keys.findIndex(item => String(item.key) === strKey)
116
- if (index === -1) {
117
- return undefined
118
- }
119
- return keys.length - index - 1
120
- }
121
-
122
- const NotificationList = defineComponent<NotificationListProps>(
123
- (props, { attrs }) => {
124
- const ctx = useNotificationContext()
125
-
126
- const configList = computed(() => props.configList ?? [])
127
- const keys = computed(() =>
128
- configList.value.map(config => ({ ...config, key: String(config.key) as Key })),
129
- )
130
-
131
- // ====================== Stack State =======================
132
- const stackConfig = toRef(props, 'stack')
133
- const [stackEnabled, stackParams] = useStack(stackConfig)
134
- const listHovering = shallowRef(false)
135
- const expanded = computed(() =>
136
- stackEnabled.value && (listHovering.value || keys.value.length <= (stackParams.threshold?.value ?? 0)),
137
- )
138
-
139
- const stackPosition = computed(() => {
140
- if (!stackEnabled.value || expanded.value) {
141
- return undefined
142
- }
143
- return {
144
- offset: stackParams.offset?.value,
145
- threshold: stackParams.threshold?.value,
146
- }
147
- })
148
-
149
- // ====================== List Measure ======================
150
- const gap = ref(0)
151
- const contentRef = ref<{ nativeElement: { value: HTMLDivElement | null } } | null>(null)
152
- const [position, setNodeSize] = useListPosition(keys as any, stackPosition as any, gap)
153
-
154
- const hasConfigList = computed(() => !!configList.value.length)
155
- watchEffect(() => {
156
- if (!hasConfigList.value) {
157
- return
158
- }
159
- const listNode = unrefElement<HTMLDivElement>(contentRef.value?.nativeElement as any)
160
- if (!listNode) {
161
- return
162
- }
163
- const { gap: cssGap, rowGap } = window.getComputedStyle(listNode)
164
- const nextGap = Number.parseFloat(rowGap || cssGap) || 0
165
- if (gap.value !== nextGap) {
166
- gap.value = nextGap
167
- }
168
- }, { flush: 'post' })
169
-
170
- // Notify when list becomes empty (after motion finished).
171
- const checkAllClosed = () => {
172
- if (configList.value.length === 0) {
173
- props.onAllRemoved?.(props.placement)
174
- }
175
- }
176
-
177
- // Compute motion props per placement.
178
- const placementMotion = computed(() => {
179
- if (typeof props.motion === 'function') {
180
- return props.placement ? props.motion(props.placement) : undefined
181
- }
182
- return props.motion
183
- })
184
-
185
- // Cleanup node sizes when items leave permanently.
186
- watch(keys, (next, prev) => {
187
- if (!prev) {
188
- return
189
- }
190
- const nextKeySet = new Set(next.map(item => String(item.key)))
191
- prev.forEach((item) => {
192
- const key = String(item.key)
193
- if (!nextKeySet.has(key)) {
194
- setNodeSize(key, null)
195
- }
196
- })
197
- })
198
-
199
- return () => {
200
- const {
201
- prefixCls = 'vc-notification',
202
- pauseOnHover,
203
- classNames: ncs,
204
- styles: nss,
205
- components,
206
- placement,
207
- className,
208
- style,
209
- onNoticeClose,
210
- } = props
211
-
212
- const listPrefixCls = `${prefixCls}-list`
213
- const positionResult = position.value
214
-
215
- let motionGroupProps: TransitionGroupProps = {}
216
- if (placementMotion.value?.name) {
217
- motionGroupProps = buildMotionGroupProps(placementMotion.value.name, placementMotion.value)
218
- }
219
-
220
- const renderItems = () =>
221
- keys.value.map((config) => {
222
- const {
223
- key,
224
- placement: _itemPlacement,
225
- classNames: configClassNames,
226
- styles: configStyles,
227
- className: configClassName,
228
- style: configStyle,
229
- // Extract onClose so the spread below does not also bind it.
230
- // Vue would otherwise merge our `onClose={...}` and the spread's
231
- // `onClose` into an Array, breaking `props.onClose?.()` in the
232
- // notice itself.
233
- onClose: configOnClose,
234
- ...notificationConfig
235
- } = config
236
- const strKey = String(key)
237
- const notificationIndex = getIndex(keys.value, key)
238
- const stackInThreshold
239
- = stackEnabled.value && notificationIndex !== undefined && notificationIndex < (stackParams.threshold?.value ?? 0)
240
-
241
- return (
242
- <Notification
243
- key={strKey}
244
- {...notificationConfig}
245
- ref={(el: any) => {
246
- const node = unrefElement<HTMLDivElement>(el?.nativeElement as any)
247
- setNodeSize(strKey, node ?? null)
248
- }}
249
- prefixCls={prefixCls}
250
- class={clsx((ctx.value as any)?.classNames?.notice, configClassName)}
251
- style={configStyle}
252
- classNames={fillClassNames([ncs, configClassNames])}
253
- styles={fillStyles([nss, configStyles])}
254
- components={{
255
- ...components,
256
- ...(config as NotificationListConfig).components,
257
- }}
258
- hovering={stackEnabled.value && listHovering.value}
259
- pauseOnHover={config.pauseOnHover ?? pauseOnHover}
260
- offset={positionResult.notificationPosition.get(strKey)}
261
- notificationIndex={notificationIndex}
262
- stackInThreshold={stackInThreshold}
263
- onClose={() => {
264
- configOnClose?.()
265
- onNoticeClose?.(key)
266
- }}
267
- />
268
- )
269
- })
270
-
271
- return (
272
- <div
273
- class={clsx(
274
- prefixCls,
275
- listPrefixCls,
276
- `${prefixCls}-${placement}`,
277
- (ctx.value as any)?.classNames?.list,
278
- className,
279
- ncs?.list,
280
- (attrs as any).class,
281
- {
282
- [`${prefixCls}-stack`]: stackEnabled.value,
283
- [`${prefixCls}-stack-expanded`]: expanded.value,
284
- [`${listPrefixCls}-hovered`]: listHovering.value,
285
- },
286
- )}
287
- onMouseenter={() => {
288
- listHovering.value = true
289
- }}
290
- onMouseleave={() => {
291
- listHovering.value = false
292
- }}
293
- style={{ ...nss?.list, ...style, ...((attrs as any).style ?? {}) } as CSSProperties}
294
- >
295
- <Content
296
- ref={contentRef as any}
297
- listPrefixCls={listPrefixCls}
298
- height={positionResult.totalHeight}
299
- topNoticeHeight={positionResult.topNoticeHeight}
300
- topNoticeWidth={positionResult.topNoticeWidth}
301
- className={ncs?.listContent}
302
- style={nss?.listContent}
303
- >
304
- <TransitionGroup
305
- appear
306
- {...motionGroupProps}
307
- onAfterLeave={checkAllClosed}
308
- >
309
- {renderItems()}
310
- </TransitionGroup>
311
- </Content>
312
- </div>
313
- )
314
- }
315
- },
316
- {
317
- name: 'NotificationList',
318
- inheritAttrs: false,
319
- },
320
- )
321
-
322
- export default NotificationList
@@ -1,19 +0,0 @@
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
- }
@@ -1,153 +0,0 @@
1
- import type { CSSProperties, TransitionGroupProps } from 'vue'
2
- import type { VueNode } from '@v-c/util/dist/type'
3
- import type { InnerOpenConfig, Key, NotificationListConfig, Placement, Placements, StackConfig } from './interface'
4
- import type { ComponentsType } from './Notification'
5
- import { defineComponent, shallowRef, Teleport, watch } from 'vue'
6
- import NotificationList, {
7
- type NotificationClassNames,
8
- type NotificationStyles,
9
- } from './NotificationList'
10
-
11
- export interface NotificationsProps {
12
- prefixCls?: string
13
- motion?: TransitionGroupProps | ((placement: Placement) => TransitionGroupProps)
14
- container?: HTMLElement | ShadowRoot
15
- maxCount?: number
16
- pauseOnHover?: boolean
17
- classNames?: NotificationClassNames
18
- styles?: NotificationStyles
19
- components?: ComponentsType
20
- className?: (placement: Placement) => string
21
- style?: (placement: Placement) => CSSProperties
22
- onAllRemoved?: VoidFunction
23
- stack?: StackConfig
24
- renderNotifications?: (
25
- node: VueNode,
26
- info: { prefixCls: string, key: Key },
27
- ) => VueNode
28
- }
29
-
30
- export interface NotificationsRef {
31
- open: (config: NotificationListConfig) => void
32
- close: (key: Key) => void
33
- destroy: () => void
34
- }
35
-
36
- const defaults = {
37
- prefixCls: 'vc-notification',
38
- } as NotificationsProps
39
-
40
- const Notifications = defineComponent<NotificationsProps>(
41
- (props = defaults, { expose }) => {
42
- const configList = shallowRef<NotificationListConfig[]>([])
43
-
44
- const onNoticeClose = (key: Key) => {
45
- configList.value = configList.value.filter(item => item.key !== key)
46
- }
47
-
48
- expose({
49
- open: (config: NotificationListConfig) => {
50
- const list = configList.value
51
- let clone = [...list]
52
- const index = clone.findIndex(item => item.key === config.key)
53
- const innerConfig: InnerOpenConfig = { ...config }
54
- if (index >= 0) {
55
- innerConfig.times = ((list[index] as InnerOpenConfig)?.times ?? 0) + 1
56
- clone[index] = innerConfig
57
- }
58
- else {
59
- innerConfig.times = 0
60
- clone.push(innerConfig)
61
- }
62
- const maxCount = props.maxCount ?? 0
63
- if (maxCount > 0 && clone.length > maxCount) {
64
- clone = clone.slice(-maxCount)
65
- }
66
- configList.value = clone
67
- },
68
- close: onNoticeClose,
69
- destroy: () => {
70
- configList.value = []
71
- },
72
- })
73
-
74
- const placements = shallowRef<Placements>({})
75
-
76
- watch(configList, () => {
77
- const next: Placements = {}
78
- configList.value.forEach((config) => {
79
- const placement = (config.placement ?? 'topRight') as Placement
80
- next[placement] = next[placement] || []
81
- next[placement]!.push(config)
82
- })
83
- // Keep existing placements so empty lists can finish leave motion.
84
- Object.keys(placements.value).forEach((placement) => {
85
- next[placement as Placement] = next[placement as Placement] || []
86
- })
87
- placements.value = next
88
- })
89
-
90
- const onAllNoticeRemoved = (placement: Placement) => {
91
- const clone = { ...placements.value }
92
- const list = clone[placement] || []
93
- if (!list.length) {
94
- delete clone[placement]
95
- }
96
- placements.value = clone
97
- }
98
-
99
- const emptyRef = shallowRef(false)
100
- watch(placements, () => {
101
- if (Object.keys(placements.value).length > 0) {
102
- emptyRef.value = true
103
- }
104
- else if (emptyRef.value) {
105
- props?.onAllRemoved?.()
106
- emptyRef.value = false
107
- }
108
- })
109
-
110
- return () => {
111
- const { container } = props
112
- const prefixCls = props.prefixCls ?? defaults.prefixCls!
113
- if (!container) {
114
- return null
115
- }
116
-
117
- return (
118
- <Teleport to={container}>
119
- {Object.keys(placements.value).map((rawPlacement) => {
120
- const placement = rawPlacement as Placement
121
- const placementConfigList = placements.value[placement]
122
- const list = (
123
- <NotificationList
124
- key={placement}
125
- configList={placementConfigList}
126
- placement={placement}
127
- prefixCls={prefixCls}
128
- pauseOnHover={props.pauseOnHover}
129
- classNames={props.classNames}
130
- styles={props.styles}
131
- components={props.components}
132
- className={props.className?.(placement)}
133
- style={props.style?.(placement)}
134
- motion={props.motion}
135
- stack={props.stack}
136
- onAllRemoved={onAllNoticeRemoved}
137
- onNoticeClose={onNoticeClose}
138
- />
139
- )
140
- return props.renderNotifications
141
- ? props.renderNotifications(list, { prefixCls, key: placement })
142
- : list
143
- })}
144
- </Teleport>
145
- )
146
- }
147
- },
148
- {
149
- name: 'Notifications',
150
- },
151
- )
152
-
153
- export default Notifications
package/src/Progress.tsx DELETED
@@ -1,27 +0,0 @@
1
- import type { CSSProperties } from 'vue'
2
- import { defineComponent } from 'vue'
3
-
4
- export interface NotificationProgressProps {
5
- className?: string
6
- style?: CSSProperties
7
- percent: number
8
- }
9
-
10
- const Progress = defineComponent<NotificationProgressProps>(
11
- (props) => {
12
- return () => (
13
- <progress
14
- class={props.className}
15
- max="100"
16
- value={props.percent}
17
- style={props.style}
18
- />
19
- )
20
- },
21
- {
22
- name: 'NotificationProgress',
23
- inheritAttrs: false,
24
- },
25
- )
26
-
27
- export default Progress
@@ -1,53 +0,0 @@
1
- import type { AriaAttributes, ComputedRef } from 'vue'
2
- import type { VueNode } from '@v-c/util/dist/type'
3
- import pickAttrs from '@v-c/util/dist/pickAttrs'
4
- import { computed } from 'vue'
5
-
6
- export type ClosableConfig = {
7
- closeIcon?: VueNode
8
- disabled?: boolean
9
- onClose?: VoidFunction
10
- } & AriaAttributes & Record<`data-${string}`, unknown>
11
-
12
- export type ClosableType = boolean | ClosableConfig | null | undefined
13
-
14
- export interface ParsedClosableConfig extends ClosableConfig {
15
- closeIcon: VueNode
16
- disabled: boolean
17
- }
18
-
19
- /**
20
- * Normalizes the closable option into a boolean flag, parsed config, and
21
- * aria props for the close button. Mirrors rc-notification@2.0 useClosable.
22
- */
23
- export default function useClosable(
24
- closable: ComputedRef<ClosableType>,
25
- ): [ComputedRef<boolean>, ComputedRef<ParsedClosableConfig>, ComputedRef<Record<string, unknown>>] {
26
- const closableObj = computed<ClosableConfig>(() => {
27
- const value = closable.value
28
- if (value === false) {
29
- return { closeIcon: null, disabled: true }
30
- }
31
- if (typeof value === 'object' && value !== null) {
32
- return value
33
- }
34
- return {}
35
- })
36
-
37
- const closableConfig = computed<ParsedClosableConfig>(() => {
38
- const obj = closableObj.value
39
- return {
40
- ...obj,
41
- closeIcon: 'closeIcon' in obj ? obj.closeIcon : '×',
42
- disabled: obj.disabled ?? false,
43
- }
44
- })
45
-
46
- const closableAriaProps = computed(() => pickAttrs(closableConfig.value, true))
47
-
48
- return [
49
- computed(() => !!closable.value),
50
- closableConfig,
51
- closableAriaProps,
52
- ]
53
- }
@@ -1,69 +0,0 @@
1
- import type { ComputedRef, Ref } from 'vue'
2
- import type { Key } from '../../interface'
3
- import { computed } from 'vue'
4
- import useSizes from './useSizes'
5
-
6
- export interface ListPositionStackConfig {
7
- threshold?: number
8
- offset?: number
9
- }
10
-
11
- /**
12
- * Calculates each notification's position and the full list height.
13
- * Mirrors rc-notification@2.0 useListPosition.
14
- */
15
- export default function useListPosition(
16
- configList: ComputedRef<{ key: Key }[]>,
17
- stack: ComputedRef<ListPositionStackConfig | undefined>,
18
- gap: Ref<number>,
19
- ) {
20
- const [sizeMap, setNodeSize] = useSizes()
21
-
22
- const result = computed(() => {
23
- let offsetY = 0
24
- let nextTotalHeight = 0
25
- const stackParams = stack.value
26
- const stackThreshold = stackParams?.threshold ?? 0
27
- const stackOffset = stackParams?.offset ?? 0
28
- const notificationPosition = new Map<string, number>()
29
- let topNoticeHeight: number | undefined
30
- let topNoticeWidth: number | undefined
31
-
32
- configList.value
33
- .slice()
34
- .reverse()
35
- .forEach((config, index) => {
36
- // Walk from newest to oldest so each notice can be positioned after the ones below it.
37
- const key = String(config.key)
38
- const height = sizeMap.value[key]?.height ?? 0
39
- const y = stackParams && index > 0 ? offsetY + stackOffset - height : offsetY
40
-
41
- notificationPosition.set(key, y)
42
-
43
- if (index === 0) {
44
- topNoticeHeight = height
45
- topNoticeWidth = sizeMap.value[key]?.width ?? 0
46
- }
47
-
48
- if (!stackParams || index < stackThreshold) {
49
- nextTotalHeight = Math.max(nextTotalHeight, y + height)
50
- }
51
-
52
- if (stackParams) {
53
- offsetY = y + height
54
- }
55
- else {
56
- offsetY += height + gap.value
57
- }
58
- })
59
-
60
- return {
61
- notificationPosition,
62
- totalHeight: nextTotalHeight,
63
- topNoticeHeight,
64
- topNoticeWidth,
65
- }
66
- })
67
-
68
- return [result, setNodeSize] as const
69
- }
@@ -1,43 +0,0 @@
1
- import { shallowRef } from 'vue'
2
-
3
- export interface NodeSize {
4
- width: number
5
- height: number
6
- }
7
-
8
- export type NodeSizeMap = Record<string, NodeSize>
9
-
10
- /**
11
- * Stores measured node sizes by key and exposes a callback to update them.
12
- * Mirrors rc-notification@2.0 useSizes.
13
- */
14
- export default function useSizes() {
15
- const sizeMap = shallowRef<NodeSizeMap>({})
16
-
17
- const setNodeSize = (key: string, node: HTMLDivElement | null) => {
18
- if (!node) {
19
- if (!(key in sizeMap.value)) {
20
- return
21
- }
22
- const next = { ...sizeMap.value }
23
- delete next[key]
24
- sizeMap.value = next
25
- return
26
- }
27
-
28
- const nextSize: NodeSize = {
29
- width: node.offsetWidth,
30
- height: node.offsetHeight,
31
- }
32
- const prev = sizeMap.value[key]
33
- if (prev && prev.width === nextSize.width && prev.height === nextSize.height) {
34
- return
35
- }
36
- sizeMap.value = {
37
- ...sizeMap.value,
38
- [key]: nextSize,
39
- }
40
- }
41
-
42
- return [sizeMap, setNodeSize] as const
43
- }