Skip to content

Instantly share code, notes, and snippets.

@IlCallo
Last active September 28, 2021 14:53
Show Gist options
  • Save IlCallo/35d6d38e4e6f3920984054c435420b36 to your computer and use it in GitHub Desktop.
Save IlCallo/35d6d38e4e6f3920984054c435420b36 to your computer and use it in GitHub Desktop.
swipable-bottom-sheet (Qv2 compliant)
<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>
<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>
@IlCallo
Copy link
Author

IlCallo commented Nov 25, 2020

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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment