feat: focus electric order drilldowns

This commit is contained in:
kkfluous
2026-08-07 17:25:40 +08:00
parent 70498fc4fa
commit 39e869ad39
+41 -3
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { BatteryCharging, CalendarDays, CheckCircle2, ChevronRight, MapPin, Plug, TrendingUp, TriangleAlert, Truck, Wallet } from 'lucide-react'; import { BatteryCharging, CalendarDays, CheckCircle2, ChevronRight, MapPin, Plug, TrendingUp, TriangleAlert, Truck, Wallet, X } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react'; import { motion, AnimatePresence } from 'motion/react';
import TrendBadge from './TrendBadge'; import TrendBadge from './TrendBadge';
import { fetchElectricMonthly, fetchElectricOrders } from './api'; import { fetchElectricMonthly, fetchElectricOrders } from './api';
@@ -53,6 +53,7 @@ export default function ElectricDaily() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [orders, setOrders] = useState<ElectricChargeOrderResponse | null>(null); const [orders, setOrders] = useState<ElectricChargeOrderResponse | null>(null);
const [ordersError, setOrdersError] = useState<string | null>(null); const [ordersError, setOrdersError] = useState<string | null>(null);
const orderTriggerRef = useRef<HTMLButtonElement | null>(null);
const effectiveRange = useMemo(() => normalizeDateRange(dateRange.start, dateRange.end), [dateRange.start, dateRange.end]); const effectiveRange = useMemo(() => normalizeDateRange(dateRange.start, dateRange.end), [dateRange.start, dateRange.end]);
const selectedDate = drillContext.selectedDate; const selectedDate = drillContext.selectedDate;
@@ -150,6 +151,9 @@ export default function ElectricDaily() {
const toggleDate = (date: string) => { const toggleDate = (date: string) => {
const opening = selectedDate !== date; const opening = selectedDate !== date;
if (opening && document.activeElement instanceof HTMLButtonElement) {
orderTriggerRef.current = document.activeElement;
}
commitDrillContext({ commitDrillContext({
...drillContext, ...drillContext,
vehicleScope, vehicleScope,
@@ -157,6 +161,19 @@ export default function ElectricDaily() {
endDate: effectiveRange.end, endDate: effectiveRange.end,
selectedDate: opening ? date : undefined, selectedDate: opening ? date : undefined,
}, opening ? 'push' : 'replace'); }, opening ? 'push' : 'replace');
window.requestAnimationFrame(() => {
if (opening) {
const details = document.getElementById(`electric-orders-${date}`);
details?.focus({ preventScroll: true });
details?.scrollIntoView({
behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth',
block: 'start',
});
} else {
const trigger = orderTriggerRef.current ?? document.getElementById(`electric-date-trigger-${date}`);
if (trigger instanceof HTMLElement) trigger.focus();
}
});
}; };
useEffect(() => { useEffect(() => {
@@ -291,11 +308,12 @@ export default function ElectricDaily() {
return ( return (
<div key={d.date} className="border-t border-slate-100"> <div key={d.date} className="border-t border-slate-100">
<button <button
id={`electric-date-trigger-${d.date}`}
type="button" type="button"
onClick={() => toggleDate(d.date)} onClick={() => toggleDate(d.date)}
className={`grid w-full grid-cols-[minmax(0,1fr)_120px_88px] gap-3 px-3 py-2 pl-9 text-left md:grid-cols-[minmax(0,1fr)_160px_120px] ${abnormalBg}`} className={`grid w-full grid-cols-[minmax(0,1fr)_120px_88px] gap-3 px-3 py-2 pl-9 text-left md:grid-cols-[minmax(0,1fr)_160px_120px] ${abnormalBg}`}
aria-expanded={selectedDate === d.date} aria-expanded={selectedDate === d.date}
aria-controls={selectedDate === d.date ? `electric-orders-${d.date}` : undefined} aria-controls={`electric-orders-${d.date}`}
> >
<span className="flex items-center gap-1 text-[12px] font-bold text-slate-600"> <span className="flex items-center gap-1 text-[12px] font-bold text-slate-600">
<ChevronRight size={12} className={`text-slate-400 transition-transform ${selectedDate === d.date ? 'rotate-90' : ''}`} /> <ChevronRight size={12} className={`text-slate-400 transition-transform ${selectedDate === d.date ? 'rotate-90' : ''}`} />
@@ -307,7 +325,27 @@ export default function ElectricDaily() {
<span className="text-right"><TrendBadge value={d.chainPct} /></span> <span className="text-right"><TrendBadge value={d.chainPct} /></span>
</button> </button>
{selectedDate === d.date && ( {selectedDate === d.date && (
<div id={`electric-orders-${d.date}`} className="border-t border-blue-100 bg-blue-50/40 px-3 py-3 md:pl-9"> <div
id={`electric-orders-${d.date}`}
tabIndex={-1}
aria-label={`${d.date} 充电订单明细`}
className="scroll-mt-3 border-t border-blue-100 bg-blue-50/40 px-3 py-3 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-500 md:pl-9"
>
<div className="mb-2 flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-1.5 text-[11px] font-black text-blue-800">
<Plug size={13} className="shrink-0" />
<span className="truncate">{d.date} </span>
</div>
<button
type="button"
onClick={() => toggleDate(d.date)}
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-white text-blue-500 ring-1 ring-blue-100 hover:text-blue-700"
aria-label={`关闭 ${d.date} 充电订单`}
title="关闭订单明细"
>
<X size={13} />
</button>
</div>
{orders && reconciliation && ( {orders && reconciliation && (
<div <div
role={reconciliation.matches ? 'status' : 'alert'} role={reconciliation.matches ? 'status' : 'alert'}