@momentum-ui/react-collaboration
Version:
Cisco Momentum UI Framework for React Collaboration Applications
296 lines • 12.3 kB
JavaScript
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