Skip to content

Commit 5d41271

Browse files
authored
feat: add hover and click events to html preview area (#2)
1 parent 2efbab5 commit 5d41271

File tree

7 files changed

+162
-47
lines changed

7 files changed

+162
-47
lines changed

src/components/App.js

-5
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,8 @@ function App() {
2929
const parsed = parser.parse(htmlPreviewRef.current, js);
3030
setParsed(parsed);
3131

32-
parsed.targets?.forEach((el) => el.classList.add('highlight'));
3332
state.save({ html, js });
3433
state.updateTitle(parsed.expression?.expression);
35-
36-
return () => {
37-
parsed.targets?.forEach((el) => el.classList.remove('highlight'));
38-
};
3934
}, [html, js, htmlPreviewRef.current]);
4035

4136
return (

src/components/ElementInfo.js

+6-33
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,8 @@
11
import React from 'react';
2-
import { getRole, computeAccessibleName } from 'dom-accessibility-api';
32
import { useAppContext } from './Context';
43
import QueryAdvise from './QueryAdvise';
54

6-
import { getExpression, getFieldName } from '../lib';
7-
8-
function getData({ root, element }) {
9-
const type = element.getAttribute('type');
10-
const tagName = element.tagName;
11-
12-
// prevent querySelector from tripping over corrupted html like <input id="button\n<button>
13-
const id = (element.getAttribute('id') || '').split('\n')[0];
14-
const labelElem = id ? root.querySelector(`[for="${id}"]`) : null;
15-
const labelText = labelElem ? labelElem.innerText : null;
16-
17-
return {
18-
role:
19-
element.getAttribute('role') ||
20-
// input's require a type for the role
21-
(tagName === 'INPUT' && type !== 'text' ? '' : getRole(element)),
22-
name: computeAccessibleName(element),
23-
tagName: tagName,
24-
type: type,
25-
labelText: labelText,
26-
placeholderText: element.getAttribute('placeholder'),
27-
text: element.innerText,
28-
displayValue: element.getAttribute('value'),
29-
30-
altText: element.getAttribute('alt'),
31-
title: element.getAttribute('title'),
32-
33-
testId: element.getAttribute('data-testid'),
34-
};
35-
}
5+
import { getExpression, getFieldName, getQueryAdvise } from '../lib';
366

377
function Section({ children }) {
388
return <div className="space-y-3">{children}</div>;
@@ -75,15 +45,18 @@ function ElementInfo() {
7545
const { htmlPreviewRef, parsed } = useAppContext();
7646
const element = parsed.target;
7747

78-
const data = element && getData({ root: htmlPreviewRef.current, element });
48+
const { data, advise } = getQueryAdvise({
49+
root: htmlPreviewRef.current,
50+
element,
51+
});
7952

8053
if (!data) {
8154
return <div />;
8255
}
8356

8457
return (
8558
<div>
86-
<QueryAdvise data={data} />
59+
<QueryAdvise data={data} advise={advise} />
8760

8861
<div className="my-6 border-b" />
8962

src/components/HtmlPreview.js

+90-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,97 @@
1-
import React, { useCallback, useRef } from 'react';
1+
import React, { useState, useEffect } from 'react';
2+
import { useAppContext } from './Context.js';
3+
import { getQueryAdvise } from '../lib';
24

35
function HtmlPreview({ html }, forwardRef) {
6+
// Okay, listen up. `highlighted` can be a number of things, as I wanted to
7+
// keep a single variable to represent the state. This to reduce bug count
8+
// by creating out-of-sync states.
9+
//
10+
// 1. When the mouse pointer enters the preview area, `highlighted` changes
11+
// to true. True indicates that the highlight no longer indicates the parsed
12+
// element.
13+
// 2. When the mouse pointer is pointing at an element, `highlighted` changes
14+
// to the target element. A dom node.
15+
// 3. When the mouse pointer leaves that element again, `highlighted` changse
16+
// back to... true. Not to false! To indicate that we still want to use
17+
// the mouse position to control the highlight.
18+
// 4. Once the mouse leaves the preview area, `highlighted` switches to false.
19+
// Indiating that the `parsed` element can be highlighted again.
20+
const [highlighted, setHighlighted] = useState(false);
21+
const { parsed, jsEditorRef } = useAppContext();
22+
23+
const { advise } = getQueryAdvise({
24+
root: forwardRef.current,
25+
element: highlighted,
26+
});
27+
28+
const handleClick = (event) => {
29+
if (event.target === forwardRef.current) {
30+
return;
31+
}
32+
33+
event.preventDefault();
34+
const expression =
35+
advise.expression ||
36+
'// No recommendation available.\n// Add some html attributes, or\n// use container.querySelector(…)';
37+
jsEditorRef.current.setValue(expression);
38+
};
39+
40+
useEffect(() => {
41+
if (highlighted) {
42+
parsed.targets?.forEach((el) => el.classList.remove('highlight'));
43+
highlighted.classList?.add('highlight');
44+
} else {
45+
highlighted?.classList?.remove('highlight');
46+
47+
if (highlighted === false) {
48+
parsed.targets?.forEach((el) => el.classList.add('highlight'));
49+
}
50+
}
51+
52+
return () => highlighted?.classList?.remove('highlight');
53+
}, [highlighted, parsed.targets]);
54+
55+
const handleMove = (event) => {
56+
const target = document.elementFromPoint(event.clientX, event.clientY);
57+
if (target === highlighted) {
58+
return;
59+
}
60+
61+
if (target === forwardRef.current) {
62+
setHighlighted(true);
63+
return;
64+
}
65+
66+
setHighlighted(target);
67+
};
68+
469
return (
570
<div
6-
className="preview"
7-
ref={forwardRef}
8-
dangerouslySetInnerHTML={{ __html: html }}
9-
/>
71+
className="relative flex flex-col"
72+
onMouseEnter={() => setHighlighted(true)}
73+
onMouseLeave={() => setHighlighted(false)}
74+
>
75+
<div
76+
className="preview flex-auto"
77+
ref={forwardRef}
78+
dangerouslySetInnerHTML={{ __html: html }}
79+
onClick={handleClick}
80+
onMouseMove={handleMove}
81+
/>
82+
<div className="p-2 bg-gray-200 rounded text-gray-800 font-mono text-xs">
83+
{advise.expression && `> ${advise.expression}`}
84+
85+
{!advise.expression && forwardRef.current && (
86+
<>
87+
<span className="font-bold">roles: </span>
88+
{Object.keys(TestingLibraryDom.getRoles(forwardRef.current))
89+
.sort()
90+
.join(', ')}
91+
</>
92+
)}
93+
</div>
94+
</div>
1095
);
1196
}
1297

src/components/QueryAdvise.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,8 @@ function Quote({ heading, content, source, href }) {
3434
);
3535
}
3636

37-
function QueryAdvise({ data }) {
37+
function QueryAdvise({ data, advise }) {
3838
const { parsed, jsEditorRef } = useAppContext();
39-
const advise = getQueryAdvise(data);
4039

4140
const used = parsed?.expression || {};
4241

@@ -116,7 +115,10 @@ function QueryAdvise({ data }) {
116115
<div className={['text-white p-4 rounded space-y-2', color].join(' ')}>
117116
<div className="font-bold text-xs">suggested query</div>
118117
{advise.expression && (
119-
<div className="font-mono cursor-pointer" onClick={handleClick}>
118+
<div
119+
className="font-mono cursor-pointer text-xs"
120+
onClick={handleClick}
121+
>
120122
&gt; {advise.expression}
121123
</div>
122124
)}

src/lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './ensureArray';
22
export * from './getExpression';
3+
export * from './queryAdvise';

src/lib/queryAdvise.js

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { messages, queries } from '../constants';
2+
import { getExpression } from './getExpression';
3+
import { computeAccessibleName, getRole } from 'dom-accessibility-api';
4+
5+
export function getData({ root, element }) {
6+
const type = element.getAttribute('type');
7+
const tagName = element.tagName;
8+
9+
// prevent querySelector from tripping over corrupted html like <input id="button\n<button>
10+
const id = (element.getAttribute('id') || '').split('\n')[0];
11+
const labelElem = id ? root.querySelector(`[for="${id}"]`) : null;
12+
const labelText = labelElem ? labelElem.innerText : null;
13+
14+
return {
15+
role:
16+
element.getAttribute('role') ||
17+
// input's require a type for the role
18+
(tagName === 'INPUT' && type !== 'text' ? '' : getRole(element)),
19+
name: computeAccessibleName(element),
20+
tagName: tagName,
21+
type: type,
22+
labelText: labelText,
23+
placeholderText: element.getAttribute('placeholder'),
24+
text: element.innerText,
25+
displayValue: element.getAttribute('value'),
26+
27+
altText: element.getAttribute('alt'),
28+
title: element.getAttribute('title'),
29+
30+
testId: element.getAttribute('data-testid'),
31+
};
32+
}
33+
34+
export function getQueryAdvise({ root, element }) {
35+
if (!root || element?.nodeType !== Node.ELEMENT_NODE) {
36+
return { data: {}, advise: {} };
37+
}
38+
39+
const data = getData({ root, element });
40+
const query = queries.find(({ method }) => getExpression({ method, data }));
41+
42+
if (!query) {
43+
return {
44+
level: 3,
45+
expression: 'container.querySelector(…)',
46+
advise: {},
47+
data,
48+
...messages[3],
49+
};
50+
}
51+
52+
const expression = getExpression({ method: query.method, data });
53+
const advise = { expression, ...query, ...messages[query.level] };
54+
55+
return {
56+
data,
57+
advise,
58+
};
59+
}

src/styles/app.pcss

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ blockquote cite:before {
124124
}
125125

126126
.preview .highlight {
127-
@apply shadow-outline;
127+
@apply shadow-outline rounded;
128128
}
129129

130130
.preview a {

0 commit comments

Comments
 (0)