From 6d23e245bdac34b2148460e15ebf0727599be005 Mon Sep 17 00:00:00 2001 From: Johan Lahti Date: Fri, 11 Mar 2022 14:27:54 +0100 Subject: [PATCH] refactor(Listbox): Keyboard navigation (#778) * fix: trim space when parsing engine URLs * refactor: add support for keyboard navigation * refactor: move key handling to own file test: cover new functionality with unit tests * refactor: rename and fix focus * refactor: improve filling the range * fix: adaptations to merge conflicts * refactor: restore style overrides for focus --- .../src/components/listbox/ListBox.jsx | 11 +- .../components/listbox/ListBoxRowColumn.jsx | 13 +- .../list-box-keyboard-navigation.spec.js | 142 +++++++++++++++ .../__tests__/list-box-row-column.spec.jsx | 172 ++++++++++++------ .../use-selections-interactions.spec.jsx | 21 ++- .../listbox/listbox-keyboard-navigation.js | 65 +++++++ .../listbox/useSelectionsInteractions.js | 9 + apis/nucleus/src/keys.js | 34 ++++ 8 files changed, 401 insertions(+), 66 deletions(-) create mode 100644 apis/nucleus/src/components/listbox/__tests__/list-box-keyboard-navigation.spec.js create mode 100644 apis/nucleus/src/components/listbox/listbox-keyboard-navigation.js create mode 100644 apis/nucleus/src/keys.js diff --git a/apis/nucleus/src/components/listbox/ListBox.jsx b/apis/nucleus/src/components/listbox/ListBox.jsx index 76689a93e..341ef9965 100644 --- a/apis/nucleus/src/components/listbox/ListBox.jsx +++ b/apis/nucleus/src/components/listbox/ListBox.jsx @@ -43,7 +43,11 @@ export default function ListBox({ const [layout] = useLayout(model); const [pages, setPages] = useState(null); const [isLoadingData, setIsLoadingData] = useState(false); - const { instantPages = [], interactionEvents } = useSelectionsInteractions({ + const { + instantPages = [], + interactionEvents, + select, + } = useSelectionsInteractions({ layout, selections, pages, @@ -186,6 +190,11 @@ export default function ListBox({ checkboxes, dense, frequencyMode, + actions: { + select, + confirm: () => selections && selections.confirm.call(selections), + cancel: () => selections && selections.cancel.call(selections), + }, frequencyMax, histogram, }} diff --git a/apis/nucleus/src/components/listbox/ListBoxRowColumn.jsx b/apis/nucleus/src/components/listbox/ListBoxRowColumn.jsx index ee7d4736a..3ff799b4c 100644 --- a/apis/nucleus/src/components/listbox/ListBoxRowColumn.jsx +++ b/apis/nucleus/src/components/listbox/ListBoxRowColumn.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { FormControlLabel, Grid, Typography } from '@material-ui/core'; @@ -8,6 +8,7 @@ import Lock from '@nebula.js/ui/icons/lock'; import Tick from '@nebula.js/ui/icons/tick'; import ListBoxCheckbox from './ListBoxCheckbox'; import getSegmentsFromRanges from './listbox-highlight'; +import getKeyboardNavigation from './listbox-keyboard-navigation'; const ellipsis = { width: '100%', @@ -35,6 +36,9 @@ const useStyles = makeStyles((theme) => ({ '&:focus': { boxShadow: `inset 0 0 0 2px ${theme.palette.custom.focusBorder} !important`, }, + '&:focus-visible': { + outline: 'none', + }, }, // The interior wrapper for all field content. @@ -153,10 +157,13 @@ export default function RowColumn({ index, style, data, column = false }) { checkboxes = false, dense = false, frequencyMode = 'N', + actions, frequencyMax = '', histogram = false, } = data; + const handleKeyDownCallback = useCallback(getKeyboardNavigation(actions), [actions]); + const [isSelected, setSelected] = useState(false); const [cell, setCell] = useState(); @@ -295,11 +302,15 @@ export default function RowColumn({ index, style, data, column = false }) { container spacing={0} className={joinClassNames(['value', ...classArr])} + classes={{ + root: classes.fieldRoot, + }} style={style} onClick={onClick} onMouseDown={onMouseDown} onMouseUp={onMouseUp} onMouseEnter={onMouseEnter} + onKeyDown={handleKeyDownCallback} role={column ? 'column' : 'row'} tabIndex={0} data-n={cell && cell.qElemNumber} diff --git a/apis/nucleus/src/components/listbox/__tests__/list-box-keyboard-navigation.spec.js b/apis/nucleus/src/components/listbox/__tests__/list-box-keyboard-navigation.spec.js new file mode 100644 index 000000000..351d76411 --- /dev/null +++ b/apis/nucleus/src/components/listbox/__tests__/list-box-keyboard-navigation.spec.js @@ -0,0 +1,142 @@ +import getKeyboardNavigation from '../listbox-keyboard-navigation'; + +describe('keyboard navigation', () => { + let actions; + let sandbox; + let handleKeyDown; + + before(() => { + global.document = {}; + sandbox = sinon.createSandbox(); + actions = { + select: sandbox.stub(), + cancel: sandbox.stub(), + confirm: sandbox.stub(), + }; + }); + + afterEach(() => { + sandbox.reset(); + }); + + after(() => { + sandbox.restore(); + }); + + beforeEach(() => { + handleKeyDown = getKeyboardNavigation(actions); + }); + + it('select values with Space', () => { + expect(actions.select).not.called; + expect(actions.confirm).not.called; + expect(actions.cancel).not.called; + + // Space should select values + const event = { + nativeEvent: { keyCode: 32 }, + currentTarget: { getAttribute: sandbox.stub().withArgs('data-n').returns(1) }, + preventDefault: sandbox.stub(), + stopPropagation: sandbox.stub(), + }; + handleKeyDown(event); + expect(actions.select).calledOnce.calledWithExactly([1]); + expect(event.preventDefault).calledOnce; + expect(event.stopPropagation).not.called; + }); + + it('confirm selections with Enter', () => { + const eventConfirm = { + nativeEvent: { keyCode: 13 }, + preventDefault: sandbox.stub(), + }; + handleKeyDown(eventConfirm); + expect(actions.confirm).calledOnce; + }); + + it('cancel selections with Escape', () => { + const eventCancel = { + nativeEvent: { keyCode: 27 }, + preventDefault: sandbox.stub(), + }; + const blur = sandbox.stub(); + global.document.activeElement = { blur }; + handleKeyDown(eventCancel); + expect(actions.cancel).calledOnce; + expect(blur).calledOnce; + }); + + it('arrow up should move focus upwards', () => { + const focus = sandbox.stub(); + const eventArrowUp = { + nativeEvent: { keyCode: 38 }, + currentTarget: { + parentElement: { + previousElementSibling: { + querySelector: () => ({ focus }), + }, + }, + }, + preventDefault: sandbox.stub(), + }; + handleKeyDown(eventArrowUp); + expect(focus).calledOnce; + }); + + it('arrow down should move focus downwards', () => { + const focus = sandbox.stub(); + const eventArrowDown = { + nativeEvent: { keyCode: 40 }, + currentTarget: { + parentElement: { + nextElementSibling: { + querySelector: () => ({ focus }), + }, + }, + }, + preventDefault: sandbox.stub(), + }; + handleKeyDown(eventArrowDown); + expect(focus).calledOnce; + }); + + it('arrow down with Shift should range select values downwards (current and next element)', () => { + const focus = sandbox.stub(); + expect(actions.select).not.called; + const eventArrowDown = { + nativeEvent: { keyCode: 40, shiftKey: true }, + currentTarget: { + parentElement: { + nextElementSibling: { + querySelector: () => ({ focus, getAttribute: sandbox.stub().withArgs('data-n').returns(2) }), + }, + }, + getAttribute: sandbox.stub().withArgs('data-n').returns(1), + }, + preventDefault: sandbox.stub(), + }; + handleKeyDown(eventArrowDown); + expect(focus).calledOnce; + expect(actions.select).calledOnce.calledWithExactly([2], true); + }); + + it('arrow up with Shift should range select values upwards (current and previous element)', () => { + const focus = sandbox.stub(); + expect(actions.select).not.called; + const eventArrowUp = { + nativeEvent: { keyCode: 38, shiftKey: true }, + currentTarget: { + getAttribute: sandbox.stub().withArgs('data-n').returns(2), + parentElement: { + previousElementSibling: { + querySelector: () => ({ focus, getAttribute: sandbox.stub().withArgs('data-n').returns(1) }), + }, + }, + }, + preventDefault: sandbox.stub(), + }; + handleKeyDown(eventArrowUp); + expect(focus).calledOnce; + expect(actions.select).calledOnce.calledWithExactly([1], true); + }); +}); diff --git a/apis/nucleus/src/components/listbox/__tests__/list-box-row-column.spec.jsx b/apis/nucleus/src/components/listbox/__tests__/list-box-row-column.spec.jsx index c6fde83dd..75f6b147a 100644 --- a/apis/nucleus/src/components/listbox/__tests__/list-box-row-column.spec.jsx +++ b/apis/nucleus/src/components/listbox/__tests__/list-box-row-column.spec.jsx @@ -3,6 +3,7 @@ import renderer from 'react-test-renderer'; import { Grid, Typography } from '@material-ui/core'; import Lock from '@nebula.js/ui/icons/lock'; import ListBoxCheckbox from '../ListBoxCheckbox'; +import * as getKeyboardNavigation from '../listbox-keyboard-navigation'; const [{ default: ListBoxRowColumn }] = aw.mock( [ @@ -31,6 +32,25 @@ async function render(content) { } describe('', () => { + let sandbox; + let actions; + let handleKeyDown; + + before(() => { + global.document = {}; + sandbox = sinon.createSandbox(); + handleKeyDown = sandbox.stub(getKeyboardNavigation, 'default').returns(() => 'handle-key-down-callback'); + actions = 'actions'; + }); + + afterEach(() => { + sandbox.reset(); + }); + + after(() => { + sandbox.restore(); + }); + describe('as row', () => { const rowCol = 'row'; @@ -38,12 +58,14 @@ describe('', () => { const index = 0; const style = {}; const data = { - onMouseDown: sinon.spy(), - onMouseUp: sinon.spy(), - onMouseEnter: sinon.spy(), - onClick: sinon.spy(), + onMouseDown: sandbox.spy(), + onMouseUp: sandbox.spy(), + onMouseEnter: sandbox.spy(), + onClick: sandbox.spy(), pages: [], + actions, }; + expect(handleKeyDown).not.called; const testRenderer = await render( ); @@ -54,6 +76,7 @@ describe('', () => { expect(type.props.spacing).to.equal(0); expect(type.props.style).to.deep.equal({}); expect(type.props.role).to.equal(rowCol); + expect(type.props.onKeyDown).to.be.a('function'); expect(type.props.onMouseDown.callCount).to.equal(0); expect(type.props.onMouseUp.callCount).to.equal(0); expect(type.props.onMouseEnter.callCount).to.equal(0); @@ -67,16 +90,20 @@ describe('', () => { const cbs = testInstance.findAllByType(ListBoxCheckbox); expect(cbs).to.have.length(0); await testRenderer.unmount(); + + expect(handleKeyDown).calledOnce.calledWith('actions'); }); + it('should have css class `value`', async () => { const index = 0; const style = {}; const data = { - onMouseDown: sinon.spy(), - onMouseUp: sinon.spy(), - onMouseEnter: sinon.spy(), - onClick: sinon.spy(), + onMouseDown: sandbox.spy(), + onMouseUp: sandbox.spy(), + onMouseEnter: sandbox.spy(), + onClick: sandbox.spy(), pages: [], + actions, }; const testRenderer = await render( @@ -89,16 +116,18 @@ describe('', () => { expect(className.split(' ')).to.include('value'); await testRenderer.unmount(); }); + it('should render with checkboxes', async () => { const index = 0; const style = {}; const data = { checkboxes: true, - onMouseDown: sinon.spy(), - onMouseUp: sinon.spy(), - onMouseEnter: sinon.spy(), - onClick: sinon.spy(), + onMouseDown: sandbox.spy(), + onMouseUp: sandbox.spy(), + onMouseEnter: sandbox.spy(), + onClick: sandbox.spy(), pages: [], + actions, }; const testRenderer = await render( @@ -130,10 +159,11 @@ describe('', () => { const style = {}; const data = { isLocked: true, - onMouseDown: sinon.spy(), - onMouseUp: sinon.spy(), - onMouseEnter: sinon.spy(), - onClick: sinon.spy(), + onMouseDown: sandbox.spy(), + onMouseUp: sandbox.spy(), + onMouseEnter: sandbox.spy(), + onClick: sandbox.spy(), + actions, pages: [ { qArea: { @@ -163,14 +193,16 @@ describe('', () => { expect(type.props.size).to.equal('small'); await testRenderer.unmount(); }); + it('should set selected', async () => { const index = 0; const style = {}; const data = { - onMouseDown: sinon.spy(), - onMouseUp: sinon.spy(), - onMouseEnter: sinon.spy(), - onClick: sinon.spy(), + onMouseDown: sandbox.spy(), + onMouseUp: sandbox.spy(), + onMouseEnter: sandbox.spy(), + onClick: sandbox.spy(), + actions, pages: [ { qArea: { @@ -197,14 +229,16 @@ describe('', () => { expect(type.props.className).to.include('selected'); await testRenderer.unmount(); }); + it('should set alternative', async () => { const index = 0; const style = {}; const data = { - onMouseDown: sinon.spy(), - onMouseUp: sinon.spy(), - onMouseEnter: sinon.spy(), - onClick: sinon.spy(), + onMouseDown: sandbox.spy(), + onMouseUp: sandbox.spy(), + onMouseEnter: sandbox.spy(), + onClick: sandbox.spy(), + actions, pages: [ { qArea: { @@ -231,14 +265,16 @@ describe('', () => { expect(type.props.className).to.include('alternative'); await testRenderer.unmount(); }); + it('should set excluded - qState X', async () => { const index = 0; const style = {}; const data = { - onMouseDown: sinon.spy(), - onMouseUp: sinon.spy(), - onMouseEnter: sinon.spy(), - onClick: sinon.spy(), + onMouseDown: sandbox.spy(), + onMouseUp: sandbox.spy(), + onMouseEnter: sandbox.spy(), + onClick: sandbox.spy(), + actions, pages: [ { qArea: { @@ -265,14 +301,16 @@ describe('', () => { expect(type.props.className).to.include('excluded'); await testRenderer.unmount(); }); + it('should set excluded - qState XS', async () => { const index = 0; const style = {}; const data = { - onMouseDown: sinon.spy(), - onMouseUp: sinon.spy(), - onMouseEnter: sinon.spy(), - onClick: sinon.spy(), + onMouseDown: sandbox.spy(), + onMouseUp: sandbox.spy(), + onMouseEnter: sandbox.spy(), + onClick: sandbox.spy(), + actions, pages: [ { qArea: { @@ -299,14 +337,16 @@ describe('', () => { expect(type.props.className).to.include('excluded'); await testRenderer.unmount(); }); + it('should set excluded - qState XL', async () => { const index = 0; const style = {}; const data = { - onMouseDown: sinon.spy(), - onMouseUp: sinon.spy(), - onMouseEnter: sinon.spy(), - onClick: sinon.spy(), + onMouseDown: sandbox.spy(), + onMouseUp: sandbox.spy(), + onMouseEnter: sandbox.spy(), + onClick: sandbox.spy(), + actions, pages: [ { qArea: { @@ -333,14 +373,16 @@ describe('', () => { expect(type.props.className).to.include('excluded'); await testRenderer.unmount(); }); + it('should highlight ranges', async () => { const index = 0; const style = {}; const data = { - onMouseDown: sinon.spy(), - onMouseUp: sinon.spy(), - onMouseEnter: sinon.spy(), - onClick: sinon.spy(), + onMouseDown: sandbox.spy(), + onMouseUp: sandbox.spy(), + onMouseEnter: sandbox.spy(), + onClick: sandbox.spy(), + actions, pages: [ { qArea: { @@ -373,14 +415,16 @@ describe('', () => { expect(types[1].props.children.props.children).to.equal(' ftw'); await testRenderer.unmount(); }); + it('should highlight ranges', async () => { const index = 0; const style = {}; const data = { - onMouseDown: sinon.spy(), - onMouseUp: sinon.spy(), - onMouseEnter: sinon.spy(), - onClick: sinon.spy(), + onMouseDown: sandbox.spy(), + onMouseUp: sandbox.spy(), + onMouseEnter: sandbox.spy(), + onClick: sandbox.spy(), + actions, pages: [ { qArea: { @@ -415,14 +459,16 @@ describe('', () => { expect(hits).to.have.length(2); await testRenderer.unmount(); }); + it('should highlight ranges', async () => { const index = 0; const style = {}; const data = { - onMouseDown: sinon.spy(), - onMouseUp: sinon.spy(), - onMouseEnter: sinon.spy(), - onClick: sinon.spy(), + onMouseDown: sandbox.spy(), + onMouseUp: sandbox.spy(), + onMouseEnter: sandbox.spy(), + onClick: sandbox.spy(), + actions, pages: [ { qArea: { @@ -456,14 +502,16 @@ describe('', () => { expect(types[2].props.children.props.children).to.equal(' buddy'); await testRenderer.unmount(); }); + it('should show frequency when enabled', async () => { const index = 0; const style = {}; const data = { - onMouseDown: sinon.spy(), - onMouseUp: sinon.spy(), - onMouseEnter: sinon.spy(), - onClick: sinon.spy(), + onMouseDown: sandbox.spy(), + onMouseUp: sandbox.spy(), + onMouseEnter: sandbox.spy(), + onClick: sandbox.spy(), + actions, frequencyMode: 'value', pages: [ { @@ -497,6 +545,7 @@ describe('', () => { const style = {}; const data = { checkboxes: true, + actions, pages: [ { qArea: { @@ -541,11 +590,12 @@ describe('', () => { const index = 0; const style = {}; const data = { - onMouseDown: sinon.spy(), - onMouseUp: sinon.spy(), - onMouseEnter: sinon.spy(), - onClick: sinon.spy(), + onMouseDown: sandbox.spy(), + onMouseUp: sandbox.spy(), + onMouseEnter: sandbox.spy(), + onClick: sandbox.spy(), pages: [], + actions, }; const testRenderer = await render( @@ -571,15 +621,17 @@ describe('', () => { expect(cbs).to.have.length(0); await testRenderer.unmount(); }); + it('should have css class `value`', async () => { const index = 0; const style = {}; const data = { - onMouseDown: sinon.spy(), - onMouseUp: sinon.spy(), - onMouseEnter: sinon.spy(), - onClick: sinon.spy(), + onMouseDown: sandbox.spy(), + onMouseUp: sandbox.spy(), + onMouseEnter: sandbox.spy(), + onClick: sandbox.spy(), pages: [], + actions, }; const testRenderer = await render( diff --git a/apis/nucleus/src/components/listbox/__tests__/use-selections-interactions.spec.jsx b/apis/nucleus/src/components/listbox/__tests__/use-selections-interactions.spec.jsx index 1b4e9fe1c..901a6ab4b 100644 --- a/apis/nucleus/src/components/listbox/__tests__/use-selections-interactions.spec.jsx +++ b/apis/nucleus/src/components/listbox/__tests__/use-selections-interactions.spec.jsx @@ -88,26 +88,39 @@ describe('use-listbox-interactions', () => { it('Without range', async () => { await render(); const arg0 = ref.current.result; - expect(Object.keys(arg0).sort()).to.deep.equal(['instantPages', 'interactionEvents']); + expect(Object.keys(arg0).sort()).to.deep.equal(['instantPages', 'interactionEvents', 'select']); expect(arg0.instantPages).to.deep.equal([]); expect(Object.keys(arg0.interactionEvents).sort()).to.deep.equal(['onMouseDown', 'onMouseUp']); }); it('With range', async () => { await render({ rangeSelect: true }); const arg0 = ref.current.result; - expect(Object.keys(arg0).sort()).to.deep.equal(['instantPages', 'interactionEvents']); + expect(Object.keys(arg0).sort()).to.deep.equal(['instantPages', 'interactionEvents', 'select']); expect(arg0.instantPages).to.deep.equal([]); expect(Object.keys(arg0.interactionEvents).sort()).to.deep.equal(['onMouseDown', 'onMouseEnter', 'onMouseUp']); }); it('With checkboxes', async () => { await render({ checkboxes: true }); const arg0 = ref.current.result; - expect(Object.keys(arg0).sort()).to.deep.equal(['instantPages', 'interactionEvents']); + expect(Object.keys(arg0).sort()).to.deep.equal(['instantPages', 'interactionEvents', 'select']); expect(arg0.instantPages).to.deep.equal([]); expect(Object.keys(arg0.interactionEvents).sort()).to.deep.equal(['onClick']); }); }); + it('Should manually pre-select and select values when calling the manual select method', async () => { + await render(); + const args = ref.current.result; + expect(listboxSelections.selectValues).not.called; + args.select([1]); + expect(listboxSelections.selectValues).calledOnce; + expect(listboxSelections.selectValues.args[0][0]).to.deep.equal({ + elemNumbers: [1], + isSingleSelect: false, + selections: { key: 'selections' }, + }); + }); + it('should select a value', async () => { await render(); const arg0 = ref.current.result; @@ -184,7 +197,7 @@ describe('use-listbox-interactions', () => { it('should return expected stuff', async () => { await render({ rangeSelect: true }); const arg0 = ref.current.result; - expect(Object.keys(arg0)).to.deep.equal(['instantPages', 'interactionEvents']); + expect(Object.keys(arg0)).to.deep.equal(['instantPages', 'interactionEvents', 'select']); expect(arg0.instantPages).to.deep.equal([]); expect(Object.keys(arg0.interactionEvents).sort()).to.deep.equal(['onMouseDown', 'onMouseEnter', 'onMouseUp']); }); diff --git a/apis/nucleus/src/components/listbox/listbox-keyboard-navigation.js b/apis/nucleus/src/components/listbox/listbox-keyboard-navigation.js new file mode 100644 index 000000000..72ee72934 --- /dev/null +++ b/apis/nucleus/src/components/listbox/listbox-keyboard-navigation.js @@ -0,0 +1,65 @@ +import KEYS from '../../keys'; + +const getElement = (elm, next = false) => { + const parentElm = elm && elm.parentElement[next ? 'nextElementSibling' : 'previousElementSibling']; + return parentElm && parentElm.querySelector('[role]'); +}; + +export default function getKeyboardNavigation({ select, confirm, cancel }) { + let startedRange = false; + const setStartedRange = (val) => { + startedRange = val; + }; + + const handleKeyDown = (event) => { + let elementToFocus; + const { keyCode, shiftKey = false } = event.nativeEvent; + + switch (keyCode) { + case KEYS.SHIFT: + // This is to ensure we include the first value when starting a range selection. + setStartedRange(true); + break; + case KEYS.SPACE: + select([+event.currentTarget.getAttribute('data-n')]); + break; + case KEYS.ARROW_DOWN: + case KEYS.ARROW_RIGHT: + elementToFocus = getElement(event.currentTarget, true); + if (shiftKey && elementToFocus) { + if (startedRange) { + select([+event.currentTarget.getAttribute('data-n')], true); + setStartedRange(false); + } + select([+elementToFocus.getAttribute('data-n')], true); + } + break; + case KEYS.ARROW_UP: + case KEYS.ARROW_LEFT: + elementToFocus = getElement(event.currentTarget, false); + if (shiftKey && elementToFocus) { + if (startedRange) { + select([+event.currentTarget.getAttribute('data-n')], true); + setStartedRange(false); + } + select([+elementToFocus.getAttribute('data-n')], true); + } + break; + case KEYS.ENTER: + confirm(); + break; + case KEYS.ESCAPE: + cancel(); + if (document.activeElement) { + document.activeElement.blur(); + } + break; + default: + } + if (elementToFocus) { + elementToFocus.focus(); + } + event.preventDefault(); + }; + return handleKeyDown; +} diff --git a/apis/nucleus/src/components/listbox/useSelectionsInteractions.js b/apis/nucleus/src/components/listbox/useSelectionsInteractions.js index 335b0addf..4157bd22b 100644 --- a/apis/nucleus/src/components/listbox/useSelectionsInteractions.js +++ b/apis/nucleus/src/components/listbox/useSelectionsInteractions.js @@ -53,6 +53,14 @@ export default function useSelectionsInteractions({ }); }; + const selectManually = (elementIds = [], additive = false) => { + setMouseDown(true); + preSelect(elementIds, additive || isRangeSelection); + const p = select(elementIds, additive || isRangeSelection); + setMouseDown(false); + return p; + }; + const onClick = useCallback( (event) => { if (selectingValues || selectDisabled()) { @@ -169,5 +177,6 @@ export default function useSelectionsInteractions({ return { instantPages, interactionEvents, + select: selectManually, // preselect and select without having to trigger an event }; } diff --git a/apis/nucleus/src/keys.js b/apis/nucleus/src/keys.js new file mode 100644 index 000000000..c951c76a0 --- /dev/null +++ b/apis/nucleus/src/keys.js @@ -0,0 +1,34 @@ +const KEYS = Object.freeze({ + ENTER: 13, + ESCAPE: 27, + SPACE: 32, + TAB: 9, + BACKSPACE: 8, + DELETE: 46, + ALT: 18, + CTRL: 17, + SHIFT: 16, + ARROW_UP: 38, + ARROW_DOWN: 40, + ARROW_LEFT: 37, + ARROW_RIGHT: 39, + PAGE_DOWN: 34, + PAGE_UP: 33, + HOME: 36, + END: 35, + F10: 121, + A: 65, + F: 70, + ZERO: 48, + NINE: 57, + NUMPAD_ZERO: 96, + NUMPAD_NINE: 105, + SUBTRACTION: 189, + DECIMAL: 190, + NUMPAD_DECIMAL: 110, + + isArrow: (key) => + key === KEYS.ARROW_UP || key === KEYS.ARROW_DOWN || key === KEYS.ARROW_LEFT || key === KEYS.ARROW_RIGHT, +}); + +export default KEYS;