"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FocusZone = void 0; var tslib_1 = require("tslib"); var React = require("react"); var FocusZone_types_1 = require("./FocusZone.types"); var utilities_1 = require("@fluentui/utilities"); var merge_styles_1 = require("@fluentui/merge-styles"); var style_utilities_1 = require("@fluentui/style-utilities"); var IS_FOCUSABLE_ATTRIBUTE = 'data-is-focusable'; var IS_ENTER_DISABLED_ATTRIBUTE = 'data-disable-click-on-enter'; var FOCUSZONE_ID_ATTRIBUTE = 'data-focuszone-id'; var TABINDEX = 'tabindex'; var NO_VERTICAL_WRAP = 'data-no-vertical-wrap'; var NO_HORIZONTAL_WRAP = 'data-no-horizontal-wrap'; var LARGE_DISTANCE_FROM_CENTER = 999999999; var LARGE_NEGATIVE_DISTANCE_FROM_CENTER = -999999999; var focusZoneStyles; var focusZoneClass = 'ms-FocusZone'; /** * Raises a click on a target element based on a keyboard event. */ function raiseClickFromKeyboardEvent(target, ev) { var event; if (typeof MouseEvent === 'function') { event = new MouseEvent('click', { ctrlKey: ev === null || ev === void 0 ? void 0 : ev.ctrlKey, metaKey: ev === null || ev === void 0 ? void 0 : ev.metaKey, shiftKey: ev === null || ev === void 0 ? void 0 : ev.shiftKey, altKey: ev === null || ev === void 0 ? void 0 : ev.altKey, bubbles: ev === null || ev === void 0 ? void 0 : ev.bubbles, cancelable: ev === null || ev === void 0 ? void 0 : ev.cancelable, }); } else { // eslint-disable-next-line no-restricted-globals event = document.createEvent('MouseEvents'); // eslint-disable-next-line @typescript-eslint/no-deprecated event.initMouseEvent('click', ev ? ev.bubbles : false, ev ? ev.cancelable : false, // eslint-disable-next-line no-restricted-globals window, // not using getWindow() since this can only be run client side 0, // detail 0, // screen x 0, // screen y 0, // client x 0, // client y ev ? ev.ctrlKey : false, ev ? ev.altKey : false, ev ? ev.shiftKey : false, ev ? ev.metaKey : false, 0, // button null); } target.dispatchEvent(event); } // Helper function that will return a class for when the root is focused function getRootClass() { if (!focusZoneStyles) { focusZoneStyles = (0, merge_styles_1.mergeStyles)({ selectors: { ':focus': { outline: 'none', }, }, }, focusZoneClass); } return focusZoneStyles; } var _allInstances = {}; var _outerZones = new Set(); var ALLOWED_INPUT_TYPES = ['text', 'number', 'password', 'email', 'tel', 'url', 'search', 'textarea']; var ALLOW_VIRTUAL_ELEMENTS = false; var FocusZone = /** @class */ (function (_super) { tslib_1.__extends(FocusZone, _super); function FocusZone(props) { var _a, _b, _c, _d; var _this = _super.call(this, props) || this; _this._root = React.createRef(); _this._mergedRef = (0, utilities_1.createMergedRef)(); _this._onFocus = function (ev) { if (_this._portalContainsElement(ev.target)) { // If the event target is inside a portal do not process the event. return; } var _a = _this.props, onActiveElementChanged = _a.onActiveElementChanged, // eslint-disable-next-line @typescript-eslint/no-deprecated doNotAllowFocusEventToPropagate = _a.doNotAllowFocusEventToPropagate, stopFocusPropagation = _a.stopFocusPropagation, // eslint-disable-next-line @typescript-eslint/no-deprecated onFocusNotification = _a.onFocusNotification, onFocus = _a.onFocus, shouldFocusInnerElementWhenReceivedFocus = _a.shouldFocusInnerElementWhenReceivedFocus, defaultTabbableElement = _a.defaultTabbableElement; var isImmediateDescendant = _this._isImmediateDescendantOfZone(ev.target); var newActiveElement; if (isImmediateDescendant) { newActiveElement = ev.target; } else { var parentElement = ev.target; while (parentElement && parentElement !== _this._root.current) { if ((0, utilities_1.isElementTabbable)(parentElement, undefined, _this._inShadowRoot) && _this._isImmediateDescendantOfZone(parentElement)) { newActiveElement = parentElement; break; } parentElement = (0, utilities_1.getParent)(parentElement, ALLOW_VIRTUAL_ELEMENTS); } } // If an inner focusable element should be focused when FocusZone container receives focus if (shouldFocusInnerElementWhenReceivedFocus && ev.target === _this._root.current) { var maybeElementToFocus = defaultTabbableElement && typeof defaultTabbableElement === 'function' && _this._root.current && defaultTabbableElement(_this._root.current); // try to focus defaultTabbable element if (maybeElementToFocus && (0, utilities_1.isElementTabbable)(maybeElementToFocus, undefined, _this._inShadowRoot)) { newActiveElement = maybeElementToFocus; maybeElementToFocus.focus(); } else { // force focus on first focusable element _this.focus(true); if (_this._activeElement) { // set to null as new active element was handled in method above newActiveElement = null; } } } var initialElementFocused = !_this._activeElement; // If the new active element is a child of this zone and received focus, // update alignment an immediate descendant if (newActiveElement && newActiveElement !== _this._activeElement) { if (isImmediateDescendant || initialElementFocused) { _this._setFocusAlignment(newActiveElement, true, true); } _this._activeElement = newActiveElement; if (initialElementFocused) { _this._updateTabIndexes(); } } if (onActiveElementChanged) { onActiveElementChanged(_this._activeElement, ev); } if (stopFocusPropagation || doNotAllowFocusEventToPropagate) { ev.stopPropagation(); } if (onFocus) { onFocus(ev); } else if (onFocusNotification) { onFocusNotification(); } }; _this._onBlur = function () { _this._setParkedFocus(false); }; _this._onMouseDown = function (ev) { if (_this._portalContainsElement(ev.target)) { // If the event target is inside a portal do not process the event. return; } var disabled = _this.props.disabled; if (disabled) { return; } var target = ev.target; var path = []; while (target && target !== _this._root.current) { path.push(target); target = (0, utilities_1.getParent)(target, ALLOW_VIRTUAL_ELEMENTS); } while (path.length) { target = path.pop(); if (target && (0, utilities_1.isElementTabbable)(target, undefined, _this._inShadowRoot)) { _this._setActiveElement(target, true); } if ((0, utilities_1.isElementFocusZone)(target)) { // Stop here since the focus zone will take care of its own children. break; } } }; /** * Handle the keystrokes. */ _this._onKeyDown = function (ev, theme) { if (_this._portalContainsElement(ev.target)) { // If the event target is inside a portal do not process the event. return; } // eslint-disable-next-line @typescript-eslint/no-deprecated var _a = _this.props, direction = _a.direction, disabled = _a.disabled, isInnerZoneKeystroke = _a.isInnerZoneKeystroke, pagingSupportDisabled = _a.pagingSupportDisabled, shouldEnterInnerZone = _a.shouldEnterInnerZone; if (disabled) { return; } if (_this.props.onKeyDown) { _this.props.onKeyDown(ev); } // If the default has been prevented, do not process keyboard events. if (ev.isDefaultPrevented()) { return; } if (_this._getDocument().activeElement === _this._root.current && _this._isInnerZone) { // If this element has focus, it is being controlled by a parent. // Ignore the keystroke. return; } if (((shouldEnterInnerZone && shouldEnterInnerZone(ev)) || (isInnerZoneKeystroke && isInnerZoneKeystroke(ev))) && _this._isImmediateDescendantOfZone(ev.target)) { // Try to focus var innerZone = _this._getFirstInnerZone(); if (innerZone) { if (!innerZone.focus(true)) { return; } } else if ((0, utilities_1.isElementFocusSubZone)(ev.target)) { if (!_this.focusElement((0, utilities_1.getNextElement)(ev.target, ev.target.firstChild, true))) { return; } } else { return; } } else if (ev.altKey) { return; } else { // eslint-disable-next-line @fluentui/deprecated-keyboard-event-props, @typescript-eslint/no-deprecated switch (ev.which) { case utilities_1.KeyCodes.space: if (_this._shouldRaiseClicksOnSpace && _this._tryInvokeClickForFocusable(ev.target, ev)) { break; } return; case utilities_1.KeyCodes.left: if (direction !== FocusZone_types_1.FocusZoneDirection.vertical) { _this._preventDefaultWhenHandled(ev); if (_this._moveFocusLeft(theme)) { break; } } return; case utilities_1.KeyCodes.right: if (direction !== FocusZone_types_1.FocusZoneDirection.vertical) { _this._preventDefaultWhenHandled(ev); if (_this._moveFocusRight(theme)) { break; } } return; case utilities_1.KeyCodes.up: if (direction !== FocusZone_types_1.FocusZoneDirection.horizontal) { _this._preventDefaultWhenHandled(ev); if (_this._moveFocusUp()) { break; } } return; case utilities_1.KeyCodes.down: if (direction !== FocusZone_types_1.FocusZoneDirection.horizontal) { _this._preventDefaultWhenHandled(ev); if (_this._moveFocusDown()) { break; } } return; case utilities_1.KeyCodes.pageDown: if (!pagingSupportDisabled && _this._moveFocusPaging(true)) { break; } return; case utilities_1.KeyCodes.pageUp: if (!pagingSupportDisabled && _this._moveFocusPaging(false)) { break; } return; case utilities_1.KeyCodes.tab: if ( // eslint-disable-next-line @typescript-eslint/no-deprecated _this.props.allowTabKey || _this.props.handleTabKey === FocusZone_types_1.FocusZoneTabbableElements.all || (_this.props.handleTabKey === FocusZone_types_1.FocusZoneTabbableElements.inputOnly && _this._isElementInput(ev.target))) { var focusChanged = false; _this._processingTabKey = true; if (direction === FocusZone_types_1.FocusZoneDirection.vertical || !_this._shouldWrapFocus(_this._activeElement, NO_HORIZONTAL_WRAP)) { focusChanged = ev.shiftKey ? _this._moveFocusUp() : _this._moveFocusDown(); } else { var tabWithDirection = (0, utilities_1.getRTL)(theme) ? !ev.shiftKey : ev.shiftKey; focusChanged = tabWithDirection ? _this._moveFocusLeft(theme) : _this._moveFocusRight(theme); } _this._processingTabKey = false; if (focusChanged) { break; } else if (_this.props.shouldResetActiveElementWhenTabFromZone) { _this._activeElement = null; } } return; case utilities_1.KeyCodes.home: if (_this._isContentEditableElement(ev.target) || (_this._isElementInput(ev.target) && !_this._shouldInputLoseFocus(ev.target, false))) { return false; } var firstChild = _this._root.current && _this._root.current.firstChild; if (_this._root.current && firstChild && _this.focusElement((0, utilities_1.getNextElement)(_this._root.current, firstChild, true))) { break; } return; case utilities_1.KeyCodes.end: if (_this._isContentEditableElement(ev.target) || (_this._isElementInput(ev.target) && !_this._shouldInputLoseFocus(ev.target, true))) { return false; } var lastChild = _this._root.current && _this._root.current.lastChild; if (_this._root.current && _this.focusElement((0, utilities_1.getPreviousElement)(_this._root.current, lastChild, true, true, true))) { break; } return; case utilities_1.KeyCodes.enter: if (_this._shouldRaiseClicksOnEnter && _this._tryInvokeClickForFocusable(ev.target, ev)) { break; } return; default: return; } } ev.preventDefault(); ev.stopPropagation(); }; _this._getHorizontalDistanceFromCenter = function (isForward, // eslint-disable-next-line @typescript-eslint/no-deprecated activeRect, // eslint-disable-next-line @typescript-eslint/no-deprecated targetRect) { // eslint-disable-next-line @typescript-eslint/no-deprecated var leftAlignment = _this._focusAlignment.left || _this._focusAlignment.x || 0; // ClientRect values can be floats that differ by very small fractions of a decimal. // If the difference between top and bottom are within a pixel then we should treat // them as equivalent by using Math.floor. For instance 5.2222 and 5.222221 should be equivalent, // but without Math.Floor they will be handled incorrectly. var targetRectTop = Math.floor(targetRect.top); var activeRectBottom = Math.floor(activeRect.bottom); var targetRectBottom = Math.floor(targetRect.bottom); var activeRectTop = Math.floor(activeRect.top); var isValidCandidateOnpagingDown = isForward && targetRectTop > activeRectBottom; var isValidCandidateOnpagingUp = !isForward && targetRectBottom < activeRectTop; if (isValidCandidateOnpagingDown || isValidCandidateOnpagingUp) { if (leftAlignment >= targetRect.left && leftAlignment <= targetRect.left + targetRect.width) { return 0; } return Math.abs(targetRect.left + targetRect.width / 2 - leftAlignment); } if (!_this._shouldWrapFocus(_this._activeElement, NO_VERTICAL_WRAP)) { return LARGE_NEGATIVE_DISTANCE_FROM_CENTER; } return LARGE_DISTANCE_FROM_CENTER; }; // Manage componentRef resolution. (0, utilities_1.initializeComponentRef)(_this); if (process.env.NODE_ENV !== 'production') { (0, utilities_1.warnDeprecations)('FocusZone', props, { rootProps: undefined, allowTabKey: 'handleTabKey', elementType: 'as', ariaDescribedBy: 'aria-describedby', ariaLabelledBy: 'aria-labelledby', }); } _this._id = (0, utilities_1.getId)('FocusZone'); _this._focusAlignment = { left: 0, top: 0, }; _this._processingTabKey = false; var shouldRaiseClicksFallback = (_b = (_a = props.shouldRaiseClicks) !== null && _a !== void 0 ? _a : FocusZone.defaultProps.shouldRaiseClicks) !== null && _b !== void 0 ? _b : true; _this._shouldRaiseClicksOnEnter = (_c = props.shouldRaiseClicksOnEnter) !== null && _c !== void 0 ? _c : shouldRaiseClicksFallback; _this._shouldRaiseClicksOnSpace = (_d = props.shouldRaiseClicksOnSpace) !== null && _d !== void 0 ? _d : shouldRaiseClicksFallback; return _this; } /** Used for testing purposes only. */ FocusZone.getOuterZones = function () { return _outerZones.size; }; /** * Handle global tab presses so that we can patch tabindexes on the fly. * HEADS UP: This must not be an arrow function in order to be referentially equal among instances * for ref counting to work correctly! */ FocusZone._onKeyDownCapture = function (ev) { // eslint-disable-next-line @typescript-eslint/no-deprecated, @fluentui/deprecated-keyboard-event-props if (ev.which === utilities_1.KeyCodes.tab) { _outerZones.forEach(function (zone) { return zone._updateTabIndexes(); }); } }; FocusZone.prototype.componentDidMount = function () { var _a; var root = this._root.current; this._inShadowRoot = !!((_a = this.context) === null || _a === void 0 ? void 0 : _a.shadowRoot); _allInstances[this._id] = this; if (root) { var parentElement = (0, utilities_1.getParent)(root, ALLOW_VIRTUAL_ELEMENTS); while (parentElement && parentElement !== this._getDocument().body && parentElement.nodeType === 1) { if ((0, utilities_1.isElementFocusZone)(parentElement)) { this._isInnerZone = true; break; } parentElement = (0, utilities_1.getParent)(parentElement, ALLOW_VIRTUAL_ELEMENTS); } if (!this._isInnerZone) { _outerZones.add(this); this._root.current && this._root.current.addEventListener('keydown', FocusZone._onKeyDownCapture, true); } this._root.current && this._root.current.addEventListener('blur', this._onBlur, true); // Assign initial tab indexes so that we can set initial focus as appropriate. this._updateTabIndexes(); if (this.props.defaultTabbableElement && typeof this.props.defaultTabbableElement === 'string') { this._activeElement = this._getDocument().querySelector(this.props.defaultTabbableElement); // eslint-disable-next-line @typescript-eslint/no-deprecated } else if (this.props.defaultActiveElement) { // eslint-disable-next-line @typescript-eslint/no-deprecated this._activeElement = this._getDocument().querySelector(this.props.defaultActiveElement); } if (this.props.shouldFocusOnMount) { this.focus(); } } }; FocusZone.prototype.componentDidUpdate = function () { var _a; var root = this._root.current; var doc = this._getDocument(); this._inShadowRoot = !!((_a = this.context) === null || _a === void 0 ? void 0 : _a.shadowRoot); // If either _activeElement or _defaultFocusElement are no longer contained by _root, // reset those variables (and update tab indexes) to avoid memory leaks if ((this._activeElement && !(0, utilities_1.elementContains)(this._root.current, this._activeElement, ALLOW_VIRTUAL_ELEMENTS)) || (this._defaultFocusElement && !(0, utilities_1.elementContains)(this._root.current, this._defaultFocusElement, ALLOW_VIRTUAL_ELEMENTS))) { this._activeElement = null; this._defaultFocusElement = null; this._updateTabIndexes(); } if (!this.props.preventFocusRestoration && doc && this._lastIndexPath && (doc.activeElement === doc.body || doc.activeElement === null || doc.activeElement === root)) { // The element has been removed after the render, attempt to restore focus. var elementToFocus = (0, utilities_1.getFocusableByIndexPath)(root, this._lastIndexPath); if (elementToFocus) { this._setActiveElement(elementToFocus, true); elementToFocus.focus(); this._setParkedFocus(false); } else { // We had a focus path to restore, but now that path is unresolvable. Park focus // on the container until we can try again. this._setParkedFocus(true); } } }; FocusZone.prototype.componentWillUnmount = function () { delete _allInstances[this._id]; if (!this._isInnerZone) { _outerZones.delete(this); this._root.current && this._root.current.removeEventListener('keydown', FocusZone._onKeyDownCapture, true); } if (this._root.current) { this._root.current.removeEventListener('blur', this._onBlur, true); } this._activeElement = null; this._defaultFocusElement = null; }; FocusZone.prototype.render = function () { var _this = this; // eslint-disable-next-line @typescript-eslint/no-deprecated var _a = this.props, tag = _a.as, elementType = _a.elementType, rootProps = _a.rootProps, ariaDescribedBy = _a.ariaDescribedBy, ariaLabelledBy = _a.ariaLabelledBy, className = _a.className; var divProps = (0, utilities_1.getNativeProps)(this.props, utilities_1.htmlElementProperties); var Tag = tag || elementType || 'div'; // Note, right before rendering/reconciling proceeds, we need to record if focus // was in the zone before the update. This helper will track this and, if focus // was actually in the zone, what the index path to the element is at this time. // Then, later in componentDidUpdate, we can evaluate if we need to restore it in // the case the element was removed. this._evaluateFocusBeforeRender(); // Only support RTL defined in global theme, not contextual theme/RTL. var theme = (0, style_utilities_1.getTheme)(); return (React.createElement(Tag, tslib_1.__assign({ "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy }, divProps, rootProps, { // Once the getClassName correctly memoizes inputs this should // be replaced so that className is passed to getRootClass and is included there so // the class names will always be in the same order. className: (0, utilities_1.css)(getRootClass(), className), // eslint-disable-next-line @typescript-eslint/no-deprecated ref: this._mergedRef(this.props.elementRef, this._root), "data-focuszone-id": this._id, // eslint-disable-next-line react/jsx-no-bind onKeyDown: function (ev) { return _this._onKeyDown(ev, theme); }, onFocus: this._onFocus, onMouseDownCapture: this._onMouseDown }), this.props.children)); }; /** * Sets focus to the first tabbable item in the zone. * @param forceIntoFirstElement - If true, focus will be forced into the first element, even * if focus is already in the focus zone. * @param bypassHiddenElements - If true, focus will be not be set on hidden elements. * @returns True if focus could be set to an active element, false if no operation was taken. */ FocusZone.prototype.focus = function (forceIntoFirstElement, bypassHiddenElements) { if (forceIntoFirstElement === void 0) { forceIntoFirstElement = false; } if (bypassHiddenElements === void 0) { bypassHiddenElements = false; } if (this._root.current) { if (!forceIntoFirstElement && this._root.current.getAttribute(IS_FOCUSABLE_ATTRIBUTE) === 'true' && this._isInnerZone) { var ownerZoneElement = this._getOwnerZone(this._root.current); if (ownerZoneElement !== this._root.current) { var ownerZone = _allInstances[ownerZoneElement.getAttribute(FOCUSZONE_ID_ATTRIBUTE)]; return !!ownerZone && ownerZone.focusElement(this._root.current); } return false; } else if (!forceIntoFirstElement && this._activeElement && (0, utilities_1.elementContains)(this._root.current, this._activeElement) && (0, utilities_1.isElementTabbable)(this._activeElement, undefined, this._inShadowRoot) && (!bypassHiddenElements || (0, utilities_1.isElementVisibleAndNotHidden)(this._activeElement))) { this._activeElement.focus(); return true; } else { var firstChild = this._root.current.firstChild; return this.focusElement((0, utilities_1.getNextElement)(this._root.current, firstChild, true, undefined, undefined, undefined, undefined, undefined, bypassHiddenElements)); } } return false; }; /** * Sets focus to the last tabbable item in the zone. * @returns True if focus could be set to an active element, false if no operation was taken. */ FocusZone.prototype.focusLast = function () { if (this._root.current) { var lastChild = this._root.current && this._root.current.lastChild; return this.focusElement((0, utilities_1.getPreviousElement)(this._root.current, lastChild, true, true, true)); } return false; }; /** * Sets focus to a specific child element within the zone. This can be used in conjunction with * shouldReceiveFocus to create delayed focus scenarios (like animate the scroll position to the correct * location and then focus.) * @param element - The child element within the zone to focus. * @param forceAlignment - If true, focus alignment will be set according to the element provided. * @returns True if focus could be set to an active element, false if no operation was taken. */ FocusZone.prototype.focusElement = function (element, forceAlignment) { // eslint-disable-next-line @typescript-eslint/no-deprecated var _a = this.props, onBeforeFocus = _a.onBeforeFocus, shouldReceiveFocus = _a.shouldReceiveFocus; if ((shouldReceiveFocus && !shouldReceiveFocus(element)) || (onBeforeFocus && !onBeforeFocus(element))) { return false; } if (element) { // when we set focus to a specific child, we should recalculate the alignment depending on its position. this._setActiveElement(element, forceAlignment); if (this._activeElement) { this._activeElement.focus(); } return true; } return false; }; /** * Forces horizontal alignment in the context of vertical arrowing to use specific point as the reference, * rather than a center based on the last horizontal motion. * @param point - the new reference point. */ FocusZone.prototype.setFocusAlignment = function (point) { this._focusAlignment = point; }; Object.defineProperty(FocusZone.prototype, "defaultFocusElement", { get: function () { return this._defaultFocusElement; }, enumerable: false, configurable: true }); Object.defineProperty(FocusZone.prototype, "activeElement", { get: function () { return this._activeElement; }, enumerable: false, configurable: true }); FocusZone.prototype._evaluateFocusBeforeRender = function () { var root = this._root.current; var doc = this._getDocument(); if (doc) { var focusedElement = doc.activeElement; // Only update the index path if we are not parked on the root. if (focusedElement !== root) { var shouldRestoreFocus = (0, utilities_1.elementContains)(root, focusedElement, false); this._lastIndexPath = shouldRestoreFocus ? (0, utilities_1.getElementIndexPath)(root, focusedElement) : undefined; } } }; /** * When focus is in the zone at render time but then all focusable elements are removed, * we "park" focus temporarily on the root. Once we update with focusable children, we restore * focus to the closest path from previous. If the user tabs away from the parked container, * we restore focusability to the pre-parked state. */ FocusZone.prototype._setParkedFocus = function (isParked) { var root = this._root.current; if (root && this._isParked !== isParked) { this._isParked = isParked; if (isParked) { if (!this.props.allowFocusRoot) { this._parkedTabIndex = root.getAttribute('tabindex'); root.setAttribute('tabindex', '-1'); } root.focus(); } else if (!this.props.allowFocusRoot) { if (this._parkedTabIndex) { root.setAttribute('tabindex', this._parkedTabIndex); this._parkedTabIndex = undefined; } else { root.removeAttribute('tabindex'); } } } }; FocusZone.prototype._setActiveElement = function (element, forceAlignment) { var previousActiveElement = this._activeElement; this._activeElement = element; if (previousActiveElement) { if ((0, utilities_1.isElementFocusZone)(previousActiveElement)) { this._updateTabIndexes(previousActiveElement); } previousActiveElement.tabIndex = -1; } if (this._activeElement) { if (!this._focusAlignment || forceAlignment) { this._setFocusAlignment(element, true, true); } this._activeElement.tabIndex = 0; } }; FocusZone.prototype._preventDefaultWhenHandled = function (ev) { this.props.preventDefaultWhenHandled && ev.preventDefault(); }; /** * Walk up the dom try to find a focusable element. */ FocusZone.prototype._tryInvokeClickForFocusable = function (targetElement, ev) { var target = targetElement; if (target === this._root.current) { return false; } do { if (target.tagName === 'BUTTON' || target.tagName === 'A' || target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SUMMARY') { return false; } if (this._isImmediateDescendantOfZone(target) && target.getAttribute(IS_FOCUSABLE_ATTRIBUTE) === 'true' && target.getAttribute(IS_ENTER_DISABLED_ATTRIBUTE) !== 'true') { raiseClickFromKeyboardEvent(target, ev); return true; } target = (0, utilities_1.getParent)(target, ALLOW_VIRTUAL_ELEMENTS); } while (target !== this._root.current); return false; }; /** * Traverse to find first child zone. */ FocusZone.prototype._getFirstInnerZone = function (rootElement) { rootElement = rootElement || this._activeElement || this._root.current; if (!rootElement) { return null; } if ((0, utilities_1.isElementFocusZone)(rootElement)) { return _allInstances[rootElement.getAttribute(FOCUSZONE_ID_ATTRIBUTE)]; } var child = rootElement.firstElementChild; while (child) { if ((0, utilities_1.isElementFocusZone)(child)) { return _allInstances[child.getAttribute(FOCUSZONE_ID_ATTRIBUTE)]; } var match = this._getFirstInnerZone(child); if (match) { return match; } child = child.nextElementSibling; } return null; }; FocusZone.prototype._moveFocus = function (isForward, // eslint-disable-next-line @typescript-eslint/no-deprecated getDistanceFromCenter, ev, useDefaultWrap) { if (useDefaultWrap === void 0) { useDefaultWrap = true; } var element = this._activeElement; var candidateDistance = -1; var candidateElement = undefined; var changedFocus = false; var isBidirectional = this.props.direction === FocusZone_types_1.FocusZoneDirection.bidirectional; if (!element || !this._root.current) { return false; } if (this._isElementInput(element)) { if (!this._shouldInputLoseFocus(element, isForward)) { return false; } } var activeRect = isBidirectional ? element.getBoundingClientRect() : null; do { element = (isForward ? (0, utilities_1.getNextElement)(this._root.current, element) : (0, utilities_1.getPreviousElement)(this._root.current, element)); if (isBidirectional) { if (element) { var targetRect = element.getBoundingClientRect(); // eslint-disable-next-line @typescript-eslint/no-deprecated var elementDistance = getDistanceFromCenter(activeRect, targetRect); if (elementDistance === -1 && candidateDistance === -1) { candidateElement = element; break; } if (elementDistance > -1 && (candidateDistance === -1 || elementDistance < candidateDistance)) { candidateDistance = elementDistance; candidateElement = element; } if (candidateDistance >= 0 && elementDistance < 0) { break; } } } else { candidateElement = element; break; } } while (element); // Focus the closest candidate if (candidateElement && candidateElement !== this._activeElement) { changedFocus = true; this.focusElement(candidateElement); } else if (this.props.isCircularNavigation && useDefaultWrap) { if (isForward) { return this.focusElement((0, utilities_1.getNextElement)(this._root.current, this._root.current.firstElementChild, true)); } else { return this.focusElement((0, utilities_1.getPreviousElement)(this._root.current, this._root.current.lastElementChild, true, true, true)); } } return changedFocus; }; FocusZone.prototype._moveFocusDown = function () { var _this = this; var targetTop = -1; // eslint-disable-next-line @typescript-eslint/no-deprecated var leftAlignment = this._focusAlignment.left || this._focusAlignment.x || 0; if ( // eslint-disable-next-line @typescript-eslint/no-deprecated this._moveFocus(true, function (activeRect, targetRect) { var distance = -1; // ClientRect values can be floats that differ by very small fractions of a decimal. // If the difference between top and bottom are within a pixel then we should treat // them as equivalent by using Math.floor. For instance 5.2222 and 5.222221 should be equivalent, // but without Math.Floor they will be handled incorrectly. var targetRectTop = Math.floor(targetRect.top); var activeRectBottom = Math.floor(activeRect.bottom); if (targetRectTop < activeRectBottom) { if (!_this._shouldWrapFocus(_this._activeElement, NO_VERTICAL_WRAP)) { return LARGE_NEGATIVE_DISTANCE_FROM_CENTER; } return LARGE_DISTANCE_FROM_CENTER; } if ((targetTop === -1 && targetRectTop >= activeRectBottom) || targetRectTop === targetTop) { targetTop = targetRectTop; if (leftAlignment >= targetRect.left && leftAlignment <= targetRect.left + targetRect.width) { distance = 0; } else { distance = Math.abs(targetRect.left + targetRect.width / 2 - leftAlignment); } } return distance; })) { this._setFocusAlignment(this._activeElement, false, true); return true; } return false; }; FocusZone.prototype._moveFocusUp = function () { var _this = this; var targetTop = -1; // eslint-disable-next-line @typescript-eslint/no-deprecated var leftAlignment = this._focusAlignment.left || this._focusAlignment.x || 0; if ( // eslint-disable-next-line @typescript-eslint/no-deprecated this._moveFocus(false, function (activeRect, targetRect) { var distance = -1; // ClientRect values can be floats that differ by very small fractions of a decimal. // If the difference between top and bottom are within a pixel then we should treat // them as equivalent by using Math.floor. For instance 5.2222 and 5.222221 should be equivalent, // but without Math.Floor they will be handled incorrectly. var targetRectBottom = Math.floor(targetRect.bottom); var targetRectTop = Math.floor(targetRect.top); var activeRectTop = Math.floor(activeRect.top); if (targetRectBottom > activeRectTop) { if (!_this._shouldWrapFocus(_this._activeElement, NO_VERTICAL_WRAP)) { return LARGE_NEGATIVE_DISTANCE_FROM_CENTER; } return LARGE_DISTANCE_FROM_CENTER; } if ((targetTop === -1 && targetRectBottom <= activeRectTop) || targetRectTop === targetTop) { targetTop = targetRectTop; if (leftAlignment >= targetRect.left && leftAlignment <= targetRect.left + targetRect.width) { distance = 0; } else { distance = Math.abs(targetRect.left + targetRect.width / 2 - leftAlignment); } } return distance; })) { this._setFocusAlignment(this._activeElement, false, true); return true; } return false; }; FocusZone.prototype._moveFocusLeft = function (theme) { var _this = this; var shouldWrap = this._shouldWrapFocus(this._activeElement, NO_HORIZONTAL_WRAP); if (this._moveFocus((0, utilities_1.getRTL)(theme), // eslint-disable-next-line @typescript-eslint/no-deprecated function (activeRect, targetRect) { var distance = -1; var topBottomComparison; if ((0, utilities_1.getRTL)(theme)) { // When in RTL, this comparison should be the same as the one in _moveFocusRight for LTR. // Going left at a leftmost rectangle will go down a line instead of up a line like in LTR. // This is important, because we want to be comparing the top of the target rect // with the bottom of the active rect. topBottomComparison = parseFloat(targetRect.top.toFixed(3)) < parseFloat(activeRect.bottom.toFixed(3)); } else { topBottomComparison = parseFloat(targetRect.bottom.toFixed(3)) > parseFloat(activeRect.top.toFixed(3)); } if (topBottomComparison && targetRect.right <= activeRect.right && _this.props.direction !== FocusZone_types_1.FocusZoneDirection.vertical) { distance = activeRect.right - targetRect.right; } else if (!shouldWrap) { distance = LARGE_NEGATIVE_DISTANCE_FROM_CENTER; } return distance; }, undefined /*ev*/, shouldWrap)) { this._setFocusAlignment(this._activeElement, true, false); return true; } return false; }; FocusZone.prototype._moveFocusRight = function (theme) { var _this = this; var shouldWrap = this._shouldWrapFocus(this._activeElement, NO_HORIZONTAL_WRAP); if (this._moveFocus(!(0, utilities_1.getRTL)(theme), // eslint-disable-next-line @typescript-eslint/no-deprecated function (activeRect, targetRect) { var distance = -1; var topBottomComparison; if ((0, utilities_1.getRTL)(theme)) { // When in RTL, this comparison should be the same as the one in _moveFocusLeft for LTR. // Going right at a rightmost rectangle will go up a line instead of down a line like in LTR. // This is important, because we want to be comparing the bottom of the target rect // with the top of the active rect. topBottomComparison = parseFloat(targetRect.bottom.toFixed(3)) > parseFloat(activeRect.top.toFixed(3)); } else { topBottomComparison = parseFloat(targetRect.top.toFixed(3)) < parseFloat(activeRect.bottom.toFixed(3)); } if (topBottomComparison && targetRect.left >= activeRect.left && _this.props.direction !== FocusZone_types_1.FocusZoneDirection.vertical) { distance = targetRect.left - activeRect.left; } else if (!shouldWrap) { distance = LARGE_NEGATIVE_DISTANCE_FROM_CENTER; } return distance; }, undefined /*ev*/, shouldWrap)) { this._setFocusAlignment(this._activeElement, true, false); return true; } return false; }; FocusZone.prototype._moveFocusPaging = function (isForward, useDefaultWrap) { if (useDefaultWrap === void 0) { useDefaultWrap = true; } var element = this._activeElement; if (!element || !this._root.current) { return false; } if (this._isElementInput(element)) { if (!this._shouldInputLoseFocus(element, isForward)) { return false; } } var scrollableParent = (0, utilities_1.findScrollableParent)(element); if (!scrollableParent) { return false; } var candidateDistance = -1; var candidateElement = undefined; var targetTop = -1; var targetBottom = -1; var pagesize = scrollableParent.clientHeight; var activeRect = element.getBoundingClientRect(); do { element = isForward ? (0, utilities_1.getNextElement)(this._root.current, element) : (0, utilities_1.getPreviousElement)(this._root.current, element); if (element) { var targetRect = element.getBoundingClientRect(); var targetRectTop = Math.floor(targetRect.top); var activeRectBottom = Math.floor(activeRect.bottom); var targetRectBottom = Math.floor(targetRect.bottom); var activeRectTop = Math.floor(activeRect.top); var elementDistance = this._getHorizontalDistanceFromCenter(isForward, activeRect, targetRect); var isElementPassedPageSizeOnPagingDown = isForward && targetRectTop > activeRectBottom + pagesize; var isElementPassedPageSizeOnPagingUp = !isForward && targetRectBottom < activeRectTop - pagesize; if (isElementPassedPageSizeOnPagingDown || isElementPassedPageSizeOnPagingUp) { break; } if (elementDistance > -1) { // for paging down if (isForward && targetRectTop > targetTop) { targetTop = targetRectTop; candidateDistance = elementDistance; candidateElement = element; } else if (!isForward && targetRectBottom < targetBottom) { // for paging up targetBottom = targetRectBottom; candidateDistance = elementDistance; candidateElement = element; } else if (candidateDistance === -1 || elementDistance <= candidateDistance) { candidateDistance = elementDistance; candidateElement = element; } } } } while (element); var changedFocus = false; // Focus the closest candidate if (candidateElement && candidateElement !== this._activeElement) { changedFocus = true; this.focusElement(candidateElement); this._setFocusAlignment(candidateElement, false, true); } else if (this.props.isCircularNavigation && useDefaultWrap) { if (isForward) { return this.focusElement((0, utilities_1.getNextElement)(this._root.current, this._root.current.firstElementChild, true)); } return this.focusElement((0, utilities_1.getPreviousElement)(this._root.current, this._root.current.lastElementChild, true, true, true)); } return changedFocus; }; FocusZone.prototype._setFocusAlignment = function (element, isHorizontal, isVertical) { if (this.props.direction === FocusZone_types_1.FocusZoneDirection.bidirectional && (!this._focusAlignment || isHorizontal || isVertical)) { var rect = element.getBoundingClientRect(); var left = rect.left + rect.width / 2; var top_1 = rect.top + rect.height / 2; if (!this._focusAlignment) { this._focusAlignment = { left: left, top: top_1 }; } if (isHorizontal) { this._focusAlignment.left = left; } if (isVertical) { this._focusAlignment.top = top_1; } } }; FocusZone.prototype._isImmediateDescendantOfZone = function (element) { return this._getOwnerZone(element) === this._root.current; }; FocusZone.prototype._getOwnerZone = function (element) { var parentElement = (0, utilities_1.getParent)(element, ALLOW_VIRTUAL_ELEMENTS); while (parentElement && parentElement !== this._root.current && parentElement !== this._getDocument().body) { if ((0, utilities_1.isElementFocusZone)(parentElement)) { return parentElement; } parentElement = (0, utilities_1.getParent)(parentElement, ALLOW_VIRTUAL_ELEMENTS); } return parentElement; }; FocusZone.prototype._updateTabIndexes = function (element) { if (!this._activeElement && this.props.defaultTabbableElement && typeof this.props.defaultTabbableElement === 'function') { this._activeElement = this.props.defaultTabbableElement(this._root.current); } if (!element && this._root.current) { this._defaultFocusElement = null; element = this._root.current; if (this._activeElement && !(0, utilities_1.elementContains)(element, this._activeElement)) { this._activeElement = null; } } // If active element changes state to disabled, set it to null. // Otherwise, we lose keyboard accessibility to other elements in focus zone. if (this._activeElement && !(0, utilities_1.isElementTabbable)(this._activeElement, undefined, this._inShadowRoot)) { this._activeElement = null; } var childNodes = element && element.children; for (var childIndex = 0; childNodes && childIndex < childNodes.length; childIndex++) { var child = childNodes[childIndex]; if (!(0, utilities_1.isElementFocusZone)(child)) { // If the item is explicitly set to not be focusable then TABINDEX needs to be set to -1. if (child.getAttribute && child.getAttribute(IS_FOCUSABLE_ATTRIBUTE) === 'false') { child.setAttribute(TABINDEX, '-1'); } if ((0, utilities_1.isElementTabbable)(child, undefined, this._inShadowRoot)) { if (this.props.disabled) { child.setAttribute(TABINDEX, '-1'); } else if (!this._isInnerZone && ((!this._activeElement && !this._defaultFocusElement) || this._activeElement === child)) { this._defaultFocusElement = child; if (child.getAttribute(TABINDEX) !== '0') { child.setAttribute(TABINDEX, '0'); } } else if (child.getAttribute(TABINDEX) !== '-1') { child.setAttribute(TABINDEX, '-1'); } } else if (child.tagName === 'svg' && child.getAttribute('focusable') !== 'false') { // Disgusting IE hack. Sad face. child.setAttribute('focusable', 'false'); } } else if (child.getAttribute(IS_FOCUSABLE_ATTRIBUTE) === 'true') { if (!this._isInnerZone && ((!this._activeElement && !this._defaultFocusElement) || this._activeElement === child)) { this._defaultFocusElement = child; if (child.getAttribute(TABINDEX) !== '0') { child.setAttribute(TABINDEX, '0'); } } else if (child.getAttribute(TABINDEX) !== '-1') { child.setAttribute(TABINDEX, '-1'); } } this._updateTabIndexes(child); } }; FocusZone.prototype._isContentEditableElement = function (element) { return element && element.getAttribute('contenteditable') === 'true'; }; FocusZone.prototype._isElementInput = function (element) { if (element && element.tagName && (element.tagName.toLowerCase() === 'input' || element.tagName.toLowerCase() === 'textarea')) { return true; } return false; }; FocusZone.prototype._shouldInputLoseFocus = function (element, isForward) { // If a tab was used, we want to focus on the next element. if (!this._processingTabKey && element && element.type && ALLOWED_INPUT_TYPES.indexOf(element.type.toLowerCase()) > -1) { var selectionStart = element.selectionStart; var selectionEnd = element.selectionEnd; var isRangeSelected = selectionStart !== selectionEnd; var inputValue = element.value; var isReadonly = element.readOnly; // We shouldn't lose focus in the following cases: // 1. There is range selected. // 2. When selection start is larger than 0 and it is backward and not readOnly. // 3. when selection start is not the end of length, it is forward and not readOnly. // 4. We press any of the arrow keys when our handleTabKey isn't none or undefined (only losing focus if we hit // tab) and if shouldInputLoseFocusOnArrowKey is defined, if scenario prefers to not loose the focus which is // determined by calling the callback shouldInputLoseFocusOnArrowKey if (isRangeSelected || (selectionStart > 0 && !isForward && !isReadonly) || (selectionStart !== inputValue.length && isForward && !isReadonly) || (!!this.props.handleTabKey && !(this.props.shouldInputLoseFocusOnArrowKey && this.props.shouldInputLoseFocusOnArrowKey(element)))) { return false; } } return true; }; FocusZone.prototype._shouldWrapFocus = function (element, noWrapDataAttribute) { return this.props.checkForNoWrap ? (0, utilities_1.shouldWrapFocus)(element, noWrapDataAttribute) : true; }; /** * Returns true if the element is a descendant of the FocusZone through a React portal. */ FocusZone.prototype._portalContainsElement = function (element) { return element && !!this._root.current && (0, utilities_1.portalContainsElement)(element, this._root.current); }; FocusZone.prototype._getDocument = function () { return (0, utilities_1.getDocument)(this._root.current); }; FocusZone.defaultProps = { isCircularNavigation: false, direction: FocusZone_types_1.FocusZoneDirection.bidirectional, shouldRaiseClicks: true, // Hardcoding uncontrolled flag for proper interop with FluentUI V9. 'data-tabster': '{"uncontrolled": {}}', }; FocusZone.contextType = utilities_1.MergeStylesShadowRootContext; return FocusZone; }(React.Component)); exports.FocusZone = FocusZone; //# sourceMappingURL=FocusZone.js.map