@sap-ux/ui-components
Version:
SAP UI Components Library
251 lines • 10.2 kB
JavaScript
;
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