1
0
mirror of https://github.com/sendevia/website.git synced 2026-03-06 07:40:50 +08:00

feat(ImageViewer): improve accessibility and touch interactions

This commit is contained in:
2025-12-05 23:48:45 +08:00
parent 3c1b3b4aed
commit 46431afb3c
2 changed files with 34 additions and 45 deletions

View File

@@ -1,5 +1,7 @@
<script setup lang="ts">
// todo: 焦点选择有问题,不能正确记录打开查看器之前的焦点
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { handleTabNavigation } from "../utils/tabNavigation";
interface Props {
images: string[];
@@ -19,6 +21,7 @@ const emit = defineEmits<{
// 缩放配置常量
const ZOOM_MIN = 0.9; // 最小缩放
const ZOOM_MAX = 2.0; // 最大缩放
const ZOOM_MAX_TOUCH = 5.0; // 触摸设备最大缩放
const ZOOM_STEP = 0.15; // 缩放步长
const TOUCH_MOVE_THRESHOLD = 10; // 触摸移动阈值 (px)
@@ -59,8 +62,9 @@ function calculateInitialTransform() {
}
function show() {
// 保存前焦点
// 在显示之前立即保存前焦点,但只保存有效的可聚焦元素
previousActiveElement.value = document.activeElement as HTMLElement | null;
console.log(previousActiveElement.value);
isVisible.value = true;
@@ -71,15 +75,11 @@ function show() {
// 开始入场
setTimeout(() => {
isAnimating.value = true;
const btn = document.querySelector<HTMLButtonElement>(".btn-close");
if (btn) {
btn.focus();
}
}, 10);
// 在动画完成后,将焦点设置到关闭按钮
// setTimeout(() => {
// const btn = document.querySelector<HTMLButtonElement>(".image-viewer__close");
// if (btn) {
// btn.focus();
// }
// }, 300);
}
function hide() {
@@ -94,9 +94,21 @@ function hide() {
setTimeout(() => {
isVisible.value = false;
// 还原之前的焦点
if (previousActiveElement.value && typeof previousActiveElement.value.focus === "function") {
// 还原之前的焦点,如果元素仍然存在且在 DOM 中
if (
previousActiveElement.value &&
typeof previousActiveElement.value.focus === "function" &&
document.body.contains(previousActiveElement.value)
) {
previousActiveElement.value.focus();
} else {
// 如果之前的焦点元素无法还原,尝试找到页面中第一个可聚焦元素
const firstFocusable = document.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
) as HTMLElement;
if (firstFocusable) {
firstFocusable.focus();
}
}
emit("close");
@@ -125,28 +137,17 @@ function nextImage() {
function handleKeydown(event: KeyboardEvent) {
if (!isVisible.value) return;
// Tab 陷阱:当按 Tab 时仅在弹窗内部循环焦点
if (event.key === "Tab") {
const container = document.querySelector(".image-viewer") as HTMLElement | null;
if (container) {
const focusable = Array.from(
container.querySelectorAll<HTMLElement>('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
).filter((el) => !el.hasAttribute("disabled"));
const container = document.querySelector(".image-viewer") as HTMLElement;
const focusableElements = container?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusable.length) {
const activeIndex = focusable.indexOf(document.activeElement as HTMLElement);
if (event.shiftKey) {
// reverse
const prev = activeIndex <= 0 ? focusable.length - 1 : activeIndex - 1;
focusable[prev].focus();
event.preventDefault();
} else {
const next = activeIndex === focusable.length - 1 ? 0 : activeIndex + 1;
focusable[next].focus();
event.preventDefault();
}
}
if (focusableElements && focusableElements.length > 0) {
event.preventDefault();
handleTabNavigation(container, focusableElements, event.shiftKey);
}
return;
}
@@ -294,7 +295,7 @@ function handleTouchMove(event: TouchEvent) {
event.preventDefault();
const currentDistance = getDistance(event.touches[0], event.touches[1]);
const scaleFactor = currentDistance / initialDistance;
const newScale = Math.min(Math.max(initialScale * scaleFactor, ZOOM_MIN), ZOOM_MAX);
const newScale = Math.min(Math.max(initialScale * scaleFactor, ZOOM_MIN), ZOOM_MAX_TOUCH);
imageScale.value = newScale;
} else if (event.touches.length === 1) {
@@ -333,18 +334,6 @@ function handleTouchEnd(event: TouchEvent) {
}
if (event.touches.length === 0) {
// 检查是否是纯点击(未产生拖拽)
if (!isDragging.value) {
// 如果没有拖拽,说明是纯点击。判断点击位置是否在图片外部
const eventTarget = event.target as HTMLElement;
const contentContainer = eventTarget.closest(".image-viewer__content");
// 如果触摸发生在 content 容器但不是图片本身,关闭查看器
if (contentContainer && eventTarget === contentContainer) {
hide();
}
}
isDragging.value = false;
}
}
@@ -378,7 +367,7 @@ defineExpose({
<template>
<div
v-if="isVisible"
class="image-viewer"
class="ImageViewer"
:class="{ animating: isAnimating }"
role="dialog"
aria-modal="true"

View File

@@ -1,6 +1,6 @@
@use "../mixin";
.image-viewer {
.ImageViewer {
display: flex;
align-items: center;
flex-direction: column;