UNPKG

office-ui-fabric-react

Version:

Reusable React components for building experiences for Microsoft 365.

576 lines • 27.8 kB
import { __assign, __extends, __spreadArrays } from "tslib"; import * as React from 'react'; import { getLayerStyles } from './KeytipLayer.styles'; import { Keytip } from '../../Keytip'; import { Layer } from '../../Layer'; import { classNamesFunction, getDocument, arraysEqual, warn, isMac, EventGroup, Async, initializeComponentRef, KeyCodes, } from '../../Utilities'; import { KeytipManager } from '../../utilities/keytips/KeytipManager'; import { KeytipTree } from './KeytipTree'; import { ktpTargetFromId, ktpTargetFromSequences, sequencesToID, mergeOverflows, } from '../../utilities/keytips/KeytipUtils'; import { transitionKeysContain, } from '../../utilities/keytips/IKeytipTransitionKey'; import { KeytipEvents, KTP_LAYER_ID, KTP_ARIA_SEPARATOR } from '../../utilities/keytips/KeytipConstants'; // Default sequence is Alt-Windows (Alt-Meta) in Windows, Option-Control (Alt-Control) in Mac var defaultStartSequence = { key: isMac() ? 'Control' : 'Meta', modifierKeys: [KeyCodes.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 * {@docCategory Keytips} */ var KeytipLayerBase = /** @class */ (function (_super) { __extends(KeytipLayerBase, _super); 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(ev); } }; _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 '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); } 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, ev); break; } }; _this._onKeyPress = function (ev) { if (_this.state.inKeytipMode && !_this._keyHandled) { // Call processInput _this.processInput(ev.key.toLocaleLowerCase(), ev); ev.preventDefault(); ev.stopPropagation(); } }; _this._onKeytipAdded = function (eventArgs) { var _a; 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)) { // Ensure existing children are still shown. _this._delayedKeytipQueue = _this._delayedKeytipQueue.concat(((_a = _this._keytipTree.currentKeytip) === null || _a === void 0 ? void 0 : _a.children) || []); _this._addKeytipToQueue(sequencesToID(keytipProps.keySequences)); // Ensure the child of currentKeytip is successfully added to currentKeytip's children and update it if not. // Note: Added this condition because KeytipTree.addNode was not always reflecting updates made to a parent node // in currentKeytip when that parent is the currentKeytip. if (_this._keytipTree.currentKeytip && _this._keytipTree.currentKeytip.hasDynamicChildren && _this._keytipTree.currentKeytip.children.indexOf(keytipProps.id) < 0) { var currNode = _this._keytipTree.getNode(_this._keytipTree.currentKeytip.id); if (currNode) { _this._keytipTree.currentKeytip = currNode; } } } _this._persistedKeytipChecks(keytipProps); }; _this._onKeytipUpdated = function (eventArgs) { var _a; var keytipProps = eventArgs.keytip; var uniqueID = eventArgs.uniqueID; _this._keytipTree.updateNode(keytipProps, uniqueID); _this._setKeytips(); if (_this._keytipTree.isCurrentKeytipParent(keytipProps)) { // Ensure existing children are still shown. _this._delayedKeytipQueue = _this._delayedKeytipQueue.concat(((_a = _this._keytipTree.currentKeytip) === null || _a === void 0 ? void 0 : _a.children) || []); _this._addKeytipToQueue(sequencesToID(keytipProps.keySequences)); } _this._persistedKeytipChecks(keytipProps); }; /** * Helper function to do checks related to persisted/overflow keytips * Done on keytip added and keytip updated * * @param keytipProps - Keytip props */ _this._persistedKeytipChecks = function (keytipProps) { 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._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; }; /** * Emits a warning if duplicate keytips are found for the children of the current keytip */ _this._warnIfDuplicateKeytips = function () { var duplicateKeytips = _this._getDuplicateIds(_this._keytipTree.getChildren()); if (duplicateKeytips.length) { warn('Duplicate keytips found for ' + duplicateKeytips.join(', ')); } }; /** * Returns duplicates among keytip IDs. * If the returned array is empty, no duplicates were found. * * @param keytipIds - Array of keytip IDs to find duplicates for * @returns - Array of duplicates that were found. Each duplicate will only be added once to this array. */ _this._getDuplicateIds = function (keytipIds) { var seenIds = {}; return keytipIds.filter(function (keytipId) { seenIds[keytipId] = seenIds[keytipId] ? seenIds[keytipId] + 1 : 1; // Only add the first duplicate keytip seen return seenIds[keytipId] === 2; }); }; initializeComponentRef(_this); _this._events = new EventGroup(_this); _this._async = new Async(_this); var managerKeytips = __spreadArrays(_this._keytipManager.getKeytips()); _this.state = { inKeytipMode: false, // Get the initial set of keytips keytips: managerKeytips, visibleKeytips: _this._getVisibleKeytips(managerKeytips), }; _this._buildTree(); _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; } KeytipLayerBase.prototype.render = function () { var _this = this; var _a = this.props, content = _a.content, styles = _a.styles; var _b = this.state, keytips = _b.keytips, visibleKeytips = _b.visibleKeytips; this._classNames = getClassNames(styles, {}); return (React.createElement(Layer, { styles: 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(KTP_ARIA_SEPARATOR))); }), visibleKeytips && visibleKeytips.map(function (visibleKeytipProps) { return React.createElement(Keytip, __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, 'pointerup', 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 */); // Add keytip listeners this._events.on(this._keytipManager, KeytipEvents.ENTER_KEYTIP_MODE, this._enterKeytipMode); this._events.on(this._keytipManager, KeytipEvents.EXIT_KEYTIP_MODE, this._exitKeytipMode); }; KeytipLayerBase.prototype.componentWillUnmount = function () { this._async.dispose(); this._events.dispose(); }; // The below public functions are only public for testing purposes // They are not intended to be used in app code by using a KeytipLayer reference KeytipLayerBase.prototype.getCurrentSequence = function () { return this._currentSequence; }; KeytipLayerBase.prototype.getKeytipTree = function () { return this._keytipTree; }; /** * Processes an IKeytipTransitionKey entered by the user * * @param transitionKey - IKeytipTransitionKey received by the layer to process */ KeytipLayerBase.prototype.processTransitionInput = function (transitionKey, ev) { 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(ev); } 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(ev); } else { // If this keytip has a onReturn prop, we execute the func. if (currKtp.onReturn) { currKtp.onReturn(this._getKtpExecuteTarget(currKtp), this._getKtpTarget(currKtp)); } // 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()); this._warnIfDuplicateKeytips(); } } } 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(); this._warnIfDuplicateKeytips(); } }; /** * Processes inputs from the document listener and traverse the keytip tree * * @param key - Key pressed by the user */ KeytipLayerBase.prototype.processInput = function (key, ev) { // 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._getKtpExecuteTarget(currKtp), this._getKtpTarget(currKtp)); // 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(ev); } else { // Show all children keytips this.showKeytips(currKtpChildren); this._warnIfDuplicateKeytips(); } // 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 (keytip.overflowSetSequence) { // Check if the ID with the overflow is the keytip we're looking for keytipId = sequencesToID(mergeOverflows(keytip.keySequences, keytip.overflowSetSequence)); } if (ids.indexOf(keytipId) >= 0) { keytip.visible = true; } else { keytip.visible = false; } } // Apply the manager changes to the Layer state this._setKeytips(); }; /** * Enters keytip mode for this layer */ KeytipLayerBase.prototype._enterKeytipMode = function () { if (this._keytipManager.shouldEnterKeytipMode) { if (this._keytipManager.delayUpdatingKeytipChange) { this._buildTree(); this._setKeytips(); } 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(); } } }; KeytipLayerBase.prototype._buildTree = function () { this._keytipTree = new KeytipTree(); // Add regular and persisted keytips to the tree for (var _i = 0, _a = Object.keys(this._keytipManager.keytips); _i < _a.length; _i++) { var id = _a[_i]; var uniqueKeytip = this._keytipManager.keytips[id]; this._keytipTree.addNode(uniqueKeytip.keytip, uniqueKeytip.uniqueID); } for (var _b = 0, _c = Object.keys(this._keytipManager.persistedKeytips); _b < _c.length; _b++) { var id = _c[_b]; var uniqueKeytip = this._keytipManager.persistedKeytips[id]; this._keytipTree.addNode(uniqueKeytip.keytip, uniqueKeytip.uniqueID); } }; /** * Exits keytip mode for this layer */ KeytipLayerBase.prototype._exitKeytipMode = function (ev) { 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(ev); } }; /** * 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) }); }; /** * 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._getKtpExecuteTarget(overflowKeytipNode), this._getKtpTarget(overflowKeytipNode)); } }; KeytipLayerBase.prototype._getVisibleKeytips = function (keytips) { // Filter out non-visible keytips and duplicates var seenIds = {}; return keytips.filter(function (keytip) { var keytipId = sequencesToID(keytip.keySequences); if (keytip.overflowSetSequence) { // Account for overflow set sequences when checking for duplicates keytipId = sequencesToID(mergeOverflows(keytip.keySequences, keytip.overflowSetSequence)); } seenIds[keytipId] = seenIds[keytipId] ? seenIds[keytipId] + 1 : 1; return keytip.visible && seenIds[keytipId] === 1; }); }; /** * 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(KeyCodes.alt); } if (ev.ctrlKey && key !== 'Control') { modifierKeys.push(KeyCodes.ctrl); } if (ev.shiftKey && key !== 'Shift') { modifierKeys.push(KeyCodes.shift); } if (ev.metaKey && key !== 'Meta') { modifierKeys.push(KeyCodes.leftWindow); } 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 = __spreadArrays(keytipProps.keySequences); 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._getKtpExecuteTarget(this._keytipTree.currentKeytip), this._getKtpTarget(this._keytipTree.currentKeytip)); } } // Unset _newCurrKtpSequences 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); } }; KeytipLayerBase.prototype._getKtpExecuteTarget = function (currKtp) { return getDocument().querySelector(ktpTargetFromId(currKtp.id)); }; KeytipLayerBase.prototype._getKtpTarget = function (currKtp) { return getDocument().querySelector(ktpTargetFromSequences(currKtp.keySequences)); }; /** * 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 - 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; }(React.Component)); export { KeytipLayerBase }; //# sourceMappingURL=KeytipLayer.base.js.map