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 without priority when it has focus', () => {
98 const Second = () => {
99 const ref = useRef<HTMLDivElement>(null);
100 useDismissable(ref, () => {});
101 return <div ref={ref} />;
102 };
103
104 mount(
105 <main>
106 <button className="outside">outside</button>
107 <Dialog />
108 <Second />
109 </main>
110 );
111
112 cy.get('.inside').as('inside').should('be.visible');
113 // not dismissed with escape press
114 cy.realPress('Escape');
115 cy.get('@inside').should('be.visible');
116 // is dismissed when it has focus
117 cy.get('@inside').focus();
118 cy.realPress('Escape');
119 cy.get('@inside').should('not.be.visible');
120});
121
122it('is dismissed when focus moves out of it, with focus loss active', () => {
123 mount(
124 <main>
125 <button className="outside">outside</button>
126 <Dialog focusLoss />
127 </main>
128 );
129
130 cy.get('.inside').as('inside').should('be.visible');
131 cy.get('@inside').focus();
132 cy.get('@inside').should('be.visible');
133 // is dismissed when it loses focus
134 cy.realPress(['Shift', 'Tab']);
135 cy.focused().contains('outside');
136 cy.get('@inside').should('not.be.visible');
137});