UNPKG

@sap-ux/ui-components

Version:

SAP UI Components Library

220 lines 8.49 kB
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