UNPKG

office-ui-fabric-react

Version:

Reusable React components for building experiences for Office 365.

505 lines • 24.8 kB
import * as tslib_1 from "tslib"; import * as React from 'react'; import { getLayerStyles } from './KeytipLayer.styles'; import { Keytip } from '../../Keytip'; import { Layer } from '../../Layer'; import { BaseComponent, classNamesFunction, getDocument, arraysEqual } from '../../Utilities'; import { KeytipManager } from '../../utilities/keytips/KeytipManager'; import { KeytipTree } from './KeytipTree'; import { ktpTargetFromId, sequencesToID, mergeOverflows } from '../../utilities/keytips/KeytipUtils'; import { transitionKeysContain, KeytipTransitionModifier } from '../../utilities/keytips/IKeytipTransitionKey'; import { KeytipEvents, KTP_LAYER_ID, KTP_ARIA_SEPARATOR } from '../../utilities/keytips/KeytipConstants'; var isMac = typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Macintosh') >= 0; // Default sequence is Alt-Windows (Alt-Meta) in Windows, Option-Control (Alt-Control) in Mac var defaultStartSequence = { key: isMac ? 'Control' : 'Meta', modifierKeys: [KeytipTransitionModifier.alt] }; // Default exit sequence is the same as the start sequence var defaultExitSequence = defaultStartSequence; // Default return sequence is Escape var defaultReturnSequence = { key: 'Escape' }; var getClassNames = classNamesFunction(); /** * A layer that holds all keytip items * * @export * @class KeytipLayer * @extends {BaseComponent<IKeytipLayerProps>} */ var KeytipLayerBase = /** @class */ (function (_super) { tslib_1.__extends(KeytipLayerBase, _super); // tslint:disable-next-line:no-any function KeytipLayerBase(props, context) { var _this = _super.call(this, props, context) || this; _this._keytipManager = KeytipManager.getInstance(); _this._delayedKeytipQueue = []; _this._keyHandled = false; _this._onDismiss = function (ev) { // if we are in keytip mode, then exit keytip mode if (_this.state.inKeytipMode) { _this.exitKeytipMode(); } }; _this._onKeyDown = function (ev) { _this._keyHandled = false; // using key since which has been deprecated and key is now widely suporrted. // See: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/which var key = ev.key; switch (key) { case 'Alt': // ALT puts focus in the browser bar, so it should not be used as a key for keytips. // It can be used as a modifier break; case 'Tab': case 'Enter': case 'Spacebar': case ' ': case 'ArrowUp': case 'Up': case 'ArrowDown': case 'Down': case 'ArrowLeft': case 'Left': case 'ArrowRight': case 'Right': if (_this.state.inKeytipMode) { _this._keyHandled = true; _this.exitKeytipMode(); ev.preventDefault(); ev.stopPropagation(); } break; default: // Special cases for browser-specific keys that are not at standard // (according to http://www.w3.org/TR/uievents-key/#keys-navigation) if (key === 'Esc') { // Edge: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/5290772/ key = 'Escape'; } else if (key === 'OS' || key === 'Win') { // Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1232918 // Edge: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8860571/ // and https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/16424492/ key = 'Meta'; } var transitionKey = { key: key }; transitionKey.modifierKeys = _this._getModifierKey(key, ev); _this.processTransitionInput(transitionKey); break; } }; _this._onKeyPress = function (ev) { if (_this.state.inKeytipMode && !_this._keyHandled) { // Call processInput _this.processInput(ev.key.toLocaleLowerCase()); ev.preventDefault(); ev.stopPropagation(); } }; _this._onKeytipAdded = function (eventArgs) { var keytipProps = eventArgs.keytip; var uniqueID = eventArgs.uniqueID; _this.keytipTree.addNode(keytipProps, uniqueID); _this.setKeytips(); // Add the keytip to the queue to show later if (_this.keytipTree.isCurrentKeytipParent(keytipProps)) { _this._addKeytipToQueue(sequencesToID(keytipProps.keySequences)); } if (_this._newCurrentKeytipSequences && arraysEqual(keytipProps.keySequences, _this._newCurrentKeytipSequences)) { _this._triggerKeytipImmediately(keytipProps); } if (_this._isCurrentKeytipAnAlias(keytipProps)) { var keytipSequence = keytipProps.keySequences; if (keytipProps.overflowSetSequence) { keytipSequence = mergeOverflows(keytipSequence, keytipProps.overflowSetSequence); } _this.keytipTree.currentKeytip = _this.keytipTree.getNode(sequencesToID(keytipSequence)); } }; _this._onKeytipUpdated = function (eventArgs) { var keytipProps = eventArgs.keytip; var uniqueID = eventArgs.uniqueID; _this.keytipTree.updateNode(keytipProps, uniqueID); _this.setKeytips(); }; _this._onKeytipRemoved = function (eventArgs) { var keytipProps = eventArgs.keytip; var uniqueID = eventArgs.uniqueID; // Remove keytip from the delayed queue _this._removeKeytipFromQueue(sequencesToID(keytipProps.keySequences)); // Remove the node from the Tree _this.keytipTree.removeNode(keytipProps, uniqueID); _this.setKeytips(); }; _this._onPersistedKeytipAdded = function (eventArgs) { var keytipProps = eventArgs.keytip; var uniqueID = eventArgs.uniqueID; _this.keytipTree.addNode(keytipProps, uniqueID, true); }; _this._onPersistedKeytipRemoved = function (eventArgs) { var keytipProps = eventArgs.keytip; var uniqueID = eventArgs.uniqueID; _this.keytipTree.removeNode(keytipProps, uniqueID); }; _this._onPersistedKeytipExecute = function (eventArgs) { _this._persistedKeytipExecute(eventArgs.overflowButtonSequences, eventArgs.keytipSequences); }; /** * Sets if we are in keytip mode. * Note, this sets both the state for the layer as well as * the value that the manager will expose externally. * @param inKeytipMode - Boolean so set whether we are in keytip mode or not */ _this._setInKeytipMode = function (inKeytipMode) { _this.setState({ inKeytipMode: inKeytipMode }); _this._keytipManager.inKeytipMode = inKeytipMode; }; var managerKeytips = _this._keytipManager.getKeytips().slice(); _this.state = { inKeytipMode: false, // Get the initial set of keytips keytips: managerKeytips, visibleKeytips: _this._getVisibleKeytips(managerKeytips) }; _this.keytipTree = new KeytipTree(); // Add regular and persisted keytips to the tree for (var _i = 0, _a = _this._keytipManager.keytips; _i < _a.length; _i++) { var uniqueKeytip = _a[_i]; _this.keytipTree.addNode(uniqueKeytip.keytip, uniqueKeytip.uniqueID); } for (var _b = 0, _c = _this._keytipManager.persistedKeytips; _b < _c.length; _b++) { var uniquePersistedKeytip = _c[_b]; _this.keytipTree.addNode(uniquePersistedKeytip.keytip, uniquePersistedKeytip.uniqueID); } _this._currentSequence = ''; // Add keytip listeners _this._events.on(_this._keytipManager, KeytipEvents.KEYTIP_ADDED, _this._onKeytipAdded); _this._events.on(_this._keytipManager, KeytipEvents.KEYTIP_UPDATED, _this._onKeytipUpdated); _this._events.on(_this._keytipManager, KeytipEvents.KEYTIP_REMOVED, _this._onKeytipRemoved); _this._events.on(_this._keytipManager, KeytipEvents.PERSISTED_KEYTIP_ADDED, _this._onPersistedKeytipAdded); _this._events.on(_this._keytipManager, KeytipEvents.PERSISTED_KEYTIP_REMOVED, _this._onPersistedKeytipRemoved); _this._events.on(_this._keytipManager, KeytipEvents.PERSISTED_KEYTIP_EXECUTE, _this._onPersistedKeytipExecute); return _this; } /** * Sets the keytips state property * * @param keytipProps - Keytips to set in this layer */ KeytipLayerBase.prototype.setKeytips = function (keytipProps) { if (keytipProps === void 0) { keytipProps = this._keytipManager.getKeytips(); } this.setState({ keytips: keytipProps, visibleKeytips: this._getVisibleKeytips(keytipProps) }); }; KeytipLayerBase.prototype.getCurrentSequence = function () { return this._currentSequence; }; KeytipLayerBase.prototype.render = function () { var _this = this; var _a = this.props, content = _a.content, getStyles = _a.getStyles; var _b = this.state, keytips = _b.keytips, visibleKeytips = _b.visibleKeytips; this._classNames = getClassNames(getStyles); return (React.createElement(Layer, { getStyles: getLayerStyles }, React.createElement("span", { id: KTP_LAYER_ID, className: this._classNames.innerContent }, "" + content + KTP_ARIA_SEPARATOR), keytips && keytips.map(function (keytipProps, index) { return (React.createElement("span", { key: index, id: sequencesToID(keytipProps.keySequences), className: _this._classNames.innerContent }, keytipProps.keySequences.join(', '))); }), visibleKeytips && visibleKeytips.map(function (visibleKeytipProps) { return React.createElement(Keytip, tslib_1.__assign({ key: sequencesToID(visibleKeytipProps.keySequences) }, visibleKeytipProps)); }))); }; KeytipLayerBase.prototype.componentDidMount = function () { // Add window listeners this._events.on(window, 'mouseup', this._onDismiss, true /* useCapture */); this._events.on(window, 'resize', this._onDismiss); this._events.on(window, 'keydown', this._onKeyDown, true /* useCapture */); this._events.on(window, 'keypress', this._onKeyPress, true /* useCapture */); this._events.on(window, 'scroll', this._onDismiss, true /* useCapture */); }; KeytipLayerBase.prototype.componentWillUnmount = function () { // Remove window listeners this._events.off(window, 'mouseup', this._onDismiss, true /* useCapture */); this._events.off(window, 'resize', this._onDismiss); this._events.off(window, 'keydown', this._onKeyDown, true /* useCapture */); this._events.off(window, 'keypress', this._onKeyPress, true /* useCapture */); this._events.off(window, 'scroll', this._onDismiss, true /* useCapture */); // Remove keytip listeners this._events.off(this._keytipManager, 'keytipAdded', this._onKeytipAdded); this._events.off(this._keytipManager, 'keytipUpdated', this._onKeytipUpdated); this._events.off(this._keytipManager, 'keytipRemoved', this._onKeytipRemoved); this._events.off(this._keytipManager, 'persistedKeytipAdded', this._onPersistedKeytipAdded); this._events.off(this._keytipManager, 'persistedKeytipRemoved', this._onPersistedKeytipRemoved); this._events.off(this._keytipManager, 'persistedKeytipExecute', this._onPersistedKeytipExecute); }; /** * Enters keytip mode for this layer */ KeytipLayerBase.prototype.enterKeytipMode = function () { this.keytipTree.currentKeytip = this.keytipTree.root; // Show children of root this.showKeytips(this.keytipTree.getChildren()); this._setInKeytipMode(true /* inKeytipMode */); if (this.props.onEnterKeytipMode) { this.props.onEnterKeytipMode(); } }; /** * Exits keytip mode for this layer */ KeytipLayerBase.prototype.exitKeytipMode = function () { this.keytipTree.currentKeytip = undefined; this._currentSequence = ''; // Hide all keytips this.showKeytips([]); // Reset the delayed keytips if any this._delayedQueueTimeout && this._async.clearTimeout(this._delayedQueueTimeout); this._delayedKeytipQueue = []; this._setInKeytipMode(false /* inKeytipMode */); if (this.props.onExitKeytipMode) { this.props.onExitKeytipMode(); } }; /** * Processes an IKeytipTransitionKey entered by the user * * @param transitionKey - IKeytipTransitionKey received by the layer to process */ KeytipLayerBase.prototype.processTransitionInput = function (transitionKey) { var currKtp = this.keytipTree.currentKeytip; if (transitionKeysContain(this.props.keytipExitSequences, transitionKey) && currKtp) { // If key sequence is in 'exit sequences', exit keytip mode this._keyHandled = true; this.exitKeytipMode(); } else if (transitionKeysContain(this.props.keytipReturnSequences, transitionKey)) { // If key sequence is in return sequences, move currentKeytip to parent (or if currentKeytip is the root, exit) if (currKtp) { this._keyHandled = true; if (currKtp.id === this.keytipTree.root.id) { // We are at the root, exit keytip mode this.exitKeytipMode(); } else { // If this keytip has a onReturn prop, we execute the func. if (currKtp.onReturn) { currKtp.onReturn(this._getKeytipDOMElement(currKtp.id)); } // Reset currentSequence this._currentSequence = ''; // Return pointer to its parent this.keytipTree.currentKeytip = this.keytipTree.getNode(currKtp.parent); // Show children keytips of the new currentKeytip this.showKeytips(this.keytipTree.getChildren()); } } } else if (transitionKeysContain(this.props.keytipStartSequences, transitionKey) && !currKtp) { // If key sequence is in 'entry sequences' and currentKeytip is null, we enter keytip mode this._keyHandled = true; this.enterKeytipMode(); } }; /** * Processes inputs from the document listener and traverse the keytip tree * * @param key - Key pressed by the user */ KeytipLayerBase.prototype.processInput = function (key) { // Concat the input key with the current sequence var currSequence = this._currentSequence + key; var currKtp = this.keytipTree.currentKeytip; // currentKeytip must be defined, otherwise we haven't entered keytip mode yet if (currKtp) { var node = this.keytipTree.getExactMatchedNode(currSequence, currKtp); if (node) { this.keytipTree.currentKeytip = currKtp = node; var currKtpChildren = this.keytipTree.getChildren(); // Execute this node's onExecute if defined if (currKtp.onExecute) { currKtp.onExecute(this._getKeytipDOMElement(currKtp.id)); // Reset currKtp, this might have changed from the onExecute currKtp = this.keytipTree.currentKeytip; } // To exit keytipMode after executing the keytip it must not have a menu or have dynamic children if (currKtpChildren.length === 0 && !(currKtp.hasDynamicChildren || currKtp.hasMenu)) { this.exitKeytipMode(); } else { // Show all children keytips this.showKeytips(currKtpChildren); } // Clear currentSequence this._currentSequence = ''; return; } var partialNodes = this.keytipTree.getPartiallyMatchedNodes(currSequence, currKtp); if (partialNodes.length > 0) { // We found nodes that partially match the sequence, so we show only those // Omit showing persisted nodes here var ids = partialNodes.filter(function (partialNode) { return !partialNode.persisted; }).map(function (partialNode) { return partialNode.id; }); this.showKeytips(ids); // Save currentSequence this._currentSequence = currSequence; } } }; /** * Show the given keytips and hide all others * * @param ids - Keytip IDs to show */ KeytipLayerBase.prototype.showKeytips = function (ids) { // Update the visible prop in the manager for (var _i = 0, _a = this._keytipManager.getKeytips(); _i < _a.length; _i++) { var keytip = _a[_i]; var keytipId = sequencesToID(keytip.keySequences); if (ids.indexOf(keytipId) >= 0) { keytip.visible = true; } else if (keytip.overflowSetSequence && ids.indexOf(sequencesToID(mergeOverflows(keytip.keySequences, keytip.overflowSetSequence))) >= 0) { // Check if the ID with the overflow is the keytip we're looking for keytip.visible = true; } else { keytip.visible = false; } } // Apply the manager changes to the Layer state this.setKeytips(); }; /** * Callback function to use for persisted keytips * * @param overflowButtonSequences - The overflow button sequence to execute * @param keytipSequences - The keytip that should become the 'currentKeytip' when it is registered */ KeytipLayerBase.prototype._persistedKeytipExecute = function (overflowButtonSequences, keytipSequences) { // Save newCurrentKeytip for later this._newCurrentKeytipSequences = keytipSequences; // Execute the overflow button's onExecute var overflowKeytipNode = this.keytipTree.getNode(sequencesToID(overflowButtonSequences)); if (overflowKeytipNode && overflowKeytipNode.onExecute) { overflowKeytipNode.onExecute(this._getKeytipDOMElement(overflowKeytipNode.id)); } }; KeytipLayerBase.prototype._getVisibleKeytips = function (keytips) { return keytips.filter(function (keytip) { return keytip.visible; }); }; /** * Gets the ModifierKeyCodes based on the keyboard event * * @param ev - React.KeyboardEvent * @returns List of ModifierKeyCodes that were pressed */ KeytipLayerBase.prototype._getModifierKey = function (key, ev) { var modifierKeys = []; if (ev.altKey && key !== 'Alt') { modifierKeys.push(KeytipTransitionModifier.alt); } if (ev.ctrlKey && key !== 'Control') { modifierKeys.push(KeytipTransitionModifier.ctrl); } if (ev.shiftKey && key !== 'Shift') { modifierKeys.push(KeytipTransitionModifier.shift); } if (ev.metaKey && key !== 'Meta') { modifierKeys.push(KeytipTransitionModifier.meta); } return modifierKeys.length ? modifierKeys : undefined; }; /** * Trigger a keytip immediately and set it as the current keytip * * @param keytipProps - Keytip to trigger immediately */ KeytipLayerBase.prototype._triggerKeytipImmediately = function (keytipProps) { // This keytip should become the currentKeytip and should execute right away var keytipSequence = keytipProps.keySequences.slice(); if (keytipProps.overflowSetSequence) { keytipSequence = mergeOverflows(keytipSequence, keytipProps.overflowSetSequence); } // Set currentKeytip this.keytipTree.currentKeytip = this.keytipTree.getNode(sequencesToID(keytipSequence)); if (this.keytipTree.currentKeytip) { // Show all children keytips if any var children = this.keytipTree.getChildren(); if (children.length) { this.showKeytips(children); } if (this.keytipTree.currentKeytip.onExecute) { this.keytipTree.currentKeytip.onExecute(this._getKeytipDOMElement(this.keytipTree.currentKeytip.id)); } } // Unset _newCurrentKeytipSequences this._newCurrentKeytipSequences = undefined; }; KeytipLayerBase.prototype._addKeytipToQueue = function (keytipID) { var _this = this; // Add keytip this._delayedKeytipQueue.push(keytipID); // Clear timeout this._delayedQueueTimeout && this._async.clearTimeout(this._delayedQueueTimeout); // Reset timeout this._delayedQueueTimeout = this._async.setTimeout(function () { if (_this._delayedKeytipQueue.length) { _this.showKeytips(_this._delayedKeytipQueue); _this._delayedKeytipQueue = []; } }, 300); }; KeytipLayerBase.prototype._removeKeytipFromQueue = function (keytipID) { var _this = this; var index = this._delayedKeytipQueue.indexOf(keytipID); if (index >= 0) { // Remove keytip this._delayedKeytipQueue.splice(index, 1); // Clear timeout this._delayedQueueTimeout && this._async.clearTimeout(this._delayedQueueTimeout); // Reset timeout this._delayedQueueTimeout = this._async.setTimeout(function () { if (_this._delayedKeytipQueue.length) { _this.showKeytips(_this._delayedKeytipQueue); _this._delayedKeytipQueue = []; } }, 300); } }; /** * Gets the DOM element for the specified keytip * * @param keytipId - ID of the keytip to query for * @returns {HTMLElement | null} DOM element of the keytip if found */ KeytipLayerBase.prototype._getKeytipDOMElement = function (keytipId) { var dataKtpExecuteTarget = ktpTargetFromId(keytipId); return getDocument().querySelector(dataKtpExecuteTarget); }; /** * Returns T/F if the keytipProps keySequences match the currentKeytip, and the currentKeytip is in an overflow well * This will make 'keytipProps' the new currentKeytip * * @param keytipProps - Keytip props to check * @returns {boolean} - T/F if this keytip should become the currentKeytip */ KeytipLayerBase.prototype._isCurrentKeytipAnAlias = function (keytipProps) { var currKtp = this.keytipTree.currentKeytip; if (currKtp && (currKtp.overflowSetSequence || currKtp.persisted) && arraysEqual(keytipProps.keySequences, currKtp.keySequences)) { return true; } return false; }; KeytipLayerBase.defaultProps = { keytipStartSequences: [defaultStartSequence], keytipExitSequences: [defaultExitSequence], keytipReturnSequences: [defaultReturnSequence], content: '' }; return KeytipLayerBase; }(BaseComponent)); export { KeytipLayerBase }; //# sourceMappingURL=KeytipLayer.base.js.map