1
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import type { MenuMode } from '../interface';
|
||||
/**
|
||||
* Get focusable elements from the element set under provided container
|
||||
*/
|
||||
export declare function getFocusableElements(container: HTMLElement, elements: Set<HTMLElement>): HTMLElement[];
|
||||
export declare const refreshElements: (keys: string[], id: string) => {
|
||||
elements: Set<HTMLElement>;
|
||||
key2element: Map<string, HTMLElement>;
|
||||
element2key: Map<HTMLElement, string>;
|
||||
};
|
||||
export declare function useAccessibility<T extends HTMLElement>(mode: MenuMode, activeKey: string, isRtl: boolean, id: string, containerRef: React.RefObject<HTMLUListElement>, getKeys: () => string[], getKeyPath: (key: string, includeOverflow?: boolean) => string[], triggerActiveKey: (key: string) => void, triggerAccessibilityOpen: (key: string, open?: boolean) => void, originOnKeyDown?: React.KeyboardEventHandler<T>): React.KeyboardEventHandler<T>;
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
import { getFocusNodeList } from "@rc-component/util/es/Dom/focus";
|
||||
import KeyCode from "@rc-component/util/es/KeyCode";
|
||||
import raf from "@rc-component/util/es/raf";
|
||||
import * as React from 'react';
|
||||
import { getMenuId } from "../context/IdContext";
|
||||
// destruct to reduce minify size
|
||||
const {
|
||||
LEFT,
|
||||
RIGHT,
|
||||
UP,
|
||||
DOWN,
|
||||
ENTER,
|
||||
ESC,
|
||||
HOME,
|
||||
END
|
||||
} = KeyCode;
|
||||
const ArrowKeys = [UP, DOWN, LEFT, RIGHT];
|
||||
function getOffset(mode, isRootLevel, isRtl, which) {
|
||||
const prev = 'prev';
|
||||
const next = 'next';
|
||||
const children = 'children';
|
||||
const parent = 'parent';
|
||||
|
||||
// Inline enter is special that we use unique operation
|
||||
if (mode === 'inline' && which === ENTER) {
|
||||
return {
|
||||
inlineTrigger: true
|
||||
};
|
||||
}
|
||||
const inline = {
|
||||
[UP]: prev,
|
||||
[DOWN]: next
|
||||
};
|
||||
const horizontal = {
|
||||
[LEFT]: isRtl ? next : prev,
|
||||
[RIGHT]: isRtl ? prev : next,
|
||||
[DOWN]: children,
|
||||
[ENTER]: children
|
||||
};
|
||||
const vertical = {
|
||||
[UP]: prev,
|
||||
[DOWN]: next,
|
||||
[ENTER]: children,
|
||||
[ESC]: parent,
|
||||
[LEFT]: isRtl ? children : parent,
|
||||
[RIGHT]: isRtl ? parent : children
|
||||
};
|
||||
const offsets = {
|
||||
inline,
|
||||
horizontal,
|
||||
vertical,
|
||||
inlineSub: inline,
|
||||
horizontalSub: vertical,
|
||||
verticalSub: vertical
|
||||
};
|
||||
const type = offsets[`${mode}${isRootLevel ? '' : 'Sub'}`]?.[which];
|
||||
switch (type) {
|
||||
case prev:
|
||||
return {
|
||||
offset: -1,
|
||||
sibling: true
|
||||
};
|
||||
case next:
|
||||
return {
|
||||
offset: 1,
|
||||
sibling: true
|
||||
};
|
||||
case parent:
|
||||
return {
|
||||
offset: -1,
|
||||
sibling: false
|
||||
};
|
||||
case children:
|
||||
return {
|
||||
offset: 1,
|
||||
sibling: false
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function findContainerUL(element) {
|
||||
let current = element;
|
||||
while (current) {
|
||||
if (current.getAttribute('data-menu-list')) {
|
||||
return current;
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
|
||||
// Normally should not reach this line
|
||||
/* istanbul ignore next */
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find focused element within element set provided
|
||||
*/
|
||||
function getFocusElement(activeElement, elements) {
|
||||
let current = activeElement || document.activeElement;
|
||||
while (current) {
|
||||
if (elements.has(current)) {
|
||||
return current;
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get focusable elements from the element set under provided container
|
||||
*/
|
||||
export function getFocusableElements(container, elements) {
|
||||
const list = getFocusNodeList(container, true);
|
||||
return list.filter(ele => elements.has(ele));
|
||||
}
|
||||
function getNextFocusElement(parentQueryContainer, elements, focusMenuElement, offset = 1) {
|
||||
// Key on the menu item will not get validate parent container
|
||||
if (!parentQueryContainer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// List current level menu item elements
|
||||
const sameLevelFocusableMenuElementList = getFocusableElements(parentQueryContainer, elements);
|
||||
|
||||
// Find next focus index
|
||||
const count = sameLevelFocusableMenuElementList.length;
|
||||
let focusIndex = sameLevelFocusableMenuElementList.findIndex(ele => focusMenuElement === ele);
|
||||
if (offset < 0) {
|
||||
if (focusIndex === -1) {
|
||||
focusIndex = count - 1;
|
||||
} else {
|
||||
focusIndex -= 1;
|
||||
}
|
||||
} else if (offset > 0) {
|
||||
focusIndex += 1;
|
||||
}
|
||||
focusIndex = (focusIndex + count) % count;
|
||||
|
||||
// Focus menu item
|
||||
return sameLevelFocusableMenuElementList[focusIndex];
|
||||
}
|
||||
export const refreshElements = (keys, id) => {
|
||||
const elements = new Set();
|
||||
const key2element = new Map();
|
||||
const element2key = new Map();
|
||||
keys.forEach(key => {
|
||||
const element = document.querySelector(`[data-menu-id='${getMenuId(id, key)}']`);
|
||||
if (element) {
|
||||
elements.add(element);
|
||||
element2key.set(element, key);
|
||||
key2element.set(key, element);
|
||||
}
|
||||
});
|
||||
return {
|
||||
elements,
|
||||
key2element,
|
||||
element2key
|
||||
};
|
||||
};
|
||||
export function useAccessibility(mode, activeKey, isRtl, id, containerRef, getKeys, getKeyPath, triggerActiveKey, triggerAccessibilityOpen, originOnKeyDown) {
|
||||
const rafRef = React.useRef();
|
||||
const activeRef = React.useRef();
|
||||
activeRef.current = activeKey;
|
||||
const cleanRaf = () => {
|
||||
raf.cancel(rafRef.current);
|
||||
};
|
||||
React.useEffect(() => () => {
|
||||
cleanRaf();
|
||||
}, []);
|
||||
return e => {
|
||||
const {
|
||||
which
|
||||
} = e;
|
||||
if ([...ArrowKeys, ENTER, ESC, HOME, END].includes(which)) {
|
||||
const keys = getKeys();
|
||||
let refreshedElements = refreshElements(keys, id);
|
||||
const {
|
||||
elements,
|
||||
key2element,
|
||||
element2key
|
||||
} = refreshedElements;
|
||||
|
||||
// First we should find current focused MenuItem/SubMenu element
|
||||
const activeElement = key2element.get(activeKey);
|
||||
const focusMenuElement = getFocusElement(activeElement, elements);
|
||||
const focusMenuKey = element2key.get(focusMenuElement);
|
||||
const offsetObj = getOffset(mode, getKeyPath(focusMenuKey, true).length === 1, isRtl, which);
|
||||
|
||||
// Some mode do not have fully arrow operation like inline
|
||||
if (!offsetObj && which !== HOME && which !== END) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Arrow prevent default to avoid page scroll
|
||||
if (ArrowKeys.includes(which) || [HOME, END].includes(which)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
const tryFocus = menuElement => {
|
||||
if (menuElement) {
|
||||
let focusTargetElement = menuElement;
|
||||
|
||||
// Focus to link instead of menu item if possible
|
||||
const link = menuElement.querySelector('a');
|
||||
if (link?.getAttribute('href')) {
|
||||
focusTargetElement = link;
|
||||
}
|
||||
const targetKey = element2key.get(menuElement);
|
||||
triggerActiveKey(targetKey);
|
||||
|
||||
/**
|
||||
* Do not `useEffect` here since `tryFocus` may trigger async
|
||||
* which makes React sync update the `activeKey`
|
||||
* that force render before `useRef` set the next activeKey
|
||||
*/
|
||||
cleanRaf();
|
||||
rafRef.current = raf(() => {
|
||||
if (activeRef.current === targetKey) {
|
||||
focusTargetElement.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
if ([HOME, END].includes(which) || offsetObj.sibling || !focusMenuElement) {
|
||||
// ========================== Sibling ==========================
|
||||
// Find walkable focus menu element container
|
||||
let parentQueryContainer;
|
||||
if (!focusMenuElement || mode === 'inline') {
|
||||
parentQueryContainer = containerRef.current;
|
||||
} else {
|
||||
parentQueryContainer = findContainerUL(focusMenuElement);
|
||||
}
|
||||
|
||||
// Get next focus element
|
||||
let targetElement;
|
||||
const focusableElements = getFocusableElements(parentQueryContainer, elements);
|
||||
if (which === HOME) {
|
||||
targetElement = focusableElements[0];
|
||||
} else if (which === END) {
|
||||
targetElement = focusableElements[focusableElements.length - 1];
|
||||
} else {
|
||||
targetElement = getNextFocusElement(parentQueryContainer, elements, focusMenuElement, offsetObj.offset);
|
||||
}
|
||||
// Focus menu item
|
||||
tryFocus(targetElement);
|
||||
|
||||
// ======================= InlineTrigger =======================
|
||||
} else if (offsetObj.inlineTrigger) {
|
||||
// Inline trigger no need switch to sub menu item
|
||||
triggerAccessibilityOpen(focusMenuKey);
|
||||
// =========================== Level ===========================
|
||||
} else if (offsetObj.offset > 0) {
|
||||
triggerAccessibilityOpen(focusMenuKey, true);
|
||||
cleanRaf();
|
||||
rafRef.current = raf(() => {
|
||||
// Async should resync elements
|
||||
refreshedElements = refreshElements(keys, id);
|
||||
const controlId = focusMenuElement.getAttribute('aria-controls');
|
||||
const subQueryContainer = document.getElementById(controlId);
|
||||
|
||||
// Get sub focusable menu item
|
||||
const targetElement = getNextFocusElement(subQueryContainer, refreshedElements.elements);
|
||||
|
||||
// Focus menu item
|
||||
tryFocus(targetElement);
|
||||
}, 5);
|
||||
} else if (offsetObj.offset < 0) {
|
||||
const keyPath = getKeyPath(focusMenuKey, true);
|
||||
const parentKey = keyPath[keyPath.length - 2];
|
||||
const parentMenuElement = key2element.get(parentKey);
|
||||
|
||||
// Focus menu item
|
||||
triggerAccessibilityOpen(parentKey, false);
|
||||
tryFocus(parentMenuElement);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass origin key down event
|
||||
originOnKeyDown?.(e);
|
||||
};
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import type { MenuHoverEventHandler } from '../interface';
|
||||
interface ActiveObj {
|
||||
active: boolean;
|
||||
onMouseEnter?: React.MouseEventHandler<HTMLElement>;
|
||||
onMouseLeave?: React.MouseEventHandler<HTMLElement>;
|
||||
}
|
||||
export default function useActive(eventKey: string, disabled: boolean, onMouseEnter?: MenuHoverEventHandler, onMouseLeave?: MenuHoverEventHandler): ActiveObj;
|
||||
export {};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react';
|
||||
import { MenuContext } from "../context/MenuContext";
|
||||
export default function useActive(eventKey, disabled, onMouseEnter, onMouseLeave) {
|
||||
const {
|
||||
// Active
|
||||
activeKey,
|
||||
onActive,
|
||||
onInactive
|
||||
} = React.useContext(MenuContext);
|
||||
const ret = {
|
||||
active: activeKey === eventKey
|
||||
};
|
||||
|
||||
// Skip when disabled
|
||||
if (!disabled) {
|
||||
ret.onMouseEnter = domEvent => {
|
||||
onMouseEnter?.({
|
||||
key: eventKey,
|
||||
domEvent
|
||||
});
|
||||
onActive(eventKey);
|
||||
};
|
||||
ret.onMouseLeave = domEvent => {
|
||||
onMouseLeave?.({
|
||||
key: eventKey,
|
||||
domEvent
|
||||
});
|
||||
onInactive(eventKey);
|
||||
};
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import * as React from 'react';
|
||||
export default function useDirectionStyle(level: number): React.CSSProperties;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import { MenuContext } from "../context/MenuContext";
|
||||
export default function useDirectionStyle(level) {
|
||||
const {
|
||||
mode,
|
||||
rtl,
|
||||
inlineIndent
|
||||
} = React.useContext(MenuContext);
|
||||
if (mode !== 'inline') {
|
||||
return null;
|
||||
}
|
||||
const len = level;
|
||||
return rtl ? {
|
||||
paddingRight: len * inlineIndent
|
||||
} : {
|
||||
paddingLeft: len * inlineIndent
|
||||
};
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export declare const OVERFLOW_KEY = "rc-menu-more";
|
||||
export default function useKeyRecords(): {
|
||||
registerPath: (key: string, keyPath: string[]) => void;
|
||||
unregisterPath: (key: string, keyPath: string[]) => void;
|
||||
refreshOverflowKeys: (keys: string[]) => void;
|
||||
isSubPathKey: (pathKeys: string[], eventKey: string) => boolean;
|
||||
getKeyPath: (eventKey: string, includeOverflow?: boolean) => string[];
|
||||
getKeys: () => string[];
|
||||
getSubPathKeys: (key: string) => Set<string>;
|
||||
};
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import * as React from 'react';
|
||||
import { useRef, useCallback } from 'react';
|
||||
import warning from "@rc-component/util/es/warning";
|
||||
import { nextSlice } from "../utils/timeUtil";
|
||||
const PATH_SPLIT = '__RC_UTIL_PATH_SPLIT__';
|
||||
const getPathStr = keyPath => keyPath.join(PATH_SPLIT);
|
||||
const getPathKeys = keyPathStr => keyPathStr.split(PATH_SPLIT);
|
||||
export const OVERFLOW_KEY = 'rc-menu-more';
|
||||
export default function useKeyRecords() {
|
||||
const [, internalForceUpdate] = React.useState({});
|
||||
const key2pathRef = useRef(new Map());
|
||||
const path2keyRef = useRef(new Map());
|
||||
const [overflowKeys, setOverflowKeys] = React.useState([]);
|
||||
const updateRef = useRef(0);
|
||||
const destroyRef = useRef(false);
|
||||
const forceUpdate = () => {
|
||||
if (!destroyRef.current) {
|
||||
internalForceUpdate({});
|
||||
}
|
||||
};
|
||||
const registerPath = useCallback((key, keyPath) => {
|
||||
// Warning for invalidate or duplicated `key`
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
warning(!key2pathRef.current.has(key), `Duplicated key '${key}' used in Menu by path [${keyPath.join(' > ')}]`);
|
||||
}
|
||||
|
||||
// Fill map
|
||||
const connectedPath = getPathStr(keyPath);
|
||||
path2keyRef.current.set(connectedPath, key);
|
||||
key2pathRef.current.set(key, connectedPath);
|
||||
updateRef.current += 1;
|
||||
const id = updateRef.current;
|
||||
nextSlice(() => {
|
||||
if (id === updateRef.current) {
|
||||
forceUpdate();
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
const unregisterPath = useCallback((key, keyPath) => {
|
||||
const connectedPath = getPathStr(keyPath);
|
||||
path2keyRef.current.delete(connectedPath);
|
||||
key2pathRef.current.delete(key);
|
||||
}, []);
|
||||
const refreshOverflowKeys = useCallback(keys => {
|
||||
setOverflowKeys(keys);
|
||||
}, []);
|
||||
const getKeyPath = useCallback((eventKey, includeOverflow) => {
|
||||
const fullPath = key2pathRef.current.get(eventKey) || '';
|
||||
const keys = getPathKeys(fullPath);
|
||||
if (includeOverflow && overflowKeys.includes(keys[0])) {
|
||||
keys.unshift(OVERFLOW_KEY);
|
||||
}
|
||||
return keys;
|
||||
}, [overflowKeys]);
|
||||
const isSubPathKey = useCallback((pathKeys, eventKey) => pathKeys.filter(item => item !== undefined).some(pathKey => {
|
||||
const pathKeyList = getKeyPath(pathKey, true);
|
||||
return pathKeyList.includes(eventKey);
|
||||
}), [getKeyPath]);
|
||||
const getKeys = () => {
|
||||
const keys = [...key2pathRef.current.keys()];
|
||||
if (overflowKeys.length) {
|
||||
keys.push(OVERFLOW_KEY);
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find current key related child path keys
|
||||
*/
|
||||
const getSubPathKeys = useCallback(key => {
|
||||
const connectedPath = `${key2pathRef.current.get(key)}${PATH_SPLIT}`;
|
||||
const pathKeys = new Set();
|
||||
[...path2keyRef.current.keys()].forEach(pathKey => {
|
||||
if (pathKey.startsWith(connectedPath)) {
|
||||
pathKeys.add(path2keyRef.current.get(pathKey));
|
||||
}
|
||||
});
|
||||
return pathKeys;
|
||||
}, []);
|
||||
React.useEffect(() => () => {
|
||||
destroyRef.current = true;
|
||||
}, []);
|
||||
return {
|
||||
// Register
|
||||
registerPath,
|
||||
unregisterPath,
|
||||
refreshOverflowKeys,
|
||||
// Util
|
||||
isSubPathKey,
|
||||
getKeyPath,
|
||||
getKeys,
|
||||
getSubPathKeys
|
||||
};
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Cache callback function that always return same ref instead.
|
||||
* This is used for context optimization.
|
||||
*/
|
||||
export default function useMemoCallback<T extends (...args: any[]) => void>(func: T): T;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* Cache callback function that always return same ref instead.
|
||||
* This is used for context optimization.
|
||||
*/
|
||||
export default function useMemoCallback(func) {
|
||||
const funRef = React.useRef(func);
|
||||
funRef.current = func;
|
||||
const callback = React.useCallback((...args) => funRef.current?.(...args), []);
|
||||
return func ? callback : undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user