Skip to content

Commit 6beb1a0

Browse files
authored
Merge pull request #6035 from nextcloud-libraries/backport/6005/next
[next] feat(NcDialogButton): Allow to return `false` from callback to keep dialog open
2 parents 285780d + 28860a1 commit 6beb1a0

File tree

4 files changed

+184
-85
lines changed

4 files changed

+184
-85
lines changed

l10n/messages.pot

+4
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ msgstr ""
191191
msgid "Load more \"{options}\""
192192
msgstr ""
193193

194+
#. TRANSLATORS: The button is in a loading state
195+
msgid "Loading …"
196+
msgstr ""
197+
194198
#. TRANSLATORS: A color name for RGB(45, 115, 190)
195199
msgid "Mariner"
196200
msgstr ""

src/components/NcDialog/NcDialog.vue

+86-7
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ Note that this is not possible if the dialog contains a navigation!
106106
</div>
107107
</template>
108108
<script>
109-
import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
110109
import IconCheck from '@mdi/svg/svg/check.svg?raw'
111110

112111
export default {
@@ -128,6 +127,82 @@ export default {
128127
}
129128
</script>
130129
```
130+
131+
### Loading buttons
132+
Sometimes a dialog ends with a request and this request might fail due to server-side-validation.
133+
In this case it is often desired to keep the dialog open, this can be done by returning `false` from the button callback,
134+
to not block this callback should return a `Promise<false>`.
135+
136+
It is also possible to get the result of the callback from the dialog, as the result is passed as the payload of the `closing` event.
137+
138+
While the promise is awaited the button will have a loading state,
139+
this means, as long as no custom `icon`-slot is used, a loading icon will be shown.
140+
Please note that the **button will not be disabled or accessibility reasons**,
141+
because disabled elements cannot be focused and so the loading state could not be communicated e.g. via screen readers.
142+
143+
```vue
144+
<template>
145+
<div>
146+
<NcButton @click="openDialog">Show dialog</NcButton>
147+
<NcDialog :buttons="buttons"
148+
name="Create user"
149+
:message="message"
150+
:open.sync="showDialog"
151+
@closing="response = $event"
152+
@update:open="clickClosesDialog = false" />
153+
<div style="margin-top: 8px;">Dialog response: {{ response }}</div>
154+
</div>
155+
</template>
156+
<script>
157+
export default {
158+
data() {
159+
return {
160+
showDialog: false,
161+
clickClosesDialog: false,
162+
response: 'none',
163+
}
164+
},
165+
166+
methods: {
167+
async callback() {
168+
// wait 3 seconds
169+
await new Promise((resolve) => window.setTimeout(resolve, 3000))
170+
this.clickClosesDialog = !this.clickClosesDialog
171+
// Do not close the dialog on first and then every second button click
172+
if (this.clickClosesDialog) {
173+
// return false means the dialog stays open
174+
return false
175+
}
176+
return '✅'
177+
},
178+
179+
openDialog() {
180+
this.response = 'none'
181+
this.showDialog = true
182+
},
183+
},
184+
185+
computed: {
186+
buttons() {
187+
return [
188+
{
189+
label: 'Create user',
190+
type: 'primary',
191+
callback: this.callback,
192+
}
193+
]
194+
},
195+
message() {
196+
if (this.clickClosesDialog) {
197+
return 'Next button click will work and close the dialog.'
198+
} else {
199+
return 'Clicking the button will load but not close the dialog.'
200+
}
201+
},
202+
},
203+
}
204+
</script>
205+
```
131206
</docs>
132207

133208
<template>
@@ -137,7 +212,7 @@ export default {
137212
:enable-swipe="false"
138213
v-bind="modalProps"
139214
@close="handleClosed"
140-
@update:show="handleClosing">
215+
@update:show="handleClosing()">
141216
<!-- The dialog name / header -->
142217
<h2 :id="navigationId" class="dialog__name" v-text="name" />
143218
<component :is="dialogTagName"
@@ -453,25 +528,29 @@ export default defineComponent({
453528
// Because NcModal does not emit `close` when show prop is changed
454529
/**
455530
* Handle clicking a dialog button -> should close
531+
* @param {MouseEvent} event The click event
532+
* @param {unknown} result Result of the callback function
456533
*/
457-
const handleButtonClose = () => {
534+
const handleButtonClose = (event, result) => {
458535
// Skip close if invalid dialog
459536
if (dialogTagName.value === 'form' && !dialogElement.value.reportValidity()) {
460537
return
461538
}
462-
handleClosing()
539+
handleClosing(result)
463540
window.setTimeout(() => handleClosed(), 300)
464541
}
465542

466543
/**
467544
* Handle closing the dialog, optional out transition did not run yet
545+
* @param {unknown} result the result of the callback
468546
*/
469-
const handleClosing = () => {
547+
const handleClosing = (result) => {
470548
showModal.value = false
471549
/**
472-
* Emitted when the dialog is closing, so the out transition did not finish yet
550+
* Emitted when the dialog is closing, so the out transition did not finish yet.
551+
* @param result The result of the button callback (`undefined` if closing because of clicking the 'close'-button)
473552
*/
474-
emit('closing')
553+
emit('closing', result)
475554
}
476555

477556
/**

src/components/NcDialogButton/NcDialogButton.vue

+91-78
Original file line numberDiff line numberDiff line change
@@ -17,103 +17,116 @@ Dialog button component used by NcDialog in the actions slot to display the butt
1717
<template #icon>
1818
<!-- @slot Allow to set a custom icon for the button -->
1919
<slot name="icon">
20-
<NcIconSvgWrapper v-if="icon !== undefined" :svg="icon" />
20+
<!-- The loading state is an information that must be accessible -->
21+
<NcLoadingIcon v-if="isLoading" :name="t('Loading …') /* TRANSLATORS: The button is in a loading state*/" />
22+
<NcIconSvgWrapper v-else-if="icon !== undefined" :svg="icon" />
2123
</slot>
2224
</template>
2325
</NcButton>
2426
</template>
2527

26-
<script lang="ts">
27-
import { defineComponent, type PropType } from 'vue'
28+
<script setup lang="ts">
29+
import type { PropType } from 'vue'
30+
import { ref } from 'vue'
31+
2832
import NcButton, { ButtonNativeType, ButtonType } from '../NcButton/index'
2933
import NcIconSvgWrapper from '../NcIconSvgWrapper/index.js'
34+
import NcLoadingIcon from '../NcLoadingIcon/index.js'
35+
import { t } from '../../l10n.js'
3036

31-
export default defineComponent({
32-
name: 'NcDialogButton',
37+
const props = defineProps({
38+
/**
39+
* 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.
41+
* @type {() => unknown|false|Promise<unknown|false>}
42+
*/
43+
callback: {
44+
type: Function,
45+
required: false,
46+
default: () => {},
47+
},
3348

34-
components: {
35-
NcButton,
36-
NcIconSvgWrapper,
49+
/**
50+
* The label of the button
51+
*/
52+
label: {
53+
type: String,
54+
required: true,
3755
},
3856

39-
props: {
40-
/**
41-
* The function that will be called when the button is pressed
42-
* @type {() => void}
43-
*/
44-
callback: {
45-
type: Function,
46-
required: false,
47-
default: () => {},
48-
},
57+
/**
58+
* Optional inline SVG icon for the button
59+
*/
60+
icon: {
61+
type: String,
62+
required: false,
63+
default: undefined,
64+
},
4965

50-
/**
51-
* The label of the button
52-
*/
53-
label: {
54-
type: String,
55-
required: true,
66+
/**
67+
* The button type, see NcButton
68+
* @type {'primary'|'secondary'|'error'|'warning'|'success'}
69+
*/
70+
type: {
71+
type: String as PropType<ButtonType>,
72+
default: ButtonType.Secondary,
73+
required: false,
74+
validator(value: string) {
75+
return typeof value === 'string'
76+
&& Object.values(ButtonType).includes(value as ButtonType)
5677
},
78+
},
5779

58-
/**
59-
* Optional inline SVG icon for the button
60-
*/
61-
icon: {
62-
type: String,
63-
required: false,
64-
default: undefined,
80+
/**
81+
* The native type of the button, see `NcButton`
82+
* @type {'button'|'submit'|'reset'}
83+
*/
84+
nativeType: {
85+
type: String as PropType<ButtonNativeType>,
86+
required: false,
87+
default: 'button',
88+
validator(value) {
89+
return typeof value === 'string'
90+
&& Object.values(ButtonNativeType).includes(value as ButtonNativeType)
6591
},
92+
},
6693

67-
/**
68-
* The button type, see NcButton
69-
* @type {'primary'|'secondary'|'error'|'warning'|'success'}
70-
*/
71-
type: {
72-
type: String as PropType<ButtonType>,
73-
default: ButtonType.Secondary,
74-
required: false,
75-
validator(value: string) {
76-
return typeof value === 'string'
77-
&& Object.values(ButtonType).includes(value as ButtonType)
78-
},
79-
},
94+
/**
95+
* If the button should be shown as disabled
96+
*/
97+
disabled: {
98+
type: Boolean,
99+
default: false,
100+
},
101+
})
80102

81-
/**
82-
* The native type of the button, see `NcButton`
83-
* @type {'button'|'submit'|'reset'}
84-
*/
85-
nativeType: {
86-
type: String as PropType<ButtonNativeType>,
87-
required: false,
88-
default: 'button',
89-
validator(value) {
90-
return typeof value === 'string'
91-
&& Object.values(ButtonNativeType).includes(value as ButtonNativeType)
92-
},
93-
},
103+
const emit = defineEmits<{
104+
(name: 'click', event: MouseEvent, payload: unknown): void
105+
}>()
94106

95-
/**
96-
* If the button should be shown as disabled
97-
*/
98-
disabled: {
99-
type: Boolean,
100-
default: false,
101-
},
102-
},
107+
const isLoading = ref(false)
103108

104-
emits: ['click'],
109+
/**
110+
* Handle clicking the button
111+
* @param {MouseEvent} e The click event
112+
*/
113+
const handleClick = async (e) => {
114+
// Do not re-emit while loading
115+
if (isLoading.value) {
116+
return
117+
}
105118

106-
setup(props, { emit }) {
107-
/**
108-
* Handle clicking the button
109-
* @param {MouseEvent} e The click event
110-
*/
111-
const handleClick = (e) => {
112-
props.callback?.()
113-
emit('click', e)
119+
isLoading.value = true
120+
try {
121+
const result = await props.callback?.()
122+
if (result !== false) {
123+
/**
124+
* The click event (`MouseEvent`) and the value returned by the callback
125+
*/
126+
emit('click', e, result)
114127
}
115-
116-
return { handleClick }
117-
},
118-
})
128+
} finally {
129+
isLoading.value = false
130+
}
131+
}
119132
</script>

styleguide.config.cjs

+3
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ module.exports = async () => {
6666
},
6767

6868
enhancePreviewApp: path.resolve(__dirname, 'styleguide/preview.js'),
69+
compilerConfig: {
70+
transforms: { asyncAwait: false },
71+
},
6972

7073
sections: [
7174
{

0 commit comments

Comments
 (0)