UNPKG

@momentum-ui/react-collaboration

Version:

Cisco Momentum UI Framework for React Collaboration Applications

296 lines 12.3 kB
var _a; import React, { useCallback, useContext } from 'react'; import { getKeyboardFocusableElements } from '../../utils/navigation'; import { STYLE as TreeNodeBaseStyle } from '../TreeNodeBase/TreeNodeBase.constants'; import { STYLE as MeetingListItemStyle } from '../MeetingListItem/MeetingListItem.constants'; export var SpatialNavigationContext = React.createContext(undefined); var DIRECTIONS = ['left', 'right', 'up', 'down']; var FAR_EDGE_VALUES = ['none', 'horizontal', 'vertical', 'both']; var NESTED_FOCUSABLE_DIRECTION_MAP = (_a = {}, _a[TreeNodeBaseStyle.wrapper] = 'horizontal', _a[MeetingListItemStyle.wrapper] = 'horizontal', _a); /** * Get the current spatial navigation context. */ export var useSpatialNavigationContext = function () { return useContext(SpatialNavigationContext); }; /** * Calculate the center point of the element */ export var getElementRectWithMidPoint = function (element) { var _a = element.getBoundingClientRect(), x = _a.x, y = _a.y, width = _a.width, height = _a.height, left = _a.left, top = _a.top, right = _a.right, bottom = _a.bottom; var xMid = x + width / 2; var yMid = y + height / 2; return { x: x, y: y, width: width, height: height, left: left, top: top, right: right, bottom: bottom, xMid: xMid, yMid: yMid }; }; /** * Complex components can have nested focusable items, for example * List or Tree nodes * * Spatial navigation can process 4 cases: * - none - no nested focusable item (default) * - horizontal - the focusable items are in a row * - vertical - the focusable items are in a column * - both - the focusable items are in a grid * * @param el - checked element */ export var getNestedFocusableDirection = function (el) { var farEdge = el.dataset.spatialNestedFocusableDirection; if (farEdge && FAR_EDGE_VALUES.includes(farEdge)) return farEdge; var key = Object.keys(NESTED_FOCUSABLE_DIRECTION_MAP).find(function (cls) { return el.classList.contains(cls); }); if (key) return NESTED_FOCUSABLE_DIRECTION_MAP[key]; return 'none'; }; /** * Calculate distance between the closest edges of the passed bounding boxes for the specified direction * * @param a bounding box 1 * @param b bounding box 2 * @param dir direction specify which edge measured * @param farEdge * @returns distance */ export var getEdgeDistance = function (a, b, dir, farEdge) { if (farEdge === void 0) { farEdge = 'none'; } if (dir === 'left') { if (farEdge === 'horizontal' || farEdge === 'both') { return a.right - b.right; } else { return a.left - b.right; } } if (dir === 'right') { if (farEdge === 'horizontal' || farEdge === 'both') { return b.left - a.left; } else { return b.left - a.right; } } if (dir === 'up') { if (farEdge === 'vertical' || farEdge === 'both') { return a.bottom - b.bottom; } else { return a.top - b.bottom; } } if (dir === 'down') { if (farEdge === 'vertical' || farEdge === 'both') { return b.top - a.top; } else { return b.top - a.bottom; } } }; /** * Calculate expanded bounding boxes in all four directions with the specified size * * @param baseRect * @param size */ export var getExpandedRect = function (baseRect, size) { return DIRECTIONS.reduce(function (acc, dir) { var x = dir === 'left' ? baseRect.x - size : baseRect.x; var width = dir === 'right' || dir === 'left' ? baseRect.width + size : baseRect.width; var y = dir === 'up' ? baseRect.y - size : baseRect.y; var height = dir === 'down' || dir === 'up' ? baseRect.height + size : baseRect.height; acc[dir] = { x: x, y: y, width: width, height: height, left: x, top: y, right: x + width, bottom: y + height, }; return acc; }, {}); }; /** * Rectangle overlap check * * @param a first rectangle * @param b second rectangle * @return `true` when the two rectangles overlap otherwise `false` */ export var isRectOverlap = function (a, b) { var xOverlap = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left)); var yOverlap = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top)); return xOverlap * yOverlap > 0; }; /** * Get the relative position of the element from the active element. * * @remarks * - `edgeDistance` is a distance between the closest edge of the `element` and one of active element's edge * the actual edge depends on the `farEdge` parameter * - `distance` is `infinite` when `element` is not in the specified `direction` * - `edgeDistance` is `infinite` when the `element` does not overlap the active element's expanded bounding rect * on the specified `direction` * - in the return value both `edgeDistance` and `distance` are the square of the real distance * * @param element * @param direction * @param activeElementMidPoint * @param activeElementExpandedRects * @param farEdge */ export var getElementRelativeDistances = function (element, direction, activeElementMidPoint, activeElementExpandedRects, farEdge) { var extendedBoundRect = activeElementExpandedRects[direction]; var elementRect = getElementRectWithMidPoint(element); var edgeDistance = Math.round(getEdgeDistance(activeElementMidPoint, elementRect, direction, farEdge) * 1.2); if (edgeDistance < 0) { return { element: element, distance: Infinity, edgeDistance: edgeDistance }; } edgeDistance = isRectOverlap(elementRect, extendedBoundRect) ? edgeDistance * edgeDistance : Infinity; var x = elementRect.xMid - activeElementMidPoint.xMid; var y = elementRect.yMid - activeElementMidPoint.yMid; var distance = x * x + y * y; return { element: element, distance: distance, edgeDistance: edgeDistance }; }; /** * Calculate the distance of the focusable elements form the active element * and return the sorted list of elements based on the distance. * The first element is the closest. * * @param activeEl Active/focused Dom element * @param focusableElements All focusable elements * @param direction Direction of the navigation */ export var orderElementsByDistance = function (activeEl, focusableElements, direction) { var active = getElementRectWithMidPoint(activeEl); var farEdge = getNestedFocusableDirection(activeEl); var expandedBoundingRects = getExpandedRect(active, window.innerWidth / 2); return focusableElements .map(function (el) { return getElementRelativeDistances(el, direction, active, expandedBoundingRects, farEdge); }) .filter(function (_a) { var element = _a.element, edgeDistance = _a.edgeDistance; return element !== activeEl && edgeDistance >= 0; }) .sort(function (a, b) { return a.edgeDistance - b.edgeDistance || a.distance - b.distance; }); }; /** * This hook helps to integrate spatial navigation with the radio group. * * Stop event propagation when user navigate between radio buttons with arrow keys except * when the first or last radio button is focused and the user presses "prev" (left or up) or "next" (right or down) arrow keys. * * @param onKeyDown Optional onKeyDown event handler */ export var useSpatialRadioGroupNavigation = function (onKeyDown) { var spatialNav = useSpatialNavigationContext(); return useCallback(function (evt) { if (spatialNav && spatialNav.directionKeys.includes(evt.key) && evt.target instanceof HTMLElement && evt.target.tagName === 'INPUT') { var inputs = Array.from(evt.currentTarget.querySelectorAll("[name=\"".concat(evt.target.getAttribute('name'), "\"]"))); var isPrevKey = evt.key === spatialNav.left || evt.key === spatialNav.up; var isNextKey = evt.key === spatialNav.right || evt.key === spatialNav.down; if ((inputs.at(0) === evt.target && isPrevKey) || (inputs.at(-1) === evt.target && isNextKey)) { // Prevent loop back to the other end on the radio group evt.preventDefault(); } else { // Prevent spatial navigation from moving the focus evt.nativeEvent.stopImmediatePropagation(); onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(evt); } } else { // Without spatial navigation, just call the onKeyDown handler onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(evt); } }, [spatialNav, onKeyDown]); }; /** * This function helps to visually debug spatial navigation * * Press Shift + Arrow keys to inspect navigation in the given direction. * Pressing any other key hide the debug layer * * Legends: * - red rectangle - focused element * - green rectangle - expanded bounding box of the focused element for edge distance calculation * - blue rectangle - considered as next focusable elements * - white/gray dot - mid-point of the element for distance calculation, it fades based on the distance * - # - order number #1 will be the next focused element * - ed - edge distance * - d - distance */ export var visualDebugger = function (root) { if (root === void 0) { root = document.body; } if (document.getElementById('spatialNavigationVisualDebugger')) return; var canvas = document.createElement('canvas'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; canvas.style.position = 'fixed'; canvas.style.top = '0'; canvas.style.left = '0'; canvas.style.pointerEvents = 'none'; canvas.id = 'spatialNavigationVisualDebugger'; root.appendChild(canvas); document.addEventListener('keydown', function (evt) { if (!evt.shiftKey || !evt.key.startsWith('Arrow')) { return draw(); } switch (evt.key) { case 'ArrowLeft': return draw('left'); case 'ArrowUp': return draw('up'); case 'ArrowDown': return draw('down'); case 'ArrowRight': return draw('right'); } }); var draw = function (direction) { if (!direction) { return (canvas.hidden = true); } canvas.hidden = false; var currentActiveElement = document.activeElement; var active = getElementRectWithMidPoint(currentActiveElement); var expandedBoundingRects = getExpandedRect(active, window.innerWidth / 2); var elements = getKeyboardFocusableElements(root); var results = orderElementsByDistance(currentActiveElement, elements, direction); var ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.lineWidth = 5; ctx.strokeStyle = 'rgba(8,99,134,0.75)'; ctx.font = '15px sans-serif'; results.forEach(function (_a, idx) { var element = _a.element, edgeDistance = _a.edgeDistance, distance = _a.distance; var rect = getElementRectWithMidPoint(element); ctx.fillStyle = "rgba(255, 255, 255, ".concat(1 - idx / elements.length, ")"); ctx.fillText("#".concat(idx + 1, ", ed: ").concat(Math.round(Math.sqrt(edgeDistance)), " d: ").concat(Math.round(Math.sqrt(distance)), " "), rect.x, rect.y - 10); ctx.strokeRect(rect.x, rect.y, rect.width, rect.height); ctx.beginPath(); ctx.arc(rect.xMid, rect.yMid, 10, 0, 2 * Math.PI); ctx.fill(); }); var exRect = expandedBoundingRects[direction]; ctx.lineWidth = 5; ctx.strokeStyle = 'rgba(19,87,5,0.75)'; ctx.strokeRect(exRect.x, exRect.y, exRect.width, exRect.height); // active ctx.lineWidth = 10; ctx.strokeStyle = 'rgba(87,5,5,0.75)'; ctx.strokeRect(active.x, active.y, active.width, active.height); }; }; //# sourceMappingURL=SpatialNavigationProvider.utils.js.map