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:
Ferdinand Thiessen 2024-08-31 21:37:19 +02:00 коммит произвёл GitHub
Родитель 847d55a176 42000099d9
Коммит 862d55245a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
5 изменённых файлов: 183 добавлений и 87 удалений

Просмотреть файл

@ -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',