diff --git a/apis/nucleus/package.json b/apis/nucleus/package.json index 9e60aba830..9e25b50e84 100644 --- a/apis/nucleus/package.json +++ b/apis/nucleus/package.json @@ -4,6 +4,7 @@ "private": true, "main": "src/index.js", "devDependencies": { + "@testing-library/react": "^14.0.0", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@mui/icons-material": "^5.11.11", diff --git a/apis/nucleus/src/components/listbox/components/__tests__/useTempKeyboard.test.js b/apis/nucleus/src/components/listbox/components/__tests__/useTempKeyboard.test.js new file mode 100644 index 0000000000..106773fd23 --- /dev/null +++ b/apis/nucleus/src/components/listbox/components/__tests__/useTempKeyboard.test.js @@ -0,0 +1,107 @@ +// import { fireEvent } from '@testing-library/react'; +import { renderHook, act, waitFor } from '@testing-library/react'; +import useTempKeyboard, { getVizCell, removeInnnerTabStops, removeLastFocused } from '../useTempKeyboard'; + +describe('removeInnnerTabStops', () => { + it('should reset tabIndex in elements with tabIndex="0"', () => { + const container = document.createElement('div'); + const button1 = document.createElement('button'); + button1.tabIndex = 0; + const button2 = document.createElement('button'); + button2.tabIndex = 1; + container.appendChild(button1); + container.appendChild(button2); + removeInnnerTabStops(container); + expect(button1.tabIndex).toBe(-1); + expect(button2.tabIndex).toBe(1); + }); + + it('should not throw when container is null or undefined', () => { + expect(() => removeInnnerTabStops(null)).not.toThrow(); + expect(() => removeInnnerTabStops(undefined)).not.toThrow(); + }); +}); + +describe('removeLastFocused', () => { + it('removes "last-focused" class from elements with that class', () => { + const container = document.createElement('div'); + const button1 = document.createElement('button'); + button1.classList.add('last-focused'); + const button2 = document.createElement('button'); + button2.classList.add('last-focused'); + container.appendChild(button1); + container.appendChild(button2); + removeLastFocused(container); + expect(button1.classList.contains('last-focused')).toBe(false); + expect(button2.classList.contains('last-focused')).toBe(false); + }); +}); + +describe('getVizCell', () => { + ['njs-cell', 'qv-gridcell'].forEach((cellClassName) => { + it(`returns the closest ancestor element with class ${cellClassName}`, () => { + const container = document.createElement('div'); + const cell = document.createElement('div'); + cell.classList.add(cellClassName); + const child = document.createElement('button'); + container.appendChild(cell); + cell.appendChild(child); + const result = getVizCell(child); + expect(result).toBe(cell); + }); + }); + + it('returns null if no ancestor element has the required class', () => { + const container = document.createElement('div'); + const child = document.createElement('button'); + container.appendChild(child); + const result = getVizCell(child); + expect(result).toBe(null); + }); +}); + +describe('useTempKeyboard', () => { + let containerRef; + let container; + + beforeEach(() => { + container = document.createElement('div'); + containerRef = { current: container }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return correct keyboard state', () => { + const enabled = true; + const innerTabStops = false; + + const { result } = renderHook(() => useTempKeyboard({ containerRef, enabled })); + const keyboard = result?.current || {}; + + expect(keyboard.enabled).toEqual(enabled); + expect(keyboard.active).toEqual(false); + expect(keyboard.innerTabStops).toEqual(innerTabStops); + expect(typeof keyboard.blur).toEqual('function'); + expect(typeof keyboard.focus).toEqual('function'); + expect(typeof keyboard.focusSelection).toEqual('function'); + }); + + it('should set keyboardActive to true when calling focus()', async () => { + const enabled = true; + + const { result } = renderHook(() => useTempKeyboard({ containerRef, enabled })); + const keyboard = result?.current || {}; + + expect(keyboard.active).toEqual(false); + + act(() => { + keyboard.focus(); + }); + + await waitFor(() => { + expect(keyboard.active).toEqual(true); + }); + }); +}); diff --git a/apis/nucleus/src/components/listbox/components/useTempKeyboard.js b/apis/nucleus/src/components/listbox/components/useTempKeyboard.js index 1e2ce0d9fe..a6b0bf793c 100644 --- a/apis/nucleus/src/components/listbox/components/useTempKeyboard.js +++ b/apis/nucleus/src/components/listbox/components/useTempKeyboard.js @@ -1,5 +1,3 @@ -import { useState } from 'react'; - export function removeInnnerTabStops(container) { container?.querySelectorAll('[tabIndex="0"]').forEach((elm) => { elm.setAttribute('tabIndex', -1); @@ -18,20 +16,22 @@ export function getVizCell(container) { // Emulate the keyboard hook, until we support it in the Listbox. export default function useTempKeyboard({ containerRef, enabled }) { - const [keyboardActive, setKeyboardActive] = useState(false); - - const innerTabStops = !enabled || keyboardActive; - const keyboard = { enabled, - active: keyboardActive, - innerTabStops, // does keyboard permit inner tab stops - outerTabStops: !innerTabStops, // does keyboard permit outer tab stops - blur: (resetFocus) => { + active: false, + }; + + Object.assign(keyboard, { + /** + * innerTabStops: whether keyboard permits inner tab stops + * (inner = everything inside .listbox-container) + */ + innerTabStops: !enabled || keyboard.active, + blur(resetFocus) { if (!enabled) { return; } - setKeyboardActive(false); + keyboard.active = false; const vizCell = getVizCell(containerRef.current) || containerRef.current?.parentElement; removeInnnerTabStops(containerRef.current); removeLastFocused(containerRef.current); @@ -41,11 +41,11 @@ export default function useTempKeyboard({ containerRef, enabled }) { vizCell.focus(); } }, - focus: () => { + focus() { if (!enabled) { return; } - setKeyboardActive(true); + keyboard.active = true; const c = containerRef.current; const searchField = c?.querySelector('.search input'); const lastSelectedRow = c?.querySelector('.value.last-focused'); @@ -55,12 +55,12 @@ export default function useTempKeyboard({ containerRef, enabled }) { elementToFocus?.setAttribute('tabIndex', 0); elementToFocus?.focus(); }, - focusSelection: () => { + focusSelection() { const confirmButton = document.querySelector('.actions-toolbar-default-actions .actions-toolbar-confirm'); confirmButton?.setAttribute('tabIndex', 0); confirmButton?.focus(); }, - }; + }); return keyboard; }