AURORA-1944: таблицы форм — шаг прокрутки колесом мыши ×2 #247
@ -20,6 +20,7 @@ import { BASE_TABLE_CONFIG, TABLE_ROW_HEIGHT, getTableBodyCellProps, getTablePap
|
||||
import { useRealtimeActions, useVspOptions } from './contexts/RealtimeContext';
|
||||
import { useColumnSettings } from './hooks/useColumnSettings';
|
||||
import { useHeaderPortal } from './hooks/useHeaderPortal';
|
||||
import { useWheelScrollStep } from './hooks/useWheelScrollStep';
|
||||
import { useTableScale } from './hooks/useTableScale';
|
||||
import { useBudgetYear } from './hooks/useBudgetYear';
|
||||
import { useValidationRules } from './hooks/useValidationRules';
|
||||
@ -158,6 +159,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||
} = useColumnSettings(`${formId}_${formType}_${sheetName}_${direction}`);
|
||||
|
||||
const { headerPortalRef, containerRef } = useHeaderPortal();
|
||||
useWheelScrollStep(containerRef);
|
||||
const rowVirtualizerRef = useRef(null);
|
||||
const columnVirtualizerRef = useRef(null);
|
||||
|
||||
|
||||
35
web/src/components/RealtimeTable/hooks/useWheelScrollStep.js
Normal file
35
web/src/components/RealtimeTable/hooks/useWheelScrollStep.js
Normal file
@ -0,0 +1,35 @@
|
||||
import { useEffect } from 'react';
|
||||
import { canScrollBy, getWheelScrollDelta, getWheelScrollTarget } from '../utils/wheelScroll';
|
||||
|
||||
// Внутри полей ввода и выпадающих списков колесо работает как обычно.
|
||||
const NATIVE_WHEEL_SELECTOR = 'textarea, select, input, [role="listbox"]';
|
||||
|
||||
export const useWheelScrollStep = (containerRef) => {
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return undefined;
|
||||
let pendingTarget = null;
|
||||
let lastWheelAt = 0;
|
||||
const onWheel = (event) => {
|
||||
if (event.target instanceof Element && event.target.closest(NATIVE_WHEEL_SELECTOR)) return;
|
||||
const delta = getWheelScrollDelta(event);
|
||||
if (delta === null || !canScrollBy(el, delta)) return;
|
||||
event.preventDefault();
|
||||
const now = performance.now();
|
||||
const target = getWheelScrollTarget({
|
||||
scrollTop: el.scrollTop,
|
||||
maxScrollTop: el.scrollHeight - el.clientHeight,
|
||||
delta,
|
||||
pendingTarget,
|
||||
msSinceLastWheel: now - lastWheelAt,
|
||||
});
|
||||
pendingTarget = target;
|
||||
lastWheelAt = now;
|
||||
const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
||||
el.scrollTo({ top: target, behavior: reduceMotion ? 'auto' : 'smooth' });
|
||||
};
|
||||
// passive: false — иначе preventDefault не работает.
|
||||
el.addEventListener('wheel', onWheel, { passive: false });
|
||||
return () => el.removeEventListener('wheel', onWheel);
|
||||
}, [containerRef]);
|
||||
};
|
||||
30
web/src/components/RealtimeTable/utils/wheelScroll.js
Normal file
30
web/src/components/RealtimeTable/utils/wheelScroll.js
Normal file
@ -0,0 +1,30 @@
|
||||
// Шаг прокрутки колесом мыши в таблице форм (AURORA-1944).
|
||||
// Браузер прокручивает ~100 px за щелчок, строки 34–48 px: за щелчок уезжает 2–3 строки.
|
||||
export const WHEEL_SCROLL_MULTIPLIER = 2;
|
||||
|
||||
const DOM_DELTA_PIXEL = 0;
|
||||
const DOM_DELTA_LINE = 1;
|
||||
// Щелчок колеса мыши в Chrome/Edge — от ~50 px; тачпад шлёт мелкие шаги, их не трогаем.
|
||||
const MIN_MOUSE_WHEEL_DELTA_PX = 50;
|
||||
// Firefox шлёт строки (deltaMode = 1, обычно 3 строки за щелчок).
|
||||
const LINE_HEIGHT_PX = 33;
|
||||
|
||||
// На сколько px прокрутить по вертикали вместо браузера, или null — отдать событие браузеру.
|
||||
export const getWheelScrollDelta = ({ deltaX, deltaY, deltaMode, ctrlKey, shiftKey }) => {
|
||||
if (ctrlKey || shiftKey || deltaY === 0 || Math.abs(deltaX) > Math.abs(deltaY)) return null;
|
||||
if (deltaMode === DOM_DELTA_LINE) return deltaY * LINE_HEIGHT_PX * WHEEL_SCROLL_MULTIPLIER;
|
||||
if (deltaMode !== DOM_DELTA_PIXEL || Math.abs(deltaY) < MIN_MOUSE_WHEEL_DELTA_PX) return null;
|
||||
return deltaY * WHEEL_SCROLL_MULTIPLIER;
|
||||
};
|
||||
|
||||
// Можно ли прокрутить элемент на delta по вертикали (иначе отдаём событие браузеру — прокрутка страницы).
|
||||
export const canScrollBy = ({ scrollTop, scrollHeight, clientHeight }, delta) =>
|
||||
delta < 0 ? scrollTop > 0 : scrollTop + clientHeight < scrollHeight;
|
||||
|
||||
// Пока идёт плавная прокрутка предыдущего щелчка, следующий щелчок добавляется к его цели.
|
||||
export const WHEEL_TARGET_RESET_MS = 300;
|
||||
|
||||
export const getWheelScrollTarget = ({ scrollTop, maxScrollTop, delta, pendingTarget, msSinceLastWheel }) => {
|
||||
const base = pendingTarget !== null && msSinceLastWheel < WHEEL_TARGET_RESET_MS ? pendingTarget : scrollTop;
|
||||
return Math.min(maxScrollTop, Math.max(0, base + delta));
|
||||
};
|
||||
66
web/src/components/RealtimeTable/utils/wheelScroll.test.js
Normal file
66
web/src/components/RealtimeTable/utils/wheelScroll.test.js
Normal file
@ -0,0 +1,66 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { canScrollBy, getWheelScrollDelta, getWheelScrollTarget } from './wheelScroll.js';
|
||||
|
||||
const wheel = (overrides) => ({ deltaX: 0, deltaY: 0, deltaMode: 0, ctrlKey: false, shiftKey: false, ...overrides });
|
||||
|
||||
test('щелчок колеса мыши удваивает шаг', () => {
|
||||
assert.equal(getWheelScrollDelta(wheel({ deltaY: 100 })), 200);
|
||||
assert.equal(getWheelScrollDelta(wheel({ deltaY: -100 })), -200);
|
||||
assert.equal(getWheelScrollDelta(wheel({ deltaY: 120 })), 240);
|
||||
});
|
||||
|
||||
test('тачпад (мелкие px-шаги) не трогаем', () => {
|
||||
assert.equal(getWheelScrollDelta(wheel({ deltaY: 10 })), null);
|
||||
assert.equal(getWheelScrollDelta(wheel({ deltaY: 49 })), null);
|
||||
});
|
||||
|
||||
test('Firefox (строки) считает в строках ×2', () => {
|
||||
assert.equal(getWheelScrollDelta(wheel({ deltaMode: 1, deltaY: 3 })), 198);
|
||||
});
|
||||
|
||||
test('прокрутка страницами (deltaMode 2) не трогаем', () => {
|
||||
assert.equal(getWheelScrollDelta(wheel({ deltaMode: 2, deltaY: 1 })), null);
|
||||
});
|
||||
|
||||
test('Ctrl и Shift не трогаем', () => {
|
||||
assert.equal(getWheelScrollDelta(wheel({ deltaY: 100, ctrlKey: true })), null);
|
||||
assert.equal(getWheelScrollDelta(wheel({ deltaY: 100, shiftKey: true })), null);
|
||||
});
|
||||
|
||||
test('горизонтальная прокрутка и нулевая дельта не трогаем', () => {
|
||||
assert.equal(getWheelScrollDelta(wheel({ deltaX: 100, deltaY: 20 })), null);
|
||||
assert.equal(getWheelScrollDelta(wheel({ deltaY: 0 })), null);
|
||||
});
|
||||
|
||||
test('canScrollBy: у верха вверх и у низа вниз — нельзя, в середине — можно', () => {
|
||||
const top = { scrollTop: 0, scrollHeight: 1000, clientHeight: 400 };
|
||||
const bottom = { scrollTop: 600, scrollHeight: 1000, clientHeight: 400 };
|
||||
const middle = { scrollTop: 300, scrollHeight: 1000, clientHeight: 400 };
|
||||
assert.equal(canScrollBy(top, -200), false);
|
||||
assert.equal(canScrollBy(bottom, 200), false);
|
||||
assert.equal(canScrollBy(middle, 200), true);
|
||||
assert.equal(canScrollBy(middle, -200), true);
|
||||
});
|
||||
|
||||
const target = (overrides) => ({ scrollTop: 0, maxScrollTop: 1000, delta: 200, pendingTarget: null, msSinceLastWheel: 0, ...overrides });
|
||||
|
||||
test('getWheelScrollTarget: нет прошлой цели — от scrollTop', () => {
|
||||
assert.equal(getWheelScrollTarget(target({ scrollTop: 250 })), 450);
|
||||
});
|
||||
|
||||
test('getWheelScrollTarget: прошлая цель не дошла, щелчок быстрый — копим от цели', () => {
|
||||
assert.equal(getWheelScrollTarget(target({ scrollTop: 250, pendingTarget: 400, msSinceLastWheel: 100 })), 600);
|
||||
});
|
||||
|
||||
test('getWheelScrollTarget: прошло больше WHEEL_TARGET_RESET_MS — от scrollTop', () => {
|
||||
assert.equal(getWheelScrollTarget(target({ scrollTop: 250, pendingTarget: 400, msSinceLastWheel: 500 })), 450);
|
||||
});
|
||||
|
||||
test('getWheelScrollTarget: ограничение сверху', () => {
|
||||
assert.equal(getWheelScrollTarget(target({ scrollTop: 900 })), 1000);
|
||||
});
|
||||
|
||||
test('getWheelScrollTarget: ограничение снизу', () => {
|
||||
assert.equal(getWheelScrollTarget(target({ scrollTop: 50, delta: -200 })), 0);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user