UNPKG

@sap-ux/ui-components

Version:
251 lines 10.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UIQuickNavigation = exports.isQuickNavigationEnabled = exports.setQuickNavigationKey = exports.QUICK_NAVIGATION_ATTRIBUTE_OFFSET_X = exports.QUICK_NAVIGATION_ATTRIBUTE_OFFSET_Y = exports.QUICK_NAVIGATION_ATTRIBUTE = void 0; const react_1 = __importStar(require("react")); const utilities_1 = require("../../utilities"); require("./UIQuickNavigation.scss"); exports.QUICK_NAVIGATION_ATTRIBUTE = 'data-quick-navigation-key'; exports.QUICK_NAVIGATION_ATTRIBUTE_OFFSET_Y = 'data-quick-navigation-offset-y'; exports.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 }; const setQuickNavigationKey = (key, offset) => { const attributes = { [exports.QUICK_NAVIGATION_ATTRIBUTE]: key.toUpperCase() }; if (offset) { attributes[exports.QUICK_NAVIGATION_ATTRIBUTE_OFFSET_Y] = offset.y.toString(); attributes[exports.QUICK_NAVIGATION_ATTRIBUTE_OFFSET_X] = offset.x.toString(); } return attributes; }; exports.setQuickNavigationKey = setQuickNavigationKey; /** * 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 && (0, utilities_1.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(exports.QUICK_NAVIGATION_ATTRIBUTE_OFFSET_Y); const xAttr = target.getAttribute(exports.QUICK_NAVIGATION_ATTRIBUTE_OFFSET_X); if (!yAttr || !xAttr) { return undefined; } const y = parseFloat(yAttr); const x = parseFloat(xAttr); return !isNaN(y) && !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) { const holder = document.body; // Cleanup container const existingContainer = document.querySelector(`.${QUICK_NAVIGATION_CLASSES.external}`); if (existingContainer) { holder.removeChild(existingContainer); } // 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(`[${exports.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(exports.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); }); holder.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. */ const isQuickNavigationEnabled = (event) => { return (event.ctrlKey || event.metaKey) && event.altKey; }; exports.isQuickNavigationEnabled = isQuickNavigationEnabled; /** * 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(); } // eslint-disable-next-line @typescript-eslint/naming-convention const UIQuickNavigation = (props) => { const { className, children, inline, offset } = props; const [enabled, setEnabled] = (0, react_1.useState)(false); const navigated = (0, react_1.useRef)(false); // Handle visibility for 'external' rendering approach(helpers rendered outside of target navigation containers) (0, react_1.useEffect)(() => { if (inline) { return; } toggleExternalVisibility(enabled, offset); }, [enabled]); /** * Method handles key down event. * * @param event Key down event. */ const onKeyDown = (0, react_1.useCallback)((event) => { let activated = enabled; if (!enabled && (0, exports.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(`[${exports.QUICK_NAVIGATION_ATTRIBUTE}="${resolvedKey}"]`); const startElement = navigationElement?.firstElementChild; if (navigationElement && startElement && (0, utilities_1.isHTMLElement)(startElement)) { const firstFocusableElement = (0, utilities_1.getUIFirstFocusable)(navigationElement, startElement, true); if (firstFocusableElement) { // Set focus visiblity in UI(fluent-ui feature) (0, utilities_1.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 = (0, react_1.useCallback)((event) => { if (enabled && (!('keyCode' in event) || !(0, exports.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. */ (0, react_1.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_1.default.createElement("div", { className: getClassName(className, enabled, inline) }, children); }; exports.UIQuickNavigation = UIQuickNavigation; exports.UIQuickNavigation.defaultProps = { inline: false, offset: EXTERNAL_HELPER_OFFSET }; //# sourceMappingURL=UIQuickNavigation.js.map