Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
1import React, { useState, useRef } from 'react';
2import { mount } from '@cypress/react';
3
4import { useDismissable } from '../useDismissable';
5
6const Dialog = ({ focusLoss }: { focusLoss?: boolean }) => {
7 const [visible, setVisible] = useState(true);
8 const ref = useRef<HTMLDivElement>(null);
9
10 const onDismiss = () => setVisible(false);
11 useDismissable(ref, onDismiss, { focusLoss, disabled: !visible });
12
13 return (
14 <div ref={ref} role="dialog" style={{ display: visible ? 'block' : 'none' }}>
15 <button className="inside">focusable</button>
16 </div>
17 );
18};
19
20it('is dismissed by clicking outside', () => {
21 mount(
22 <main>
23 <button className="outside">outside</button>
24 <Dialog />
25 </main>
26 );
27
28 cy.get('.inside').as('inside').realClick();
29 cy.get('@inside').should('be.visible');
30 cy.get('.outside').first().realClick();
31 cy.get('@inside').should('not.be.visible');
32});
33
34it('is not dismissed by clicking outside when it does not have priority', () => {
35 mount(
36 <main>
37 <button className="outside">outside</button>
38 <Dialog />
39 <Dialog />
40 </main>
41 );
42
43 cy.get('.inside').as('inside').should('be.visible');
44 // at first not dismissed
45 cy.get('.outside').first().realClick();
46 cy.get('@inside').should('be.visible');
47 // dismissed when the second Dialog loses focus
48 cy.get('.outside').first().realClick();
49 cy.get('@inside').should('not.be.visible');
50});
51
52it('is dismissed by tapping outside', () => {
53 mount(
54 <main>
55 <button className="outside">outside</button>
56 <Dialog />
57 </main>
58 );
59
60 cy.get('.inside').as('inside').realClick();
61 cy.get('@inside').should('be.visible');
62 cy.get('.outside').first().realTouch();
63 cy.get('@inside').should('not.be.visible');
64});
65
66it('is dismissed by pressing Escape', () => {
67 mount(
68 <main>
69 <button className="outside">outside</button>
70 <Dialog />
71 </main>
72 );
73
74 cy.get('.inside').as('inside').should('be.visible');
75 cy.realPress('Escape');
76 cy.get('@inside').should('not.be.visible');
77});
78
79it('is not dismissed by pressing Escape when it does not have priority', () => {
80 mount(
81 <main>
82 <button className="outside">outside</button>
83 <Dialog />
84 <Dialog />
85 </main>
86 );
87
88 cy.get('.inside').as('inside').should('be.visible');
89 // at first not dismissed
90 cy.realPress('Escape');
91 cy.get('@inside').should('be.visible');
92 // dismissed when the second Dialog loses focus
93 cy.realPress('Escape');
94 cy.get('@inside').should('not.be.visible');
95});
96
97it('is dismissed when focus moves out of it, with focus loss active', () => {
98 mount(
99 <main>
100 <button className="outside">outside</button>
101 <Dialog focusLoss />
102 </main>
103 );
104
105 cy.get('.inside').as('inside').should('be.visible');
106 cy.get('@inside').focus();
107 cy.get('@inside').should('be.visible');
108 // is dismissed when it loses focus
109 cy.realPress(['Shift', 'Tab']);
110 cy.focused().contains('outside');
111 cy.get('@inside').should('not.be.visible');
112});