Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(NcDialogButton): Allow to return false from callback to keep dialog open #6005

Merged
merged 2 commits into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions l10n/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down
8 changes: 4 additions & 4 deletions src/components/NcAppNavigationItem/NcAppNavigationItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -609,10 +609,6 @@ export default {
}
},

mounted() {
this.actionsBoundariesElement = document.querySelector('#content-vue') || undefined
},

data() {
return {
editingValue: '',
Expand Down Expand Up @@ -666,6 +662,10 @@ export default {
},
},

mounted() {
this.actionsBoundariesElement = document.querySelector('#content-vue') || undefined
},

created() {
this.updateSlotInfo()
},
Expand Down
93 changes: 86 additions & 7 deletions src/components/NcDialog/NcDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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>
Expand All @@ -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"
Expand Down Expand Up @@ -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)
}

/**
Expand Down
157 changes: 83 additions & 74 deletions src/components/NcDialogButton/NcDialogButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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',
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: () => {},
},

components: {
NcButton,
NcIconSvgWrapper,
/**
* The label of the button
*/
label: {
type: String,
required: true,
},

props: {
/**
* The function that will be called when the button is pressed
* @type {() => void}
*/
callback: {
type: Function,
required: false,
default: () => {},
},
/**
* Optional inline SVG icon for the button
*/
icon: {
type: String,
required: false,
default: undefined,
},

/**
* The label of the button
*/
label: {
type: String,
required: true,
},
/**
* 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),
},

/**
* Optional inline SVG icon for the button
*/
icon: {
type: String,
required: false,
default: undefined,
/**
* See `nativeType` of `NcButton`
*/
nativeType: {
type: String,
required: false,
default: 'button',
validator(value) {
return ['submit', 'reset', 'button'].includes(value)
},
},

/**
* 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),
},
/**
* 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)
},
},
const emit = defineEmits(['click'])

/**
* If the button should be shown as disabled
*/
disabled: {
type: Boolean,
default: false,
},
},
const isLoading = ref(false)

emits: ['click'],
/**
* Handle clicking the button
* @param {MouseEvent} e The click event
*/
const handleClick = async (e) => {
// Do not re-emit while loading
if (isLoading.value) {
return
}

setup(props, { emit }) {
/**
* Handle clicking the button
* @param {MouseEvent} e The click event
*/
const handleClick = (e) => {
props.callback?.()
emit('click', e)
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)
}

return { handleClick }
},
})
} finally {
isLoading.value = false
}
}
</script>
4 changes: 4 additions & 0 deletions styleguide.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ module.exports = async () => {
return `import ${name} from '@nextcloud/vue/dist/Components/${name}.js'`
},

compilerConfig: {
transforms: { asyncAwait: false },
},

sections: [
{
name: 'Introduction',
Expand Down
Loading