@@ -143,6 +149,7 @@ describe('useFocusTrap', () => {
);
}
render();
+
const middleButton = screen.getByTestId('middle-btn');
middleButton.focus();
@@ -154,24 +161,85 @@ describe('useFocusTrap', () => {
}).not.toThrow();
});
- it('should call preventDefault on Tab key when active', () => {
- // Tab key triggers preventDefault (even if focus cycling doesn't match)
- // because the handler is called with the Tab key
+ it('should call preventDefault on Tab when focus is on last element', () => {
const preventDefaultSpy = jest.spyOn(Event.prototype, 'preventDefault');
render();
+ const lastButton = screen.getByTestId('last-btn');
+ lastButton.focus();
+
fireEvent.keyDown(document, { key: 'Tab' });
- // Tab key should trigger the handler and check for focus cycling
- // The handler is at least called without crashing
- expect(() => {
- fireEvent.keyDown(document, { key: 'Tab', shiftKey: true });
- }).not.toThrow();
-
+ // The handler should call preventDefault because activeElement === lastElement
+ expect(preventDefaultSpy).toHaveBeenCalled();
preventDefaultSpy.mockRestore();
});
-
+ it('should call preventDefault on Shift+Tab when focus is on first element', () => {
+ const preventDefaultSpy = jest.spyOn(Event.prototype, 'preventDefault');
+ render();
+
+
+ fireEvent.keyDown(document, { key: 'Tab', shiftKey: true });
+
+ // The handler should call preventDefault because activeElement === firstElement
+ expect(preventDefaultSpy).toHaveBeenCalled();
+ preventDefaultSpy.mockRestore();
+ });
+
+ it('should not call preventDefault on Tab when focus is on middle element', () => {
+ function ThreeButtonTrap() {
+ const ref = useFocusTrap(true);
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+ const preventDefaultSpy = jest.spyOn(Event.prototype, 'preventDefault');
+ render();
+
+ const middleButton = screen.getByTestId('middle-btn');
+ middleButton.focus();
+
+ fireEvent.keyDown(document, { key: 'Tab' });
+
+ // Middle element is not the last, so preventDefault should NOT be called
+ expect(preventDefaultSpy).not.toHaveBeenCalled();
+ preventDefaultSpy.mockRestore();
+ });
+
+ it('should not call preventDefault on Shift+Tab when focus is on last element', () => {
+ function ThreeButtonTrap() {
+ const ref = useFocusTrap(true);
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+ const preventDefaultSpy = jest.spyOn(Event.prototype, 'preventDefault');
+ render();
+
+ const lastButton = screen.getByTestId('last-btn');
+ lastButton.focus();
+
+ fireEvent.keyDown(document, { key: 'Tab', shiftKey: true });
+
+ // Last element is not the first, so preventDefault should NOT be called
+ expect(preventDefaultSpy).not.toHaveBeenCalled();
+ preventDefaultSpy.mockRestore();
+ });
});
describe('Escape Key Behavior', () => {
@@ -374,7 +442,7 @@ describe('useFocusTrap', () => {
function ContainerWithDisabled() {
const ref = useFocusTrap(true);
return (
-
+
@@ -382,21 +450,24 @@ describe('useFocusTrap', () => {
}
render();
- const enabledButton = screen.getByTestId('enabled-btn');
- enabledButton.focus();
- // Only the enabled button is focusable, so it's both first and last.
- // Tab should cycle without crashing.
- expect(() => {
- fireEvent.keyDown(document, { key: 'Tab' });
- }).not.toThrow();
+
+ // After useEffect, the first non-disabled focusable element is focused
+ // Since the disabled button is excluded from focusable elements,
+ // the enabled button should be focused (it's the only focusable element).
+ // It's both first and last, so Tab should cycle to the same element.
+ const preventDefaultSpy = jest.spyOn(Event.prototype, 'preventDefault');
+ fireEvent.keyDown(document, { key: 'Tab' });
+ // preventDefault should be called because activeElement === lastElement (same element)
+ expect(preventDefaultSpy).toHaveBeenCalled();
+ preventDefaultSpy.mockRestore();
});
it('should handle hidden focusable elements', () => {
function ContainerWithHidden() {
const ref = useFocusTrap(true);
return (
-
+
@@ -404,13 +475,24 @@ describe('useFocusTrap', () => {
}
render();
+ // Override the prototype-level mock for the hidden element to test filtering
+ const hiddenBtn = screen.getByTestId('hidden-container').querySelector('button:first-child');
+ if (hiddenBtn) {
+ Object.defineProperty(hiddenBtn, 'offsetParent', {
+ value: null,
+ configurable: true,
+ });
+ }
+
const visibleButton = screen.getByTestId('visible-btn');
visibleButton.focus();
// Only the visible button is focusable, so it's both first and last.
- expect(() => {
- fireEvent.keyDown(document, { key: 'Tab' });
- }).not.toThrow();
+ // Tab should cycle the visible element (it's first and last)
+ const preventDefaultSpy = jest.spyOn(Event.prototype, 'preventDefault');
+ fireEvent.keyDown(document, { key: 'Tab' });
+ expect(preventDefaultSpy).toHaveBeenCalled();
+ preventDefaultSpy.mockRestore();
});
it('should not respond to non-Tab non-Escape keys', () => {
@@ -487,7 +569,7 @@ describe('useFocusTrap', () => {
function ManyFocusableTypes() {
const ref = useFocusTrap(true);
return (
-
+
link
@@ -499,24 +581,35 @@ describe('useFocusTrap', () => {
}
render();
-
- expect(() => {
- fireEvent.keyDown(document, { key: 'Tab' });
- }).not.toThrow();
+
+ // Focus the last focusable element (span with tabIndex=0) to trigger Tab cycling
+ const lastFocusable = screen.getByTestId('many-types').querySelector('[tabindex="0"]') as HTMLElement;
+ if (lastFocusable) lastFocusable.focus();
+
+ const preventDefaultSpy = jest.spyOn(Event.prototype, 'preventDefault');
+ fireEvent.keyDown(document, { key: 'Tab' });
+ // Multiple elements exist, so Tab should cycle
+ expect(preventDefaultSpy).toHaveBeenCalled();
+ preventDefaultSpy.mockRestore();
});
it('should handle container with only tabindex=-1 elements', () => {
function ContainerWithNegativeTabIndex() {
const ref = useFocusTrap(true);
return (
-
-
+
+
);
}
render();
+ const negBtn = screen.getByTestId('neg-btn');
+ negBtn.focus();
+
+ // tabindex=-1 is excluded by the selector, so no focusable elements
+ // Tab should not crash
expect(() => {
fireEvent.keyDown(document, { key: 'Tab' });
}).not.toThrow();