UNPKG

@salesforce/design-system-react

Version:

Salesforce Lightning Design System for React

235 lines (204 loc) 7.52 kB
/* Copyright (c) 2015-present, salesforce.com, inc. All rights reserved */ /* Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license */ // # Keyboard Navigable Trait // ## Dependencies // ### React import ReactDOM from 'react-dom'; // ### escapeRegExp import escapeRegExp from 'lodash.escaperegexp'; // ### isFunction import isFunction from 'lodash.isfunction'; // ### Event Helpers import KEYS from './key-code'; /* eslint-disable react/no-find-dom-node */ var noop = function noop() {}; export function KeyBuffer() { var _this = this; this.buffer = ''; return function (key) { if (_this.timeout) { clearTimeout(_this.timeout); _this.timeout = undefined; } _this.timeout = setTimeout(function () { _this.buffer = ''; }, 400); _this.buffer = _this.buffer + key; return _this.buffer; }; } export function itemIsSelectable(item) { return item.type !== 'header' && item.type !== 'divider' && !item.disabled; } export function getNavigableItems(items) { var navigableItems = []; navigableItems.indexes = []; navigableItems.keyBuffer = new KeyBuffer(); if (Array.isArray(items)) { items.forEach(function (item, index) { if (itemIsSelectable(item)) { navigableItems.push({ index: index, text: "".concat(item.label).toLowerCase() }); navigableItems.indexes.push(index); } }); } return navigableItems; } export function keyboardNavigate(_ref) { var componentContext = _ref.componentContext, currentFocusedIndex = _ref.currentFocusedIndex, isOpen = _ref.isOpen, event = _ref.event, key = _ref.key, keyCode = _ref.keyCode, navigableItems = _ref.navigableItems, onFocus = _ref.onFocus, onSelect = _ref.onSelect, target = _ref.target, toggleOpen = _ref.toggleOpen; var indexes = navigableItems.indexes; var lastIndex = indexes.length - 1; var focusedIndex; var ch = key || String.fromCharCode(keyCode); if (/^[ -~]$/.test(ch)) { ch = ch.toLowerCase(); } else { ch = null; } var openMenuKeys = keyCode === KEYS.ENTER || keyCode === KEYS.SPACE || keyCode === KEYS.UP; if (keyCode === KEYS.ESCAPE) { if (isOpen) toggleOpen(); } else if (!isOpen) { focusedIndex = indexes[0]; if (openMenuKeys || ch) { toggleOpen(); } if (openMenuKeys && componentContext.trigger && ReactDOM.findDOMNode(componentContext.trigger) === target) { // eslint-disable-line react/no-find-dom-node componentContext.handleClick(event); } } else if (keyCode === KEYS.ENTER || keyCode === KEYS.SPACE) { onSelect(currentFocusedIndex); } else { var navigableIndex = indexes.indexOf(currentFocusedIndex); if (keyCode === KEYS.DOWN) { if (navigableIndex < lastIndex) { var newNavigableIndex = navigableIndex + 1; focusedIndex = indexes[newNavigableIndex]; } else { focusedIndex = indexes[0]; } } else if (keyCode === KEYS.UP) { if (navigableIndex > 0) { var _newNavigableIndex = navigableIndex - 1; focusedIndex = indexes[_newNavigableIndex]; } else { focusedIndex = indexes[lastIndex]; } } else if (ch) { // Combine subsequent keypresses var pattern = navigableItems.keyBuffer(ch); var consecutive = 0; // Support for navigating to the next option of the same letter with repeated presses of the same key if (pattern.length > 1 && new RegExp("^[".concat(escapeRegExp(ch), "]+$")).test(pattern)) { consecutive = pattern.length; } navigableItems.forEach(function (item) { if (focusedIndex === undefined && item.text.substr(0, pattern.length) === pattern || consecutive > 0 && item.text.substr(0, 1) === ch) { consecutive -= 1; focusedIndex = item.index; } }); } } onFocus(focusedIndex); return focusedIndex; } function getMenu(componentRef) { return ReactDOM.findDOMNode(componentRef).querySelector('ul.dropdown__list'); // eslint-disable-line react/no-find-dom-node } function getMenuItem(menuItemId) { var context = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : document; var menuItem; if (menuItemId) { menuItem = context.getElementById(menuItemId); } return menuItem; } export var KeyboardNavigableMixin = { componentWillMount: function componentWillMount() { this.navigableItems = getNavigableItems(this.props.options); }, componentWillReceiveProps: function componentWillReceiveProps(nextProps) { if (nextProps.options) { this.navigableItems = getNavigableItems(nextProps.options); } }, // Handling open / close toggling is optional, and a default implementation is provided for handling focus, but selection _must_ be handled handleKeyboardNavigate: function handleKeyboardNavigate(_ref2) { var event = _ref2.event, _ref2$isOpen = _ref2.isOpen, isOpen = _ref2$isOpen === void 0 ? true : _ref2$isOpen, keyCode = _ref2.keyCode, _ref2$onFocus = _ref2.onFocus, onFocus = _ref2$onFocus === void 0 ? this.handleKeyboardFocus : _ref2$onFocus, onSelect = _ref2.onSelect, target = _ref2.target, _ref2$toggleOpen = _ref2.toggleOpen, toggleOpen = _ref2$toggleOpen === void 0 ? noop : _ref2$toggleOpen; keyboardNavigate({ componentContext: this, currentFocusedIndex: this.state.focusedIndex, event: event, isOpen: isOpen, keyCode: keyCode, navigableItems: this.navigableItems, onFocus: onFocus, onSelect: onSelect, target: target, toggleOpen: toggleOpen }); }, // This is a bit of an anti-pattern, but it has the upside of being a nice default. Component authors can always override to only set state and do their own focusing in their subcomponents. handleKeyboardFocus: function handleKeyboardFocus(focusedIndex) { if (this.state.focusedIndex !== focusedIndex) { this.setState({ focusedIndex: focusedIndex }); } var menu = isFunction(this.getMenu) ? this.getMenu() : getMenu(this); var menuItem = isFunction(this.getMenuItem) ? this.getMenuItem(focusedIndex, menu) : getMenuItem(this.getListItemId(focusedIndex)); if (menuItem) { this.focusMenuItem(menuItem); this.scrollToMenuItem(menu, menuItem); } }, getListItemId: function getListItemId(index) { var menuItemId; if (index !== undefined) { var menuId = isFunction(this.getId) ? this.getId() : this.props.id; menuItemId = "".concat(menuId, "-item-").concat(index); } return menuItemId; }, focusMenuItem: function focusMenuItem(menuItem) { menuItem.getElementsByTagName('a')[0].focus(); }, scrollToMenuItem: function scrollToMenuItem(menu, menuItem) { if (menu && menuItem) { var menuHeight = menu.offsetHeight; var menuTop = menu.scrollTop; var menuItemTop = menuItem.offsetTop - menu.offsetTop; if (menuItemTop < menuTop) { menu.scrollTop = menuItemTop; } else { var menuBottom = menuTop + menuHeight + menu.offsetTop; var menuItemBottom = menuItemTop + menuItem.offsetHeight + menu.offsetTop; if (menuItemBottom > menuBottom) { menu.scrollTop = menuItemBottom - menuHeight - menu.offsetTop; } } } } }; export default KeyboardNavigableMixin; //# sourceMappingURL=keyboard-navigable-menu.js.map