Mirror: React hooks for accessible, common web interactions. UI super powers without the UI.
1import React, { ReactNode, useState, useReducer, useLayoutEffect, useRef } from 'react';
2import { mount } from '@cypress/react';
3
4import { makePriorityHook } from '../usePriority';
5
6const usePriority = makePriorityHook();
7
8const FocusOnPriority = (
9 { id, children = null }:
10 { id: string, children?: ReactNode }
11) => {
12 const forceUpdate = useReducer(() => [], [])[1]
13 const ref = useRef<HTMLDivElement>(null);
14 const hasPriority = usePriority(ref);
15
16 if (!(hasPriority as any).__marked) {
17 (hasPriority as any).__marked = true;
18 let current = hasPriority.current
19 Object.defineProperty(hasPriority, 'current', {
20 get() {
21 return current;
22 },
23 set(value) {
24 current = value;
25 forceUpdate();
26 },
27 })
28 }
29
30 useLayoutEffect(() => {
31 if (hasPriority.current && ref.current) {
32 ref.current!.focus();
33 }
34 }, [hasPriority.current, ref]);
35
36 return (
37 <div tabIndex={-1} ref={ref} id={id}>
38 {children}
39 </div>
40 );
41};
42
43it('allows sole element to take priority', () => {
44 mount(
45 <FocusOnPriority id="only" />
46 );
47
48 cy.focused().should('have.id', 'only');
49});
50
51it('tracks priority of sibling elements in order', () => {
52 mount(
53 <main>
54 <FocusOnPriority id="first" />
55 <FocusOnPriority id="second" />
56 </main>
57 );
58
59 cy.focused().should('have.id', 'second');
60});
61
62it('tracks priority of nested elements in order', () => {
63 mount(
64 <FocusOnPriority id="outer">
65 <FocusOnPriority id="inner" />
66 </FocusOnPriority>
67 );
68
69 cy.focused().should('have.id', 'inner');
70});
71
72it('should switch priorities of sibling elements as needed', () => {
73 const App = () => {
74 const [visible, setVisible] = useState(true);
75
76 return (
77 <main>
78 <FocusOnPriority id="first" />
79 {visible && <FocusOnPriority id="second" />}
80 <button onClick={() => setVisible(false)}>switch</button>
81 </main>
82 );
83 };
84
85 mount(<App />);
86
87 cy.focused().should('have.id', 'second');
88 cy.get('button').first().click();
89 cy.focused().should('have.id', 'first');
90});
91
92it('should switch priorities of nested elements as needed', () => {
93 const App = () => {
94 const [visible, setVisible] = useState(true);
95
96 return (
97 <main>
98 <FocusOnPriority id="outer">
99 {visible && <FocusOnPriority id="inner" />}
100 </FocusOnPriority>
101 <button onClick={() => setVisible(false)}>switch</button>
102 </main>
103 );
104 };
105
106 mount(<App />);
107
108 cy.focused().should('have.id', 'inner');
109 cy.get('button').first().click();
110 cy.focused().should('have.id', 'outer');
111});
112
113it('should preserve priorities when non-prioritised item is removed', () => {
114 const App = () => {
115 const [visible, setVisible] = useState(true);
116
117 return (
118 <main>
119 {visible && <FocusOnPriority id="first" />}
120 <FocusOnPriority id="second" />
121 <button onClick={() => setVisible(false)}>switch</button>
122 </main>
123 );
124 };
125
126 mount(<App />);
127
128 cy.focused().should('have.id', 'second');
129 cy.get('button').first().click();
130 cy.get('button').first().should('have.focus');
131});
132
133it('should update priorities when new prioritised item is added', () => {
134 const App = () => {
135 const [visible, setVisible] = useState(false);
136
137 return (
138 <main>
139 <FocusOnPriority id="first" />
140 <FocusOnPriority id="second" />
141 {visible && <FocusOnPriority id="third" />}
142 <button onClick={() => setVisible(true)}>switch</button>
143 </main>
144 );
145 };
146
147 mount(<App />);
148
149 cy.focused().should('have.id', 'second');
150 cy.get('button').first().click();
151 cy.focused().should('have.id', 'third');
152});