Skip to content

Commit eb40389

Browse files
fix: test
1 parent 01a93c6 commit eb40389

File tree

2 files changed

+241
-3
lines changed

2 files changed

+241
-3
lines changed

tests/focus.test.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,6 @@ describe('Trigger focus management', () => {
252252
const popup = document.querySelector('.rc-trigger-popup')!;
253253
expect(popup).toBeTruthy();
254254

255-
const btnA = Array.from(document.querySelectorAll('button')).find(
256-
(b) => b.textContent === 'a',
257-
)!;
258255
const btnB = Array.from(document.querySelectorAll('button')).find(
259256
(b) => b.textContent === 'b',
260257
)!;

tests/focusUtils.test.tsx

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import { spyElementPrototypes } from '@rc-component/util/lib/test/domHook';
2+
import * as React from 'react';
3+
import {
4+
focusPopupRootOrFirst,
5+
getTabbableEdges,
6+
handlePopupTabTrap,
7+
} from '../src/focusUtils';
8+
9+
describe('focusUtils', () => {
10+
let eleRect = { width: 100, height: 100 };
11+
12+
beforeAll(() => {
13+
spyElementPrototypes(HTMLElement, {
14+
offsetWidth: { get: () => eleRect.width },
15+
offsetHeight: { get: () => eleRect.height },
16+
offsetParent: { get: () => document.body },
17+
});
18+
spyElementPrototypes(HTMLButtonElement, {
19+
offsetWidth: { get: () => eleRect.width },
20+
offsetHeight: { get: () => eleRect.height },
21+
offsetParent: { get: () => document.body },
22+
});
23+
});
24+
25+
beforeEach(() => {
26+
eleRect = { width: 100, height: 100 };
27+
});
28+
29+
function mount(el: HTMLElement) {
30+
document.body.appendChild(el);
31+
return () => {
32+
document.body.removeChild(el);
33+
};
34+
}
35+
36+
it('getTabbableEdges returns nulls when there are no tabbables', () => {
37+
const el = document.createElement('div');
38+
expect(getTabbableEdges(el)).toEqual([null, null]);
39+
});
40+
41+
it('getTabbableEdges returns first and last for multiple buttons', () => {
42+
const el = document.createElement('div');
43+
el.innerHTML =
44+
'<button type="button" id="a">a</button><button type="button" id="b">b</button>';
45+
const unmount = mount(el);
46+
const [first, last] = getTabbableEdges(el);
47+
expect(first?.id).toBe('a');
48+
expect(last?.id).toBe('b');
49+
unmount();
50+
});
51+
52+
it('getTabbableEdges uses one node for first and last when only one tabbable', () => {
53+
const el = document.createElement('div');
54+
el.innerHTML = '<button type="button" id="only">x</button>';
55+
const unmount = mount(el);
56+
const [first, last] = getTabbableEdges(el);
57+
expect(first).toBe(last);
58+
expect(first?.id).toBe('only');
59+
unmount();
60+
});
61+
62+
it('focusPopupRootOrFirst focuses container when there are no tabbables', () => {
63+
const el = document.createElement('div');
64+
document.body.appendChild(el);
65+
const result = focusPopupRootOrFirst(el);
66+
expect(result).toBe(el);
67+
expect(document.activeElement).toBe(el);
68+
expect(el.getAttribute('tabindex')).toBe('-1');
69+
document.body.removeChild(el);
70+
});
71+
72+
it('findTabbableEl walks into open shadow roots', () => {
73+
const host = document.createElement('div');
74+
const unmount = mount(host);
75+
const shadow = host.attachShadow({ mode: 'open', delegatesFocus: false });
76+
const btn = document.createElement('button');
77+
btn.type = 'button';
78+
btn.id = 'in-shadow';
79+
shadow.appendChild(btn);
80+
81+
const [first] = getTabbableEdges(host);
82+
expect(first?.id).toBe('in-shadow');
83+
84+
unmount();
85+
});
86+
87+
it('skips shadow host subtree when host has tabindex -1', () => {
88+
const host = document.createElement('div');
89+
host.setAttribute('tabindex', '-1');
90+
document.body.appendChild(host);
91+
const shadow = host.attachShadow({ mode: 'open' });
92+
const btn = document.createElement('button');
93+
btn.type = 'button';
94+
btn.id = 'skipped';
95+
shadow.appendChild(btn);
96+
97+
const [first] = getTabbableEdges(host);
98+
expect(first).toBeNull();
99+
100+
document.body.removeChild(host);
101+
});
102+
103+
it('findTabbableEl walks slotted light-DOM content', () => {
104+
const host = document.createElement('div');
105+
const shadow = host.attachShadow({ mode: 'open' });
106+
const slot = document.createElement('slot');
107+
slot.setAttribute('name', 's');
108+
shadow.appendChild(slot);
109+
110+
const slotted = document.createElement('button');
111+
slotted.type = 'button';
112+
slotted.id = 'slotted';
113+
slotted.setAttribute('slot', 's');
114+
host.appendChild(slotted);
115+
116+
const unmount = mount(host);
117+
const [first] = getTabbableEdges(host);
118+
expect(first?.id).toBe('slotted');
119+
120+
unmount();
121+
});
122+
123+
it('handlePopupTabTrap ignores non-Tab keys', () => {
124+
const el = document.createElement('div');
125+
el.innerHTML = '<button type="button">a</button>';
126+
const unmount = mount(el);
127+
const btn = el.querySelector('button')!;
128+
btn.focus();
129+
130+
const ev = new KeyboardEvent('keydown', {
131+
key: 'Escape',
132+
bubbles: true,
133+
cancelable: true,
134+
});
135+
const preventDefault = jest.spyOn(ev, 'preventDefault');
136+
handlePopupTabTrap(ev as unknown as React.KeyboardEvent, el);
137+
138+
expect(preventDefault).not.toHaveBeenCalled();
139+
unmount();
140+
});
141+
142+
it('handlePopupTabTrap respects defaultPrevented', () => {
143+
const el = document.createElement('div');
144+
el.innerHTML = '<button type="button">a</button><button type="button">b</button>';
145+
const unmount = mount(el);
146+
el.querySelectorAll('button')[1].focus();
147+
148+
const ev = {
149+
key: 'Tab',
150+
shiftKey: false,
151+
defaultPrevented: true,
152+
preventDefault: jest.fn(),
153+
} as unknown as React.KeyboardEvent;
154+
handlePopupTabTrap(ev, el);
155+
expect(ev.preventDefault).not.toHaveBeenCalled();
156+
157+
unmount();
158+
});
159+
160+
it('handlePopupTabTrap does nothing when activeElement is outside container', () => {
161+
const outer = document.createElement('button');
162+
outer.type = 'button';
163+
outer.id = 'outer';
164+
const inner = document.createElement('div');
165+
inner.innerHTML = '<button type="button">a</button>';
166+
document.body.appendChild(outer);
167+
document.body.appendChild(inner);
168+
outer.focus();
169+
170+
const ev = {
171+
key: 'Tab',
172+
shiftKey: false,
173+
defaultPrevented: false,
174+
preventDefault: jest.fn(),
175+
} as unknown as React.KeyboardEvent;
176+
handlePopupTabTrap(ev, inner);
177+
178+
expect(ev.preventDefault).not.toHaveBeenCalled();
179+
document.body.removeChild(outer);
180+
document.body.removeChild(inner);
181+
});
182+
183+
it('handlePopupTabTrap prevents default when no tabbables and focus on container', () => {
184+
const el = document.createElement('div');
185+
el.setAttribute('tabindex', '-1');
186+
const unmount = mount(el);
187+
el.focus();
188+
189+
const ev = {
190+
key: 'Tab',
191+
shiftKey: false,
192+
defaultPrevented: false,
193+
preventDefault: jest.fn(),
194+
} as unknown as React.KeyboardEvent;
195+
handlePopupTabTrap(ev, el);
196+
197+
expect(ev.preventDefault).toHaveBeenCalled();
198+
unmount();
199+
});
200+
201+
it('handlePopupTabTrap moves focus from first to last with Shift+Tab', () => {
202+
const el = document.createElement('div');
203+
el.innerHTML =
204+
'<button type="button" id="x">a</button><button type="button" id="y">b</button>';
205+
const unmount = mount(el);
206+
el.querySelector('#x')!.focus();
207+
208+
const ev = {
209+
key: 'Tab',
210+
shiftKey: true,
211+
defaultPrevented: false,
212+
preventDefault: jest.fn(),
213+
} as unknown as React.KeyboardEvent;
214+
handlePopupTabTrap(ev, el);
215+
216+
expect(ev.preventDefault).toHaveBeenCalled();
217+
expect(document.activeElement?.id).toBe('y');
218+
219+
unmount();
220+
});
221+
222+
it('skips disabled buttons and hidden inputs', () => {
223+
const el = document.createElement('div');
224+
el.innerHTML =
225+
'<button type="button" disabled id="d">d</button><input type="hidden" /><button type="button" id="ok">ok</button>';
226+
const unmount = mount(el);
227+
const [first] = getTabbableEdges(el);
228+
expect(first?.id).toBe('ok');
229+
unmount();
230+
});
231+
232+
it('skips elements inside closed details (except summary)', () => {
233+
const el = document.createElement('div');
234+
el.innerHTML =
235+
'<details><summary>s</summary><button type="button" id="hidden">h</button></details><button type="button" id="ok">ok</button>';
236+
const unmount = mount(el);
237+
const [first] = getTabbableEdges(el);
238+
expect(first?.id).toBe('ok');
239+
unmount();
240+
});
241+
});

0 commit comments

Comments
 (0)