fix: animate quick entry drawer
This commit is contained in:
parent
d7bd667ea2
commit
9b70e4fec4
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { currencies, currencyMinorUnits, type CurrencyCode, type EntryType } from "@cents/domain";
|
||||
import { BookOpen, CalendarDays, Check, ChevronDown, Delete as BackspaceIcon, X } from "@lucide/vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { entryTypes, type Category } from "../data/categories";
|
||||
import CategoryCoordinateSelector from "./CategoryCoordinateSelector.vue";
|
||||
import CurrencyPicker from "./CurrencyPicker.vue";
|
||||
|
|
@ -40,6 +40,14 @@ const selectorOpen = ref(false);
|
|||
const noteEditing = ref(false);
|
||||
const ledgerPickerOpen = ref(false);
|
||||
const currencyPickerOpen = ref(false);
|
||||
const drawerDragging = ref(false);
|
||||
const drawerDragOffset = ref(0);
|
||||
const drawerClosing = ref(false);
|
||||
const drawerEntered = ref(false);
|
||||
const drawerPointerId = ref<number | null>(null);
|
||||
let drawerDragStartY = 0;
|
||||
let drawerCloseTimer: number | null = null;
|
||||
let drawerEnterFrame: number | null = null;
|
||||
const selectedLedgerIds = ref([...new Set([props.ledgerId, props.personalLedgerId].filter(Boolean))]);
|
||||
|
||||
const typeOption = computed(() => entryTypes.find((item) => item.id === entryType.value)!);
|
||||
|
|
@ -194,6 +202,61 @@ function finishNoteEditing(event: KeyboardEvent) {
|
|||
(event.currentTarget as HTMLInputElement).blur();
|
||||
}
|
||||
|
||||
function startDrawerDrag(event: PointerEvent) {
|
||||
if (drawerClosing.value || (event.pointerType === "mouse" && event.button !== 0)) return;
|
||||
drawerPointerId.value = event.pointerId;
|
||||
drawerDragStartY = event.clientY;
|
||||
drawerDragOffset.value = 0;
|
||||
drawerDragging.value = true;
|
||||
(event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId);
|
||||
}
|
||||
|
||||
function moveDrawerDrag(event: PointerEvent) {
|
||||
if (drawerPointerId.value !== event.pointerId) return;
|
||||
drawerDragOffset.value = Math.max(0, Math.min(window.innerHeight * 0.82, event.clientY - drawerDragStartY));
|
||||
}
|
||||
|
||||
function finishDrawerDrag(event: PointerEvent) {
|
||||
if (drawerPointerId.value !== event.pointerId) return;
|
||||
drawerPointerId.value = null;
|
||||
if (drawerDragOffset.value >= 56) {
|
||||
requestClose();
|
||||
return;
|
||||
}
|
||||
drawerDragging.value = false;
|
||||
drawerDragOffset.value = 0;
|
||||
}
|
||||
|
||||
function cancelDrawerDrag(event: PointerEvent) {
|
||||
if (drawerPointerId.value !== event.pointerId) return;
|
||||
drawerPointerId.value = null;
|
||||
drawerDragging.value = false;
|
||||
drawerDragOffset.value = 0;
|
||||
}
|
||||
|
||||
function requestClose() {
|
||||
if (drawerClosing.value) return;
|
||||
drawerClosing.value = true;
|
||||
drawerDragging.value = false;
|
||||
drawerDragOffset.value = 0;
|
||||
drawerCloseTimer = window.setTimeout(() => {
|
||||
drawerCloseTimer = null;
|
||||
emit("close");
|
||||
}, 220);
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (drawerCloseTimer !== null) window.clearTimeout(drawerCloseTimer);
|
||||
if (drawerEnterFrame !== null) window.cancelAnimationFrame(drawerEnterFrame);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
drawerEnterFrame = window.requestAnimationFrame(() => {
|
||||
drawerEnterFrame = null;
|
||||
drawerEntered.value = true;
|
||||
});
|
||||
});
|
||||
|
||||
function openLedgerPicker() {
|
||||
confirmCategory();
|
||||
noteEditing.value = false;
|
||||
|
|
@ -234,10 +297,24 @@ function saveEntry() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="drawer-scrim" :class="{ 'category-mode': selectorOpen }" @click="emit('close')"></div>
|
||||
<section class="quick-drawer" aria-label="快速记账">
|
||||
<div class="drawer-scrim" :class="{ 'category-mode': selectorOpen, 'drawer-entered': drawerEntered, 'drawer-closing': drawerClosing }" @click="requestClose"></div>
|
||||
<section
|
||||
class="quick-drawer"
|
||||
:class="{ 'drawer-entered': drawerEntered, 'drawer-dragging': drawerDragging, 'drawer-closing': drawerClosing }"
|
||||
:style="drawerDragging ? { transform: `translateY(${drawerDragOffset}px)` } : undefined"
|
||||
aria-label="快速记账"
|
||||
>
|
||||
<div v-if="selectorOpen" class="category-focus-scrim"></div>
|
||||
<div class="drawer-handle"></div>
|
||||
<div
|
||||
class="drawer-handle"
|
||||
role="button"
|
||||
aria-label="向下拖动关闭快速记账"
|
||||
title="向下拖动关闭"
|
||||
@pointerdown="startDrawerDrag"
|
||||
@pointermove="moveDrawerDrag"
|
||||
@pointerup="finishDrawerDrag"
|
||||
@pointercancel="cancelDrawerDrag"
|
||||
></div>
|
||||
|
||||
<header class="drawer-header" :class="{ 'long-amount': amountNeedsFullRow }">
|
||||
<div class="amount-block">
|
||||
|
|
|
|||
|
|
@ -791,8 +791,12 @@ input:focus-visible {
|
|||
z-index: 10;
|
||||
background: rgba(18, 35, 32, 0.36);
|
||||
backdrop-filter: blur(1px);
|
||||
transition: opacity 220ms ease;
|
||||
}
|
||||
|
||||
.drawer-scrim:not(.drawer-entered) { opacity: 0; }
|
||||
.drawer-scrim.drawer-closing { opacity: 0; }
|
||||
|
||||
.drawer-scrim.category-mode {
|
||||
background: rgba(18, 35, 32, 0.42);
|
||||
backdrop-filter: blur(7px);
|
||||
|
|
@ -809,16 +813,25 @@ input:focus-visible {
|
|||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 -16px 36px rgba(22, 45, 40, 0.22);
|
||||
transition: transform 220ms ease;
|
||||
}
|
||||
|
||||
.quick-drawer.drawer-dragging { transition: none; }
|
||||
.quick-drawer:not(.drawer-entered):not(.drawer-closing) { transform: translateY(100%); }
|
||||
.quick-drawer.drawer-closing { transform: translateY(100%); }
|
||||
|
||||
.drawer-handle {
|
||||
width: 38px;
|
||||
height: 4px;
|
||||
margin: 8px auto 0;
|
||||
border-radius: 2px;
|
||||
background: #b9cac5;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.drawer-handle:active { cursor: grabbing; }
|
||||
|
||||
.drawer-header {
|
||||
min-height: 76px;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -65,4 +65,24 @@ test.describe("快速记账:新增账目", () => {
|
|||
await expect(drawer.getByRole("button", { name: "完成记账" })).toBeVisible();
|
||||
await expect(note).toHaveValue("回车备注");
|
||||
});
|
||||
|
||||
test("06-点击遮罩或向下拖动把手都带动画关闭抽屉", async ({ page }) => {
|
||||
await mockApi(page);
|
||||
await 登录(page);
|
||||
await page.getByRole("button", { name: "记一笔" }).click();
|
||||
const drawer = page.getByRole("region", { name: "快速记账" });
|
||||
await expect(drawer).toHaveClass(/drawer-entered/);
|
||||
const handle = page.getByRole("button", { name: "向下拖动关闭快速记账" });
|
||||
const handleBox = await handle.boundingBox();
|
||||
expect(handleBox).not.toBeNull();
|
||||
const startY = handleBox!.y + handleBox!.height / 2;
|
||||
await handle.dispatchEvent("pointerdown", { pointerId: 1, pointerType: "touch", isPrimary: true, button: 0, clientY: startY });
|
||||
await handle.dispatchEvent("pointermove", { pointerId: 1, pointerType: "touch", isPrimary: true, buttons: 1, clientY: startY + 80 });
|
||||
await handle.dispatchEvent("pointerup", { pointerId: 1, pointerType: "touch", isPrimary: true, button: 0, clientY: startY + 80 });
|
||||
await expect(drawer).not.toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "记一笔" }).click();
|
||||
await page.locator(".drawer-scrim").click({ position: { x: 8, y: 8 } });
|
||||
await expect(page.getByRole("region", { name: "快速记账" })).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue