Last active
September 28, 2021 14:53
-
-
Save IlCallo/35d6d38e4e6f3920984054c435420b36 to your computer and use it in GitHub Desktop.
swipable-bottom-sheet (Qv2 compliant)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script lang="ts"> | |
import { Platform, Screen } from 'quasar'; | |
import { defineComponent, ref } from 'vue'; | |
import SwipableBottomSheet from './swipable-bottom-sheet.vue'; | |
export default defineComponent({ | |
name: 'MainLayout', | |
components: { SwipableBottomSheet }, | |
setup() { | |
const openSheet = ref(false); | |
return { openSheet, shouldUseSwipableSheet } | |
} | |
}); | |
</script> | |
<template> | |
<q-layout view="lHh Lpr lFf"> | |
<q-header elevated> | |
<q-toolbar>My App</q-toolbar> | |
</q-header> | |
<q-swipable-sheet | |
v-model="openSheet" | |
title="List title when fullscreen" | |
> | |
<q-list> | |
<q-item v-for="value in [0,1,2,3,4,5,6,7,8,9,10]" :key="value">Item { value }</q-item> | |
</q-list> | |
</q-swipable-sheet> | |
<q-page-container> | |
<router-view /> | |
</q-page-container> | |
</q-layout> | |
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script lang="ts"> | |
import { fromPairs } from 'lodash-es'; | |
import { Screen, QDialog, useDialogPluginComponent } from 'quasar'; | |
import { defineComponent, computed, ref, Ref } from 'vue'; | |
interface TouchSwipeParams { | |
evt: TouchEvent | MouseEvent; | |
touch: boolean; | |
mouse: boolean; | |
direction: 'up' | 'down' | 'left' | 'right'; | |
duration: number; | |
distance: { | |
x: number; | |
y: number; | |
}; | |
} | |
function useDialog() { | |
const dialogComposable = useDialogPluginComponent(); | |
const dialogRef = dialogComposable.dialogRef as Ref<QDialog>; | |
function show() { | |
dialogRef.value.show(); | |
} | |
function hide() { | |
dialogRef.value.hide(); | |
} | |
return { ...dialogComposable, dialogRef, show, hide }; | |
} | |
useDialog.emits = useDialogPluginComponent.emits; | |
useDialog.emitsObject = fromPairs( | |
useDialogPluginComponent.emits.map((eventName) => [ | |
eventName, | |
(...args: unknown[]) => !!args, | |
]), | |
); | |
export default defineComponent({ | |
name: 'SwipableBottomSheet', | |
props: { | |
title: { type: String, default: undefined }, | |
modelValue: Boolean, | |
cardClass: { type: [String, Array, Object], default: undefined }, | |
cardStyle: { type: [String, Array, Object], default: undefined }, | |
}, | |
emits: { | |
...useDialog.emitsObject, | |
'update:model-value': (payload: boolean) => payload !== undefined, | |
}, | |
setup(props, { emit }) { | |
const valueProxy = computed({ | |
get: () => props.modelValue, | |
set: (value) => emit('update:model-value', value), | |
}); | |
const dialogComposable = useDialog(); | |
const isFullscreen = ref(false); | |
const contentStyle = computed(() => ({ | |
// vh is unreliable on mobile as it takes into consideration the browser URL bar | |
// See https://stackoverflow.com/a/37113430/7931540 | |
height: isFullscreen.value ? `${Screen.height}px` : '50vh', | |
})); | |
function swipeHandler({ evt, duration, direction }: TouchSwipeParams) { | |
if (duration >= 25) { | |
if (isFullscreen.value) { | |
if (direction === 'up') { | |
return; | |
} else if (duration < 80) { | |
dialogComposable.hide(); | |
} else { | |
isFullscreen.value = false; | |
} | |
} else { | |
if (direction === 'up') { | |
isFullscreen.value = true; | |
} else { | |
dialogComposable.hide(); | |
} | |
} | |
} | |
evt.cancelable !== false && evt.preventDefault(); | |
evt.stopPropagation(); | |
} | |
return { | |
...dialogComposable, | |
valueProxy, | |
isFullscreen, | |
contentStyle, | |
swipeHandler, | |
}; | |
}, | |
}); | |
</script> | |
<template> | |
<q-dialog | |
ref="dialogRef" | |
v-model="valueProxy" | |
:maximized="isFullscreen" | |
position="bottom" | |
@hide=" | |
onDialogHide(); | |
isFullscreen = false; | |
" | |
> | |
<!-- q-dialog "swallows" static classes applied to it --> | |
<q-card class="swipable-bottom-sheet" :class="cardClass" :style="cardStyle"> | |
<!-- This wrapper div manage swipe both for the handle and the header --> | |
<div v-touch-swipe.vertical="swipeHandler"> | |
<template v-if="isFullscreen"> | |
<transition | |
appear | |
enter-active-class="animated fadeInDown header-enter" | |
leave-active-class="animated fadeOutUp header-leave" | |
> | |
<slot name="fullscreen-header"> | |
<q-toolbar class="fullscreen-dialog-header"> | |
<q-btn v-close-popup flat round icon="mdi-close" /> | |
<q-toolbar-title>{{ title }}</q-toolbar-title> | |
</q-toolbar> | |
</slot> | |
</transition> | |
</template> | |
<div v-else class="column items-center q-pt-sm q-pb-md q-mb-sm"> | |
<svg viewBox="0 0 50 4" width="50" height="4"> | |
<rect fill="#c5c5c5" width="50" height="4" rx="2" /> | |
</svg> | |
</div> | |
</div> | |
<div | |
class="scroll content" | |
:class=" | |
isFullscreen | |
? 'fullscreen-dialog-body content-expanding' | |
: 'content-shrinking' | |
" | |
:style="contentStyle" | |
> | |
<slot /> | |
</div> | |
</q-card> | |
</q-dialog> | |
</template> | |
<style lang="scss" scoped> | |
$accelerate-timing-function: cubic-bezier(0, 0, 0.2, 1); | |
$decelerate-timing-function: cubic-bezier(0.4, 0, 1, 1); | |
$standard-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | |
/* | |
[1] Keep the header top | |
[2] Same dimension as normal header | |
*/ | |
$header-height: 64px; // [2] | |
.fullscreen-dialog-body { | |
background-color: white; | |
padding-top: $header-height; // [1] | |
} | |
.fullscreen-dialog-header { | |
background-color: $primary; | |
box-shadow: $shadow-4; | |
color: white; | |
height: $header-height; // [1] | |
position: fixed; // [1] | |
top: 0; // [1] | |
z-index: 1; // [1] | |
} | |
.header-enter { | |
transition-delay: 100ms; | |
transition-duration: 150ms; | |
transition-timing-function: $accelerate-timing-function; | |
} | |
.header-leave { | |
transition-duration: 75ms; | |
transition-timing-function: $decelerate-timing-function; | |
} | |
.content { | |
transition-property: height; | |
} | |
.content-expanding { | |
transition-duration: 250ms; | |
transition-timing-function: $standard-timing-function; | |
} | |
.content-shrinking { | |
transition-duration: 200ms; | |
transition-timing-function: $standard-timing-function; | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Only works on
xs
screens (< 600px wide).When used on webkit, a strange glitch cover the bottom half of full screen mode in white, even if it still accepts touch input and works normally
If using dynamic component to switch between this a substitute for desktop (eg. a QDrawer),
<component :is=" $q.screen.xs && !$q.platform.is.ios ? SwipableBottomSheet : 'QDrawer'"> ... </component>