Skip to content

Commit 45adc66

Browse files
author
João Dias
committed
fix(FocusManager): Replaces implementation with focus-trap-react
In order to fix the bug reported on issue 2, this commit does a re-implementation of the FocusManager, by replacing the current functionality, with the functionality provided by the "focus-trap" package. In order to keep the same API as before, it makes them still available as before, but changes the default values for autoFocus, restoreFocus and contain props. It also exposes the "focus-trap" API on the "options". BREAKING CHANGE: autoFocus, restoreFocus and contain are now set to true by default Closes #27
1 parent afe7190 commit 45adc66

25 files changed

+2475
-2388
lines changed

cypress/support/commands.ts

Lines changed: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,23 @@ declare global {
2525
namespace Cypress {
2626
interface Chainable {
2727
tab(options?: Partial<{ shift: boolean }>): Chainable;
28+
29+
/**
30+
* Presses the tab key until a predicate element is true.
31+
* It accepts a callback for finding the target element, and an optional shift element to tab backwards.
32+
* This commmand is specially useful to avoid chaining `.realPress("Tab")` multiple times before reaching an element.
33+
*
34+
* @requires `cypress-real-events` needs to be installed
35+
*
36+
* @example
37+
*
38+
* // Press tab until cypress finds the tab with the name "Transaction History"
39+
* cy.tabUntil(() => cy.getTab("Transaction History"));
40+
*
41+
* // Press tab until cypress finds the tab with the name "Transaction History",
42+
* // BUT travel backwards, using the `Shift+Tab` key combo
43+
* cy.tabUntil(() => cy.getTab("Transaction History", true));
44+
*/
2845
tabUntil<GenericCallback extends Cypress.Chainable<JQuery<HTMLElement>>>(
2946
element: () => GenericCallback,
3047
shift?: boolean,
@@ -36,48 +53,35 @@ declare global {
3653
}
3754
}
3855

39-
/**
40-
* Presses the tab key until a predicate element is true.
41-
* It accepts a callback for finding the target element, and an optional shift element to tab backwards.
42-
* This commmand is specially useful to avoid chaining `.realPress("Tab")` multiple times before reaching an element.
43-
*
44-
* @requires `cypress-real-events` needs to be installed
45-
*
46-
* @example
47-
*
48-
* // Press tab until cypress finds the tab with the name "Transaction History"
49-
* cy.tabUntil(() => cy.getTab("Transaction History"));
50-
*
51-
* // Press tab until cypress finds the tab with the name "Transaction History",
52-
* // BUT travel backwards, using the `Shift+Tab` key combo
53-
* cy.tabUntil(() => cy.getTab("Transaction History", true));
54-
*/
55-
Cypress.Commands.add(
56-
"tabUntil",
57-
/**
58-
* @param getElement
59-
* @param shift
60-
* @returns
61-
*/
62-
<GenericCallback extends Cypress.Chainable<JQuery<HTMLElement>>>(
63-
getElement: () => GenericCallback,
64-
shift = false,
65-
) => {
66-
return recurse(
67-
() => getElement(),
68-
/**
69-
* Element assertion.
70-
*
71-
* @param {JQuery<HTMLElement>} $el
72-
* @returns {boolean}
73-
*/
74-
($el: JQuery<HTMLElement>): boolean => $el.is(":focus"),
75-
{
76-
log: "Found the element!",
77-
post() {
78-
cy.focused().realPress(shift ? ["Shift", "Tab"] : "Tab");
79-
},
56+
function tab<GenericSubject>(
57+
prevSubject: GenericSubject,
58+
options: Partial<{ shift: boolean }> = { shift: false },
59+
) {
60+
return cy.wrap(prevSubject).realPress(options.shift ? ["Shift", "Tab"] : "Tab");
61+
}
62+
63+
function tabUntil<GenericCallback extends Cypress.Chainable<JQuery<HTMLElement>>>(
64+
getElement: () => GenericCallback,
65+
shift = false,
66+
) {
67+
return recurse(
68+
() => getElement(),
69+
/**
70+
* Element assertion.
71+
*
72+
* @param {JQuery<HTMLElement>} $el
73+
* @returns {boolean}
74+
*/
75+
($el: JQuery<HTMLElement>): boolean => $el.is(":focus"),
76+
{
77+
log: "Found the element!",
78+
post() {
79+
cy.focused().realPress(shift ? ["Shift", "Tab"] : "Tab");
8080
},
81-
).should("have.focus");
82-
},
83-
);
81+
},
82+
).should("have.focus");
83+
}
84+
85+
Cypress.Commands.add("tab", { prevSubject: ["element"] }, tab);
86+
87+
Cypress.Commands.add("tabUntil", tabUntil);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* The copyright of this file belongs to Feedzai. The file cannot be
3+
* reproduced in whole or in part, stored in a retrieval system, transmitted
4+
* in any form, or by any means electronic, mechanical, or otherwise, without
5+
* the prior permission of the owner. Please refer to the terms of the license
6+
* agreement.
7+
*
8+
* (c) 2024 Feedzai, Rights Reserved.
9+
*/
10+
.table {
11+
font-family: arial, sans-serif;
12+
border-collapse: collapse;
13+
width: 100%;
14+
td,
15+
th {
16+
border: 1px solid #dddddd;
17+
text-align: left;
18+
padding: 8px;
19+
}
20+
21+
tr:nth-child(even) {
22+
background-color: #dddddd;
23+
}
24+
25+
tr:focus-within {
26+
outline: 2px solid blue;
27+
background-color: lightblue;
28+
}
29+
}
30+
31+
.dialog {
32+
position: absolute;
33+
inset: 0;
34+
margin: auto;
35+
max-width: 50vw;
36+
display: grid;
37+
place-items: center;
38+
background-color: white;
39+
outline: 2px solid black;
40+
height: 50vh;
41+
42+
&:focus-within {
43+
outline-color: blue;
44+
}
45+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Please refer to the terms of the license
3+
* agreement.
4+
*
5+
* (c) 2024 Feedzai, Rights Reserved.
6+
*/
7+
import React from "react";
8+
import { useState } from "react";
9+
import { FocusManager } from "src/components";
10+
import styles from "./MultipleManagers.module.scss";
11+
12+
export function MultipleManagers() {
13+
const [isOpen, setIsOpen] = useState(false);
14+
const [isSecondOpen, setIsSecondOpen] = useState(false);
15+
return (
16+
<div>
17+
<button type="button">I don't do anything</button>
18+
<table className={styles.table}>
19+
<tr>
20+
<th>Company</th>
21+
<th>Contact</th>
22+
<th>Country</th>
23+
</tr>
24+
<tr>
25+
<td>Alfreds Futterkiste</td>
26+
<td>Maria Anders</td>
27+
<td>
28+
<button type="button">I don't do anything</button>
29+
</td>
30+
</tr>
31+
<tr>
32+
<td>Centro comercial Moctezuma</td>
33+
<td>Francisco Chang</td>
34+
<td>
35+
<button type="button">I don't do anything</button>
36+
</td>
37+
</tr>
38+
<tr>
39+
<td>Ernst Handel</td>
40+
<td>Roland Mendel</td>
41+
<td>
42+
<button type="button">I don't do anything</button>
43+
</td>
44+
</tr>
45+
<tr>
46+
<td>Island Trading</td>
47+
<td>Helen Bennett</td>
48+
<td>
49+
<button type="button" onClick={() => setIsOpen(true)}>
50+
Open Dialog
51+
</button>
52+
</td>
53+
</tr>
54+
<tr>
55+
<td>Laughing Bacchus Winecellars</td>
56+
<td>Yoshi Tannamuri</td>
57+
<td>
58+
{" "}
59+
<button type="button">I don't do anything</button>
60+
</td>
61+
</tr>
62+
<tr>
63+
<td>Magazzini Alimentari Riuniti</td>
64+
<td>Giovanni Rovelli</td>
65+
<td>
66+
{" "}
67+
<button type="button">I don't do anything</button>
68+
</td>
69+
</tr>
70+
</table>
71+
{isOpen ? (
72+
<div role="dialog" className={styles.dialog}>
73+
<FocusManager>
74+
<button type="button" onClick={() => setIsOpen(false)}>
75+
Close Dialog
76+
</button>
77+
<input data-testid="fdz-js-input-1" />
78+
<input data-testid="fdz-js-input-2" />
79+
<input data-testid="fdz-js-input-3" />
80+
<button type="button" onClick={() => setIsSecondOpen(!isSecondOpen)}>
81+
Open Second Dialog
82+
</button>
83+
</FocusManager>
84+
</div>
85+
) : null}
86+
{isSecondOpen ? (
87+
<div role="dialog" className={styles.dialog}>
88+
<FocusManager>
89+
<button type="button" onClick={() => setIsSecondOpen(false)}>
90+
Close Second Dialog
91+
</button>
92+
<input data-testid="fdz-js-input-4" />
93+
<input data-testid="fdz-js-input-5" />
94+
<input data-testid="fdz-js-input-6" />
95+
</FocusManager>
96+
</div>
97+
) : null}
98+
</div>
99+
);
100+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Please refer to the terms of the license
3+
* agreement.
4+
*
5+
* (c) 2024 Feedzai, Rights Reserved.
6+
*/
7+
import React from "react";
8+
import { useState } from "react";
9+
import { FocusManager } from "src/components";
10+
import styles from "./MultipleManagers.module.scss";
11+
12+
export function RestoreFocus() {
13+
const [isOpen, setIsOpen] = useState(false);
14+
return (
15+
<div>
16+
<table className={styles.table}>
17+
<tr>
18+
<th>Company</th>
19+
<th>Contact</th>
20+
<th>Country</th>
21+
</tr>
22+
<tr>
23+
<td>Alfreds Futterkiste</td>
24+
<td>Maria Anders</td>
25+
<td>
26+
<button type="button">I don't do anything</button>
27+
</td>
28+
</tr>
29+
<tr>
30+
<td>Centro comercial Moctezuma</td>
31+
<td>Francisco Chang</td>
32+
<td>
33+
<button type="button">I don't do anything</button>
34+
</td>
35+
</tr>
36+
<tr>
37+
<td>Ernst Handel</td>
38+
<td>Roland Mendel</td>
39+
<td>
40+
<button type="button">I don't do anything</button>
41+
</td>
42+
</tr>
43+
<tr>
44+
<td>Island Trading</td>
45+
<td>Helen Bennett</td>
46+
<td>
47+
<button type="button" onClick={() => setIsOpen(true)}>
48+
Open Dialog
49+
</button>
50+
</td>
51+
</tr>
52+
<tr>
53+
<td>Laughing Bacchus Winecellars</td>
54+
<td>Yoshi Tannamuri</td>
55+
<td>
56+
<button type="button">I don't do anything</button>
57+
</td>
58+
</tr>
59+
<tr>
60+
<td>Magazzini Alimentari Riuniti</td>
61+
<td>Giovanni Rovelli</td>
62+
<td>
63+
<button type="button">I don't do anything</button>
64+
</td>
65+
</tr>
66+
</table>
67+
{isOpen ? (
68+
<div role="dialog" className={styles.dialog}>
69+
<FocusManager>
70+
<button type="button" onClick={() => setIsOpen(false)}>
71+
Close Dialog
72+
</button>
73+
</FocusManager>
74+
</div>
75+
) : null}
76+
</div>
77+
);
78+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* Please refer to the terms of the license
3+
* agreement.
4+
*
5+
* (c) 2024 Feedzai, Rights Reserved.
6+
*/
7+
export * from "./MultipleManagers";
8+
export * from "./RestoreFocus";

0 commit comments

Comments
 (0)