Merge pull request #6005 from nextcloud-libraries/feat/allow-keep-dialog-open
feat(NcDialogButton): Allow to return `false` from callback to keep dialog open
This commit is contained in:
Коммит
862d55245a
|
@ -194,6 +194,10 @@ msgstr ""
|
|||
msgid "Load more \"{options}\""
|
||||
msgstr ""
|
||||
|
||||
#. TRANSLATORS: The button is in a loading state
|
||||
msgid "Loading …"
|
||||
msgstr ""
|
||||
|
||||
#. TRANSLATORS: A color name for RGB(45, 115, 190)
|
||||
msgid "Mariner"
|
||||
msgstr ""
|
||||
|
|
|
@ -609,10 +609,6 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.actionsBoundariesElement = document.querySelector('#content-vue') || undefined
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editingValue: '',
|
||||
|
@ -666,6 +662,10 @@ export default {
|
|||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.actionsBoundariesElement = document.querySelector('#content-vue') || undefined
|
||||
},
|
||||
|
||||
created() {
|
||||
this.updateSlotInfo()
|
||||
},
|
||||
|
|
|
@ -106,7 +106,6 @@ Note that this is not possible if the dialog contains a navigation!
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import IconCancel from '@mdi/svg/svg/cancel.svg?raw'
|
||||
import IconCheck from '@mdi/svg/svg/check.svg?raw'
|
||||
|
||||
export default {
|
||||
|
@ -128,6 +127,82 @@ export default {
|
|||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Loading buttons
|
||||
Sometimes a dialog ends with a request and this request might fail due to server-side-validation.
|
||||
In this case it is often desired to keep the dialog open, this can be done by returning `false` from the button callback,
|
||||
to not block this callback should return a `Promise<false>`.
|
||||
|
||||
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.
|
||||
|
||||
While the promise is awaited the button will have a loading state,
|
||||
this means, as long as no custom `icon`-slot is used, a loading icon will be shown.
|
||||
Please note that the **button will not be disabled or accessibility reasons**,
|
||||
because disabled elements cannot be focused and so the loading state could not be communicated e.g. via screen readers.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<NcButton @click="openDialog">Show dialog</NcButton>
|
||||
<NcDialog :buttons="buttons"
|
||||
name="Create user"
|
||||
:message="message"
|
||||
:open.sync="showDialog"
|
||||
@closing="response = $event"
|
||||
@update:open="clickClosesDialog = false" />
|
||||
<div style="margin-top: 8px;">Dialog response: {{ response }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showDialog: false,
|
||||
clickClosesDialog: false,
|
||||
response: 'none',
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async callback() {
|
||||
// wait 3 seconds
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 3000))
|
||||
this.clickClosesDialog = !this.clickClosesDialog
|
||||
// Do not close the dialog on first and then every second button click
|
||||
if (this.clickClosesDialog) {
|
||||
// return false means the dialog stays open
|
||||
return false
|
||||
}
|
||||
return '✅'
|
||||
},
|
||||
|
||||
openDialog() {
|
||||
this.response = 'none'
|
||||
this.showDialog = true
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
buttons() {
|
||||
return [
|
||||
{
|
||||
label: 'Create user',
|
||||
type: 'primary',
|
||||
callback: this.callback,
|
||||
}
|
||||
]
|
||||
},
|
||||
message() {
|
||||
if (this.clickClosesDialog) {
|
||||
return 'Next button click will work and close the dialog.'
|
||||
} else {
|
||||
return 'Clicking the button will load but not close the dialog.'
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
```
|
||||
</docs>
|
||||
|
||||
<template>
|
||||
|
@ -137,7 +212,7 @@ export default {
|
|||
:enable-swipe="false"
|
||||
v-bind="modalProps"
|
||||
@close="handleClosed"
|
||||
@update:show="handleClosing">
|
||||
@update:show="handleClosing()">
|
||||
<!-- The dialog name / header -->
|
||||
<h2 :id="navigationId" class="dialog__name" v-text="name" />
|
||||
<component :is="dialogTagName"
|
||||
|
@ -446,25 +521,29 @@ export default defineComponent({
|
|||
// Because NcModal does not emit `close` when show prop is changed
|
||||
/**
|
||||
* Handle clicking a dialog button -> should close
|
||||
* @param {MouseEvent} event The click event
|
||||
* @param {unknown} result Result of the callback function
|
||||
*/
|
||||
const handleButtonClose = () => {
|
||||
const handleButtonClose = (event, result) => {
|
||||
// Skip close if invalid dialog
|
||||
if (dialogTagName.value === 'form' && !dialogElement.value.reportValidity()) {
|
||||
return
|
||||
}
|
||||
handleClosing()
|
||||
handleClosing(result)
|
||||
window.setTimeout(() => handleClosed(), 300)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle closing the dialog, optional out transition did not run yet
|
||||
* @param {unknown} result the result of the callback
|
||||
*/
|
||||
const handleClosing = () => {
|
||||
const handleClosing = (result) => {
|
||||
showModal.value = false
|
||||
/**
|
||||
* Emitted when the dialog is closing, so the out transition did not finish yet
|
||||
* Emitted when the dialog is closing, so the out transition did not finish yet.
|
||||
* @param result The result of the button callback (`undefined` if closing because of clicking the 'close'-button)
|
||||
*/
|
||||
emit('closing')
|
||||
emit('closing', result)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,98 +17,107 @@ Dialog button component used by NcDialog in the actions slot to display the butt
|
|||
<template #icon>
|
||||
<!-- @slot Allow to set a custom icon for the button -->
|
||||
<slot name="icon">
|
||||
<NcIconSvgWrapper v-if="icon !== undefined" :svg="icon" />
|
||||
<!-- The loading state is an information that must be accessible -->
|
||||
<NcLoadingIcon v-if="isLoading" :name="t('Loading …') /* TRANSLATORS: The button is in a loading state*/" />
|
||||
<NcIconSvgWrapper v-else-if="icon !== undefined" :svg="icon" />
|
||||
</slot>
|
||||
</template>
|
||||
</NcButton>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import NcButton from '../NcButton/index.js'
|
||||
import NcIconSvgWrapper from '../NcIconSvgWrapper/index.js'
|
||||
import NcLoadingIcon from '../NcLoadingIcon/index.js'
|
||||
import { t } from '../../l10n.js'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NcDialogButton',
|
||||
|
||||
components: {
|
||||
NcButton,
|
||||
NcIconSvgWrapper,
|
||||
const props = defineProps({
|
||||
/**
|
||||
* The function that will be called when the button is pressed.
|
||||
* If the function returns `false` the click is ignored and the dialog will not be closed.
|
||||
* @type {() => unknown|false|Promise<unknown|false>}
|
||||
*/
|
||||
callback: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: () => {},
|
||||
},
|
||||
|
||||
props: {
|
||||
/**
|
||||
* The function that will be called when the button is pressed
|
||||
* @type {() => void}
|
||||
*/
|
||||
callback: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: () => {},
|
||||
},
|
||||
/**
|
||||
* The label of the button
|
||||
*/
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* The label of the button
|
||||
*/
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* Optional inline SVG icon for the button
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
|
||||
/**
|
||||
* Optional inline SVG icon for the button
|
||||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
/**
|
||||
* The button type, see NcButton
|
||||
* @type {'primary'|'secondary'|'error'|'warning'|'success'}
|
||||
*/
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'secondary',
|
||||
validator: (type) => typeof type === 'string' && ['primary', 'secondary', 'tertiary', 'error', 'warning', 'success'].includes(type),
|
||||
},
|
||||
|
||||
/**
|
||||
* The button type, see NcButton
|
||||
* @type {'primary'|'secondary'|'error'|'warning'|'success'}
|
||||
*/
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'secondary',
|
||||
validator: (type) => typeof type === 'string' && ['primary', 'secondary', 'tertiary', 'error', 'warning', 'success'].includes(type),
|
||||
},
|
||||
|
||||
/**
|
||||
* See `nativeType` of `NcButton`
|
||||
*/
|
||||
nativeType: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'button',
|
||||
validator(value) {
|
||||
return ['submit', 'reset', 'button'].includes(value)
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* If the button should be shown as disabled
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
/**
|
||||
* See `nativeType` of `NcButton`
|
||||
*/
|
||||
nativeType: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'button',
|
||||
validator(value) {
|
||||
return ['submit', 'reset', 'button'].includes(value)
|
||||
},
|
||||
},
|
||||
|
||||
emits: ['click'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
/**
|
||||
* Handle clicking the button
|
||||
* @param {MouseEvent} e The click event
|
||||
*/
|
||||
const handleClick = (e) => {
|
||||
props.callback?.()
|
||||
emit('click', e)
|
||||
}
|
||||
|
||||
return { handleClick }
|
||||
/**
|
||||
* If the button should be shown as disabled
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
/**
|
||||
* Handle clicking the button
|
||||
* @param {MouseEvent} e The click event
|
||||
*/
|
||||
const handleClick = async (e) => {
|
||||
// Do not re-emit while loading
|
||||
if (isLoading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
const result = await props.callback?.()
|
||||
if (result !== false) {
|
||||
/**
|
||||
* The click event (`MouseEvent`) and the value returned by the callback
|
||||
*/
|
||||
emit('click', e, result)
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -65,6 +65,10 @@ module.exports = async () => {
|
|||
return `import ${name} from '@nextcloud/vue/dist/Components/${name}.js'`
|
||||
},
|
||||
|
||||
compilerConfig: {
|
||||
transforms: { asyncAwait: false },
|
||||
},
|
||||
|
||||
sections: [
|
||||
{
|
||||
name: 'Introduction',
|
||||
|
|
Загрузка…
Ссылка в новой задаче