hanav
Version:
A React Navigation Menu Component Library.
111 lines (97 loc) • 3.41 kB
JavaScript
import React, { useState, useContext, useEffect, useRef } from "react";
import { ContextForMiniMenu } from "./context";
export default function Menu({ children, ...otherProps }) {
const [h, setH] = useState(0);
const { navRef, expanded, setE, btnsRef, openedMenuIdx,
tailFocusItemInContent, headFocusItemInContent, menuId, menuRef, mockTailFocusByWrap, mockTail, mockHeadFocusByWrap, mockHead, } = useContext(ContextForMiniMenu);
const [supportDvh, setDvh] = useState(false);
useEffect(() => {
setH(navRef.current.clientHeight);
setDvh(supportsDynamicViewportHeight());
}, []);
const { style, ..._otherProps } = otherProps;
if (!expanded) return null;
return <div
id={menuId}
role="dialog"
aria-modal
aria-label="Menu"
tabIndex={-1}
ref={menuRef}
style={{
height: `calc(100${supportDvh ? "d" : ""}vh - ${h}px)`,
position: "absolute",
left: 0,
top: "100%",
width: "100%",
...style,
}}
onKeyDown={trapTab}
{..._otherProps}>
{children}
</div>;
function trapTab(e) {
if (e.key === "Escape" || e.keyCode === 27)
setE(false);
else if (e.key === "Tab" || e.keyCode === 9) {
const btnsCount = btnsRef.current.length;
// 焦点矫正
if (e.shiftKey && e.target === menuRef.current) {
if (openedMenuIdx < 0) {
btnsRef.current.slice(-1)[0].focus();
e.preventDefault();
} else {
const tail = tailFocusItemInContent.current[openedMenuIdx];
if (tail) {
tail.focus();
e.preventDefault();
} else {
mockTailFocusByWrap.current = true;
mockTail.current.focus();
}
}
}
else if (!e.shiftKey && e.target === menuRef.current) {
if (openedMenuIdx > -1) {
const head = headFocusItemInContent.current[openedMenuIdx];
if (head) {
head.focus();
e.preventDefault();
} else {
mockHeadFocusByWrap.current = true;
mockHead.current.focus();
}
}
}
// 内容面板内的首尾聚焦
else if (e.shiftKey && openedMenuIdx > -1 && e.target === headFocusItemInContent.current[openedMenuIdx]) {
const tail = tailFocusItemInContent.current[openedMenuIdx] || menuRef.current;
tail.focus();
e.preventDefault();
}
else if (!e.shiftKey && openedMenuIdx > -1 && e.target === tailFocusItemInContent.current[openedMenuIdx]) {
const head = headFocusItemInContent.current[openedMenuIdx] || menuRef.current;
head.focus();
e.preventDefault();
}
// 触发器的首尾聚焦
else if (e.shiftKey && openedMenuIdx < 0 && e.target === btnsRef.current[0]) {
btnsRef.current[btnsCount - 1].focus();
e.preventDefault();
}
else if (!e.shiftKey && openedMenuIdx < 0 && e.target === btnsRef.current[btnsCount - 1]) {
btnsRef.current[0].focus();
e.preventDefault();
}
}
}
}
function supportsDynamicViewportHeight() {
const testElement = document.createElement('div');
testElement.style.height = '1dvh';
document.body.appendChild(testElement);
// Check if the computed height is greater than 0
const isSupported = testElement.clientHeight > 0;
document.body.removeChild(testElement);
return isSupported;
}