Skip to content

Commit 43a30d8

Browse files
authored
Merge pull request #6564 from nextcloud-libraries/backport/6006/next
[next] feat(NcDialog): Allow to catch `reset` event
2 parents f1260e1 + e41c5bb commit 43a30d8

File tree

5 files changed

+160
-16
lines changed

5 files changed

+160
-16
lines changed

src/components/NcButton/NcButton.vue

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -686,9 +686,7 @@ export default defineComponent({
686686
hasIcon
687687
? h('span', {
688688
class: 'button-vue__icon',
689-
attrs: {
690-
'aria-hidden': 'true',
691-
},
689+
'aria-hidden': 'true',
692690
},
693691
[this.$slots.icon?.()],
694692
)

src/components/NcDialog/NcDialog.vue

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Note that this is not possible if the dialog contains a navigation!
9595
name="Choose a name"
9696
v-model:open="showDialog"
9797
@submit="currentName = newName"
98+
@reset="newName = ''"
9899
@closing="newName = ''">
99100
<NcTextField v-model="newName"
100101
label="New name"
@@ -115,6 +116,10 @@ export default {
115116
newName: '',
116117
currentName: 'none yet.',
117118
buttons: [
119+
{
120+
label: 'Reset',
121+
nativeType: 'reset',
122+
},
118123
{
119124
label: 'Submit',
120125
type: 'primary',
@@ -244,7 +249,7 @@ export default {
244249
<NcDialogButton v-for="(button, idx) in buttons"
245250
:key="idx"
246251
v-bind="button"
247-
@click="handleButtonClose" />
252+
@click="(_, result) => handleButtonClose(button, result)" />
248253
</slot>
249254
</div>
250255
</component>
@@ -383,7 +388,7 @@ export default defineComponent({
383388
},
384389
385390
/**
386-
* Optionally pass additionaly classes which will be set on the navigation for custom styling
391+
* Optionally pass additional classes which will be set on the navigation for custom styling
387392
* @default ''
388393
* @example
389394
* ```html
@@ -427,7 +432,7 @@ export default defineComponent({
427432
},
428433
429434
/**
430-
* Optionally pass additionaly classes which will be set on the content wrapper for custom styling
435+
* Optionally pass additional classes which will be set on the content wrapper for custom styling
431436
* @default ''
432437
*/
433438
contentClasses: {
@@ -437,7 +442,7 @@ export default defineComponent({
437442
},
438443
439444
/**
440-
* Optionally pass additionaly classes which will be set on the dialog itself
445+
* Optionally pass additional classes which will be set on the dialog itself
441446
* (the default `class` attribute will be set on the modal wrapper)
442447
* @default ''
443448
*/
@@ -516,6 +521,16 @@ export default defineComponent({
516521
/** Forwarded HTMLFormElement submit event (only if `is-form` is set) */
517522
emit('submit', event)
518523
},
524+
/**
525+
* @param {Event} event Form submit event
526+
*/
527+
reset(event) {
528+
event.preventDefault()
529+
/**
530+
* Forwarded HTMLFormElement reset event (only if `is-form` is set).
531+
*/
532+
emit('reset', event)
533+
},
519534
}
520535
: {},
521536
)
@@ -528,12 +543,14 @@ export default defineComponent({
528543
// Because NcModal does not emit `close` when show prop is changed
529544
/**
530545
* Handle clicking a dialog button -> should close
531-
* @param {MouseEvent} event The click event
546+
* @param {MouseEvent} button The button that was clicked
532547
* @param {unknown} result Result of the callback function
533548
*/
534-
const handleButtonClose = (event, result) => {
535-
// Skip close if invalid dialog
536-
if (dialogTagName.value === 'form' && !dialogElement.value.reportValidity()) {
549+
function handleButtonClose(button, result) {
550+
// Skip close on submit if invalid dialog
551+
if (button.nativeType === 'submit'
552+
&& dialogTagName.value === 'form'
553+
&& !dialogElement.value.reportValidity()) {
537554
return
538555
}
539556
handleClosing(result)

src/components/NcDialogButton/NcDialogButton.vue

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,18 @@ Dialog button component used by NcDialog in the actions slot to display the butt
2828
<script setup lang="ts">
2929
import type { PropType } from 'vue'
3030
import { ref } from 'vue'
31+
import { t } from '../../l10n.js'
3132
3233
import NcButton, { ButtonNativeType, ButtonType } from '../NcButton/index'
3334
import NcIconSvgWrapper from '../NcIconSvgWrapper/index.js'
3435
import NcLoadingIcon from '../NcLoadingIcon/index.js'
35-
import { t } from '../../l10n.js'
3636
3737
const props = defineProps({
3838
/**
3939
* The function that will be called when the button is pressed.
40-
* If the function returns `false` the click is ignored and the dialog will not be closed.
40+
* If the function returns `false` the click is ignored and the dialog will not be closed,
41+
* which is the default behavior of "reset"-buttons.
42+
*
4143
* @type {() => unknown|false|Promise<unknown|false>}
4244
*/
4345
callback: {
@@ -108,17 +110,19 @@ const isLoading = ref(false)
108110
109111
/**
110112
* Handle clicking the button
111-
* @param {MouseEvent} e The click event
113+
* @param e The click event
112114
*/
113-
const handleClick = async (e) => {
115+
async function handleClick(e: MouseEvent) {
114116
// Do not re-emit while loading
115117
if (isLoading.value) {
116118
return
117119
}
118120
119121
isLoading.value = true
120122
try {
121-
const result = await props.callback?.()
123+
// for reset buttons the default is "false"
124+
const fallback = props.nativeType === 'reset' ? false : undefined
125+
const result = await props.callback?.() ?? fallback
122126
if (result !== false) {
123127
/**
124128
* The click event (`MouseEvent`) and the value returned by the callback

tests/setup.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import { vi } from 'vitest'
77
import OC from './OC.js'
8+
// TODO: Remove when we support Node 22
9+
import 'core-js/actual/promise/with-resolvers.js'
810

911
vi.stubGlobal('OC', OC)
1012
vi.stubGlobal('appName', 'nextcloud-vue')
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { describe, expect, it } from 'vitest'
7+
import { mount } from '@vue/test-utils'
8+
import { nextTick } from 'vue'
9+
import { ButtonNativeType } from '../../../../src/components/NcButton'
10+
import NcDialogButton from '../../../../src/components/NcDialogButton/NcDialogButton.vue'
11+
12+
describe('NcDialogButton', () => {
13+
it.each([
14+
[ButtonNativeType.Reset],
15+
[ButtonNativeType.Button],
16+
[ButtonNativeType.Submit],
17+
])('forwards the native type', async (nativeType: ButtonNativeType) => {
18+
const wrapper = mount(NcDialogButton, {
19+
props: {
20+
label: 'button',
21+
nativeType,
22+
},
23+
})
24+
expect(wrapper.find('button').attributes('type')).toBe(nativeType)
25+
})
26+
27+
it('handles click', async () => {
28+
const wrapper = mount(NcDialogButton, {
29+
props: {
30+
label: 'button',
31+
},
32+
})
33+
await wrapper.find('button').trigger('click')
34+
expect(wrapper.emitted('click')).toHaveLength(1)
35+
expect(wrapper.emitted('click')![0]).toHaveLength(2)
36+
})
37+
38+
it('has mouse event as click payload', async () => {
39+
const wrapper = mount(NcDialogButton, {
40+
props: {
41+
label: 'button',
42+
},
43+
})
44+
const event = { id: 'my-event' }
45+
await wrapper.find('button').trigger('click', event)
46+
expect(wrapper.emitted('click')).toHaveLength(1)
47+
expect(wrapper.emitted('click')![0][0]).toMatchObject(event)
48+
})
49+
50+
it('has callback response as second click event payload', async () => {
51+
const wrapper = mount(NcDialogButton, {
52+
props: {
53+
label: 'button',
54+
callback: () => 'payload',
55+
},
56+
})
57+
await wrapper.find('button').trigger('click')
58+
expect(wrapper.emitted('click')).toHaveLength(1)
59+
expect(wrapper.emitted('click')![0][1]).toBe('payload')
60+
})
61+
62+
it('callback defaults to undefined', async () => {
63+
const wrapper = mount(NcDialogButton, {
64+
props: {
65+
label: 'button',
66+
},
67+
})
68+
await wrapper.find('button').trigger('click')
69+
expect(wrapper.emitted('click')).toHaveLength(1)
70+
expect(wrapper.emitted('click')![0]).toHaveLength(2)
71+
expect(wrapper.emitted('click')![0][1]).toBeUndefined()
72+
})
73+
74+
it('reset-button callback defaults to false', async () => {
75+
const wrapper = mount(NcDialogButton, {
76+
props: {
77+
label: 'button',
78+
nativeType: ButtonNativeType.Reset,
79+
},
80+
})
81+
await wrapper.find('button').trigger('click')
82+
expect(wrapper.emitted('click')).toBe(undefined)
83+
})
84+
85+
it('reset-button with callback emits click', async () => {
86+
const wrapper = mount(NcDialogButton, {
87+
props: {
88+
label: 'button',
89+
nativeType: ButtonNativeType.Reset,
90+
callback: () => true,
91+
},
92+
})
93+
await wrapper.find('button').trigger('click')
94+
expect(wrapper.emitted('click')).toHaveLength(1)
95+
})
96+
97+
it('has a loading state while the callback is awaited', async () => {
98+
const { promise, resolve } = Promise.withResolvers<void>()
99+
const wrapper = mount(NcDialogButton, {
100+
props: {
101+
label: 'button',
102+
callback: () => promise,
103+
},
104+
})
105+
// click the button
106+
const button = wrapper.find('button')
107+
await button.trigger('click')
108+
await nextTick()
109+
// no event because it is still resolving
110+
expect(wrapper.emitted('click')).toBeUndefined()
111+
// see there is the loading indicator
112+
expect(button.find('[aria-label="Loading …"]').exists()).toBe(true)
113+
// resolve the callback
114+
resolve()
115+
await nextTick()
116+
// see there is the event now
117+
expect(wrapper.emitted('click')).toHaveLength(1)
118+
await nextTick()
119+
// and the loading indicator is gone
120+
// see there is the loading indicator
121+
expect(button.find('[aria-label="Loading …"]').exists()).toBe(false)
122+
})
123+
})

0 commit comments

Comments
 (0)