feat: make energy chart drilldowns keyboard accessible
This commit is contained in:
@@ -5,6 +5,7 @@ import { fetchElectricOverview, type ElectricOverviewResponse } from './api';
|
||||
import RotatingFooterHint from '../../components/RotatingFooterHint';
|
||||
import { ErrorState, LoadingState, MetricTile, SurfaceCard } from '../../components/ui/surface';
|
||||
import { buildElectricDrillUrl } from './electric-drill-context';
|
||||
import { activateDrillOnKeyDown } from './drill-interaction';
|
||||
|
||||
function fmtYuan(yuan: number) {
|
||||
return `¥${yuan.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}`;
|
||||
@@ -141,12 +142,7 @@ export default function ElectricOverview() {
|
||||
tabIndex={0}
|
||||
aria-label={`${item.date},充电费用 ${fmtYuan(item.fee)},查看全部车辆订单`}
|
||||
onClick={() => openDay(item.date)}
|
||||
onKeyDown={event => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
openDay(item.date);
|
||||
}
|
||||
}}
|
||||
onKeyDown={event => activateDrillOnKeyDown(event, () => openDay(item.date))}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import HydrogenOrders from './HydrogenOrders';
|
||||
import AnalysisDateFilters from './AnalysisDateFilters';
|
||||
import AnalysisScopeSwitch, { type AnalysisScopeOption } from './AnalysisScopeSwitch';
|
||||
import { activateDrillOnKeyDown } from './drill-interaction';
|
||||
import {
|
||||
dateRangeModeLabel,
|
||||
getQuickDateRange,
|
||||
@@ -88,6 +89,11 @@ export default function HydrogenDaily() {
|
||||
const avgKg = activeDays > 0 ? totalKg / activeDays : 0;
|
||||
const scopeLabel = dateRangeModeLabel(pick);
|
||||
const rangeText = `${effectiveRange.start} 至 ${effectiveRange.end}`;
|
||||
const orderScopeLabel = selectedCustomerName
|
||||
? `客户 ${selectedCustomerName}`
|
||||
: selectedStationName
|
||||
? `站点 ${selectedStationName}`
|
||||
: vehicleScope === 'external' ? '外部车辆' : '羚牛车辆';
|
||||
const peakDay = trendData.reduce<HydrogenDailyRow | null>((best, item) => (!best || item.totalKg > best.totalKg ? item : best), null);
|
||||
const lowDay = trendData
|
||||
.filter(item => item.totalKg > 0)
|
||||
@@ -346,17 +352,23 @@ export default function HydrogenDaily() {
|
||||
label={{ value: '均值', position: 'right', fill: '#d97706', fontSize: 10, fontWeight: 700 }}
|
||||
/>
|
||||
)}
|
||||
<Bar
|
||||
dataKey="totalKg"
|
||||
radius={[4, 4, 0, 0]}
|
||||
className="cursor-pointer"
|
||||
onClick={entry => {
|
||||
const date = String(entry.payload?.date ?? '');
|
||||
if (date && Number(entry.payload?.totalKg) > 0) openOrders(date);
|
||||
}}
|
||||
>
|
||||
{trendData.map((_, i) => (
|
||||
<Cell key={i} fill="url(#hydrogenBarGrad)" />
|
||||
<Bar dataKey="totalKg" radius={[4, 4, 0, 0]}>
|
||||
{trendData.map(item => (
|
||||
<Cell
|
||||
key={item.date}
|
||||
fill="url(#hydrogenBarGrad)"
|
||||
stroke={selectedDate === item.date ? '#1d4ed8' : undefined}
|
||||
strokeWidth={selectedDate === item.date ? 2 : undefined}
|
||||
className={item.totalKg > 0 ? 'cursor-pointer outline-none focus:stroke-blue-700 focus:stroke-2' : undefined}
|
||||
role={item.totalKg > 0 ? 'button' : undefined}
|
||||
tabIndex={item.totalKg > 0 ? 0 : -1}
|
||||
aria-pressed={item.totalKg > 0 ? selectedDate === item.date : undefined}
|
||||
aria-label={`${item.date},加氢量 ${item.totalKg.toLocaleString('zh-CN', { maximumFractionDigits: 2 })} Kg${item.totalKg > 0 ? `,查看${orderScopeLabel}加氢订单` : ',无可下钻订单'}`}
|
||||
onClick={item.totalKg > 0 ? () => openOrders(item.date) : undefined}
|
||||
onKeyDown={item.totalKg > 0
|
||||
? event => activateDrillOnKeyDown(event, () => openOrders(item.date))
|
||||
: undefined}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
<defs>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { activateDrillOnKeyDown } from './drill-interaction.js';
|
||||
|
||||
test('activates drilldown with Enter and Space only', () => {
|
||||
for (const key of ['Enter', ' ']) {
|
||||
let prevented = false;
|
||||
let activations = 0;
|
||||
const handled = activateDrillOnKeyDown(
|
||||
{ key, preventDefault: () => { prevented = true; } },
|
||||
() => { activations += 1; },
|
||||
);
|
||||
assert.equal(handled, true);
|
||||
assert.equal(prevented, true);
|
||||
assert.equal(activations, 1);
|
||||
}
|
||||
|
||||
let activations = 0;
|
||||
const handled = activateDrillOnKeyDown(
|
||||
{ key: 'ArrowRight', preventDefault: () => assert.fail('must not prevent navigation keys') },
|
||||
() => { activations += 1; },
|
||||
);
|
||||
assert.equal(handled, false);
|
||||
assert.equal(activations, 0);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface DrillKeyEvent {
|
||||
key: string;
|
||||
preventDefault: () => void;
|
||||
}
|
||||
|
||||
export function activateDrillOnKeyDown(event: DrillKeyEvent, activate: () => void): boolean {
|
||||
if (event.key !== 'Enter' && event.key !== ' ') return false;
|
||||
event.preventDefault();
|
||||
activate();
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user