UNPKG

@carbon/ibm-products

Version:

Carbon for IBM Products

227 lines (217 loc) 9.64 kB
/** * Copyright IBM Corp. 2020, 2025 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ import { blockClass, HIERARCHICAL_VARIANT, focusThisItem, manageTabIndexAndFocus, traverseClockVise, traverseReverse, checkForHoldingKey, focusThisField } from './util.js'; const handleKeyDown = (evt, conditionBuilderRef, variant) => { const activeElement = document.activeElement; if (!activeElement.closest(`.${blockClass}__popover`)?.querySelector('[role="dialog"]')) { handleKeyPressForMainContent(evt, conditionBuilderRef, variant); } }; const handleKeyDownForPopover = (evt, conditionBuilderRef, popoverRef) => { if (excludeKeyPress(evt)) { return; } handleKeyPressForPopover(evt, popoverRef?.current, conditionBuilderRef); }; //skipping keyboard handling for date and time fields to get take carbon's const excludeKeyPress = evt => { return !['Escape'].includes(evt.key) && evt.target.closest(`.${blockClass}__item-date`); }; const handleKeyPressForPopover = (evt, parentContainer, conditionBuilderRef) => { const key = evt.key; const isHoldingShiftKey = checkForHoldingKey(evt, 'shiftKey'); const isMultiSelect = parentContainer.querySelector('ul')?.dataset.multiSelect; const isOptionInput = parentContainer.querySelectorAll(`[role="option"]`)?.length; let allItems = []; if (key === 'Escape') { //focus the corresponding field in which the popover is triggered from focusThisField(evt, conditionBuilderRef); evt.preventDefault(); evt.stopPropagation(); } if (isOptionInput) { switch (key) { case 'ArrowUp': evt.preventDefault(); //traverse through the popover options, search box, selectAll button parentContainer.querySelectorAll(`[role="option"]`).forEach((eachElem, index, allElements) => { traverseReverse(eachElem, index, allElements, null, null, conditionBuilderRef); }); //scroll to top when we reach a the top of the list to make search box visible if (Array.from(evt.target.closest('ul')?.querySelectorAll('li') ?? []).indexOf(evt.target) === 1) { parentContainer.querySelector(`.${blockClass}__popover-content-wrapper`).scrollTop = 0; } break; case 'ArrowDown': evt.preventDefault(); //traverse through the popover options, search box, selectAll button parentContainer.querySelectorAll(`[role="option"]`).forEach((eachElem, index, allElements) => { traverseClockVise(eachElem, index, allElements, null, null, conditionBuilderRef); }); break; case 'Tab': allItems = [...Array.from(parentContainer.querySelectorAll(`.${blockClass}__selectAll-button,[role="searchbox"]`)), parentContainer.querySelector(`[role="option"]`)]; allItems.forEach((eachElem, index, allElements) => { if (isHoldingShiftKey) { traverseReverse(eachElem, index, allElements, true, true, conditionBuilderRef); } else { traverseClockVise(eachElem, index, allElements, true, true, conditionBuilderRef); } }); evt.preventDefault(); break; case ' ': if (isMultiSelect === 'true') { if (document.activeElement.type !== 'button') { //for button , enter key is click which already handled by framework, for other elements trigger click document.activeElement?.click(); } evt.preventDefault(); } break; case 'Enter': if (document.activeElement.type !== 'button') { //for button , enter key is click which already handled by framework, else trigger click focusThisField(evt, conditionBuilderRef); document.activeElement?.click(); } break; } } else { if (key === 'Enter' && !isHoldingShiftKey) { if (document.activeElement.type !== 'button') { //for button , enter key is click which already handled by framework, else trigger click focusThisField(evt, conditionBuilderRef); document.activeElement?.click(); } } } }; const handleKeyPressForMainContent = (evt, conditionBuilderRef, variant) => { switch (evt.key) { case 'ArrowRight': evt.preventDefault(); if (variant == HIERARCHICAL_VARIANT) { let allCellsInRow = Array.from(evt.target.closest('[role="row"]')?.querySelectorAll('[role="gridcell"] button')); if (allCellsInRow.length === 1) { evt.target = evt.target.closest('[role="row"]'); handleRowNavigationHierarchical(evt, conditionBuilderRef, variant); //focus next row } else if (evt.target.getAttribute('role') == 'row') { //when current focus is on a row, then we need to enter inside and focus the first cell of that row //focus first cell manageTabIndexAndFocus(allCellsInRow[0], conditionBuilderRef); } else { //finding the next cell to be focussed //next cell = current cell index + 1 let currentItemIndex = allCellsInRow.indexOf(evt.target); if (currentItemIndex < allCellsInRow.length - 1) { focusThisItem(allCellsInRow[currentItemIndex + 1], conditionBuilderRef); } } } else { handleCellNavigation(evt, conditionBuilderRef); } break; case 'ArrowLeft': evt.preventDefault(); if (variant == HIERARCHICAL_VARIANT) { if (evt.target.getAttribute('role') !== 'row') { //when any cell is focussed, arrow left will select the previous cell or current row let allCellsInRow = Array.from(evt.target.closest('[role="row"]')?.querySelectorAll('[role="gridcell"] button')); let currentItemIndex = allCellsInRow.indexOf(evt.target); if (currentItemIndex > 0) { focusThisItem(allCellsInRow[currentItemIndex - 1], conditionBuilderRef); } else { //focus the row let wrapper = evt.target.closest(`[role="row"]`); manageTabIndexAndFocus(wrapper, conditionBuilderRef); } } } else { handleCellNavigation(evt, conditionBuilderRef); } break; case 'ArrowUp': case 'ArrowDown': evt.preventDefault(); if (variant == HIERARCHICAL_VARIANT) { handleRowNavigationHierarchical(evt, conditionBuilderRef, variant); } else { handleRowNavigation(evt, conditionBuilderRef, variant); } break; case 'Enter': if (document.activeElement?.querySelectorAll(`button`)?.length === 1) { document.activeElement?.querySelectorAll(`button`)[0]?.click(); } break; } }; const getRows = conditionBuilderRef => { return Array.from(conditionBuilderRef.current?.querySelectorAll('[role="row"]')); }; const getRowIndex = (element, conditionBuilderRef) => { const rows = getRows(conditionBuilderRef); return rows.findIndex(row => row.contains(element)); }; const handleRowNavigation = (evt, conditionBuilderRef, variant) => { const rows = getRows(conditionBuilderRef); const currentRowIndex = getRowIndex(evt.target, conditionBuilderRef); navigateToNextRowCell(evt, currentRowIndex, rows, variant, conditionBuilderRef); }; const handleRowNavigationHierarchical = (evt, conditionBuilderRef, variant) => { const rows = getRows(conditionBuilderRef); const currentRowIndex = getRowIndex(evt.target, conditionBuilderRef); let nextRowIndex = currentRowIndex; if (evt.target.getAttribute('role') == 'row') { if (['ArrowDown', 'ArrowRight'].includes(evt.key)) { nextRowIndex += 1; } else if (evt.key === 'ArrowUp') { nextRowIndex -= 1; } //maintaining selection for first and last rows if (nextRowIndex < 0) { nextRowIndex = 0; } else if (nextRowIndex >= rows.length) { nextRowIndex = rows.length - 1; } if (nextRowIndex !== currentRowIndex) { manageTabIndexAndFocus(rows[nextRowIndex], conditionBuilderRef); } } else { navigateToNextRowCell(evt, currentRowIndex, rows, variant, conditionBuilderRef); } }; const navigateToNextRowCell = (evt, currentRowIndex, rows, variant, conditionBuilderRef) => { //when the current focussed element is a cell of any row, arrow up/down will focus the next row same cell. let nextRowIndex = currentRowIndex; if (evt.key === 'ArrowUp') { nextRowIndex = currentRowIndex == 0 ? currentRowIndex : currentRowIndex - 1; } if (evt.key === 'ArrowDown') { nextRowIndex = currentRowIndex === rows.length - 1 ? rows.length - 1 : currentRowIndex + 1; } const nextRow = rows[nextRowIndex]; const itemName = evt.target.dataset.name; if (nextRow?.querySelector(`[data-name="${itemName}"]`)) { manageTabIndexAndFocus(nextRow?.querySelector(`[data-name="${itemName}"]`), conditionBuilderRef); } else if (variant === HIERARCHICAL_VARIANT) { //when the next row is a if statement , then that row is focused. From any cell of last row of an group , arrow down select the next row (if) manageTabIndexAndFocus(nextRow, conditionBuilderRef); } }; const handleCellNavigation = (evt, conditionBuilderRef) => { conditionBuilderRef.current.querySelectorAll(`[role="gridcell"] button`).forEach((eachElem, index, allElements) => { if (evt.key === 'ArrowRight') { traverseClockVise(eachElem, index, allElements, null, null, conditionBuilderRef); } else { traverseReverse(eachElem, index, allElements, null, null, conditionBuilderRef); } }); }; export { handleKeyDown, handleKeyDownForPopover };