@sap-ux/ui-components
Version:
SAP UI Components Library
220 lines • 8.49 kB
JavaScript
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { getUIFirstFocusable, isHTMLElement, setUIFocusVisibility } from '../../utilities/index.js';
import './UIQuickNavigation.scss';
export const QUICK_NAVIGATION_ATTRIBUTE = 'data-quick-navigation-key';
export const QUICK_NAVIGATION_ATTRIBUTE_OFFSET_Y = 'data-quick-navigation-offset-y';
export const QUICK_NAVIGATION_ATTRIBUTE_OFFSET_X = 'data-quick-navigation-offset-x';
const QUICK_NAVIGATION_CLASSES = {
inline: 'quick-navigation--inline',
external: 'quick-navigation--external'
};
const EXTERNAL_HELPER_OFFSET = {
x: 15,
y: 15
};
export const setQuickNavigationKey = (key, offset) => {
const attributes = {
[QUICK_NAVIGATION_ATTRIBUTE]: key.toUpperCase()
};
if (offset) {
attributes[QUICK_NAVIGATION_ATTRIBUTE_OFFSET_Y] = offset.y.toString();
attributes[QUICK_NAVIGATION_ATTRIBUTE_OFFSET_X] = offset.x.toString();
}
return attributes;
};
/**
* Method returns CSS classnames based on current quick navigation state.
*
* @param className External classname(s).
* @param enabled Is quick navigation enabled/activated.
* @param inline Is quick navigation should be rendered with inline approach.
* @returns CSS classnames based on current quick navigation state.
*/
function getClassName(className, enabled, inline) {
const result = [className];
if (enabled && inline) {
result.push(QUICK_NAVIGATION_CLASSES.inline);
}
return result.join(' ');
}
/**
* Method returns scroll offset position for external rendering approach.
* External rendering happens in 'document.body' and body scroll position should be considered.
*
* @returns Scroll position of body and html.
*/
function getScrollOffset() {
let container = document.body;
const scrollOffset = {
y: 0,
x: 0
};
do {
scrollOffset.y += container.scrollTop || 0;
scrollOffset.x += container.scrollLeft || 0;
container = container.parentNode && isHTMLElement(container.parentNode) ? container.parentNode : undefined;
} while (container);
return scrollOffset;
}
/**
* Method returns offset for passed DOM element.
*
* @param target Target element to detect offset.
* @returns Scroll position of body and html.
*/
function getOffsetFromElement(target) {
const yAttr = target.getAttribute(QUICK_NAVIGATION_ATTRIBUTE_OFFSET_Y);
const xAttr = target.getAttribute(QUICK_NAVIGATION_ATTRIBUTE_OFFSET_X);
if (!yAttr || !xAttr) {
return undefined;
}
const y = Number.parseFloat(yAttr);
const x = Number.parseFloat(xAttr);
return !Number.isNaN(y) && !Number.isNaN(x) ? { y, x } : undefined;
}
/**
* Method toggles visibility of external quick navigation helpers UI.
*
* @param enabled Is quick navigation enabled/activated.
* @param offset Offset values for helper position.
*/
function toggleExternalVisibility(enabled, offset = EXTERNAL_HELPER_OFFSET) {
// Cleanup container
const existingContainer = document.querySelector(`.${QUICK_NAVIGATION_CLASSES.external}`);
if (existingContainer) {
existingContainer.remove();
}
// Show helpers if quick navigation is active
if (enabled) {
// Create external container
const externalContainer = document.createElement('div');
externalContainer.classList.add(QUICK_NAVIGATION_CLASSES.external);
const navigationTargets = document.querySelectorAll(`[${QUICK_NAVIGATION_ATTRIBUTE}]`);
// Create external DOM element to each navigation target
const scrollOffset = getScrollOffset();
navigationTargets.forEach((target) => {
const rect = target.getBoundingClientRect();
const helper = document.createElement('div');
helper.textContent = target.getAttribute(QUICK_NAVIGATION_ATTRIBUTE);
const elementOffset = getOffsetFromElement(target) ?? offset;
const position = {
top: rect.top + scrollOffset.y - elementOffset.y,
left: rect.left + scrollOffset.x - elementOffset.x
};
position.top = Math.max(position.top, 0);
position.left = Math.max(position.left, 0);
helper.style.top = `${position.top}px`;
helper.style.left = `${position.left}px`;
externalContainer.appendChild(helper);
});
document.body.appendChild(externalContainer);
}
}
/**
* Method checks if quick navigation should be enabled for passed keyboard event.
* Quick navigation is enabled when user hold 'ctrl + alt' or 'meta + alt'.
*
* @param event Keyboard event.
* @returns True if quick navigation should be enabled for passed keyboard event.
*/
export const isQuickNavigationEnabled = (event) => {
return (event.ctrlKey || event.metaKey) && event.altKey;
};
/**
* Method resolves passed keyboard code by removing 'Digit' and 'Key' keywords.
*
* @param code Code from keyboard event.
* @returns Resolved char or digit.
*/
function resolveKeyCode(code) {
return code.replace('Digit', '').replace('Key', '').toUpperCase();
}
/**
* Method to stop event bubbling.
*
* @param event Keyboard event.
*/
function stopEventBubling(event) {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
}
export const UIQuickNavigation = (props) => {
const { className, children, inline, offset } = props;
const [enabled, setEnabled] = useState(false);
const navigated = useRef(false);
// Handle visibility for 'external' rendering approach(helpers rendered outside of target navigation containers)
useEffect(() => {
if (inline) {
return;
}
toggleExternalVisibility(enabled, offset);
}, [enabled]);
/**
* Method handles key down event.
*
* @param event Key down event.
*/
const onKeyDown = useCallback((event) => {
let activated = enabled;
if (!enabled && isQuickNavigationEnabled(event)) {
setEnabled(true);
activated = true;
}
if (!activated) {
return;
}
// Quick navigation is active - handle final key press
const resolvedKey = resolveKeyCode(event.code);
const navigationElement = document.querySelector(`[${QUICK_NAVIGATION_ATTRIBUTE}="${resolvedKey}"]`);
const startElement = navigationElement?.firstElementChild;
if (navigationElement && startElement && isHTMLElement(startElement)) {
const firstFocusableElement = getUIFirstFocusable(navigationElement, startElement, true);
if (firstFocusableElement) {
// Set focus visiblity in UI(fluent-ui feature)
setUIFocusVisibility(true, firstFocusableElement);
// Apply focus to element
firstFocusableElement?.focus();
// Prevent event bubbling
stopEventBubling(event);
navigated.current = true;
}
// Disable/deactivate UI with quick navigation
setEnabled(false);
}
}, [enabled]);
/**
* Method handles key up and window blur events to detect if quick navigation should be deactivated.
*
* @param event Key up or blur event.
*/
const onRelease = useCallback((event) => {
if (enabled && (!('keyCode' in event) || !isQuickNavigationEnabled(event))) {
setEnabled(false);
}
if (navigated.current) {
navigated.current = false;
stopEventBubling(event);
}
}, [enabled]);
/**
* Attach events to listen keydown/keyup within window and blur from window.
*/
useEffect(() => {
document.body.addEventListener('keydown', onKeyDown);
document.body.addEventListener('keyup', onRelease);
window.addEventListener('blur', onRelease);
return () => {
toggleExternalVisibility(false);
document.body.removeEventListener('keydown', onKeyDown);
document.body.removeEventListener('keyup', onRelease);
window.removeEventListener('blur', onRelease);
};
}, [onKeyDown, onRelease]);
return React.createElement("div", { className: getClassName(className, enabled, inline) }, children);
};
UIQuickNavigation.defaultProps = {
inline: false,
offset: EXTERNAL_HELPER_OFFSET
};
//# sourceMappingURL=UIQuickNavigation.js.map