From 9b81f6efd23059c3c2792fdcd0ceca4b0c990f42 Mon Sep 17 00:00:00 2001 From: Pat Nellesen Date: Sat, 9 Jun 2018 18:00:43 -0500 Subject: [PATCH] Feature/#293 header keynav (#303) Added keydown and focus handlers for Slot Section Menus ("Core Internal", "Optional Internal", etc.) When focus is on the header, Enter key will open the menu and set focus to either the first option, or else the currently selected option, such as "Planetary Explorer" in Core Internal menu (if one has been previously selected). While menu is open, Tab and Shift-Tab will move the focus up and down as expected. Shift-tab on first option will move focus to last option in the menu, and Tab on the last option will move focus to the top. Focus will stay inside the menu until menu is closed. When focus is on a menu options, hitting the Enter key will trigger the onClick function for that option, and will set the option as the currently selected option for that menu. Esc key will close the menu and set focus to the menu header H1 element. --- src/app/components/AvailableModulesMenu.jsx | 1008 +++++++++---------- src/app/components/HardpointSlotSection.jsx | 56 +- src/app/components/InternalSlotSection.jsx | 42 +- src/app/components/ModificationsMenu.jsx | 950 +++++++++-------- src/app/components/Slider.jsx | 148 ++- src/app/components/Slot.jsx | 325 +++--- src/app/components/SlotSection.jsx | 64 +- src/app/components/StandardSlot.jsx | 320 +++--- src/app/components/StandardSlotSection.jsx | 34 +- src/app/components/UtilitySlotSection.jsx | 31 +- src/app/pages/OutfittingPage.jsx | 12 +- 11 files changed, 1540 insertions(+), 1450 deletions(-) diff --git a/src/app/components/AvailableModulesMenu.jsx b/src/app/components/AvailableModulesMenu.jsx index a31960cd..6d10c191 100644 --- a/src/app/components/AvailableModulesMenu.jsx +++ b/src/app/components/AvailableModulesMenu.jsx @@ -1,504 +1,504 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import * as ModuleUtils from '../shipyard/ModuleUtils'; -import TranslatedComponent from './TranslatedComponent'; -import { stopCtxPropagation } from '../utils/UtilityFunctions'; -import cn from 'classnames'; -import { MountFixed, MountGimballed, MountTurret } from './SvgIcons'; - -const PRESS_THRESHOLD = 500; // mouse/touch down threshold - -/* - * Categorisation of module groups - */ -const GRPCAT = { - 'sg': 'shields', - 'bsg': 'shields', - 'psg': 'shields', - 'scb': 'shields', - 'cc': 'limpet controllers', - 'fx': 'limpet controllers', - 'hb': 'limpet controllers', - 'pc': 'limpet controllers', - 'rpl': 'limpet controllers', - 'pce': 'passenger cabins', - 'pci': 'passenger cabins', - 'pcm': 'passenger cabins', - 'pcq': 'passenger cabins', - 'fh': 'hangars', - 'pv': 'hangars', - 'fs': 'fuel', - 'ft': 'fuel', - 'hr': 'structural reinforcement', - 'mrp': 'structural reinforcement', - 'bl': 'lasers', - 'pl': 'lasers', - 'ul': 'lasers', - 'ml': 'lasers', - 'c': 'projectiles', - 'mc': 'projectiles', - 'axmc': 'experimental', - 'fc': 'projectiles', - 'rfl': 'experimental', - 'pa': 'projectiles', - 'rg': 'projectiles', - 'mr': 'ordnance', - 'axmr': 'experimental', - 'rcpl': 'experimental', - 'tp': 'ordnance', - 'nl': 'ordnance', - 'sc': 'scanners', - 'ss': 'scanners', - // Utilities - 'cs': 'scanners', - 'kw': 'scanners', - 'ws': 'scanners', - 'xs': 'scanners', - 'ch': 'defence', - 'po': 'defence', - 'ec': 'defence', - 'sfn': 'defence', - // Standard - 'gpp': 'guardian', - 'gpc': 'guardian', - 'ggc': 'guardian' -}; -// Order here is the order in which items will be shown in the modules menu -const CATEGORIES = { - // Internals - 'am': ['am'], - 'cr': ['cr'], - 'fi': ['fi'], - 'fuel': ['ft', 'fs'], - 'hangars': ['fh', 'pv'], - 'limpet controllers': ['cc', 'fx', 'hb', 'pc', 'rpl'], - 'passenger cabins': ['pce', 'pci', 'pcm', 'pcq'], - 'rf': ['rf'], - 'shields': ['sg', 'bsg', 'psg', 'scb'], - 'structural reinforcement': ['hr', 'mrp'], - 'dc': ['dc'], - // Hardpoints - 'lasers': ['pl', 'ul', 'bl', 'ml'], - 'projectiles': ['mc', 'c', 'fc', 'pa', 'rg'], - 'ordnance': ['mr', 'tp', 'nl'], - // Utilities - 'sb': ['sb'], - 'hs': ['hs'], - 'defence': ['ch', 'po', 'ec'], - 'scanners': ['sc', 'ss', 'cs', 'kw', 'ws'], // Overloaded with internal scanners - // Experimental - 'experimental': ['axmc', 'axmr', 'rfl', 'xs', 'sfn', 'rcpl'], - - // Guardian - 'guardian': ['gpp', 'gpc', 'ggc'] -}; - -/** - * Available modules menu - */ -export default class AvailableModulesMenu extends TranslatedComponent { - - static propTypes = { - modules: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired, - onSelect: PropTypes.func.isRequired, - diffDetails: PropTypes.func, - m: PropTypes.object, - shipMass: PropTypes.number, - warning: PropTypes.func, - firstSlotId: PropTypes.string, - lastSlotId: PropTypes.string, - activeSlotId: PropTypes.string, - slotDiv: PropTypes.object - }; - - static defaultProps = { - shipMass: 0 - }; - - /** - * Constructor - * @param {Object} props React Component properties - * @param {Object} context React Component context - */ - constructor(props, context) { - super(props); - this._hideDiff = this._hideDiff.bind(this); - this.state = this._initState(props, context); - this.slotItems = [];// Array to hold
  • refs. - } - - /** - * Initiate the list of available moduels - * @param {Object} props React Component properties - * @param {Object} context React Component context - * @return {Object} list: Array of React Components, currentGroup Component if any - */ - _initState(props, context) { - let translate = context.language.translate; - let { m, warning, shipMass, onSelect, modules, firstSlotId, lastSlotId } = props; - let list, currentGroup; - - let buildGroup = this._buildGroup.bind( - this, - translate, - m, - warning, - shipMass - (m && m.mass ? m.mass : 0), - (m, event) => { - this._hideDiff(event); - onSelect(m); - } - ); - - if (modules instanceof Array) { - list = buildGroup(modules[0].grp, modules); - } else { - list = []; - // At present time slots with grouped options (Hardpoints and Internal) can be empty - if (m) { - let emptyId = 'empty'; - if(this.firstSlotId == null) this.firstSlotId = emptyId; - let keyDown = this._keyDown.bind(this, onSelect); - list.push(
    this.slotItems[emptyId] = slotItem} >{translate('empty')}
    ); - } - - // Need to regroup the modules by our own categorisation - let catmodules = {}; - // Pre-create to preserve ordering - for (let cat in CATEGORIES) { - catmodules[cat] = []; - } - for (let g in modules) { - const moduleCategory = GRPCAT[g] || g; - const existing = catmodules[moduleCategory] || []; - catmodules[moduleCategory] = existing.concat(modules[g]); - } - - for (let category in catmodules) { - let categoryHeader = false; - // Order through CATEGORIES if present - const categories = CATEGORIES[category] || [category]; - if (categories && categories.length) { - for (let n in categories) { - const grp = categories[n]; - // We now have the group and the category. We might not have any modules, though... - if (modules[grp]) { - // Decide if we need a category header as well as a group header - if (categories.length === 1) { - // Show category header instead of group header - if (m && grp == m.grp) { - list.push(
    this.groupElem = elem} key={category} className={'select-category upp'}>{translate(category)}
    ); - } else { - list.push(
    {translate(category)}
    ); - } - } else { - // Show category header as well as group header - if (!categoryHeader) { - list.push(
    {translate(category)}
    ); - categoryHeader = true; - } - if (m && grp == m.grp) { - list.push(
    this.groupElem = elem} key={grp} className={'select-group cap'}>{translate(grp)}
    ); - } else { - list.push(
    {translate(grp)}
    ); - } - } - list.push(buildGroup(grp, modules[grp])); - } - } - } - } - } - let trackingFocus = false; - return { list, currentGroup, trackingFocus }; - } - - /** - * Generate React Components for Module Group - * @param {Function} translate Translate function - * @param {Object} mountedModule Mounted Module - * @param {Function} warningFunc Warning function - * @param {number} mass Mass - * @param {function} onSelect Select/Mount callback - * @param {string} grp Group name - * @param {Array} modules Available modules - * @return {*} Available Module Group contents - * @param {*} firstSlotId ID of the first slot - * @param {*} lastSlotId ID of the last slot - */ - _buildGroup(translate, mountedModule, warningFunc, mass, onSelect, grp, modules, firstSlotId, lastSlotId) { - let prevClass = null, prevRating = null, prevName; - let elems = []; - - const sortedModules = modules.sort(this._moduleOrder); - - - // Calculate the number of items per class. Used so we don't have long lists with only a few items in each row - const tmp = sortedModules.map((v, i) => v['class']).reduce((count, cls) => { count[cls] = ++count[cls] || 1; return count; }, {}); - const itemsPerClass = Math.max.apply(null, Object.keys(tmp).map(key => tmp[key])); - - let itemsOnThisRow = 0; - for (let i = 0; i < sortedModules.length; i++) { - let m = sortedModules[i]; - let mount = null; - let disabled = false; - prevName = m.name; - if (ModuleUtils.isShieldGenerator(m.grp)) { - // Shield generators care about maximum hull mass - disabled = mass > m.maxmass; - } else if (m.maxmass) { - // Thrusters care about total mass - disabled = mass + m.mass > m.maxmass; - } - let active = mountedModule && mountedModule.id === m.id; - let classes = cn(m.name ? 'lc' : 'c', { - warning: !disabled && warningFunc && warningFunc(m), - active, - disabled - }); - let eventHandlers; - - if (disabled) { - eventHandlers = { - onKeyDown: this._keyDown.bind(this, null), - onKeyUp: this._keyUp.bind(this, null) - - }; - } else { - /** - * Get the ids of the first and last
  • elements in the