UNPKG

office-ui-fabric-react

Version:

Reusable React components for building experiences for Office 365.

662 lines • 31.2 kB
import * as tslib_1 from "tslib"; import * as React from 'react'; import { FocusZoneDirection } from './FocusZone.types'; import { BaseComponent, EventGroup, css, htmlElementProperties, elementContains, getDocument, getId, getNextElement, getNativeProps, getParent, getPreviousElement, getRTL, isElementFocusZone, isElementFocusSubZone, isElementTabbable, shouldWrapFocus, createRef } from '../../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 _allInstances = {}; var ALLOWED_INPUT_TYPES = ['text', 'number', 'password', 'email', 'tel', 'url', 'search']; var ALLOW_VIRTUAL_ELEMENTS = false; var FocusZone = /** @class */ (function (_super) { tslib_1.__extends(FocusZone, _super); function FocusZone(props) { var _this = _super.call(this, props) || this; _this._root = createRef(); _this._onFocus = function (ev) { var _a = _this.props, onActiveElementChanged = _a.onActiveElementChanged, doNotAllowFocusEventToPropagate = _a.doNotAllowFocusEventToPropagate, onFocusNotification = _a.onFocusNotification; if (onFocusNotification) { onFocusNotification(); } if (_this._isImmediateDescendantOfZone(ev.target)) { _this._activeElement = ev.target; _this._setFocusAlignment(_this._activeElement); } else { var parentElement = ev.target; while (parentElement && parentElement !== _this._root.current) { if (isElementTabbable(parentElement) && _this._isImmediateDescendantOfZone(parentElement)) { _this._activeElement = parentElement; break; } parentElement = getParent(parentElement, ALLOW_VIRTUAL_ELEMENTS); } } if (onActiveElementChanged) { onActiveElementChanged(_this._activeElement, ev); } if (doNotAllowFocusEventToPropagate) { ev.stopPropagation(); } }; _this._onMouseDown = function (ev) { var disabled = _this.props.disabled; if (disabled) { return; } var target = ev.target; var path = []; while (target && target !== _this._root.current) { path.push(target); target = getParent(target, ALLOW_VIRTUAL_ELEMENTS); } while (path.length) { target = path.pop(); if (target && isElementTabbable(target)) { _this._setActiveElement(target, true); } if (isElementFocusZone(target)) { // Stop here since the focus zone will take care of its own children. break; } } }; /** * Handle the keystrokes. */ _this._onKeyDown = function (ev) { var _a = _this.props, direction = _a.direction, disabled = _a.disabled, isInnerZoneKeystroke = _a.isInnerZoneKeystroke; if (disabled) { return; } if (document.activeElement === _this._root.current && _this._isInnerZone) { // If this element has focus, it is being controlled by a parent. // Ignore the keystroke. 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 (isInnerZoneKeystroke && isInnerZoneKeystroke(ev) && _this._isImmediateDescendantOfZone(ev.target)) { // Try to focus var innerZone = _this._getFirstInnerZone(); if (innerZone) { if (!innerZone.focus(true)) { return; } } else if (isElementFocusSubZone(ev.target)) { if (!_this.focusElement(getNextElement(ev.target, ev.target.firstChild, true))) { return; } } else { return; } } else if (ev.altKey) { return; } else { switch (ev.which) { case 32 /* space */: if (_this._tryInvokeClickForFocusable(ev.target)) { break; } return; case 37 /* left */: if (direction !== FocusZoneDirection.vertical && _this._moveFocusLeft()) { break; } return; case 39 /* right */: if (direction !== FocusZoneDirection.vertical && _this._moveFocusRight()) { break; } return; case 38 /* up */: if (direction !== FocusZoneDirection.horizontal && _this._moveFocusUp()) { break; } return; case 40 /* down */: if (direction !== FocusZoneDirection.horizontal && _this._moveFocusDown()) { break; } return; case 9 /* tab */: if (_this.props.allowTabKey || _this.props.handleTabKey === 1 /* all */ || (_this.props.handleTabKey === 2 /* inputOnly */ && _this._isElementInput(ev.target))) { var focusChanged = false; _this._processingTabKey = true; if (direction === FocusZoneDirection.vertical || !_this._shouldWrapFocus(_this._activeElement, NO_HORIZONTAL_WRAP)) { focusChanged = ev.shiftKey ? _this._moveFocusUp() : _this._moveFocusDown(); } else if (direction === FocusZoneDirection.horizontal || direction === FocusZoneDirection.bidirectional) { var tabWithDirection = getRTL() ? !ev.shiftKey : ev.shiftKey; focusChanged = tabWithDirection ? _this._moveFocusLeft() : _this._moveFocusRight(); } _this._processingTabKey = false; if (focusChanged) { break; } } return; case 36 /* home */: if (_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(getNextElement(_this._root.current, firstChild, true))) { break; } return; case 35 /* end */: if (_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(getPreviousElement(_this._root.current, lastChild, true, true, true))) { break; } return; case 13 /* enter */: if (_this._tryInvokeClickForFocusable(ev.target)) { break; } return; default: return; } } ev.preventDefault(); ev.stopPropagation(); }; _this._warnDeprecations({ rootProps: undefined, allowTabKey: 'handleTabKey' }); _this._id = getId('FocusZone'); _this._focusAlignment = { left: 0, top: 0 }; _this._processingTabKey = false; return _this; } FocusZone.prototype.componentDidMount = function () { _allInstances[this._id] = this; if (this._root.current) { var windowElement = this._root.current.ownerDocument.defaultView; var parentElement = getParent(this._root.current, ALLOW_VIRTUAL_ELEMENTS); while (parentElement && parentElement !== document.body && parentElement.nodeType === 1) { if (isElementFocusZone(parentElement)) { this._isInnerZone = true; break; } parentElement = getParent(parentElement, ALLOW_VIRTUAL_ELEMENTS); } if (!this._isInnerZone) { this._events.on(windowElement, 'keydown', this._onKeyDownCapture, true); } // Assign initial tab indexes so that we can set initial focus as appropriate. this._updateTabIndexes(); if (this.props.defaultActiveElement) { this._activeElement = getDocument().querySelector(this.props.defaultActiveElement); this.focus(); } } }; FocusZone.prototype.componentWillUnmount = function () { delete _allInstances[this._id]; }; FocusZone.prototype.render = function () { var _a = this.props, rootProps = _a.rootProps, ariaDescribedBy = _a.ariaDescribedBy, ariaLabelledBy = _a.ariaLabelledBy, className = _a.className; var divProps = getNativeProps(this.props, htmlElementProperties); var Tag = this.props.elementType || 'div'; return (React.createElement(Tag, tslib_1.__assign({ role: "presentation" }, divProps, rootProps, { className: css('ms-FocusZone', className), ref: this._root, "data-focuszone-id": this._id, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, onKeyDown: this._onKeyDown, onFocus: this._onFocus, onMouseDownCapture: this._onMouseDown }), this.props.children)); }; /** * Sets focus to the first tabbable item in the zone. * @param {boolean} forceIntoFirstElement If true, focus will be forced into the first element, even if focus is already in the focus zone. * @returns True if focus could be set to an active element, false if no operation was taken. */ FocusZone.prototype.focus = function (forceIntoFirstElement) { if (forceIntoFirstElement === void 0) { forceIntoFirstElement = 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 && elementContains(this._root.current, this._activeElement) && isElementTabbable(this._activeElement)) { this._activeElement.focus(); return true; } else { var firstChild = this._root.current.firstChild; return this.focusElement(getNextElement(this._root.current, firstChild, true)); } } return false; }; /** * Sets focus to a specific child element within the zone. This can be used in conjunction with * onBeforeFocus to created delayed focus scenarios (like animate the scroll position to the correct * location and then focus.) * @param {HTMLElement} element The child element within the zone to focus. * @returns True if focus could be set to an active element, false if no operation was taken. */ FocusZone.prototype.focusElement = function (element) { var onBeforeFocus = this.props.onBeforeFocus; if (onBeforeFocus && !onBeforeFocus(element)) { return false; } if (element) { this._setActiveElement(element); if (this._activeElement) { this._activeElement.focus(); } return true; } return false; }; /** * Handle global tab presses so that we can patch tabindexes on the fly. */ FocusZone.prototype._onKeyDownCapture = function (ev) { if (ev.which === 9 /* tab */) { this._updateTabIndexes(); } }; FocusZone.prototype._setActiveElement = function (element, forceAlignemnt) { var previousActiveElement = this._activeElement; this._activeElement = element; if (previousActiveElement) { if (isElementFocusZone(previousActiveElement)) { this._updateTabIndexes(previousActiveElement); } previousActiveElement.tabIndex = -1; } if (this._activeElement) { if (!this._focusAlignment || forceAlignemnt) { this._setFocusAlignment(element, true, true); } this._activeElement.tabIndex = 0; } }; /** * Walk up the dom try to find a focusable element. */ FocusZone.prototype._tryInvokeClickForFocusable = function (target) { if (target === this._root.current) { return false; } do { if (target.tagName === 'BUTTON' || target.tagName === 'A' || target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') { return false; } if (this._isImmediateDescendantOfZone(target) && target.getAttribute(IS_FOCUSABLE_ATTRIBUTE) === 'true' && target.getAttribute(IS_ENTER_DISABLED_ATTRIBUTE) !== 'true') { EventGroup.raise(target, 'click', null, true); return true; } target = 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 (isElementFocusZone(rootElement)) { return _allInstances[rootElement.getAttribute(FOCUSZONE_ID_ATTRIBUTE)]; } var child = rootElement.firstElementChild; while (child) { if (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, 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 === 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 ? getNextElement(this._root.current, element) : getPreviousElement(this._root.current, element)); if (isBidirectional) { if (element) { var targetRect = element.getBoundingClientRect(); 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(getNextElement(this._root.current, this._root.current.firstElementChild, true)); } else { return this.focusElement(getPreviousElement(this._root.current, this._root.current.lastElementChild, true, true, true)); } } return changedFocus; }; FocusZone.prototype._moveFocusDown = function () { var _this = this; var targetTop = -1; var leftAlignment = this._focusAlignment.left; if (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; var leftAlignment = this._focusAlignment.left; if (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 () { var _this = this; var shouldWrap = this._shouldWrapFocus(this._activeElement, NO_HORIZONTAL_WRAP); if (this._moveFocus(getRTL(), function (activeRect, targetRect) { var distance = -1; if (targetRect.bottom > activeRect.top && targetRect.right <= activeRect.right && _this.props.direction !== 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 () { var _this = this; var shouldWrap = this._shouldWrapFocus(this._activeElement, NO_HORIZONTAL_WRAP); if (this._moveFocus(!getRTL(), function (activeRect, targetRect) { var distance = -1; if (targetRect.top < activeRect.bottom && targetRect.left >= activeRect.left && _this.props.direction !== 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._setFocusAlignment = function (element, isHorizontal, isVertical) { if (this.props.direction === 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 = getParent(element, ALLOW_VIRTUAL_ELEMENTS); while (parentElement && parentElement !== this._root.current && parentElement !== document.body) { if (isElementFocusZone(parentElement)) { return parentElement; } parentElement = getParent(parentElement, ALLOW_VIRTUAL_ELEMENTS); } return this._root.current; }; FocusZone.prototype._updateTabIndexes = function (element) { if (!element && this._root.current) { this._defaultFocusElement = null; element = this._root.current; if (this._activeElement && !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 && !isElementTabbable(this._activeElement)) { this._activeElement = null; } var childNodes = element && element.children; for (var childIndex = 0; childNodes && childIndex < childNodes.length; childIndex++) { var child = childNodes[childIndex]; if (!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 (isElementTabbable(child)) { 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._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; // 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. // 3. when selection start is not the end of length and it is forward. // 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) || (selectionStart !== inputValue.length && isForward) || (!!this.props.handleTabKey && !(this.props.shouldInputLoseFocusOnArrowKey && this.props.shouldInputLoseFocusOnArrowKey(element)))) { return false; } } return true; }; FocusZone.prototype._shouldWrapFocus = function (element, noWrapDataAttribute) { return !!this.props.checkForNoWrap ? shouldWrapFocus(element, noWrapDataAttribute) : true; }; FocusZone.defaultProps = { isCircularNavigation: false, direction: FocusZoneDirection.bidirectional }; return FocusZone; }(BaseComponent)); export { FocusZone }; //# sourceMappingURL=FocusZone.js.map