feat: sticky action buttons (#2622)

Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2622
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
This commit is contained in:
Dominik Pschenitschni 2022-11-04 13:49:28 +00:00 committed by konrad
parent f7728e5384
commit f4bc2b94f0
3 changed files with 59 additions and 26 deletions

View File

@ -99,6 +99,9 @@ watchEffect(() => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
$modal-margin: 4rem;
$modal-width: 1024px;
.modal-mask { .modal-mask {
position: fixed; position: fixed;
z-index: 4000; z-index: 4000;
@ -147,16 +150,16 @@ watchEffect(() => {
// scrolling-content // scrolling-content
// used e.g. for <TaskDetailViewModal> // used e.g. for <TaskDetailViewModal>
.scrolling .modal-content { .scrolling .modal-content {
max-width: 1024px; max-width: $modal-width;
width: 100%; width: 100%;
margin: 4rem auto; margin: $modal-margin auto;
max-height: none; // reset bulma max-height: none; // reset bulma
overflow: visible; // reset bulma overflow: visible; // reset bulma
@media screen and (min-width: $tablet) { @media screen and (min-width: $tablet) {
max-height: none; // reset bulma max-height: none; // reset bulma
margin: 4rem auto; // reset bulma margin: $modal-margin auto; // reset bulma
width: 100%; width: 100%;
} }
@ -189,14 +192,23 @@ watchEffect(() => {
} }
.close { .close {
$close-button-min-space: 84px;
$close-button-padding: 26px;
position: fixed; position: fixed;
top: 5px; top: 5px;
right: 26px; right: $close-button-padding;
color: var(--white); color: var(--grey-900);
font-size: 2rem; font-size: 2rem;
@media screen and (max-width: $desktop) { @media screen and (min-width: $desktop) and (max-width: calc(#{$desktop } + #{$close-button-min-space})) {
color: var(--grey-900); top: calc(5px + $modal-margin);
right: 50%;
// we align the close button to the modal until there is enough space outside for it
transform: translateX(calc((#{$modal-width} / 2) - #{$close-button-padding}));
}
// we can only use light color when there is enough space for the close button next to the modal
@media screen and (min-width: calc(#{$desktop } + #{$close-button-min-space})) {
color: var(--white);
} }
} }
</style> </style>

View File

@ -19,7 +19,7 @@ export function useRouteWithModal() {
return return
} }
// logic from vue-router // this is adapted from vue-router
// https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125 // https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125
const routePropsOption = route.matched[0]?.props.default const routePropsOption = route.matched[0]?.props.default
const routeProps = routePropsOption const routeProps = routePropsOption
@ -28,7 +28,9 @@ export function useRouteWithModal() {
: typeof routePropsOption === 'function' : typeof routePropsOption === 'function'
? routePropsOption(route) ? routePropsOption(route)
: routePropsOption : routePropsOption
: null : {}
routeProps.backdropView = backdropView.value
const component = route.matched[0]?.components?.default const component = route.matched[0]?.components?.default

View File

@ -1,5 +1,12 @@
<template> <template>
<div :class="{ 'is-loading': taskService.loading, 'visible': visible}" class="loader-container task-view-container"> <div
class="loader-container task-view-container"
:class="{
'is-loading': taskService.loading,
'visible': visible,
'is-modal': isModal,
}"
>
<div class="task-view"> <div class="task-view">
<Heading v-model:task="task" :can-write="canWrite" ref="heading"/> <Heading v-model:task="task" :can-write="canWrite" ref="heading"/>
<h6 class="subtitle" v-if="parent && parent.namespace && parent.list"> <h6 class="subtitle" v-if="parent && parent.namespace && parent.list">
@ -267,15 +274,7 @@
<!-- Comments --> <!-- Comments -->
<comments :can-write="canWrite" :task-id="taskId"/> <comments :can-write="canWrite" :task-id="taskId"/>
</div> </div>
<div class="column is-one-third action-buttons d-print-none" v-if="canWrite || shouldShowClosePopup"> <div class="column is-one-third action-buttons d-print-none" v-if="canWrite || isModal">
<BaseButton
v-if="shouldShowClosePopup"
@click="$router.back()"
class="is-fullwidth is-block has-text-centered mb-4 has-text-primary"
>
<icon icon="arrow-left"/>
{{ $t('task.detail.closePopup') }}
</BaseButton>
<template v-if="canWrite"> <template v-if="canWrite">
<x-button <x-button
:class="{'is-success': !task.done}" :class="{'is-success': !task.done}"
@ -419,7 +418,7 @@
</div> </div>
</div> </div>
<!-- Created / Updated [by] --> <!-- Created / Updated [by] -->
<created-updated :task="task" v-if="!canWrite && !shouldShowClosePopup"/> <created-updated :task="task" v-if="!canWrite && !isModal"/>
</div> </div>
<modal <modal
@ -439,7 +438,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import {ref, reactive, toRef, shallowReactive, computed, watch, watchEffect, nextTick, type PropType} from 'vue' import {ref, reactive, toRef, shallowReactive, computed, watch, watchEffect, nextTick, type PropType} from 'vue'
import {useRoute, useRouter} from 'vue-router' import {useRouter, type RouteLocation} from 'vue-router'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {unrefElement} from '@vueuse/core' import {unrefElement} from '@vueuse/core'
import cloneDeep from 'lodash.clonedeep' import cloneDeep from 'lodash.clonedeep'
@ -494,11 +493,13 @@ const props = defineProps({
type: Number as PropType<ITask['id']>, type: Number as PropType<ITask['id']>,
required: true, required: true,
}, },
backdropView: {
type: String as PropType<RouteLocation['fullPath']>,
},
}) })
defineEmits(['close']) defineEmits(['close'])
const route = useRoute()
const router = useRouter() const router = useRouter()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
@ -567,8 +568,7 @@ const color = computed(() => {
const hasAttachments = computed(() => attachmentStore.attachments.length > 0) const hasAttachments = computed(() => attachmentStore.attachments.length > 0)
// HACK: const isModal = computed(() => Boolean(props.backdropView))
const shouldShowClosePopup = computed(() => (route.name as string).includes('kanban'))
function attachmentUpload(file: File, onSuccess?: (url: string) => void) { function attachmentUpload(file: File, onSuccess?: (url: string) => void) {
return uploadFile(taskId.value, file, onSuccess) return uploadFile(taskId.value, file, onSuccess)
@ -799,6 +799,7 @@ $flash-background-duration: 750ms;
@media screen and (max-width: $desktop) { @media screen and (max-width: $desktop) {
padding-bottom: 0; padding-bottom: 0;
} }
}
.subtitle { .subtitle {
color: var(--grey-500); color: var(--grey-500);
@ -965,6 +966,12 @@ $flash-background-duration: 750ms;
} }
.action-buttons { .action-buttons {
@media screen and (min-width: $tablet) {
position: sticky;
top: $navbar-height + 1.5rem;
align-self: flex-start;
}
.button { .button {
width: 100%; width: 100%;
margin-bottom: .5rem; margin-bottom: .5rem;
@ -976,6 +983,18 @@ $flash-background-duration: 750ms;
} }
} }
.is-modal .action-buttons {
// we need same top margin for the modal close button
@media screen and (min-width: $tablet) {
top: 6.5rem;
}
// this is the moment when the fixed close button is outside the modal
// => we can fill up the space again
@media screen and (min-width: calc(#{$desktop} + 84px)) {
top: 0;
}
}
.created { .created {
font-size: .75rem; font-size: .75rem;
color: var(--grey-500); color: var(--grey-500);
@ -985,7 +1004,7 @@ $flash-background-duration: 750ms;
.checklist-summary { .checklist-summary {
padding-left: .25rem; padding-left: .25rem;
} }
}
.task-view-container { .task-view-container {
padding-bottom: 1rem; padding-bottom: 1rem;