Skip to content

Commit

Permalink
fix(FocusManager): Replaces implementation with focus-trap-react
Browse files Browse the repository at this point in the history
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
  • Loading branch information
João Dias committed Jul 15, 2024
1 parent afe7190 commit d6e08f9
Show file tree
Hide file tree
Showing 25 changed files with 2,485 additions and 2,388 deletions.
92 changes: 48 additions & 44 deletions cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ declare global {
namespace Cypress {
interface Chainable {
tab(options?: Partial<{ shift: boolean }>): Chainable;

/**
* Presses the tab key until a predicate element is true.
* It accepts a callback for finding the target element, and an optional shift element to tab backwards.
* This commmand is specially useful to avoid chaining `.realPress("Tab")` multiple times before reaching an element.
*
* @requires `cypress-real-events` needs to be installed
*
* @example
*
* // Press tab until cypress finds the tab with the name "Transaction History"
* cy.tabUntil(() => cy.getTab("Transaction History"));
*
* // Press tab until cypress finds the tab with the name "Transaction History",
* // BUT travel backwards, using the `Shift+Tab` key combo
* cy.tabUntil(() => cy.getTab("Transaction History", true));
*/
tabUntil<GenericCallback extends Cypress.Chainable<JQuery<HTMLElement>>>(
element: () => GenericCallback,
shift?: boolean,
Expand All @@ -36,48 +53,35 @@ declare global {
}
}

/**
* Presses the tab key until a predicate element is true.
* It accepts a callback for finding the target element, and an optional shift element to tab backwards.
* This commmand is specially useful to avoid chaining `.realPress("Tab")` multiple times before reaching an element.
*
* @requires `cypress-real-events` needs to be installed
*
* @example
*
* // Press tab until cypress finds the tab with the name "Transaction History"
* cy.tabUntil(() => cy.getTab("Transaction History"));
*
* // Press tab until cypress finds the tab with the name "Transaction History",
* // BUT travel backwards, using the `Shift+Tab` key combo
* cy.tabUntil(() => cy.getTab("Transaction History", true));
*/
Cypress.Commands.add(
"tabUntil",
/**
* @param getElement
* @param shift
* @returns
*/
<GenericCallback extends Cypress.Chainable<JQuery<HTMLElement>>>(
getElement: () => GenericCallback,
shift = false,
) => {
return recurse(
() => getElement(),
/**
* Element assertion.
*
* @param {JQuery<HTMLElement>} $el
* @returns {boolean}
*/
($el: JQuery<HTMLElement>): boolean => $el.is(":focus"),
{
log: "Found the element!",
post() {
cy.focused().realPress(shift ? ["Shift", "Tab"] : "Tab");
},
function tab<GenericSubject>(
prevSubject: GenericSubject,
options: Partial<{ shift: boolean }> = { shift: false },
) {
return cy.wrap(prevSubject).realPress(options.shift ? ["Shift", "Tab"] : "Tab");
}

function tabUntil<GenericCallback extends Cypress.Chainable<JQuery<HTMLElement>>>(
getElement: () => GenericCallback,
shift = false,
) {
return recurse(
() => getElement(),
/**
* Element assertion.
*
* @param {JQuery<HTMLElement>} $el
* @returns {boolean}
*/
($el: JQuery<HTMLElement>): boolean => $el.is(":focus"),
{
log: "Found the element!",
post() {
cy.focused().realPress(shift ? ["Shift", "Tab"] : "Tab");
},
).should("have.focus");
},
);
},
).should("have.focus");
}

Cypress.Commands.add("tab", { prevSubject: ["element"] }, tab);

Cypress.Commands.add("tabUntil", tabUntil);
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* The copyright of this file belongs to Feedzai. The file cannot be
* reproduced in whole or in part, stored in a retrieval system, transmitted
* in any form, or by any means electronic, mechanical, or otherwise, without
* the prior permission of the owner. Please refer to the terms of the license
* agreement.
*
* (c) 2024 Feedzai, Rights Reserved.
*/
.table {
font-family: arial, sans-serif;
border-collapse: collapse;
width: 100%;
td,
th {
border: 1px solid #dddddd;
text-align: left;
padding: 8px;
}

tr:nth-child(even) {
background-color: #dddddd;
}

tr:focus-within {
outline: 2px solid blue;
background-color: lightblue;
}
}

.dialog {
position: absolute;
inset: 0;
margin: auto;
max-width: 50vw;
display: grid;
place-items: center;
background-color: white;
outline: 2px solid black;
height: 50vh;

&:focus-within {
outline-color: blue;
}
}
100 changes: 100 additions & 0 deletions cypress/test/components/focus-manager/demos/MultipleManagers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Please refer to the terms of the license
* agreement.
*
* (c) 2024 Feedzai, Rights Reserved.
*/
import React from "react";
import { useState } from "react";
import { FocusManager } from "src/components";
import styles from "./MultipleManagers.module.scss";

export function MultipleManagers() {
const [isOpen, setIsOpen] = useState(false);
const [isSecondOpen, setIsSecondOpen] = useState(false);
return (
<div>
<button type="button">I don't do anything</button>
<table className={styles.table}>
<tr>
<th>Company</th>
<th>Contact</th>
<th>Country</th>
</tr>
<tr>
<td>Alfreds Futterkiste</td>
<td>Maria Anders</td>
<td>
<button type="button">I don't do anything</button>
</td>
</tr>
<tr>
<td>Centro comercial Moctezuma</td>
<td>Francisco Chang</td>
<td>
<button type="button">I don't do anything</button>
</td>
</tr>
<tr>
<td>Ernst Handel</td>
<td>Roland Mendel</td>
<td>
<button type="button">I don't do anything</button>
</td>
</tr>
<tr>
<td>Island Trading</td>
<td>Helen Bennett</td>
<td>
<button type="button" onClick={() => setIsOpen(true)}>
Open Dialog
</button>
</td>
</tr>
<tr>
<td>Laughing Bacchus Winecellars</td>
<td>Yoshi Tannamuri</td>
<td>
{" "}
<button type="button">I don't do anything</button>
</td>
</tr>
<tr>
<td>Magazzini Alimentari Riuniti</td>
<td>Giovanni Rovelli</td>
<td>
{" "}
<button type="button">I don't do anything</button>
</td>
</tr>
</table>
{isOpen ? (
<div role="dialog" className={styles.dialog}>
<FocusManager>
<button type="button" onClick={() => setIsOpen(false)}>
Close Dialog
</button>
<input data-testid="fdz-js-input-1" />
<input data-testid="fdz-js-input-2" />
<input data-testid="fdz-js-input-3" />
<button type="button" onClick={() => setIsSecondOpen(!isSecondOpen)}>
Open Second Dialog
</button>
</FocusManager>
</div>
) : null}
{isSecondOpen ? (
<div role="dialog" className={styles.dialog}>
<FocusManager>
<button type="button" onClick={() => setIsSecondOpen(false)}>
Close Second Dialog
</button>
<input data-testid="fdz-js-input-4" />
<input data-testid="fdz-js-input-5" />
<input data-testid="fdz-js-input-6" />
</FocusManager>
</div>
) : null}
</div>
);
}
78 changes: 78 additions & 0 deletions cypress/test/components/focus-manager/demos/RestoreFocus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Please refer to the terms of the license
* agreement.
*
* (c) 2024 Feedzai, Rights Reserved.
*/
import React from "react";
import { useState } from "react";
import { FocusManager } from "src/components";
import styles from "./MultipleManagers.module.scss";

export function RestoreFocus() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<table className={styles.table}>
<tr>
<th>Company</th>
<th>Contact</th>
<th>Country</th>
</tr>
<tr>
<td>Alfreds Futterkiste</td>
<td>Maria Anders</td>
<td>
<button type="button">I don't do anything</button>
</td>
</tr>
<tr>
<td>Centro comercial Moctezuma</td>
<td>Francisco Chang</td>
<td>
<button type="button">I don't do anything</button>
</td>
</tr>
<tr>
<td>Ernst Handel</td>
<td>Roland Mendel</td>
<td>
<button type="button">I don't do anything</button>
</td>
</tr>
<tr>
<td>Island Trading</td>
<td>Helen Bennett</td>
<td>
<button type="button" onClick={() => setIsOpen(true)}>
Open Dialog
</button>
</td>
</tr>
<tr>
<td>Laughing Bacchus Winecellars</td>
<td>Yoshi Tannamuri</td>
<td>
<button type="button">I don't do anything</button>
</td>
</tr>
<tr>
<td>Magazzini Alimentari Riuniti</td>
<td>Giovanni Rovelli</td>
<td>
<button type="button">I don't do anything</button>
</td>
</tr>
</table>
{isOpen ? (
<div role="dialog" className={styles.dialog}>
<FocusManager>
<button type="button" onClick={() => setIsOpen(false)}>
Close Dialog
</button>
</FocusManager>
</div>
) : null}
</div>
);
}
8 changes: 8 additions & 0 deletions cypress/test/components/focus-manager/demos/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Please refer to the terms of the license
* agreement.
*
* (c) 2024 Feedzai, Rights Reserved.
*/
export * from "./MultipleManagers";
export * from "./RestoreFocus";
Loading

0 comments on commit d6e08f9

Please sign in to comment.