hanav
Version:
A React Navigation Menu Component Library.
180 lines (165 loc) • 6.22 kB
JavaScript
import React, { cloneElement, useContext, useEffect, useId, useRef, useState } from "react";
import { ContextForItem, ContextForItemOrder } from "./context";
export default function Item({ children, type, orderI, contentItemStyle, transRunning }) {
const isTrigger = type === 'T';
const isContent = type === 'C';
const context = useContext(ContextForItem);
const ariaId = useId();
const mockTail = useRef();
const mockTailFocusByWrap = useRef();
const { triggerAriaIds, contentAriaIds, prevMenuIdxRef, openedMenuIdx, isKeyActive, setActivePanel } = context;
const [controlContentId, setControl] = useState(); // 收起 slate 会移除 dom,因此动态设置 id
useEffect(() => {
if (openedMenuIdx > -1 && prevMenuIdxRef.current < 0) {
setControl(contentAriaIds.current[orderI]);
} else if (openedMenuIdx < 0) {
setControl(null);
}
}, [openedMenuIdx, orderI, controlContentId]);
if (isTrigger) {
const { btnsRef, overMenu, leaveMenu } = context;
const openedMenu = openedMenuIdx === orderI;
triggerAriaIds.current[orderI] = ariaId;
/** 点击菜单按钮 */
const clickMenuBtn = e => {
const target = e.target;
let targetIdx = btnsRef.current.findIndex(e => e === target);
if (targetIdx < 0) targetIdx = btnsRef.current.findIndex(e => e.contains(target));
if (targetIdx > -1) {
isKeyActive.current = e.nativeEvent.offsetX === 0 && e.nativeEvent.offsetY === 0;
if (targetIdx === openedMenuIdx) {
// 关闭菜单
setActivePanel(-1);
} else {
// 打开菜单
setActivePanel(targetIdx);
}
}
};
const triggerProps = {
ref: e => btnsRef.current[orderI] = e,
onClick: clickMenuBtn,
onMouseOver: overMenu,
onMouseLeave: leaveMenu,
id: ariaId,
"aria-expanded": openedMenu,
"aria-controls": controlContentId,
};
// render props
if (typeof children === "function")
return children(triggerProps, openedMenu);
// component
return cloneElement(children, triggerProps);
}
if (isContent) {
const {
panelsRef,
headFocusItemInContent,
tailFocusItemInContent,
checkedFocusOwnerContent,
prevMenuIdxRef,
onlyKeyFocus,
contentWrapperRef
} = context;
const openedMenu = openedMenuIdx === orderI;
contentAriaIds.current[orderI] = ariaId;
/** 菜单面板上的键盘操作 */
const onKeyDown = e => {
if (e.key === "Escape" || e.key === "Esc" || e.keyCode === 27) {
// 关闭菜单
isKeyActive.current = true;
setActivePanel(-1);
return;
}
if (e.key === "Tab" || e.keyCode === 9) {
// 动画进行时期,禁止 tab,避免聚焦引起的样式错位
if (transRunning?.current) {
e.preventDefault();
return;
}
// 非键盘模式下切换菜单之后,按下 tab
if (!checkedFocusOwnerContent.current && prevMenuIdxRef.current > -1 && onlyKeyFocus && !isKeyActive.current) {
const activeE = document.activeElement;
if (contentWrapperRef.current?.contains(activeE)) { // 焦点在所有面板的 wrapper 中
const focusTarget = panelsRef.current[openedMenuIdx]; // 当前面板
if (!focusTarget.contains(activeE)) { // 焦点不在当前面板
checkedFocusOwnerContent.current = true;
focusTarget.focus({ preventScroll: true });
e.preventDefault();
return;
}
}
}
}
const head = headFocusItemInContent.current[orderI];
const tail = tailFocusItemInContent.current[orderI];
// 焦点矫正
if (e.target === panelsRef.current[orderI]) {
if (e.key === "Tab" || e.keyCode === 9) {
if (e.shiftKey) {
if (tail) {
tail.focus();
e.preventDefault();
} else {
mockTailFocusByWrap.current = true;
mockTail.current.focus();
}
} else {
if (head) {
head.focus();
e.preventDefault();
}
}
}
}
// 回尾
if (e.target === head && (e.key === "Tab" || e.keyCode === 9) && e.shiftKey) {
const _tail = tail || panelsRef.current[orderI];
_tail.focus();
e.preventDefault();
}
// 回头
if (e.target === tail && (e.key === "Tab" || e.keyCode === 9) && !e.shiftKey) {
const _head = head || panelsRef.current[orderI];
_head.focus();
e.preventDefault();
}
};
const contentProps = {
onKeyDown,
ref: e => panelsRef.current[orderI] = e,
style: contentItemStyle,
id: ariaId,
"aria-labelledby": triggerAriaIds.current[orderI],
"aria-hidden": !openedMenu,
tabIndex: -1
};
const getHead = e => headFocusItemInContent.current[orderI] = e;
const getTail = e => tailFocusItemInContent.current[orderI] = e;
return <ContextForItemOrder.Provider value={orderI}>
{/* mock head */}
{headFocusItemInContent.current[orderI] == null && <button role="presentation" style={{ position: "absolute", opacity: 0 }} onFocus={(e) => {
const tail = tailFocusItemInContent.current[orderI] || panelsRef.current[orderI];
tail.focus();
e.preventDefault();
}}></button>}
{typeof children === "function" ?
// render props
children(contentProps, getHead, getTail) :
// component
cloneElement(children, contentProps)}
{/* mock tail */}
{tailFocusItemInContent.current[orderI] == null && <button ref={mockTail} role="presentation" style={{ position: "absolute", opacity: 0 }} onFocus={(e) => {
if (mockTailFocusByWrap.current) {
mockTailFocusByWrap.current = false;
return ;
}
const head = headFocusItemInContent.current[orderI] || panelsRef.current[orderI];
head.focus();
e.preventDefault();
}}></button>}
</ContextForItemOrder.Provider>;
}
return children;
}
Item.displayName = "Item";