mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-25 01:04:14 -05:00
refactor(listbox): range selections (#751)
* feat: working range selections * refactor: support single select * test: cover with unit tests * test: use act instead of render * Update apis/nucleus/src/components/listbox/listbox-selections.js Co-authored-by: Tobias Linsefors <T-Wizard@users.noreply.github.com>
This commit is contained in:
@@ -1,12 +1,6 @@
|
||||
/* eslint no-underscore-dangle:0 */
|
||||
|
||||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
useRef,
|
||||
// useMemo,
|
||||
} from 'react';
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import InfiniteLoader from 'react-window-infinite-loader';
|
||||
@@ -25,6 +19,7 @@ export default function ListBox({
|
||||
height,
|
||||
width,
|
||||
listLayout = 'vertical',
|
||||
rangeSelect = true,
|
||||
update = undefined,
|
||||
}) {
|
||||
const [layout] = useLayout(model);
|
||||
@@ -34,6 +29,7 @@ export default function ListBox({
|
||||
layout,
|
||||
selections,
|
||||
pages,
|
||||
rangeSelect,
|
||||
doc: document,
|
||||
});
|
||||
const loaderRef = useRef(null);
|
||||
@@ -125,7 +121,7 @@ export default function ListBox({
|
||||
}, [layout]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!instantPages && isLoadingData) {
|
||||
if (!instantPages || isLoadingData) {
|
||||
return;
|
||||
}
|
||||
setPages(instantPages);
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function Column({ index, style, data }) {
|
||||
const classArr = [classes.column];
|
||||
|
||||
let label = '';
|
||||
const { onMouseDown, pages } = data;
|
||||
const { onMouseDown, onMouseUp, onMouseEnter, pages } = data;
|
||||
let cell;
|
||||
if (pages) {
|
||||
const page = pages.filter((p) => p.qArea.qTop <= index && index < p.qArea.qTop + p.qArea.qHeight)[0];
|
||||
@@ -116,6 +116,8 @@ export default function Column({ index, style, data }) {
|
||||
style={style}
|
||||
alignItems="center"
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseEnter={onMouseEnter}
|
||||
role="row"
|
||||
tabIndex={0}
|
||||
data-n={cell && cell.qElemNumber}
|
||||
|
||||
@@ -35,6 +35,7 @@ export function ListBoxInline({ app, fieldIdentifier, stateName = '$', options =
|
||||
listLayout,
|
||||
search = true,
|
||||
toolbar = true,
|
||||
rangeSelect = true,
|
||||
properties = {},
|
||||
sessionModel = undefined,
|
||||
selectionsApi = undefined,
|
||||
@@ -211,6 +212,7 @@ export function ListBoxInline({ app, fieldIdentifier, stateName = '$', options =
|
||||
selections={selections}
|
||||
direction={direction}
|
||||
listLayout={listLayout}
|
||||
rangeSelect={rangeSelect}
|
||||
height={height}
|
||||
width={width}
|
||||
update={update}
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function Row({ index, style, data }) {
|
||||
const classArr = [classes.row];
|
||||
|
||||
let label = '';
|
||||
const { onMouseDown, pages } = data;
|
||||
const { onMouseDown, onMouseUp, onMouseEnter, pages } = data;
|
||||
let cell;
|
||||
if (pages) {
|
||||
const page = pages.filter((p) => p.qArea.qTop <= index && index < p.qArea.qTop + p.qArea.qHeight)[0];
|
||||
@@ -115,6 +115,8 @@ export default function Row({ index, style, data }) {
|
||||
className={classArr.join(' ').trim()}
|
||||
style={style}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseEnter={onMouseEnter}
|
||||
role="row"
|
||||
tabIndex={0}
|
||||
data-n={cell && cell.qElemNumber}
|
||||
|
||||
@@ -74,6 +74,7 @@ describe('<Listbox />', () => {
|
||||
width: 100,
|
||||
listLayout: 'vertical',
|
||||
update: sandbox.stub(),
|
||||
rangeSelect: false,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -86,7 +87,7 @@ describe('<Listbox />', () => {
|
||||
});
|
||||
|
||||
describe('Check rendering with different options', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
render = async () => {
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
@@ -95,6 +96,7 @@ describe('<Listbox />', () => {
|
||||
direction={args.direction}
|
||||
height={args.height}
|
||||
width={args.width}
|
||||
rangeSelect={args.rangeSelect}
|
||||
listLayout={args.listLayout}
|
||||
update={args.update}
|
||||
/>
|
||||
@@ -104,11 +106,12 @@ describe('<Listbox />', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.reset();
|
||||
sandbox.restore();
|
||||
renderer.unmount();
|
||||
});
|
||||
|
||||
it('should render and call stuff', async () => {
|
||||
args.rangeSelect = false;
|
||||
await render();
|
||||
|
||||
// check rendering
|
||||
@@ -124,6 +127,19 @@ describe('<Listbox />', () => {
|
||||
layout,
|
||||
selections,
|
||||
pages: [],
|
||||
rangeSelect: false,
|
||||
doc: 'document',
|
||||
});
|
||||
});
|
||||
|
||||
it('should call useSelectionsInteractions with rangeSelect true', async () => {
|
||||
args.rangeSelect = true;
|
||||
await render();
|
||||
expect(useSelectionsInteractions.args[useSelectionsInteractions.callCount - 1][0]).to.deep.equal({
|
||||
layout,
|
||||
selections,
|
||||
pages: [],
|
||||
rangeSelect: true,
|
||||
doc: 'document',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -176,4 +176,44 @@ describe('use-listbox-interactions', () => {
|
||||
}).not.to.throw();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getElemNumbersFromPages', () => {
|
||||
let pages;
|
||||
|
||||
beforeEach(() => {
|
||||
pages = [
|
||||
{
|
||||
qMatrix: [
|
||||
[{ qState: 'S', qElemNumber: 0 }],
|
||||
[{ qState: 'XS', qElemNumber: 1 }],
|
||||
[{ qState: 'L', qElemNumber: 2 }],
|
||||
[{ qState: 'A', qElemNumber: 3 }],
|
||||
[{ qState: 'XL', qElemNumber: 4 }],
|
||||
[{ qState: 'XS', qElemNumber: 5 }],
|
||||
[{ qState: 'A', qElemNumber: 6 }],
|
||||
],
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
it('listboxSelections', () => {
|
||||
expect(listboxSelections.getElemNumbersFromPages(undefined)).to.deep.equal([]);
|
||||
const resp = listboxSelections.getElemNumbersFromPages(pages);
|
||||
expect(resp).to.deep.equal([0, 1, 2, 3, 4, 5, 6]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fillRange', () => {
|
||||
it('should fill the numbers according to ground-truth array', () => {
|
||||
expect(listboxSelections.fillRange([], [])).to.deep.equal([]);
|
||||
expect(listboxSelections.fillRange([1, 10], []), 'without ground-truth no range').to.deep.equal([]);
|
||||
|
||||
expect(listboxSelections.fillRange([0, 5], [0, 1, 2, 3, 4, 5, 6])).to.deep.equal([0, 1, 2, 3, 4, 5]);
|
||||
expect(listboxSelections.fillRange([0], [0, 1, 2, 3, 4, 5, 6])).to.deep.equal([0]);
|
||||
expect(listboxSelections.fillRange([], [0, 1, 2, 3, 4, 5, 6])).to.deep.equal([]);
|
||||
expect(listboxSelections.fillRange([1, 6], [0, 1, 8, 16, 6, 2]), 'should fill using ground-truth').to.deep.equal([
|
||||
1, 8, 16, 6,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,9 @@ describe('use-listbox-interactions', () => {
|
||||
let getUniques;
|
||||
let selectValues;
|
||||
let applySelectionsOnPages;
|
||||
let fillRange;
|
||||
let getSelectedValues;
|
||||
let getElemNumbersFromPages;
|
||||
|
||||
before(() => {
|
||||
sandbox = sinon.createSandbox({ useFakeTimers: true });
|
||||
@@ -29,6 +32,9 @@ describe('use-listbox-interactions', () => {
|
||||
getUniques = sandbox.stub(listboxSelections, 'getUniques');
|
||||
selectValues = sandbox.stub(listboxSelections, 'selectValues');
|
||||
applySelectionsOnPages = sandbox.stub(listboxSelections, 'applySelectionsOnPages');
|
||||
fillRange = sandbox.stub(listboxSelections, 'fillRange');
|
||||
getSelectedValues = sandbox.stub(listboxSelections, 'getSelectedValues');
|
||||
getElemNumbersFromPages = sandbox.stub(listboxSelections, 'getElemNumbersFromPages');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -39,7 +45,10 @@ describe('use-listbox-interactions', () => {
|
||||
|
||||
getUniques.callsFake((input) => input);
|
||||
selectValues.resolves();
|
||||
applySelectionsOnPages.returns('instant pages');
|
||||
applySelectionsOnPages.returns([]);
|
||||
fillRange.callsFake((arr) => arr);
|
||||
getSelectedValues.returns([]);
|
||||
getElemNumbersFromPages.returns([]);
|
||||
|
||||
layout = {
|
||||
qListObject: { qDimensionInfo: { qIsOneAndOnlyOne: false } },
|
||||
@@ -48,13 +57,13 @@ describe('use-listbox-interactions', () => {
|
||||
pages = [];
|
||||
|
||||
ref = React.createRef();
|
||||
render = async () => {
|
||||
render = async (overrides = {}) => {
|
||||
await act(async () => {
|
||||
create(
|
||||
<TestHook
|
||||
ref={ref}
|
||||
hook={useSelectionsInteractions}
|
||||
hookProps={[{ layout, selections, pages, doc: global.document }]}
|
||||
hookProps={[{ layout, selections, rangeSelect: false, pages, doc: global.document, ...overrides }]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -70,35 +79,151 @@ describe('use-listbox-interactions', () => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('it should behave', () => {
|
||||
it('should behave', async () => {
|
||||
describe('it should behave without range select', () => {
|
||||
it('should return expected stuff', async () => {
|
||||
await render();
|
||||
const arg0 = ref.current.result;
|
||||
expect(Object.keys(arg0)).to.deep.equal(['instantPages', 'interactionEvents']);
|
||||
expect(arg0.instantPages).to.equal('instant pages');
|
||||
expect(arg0.instantPages).to.deep.equal([]);
|
||||
expect(Object.keys(arg0.interactionEvents)).to.deep.equal(['onMouseDown']);
|
||||
});
|
||||
|
||||
expect(applySelectionsOnPages).calledOnce.calledWithExactly([], [], false, false);
|
||||
expect(global.document.addEventListener.args[0][0]).to.equal('mouseup');
|
||||
it('should select a value', async () => {
|
||||
await render();
|
||||
const arg0 = ref.current.result;
|
||||
|
||||
expect(applySelectionsOnPages).not.called;
|
||||
const [eventName, docMouseUpListener] = global.document.addEventListener.args[0];
|
||||
expect(eventName).to.equal('mouseup');
|
||||
expect(global.document.removeEventListener).not.called;
|
||||
|
||||
expect(listboxSelections.selectValues).not.called;
|
||||
|
||||
arg0.interactionEvents.onMouseDown({
|
||||
currentTarget: {
|
||||
getAttribute: sandbox.stub().withArgs('data-n').returns(23),
|
||||
},
|
||||
act(() => {
|
||||
arg0.interactionEvents.onMouseDown({
|
||||
currentTarget: {
|
||||
getAttribute: sandbox.stub().withArgs('data-n').returns(23),
|
||||
},
|
||||
});
|
||||
});
|
||||
await render();
|
||||
|
||||
const arg1 = ref.current.result;
|
||||
|
||||
expect(listboxSelections.selectValues, 'since mouseup has not been called yet').not.called;
|
||||
expect(arg1.instantPages).to.deep.equal([]);
|
||||
|
||||
act(() => {
|
||||
docMouseUpListener(); // trigger doc mouseup listener to set mouseDown => false
|
||||
});
|
||||
|
||||
const arg2 = ref.current.result;
|
||||
|
||||
expect(listboxSelections.selectValues).calledOnce.calledWithExactly({
|
||||
selections: { key: 'selections' },
|
||||
elemNumbers: [23],
|
||||
isSingleSelect: false,
|
||||
});
|
||||
const arg1 = ref.current.result;
|
||||
expect(applySelectionsOnPages).calledThrice;
|
||||
expect(applySelectionsOnPages.args[1]).to.deep.equal([[], [23], false, true]);
|
||||
expect(arg1.instantPages).to.equal('instant pages');
|
||||
expect(applySelectionsOnPages).calledOnce;
|
||||
expect(applySelectionsOnPages.args[0]).to.deep.equal([[], [23]]);
|
||||
expect(arg2.instantPages).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it('should unselect a value', async () => {
|
||||
getSelectedValues.returns([24]); // mock element nbr 24 as already selected
|
||||
await render();
|
||||
const arg0 = ref.current.result;
|
||||
|
||||
act(() => {
|
||||
arg0.interactionEvents.onMouseDown({
|
||||
currentTarget: {
|
||||
getAttribute: sandbox.stub().withArgs('data-n').returns(24), // fake mousedown on element nbr 24
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(applySelectionsOnPages).calledOnce;
|
||||
expect(applySelectionsOnPages.args[0]).to.deep.equal([[], [24]]);
|
||||
expect(listboxSelections.selectValues, 'should only preselect - not select - while mousedown').not.called;
|
||||
const [, docMouseUpListener] = global.document.addEventListener.args[0];
|
||||
|
||||
act(() => {
|
||||
docMouseUpListener();
|
||||
});
|
||||
|
||||
expect(listboxSelections.selectValues).calledOnce.calledWithExactly({
|
||||
selections: { key: 'selections' },
|
||||
elemNumbers: [24],
|
||||
isSingleSelect: false,
|
||||
});
|
||||
|
||||
expect(applySelectionsOnPages, 'should not set instant pages again (after mouseup)').calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
describe('it should behave with range select', () => {
|
||||
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(arg0.instantPages).to.deep.equal([]);
|
||||
expect(Object.keys(arg0.interactionEvents)).to.deep.equal(['onMouseDown', 'onMouseUp', 'onMouseEnter']);
|
||||
});
|
||||
|
||||
it('should select a range (in theory)', async () => {
|
||||
getElemNumbersFromPages.returns([24, 25, 26, 27, 28, 29, 30, 31]);
|
||||
|
||||
await render({ rangeSelect: true });
|
||||
|
||||
expect(applySelectionsOnPages.callCount).to.equal(0);
|
||||
|
||||
// Simulate a typical select range scenario.
|
||||
act(() => {
|
||||
ref.current.result.interactionEvents.onMouseDown({
|
||||
currentTarget: {
|
||||
getAttribute: sandbox.stub().withArgs('data-n').returns(24),
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(applySelectionsOnPages.callCount).to.equal(1);
|
||||
act(() => {
|
||||
ref.current.result.interactionEvents.onMouseEnter({
|
||||
currentTarget: {
|
||||
getAttribute: sandbox.stub().withArgs('data-n').returns(25),
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(applySelectionsOnPages.callCount).to.equal(2);
|
||||
act(() => {
|
||||
ref.current.result.interactionEvents.onMouseEnter({
|
||||
currentTarget: {
|
||||
getAttribute: sandbox.stub().withArgs('data-n').returns(28),
|
||||
},
|
||||
});
|
||||
});
|
||||
act(() => {
|
||||
ref.current.result.interactionEvents.onMouseUp({
|
||||
currentTarget: {
|
||||
getAttribute: sandbox.stub().withArgs('data-n').returns(30),
|
||||
},
|
||||
});
|
||||
});
|
||||
act(() => {
|
||||
const [, docMouseUpListener] = global.document.addEventListener.args.pop();
|
||||
docMouseUpListener();
|
||||
});
|
||||
|
||||
expect(applySelectionsOnPages.callCount, 'should pre-select once for each new value').to.equal(4);
|
||||
expect(listboxSelections.selectValues).calledOnce.calledWithExactly({
|
||||
selections: { key: 'selections' },
|
||||
elemNumbers: [24, 25, 28, 30], // without mocking fillRange this range would be filled
|
||||
isSingleSelect: false,
|
||||
});
|
||||
|
||||
// Should pre-select "cumulative", once for each new value
|
||||
expect(applySelectionsOnPages.args[0]).to.deep.equal([[], [24]]);
|
||||
expect(applySelectionsOnPages.args[1]).to.deep.equal([[], [24, 25]]);
|
||||
expect(applySelectionsOnPages.args[2]).to.deep.equal([[], [24, 25, 28]]);
|
||||
expect(applySelectionsOnPages.args[3]).to.deep.equal([[], [24, 25, 28, 30]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const SELECTED_STATES = ['S', 'XS'];
|
||||
|
||||
const flattenArr = (arr) => arr.reduce((prev, cur) => prev.concat(cur));
|
||||
const flatten = (arr) => arr.reduce((prev, cur) => prev.concat(cur));
|
||||
|
||||
function isStateSelected(qState) {
|
||||
return SELECTED_STATES.includes(qState);
|
||||
@@ -11,7 +11,7 @@ export function getUniques(arr) {
|
||||
}
|
||||
|
||||
export function getSelectedValues(pages) {
|
||||
if (!pages) {
|
||||
if (!pages || !pages.length) {
|
||||
return [];
|
||||
}
|
||||
const elementNbrs = pages.map((page) => {
|
||||
@@ -21,11 +21,11 @@ export function getSelectedValues(pages) {
|
||||
});
|
||||
return elementNumbers.filter((n) => n !== false);
|
||||
});
|
||||
return flattenArr(elementNbrs);
|
||||
return flatten(elementNbrs);
|
||||
}
|
||||
|
||||
export function applySelectionsOnPages(pages, elmNumbers, toggle = false) {
|
||||
const getNewSelectionState = (qState) => (toggle && elmNumbers.length === 1 && isStateSelected(qState) ? 'A' : 'S');
|
||||
export function applySelectionsOnPages(pages, elmNumbers) {
|
||||
const getNewSelectionState = (qState) => (elmNumbers.length <= 1 && isStateSelected(qState) ? 'A' : 'S');
|
||||
const matrices = pages.map((page) => {
|
||||
const qMatrix = page.qMatrix.map((p) => {
|
||||
const [p0] = p;
|
||||
@@ -53,3 +53,49 @@ export async function selectValues({ selections, elemNumbers, isSingleSelect = f
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
export function getElemNumbersFromPages(pages) {
|
||||
if (!pages || !pages.length) {
|
||||
return [];
|
||||
}
|
||||
const elemNumbersArr = pages.map((page) => {
|
||||
const qElemNumbers = page.qMatrix.map((p) => {
|
||||
const [{ qElemNumber }] = p;
|
||||
return qElemNumber;
|
||||
});
|
||||
return qElemNumbers;
|
||||
});
|
||||
const elemNumbers = flatten(elemNumbersArr);
|
||||
return elemNumbers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the min and max indices of elemNumbersOrdered which contains
|
||||
* all numbers in elementNbrs.
|
||||
*
|
||||
* @param {array(number)} elementNbrs
|
||||
* @param {array(number)} elemNumbersOrdered
|
||||
* @returns { min: {number}, max: {number} }
|
||||
*/
|
||||
function getMinMax(elementNbrs, elemNumbersOrdered) {
|
||||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
elementNbrs.forEach((nbr) => {
|
||||
const index = elemNumbersOrdered.indexOf(nbr);
|
||||
min = index < min ? index : min;
|
||||
max = index > max ? index : max;
|
||||
});
|
||||
return { min, max };
|
||||
}
|
||||
|
||||
export function fillRange(elementNbrs, elemNumbersOrdered) {
|
||||
if (!elementNbrs) {
|
||||
return [];
|
||||
}
|
||||
if (elementNbrs.length <= 1) {
|
||||
return elementNbrs;
|
||||
}
|
||||
// Interpolate values algorithm
|
||||
const { min, max } = getMinMax(elementNbrs, elemNumbersOrdered);
|
||||
return elemNumbersOrdered.slice(min, max + 1);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,47 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { getUniques, selectValues, applySelectionsOnPages } from './listbox-selections';
|
||||
import {
|
||||
getUniques,
|
||||
selectValues,
|
||||
applySelectionsOnPages,
|
||||
fillRange,
|
||||
getSelectedValues,
|
||||
getElemNumbersFromPages,
|
||||
} from './listbox-selections';
|
||||
|
||||
export default function useSelectionsInteractions({ layout, selections, pages = [], doc = document }) {
|
||||
const [mouseDown, setMouseDown] = useState(false);
|
||||
const [selectedElementNumbers, setSelectedElementNumbers] = useState([]);
|
||||
const [selectingValues, setSelectingValues] = useState(false);
|
||||
export default function useSelectionsInteractions({
|
||||
layout,
|
||||
selections,
|
||||
pages = [],
|
||||
rangeSelect = true,
|
||||
doc = document,
|
||||
}) {
|
||||
const [instantPages, setInstantPages] = useState(pages);
|
||||
const [mouseDown, setMouseDown] = useState(false);
|
||||
const [selectingValues, setSelectingValues] = useState(false);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [isRangeSelection, setIsRangeSelection] = useState(false);
|
||||
const [preSelected, setPreSelected] = useState([]);
|
||||
|
||||
const select = ({ elemNumbers }) => {
|
||||
const elemNumbersOrdered = getElemNumbersFromPages(pages);
|
||||
|
||||
const getIsSingleSelect = () => !!(layout && layout.qListObject.qDimensionInfo.qIsOneAndOnlyOne);
|
||||
|
||||
// Select values for real, by calling the backend.
|
||||
const select = async (elemNumbers = [], additive = false) => {
|
||||
setSelectingValues(true);
|
||||
const isSingleSelect = layout.qListObject.qDimensionInfo.qIsOneAndOnlyOne;
|
||||
return selectValues({ selections, elemNumbers, isSingleSelect }).then(() => {
|
||||
setSelectingValues(false);
|
||||
const isSingleSelect = getIsSingleSelect();
|
||||
const filtered = additive ? elemNumbers.filter((n) => !selected.includes(n)) : elemNumbers;
|
||||
await selectValues({ selections, elemNumbers: filtered, isSingleSelect });
|
||||
setSelectingValues(false);
|
||||
};
|
||||
|
||||
// Show estimated selection states instantly before applying the selections for real.
|
||||
const preSelect = (elemNumbers, additive = false) => {
|
||||
setPreSelected((existing) => {
|
||||
const uniques = getUniques([...existing, ...elemNumbers]);
|
||||
const filtered = additive ? uniques.filter((n) => !selected.includes(n)) : uniques;
|
||||
const filled = additive ? fillRange(uniques, elemNumbersOrdered) : filtered;
|
||||
return filled;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -20,26 +50,66 @@ export default function useSelectionsInteractions({ layout, selections, pages =
|
||||
if (selectingValues) {
|
||||
return;
|
||||
}
|
||||
const elemNumber = +event.currentTarget.getAttribute('data-n');
|
||||
setSelectedElementNumbers([elemNumber]);
|
||||
setIsRangeSelection(false);
|
||||
setMouseDown(true);
|
||||
|
||||
const elemNumber = +event.currentTarget.getAttribute('data-n');
|
||||
setPreSelected([elemNumber]);
|
||||
},
|
||||
[selectingValues, layout]
|
||||
[selectingValues]
|
||||
);
|
||||
|
||||
const onMouseUp = useCallback(
|
||||
(event) => {
|
||||
const elemNumber = +event.currentTarget.getAttribute('data-n');
|
||||
if (
|
||||
getIsSingleSelect() ||
|
||||
!rangeSelect ||
|
||||
!mouseDown ||
|
||||
selectingValues ||
|
||||
(preSelected.length === 1 && preSelected[0] === elemNumber) // prevent toggling again on mouseup
|
||||
) {
|
||||
return;
|
||||
}
|
||||
preSelect([elemNumber]);
|
||||
},
|
||||
[mouseDown, selectingValues, preSelected, selected, isRangeSelection]
|
||||
);
|
||||
|
||||
const onMouseUpDoc = useCallback(() => {
|
||||
// Ensure we end interactions when mouseup happens outside the Listbox.
|
||||
setMouseDown(false);
|
||||
setSelectingValues(false);
|
||||
}, []);
|
||||
|
||||
const onMouseEnter = useCallback(
|
||||
(event) => {
|
||||
if (getIsSingleSelect() || !mouseDown || selectingValues) {
|
||||
return;
|
||||
}
|
||||
setIsRangeSelection(true);
|
||||
const elemNumber = +event.currentTarget.getAttribute('data-n');
|
||||
preSelect([elemNumber], true);
|
||||
},
|
||||
[
|
||||
mouseDown,
|
||||
selectingValues,
|
||||
isRangeSelection,
|
||||
preSelected,
|
||||
selected,
|
||||
layout && layout.qListObject.qDimensionInfo.qIsOneAndOnlyOne,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mouseDown) {
|
||||
// Perform selections of pre-selected values. This can
|
||||
// happen only when interactions have finished (mouseup).
|
||||
const interactionIsFinished = !mouseDown;
|
||||
if (!preSelected || !preSelected.length || selectingValues || !interactionIsFinished || !layout) {
|
||||
return;
|
||||
}
|
||||
const elemNumbers = getUniques(selectedElementNumbers);
|
||||
if (elemNumbers.length) {
|
||||
select({ elemNumbers });
|
||||
}
|
||||
}, [selectedElementNumbers, mouseDown]);
|
||||
select(preSelected, isRangeSelection);
|
||||
}, [preSelected, mouseDown]);
|
||||
|
||||
useEffect(() => {
|
||||
doc.addEventListener('mouseup', onMouseUpDoc);
|
||||
@@ -49,22 +119,30 @@ export default function useSelectionsInteractions({ layout, selections, pages =
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectingValues || !pages) {
|
||||
if (selectingValues || mouseDown) {
|
||||
return;
|
||||
}
|
||||
const newPages = applySelectionsOnPages(
|
||||
pages,
|
||||
selectedElementNumbers,
|
||||
mouseDown,
|
||||
selectedElementNumbers.length === 1
|
||||
);
|
||||
// Keep track of (truely) selected fields so we can prevent toggling them on range select.
|
||||
const alreadySelected = getSelectedValues(pages);
|
||||
setSelected(alreadySelected);
|
||||
}, [pages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectingValues || !pages || !mouseDown) {
|
||||
return;
|
||||
}
|
||||
// Render pre-selections before they have been selected in Engine.
|
||||
const newPages = applySelectionsOnPages(pages, preSelected);
|
||||
setInstantPages(newPages);
|
||||
}, [selectedElementNumbers]);
|
||||
}, [preSelected]);
|
||||
|
||||
const rangeSelectEvents = rangeSelect ? { onMouseUp, onMouseEnter } : {};
|
||||
|
||||
return {
|
||||
instantPages,
|
||||
interactionEvents: {
|
||||
onMouseDown,
|
||||
...rangeSelectEvents,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user