UNPKG

hanav

Version:

A React Navigation Menu Component Library.

180 lines (165 loc) 6.22 kB
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";