hanav
Version:
A React Navigation Menu Component Library.
177 lines (157 loc) • 5.29 kB
JavaScript
import React, { cloneElement, useEffect, useRef, forwardRef } from "react";
import { ContextForArrFocus } from "./context";
/** spatial navigation component */
export default forwardRef(function ArrowFocus({ children, ...otherProps }, ref) {
const { onKeyDown, ..._otherProps } = otherProps;
const items = useRef([]);
const dirMap = useRef();
useEffect(() => {
const squares = items.current.map(e => {
const t = e.getBoundingClientRect();
const { x, y, width, height } = t;
const halfW = width / 2;
const halfH = height / 2;
return [x + halfW, y + halfH, halfW, halfH, e];
});
dirMap.current = genDir(squares);
}, []);
const _onKeydown = e => {
onKeyDown(e);
const active = document.activeElement;
const d = items.current[0];
const target = dirMap.current.get(active) || { left: d, right: d, up: d, down: d };
if (e.key === "ArrowRight" || e.keyCode === 39) focus(target.right);
else if (e.key === "ArrowLeft" || e.keyCode === 37) focus(target.left);
else if (e.key === "ArrowUp" || e.keyCode === 38) focus(target.up);
else if (e.key === "ArrowDown" || e.keyCode === 40) focus(target.down);
function focus(tar) {
if (tar) {
tar.focus();
e.preventDefault();
}
}
}
return <ContextForArrFocus.Provider value={items}>
{cloneElement(children, { onKeyDown: _onKeydown, ref, ..._otherProps })}
</ContextForArrFocus.Provider>;
});
function genDir(squares) {
const sortedX = [...squares];
sortedX.sort((s1, s2) => s1[0] - s2[0]);
const sortedY = [...squares];
sortedY.sort((s1, s2) => s1[1] - s2[1]);
const dirMap = new Map();
squares.forEach((s, i) => {
const [x, y, t1, t2, e] = s;
const sOrderY = sortedY.findIndex(e => e === s);
const sOrderX = sortedX.findIndex(e => e === s);
// next down element
let minDownDis = Infinity;
let downS = null;
sortedY.slice(sOrderY + 1).forEach(s2 => {
const [x2, y2, t1a, t2a, e2] = s2;
let dis = Infinity;
if (y + t2 <= y2 - t2a) { // is below
if (x2 - t1a >= x + t1) { // is right corner
if (x2 - t1a - x - t1 < y2 - t2a - y - t2) {
dis = getDistance(x + t1, y + t2, x2 - t1a, y2 - t2a);
}
} else if (x2 + t1a <= x - t1) { // is left corner
if (x - t1 - x2 - t1a < y2 - t2a - y - t2) {
dis = getDistance(x + t1, y + t2, x2 + t1a, y2 - t2a);
}
} else { // is project
dis = Math.pow(y2 - t2a - y - t2, 2);
}
}
if (dis < minDownDis) {
downS = e2;
minDownDis = dis;
}
});
// next up element
let minUpDis = Infinity;
let upS = null;
sortedY.slice(0, sOrderY).forEach(s2 => {
const [x2, y2, t1a, t2a, e2] = s2;
let dis = Infinity;
if (y - t2 >= y2 + t2a) { // is above
if (x2 - t1a >= x + t1) { // is right corner
if (x2 - t1a - x - t1 < y - t2 - y2 - t2a) {
dis = getDistance(x + t1, y - t2, x2 - t1a, y2 + t2a);
}
} else if (x2 + t1a <= x - t1) { // is left corner
if (x - t1 - x2 - t1a < y - t2 - y2 - t2a) {
dis = getDistance(x - t1, y - t2, x2 + t1a, y2 + t2a);
}
} else { // is project
dis = Math.pow(y - t2 - y2 - t2a, 2);
}
}
if (dis < minUpDis) {
upS = e2;
minUpDis = dis;
}
});
// next left element
let minLDis = Infinity;
let lS = null;
sortedX.slice(0, sOrderX).forEach(s2 => {
const [x2, y2, t1a, t2a, e2] = s2;
let dis = Infinity;
if (x - t1 >= x2 + t1a) { // is left
if (y2 + t2a <= y - t2) { // is top corner
if (y - t2 - y2 - t2a < x - t1 - x2 - t1a) { // closer x
dis = getDistance(x - t1, y - t2, x2 + t1a, y2 + t2a);
}
} else if (y2 - t2a >= y + t2) { // is bottom corder
if (y2 - t2a - y - t2 < x - t1 - x2 - t1a) { // closer x
dis = getDistance(x - t1, y + t2, x2 + t1a, y2 - t2a);
}
} else { // is project
dis = Math.pow(x - t1 - x2 - t1a, 2);
}
}
if (dis < minLDis) {
lS = e2;
minLDis = dis;
}
})
// next right element
let minRDis = Infinity;
let rS = null;
sortedX.slice(sOrderX + 1).forEach(s2 => {
const [x2, y2, t1a, t2a, e2] = s2;
let dis = Infinity;
if (x2 - t1a >= x + t1) { // is right
if (y2 + t2a <= y - t2) { // is top corner
if (y - t2 - y2 - t2a < x2 - t1a - x - t1) { // closer x
dis = getDistance(x + t1, y - t2, x2 - t1a, y2 + t2a);
}
} else if (y2 - t2a >= y + t2) { // is bottom corder
if (y2 - t2a - y - t2 < x2 - t1a - x - t1) {
dis = getDistance(x + t1, y + t2, x2 - t1a, y2 - t2a);
}
} else {
dis = Math.pow(x2 - t1a - x - t1, 2);
}
}
if (dis < minRDis) {
rS = e2;
minRDis = dis;
}
});
dirMap.set(e, {
up: upS,
down: downS,
left: lS,
right: rS,
});
});
return dirMap;
}
function getDistance(x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
return dx * dx + dy * dy; // without sqrt
}