feat(ui): modals close only via Esc/X/cancel; standard 3:1 form cover processing
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import { useEscClose } from '@/lib/modal-stack';
|
||||
import type { MessageAutomationDelayUnit } from '@mento/shared';
|
||||
import { toFaDigits } from '@/lib/jalali';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
@@ -92,17 +93,13 @@ function pickImmediate() {
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
function onKey(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') open.value = false;
|
||||
}
|
||||
useEscClose(() => open.value, () => { open.value = false; });
|
||||
|
||||
watch(open, (isOpen) => {
|
||||
document.body.style.overflow = isOpen ? 'hidden' : '';
|
||||
});
|
||||
|
||||
onMounted(() => window.addEventListener('keydown', onKey));
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', onKey);
|
||||
document.body.style.overflow = '';
|
||||
});
|
||||
</script>
|
||||
@@ -116,7 +113,7 @@ onUnmounted(() => {
|
||||
|
||||
<Teleport to="body">
|
||||
<div v-if="open" class="layer">
|
||||
<button class="dim" type="button" aria-label="بستن" @click="open = false" />
|
||||
<div class="dim" aria-hidden="true" />
|
||||
<section class="sheet" role="dialog" aria-modal="true" aria-label="انتخاب فاصله زمانی">
|
||||
<header>
|
||||
<div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { api } from '@/lib/api';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import { useEscClose } from '@/lib/modal-stack';
|
||||
import type { SavedViewDto, SavedViewScope } from '@mento/shared';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -22,6 +23,7 @@ const error = ref('');
|
||||
const saveOpen = ref(false);
|
||||
const saveName = ref('');
|
||||
const saveShared = ref(false);
|
||||
useEscClose(() => saveOpen.value, () => { saveOpen.value = false; });
|
||||
|
||||
const activeView = computed(
|
||||
() => views.value.find((v) => v.id === activeId.value) ?? null,
|
||||
@@ -151,9 +153,14 @@ onMounted(() => {
|
||||
<p v-if="error" class="err">{{ error }}</p>
|
||||
|
||||
<div v-if="saveOpen" class="sheet">
|
||||
<div class="sheet-backdrop" @click="saveOpen = false" />
|
||||
<div class="sheet-panel">
|
||||
<h3>{{ activeId ? 'بهروزرسانی فیلتر' : 'ذخیره فیلتر' }}</h3>
|
||||
<div class="sheet-backdrop" aria-hidden="true" />
|
||||
<div class="sheet-panel" role="dialog" aria-modal="true">
|
||||
<header class="sheet-head">
|
||||
<h3>{{ activeId ? 'بهروزرسانی فیلتر' : 'ذخیره فیلتر' }}</h3>
|
||||
<button type="button" class="x-btn" aria-label="بستن (Esc)" title="بستن (Esc)" @click="saveOpen = false">
|
||||
<UiIcon name="x" :size="16" />
|
||||
</button>
|
||||
</header>
|
||||
<label>
|
||||
نام
|
||||
<input v-model="saveName" type="text" maxlength="120" placeholder="مثلاً سرنخهای این هفته" />
|
||||
@@ -253,6 +260,9 @@ input[type='text'] {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.sheet-head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
|
||||
.x-btn { width: 32px; height: 32px; border: 1px solid #e2e8f0; border-radius: 8px; background: #fff; color: #64748b; display: grid; place-items: center; cursor: pointer; }
|
||||
.x-btn:hover { background: #f8fafc; color: #0f172a; }
|
||||
.sheet-panel label {
|
||||
display: grid;
|
||||
gap: 0.3rem;
|
||||
|
||||
@@ -149,7 +149,7 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="layer" role="presentation">
|
||||
<button class="backdrop" type="button" aria-label="بستن" @click="close" />
|
||||
<div class="backdrop" aria-hidden="true" />
|
||||
<section
|
||||
class="sheet"
|
||||
role="dialog"
|
||||
|
||||
@@ -156,7 +156,7 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="layer" role="presentation">
|
||||
<button class="backdrop" type="button" aria-label="بستن" @click="close" />
|
||||
<div class="backdrop" aria-hidden="true" />
|
||||
<section class="sheet" role="dialog" aria-modal="true" aria-labelledby="postpone-title">
|
||||
<div class="handle" aria-hidden="true" />
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { onUnmounted, watch } from 'vue';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import { useEscClose } from '@/lib/modal-stack';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -13,33 +13,20 @@ const props = withDefaults(
|
||||
|
||||
const emit = defineEmits<{ close: [] }>();
|
||||
|
||||
function onKey(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') emit('close');
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
if (open) window.addEventListener('keydown', onKey);
|
||||
else window.removeEventListener('keydown', onKey);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onUnmounted(() => window.removeEventListener('keydown', onKey));
|
||||
useEscClose(() => props.open, () => emit('close'));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="ui-drawer">
|
||||
<div v-if="open" class="ui-drawer-layer">
|
||||
<button class="ui-drawer-dim" type="button" aria-label="بستن" @click="emit('close')" />
|
||||
<div class="ui-drawer-dim" aria-hidden="true" />
|
||||
<aside class="ui-drawer" role="dialog" aria-modal="true" :aria-label="title" :style="{ '--drawer-w': width }">
|
||||
<header class="ui-drawer-head">
|
||||
<slot name="header">
|
||||
<h2>{{ title }}</h2>
|
||||
</slot>
|
||||
<button type="button" class="ui-drawer-close" aria-label="بستن" @click="emit('close')">
|
||||
<button type="button" class="ui-drawer-close" aria-label="بستن (Esc)" title="بستن (Esc)" @click="emit('close')">
|
||||
<UiIcon name="x" :size="18" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
@@ -5,6 +5,7 @@ let openModalCount = 0;
|
||||
<script setup lang="ts">
|
||||
import { onUnmounted, watch } from 'vue';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import { useEscClose } from '@/lib/modal-stack';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -12,15 +13,9 @@ const props = withDefaults(
|
||||
title: string;
|
||||
/** برای مودال روی مودال (مثل انتخاب مخاطب داخل فرم) */
|
||||
nest?: boolean;
|
||||
/**
|
||||
* بستن با کلیک روی پسزمینه.
|
||||
* پیشفرض: فقط موبایل (تمامصفحه). در دسکتاپ عمداً بسته نمیشود.
|
||||
*/
|
||||
closeOnBackdrop?: boolean | 'mobile';
|
||||
}>(),
|
||||
{
|
||||
nest: false,
|
||||
closeOnBackdrop: 'mobile',
|
||||
},
|
||||
);
|
||||
|
||||
@@ -28,6 +23,8 @@ const emit = defineEmits<{
|
||||
close: [];
|
||||
}>();
|
||||
|
||||
useEscClose(() => props.open, () => emit('close'));
|
||||
|
||||
let locked = false;
|
||||
|
||||
function setLock(next: boolean) {
|
||||
@@ -44,34 +41,20 @@ watch(
|
||||
);
|
||||
|
||||
onUnmounted(() => setLock(false));
|
||||
|
||||
function isMobileViewport() {
|
||||
return typeof window !== 'undefined' && window.matchMedia('(max-width: 900px)').matches;
|
||||
}
|
||||
|
||||
function onBackdrop(event: MouseEvent) {
|
||||
if (event.target !== event.currentTarget) return;
|
||||
const mode = props.closeOnBackdrop;
|
||||
if (mode === false) return;
|
||||
if (mode === true || (mode === 'mobile' && isMobileViewport())) {
|
||||
emit('close');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="open" class="backdrop" :class="{ nest }" @click="onBackdrop">
|
||||
<div v-if="open" class="backdrop" :class="{ nest }">
|
||||
<section
|
||||
class="dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
:aria-label="title"
|
||||
@click.stop
|
||||
>
|
||||
<header class="head">
|
||||
<h2>{{ title }}</h2>
|
||||
<button type="button" class="close" aria-label="بستن" @click="emit('close')">
|
||||
<button type="button" class="close" aria-label="بستن (Esc)" title="بستن (Esc)" @click="emit('close')">
|
||||
<UiIcon name="x" :size="18" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
Reference in New Issue
Block a user