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 [layout] = useLayout(model);
|
||||||
const [pages, setPages] = useState(null);
|
const [pages, setPages] = useState(null);
|
||||||
const [isLoadingData, setIsLoadingData] = useState(false);
|
const [isLoadingData, setIsLoadingData] = useState(false);
|
||||||
const { instantPages = [], interactionEvents } = useSelectionsInteractions({
|
const {
|
||||||
|
instantPages = [],
|
||||||
|
interactionEvents,
|
||||||
|
select,
|
||||||
|
} = useSelectionsInteractions({
|
||||||
layout,
|
layout,
|
||||||
selections,
|
selections,
|
||||||
pages,
|
pages,
|
||||||
@@ -186,6 +190,11 @@ export default function ListBox({
|
|||||||
checkboxes,
|
checkboxes,
|
||||||
dense,
|
dense,
|
||||||
frequencyMode,
|
frequencyMode,
|
||||||
|
actions: {
|
||||||
|
select,
|
||||||
|
confirm: () => selections && selections.confirm.call(selections),
|
||||||
|
cancel: () => selections && selections.cancel.call(selections),
|
||||||
|
},
|
||||||
frequencyMax,
|
frequencyMax,
|
||||||
histogram,
|
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';
|
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 Tick from '@nebula.js/ui/icons/tick';
|
||||||
import ListBoxCheckbox from './ListBoxCheckbox';
|
import ListBoxCheckbox from './ListBoxCheckbox';
|
||||||
import getSegmentsFromRanges from './listbox-highlight';
|
import getSegmentsFromRanges from './listbox-highlight';
|
||||||
|
import getKeyboardNavigation from './listbox-keyboard-navigation';
|
||||||
|
|
||||||
const ellipsis = {
|
const ellipsis = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -35,6 +36,9 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
'&:focus': {
|
'&:focus': {
|
||||||
boxShadow: `inset 0 0 0 2px ${theme.palette.custom.focusBorder} !important`,
|
boxShadow: `inset 0 0 0 2px ${theme.palette.custom.focusBorder} !important`,
|
||||||
},
|
},
|
||||||
|
'&:focus-visible': {
|
||||||
|
outline: 'none',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// The interior wrapper for all field content.
|
// The interior wrapper for all field content.
|
||||||
@@ -153,10 +157,13 @@ export default function RowColumn({ index, style, data, column = false }) {
|
|||||||
checkboxes = false,
|
checkboxes = false,
|
||||||
dense = false,
|
dense = false,
|
||||||
frequencyMode = 'N',
|
frequencyMode = 'N',
|
||||||
|
actions,
|
||||||
frequencyMax = '',
|
frequencyMax = '',
|
||||||
histogram = false,
|
histogram = false,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
|
const handleKeyDownCallback = useCallback(getKeyboardNavigation(actions), [actions]);
|
||||||
|
|
||||||
const [isSelected, setSelected] = useState(false);
|
const [isSelected, setSelected] = useState(false);
|
||||||
const [cell, setCell] = useState();
|
const [cell, setCell] = useState();
|
||||||
|
|
||||||
@@ -295,11 +302,15 @@ export default function RowColumn({ index, style, data, column = false }) {
|
|||||||
container
|
container
|
||||||
spacing={0}
|
spacing={0}
|
||||||
className={joinClassNames(['value', ...classArr])}
|
className={joinClassNames(['value', ...classArr])}
|
||||||
|
classes={{
|
||||||
|
root: classes.fieldRoot,
|
||||||
|
}}
|
||||||
style={style}
|
style={style}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onMouseUp={onMouseUp}
|
onMouseUp={onMouseUp}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
|
onKeyDown={handleKeyDownCallback}
|
||||||
role={column ? 'column' : 'row'}
|
role={column ? 'column' : 'row'}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
data-n={cell && cell.qElemNumber}
|
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 { Grid, Typography } from '@material-ui/core';
|
||||||
import Lock from '@nebula.js/ui/icons/lock';
|
import Lock from '@nebula.js/ui/icons/lock';
|
||||||
import ListBoxCheckbox from '../ListBoxCheckbox';
|
import ListBoxCheckbox from '../ListBoxCheckbox';
|
||||||
|
import * as getKeyboardNavigation from '../listbox-keyboard-navigation';
|
||||||
|
|
||||||
const [{ default: ListBoxRowColumn }] = aw.mock(
|
const [{ default: ListBoxRowColumn }] = aw.mock(
|
||||||
[
|
[
|
||||||
@@ -31,6 +32,25 @@ async function render(content) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('<ListBoxRowColumn />', () => {
|
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', () => {
|
describe('as row', () => {
|
||||||
const rowCol = 'row';
|
const rowCol = 'row';
|
||||||
|
|
||||||
@@ -38,12 +58,14 @@ describe('<ListBoxRowColumn />', () => {
|
|||||||
const index = 0;
|
const index = 0;
|
||||||
const style = {};
|
const style = {};
|
||||||
const data = {
|
const data = {
|
||||||
onMouseDown: sinon.spy(),
|
onMouseDown: sandbox.spy(),
|
||||||
onMouseUp: sinon.spy(),
|
onMouseUp: sandbox.spy(),
|
||||||
onMouseEnter: sinon.spy(),
|
onMouseEnter: sandbox.spy(),
|
||||||
onClick: sinon.spy(),
|
onClick: sandbox.spy(),
|
||||||
pages: [],
|
pages: [],
|
||||||
|
actions,
|
||||||
};
|
};
|
||||||
|
expect(handleKeyDown).not.called;
|
||||||
const testRenderer = await render(
|
const testRenderer = await render(
|
||||||
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
|
<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.spacing).to.equal(0);
|
||||||
expect(type.props.style).to.deep.equal({});
|
expect(type.props.style).to.deep.equal({});
|
||||||
expect(type.props.role).to.equal(rowCol);
|
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.onMouseDown.callCount).to.equal(0);
|
||||||
expect(type.props.onMouseUp.callCount).to.equal(0);
|
expect(type.props.onMouseUp.callCount).to.equal(0);
|
||||||
expect(type.props.onMouseEnter.callCount).to.equal(0);
|
expect(type.props.onMouseEnter.callCount).to.equal(0);
|
||||||
@@ -67,16 +90,20 @@ describe('<ListBoxRowColumn />', () => {
|
|||||||
const cbs = testInstance.findAllByType(ListBoxCheckbox);
|
const cbs = testInstance.findAllByType(ListBoxCheckbox);
|
||||||
expect(cbs).to.have.length(0);
|
expect(cbs).to.have.length(0);
|
||||||
await testRenderer.unmount();
|
await testRenderer.unmount();
|
||||||
|
|
||||||
|
expect(handleKeyDown).calledOnce.calledWith('actions');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have css class `value`', async () => {
|
it('should have css class `value`', async () => {
|
||||||
const index = 0;
|
const index = 0;
|
||||||
const style = {};
|
const style = {};
|
||||||
const data = {
|
const data = {
|
||||||
onMouseDown: sinon.spy(),
|
onMouseDown: sandbox.spy(),
|
||||||
onMouseUp: sinon.spy(),
|
onMouseUp: sandbox.spy(),
|
||||||
onMouseEnter: sinon.spy(),
|
onMouseEnter: sandbox.spy(),
|
||||||
onClick: sinon.spy(),
|
onClick: sandbox.spy(),
|
||||||
pages: [],
|
pages: [],
|
||||||
|
actions,
|
||||||
};
|
};
|
||||||
const testRenderer = await render(
|
const testRenderer = await render(
|
||||||
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
|
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
|
||||||
@@ -89,16 +116,18 @@ describe('<ListBoxRowColumn />', () => {
|
|||||||
expect(className.split(' ')).to.include('value');
|
expect(className.split(' ')).to.include('value');
|
||||||
await testRenderer.unmount();
|
await testRenderer.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render with checkboxes', async () => {
|
it('should render with checkboxes', async () => {
|
||||||
const index = 0;
|
const index = 0;
|
||||||
const style = {};
|
const style = {};
|
||||||
const data = {
|
const data = {
|
||||||
checkboxes: true,
|
checkboxes: true,
|
||||||
onMouseDown: sinon.spy(),
|
onMouseDown: sandbox.spy(),
|
||||||
onMouseUp: sinon.spy(),
|
onMouseUp: sandbox.spy(),
|
||||||
onMouseEnter: sinon.spy(),
|
onMouseEnter: sandbox.spy(),
|
||||||
onClick: sinon.spy(),
|
onClick: sandbox.spy(),
|
||||||
pages: [],
|
pages: [],
|
||||||
|
actions,
|
||||||
};
|
};
|
||||||
const testRenderer = await render(
|
const testRenderer = await render(
|
||||||
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
|
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
|
||||||
@@ -130,10 +159,11 @@ describe('<ListBoxRowColumn />', () => {
|
|||||||
const style = {};
|
const style = {};
|
||||||
const data = {
|
const data = {
|
||||||
isLocked: true,
|
isLocked: true,
|
||||||
onMouseDown: sinon.spy(),
|
onMouseDown: sandbox.spy(),
|
||||||
onMouseUp: sinon.spy(),
|
onMouseUp: sandbox.spy(),
|
||||||
onMouseEnter: sinon.spy(),
|
onMouseEnter: sandbox.spy(),
|
||||||
onClick: sinon.spy(),
|
onClick: sandbox.spy(),
|
||||||
|
actions,
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
qArea: {
|
qArea: {
|
||||||
@@ -163,14 +193,16 @@ describe('<ListBoxRowColumn />', () => {
|
|||||||
expect(type.props.size).to.equal('small');
|
expect(type.props.size).to.equal('small');
|
||||||
await testRenderer.unmount();
|
await testRenderer.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set selected', async () => {
|
it('should set selected', async () => {
|
||||||
const index = 0;
|
const index = 0;
|
||||||
const style = {};
|
const style = {};
|
||||||
const data = {
|
const data = {
|
||||||
onMouseDown: sinon.spy(),
|
onMouseDown: sandbox.spy(),
|
||||||
onMouseUp: sinon.spy(),
|
onMouseUp: sandbox.spy(),
|
||||||
onMouseEnter: sinon.spy(),
|
onMouseEnter: sandbox.spy(),
|
||||||
onClick: sinon.spy(),
|
onClick: sandbox.spy(),
|
||||||
|
actions,
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
qArea: {
|
qArea: {
|
||||||
@@ -197,14 +229,16 @@ describe('<ListBoxRowColumn />', () => {
|
|||||||
expect(type.props.className).to.include('selected');
|
expect(type.props.className).to.include('selected');
|
||||||
await testRenderer.unmount();
|
await testRenderer.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set alternative', async () => {
|
it('should set alternative', async () => {
|
||||||
const index = 0;
|
const index = 0;
|
||||||
const style = {};
|
const style = {};
|
||||||
const data = {
|
const data = {
|
||||||
onMouseDown: sinon.spy(),
|
onMouseDown: sandbox.spy(),
|
||||||
onMouseUp: sinon.spy(),
|
onMouseUp: sandbox.spy(),
|
||||||
onMouseEnter: sinon.spy(),
|
onMouseEnter: sandbox.spy(),
|
||||||
onClick: sinon.spy(),
|
onClick: sandbox.spy(),
|
||||||
|
actions,
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
qArea: {
|
qArea: {
|
||||||
@@ -231,14 +265,16 @@ describe('<ListBoxRowColumn />', () => {
|
|||||||
expect(type.props.className).to.include('alternative');
|
expect(type.props.className).to.include('alternative');
|
||||||
await testRenderer.unmount();
|
await testRenderer.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set excluded - qState X', async () => {
|
it('should set excluded - qState X', async () => {
|
||||||
const index = 0;
|
const index = 0;
|
||||||
const style = {};
|
const style = {};
|
||||||
const data = {
|
const data = {
|
||||||
onMouseDown: sinon.spy(),
|
onMouseDown: sandbox.spy(),
|
||||||
onMouseUp: sinon.spy(),
|
onMouseUp: sandbox.spy(),
|
||||||
onMouseEnter: sinon.spy(),
|
onMouseEnter: sandbox.spy(),
|
||||||
onClick: sinon.spy(),
|
onClick: sandbox.spy(),
|
||||||
|
actions,
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
qArea: {
|
qArea: {
|
||||||
@@ -265,14 +301,16 @@ describe('<ListBoxRowColumn />', () => {
|
|||||||
expect(type.props.className).to.include('excluded');
|
expect(type.props.className).to.include('excluded');
|
||||||
await testRenderer.unmount();
|
await testRenderer.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set excluded - qState XS', async () => {
|
it('should set excluded - qState XS', async () => {
|
||||||
const index = 0;
|
const index = 0;
|
||||||
const style = {};
|
const style = {};
|
||||||
const data = {
|
const data = {
|
||||||
onMouseDown: sinon.spy(),
|
onMouseDown: sandbox.spy(),
|
||||||
onMouseUp: sinon.spy(),
|
onMouseUp: sandbox.spy(),
|
||||||
onMouseEnter: sinon.spy(),
|
onMouseEnter: sandbox.spy(),
|
||||||
onClick: sinon.spy(),
|
onClick: sandbox.spy(),
|
||||||
|
actions,
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
qArea: {
|
qArea: {
|
||||||
@@ -299,14 +337,16 @@ describe('<ListBoxRowColumn />', () => {
|
|||||||
expect(type.props.className).to.include('excluded');
|
expect(type.props.className).to.include('excluded');
|
||||||
await testRenderer.unmount();
|
await testRenderer.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set excluded - qState XL', async () => {
|
it('should set excluded - qState XL', async () => {
|
||||||
const index = 0;
|
const index = 0;
|
||||||
const style = {};
|
const style = {};
|
||||||
const data = {
|
const data = {
|
||||||
onMouseDown: sinon.spy(),
|
onMouseDown: sandbox.spy(),
|
||||||
onMouseUp: sinon.spy(),
|
onMouseUp: sandbox.spy(),
|
||||||
onMouseEnter: sinon.spy(),
|
onMouseEnter: sandbox.spy(),
|
||||||
onClick: sinon.spy(),
|
onClick: sandbox.spy(),
|
||||||
|
actions,
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
qArea: {
|
qArea: {
|
||||||
@@ -333,14 +373,16 @@ describe('<ListBoxRowColumn />', () => {
|
|||||||
expect(type.props.className).to.include('excluded');
|
expect(type.props.className).to.include('excluded');
|
||||||
await testRenderer.unmount();
|
await testRenderer.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should highlight ranges', async () => {
|
it('should highlight ranges', async () => {
|
||||||
const index = 0;
|
const index = 0;
|
||||||
const style = {};
|
const style = {};
|
||||||
const data = {
|
const data = {
|
||||||
onMouseDown: sinon.spy(),
|
onMouseDown: sandbox.spy(),
|
||||||
onMouseUp: sinon.spy(),
|
onMouseUp: sandbox.spy(),
|
||||||
onMouseEnter: sinon.spy(),
|
onMouseEnter: sandbox.spy(),
|
||||||
onClick: sinon.spy(),
|
onClick: sandbox.spy(),
|
||||||
|
actions,
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
qArea: {
|
qArea: {
|
||||||
@@ -373,14 +415,16 @@ describe('<ListBoxRowColumn />', () => {
|
|||||||
expect(types[1].props.children.props.children).to.equal(' ftw');
|
expect(types[1].props.children.props.children).to.equal(' ftw');
|
||||||
await testRenderer.unmount();
|
await testRenderer.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should highlight ranges', async () => {
|
it('should highlight ranges', async () => {
|
||||||
const index = 0;
|
const index = 0;
|
||||||
const style = {};
|
const style = {};
|
||||||
const data = {
|
const data = {
|
||||||
onMouseDown: sinon.spy(),
|
onMouseDown: sandbox.spy(),
|
||||||
onMouseUp: sinon.spy(),
|
onMouseUp: sandbox.spy(),
|
||||||
onMouseEnter: sinon.spy(),
|
onMouseEnter: sandbox.spy(),
|
||||||
onClick: sinon.spy(),
|
onClick: sandbox.spy(),
|
||||||
|
actions,
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
qArea: {
|
qArea: {
|
||||||
@@ -415,14 +459,16 @@ describe('<ListBoxRowColumn />', () => {
|
|||||||
expect(hits).to.have.length(2);
|
expect(hits).to.have.length(2);
|
||||||
await testRenderer.unmount();
|
await testRenderer.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should highlight ranges', async () => {
|
it('should highlight ranges', async () => {
|
||||||
const index = 0;
|
const index = 0;
|
||||||
const style = {};
|
const style = {};
|
||||||
const data = {
|
const data = {
|
||||||
onMouseDown: sinon.spy(),
|
onMouseDown: sandbox.spy(),
|
||||||
onMouseUp: sinon.spy(),
|
onMouseUp: sandbox.spy(),
|
||||||
onMouseEnter: sinon.spy(),
|
onMouseEnter: sandbox.spy(),
|
||||||
onClick: sinon.spy(),
|
onClick: sandbox.spy(),
|
||||||
|
actions,
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
qArea: {
|
qArea: {
|
||||||
@@ -456,14 +502,16 @@ describe('<ListBoxRowColumn />', () => {
|
|||||||
expect(types[2].props.children.props.children).to.equal(' buddy');
|
expect(types[2].props.children.props.children).to.equal(' buddy');
|
||||||
await testRenderer.unmount();
|
await testRenderer.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show frequency when enabled', async () => {
|
it('should show frequency when enabled', async () => {
|
||||||
const index = 0;
|
const index = 0;
|
||||||
const style = {};
|
const style = {};
|
||||||
const data = {
|
const data = {
|
||||||
onMouseDown: sinon.spy(),
|
onMouseDown: sandbox.spy(),
|
||||||
onMouseUp: sinon.spy(),
|
onMouseUp: sandbox.spy(),
|
||||||
onMouseEnter: sinon.spy(),
|
onMouseEnter: sandbox.spy(),
|
||||||
onClick: sinon.spy(),
|
onClick: sandbox.spy(),
|
||||||
|
actions,
|
||||||
frequencyMode: 'value',
|
frequencyMode: 'value',
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
@@ -497,6 +545,7 @@ describe('<ListBoxRowColumn />', () => {
|
|||||||
const style = {};
|
const style = {};
|
||||||
const data = {
|
const data = {
|
||||||
checkboxes: true,
|
checkboxes: true,
|
||||||
|
actions,
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
qArea: {
|
qArea: {
|
||||||
@@ -541,11 +590,12 @@ describe('<ListBoxRowColumn />', () => {
|
|||||||
const index = 0;
|
const index = 0;
|
||||||
const style = {};
|
const style = {};
|
||||||
const data = {
|
const data = {
|
||||||
onMouseDown: sinon.spy(),
|
onMouseDown: sandbox.spy(),
|
||||||
onMouseUp: sinon.spy(),
|
onMouseUp: sandbox.spy(),
|
||||||
onMouseEnter: sinon.spy(),
|
onMouseEnter: sandbox.spy(),
|
||||||
onClick: sinon.spy(),
|
onClick: sandbox.spy(),
|
||||||
pages: [],
|
pages: [],
|
||||||
|
actions,
|
||||||
};
|
};
|
||||||
const testRenderer = await render(
|
const testRenderer = await render(
|
||||||
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
|
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
|
||||||
@@ -571,15 +621,17 @@ describe('<ListBoxRowColumn />', () => {
|
|||||||
expect(cbs).to.have.length(0);
|
expect(cbs).to.have.length(0);
|
||||||
await testRenderer.unmount();
|
await testRenderer.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have css class `value`', async () => {
|
it('should have css class `value`', async () => {
|
||||||
const index = 0;
|
const index = 0;
|
||||||
const style = {};
|
const style = {};
|
||||||
const data = {
|
const data = {
|
||||||
onMouseDown: sinon.spy(),
|
onMouseDown: sandbox.spy(),
|
||||||
onMouseUp: sinon.spy(),
|
onMouseUp: sandbox.spy(),
|
||||||
onMouseEnter: sinon.spy(),
|
onMouseEnter: sandbox.spy(),
|
||||||
onClick: sinon.spy(),
|
onClick: sandbox.spy(),
|
||||||
pages: [],
|
pages: [],
|
||||||
|
actions,
|
||||||
};
|
};
|
||||||
const testRenderer = await render(
|
const testRenderer = await render(
|
||||||
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
|
<ListBoxRowColumn index={index} style={style} data={data} column={rowCol === 'column'} />
|
||||||
|
|||||||
@@ -88,26 +88,39 @@ describe('use-listbox-interactions', () => {
|
|||||||
it('Without range', async () => {
|
it('Without range', async () => {
|
||||||
await render();
|
await render();
|
||||||
const arg0 = ref.current.result;
|
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(arg0.instantPages).to.deep.equal([]);
|
||||||
expect(Object.keys(arg0.interactionEvents).sort()).to.deep.equal(['onMouseDown', 'onMouseUp']);
|
expect(Object.keys(arg0.interactionEvents).sort()).to.deep.equal(['onMouseDown', 'onMouseUp']);
|
||||||
});
|
});
|
||||||
it('With range', async () => {
|
it('With range', async () => {
|
||||||
await render({ rangeSelect: true });
|
await render({ rangeSelect: true });
|
||||||
const arg0 = ref.current.result;
|
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(arg0.instantPages).to.deep.equal([]);
|
||||||
expect(Object.keys(arg0.interactionEvents).sort()).to.deep.equal(['onMouseDown', 'onMouseEnter', 'onMouseUp']);
|
expect(Object.keys(arg0.interactionEvents).sort()).to.deep.equal(['onMouseDown', 'onMouseEnter', 'onMouseUp']);
|
||||||
});
|
});
|
||||||
it('With checkboxes', async () => {
|
it('With checkboxes', async () => {
|
||||||
await render({ checkboxes: true });
|
await render({ checkboxes: true });
|
||||||
const arg0 = ref.current.result;
|
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(arg0.instantPages).to.deep.equal([]);
|
||||||
expect(Object.keys(arg0.interactionEvents).sort()).to.deep.equal(['onClick']);
|
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 () => {
|
it('should select a value', async () => {
|
||||||
await render();
|
await render();
|
||||||
const arg0 = ref.current.result;
|
const arg0 = ref.current.result;
|
||||||
@@ -184,7 +197,7 @@ describe('use-listbox-interactions', () => {
|
|||||||
it('should return expected stuff', async () => {
|
it('should return expected stuff', async () => {
|
||||||
await render({ rangeSelect: true });
|
await render({ rangeSelect: true });
|
||||||
const arg0 = ref.current.result;
|
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(arg0.instantPages).to.deep.equal([]);
|
||||||
expect(Object.keys(arg0.interactionEvents).sort()).to.deep.equal(['onMouseDown', 'onMouseEnter', 'onMouseUp']);
|
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(
|
const onClick = useCallback(
|
||||||
(event) => {
|
(event) => {
|
||||||
if (selectingValues || selectDisabled()) {
|
if (selectingValues || selectDisabled()) {
|
||||||
@@ -169,5 +177,6 @@ export default function useSelectionsInteractions({
|
|||||||
return {
|
return {
|
||||||
instantPages,
|
instantPages,
|
||||||
interactionEvents,
|
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