Skip to content

Commit a20bc43

Browse files
author
Kubit
committed
Improve focusHandlers
1 parent f700b47 commit a20bc43

File tree

3 files changed

+83
-89
lines changed

3 files changed

+83
-89
lines changed

src/utils/focusHandlers/__tests__/focusHandlers.test.tsx

Lines changed: 64 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import * as React from 'react';
2-
31
import {
42
focusFirstDescendant,
53
focusNextFocusableElement,
64
focusPreviousFocusableElement,
75
getFocusableDescendants,
8-
trapFocus,
6+
getFocusableDescendantsV2,
97
} from '../focusHandlers';
108

119
test('focusFirstDescendant - It focuses a button', () => {
@@ -87,61 +85,6 @@ test('focusFirstDescendant - It does not focus on an element with negative tabin
8785
expect(button).not.toHaveFocus();
8886
});
8987

90-
test('trapFocus - will focus on the first element when try to scape from forward or backward', () => {
91-
const focusableSection = document.createElement('section');
92-
focusableSection.id = 'test-section';
93-
94-
const button1Focusable = document.createElement('button');
95-
button1Focusable.id = 'button1Focusable';
96-
97-
const button2Focusable = document.createElement('button');
98-
button2Focusable.id = 'button2Focusable';
99-
100-
const button3Focusable = document.createElement('button');
101-
button3Focusable.id = 'button3Focusable';
102-
103-
focusableSection.appendChild(button1Focusable);
104-
focusableSection.appendChild(button2Focusable);
105-
focusableSection.appendChild(button3Focusable);
106-
107-
document.body.appendChild(focusableSection);
108-
109-
// Focus last element
110-
button3Focusable.focus();
111-
112-
// Trying to forward scape redirect the focus to the first one
113-
const mockEventForwardScape = {
114-
shiftKey: false,
115-
preventDefault: jest.fn(),
116-
} as unknown as React.KeyboardEvent<HTMLElement>;
117-
trapFocus(focusableSection, mockEventForwardScape);
118-
expect(document.activeElement).toBe(button1Focusable);
119-
120-
// Focus in the first element
121-
button1Focusable.focus();
122-
123-
// Trying to backward scape redirect the focus to the first one
124-
const mockEventBackwardScape = {
125-
shiftKey: true,
126-
preventDefault: jest.fn(),
127-
} as unknown as React.KeyboardEvent<HTMLElement>;
128-
trapFocus(focusableSection, mockEventBackwardScape);
129-
expect(document.activeElement).toBe(button3Focusable);
130-
});
131-
132-
test('trapFocus - It executes preventDefault if there are nothing to get the focus', () => {
133-
const element = document.createElement('div');
134-
135-
const mockPreventDefault = jest.fn();
136-
const event = {
137-
key: 'ArrowDown',
138-
preventDefault: mockPreventDefault,
139-
} as unknown as React.KeyboardEvent<HTMLInputElement>;
140-
141-
trapFocus(element, event);
142-
expect(mockPreventDefault).toHaveBeenCalled();
143-
});
144-
14588
test('focusNextFocusableElement - The next element focusable is the brother', () => {
14689
const element = document.createElement('div');
14790
const button1 = document.createElement('button');
@@ -208,3 +151,66 @@ test('focusPreviousFocusableElement - The previous element focusable is the brot
208151
const result = focusPreviousFocusableElement(button2);
209152
expect(result).toBeTruthy();
210153
});
154+
155+
describe('getFocusableDescendantsV2', () => {
156+
it('Filter disabled', () => {
157+
const element = document.createElement('div');
158+
const button = document.createElement('button');
159+
button.disabled = true;
160+
element.appendChild(button);
161+
162+
const result = getFocusableDescendantsV2({ element });
163+
expect(result).toHaveLength(0);
164+
});
165+
166+
it('Filter aria-disabled elements', () => {
167+
const element = document.createElement('div');
168+
const button = document.createElement('button');
169+
button.setAttribute('aria-disabled', 'true');
170+
element.appendChild(button);
171+
172+
const result = getFocusableDescendantsV2({ element });
173+
expect(result).toHaveLength(0);
174+
});
175+
176+
it('Filter aria-hidden attributes', () => {
177+
const element = document.createElement('div');
178+
const button = document.createElement('button');
179+
button.setAttribute('aria-hidden', 'true');
180+
element.appendChild(button);
181+
182+
const result = getFocusableDescendantsV2({ element });
183+
expect(result).toHaveLength(0);
184+
});
185+
186+
it('Filter elements to omit', () => {
187+
const element = document.createElement('div');
188+
const button = document.createElement('button');
189+
const omit = document.createElement('div');
190+
omit.appendChild(button);
191+
element.appendChild(omit);
192+
193+
const result = getFocusableDescendantsV2({ element, elementsToOmit: [omit] });
194+
expect(result).toHaveLength(0);
195+
});
196+
197+
it('Filter elements that are not summary and are inside a closed details', () => {
198+
const element = document.createElement('div');
199+
const button = document.createElement('button');
200+
const details = document.createElement('details');
201+
details.appendChild(button);
202+
element.appendChild(details);
203+
204+
const result = getFocusableDescendantsV2({ element });
205+
expect(result).toHaveLength(0);
206+
});
207+
208+
it('Return focusable elements', () => {
209+
const element = document.createElement('div');
210+
const button = document.createElement('button');
211+
element.appendChild(button);
212+
213+
const result = getFocusableDescendantsV2({ element });
214+
expect(result).toHaveLength(1);
215+
});
216+
});

src/utils/focusHandlers/focusHandlers.ts

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import * as React from 'react';
2-
31
const FOCUSABLE_QUERY_SELECTOR =
42
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]):not([tabindex="-1"]), summary, [tabindex]:not([tabindex="-1"])';
53

@@ -32,13 +30,25 @@ export const getFocusableDescendantsV2 = ({
3230
}): HTMLElement[] => {
3331
const focusableNodes = Array.from(
3432
element.querySelectorAll<HTMLElement>(FOCUSABLE_QUERY_SELECTOR)
35-
).filter(
36-
node =>
37-
!node.hasAttribute('aria-disabled:true') &&
38-
!node.getAttribute('aria-hidden:true') &&
39-
!node.hasAttribute('disabled') &&
40-
!elementsToOmit?.some(elementToOmit => elementToOmit.contains(node))
41-
);
33+
).filter(node => {
34+
// It is not focusable if it is disabled
35+
if (node.getAttribute('aria-disabled') === 'true' || node.hasAttribute('disabled')) {
36+
return false;
37+
}
38+
// It is not focusable if it is aria-hidden
39+
if (node.getAttribute('aria-hidden') === 'true') {
40+
return false;
41+
}
42+
// It is not focusable if it is inside elements to omit
43+
if (elementsToOmit?.some(elementToOmit => elementToOmit.contains(node))) {
44+
return false;
45+
}
46+
// It is not focusable if it is not a summary element and it is inside details closed element
47+
if (node.tagName !== 'SUMMARY' && node.closest('details:not([open])')) {
48+
return false;
49+
}
50+
return true;
51+
});
4252
return focusableNodes;
4353
};
4454

@@ -186,24 +196,3 @@ export const focusFirstDescendantV2 = ({
186196
}
187197
}
188198
};
189-
190-
export const trapFocus = (element: HTMLElement, e: React.KeyboardEvent<HTMLElement>): void => {
191-
const focusableElements = getFocusableDescendants(element);
192-
if (typeof focusableElements !== 'boolean' && focusableElements.length) {
193-
const firstFocusableElement = focusableElements[0];
194-
const lastFocusableElement = focusableElements[focusableElements.length - 1];
195-
if (e.shiftKey) {
196-
if (document.activeElement === firstFocusableElement) {
197-
lastFocusableElement?.focus();
198-
e.preventDefault();
199-
}
200-
} else {
201-
if (document.activeElement === lastFocusableElement) {
202-
firstFocusableElement?.focus();
203-
e.preventDefault();
204-
}
205-
}
206-
} else {
207-
e.preventDefault();
208-
}
209-
};

src/utils/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ export {
44
focusNextFocusableElement,
55
focusPreviousFocusableElement,
66
getFocusableDescendants,
7-
trapFocus,
87
} from './focusHandlers/focusHandlers';
98
export { getSizeWindowRem } from './getSizeWindowRem/getSizeWindowRem';
109
export { checkRegex } from './validations/checkRegex';

0 commit comments

Comments
 (0)