mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 17:58:43 -05:00
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
This commit is contained in:
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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('<ListBoxRowColumn />', () => {
|
||||
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('<ListBoxRowColumn />', () => {
|
||||
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(
|
||||
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
|
||||
);
|
||||
@@ -54,6 +76,7 @@ describe('<ListBoxRowColumn />', () => {
|
||||
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('<ListBoxRowColumn />', () => {
|
||||
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(
|
||||
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
|
||||
@@ -89,16 +116,18 @@ describe('<ListBoxRowColumn />', () => {
|
||||
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(
|
||||
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
|
||||
@@ -130,10 +159,11 @@ describe('<ListBoxRowColumn />', () => {
|
||||
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('<ListBoxRowColumn />', () => {
|
||||
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('<ListBoxRowColumn />', () => {
|
||||
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('<ListBoxRowColumn />', () => {
|
||||
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('<ListBoxRowColumn />', () => {
|
||||
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('<ListBoxRowColumn />', () => {
|
||||
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('<ListBoxRowColumn />', () => {
|
||||
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('<ListBoxRowColumn />', () => {
|
||||
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('<ListBoxRowColumn />', () => {
|
||||
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('<ListBoxRowColumn />', () => {
|
||||
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('<ListBoxRowColumn />', () => {
|
||||
const style = {};
|
||||
const data = {
|
||||
checkboxes: true,
|
||||
actions,
|
||||
pages: [
|
||||
{
|
||||
qArea: {
|
||||
@@ -541,11 +590,12 @@ describe('<ListBoxRowColumn />', () => {
|
||||
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(
|
||||
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
|
||||
@@ -571,15 +621,17 @@ describe('<ListBoxRowColumn />', () => {
|
||||
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(
|
||||
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
34
apis/nucleus/src/keys.js
Normal file
34
apis/nucleus/src/keys.js
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user