Skip to content

Commit 50a28d0

Browse files
committed
feat: add event-debugging environment
1 parent 85c240c commit 50a28d0

File tree

11 files changed

+740
-19
lines changed

11 files changed

+740
-19
lines changed

package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
"react-dom": "^16.13.1",
5555
"react-router-dom": "^5.2.0",
5656
"react-toastify": "^6.0.5",
57-
"react-virtualized-auto-sizer": "^1.0.2"
57+
"react-virtualized-auto-sizer": "^1.0.2",
58+
"react-window": "^1.8.5"
5859
},
5960
"devDependencies": {
6061
"@babel/core": "^7.10.2",

src/components/App.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,23 @@ import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
33
import Playground from './Playground';
44
import Layout from './Layout';
55
import Embedded from './Embedded';
6-
import { ToastContainer } from 'react-toastify';
6+
import DomEvents from './DomEvents';
77

88
function App() {
99
return (
1010
<Router>
1111
<Switch>
12-
<Route path="/embed/">
12+
<Route path="/embed">
1313
<Embedded />
1414
</Route>
15+
<Route path="/events">
16+
<Layout>
17+
<DomEvents />
18+
</Layout>
19+
</Route>
1520
<Route path="/">
1621
<Layout>
1722
<Playground />
18-
<ToastContainer />
1923
</Layout>
2024
</Route>
2125
</Switch>

src/components/DomEvents.js

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import React, { useRef, useCallback, useState } from 'react';
2+
3+
import Preview from './Preview';
4+
import MarkupEditor from './MarkupEditor';
5+
import usePlayground from '../hooks/usePlayground';
6+
import state from '../lib/state';
7+
import { eventMap } from '@testing-library/dom/dist/event-map';
8+
import { VirtualScrollable } from './Scrollable';
9+
import { FixedSizeList as List } from 'react-window';
10+
import throttle from 'lodash.throttle';
11+
import AutoSizer from 'react-virtualized-auto-sizer';
12+
import IconButton from './IconButton';
13+
import TrashcanIcon from './TrashcanIcon';
14+
import EmptyStreetImg from '../images/EmptyStreetImg';
15+
16+
function onStateChange({ markup, query, result }) {
17+
state.save({ markup, query });
18+
state.updateTitle(result?.expression?.expression);
19+
}
20+
21+
const initialValues = state.load() || {};
22+
23+
function targetToString() {
24+
return [
25+
this.tagName.toLowerCase(),
26+
this.id && `#${this.id}`,
27+
this.name && `[name="${this.name}"]`,
28+
this.htmlFor && `[for="${this.htmlFor}"]`,
29+
this.value && `[value="${this.value}"]`,
30+
this.checked !== null && `[checked=${this.checked}]`,
31+
]
32+
.filter(Boolean)
33+
.join('');
34+
}
35+
36+
function getElementData(element) {
37+
const value =
38+
element.tagName === 'SELECT' && element.multiple
39+
? element.selectedOptions.length > 0
40+
? JSON.stringify(
41+
Array.from(element.selectedOptions).map((o) => o.value),
42+
)
43+
: null
44+
: element.value;
45+
46+
const hasChecked = element.type === 'checkbox' || element.type === 'radio';
47+
48+
return {
49+
tagName: element.tagName.toLowerCase(),
50+
id: element.id || null,
51+
name: element.name || null,
52+
htmlFor: element.htmlFor || null,
53+
value: value || null,
54+
checked: hasChecked ? !!element.checked : null,
55+
toString: targetToString,
56+
};
57+
}
58+
59+
function addLoggingEvents(node, log) {
60+
function createEventLogger(eventType) {
61+
return function logEvent(event) {
62+
if (event.target === event.currentTarget) {
63+
return;
64+
}
65+
66+
log({
67+
event: eventType,
68+
target: getElementData(event.target),
69+
});
70+
};
71+
}
72+
73+
Object.keys(eventMap).forEach((name) => {
74+
node.addEventListener(
75+
name.toLowerCase(),
76+
createEventLogger({ name, ...eventMap[name] }),
77+
true,
78+
);
79+
});
80+
}
81+
82+
function EventRecord({ index, style, data }) {
83+
const { id, event, target } = data[index];
84+
85+
return (
86+
<div
87+
className={`w-full h-8 flex items-center text-sm ${
88+
index % 2 ? 'bg-gray-100' : ''
89+
}`}
90+
style={style}
91+
>
92+
<div className="p-2 flex-none w-16">{id}</div>
93+
94+
<div className="p-2 flex-none w-32">{event.EventType}</div>
95+
<div className="p-2 flex-none w-32">{event.name}</div>
96+
97+
<div className="p-2 flex-none w-40">{target.tagName}</div>
98+
<div className="p-2 flex-auto whitespace-no-wrap">
99+
{target.toString()}
100+
</div>
101+
</div>
102+
);
103+
}
104+
105+
const noop = () => {};
106+
function DomEvents() {
107+
const [{ markup, result }, dispatch] = usePlayground({
108+
onChange: onStateChange,
109+
...initialValues,
110+
});
111+
112+
const buffer = useRef([]);
113+
const previewRef = useRef();
114+
const listRef = useRef();
115+
116+
const [eventCount, setEventCount] = useState(0);
117+
118+
const reset = () => {
119+
buffer.current = [];
120+
setEventCount(0);
121+
};
122+
123+
const flush = useCallback(
124+
throttle(() => setEventCount(buffer.current.length), 16, {
125+
leading: false,
126+
}),
127+
[setEventCount],
128+
);
129+
130+
const setPreviewRef = useCallback((node) => {
131+
previewRef.current = node;
132+
133+
if (!node) return;
134+
135+
addLoggingEvents(node, (event) => {
136+
// insert at index 0
137+
event.id = buffer.current.length;
138+
buffer.current.splice(0, 0, event);
139+
setTimeout(flush, 0);
140+
});
141+
}, []);
142+
143+
return (
144+
<div className="flex flex-col h-auto md:h-full w-full">
145+
<div className="editor markup-editor gap-4 md:gap-8 md:h-56 flex-auto grid-cols-1 md:grid-cols-2">
146+
<div className="flex-auto relative h-56 md:h-full">
147+
<MarkupEditor markup={markup} dispatch={dispatch} />
148+
</div>
149+
150+
<div className="flex-auto h-56 md:h-full" ref={setPreviewRef}>
151+
<Preview
152+
markup={markup}
153+
elements={result.elements}
154+
accessibleRoles={result.accessibleRoles}
155+
dispatch={noop}
156+
variant="minimal"
157+
/>
158+
</div>
159+
</div>
160+
161+
<div className="flex-none h-8" />
162+
163+
<div className="editor md:h-56 flex-auto overflow-hidden">
164+
<div className="h-56 md:h-full w-full flex flex-col">
165+
<div className="h-8 flex items-center w-full text-sm font-bold">
166+
<div className="p-2 w-16">#</div>
167+
168+
<div className="p-2 w-32">type</div>
169+
<div className="p-2 w-32">name</div>
170+
171+
<div className="p-2 w-40">element</div>
172+
<div className="flex-auto p-2 flex justify-between">
173+
<span>selector</span>
174+
<IconButton title="clear event log" onClick={reset}>
175+
<TrashcanIcon />
176+
</IconButton>
177+
</div>
178+
</div>
179+
180+
<div className="flex-auto relative overflow-hidden">
181+
{eventCount === 0 ? (
182+
<div className="flex w-full h-full opacity-50 items-end justify-center">
183+
<EmptyStreetImg height="80%" />
184+
</div>
185+
) : (
186+
<AutoSizer>
187+
{({ width, height }) => (
188+
<List
189+
ref={listRef}
190+
height={height}
191+
itemCount={eventCount}
192+
itemData={buffer.current}
193+
itemSize={32}
194+
width={width}
195+
outerElementType={VirtualScrollable}
196+
>
197+
{EventRecord}
198+
</List>
199+
)}
200+
</AutoSizer>
201+
)}
202+
</div>
203+
</div>
204+
</div>
205+
</div>
206+
);
207+
}
208+
209+
export default DomEvents;

src/components/IconButton.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22

3-
function IconButton({ children, variant, onClick, className }) {
3+
function IconButton({ children, title, variant, onClick, className }) {
44
return (
55
<button
66
className={[
@@ -13,6 +13,7 @@ function IconButton({ children, variant, onClick, className }) {
1313
.filter(Boolean)
1414
.join(' ')}
1515
onClick={onClick}
16+
title={title}
1617
>
1718
{children}
1819
</button>

src/components/Layout.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import Header from './Header';
33
import Footer from './Footer';
4+
import { ToastContainer } from 'react-toastify';
45

56
function Layout({ children }) {
67
return (
@@ -14,6 +15,8 @@ function Layout({ children }) {
1415
<div className="flex-none">
1516
<Footer />
1617
</div>
18+
19+
<ToastContainer />
1720
</div>
1821
);
1922
}

src/components/Preview.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ function selectByCssPath(rootNode, cssPath) {
88
return rootNode?.querySelector(cssPath.toString().replace(/^body > /, ''));
99
}
1010

11-
function Preview({ markup, accessibleRoles, elements, dispatch }) {
11+
function Preview({ markup, accessibleRoles, elements, dispatch, variant }) {
1212
// Okay, listen up. `highlighted` can be a number of things, as I wanted to
1313
// keep a single variable to represent the state. This to reduce bug count
1414
// by creating out-of-sync states.
@@ -147,7 +147,7 @@ function Preview({ markup, accessibleRoles, elements, dispatch }) {
147147
onMouseEnter={() => setHighlighted(true)}
148148
onMouseLeave={() => setHighlighted(false)}
149149
>
150-
<div className="flex-auto relative overflow-hidden h-1">
150+
<div className="flex-auto relative overflow-hidden">
151151
<Scrollable>
152152
<div
153153
id="view"
@@ -162,7 +162,9 @@ function Preview({ markup, accessibleRoles, elements, dispatch }) {
162162
</Scrollable>
163163
</div>
164164

165-
<PreviewHint roles={roles} suggestion={suggestion} />
165+
{variant !== 'minimal' && (
166+
<PreviewHint roles={roles} suggestion={suggestion} />
167+
)}
166168
</div>
167169
) : (
168170
<div className="w-full h-full flex flex-col relative overflow-hidden">

0 commit comments

Comments
 (0)