-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathToastNotification.vue
More file actions
231 lines (203 loc) · 5.73 KB
/
ToastNotification.vue
File metadata and controls
231 lines (203 loc) · 5.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div
class="nc-toast"
:class="[typeClass, { 'nc-toast--clickable': onClick }]"
:role="role"
@click="onClick?.()">
<!-- Loading: text + spinner pushed to the right -->
<template v-if="isLoading">
<span class="nc-toast__message">{{ message }}</span>
<span class="nc-toast__loader" aria-hidden="true">
<NcLoadingIcon :size="20" />
</span>
</template>
<!-- Undo: text + undo button -->
<template v-else-if="isUndo">
<span class="nc-toast__message">{{ message }}</span>
<NcButton
class="nc-toast__undo-button"
variant="tertiary"
@click.stop="handleUndoClick">
{{ t('Undo') }}
</NcButton>
</template>
<!-- Default: plain string, HTML string, or arbitrary DOM Node -->
<template v-else>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-if="isHTML && isStringMessage" class="nc-toast__message" v-html="message" />
<span v-else-if="isStringMessage" class="nc-toast__message">{{ message }}</span>
<!-- Node content is appended in onMounted -->
<span v-else ref="nodeRef" class="nc-toast__message" />
</template>
<!-- Close button -->
<NcButton
v-if="close"
class="nc-toast__close"
variant="tertiary"
:aria-label="t('Close')"
@click.stop="dismiss">
<template #icon>
<NcIconSvgWrapper :path="mdiClose" :size="20" />
</template>
</NcButton>
</div>
</template>
<script setup lang="ts">
import { mdiClose } from '@mdi/js'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import { ToastType } from '../toast.ts'
import { t } from '../utils/l10n.js'
const props = withDefaults(defineProps<{
/** Text or DOM node to display as the toast body */
message: string | Node
/** Allow raw HTML in the message (only when message is a string) */
isHTML?: boolean
/** Visual type variant (controls left-border colour and element icons) */
type?: ToastType
/** Auto-dismiss delay in ms; -1 means permanent */
timeout: number
/** Show the close button */
close?: boolean
/** ARIA role derived from the aria-live level */
role: 'alert' | 'status'
/** Optional click handler for the whole toast */
onClick?: () => void
/** Undo callback, only used when type is UNDO */
onUndo?: (event: MouseEvent) => void
}>(), {
isHTML: false,
type: undefined,
close: true,
onClick: undefined,
onUndo: undefined,
})
const emit = defineEmits<{
/** Emitted when the toast should be removed (close btn, timer, or undo) */
dismiss: []
}>()
/** Mount target for arbitrary DOM Node content */
const nodeRef = ref<HTMLElement | null>(null)
let _timer: ReturnType<typeof setTimeout> | null = null
// Derived state
const typeClass = computed(() => props.type ? `nc-toast--${props.type.replace(/^toast-/, '')}` : null)
const isStringMessage = computed(() => typeof props.message === 'string')
const isLoading = computed(() => props.type === ToastType.LOADING)
const isUndo = computed(() => props.type === ToastType.UNDO)
/** Remove the toast (clears the auto-dismiss timer and emits dismiss). */
function dismiss(): void {
if (_timer !== null) {
clearTimeout(_timer)
_timer = null
}
emit('dismiss')
}
/**
* Handle click on the undo button: stop propagation,
* call the undo callback, and dismiss the toast.
*
* @param event The click event from the undo button
*/
function handleUndoClick(event: MouseEvent): void {
// Prevent the click from bubbling up to the toast's onClick handler
event.stopPropagation()
props.onUndo?.(event)
dismiss()
}
onMounted(() => {
// Attach arbitrary DOM Node content into the message slot
if (props.message instanceof Node && nodeRef.value) {
nodeRef.value.appendChild(props.message)
}
// Start the auto-dismiss countdown
if (props.timeout > 0) {
_timer = setTimeout(dismiss, props.timeout)
}
})
onUnmounted(() => {
if (_timer !== null) {
clearTimeout(_timer)
_timer = null
}
})
defineExpose({ hide: dismiss })
</script>
<style lang="scss">
$spacing: 12px;
.nc-toast-container {
position: fixed;
top: calc(var(--header-height) + 10px);
right: 10px;
z-index: 10100;
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-end;
// Individual toasts manage their own pointer-events
pointer-events: none;
}
@keyframes nc-toast-in {
from {
opacity: 0;
transform: translateY(-6px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.nc-toast {
min-width: 200px;
background-color: var(--color-main-background);
color: var(--color-main-text);
box-shadow: 0 0 6px 0 var(--color-box-shadow);
padding: 0 $spacing;
border-radius: var(--border-radius);
display: flex;
align-items: center;
min-height: var(--clickable-area-large);
pointer-events: auto;
animation: nc-toast-in var(--animation-slow) ease-out;
// Modifiers
&--clickable {
cursor: pointer;
}
&--error {
border-left: 3px solid var(--color-element-error, var(--color-error));
}
&--info {
border-left: 3px solid var(--color-element-info, var(--color-primary));
}
&--warning {
border-left: 3px solid var(--color-element-warning, var(--color-warning));
}
&--success {
border-left: 3px solid var(--color-element-success, var(--color-success));
}
&--undo {
border-left: 3px solid var(--color-element-success, var(--color-success));
}
&--loading {
border-left: 3px solid var(--color-element-info, var(--color-primary));
}
// Elements
&__message {
flex: 1;
padding: $spacing 0;
}
&__loader,
&__close,
&__undo-button {
display: flex;
align-items: center;
margin-left: $spacing;
flex-shrink: 0;
}
}
</style>