Skip to content

feat: add hover and click events to html preview area #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions src/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,8 @@ function App() {
const parsed = parser.parse(htmlPreviewRef.current, js);
setParsed(parsed);

parsed.targets?.forEach((el) => el.classList.add('highlight'));
state.save({ html, js });
state.updateTitle(parsed.expression?.expression);

return () => {
parsed.targets?.forEach((el) => el.classList.remove('highlight'));
};
}, [html, js, htmlPreviewRef.current]);

return (
Expand Down
39 changes: 6 additions & 33 deletions src/components/ElementInfo.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,8 @@
import React from 'react';
import { getRole, computeAccessibleName } from 'dom-accessibility-api';
import { useAppContext } from './Context';
import QueryAdvise from './QueryAdvise';

import { getExpression, getFieldName } from '../lib';

function getData({ root, element }) {
const type = element.getAttribute('type');
const tagName = element.tagName;

// prevent querySelector from tripping over corrupted html like <input id="button\n<button>
const id = (element.getAttribute('id') || '').split('\n')[0];
const labelElem = id ? root.querySelector(`[for="${id}"]`) : null;
const labelText = labelElem ? labelElem.innerText : null;

return {
role:
element.getAttribute('role') ||
// input's require a type for the role
(tagName === 'INPUT' && type !== 'text' ? '' : getRole(element)),
name: computeAccessibleName(element),
tagName: tagName,
type: type,
labelText: labelText,
placeholderText: element.getAttribute('placeholder'),
text: element.innerText,
displayValue: element.getAttribute('value'),

altText: element.getAttribute('alt'),
title: element.getAttribute('title'),

testId: element.getAttribute('data-testid'),
};
}
import { getExpression, getFieldName, getQueryAdvise } from '../lib';

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

const data = element && getData({ root: htmlPreviewRef.current, element });
const { data, advise } = getQueryAdvise({
root: htmlPreviewRef.current,
element,
});

if (!data) {
return <div />;
}

return (
<div>
<QueryAdvise data={data} />
<QueryAdvise data={data} advise={advise} />

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

Expand Down
95 changes: 90 additions & 5 deletions src/components/HtmlPreview.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,97 @@
import React, { useCallback, useRef } from 'react';
import React, { useState, useEffect } from 'react';
import { useAppContext } from './Context.js';
import { getQueryAdvise } from '../lib';

function HtmlPreview({ html }, forwardRef) {
// Okay, listen up. `highlighted` can be a number of things, as I wanted to
// keep a single variable to represent the state. This to reduce bug count
// by creating out-of-sync states.
//
// 1. When the mouse pointer enters the preview area, `highlighted` changes
// to true. True indicates that the highlight no longer indicates the parsed
// element.
// 2. When the mouse pointer is pointing at an element, `highlighted` changes
// to the target element. A dom node.
// 3. When the mouse pointer leaves that element again, `highlighted` changse
// back to... true. Not to false! To indicate that we still want to use
// the mouse position to control the highlight.
// 4. Once the mouse leaves the preview area, `highlighted` switches to false.
// Indiating that the `parsed` element can be highlighted again.
const [highlighted, setHighlighted] = useState(false);
const { parsed, jsEditorRef } = useAppContext();

const { advise } = getQueryAdvise({
root: forwardRef.current,
element: highlighted,
});

const handleClick = (event) => {
if (event.target === forwardRef.current) {
return;
}

event.preventDefault();
const expression =
advise.expression ||
'// No recommendation available.\n// Add some html attributes, or\n// use container.querySelector(…)';
jsEditorRef.current.setValue(expression);
};

useEffect(() => {
if (highlighted) {
parsed.targets?.forEach((el) => el.classList.remove('highlight'));
highlighted.classList?.add('highlight');
} else {
highlighted?.classList?.remove('highlight');

if (highlighted === false) {
parsed.targets?.forEach((el) => el.classList.add('highlight'));
}
}

return () => highlighted?.classList?.remove('highlight');
}, [highlighted, parsed.targets]);

const handleMove = (event) => {
const target = document.elementFromPoint(event.clientX, event.clientY);
if (target === highlighted) {
return;
}

if (target === forwardRef.current) {
setHighlighted(true);
return;
}

setHighlighted(target);
};

return (
<div
className="preview"
ref={forwardRef}
dangerouslySetInnerHTML={{ __html: html }}
/>
className="relative flex flex-col"
onMouseEnter={() => setHighlighted(true)}
onMouseLeave={() => setHighlighted(false)}
>
<div
className="preview flex-auto"
ref={forwardRef}
dangerouslySetInnerHTML={{ __html: html }}
onClick={handleClick}
onMouseMove={handleMove}
/>
<div className="p-2 bg-gray-200 rounded text-gray-800 font-mono text-xs">
{advise.expression && `> ${advise.expression}`}

{!advise.expression && forwardRef.current && (
<>
<span className="font-bold">roles: </span>
{Object.keys(TestingLibraryDom.getRoles(forwardRef.current))
.sort()
.join(', ')}
</>
)}
</div>
</div>
);
}

Expand Down
8 changes: 5 additions & 3 deletions src/components/QueryAdvise.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@ function Quote({ heading, content, source, href }) {
);
}

function QueryAdvise({ data }) {
function QueryAdvise({ data, advise }) {
const { parsed, jsEditorRef } = useAppContext();
const advise = getQueryAdvise(data);

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

Expand Down Expand Up @@ -116,7 +115,10 @@ function QueryAdvise({ data }) {
<div className={['text-white p-4 rounded space-y-2', color].join(' ')}>
<div className="font-bold text-xs">suggested query</div>
{advise.expression && (
<div className="font-mono cursor-pointer" onClick={handleClick}>
<div
className="font-mono cursor-pointer text-xs"
onClick={handleClick}
>
&gt; {advise.expression}
</div>
)}
Expand Down
1 change: 1 addition & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './ensureArray';
export * from './getExpression';
export * from './queryAdvise';
59 changes: 59 additions & 0 deletions src/lib/queryAdvise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { messages, queries } from '../constants';
import { getExpression } from './getExpression';
import { computeAccessibleName, getRole } from 'dom-accessibility-api';

export function getData({ root, element }) {
const type = element.getAttribute('type');
const tagName = element.tagName;

// prevent querySelector from tripping over corrupted html like <input id="button\n<button>
const id = (element.getAttribute('id') || '').split('\n')[0];
const labelElem = id ? root.querySelector(`[for="${id}"]`) : null;
const labelText = labelElem ? labelElem.innerText : null;

return {
role:
element.getAttribute('role') ||
// input's require a type for the role
(tagName === 'INPUT' && type !== 'text' ? '' : getRole(element)),
name: computeAccessibleName(element),
tagName: tagName,
type: type,
labelText: labelText,
placeholderText: element.getAttribute('placeholder'),
text: element.innerText,
displayValue: element.getAttribute('value'),

altText: element.getAttribute('alt'),
title: element.getAttribute('title'),

testId: element.getAttribute('data-testid'),
};
}

export function getQueryAdvise({ root, element }) {
if (!root || element?.nodeType !== Node.ELEMENT_NODE) {
return { data: {}, advise: {} };
}

const data = getData({ root, element });
const query = queries.find(({ method }) => getExpression({ method, data }));

if (!query) {
return {
level: 3,
expression: 'container.querySelector(…)',
advise: {},
data,
...messages[3],
};
}

const expression = getExpression({ method: query.method, data });
const advise = { expression, ...query, ...messages[query.level] };

return {
data,
advise,
};
}
2 changes: 1 addition & 1 deletion src/styles/app.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ blockquote cite:before {
}

.preview .highlight {
@apply shadow-outline;
@apply shadow-outline rounded;
}

.preview a {
Expand Down