From 9dab7f3aaab4bfda88ef238b465cddeb98d86226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sjo=CC=88strand?= <99665802+DanielS-Qlik@users.noreply.github.com> Date: Wed, 29 Mar 2023 13:52:47 +0200 Subject: [PATCH 1/7] feat: wip select on multi tap --- .../src/components/listbox/ListBoxInline.jsx | 5 ++++- .../listbox-keyboard-navigation.js | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apis/nucleus/src/components/listbox/ListBoxInline.jsx b/apis/nucleus/src/components/listbox/ListBoxInline.jsx index 29f195b78..418a3f481 100644 --- a/apis/nucleus/src/components/listbox/ListBoxInline.jsx +++ b/apis/nucleus/src/components/listbox/ListBoxInline.jsx @@ -13,7 +13,7 @@ import createListboxSelectionToolbar from './interactions/listbox-selection-tool import ActionsToolbar from '../ActionsToolbar'; import InstanceContext from '../../contexts/InstanceContext'; import ListBoxSearch from './components/ListBoxSearch'; -import { getListboxInlineKeyboardNavigation } from './interactions/listbox-keyboard-navigation'; +import { getListboxInlineKeyboardNavigation, getTouchSelection } from './interactions/listbox-keyboard-navigation'; import addListboxTheme from './assets/addListboxTheme'; import useAppSelections from '../../hooks/useAppSelections'; import showToolbarDetached from './interactions/listbox-show-toolbar-detached'; @@ -230,6 +230,8 @@ function ListBoxInline({ options, layout }) { selections, }); + const handleOnTouchStart = getTouchSelection({ selections, selectionState }); + const shouldAutoFocus = searchVisible && search === 'toggle'; const showSearchIcon = searchEnabled !== false && search === 'toggle'; const showSearchOrLockIcon = isLocked || showSearchIcon; @@ -282,6 +284,7 @@ function ListBoxInline({ options, layout }) { onKeyDown={handleKeyDown} onMouseEnter={handleOnMouseEnter} onMouseLeave={handleOnMouseLeave} + onTouchStart={handleOnTouchStart} ref={containerRef} hasIcon={showIcons} > diff --git a/apis/nucleus/src/components/listbox/interactions/listbox-keyboard-navigation.js b/apis/nucleus/src/components/listbox/interactions/listbox-keyboard-navigation.js index 1d41e3d07..ff61bd46d 100644 --- a/apis/nucleus/src/components/listbox/interactions/listbox-keyboard-navigation.js +++ b/apis/nucleus/src/components/listbox/interactions/listbox-keyboard-navigation.js @@ -1,4 +1,5 @@ import KEYS from '../../../keys'; +import { fillRange, getElemNumbersFromPages, selectValues } from '../hooks/selections/listbox-selections'; export function getFieldKeyboardNavigation({ select, @@ -207,3 +208,20 @@ export function getListboxInlineKeyboardNavigation({ return { handleKeyDown, handleOnMouseEnter, handleOnMouseLeave }; } + +export function getTouchSelection({ selections, selectionState }) { + const handleTouchStart = (event) => { + if (event?.touches?.length === 2) { + const startId = +event.touches[0].target.parentElement.getAttribute('data-n'); + const stopId = +event.touches[1].target.parentElement.getAttribute('data-n'); + const elements = getElemNumbersFromPages(selectionState.enginePages); + const elemNumbers = fillRange([startId, stopId], elements); + + // TODO: Keep already selected values + selectionState.updateItems(elemNumbers, true, elemNumbers); + // selectionState.setSelectableValuesUpdating(); + selectValues({ selections, elemNumbers, toggle: false, isSingleSelect: false }); + } + }; + return handleTouchStart; +} From 4ad669a586ab8c7462de8a658ce469b69fffed61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sjo=CC=88strand?= <99665802+DanielS-Qlik@users.noreply.github.com> Date: Wed, 29 Mar 2023 14:32:40 +0200 Subject: [PATCH 2/7] refactor: wip use select from listbox --- apis/nucleus/src/components/listbox/ListBox.jsx | 3 +++ .../nucleus/src/components/listbox/ListBoxInline.jsx | 4 +++- .../interactions/listbox-keyboard-navigation.js | 12 ++++++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/apis/nucleus/src/components/listbox/ListBox.jsx b/apis/nucleus/src/components/listbox/ListBox.jsx index f5c9b2fb1..2a759946b 100644 --- a/apis/nucleus/src/components/listbox/ListBox.jsx +++ b/apis/nucleus/src/components/listbox/ListBox.jsx @@ -42,6 +42,7 @@ export default function ListBox({ currentScrollIndex = { set: () => {} }, renderedCallback, onCtrlF, + selectRef, }) { const [initScrollPosIsSet, setInitScrollPosIsSet] = useState(false); const isSingleSelect = !!(layout && layout.qListObject.qDimensionInfo.qIsOneAndOnlyOne); @@ -114,6 +115,8 @@ export default function ListBox({ checkboxes, doc: document, }); + // eslint-disable-next-line no-param-reassign + selectRef.current.select = select; const { layoutOptions = {} } = layout || {}; diff --git a/apis/nucleus/src/components/listbox/ListBoxInline.jsx b/apis/nucleus/src/components/listbox/ListBoxInline.jsx index 418a3f481..16f21a403 100644 --- a/apis/nucleus/src/components/listbox/ListBoxInline.jsx +++ b/apis/nucleus/src/components/listbox/ListBoxInline.jsx @@ -122,6 +122,7 @@ function ListBoxInline({ options, layout }) { const [selectionState] = useState(() => createSelectionState()); const isInvalid = layout?.qListObject.qDimensionInfo.qError; const errorText = isInvalid && constraints.active ? 'Visualization.Invalid.Dimension' : 'Visualization.Incomplete'; + const selectRef = useRef({ select: null }); // TODO: Move useSelectionInteraction from Listbox to here? const { handleKeyDown, handleOnMouseEnter, handleOnMouseLeave } = getListboxInlineKeyboardNavigation({ setKeyboardActive, @@ -230,7 +231,7 @@ function ListBoxInline({ options, layout }) { selections, }); - const handleOnTouchStart = getTouchSelection({ selections, selectionState }); + const handleOnTouchStart = getTouchSelection({ selections, selectionState, selectRef }); const shouldAutoFocus = searchVisible && search === 'toggle'; const showSearchIcon = searchEnabled !== false && search === 'toggle'; @@ -400,6 +401,7 @@ function ListBoxInline({ options, layout }) { }} renderedCallback={renderedCallback} onCtrlF={onCtrlF} + selectRef={selectRef} /> )} diff --git a/apis/nucleus/src/components/listbox/interactions/listbox-keyboard-navigation.js b/apis/nucleus/src/components/listbox/interactions/listbox-keyboard-navigation.js index ff61bd46d..50e9cbeb2 100644 --- a/apis/nucleus/src/components/listbox/interactions/listbox-keyboard-navigation.js +++ b/apis/nucleus/src/components/listbox/interactions/listbox-keyboard-navigation.js @@ -1,5 +1,5 @@ import KEYS from '../../../keys'; -import { fillRange, getElemNumbersFromPages, selectValues } from '../hooks/selections/listbox-selections'; +import { fillRange, getElemNumbersFromPages /* , selectValues */ } from '../hooks/selections/listbox-selections'; export function getFieldKeyboardNavigation({ select, @@ -209,7 +209,7 @@ export function getListboxInlineKeyboardNavigation({ return { handleKeyDown, handleOnMouseEnter, handleOnMouseLeave }; } -export function getTouchSelection({ selections, selectionState }) { +export function getTouchSelection({ /* selections, */ selectionState, selectRef }) { const handleTouchStart = (event) => { if (event?.touches?.length === 2) { const startId = +event.touches[0].target.parentElement.getAttribute('data-n'); @@ -217,10 +217,14 @@ export function getTouchSelection({ selections, selectionState }) { const elements = getElemNumbersFromPages(selectionState.enginePages); const elemNumbers = fillRange([startId, stopId], elements); + selectRef.current?.select?.(elemNumbers, true); + + // TODO: Replace selectRef with below? // TODO: Keep already selected values - selectionState.updateItems(elemNumbers, true, elemNumbers); + // TODO: Select <=4 values does not work + // selectionState.updateItems(elemNumbers, true, elemNumbers); // selectionState.setSelectableValuesUpdating(); - selectValues({ selections, elemNumbers, toggle: false, isSingleSelect: false }); + // selectValues({ selections, elemNumbers, toggle: false, isSingleSelect: false }); } }; return handleTouchStart; From 35bbaf86d910f43a4a99a7c4f97efbfdae8a26d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sjo=CC=88strand?= <99665802+DanielS-Qlik@users.noreply.github.com> Date: Thu, 30 Mar 2023 10:48:44 +0200 Subject: [PATCH 3/7] refactor: wip, select on onTouchEnd --- .../src/components/listbox/ListBoxInline.jsx | 5 +-- .../ListBoxRowColumn/ListBoxRowColumn.jsx | 4 ++ .../selections/useSelectionsInteractions.js | 44 ++++++++++++++++++- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/apis/nucleus/src/components/listbox/ListBoxInline.jsx b/apis/nucleus/src/components/listbox/ListBoxInline.jsx index 16f21a403..8a82d8292 100644 --- a/apis/nucleus/src/components/listbox/ListBoxInline.jsx +++ b/apis/nucleus/src/components/listbox/ListBoxInline.jsx @@ -13,7 +13,7 @@ import createListboxSelectionToolbar from './interactions/listbox-selection-tool import ActionsToolbar from '../ActionsToolbar'; import InstanceContext from '../../contexts/InstanceContext'; import ListBoxSearch from './components/ListBoxSearch'; -import { getListboxInlineKeyboardNavigation, getTouchSelection } from './interactions/listbox-keyboard-navigation'; +import { getListboxInlineKeyboardNavigation } from './interactions/listbox-keyboard-navigation'; import addListboxTheme from './assets/addListboxTheme'; import useAppSelections from '../../hooks/useAppSelections'; import showToolbarDetached from './interactions/listbox-show-toolbar-detached'; @@ -231,8 +231,6 @@ function ListBoxInline({ options, layout }) { selections, }); - const handleOnTouchStart = getTouchSelection({ selections, selectionState, selectRef }); - const shouldAutoFocus = searchVisible && search === 'toggle'; const showSearchIcon = searchEnabled !== false && search === 'toggle'; const showSearchOrLockIcon = isLocked || showSearchIcon; @@ -285,7 +283,6 @@ function ListBoxInline({ options, layout }) { onKeyDown={handleKeyDown} onMouseEnter={handleOnMouseEnter} onMouseLeave={handleOnMouseLeave} - onTouchStart={handleOnTouchStart} ref={containerRef} hasIcon={showIcons} > diff --git a/apis/nucleus/src/components/listbox/components/ListBoxRowColumn/ListBoxRowColumn.jsx b/apis/nucleus/src/components/listbox/components/ListBoxRowColumn/ListBoxRowColumn.jsx index 01b4c3ae8..ab2303135 100644 --- a/apis/nucleus/src/components/listbox/components/ListBoxRowColumn/ListBoxRowColumn.jsx +++ b/apis/nucleus/src/components/listbox/components/ListBoxRowColumn/ListBoxRowColumn.jsx @@ -25,6 +25,8 @@ function RowColumn({ index, rowIndex, columnIndex, style, data }) { onMouseDown, onMouseUp, onMouseEnter, + onTouchStart, + onTouchEnd, pages, isLocked, column = false, @@ -196,6 +198,8 @@ function RowColumn({ index, rowIndex, columnIndex, style, data }) { onMouseUp={onMouseUp} onMouseEnter={onMouseEnter} onKeyDown={handleKeyDownCallback} + onTouchStart={onTouchStart} + onTouchEnd={onTouchEnd} onContextMenu={preventContextMenu} role={column ? 'column' : 'row'} tabIndex={isFirstElement && (!keyboard.enabled || keyboard.active) ? 0 : -1} diff --git a/apis/nucleus/src/components/listbox/hooks/selections/useSelectionsInteractions.js b/apis/nucleus/src/components/listbox/hooks/selections/useSelectionsInteractions.js index 81cd7cf37..0c6901025 100644 --- a/apis/nucleus/src/components/listbox/hooks/selections/useSelectionsInteractions.js +++ b/apis/nucleus/src/components/listbox/hooks/selections/useSelectionsInteractions.js @@ -1,6 +1,8 @@ import { useEffect, useCallback, useRef } from 'react'; import { selectValues, fillRange, getElemNumbersFromPages } from './listbox-selections'; +import rowColClasses from '../../components/ListBoxRowColumn/helpers/classes'; +const dataItemSelector = `.${rowColClasses.fieldRoot}`; const getKeyAsToggleSelected = (event) => !(event?.metaKey || event?.ctrlKey); export default function useSelectionsInteractions({ selectionState, selections, checkboxes = false, doc = document }) { @@ -10,6 +12,7 @@ export default function useSelectionsInteractions({ selectionState, selections, isRange: false, toggle: false, active: false, + touchElemNumbers: [], }); // eslint-disable-next-line arrow-body-style @@ -138,6 +141,45 @@ export default function useSelectionsInteractions({ selectionState, selections, addToRange(elemNumber); }, []); + const onTouchStart = useCallback((event) => { + // Handle range selection with two finger touch + if (currentSelect.current.active || currentSelect.current.isRange) { + return; + } + if (event.touches.length <= 1) { + return; + } + if (event.touches.length > 2) { + currentSelect.current.active = false; + doSelect(); + return; + } + const startElemNumber = Number(event.touches[0].target?.closest(dataItemSelector)?.getAttribute('data-n')); + const endElemNumber = Number(event.touches[1].target?.closest(dataItemSelector)?.getAttribute('data-n')); + + if (Number.isNaN(startElemNumber) || Number.isNaN(startElemNumber)) { + currentSelect.current.active = false; + doSelect(); + return; + } + + currentSelect.current.active = true; + currentSelect.current.touchElemNumbers = [startElemNumber, endElemNumber]; + }, []); + + const onTouchEnd = useCallback(() => { + if (!currentSelect.current.active || currentSelect.current.touchElemNumbers.length !== 2) { + return; + } + + // eslint-disable-next-line prefer-destructuring + currentSelect.current.startElemNumber = currentSelect.current.touchElemNumbers[0]; + addToRange(currentSelect.current.touchElemNumbers[1]); + currentSelect.current.active = false; + currentSelect.current.toggle = false; // TODO: Deselects previously selected items + doSelect(); + }, []); + useEffect(() => { doc.addEventListener('mouseup', onMouseUpDoc); return () => { @@ -169,7 +211,7 @@ export default function useSelectionsInteractions({ selectionState, selections, if (checkboxes) { Object.assign(interactionEvents, { onChange }); } else { - Object.assign(interactionEvents, { onMouseUp, onMouseDown, onMouseEnter }); + Object.assign(interactionEvents, { onMouseUp, onMouseDown, onMouseEnter, onTouchStart, onTouchEnd }); } return { From fa9058e56a00e07d70de3f734bbcd86fdc8562ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sjo=CC=88strand?= <99665802+DanielS-Qlik@users.noreply.github.com> Date: Thu, 30 Mar 2023 10:51:22 +0200 Subject: [PATCH 4/7] fix: remove select ref --- apis/nucleus/src/components/listbox/ListBox.jsx | 3 --- apis/nucleus/src/components/listbox/ListBoxInline.jsx | 2 -- 2 files changed, 5 deletions(-) diff --git a/apis/nucleus/src/components/listbox/ListBox.jsx b/apis/nucleus/src/components/listbox/ListBox.jsx index 2a759946b..f5c9b2fb1 100644 --- a/apis/nucleus/src/components/listbox/ListBox.jsx +++ b/apis/nucleus/src/components/listbox/ListBox.jsx @@ -42,7 +42,6 @@ export default function ListBox({ currentScrollIndex = { set: () => {} }, renderedCallback, onCtrlF, - selectRef, }) { const [initScrollPosIsSet, setInitScrollPosIsSet] = useState(false); const isSingleSelect = !!(layout && layout.qListObject.qDimensionInfo.qIsOneAndOnlyOne); @@ -115,8 +114,6 @@ export default function ListBox({ checkboxes, doc: document, }); - // eslint-disable-next-line no-param-reassign - selectRef.current.select = select; const { layoutOptions = {} } = layout || {}; diff --git a/apis/nucleus/src/components/listbox/ListBoxInline.jsx b/apis/nucleus/src/components/listbox/ListBoxInline.jsx index 8a82d8292..29f195b78 100644 --- a/apis/nucleus/src/components/listbox/ListBoxInline.jsx +++ b/apis/nucleus/src/components/listbox/ListBoxInline.jsx @@ -122,7 +122,6 @@ function ListBoxInline({ options, layout }) { const [selectionState] = useState(() => createSelectionState()); const isInvalid = layout?.qListObject.qDimensionInfo.qError; const errorText = isInvalid && constraints.active ? 'Visualization.Invalid.Dimension' : 'Visualization.Incomplete'; - const selectRef = useRef({ select: null }); // TODO: Move useSelectionInteraction from Listbox to here? const { handleKeyDown, handleOnMouseEnter, handleOnMouseLeave } = getListboxInlineKeyboardNavigation({ setKeyboardActive, @@ -398,7 +397,6 @@ function ListBoxInline({ options, layout }) { }} renderedCallback={renderedCallback} onCtrlF={onCtrlF} - selectRef={selectRef} /> )} From 7755ff6350d51f04298703534367126eb020b026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sjo=CC=88strand?= <99665802+DanielS-Qlik@users.noreply.github.com> Date: Fri, 31 Mar 2023 10:41:42 +0200 Subject: [PATCH 5/7] fix: only range select if range is > 7 --- .../selections/useSelectionsInteractions.js | 53 ++++++++++++------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/apis/nucleus/src/components/listbox/hooks/selections/useSelectionsInteractions.js b/apis/nucleus/src/components/listbox/hooks/selections/useSelectionsInteractions.js index 0c6901025..eb1a5a1d8 100644 --- a/apis/nucleus/src/components/listbox/hooks/selections/useSelectionsInteractions.js +++ b/apis/nucleus/src/components/listbox/hooks/selections/useSelectionsInteractions.js @@ -13,6 +13,7 @@ export default function useSelectionsInteractions({ selectionState, selections, toggle: false, active: false, touchElemNumbers: [], + touchRangeLow: false, }); // eslint-disable-next-line arrow-body-style @@ -26,14 +27,17 @@ export default function useSelectionsInteractions({ selectionState, selections, }); }; + const getRange = (start, end) => { + const elemNumbersOrdered = getElemNumbersFromPages(selectionState.enginePages); + return fillRange([start, end], elemNumbersOrdered); + }; + const addToRange = (elemNumber) => { const { startElemNumber } = currentSelect.current; if (startElemNumber === elemNumber) { return; } - const rangeEnds = [currentSelect.current.startElemNumber, elemNumber]; - const elemNumbersOrdered = getElemNumbersFromPages(selectionState.enginePages); - const toMaybeAdd = fillRange(rangeEnds, elemNumbersOrdered); + const toMaybeAdd = getRange(currentSelect.current.startElemNumber, elemNumber); selectionState.updateItems(toMaybeAdd, true, currentSelect.current.elemNumbers); }; @@ -143,40 +147,53 @@ export default function useSelectionsInteractions({ selectionState, selections, const onTouchStart = useCallback((event) => { // Handle range selection with two finger touch - if (currentSelect.current.active || currentSelect.current.isRange) { - return; - } - if (event.touches.length <= 1) { + if ( + currentSelect.current.active || + currentSelect.current.isRange || + selectionState.isSingleSelect || + event.touches.length <= 1 + ) { return; } if (event.touches.length > 2) { - currentSelect.current.active = false; doSelect(); return; } - const startElemNumber = Number(event.touches[0].target?.closest(dataItemSelector)?.getAttribute('data-n')); - const endElemNumber = Number(event.touches[1].target?.closest(dataItemSelector)?.getAttribute('data-n')); + const startTouchElemNumber = Number(event.touches[0].target?.closest(dataItemSelector)?.getAttribute('data-n')); + const endTouchElemNumber = Number(event.touches[1].target?.closest(dataItemSelector)?.getAttribute('data-n')); - if (Number.isNaN(startElemNumber) || Number.isNaN(startElemNumber)) { - currentSelect.current.active = false; + if (Number.isNaN(startTouchElemNumber) || Number.isNaN(startTouchElemNumber)) { doSelect(); return; } currentSelect.current.active = true; - currentSelect.current.touchElemNumbers = [startElemNumber, endElemNumber]; + const range = getRange(startTouchElemNumber, endTouchElemNumber); + if (range.length < 7) { + currentSelect.current.touchRangeLow = true; + } + + currentSelect.current.elemNumbers = []; + currentSelect.current.touchElemNumbers = [startTouchElemNumber, endTouchElemNumber]; }, []); const onTouchEnd = useCallback(() => { - if (!currentSelect.current.active || currentSelect.current.touchElemNumbers.length !== 2) { + if (currentSelect.current.touchElemNumbers.length !== 2) { + return; + } + if (currentSelect.current.touchRangeLow) { + currentSelect.current.touchRangeLow = false; + currentSelect.current.touchElemNumbers = []; + currentSelect.current.active = false; return; } - // eslint-disable-next-line prefer-destructuring - currentSelect.current.startElemNumber = currentSelect.current.touchElemNumbers[0]; - addToRange(currentSelect.current.touchElemNumbers[1]); + const [startTouchElemNumber, endTouchElemNumber] = currentSelect.current.touchElemNumbers; + currentSelect.current.startElemNumber = startTouchElemNumber; + addToRange(endTouchElemNumber); + currentSelect.current.touchElemNumbers = []; currentSelect.current.active = false; - currentSelect.current.toggle = false; // TODO: Deselects previously selected items + currentSelect.current.toggle = true; doSelect(); }, []); From 82992dced0eca92e1e4498e605869459b0dffa53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sjo=CC=88strand?= <99665802+DanielS-Qlik@users.noreply.github.com> Date: Fri, 31 Mar 2023 13:15:54 +0200 Subject: [PATCH 6/7] test: add test --- .../use-selections-interactions.test.jsx | 80 ++++++++++++++++++- .../selections/useSelectionsInteractions.js | 8 +- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/apis/nucleus/src/components/listbox/hooks/selections/__tests__/use-selections-interactions.test.jsx b/apis/nucleus/src/components/listbox/hooks/selections/__tests__/use-selections-interactions.test.jsx index 95a545667..49892113d 100644 --- a/apis/nucleus/src/components/listbox/hooks/selections/__tests__/use-selections-interactions.test.jsx +++ b/apis/nucleus/src/components/listbox/hooks/selections/__tests__/use-selections-interactions.test.jsx @@ -94,7 +94,13 @@ describe('use-listbox-interactions', () => { await render(); const arg0 = ref.current.result; expect(Object.keys(arg0).sort()).toEqual(['interactionEvents', 'select']); - expect(Object.keys(arg0.interactionEvents).sort()).toEqual(['onMouseDown', 'onMouseEnter', 'onMouseUp']); + expect(Object.keys(arg0.interactionEvents).sort()).toEqual([ + 'onMouseDown', + 'onMouseEnter', + 'onMouseUp', + 'onTouchEnd', + 'onTouchStart', + ]); }); test('With checkboxes', async () => { await render({ checkboxes: true }); @@ -210,7 +216,13 @@ describe('use-listbox-interactions', () => { await render(); const arg0 = ref.current.result; expect(Object.keys(arg0)).toEqual(['interactionEvents', 'select']); - expect(Object.keys(arg0.interactionEvents).sort()).toEqual(['onMouseDown', 'onMouseEnter', 'onMouseUp']); + expect(Object.keys(arg0.interactionEvents).sort()).toEqual([ + 'onMouseDown', + 'onMouseEnter', + 'onMouseUp', + 'onTouchEnd', + 'onTouchStart', + ]); }); test('should select a range (in theory)', async () => { @@ -288,6 +300,70 @@ describe('use-listbox-interactions', () => { }); }); + test('Should handle range select on two finger tap', async () => { + const createPage = (s24, s25, s26, s27, s28, s29, s30, s31) => + createPageWithRange( + { qElemNumber: 24, qState: s24 }, + { qElemNumber: 25, qState: s25 }, + { qElemNumber: 26, qState: s26 }, + { qElemNumber: 27, qState: s27 }, + { qElemNumber: 28, qState: s28 }, + { qElemNumber: 29, qState: s29 }, + { qElemNumber: 30, qState: s30 }, + { qElemNumber: 31, qState: s31 } + ); + + updateSelectionState({ + pages: createPage('O', 'O', 'O', 'O', 'O', 'O', 'O', 'O'), + }); + + await render(); + + await act(() => { + const touchOne = { + target: { + closest: () => ({ getAttribute: jest.fn().mockReturnValue('24') }), + }, + }; + const touchTwo = { + target: { + closest: () => ({ getAttribute: jest.fn().mockReturnValue('29') }), + }, + }; + + ref.current.result.interactionEvents.onTouchStart({ + touches: [touchOne, touchTwo], + }); + ref.current.result.interactionEvents.onTouchEnd(); + }); + // Touch range too small + expect(listboxSelections.selectValues).not.toHaveBeenCalled(); + + await act(() => { + const touchOne = { + target: { + closest: () => ({ getAttribute: jest.fn().mockReturnValue('24') }), + }, + }; + const touchTwo = { + target: { + closest: () => ({ getAttribute: jest.fn().mockReturnValue('30') }), + }, + }; + + ref.current.result.interactionEvents.onTouchStart({ + touches: [touchOne, touchTwo], + }); + ref.current.result.interactionEvents.onTouchEnd(); + }); + expect(listboxSelections.selectValues).toHaveBeenCalledWith({ + selections, + elemNumbers: [24, 25, 26, 27, 28, 29, 30], + isSingleSelect: false, + toggle: true, + }); + }); + test('Should "toggle" checkboxes', async () => { updateSelectionState({ pages: createPageWithSingle(24, 'O'), diff --git a/apis/nucleus/src/components/listbox/hooks/selections/useSelectionsInteractions.js b/apis/nucleus/src/components/listbox/hooks/selections/useSelectionsInteractions.js index eb1a5a1d8..113c9b5dc 100644 --- a/apis/nucleus/src/components/listbox/hooks/selections/useSelectionsInteractions.js +++ b/apis/nucleus/src/components/listbox/hooks/selections/useSelectionsInteractions.js @@ -13,7 +13,7 @@ export default function useSelectionsInteractions({ selectionState, selections, toggle: false, active: false, touchElemNumbers: [], - touchRangeLow: false, + touchRangeSmall: false, }); // eslint-disable-next-line arrow-body-style @@ -170,7 +170,7 @@ export default function useSelectionsInteractions({ selectionState, selections, currentSelect.current.active = true; const range = getRange(startTouchElemNumber, endTouchElemNumber); if (range.length < 7) { - currentSelect.current.touchRangeLow = true; + currentSelect.current.touchRangeSmall = true; } currentSelect.current.elemNumbers = []; @@ -181,8 +181,8 @@ export default function useSelectionsInteractions({ selectionState, selections, if (currentSelect.current.touchElemNumbers.length !== 2) { return; } - if (currentSelect.current.touchRangeLow) { - currentSelect.current.touchRangeLow = false; + if (currentSelect.current.touchRangeSmall) { + currentSelect.current.touchRangeSmall = false; currentSelect.current.touchElemNumbers = []; currentSelect.current.active = false; return; From ffaf757fa5fb8a8948789b4e37074b7aaaa2159c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sjo=CC=88strand?= <99665802+DanielS-Qlik@users.noreply.github.com> Date: Fri, 31 Mar 2023 16:44:45 +0200 Subject: [PATCH 7/7] fix: prevent gesture events in listbox --- .../src/components/listbox/ListBox.jsx | 1 + .../use-selections-interactions.test.jsx | 6 +++- .../selections/useSelectionsInteractions.js | 28 ++++++++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/apis/nucleus/src/components/listbox/ListBox.jsx b/apis/nucleus/src/components/listbox/ListBox.jsx index 4477a9f9c..9c340bf23 100644 --- a/apis/nucleus/src/components/listbox/ListBox.jsx +++ b/apis/nucleus/src/components/listbox/ListBox.jsx @@ -128,6 +128,7 @@ export default function ListBox({ selections, checkboxes, doc: document, + loaderRef, }); const { layoutOptions = {} } = layout || {}; diff --git a/apis/nucleus/src/components/listbox/hooks/selections/__tests__/use-selections-interactions.test.jsx b/apis/nucleus/src/components/listbox/hooks/selections/__tests__/use-selections-interactions.test.jsx index 49892113d..543173b28 100644 --- a/apis/nucleus/src/components/listbox/hooks/selections/__tests__/use-selections-interactions.test.jsx +++ b/apis/nucleus/src/components/listbox/hooks/selections/__tests__/use-selections-interactions.test.jsx @@ -25,6 +25,7 @@ describe('use-listbox-interactions', () => { let setPages; let layout; let updateSelectionState; + let loaderRef; beforeEach(() => { jest.spyOn(global.document, 'addEventListener').mockImplementation(jest.fn()); @@ -69,13 +70,16 @@ describe('use-listbox-interactions', () => { }; ref = React.createRef(); + loaderRef = { + current: null, + }; render = async (overrides = {}) => { await act(async () => { create( ); }); diff --git a/apis/nucleus/src/components/listbox/hooks/selections/useSelectionsInteractions.js b/apis/nucleus/src/components/listbox/hooks/selections/useSelectionsInteractions.js index 113c9b5dc..be0d0bbbe 100644 --- a/apis/nucleus/src/components/listbox/hooks/selections/useSelectionsInteractions.js +++ b/apis/nucleus/src/components/listbox/hooks/selections/useSelectionsInteractions.js @@ -1,3 +1,4 @@ +/* eslint-disable no-underscore-dangle */ import { useEffect, useCallback, useRef } from 'react'; import { selectValues, fillRange, getElemNumbersFromPages } from './listbox-selections'; import rowColClasses from '../../components/ListBoxRowColumn/helpers/classes'; @@ -5,7 +6,13 @@ import rowColClasses from '../../components/ListBoxRowColumn/helpers/classes'; const dataItemSelector = `.${rowColClasses.fieldRoot}`; const getKeyAsToggleSelected = (event) => !(event?.metaKey || event?.ctrlKey); -export default function useSelectionsInteractions({ selectionState, selections, checkboxes = false, doc = document }) { +export default function useSelectionsInteractions({ + selectionState, + selections, + checkboxes = false, + doc = document, + loaderRef, +}) { const currentSelect = useRef({ startElemNumber: undefined, elemNumbers: [], @@ -16,6 +23,25 @@ export default function useSelectionsInteractions({ selectionState, selections, touchRangeSmall: false, }); + useEffect(() => { + if (!loaderRef.current?._listRef?._outerRef) { + return undefined; + } + const preventGestureStart = (e) => e.preventDefault(); + const preventGestureChange = (e) => e.preventDefault(); + const preventGestureEnd = (e) => e.preventDefault(); + + const listRef = loaderRef.current._listRef._outerRef; + listRef.addEventListener('gesturestart', preventGestureStart); + listRef.addEventListener('gesturechange', preventGestureChange); + listRef.addEventListener('gestureend', preventGestureEnd); + return () => { + listRef.removeEventListener('gesturestart', preventGestureStart); + listRef.removeEventListener('gesturechange', preventGestureChange); + listRef.removeEventListener('gestureend', preventGestureEnd); + }; + }, [loaderRef.current?._listRef?._outerRef]); + // eslint-disable-next-line arrow-body-style const doSelect = () => { selectionState.setSelectableValuesUpdating();