diff --git a/src/__tests__/tab.js b/src/__tests__/tab.js index de9d0f46..fbd0493d 100644 --- a/src/__tests__/tab.js +++ b/src/__tests__/tab.js @@ -17,7 +17,7 @@ test('fires events when tabbing between two elements', () => { userEvent.tab() expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: div - + input#a[value=""] - keydown: Tab (9) input#a[value=""] - focusout input#b[value=""] - focusin @@ -45,7 +45,7 @@ test('does not change focus if default prevented on keydown', () => { userEvent.tab() expect(getEventSnapshot()).toMatchInlineSnapshot(` Events fired on: div - + input#a[value=""] - keydown: Tab (9) input#a[value=""] - keyup: Tab (9) `) @@ -418,6 +418,26 @@ test('should not focus disabled elements', () => { expect(five).toHaveFocus() }) +test('should not focus elements inside a hidden parent', () => { + setup(` +
+ + + +
`) + + const one = document.querySelector('[data-testid="one"]') + const three = document.querySelector('[data-testid="three"]') + + userEvent.tab() + expect(one).toHaveFocus() + + userEvent.tab() + expect(three).toHaveFocus() +}) + test('should keep focus on the document if there are no enabled, focusable elements', () => { setup(``) userEvent.tab() diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js index bdd0d0e8..680f5cfa 100644 --- a/src/__tests__/utils.js +++ b/src/__tests__/utils.js @@ -1,4 +1,5 @@ -import {isInstanceOfElement} from '../utils' +import { screen } from '@testing-library/dom' +import {isInstanceOfElement, isVisible} from '../utils' import {setup} from './helpers/utils' // isInstanceOfElement can be removed once the peerDependency for @testing-library/dom is bumped to a version that includes https://github.com/testing-library/dom-testing-library/pull/885 @@ -71,3 +72,19 @@ describe('check element type per isInstanceOfElement', () => { expect(() => isInstanceOfElement(element, 'HTMLSpanElement')).toThrow() }) }) + +test('check if element is visible', () => { + setup(` + + + + +
+ `) + + expect(isVisible(screen.getByTestId('visibleInput'))).toBe(true) + expect(isVisible(screen.getByTestId('styledDisplayedInput'))).toBe(true) + expect(isVisible(screen.getByTestId('styledHiddenInput'))).toBe(false) + expect(isVisible(screen.getByTestId('childInput'))).toBe(false) + expect(isVisible(screen.getByTestId('hiddenInput'))).toBe(false) +}) diff --git a/src/tab.js b/src/tab.js index 4bca7ff6..975e99ca 100644 --- a/src/tab.js +++ b/src/tab.js @@ -1,5 +1,5 @@ import {fireEvent} from '@testing-library/dom' -import {getActiveElement, FOCUSABLE_SELECTOR} from './utils' +import {getActiveElement, FOCUSABLE_SELECTOR, isVisible} from './utils' import {focus} from './focus' import {blur} from './blur' @@ -31,7 +31,11 @@ function tab({shift = false, focusTrap} = {}) { const enabledElements = [...focusableElements].filter( el => el === previousElement || - (el.getAttribute('tabindex') !== '-1' && !el.disabled), + (el.getAttribute('tabindex') !== '-1' && + !el.disabled && + // Hidden elements are not tabable + isVisible(el) + ), ) if (enabledElements.length === 0) return diff --git a/src/utils.js b/src/utils.js index 7b96a086..b4825948 100644 --- a/src/utils.js +++ b/src/utils.js @@ -293,6 +293,19 @@ function isClickableInput(element) { ) } +function isVisible(element) { + const getComputedStyle = getWindowFromNode(element).getComputedStyle + + for(; element && element.ownerDocument; element = element.parentNode) { + const display = getComputedStyle(element).display + if (display === 'none') { + return false + } + } + + return true +} + function eventWrapper(cb) { let result getConfig().eventWrapper(() => { @@ -367,4 +380,5 @@ export { getSelectionRange, isContentEditable, isInstanceOfElement, + isVisible, }