diff --git a/desktop-app/src/renderer/components/DropDown/index.tsx b/desktop-app/src/renderer/components/DropDown/index.tsx index acfca8de1..3747d8729 100644 --- a/desktop-app/src/renderer/components/DropDown/index.tsx +++ b/desktop-app/src/renderer/components/DropDown/index.tsx @@ -1,15 +1,16 @@ import { Menu, Transition } from '@headlessui/react'; import { Icon } from '@iconify/react'; import cx from 'classnames'; -import { Fragment, useEffect, useRef, useState } from 'react'; +import { Fragment } from 'react'; interface Separator { type: 'separator'; } + interface Option { type?: 'option'; label: JSX.Element | string; - onClick: () => void; + onClick: (() => void) | null; } type OptionOrSeparator = Option | Separator; @@ -17,14 +18,15 @@ type OptionOrSeparator = Option | Separator; interface Props { label: JSX.Element | string; options: OptionOrSeparator[]; + className?: string | null; } -export function DropDown({ label, options }: Props) { +export function DropDown({ label, options, className }: Props) { return (
- +
- + {label} @@ -53,18 +55,29 @@ export function DropDown({ label, options }: Props) { return ( // eslint-disable-next-line react/no-array-index-key - {({ active }) => ( - - )} + {({ active }) => + option.onClick !== null ? ( + + ) : ( +
+ {option.label} +
+ ) + }
); })} diff --git a/desktop-app/src/renderer/components/Previewer/Device/Toolbar.tsx b/desktop-app/src/renderer/components/Previewer/Device/Toolbar.tsx index 2701d2216..3ec298d61 100644 --- a/desktop-app/src/renderer/components/Previewer/Device/Toolbar.tsx +++ b/desktop-app/src/renderer/components/Previewer/Device/Toolbar.tsx @@ -8,6 +8,14 @@ import WebPage from 'main/screenshot/webpage'; import screenshotSfx from 'renderer/assets/sfx/screenshot.mp3'; import { updateWebViewHeightAndScale } from 'common/webViewUtils'; +import { useDispatch, useSelector } from 'react-redux'; +import { DropDown } from '../../DropDown'; + +export interface InjectedCss { + key: string; + css: string; + name: string; +} interface Props { webview: Electron.WebviewTag | null; @@ -28,6 +36,9 @@ const Toolbar = ({ onIndividualLayoutHandler, isIndividualLayout, }: Props) => { + const dispatch = useDispatch(); + // const cssSelector: InjectedCss | undefined = useSelector(selectCss); + const [injectCss, setInjectCss] = useState(); const [eventMirroringOff, setEventMirroringOff] = useState(false); const [playScreenshotDone] = useSound(screenshotSfx, { volume: 0.5 }); const [screenshotLoading, setScreenshotLoading] = useState(false); @@ -35,6 +46,17 @@ const Toolbar = ({ useState(false); const [rotated, setRotated] = useState(false); + const redgreen = [ + 'Deuteranopia', + 'Deuteranomaly', + 'Protanopia', + 'Protanomaly', + ]; + const blueyellow = ['Tritanopia', 'Tritanomaly']; + const full = ['Achromatomaly', 'Achromatopsia']; + const visualimpairments = ['Cataract', 'Farsightedness', 'Glaucome']; + const sunlight = ['Solarize']; + const refreshView = () => { if (webview) { webview.reload(); @@ -42,7 +64,7 @@ const Toolbar = ({ }; const toggleEventMirroring = async () => { - if (webview == null) { + if (webview === null) { return; } try { @@ -64,7 +86,7 @@ const Toolbar = ({ }; const quickScreenshot = async () => { - if (webview == null) { + if (webview === null) { return; } setScreenshotLoading(true); @@ -84,14 +106,128 @@ const Toolbar = ({ setScreenshotLoading(false); }; + const applyCss = async ( + debugType: string, + css: string, + js: string | null = null + ) => { + if (webview === null) { + return; + } + if (css === undefined) { + return; + } + + if (injectCss !== undefined) { + if (js !== null) { + webview.reload(); + } + if (injectCss.css === css) { + await webview.removeInsertedCSS(injectCss.key); + setInjectCss(undefined); + return; + } + await webview.removeInsertedCSS(injectCss.key); + setInjectCss(undefined); + } + + try { + const key = await webview.insertCSS(css); + setInjectCss({ key, css, name: debugType }); + if (js !== null) { + await webview.executeJavaScript(js); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error inserting css', error); + // dispatch(setCss(undefined)); + setInjectCss(undefined); + } + }; + + const applyColorDeficiency = async (colorDeficiency: string) => { + const xsltPath = + 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgdmVyc2lvbj0iMS4xIj4KICA8ZGVmcz4KICAgIDxmaWx0ZXIgaWQ9InByb3Rhbm9waWEiPgogICAgICA8ZmVDb2xvck1hdHJpeAogICAgICAgIGluPSJTb3VyY2VHcmFwaGljIgogICAgICAgIHR5cGU9Im1hdHJpeCIKICAgICAgICB2YWx1ZXM9IjAuNTY3LCAwLjQzMywgMCwgICAgIDAsIDAKICAgICAgICAgICAgICAgIDAuNTU4LCAwLjQ0MiwgMCwgICAgIDAsIDAKICAgICAgICAgICAgICAgIDAsICAgICAwLjI0MiwgMC43NTgsIDAsIDAKICAgICAgICAgICAgICAgIDAsICAgICAwLCAgICAgMCwgICAgIDEsIDAiLz4KICAgIDwvZmlsdGVyPgogICAgPGZpbHRlciBpZD0icHJvdGFub21hbHkiPgogICAgICA8ZmVDb2xvck1hdHJpeAogICAgICAgIGluPSJTb3VyY2VHcmFwaGljIgogICAgICAgIHR5cGU9Im1hdHJpeCIKICAgICAgICB2YWx1ZXM9IjAuODE3LCAwLjE4MywgMCwgICAgIDAsIDAKICAgICAgICAgICAgICAgIDAuMzMzLCAwLjY2NywgMCwgICAgIDAsIDAKICAgICAgICAgICAgICAgIDAsICAgICAwLjEyNSwgMC44NzUsIDAsIDAKICAgICAgICAgICAgICAgIDAsICAgICAwLCAgICAgMCwgICAgIDEsIDAiLz4KICAgIDwvZmlsdGVyPgogICAgPGZpbHRlciBpZD0iZGV1dGVyYW5vcGlhIj4KICAgICAgPGZlQ29sb3JNYXRyaXgKICAgICAgICBpbj0iU291cmNlR3JhcGhpYyIKICAgICAgICB0eXBlPSJtYXRyaXgiCiAgICAgICAgdmFsdWVzPSIwLjYyNSwgMC4zNzUsIDAsICAgMCwgMAogICAgICAgICAgICAgICAgMC43LCAgIDAuMywgICAwLCAgIDAsIDAKICAgICAgICAgICAgICAgIDAsICAgICAwLjMsICAgMC43LCAwLCAwCiAgICAgICAgICAgICAgICAwLCAgICAgMCwgICAgIDAsICAgMSwgMCIvPgogICAgPC9maWx0ZXI+CiAgICA8ZmlsdGVyIGlkPSJkZXV0ZXJhbm9tYWx5Ij4KICAgICAgPGZlQ29sb3JNYXRyaXgKICAgICAgICBpbj0iU291cmNlR3JhcGhpYyIKICAgICAgICB0eXBlPSJtYXRyaXgiCiAgICAgICAgdmFsdWVzPSIwLjgsICAgMC4yLCAgIDAsICAgICAwLCAwCiAgICAgICAgICAgICAgICAwLjI1OCwgMC43NDIsIDAsICAgICAwLCAwCiAgICAgICAgICAgICAgICAwLCAgICAgMC4xNDIsIDAuODU4LCAwLCAwCiAgICAgICAgICAgICAgICAwLCAgICAgMCwgICAgIDAsICAgICAxLCAwIi8+CiAgICA8L2ZpbHRlcj4KICAgIDxmaWx0ZXIgaWQ9InRyaXRhbm9waWEiPgogICAgICA8ZmVDb2xvck1hdHJpeAogICAgICAgIGluPSJTb3VyY2VHcmFwaGljIgogICAgICAgIHR5cGU9Im1hdHJpeCIKICAgICAgICB2YWx1ZXM9IjAuOTUsIDAuMDUsICAwLCAgICAgMCwgMAogICAgICAgICAgICAgICAgMCwgICAgMC40MzMsIDAuNTY3LCAwLCAwCiAgICAgICAgICAgICAgICAwLCAgICAwLjQ3NSwgMC41MjUsIDAsIDAKICAgICAgICAgICAgICAgIDAsICAgIDAsICAgICAwLCAgICAgMSwgMCIvPgogICAgPC9maWx0ZXI+CiAgICA8ZmlsdGVyIGlkPSJ0cml0YW5vbWFseSI+CiAgICAgIDxmZUNvbG9yTWF0cml4CiAgICAgICAgaW49IlNvdXJjZUdyYXBoaWMiCiAgICAgICAgdHlwZT0ibWF0cml4IgogICAgICAgIHZhbHVlcz0iMC45NjcsIDAuMDMzLCAwLCAgICAgMCwgMAogICAgICAgICAgICAgICAgMCwgICAgIDAuNzMzLCAwLjI2NywgMCwgMAogICAgICAgICAgICAgICAgMCwgICAgIDAuMTgzLCAwLjgxNywgMCwgMAogICAgICAgICAgICAgICAgMCwgICAgIDAsICAgICAwLCAgICAgMSwgMCIvPgogICAgPC9maWx0ZXI+CiAgICA8ZmlsdGVyIGlkPSJhY2hyb21hdG9wc2lhIj4KICAgICAgPGZlQ29sb3JNYXRyaXgKICAgICAgICBpbj0iU291cmNlR3JhcGhpYyIKICAgICAgICB0eXBlPSJtYXRyaXgiCiAgICAgICAgdmFsdWVzPSIwLjI5OSwgMC41ODcsIDAuMTE0LCAwLCAwCiAgICAgICAgICAgICAgICAwLjI5OSwgMC41ODcsIDAuMTE0LCAwLCAwCiAgICAgICAgICAgICAgICAwLjI5OSwgMC41ODcsIDAuMTE0LCAwLCAwCiAgICAgICAgICAgICAgICAwLCAgICAgMCwgICAgIDAsICAgICAxLCAwIi8+CiAgICA8L2ZpbHRlcj4KICAgIDxmaWx0ZXIgaWQ9ImFjaHJvbWF0b21hbHkiPgogICAgICA8ZmVDb2xvck1hdHJpeAogICAgICAgIGluPSJTb3VyY2VHcmFwaGljIgogICAgICAgIHR5cGU9Im1hdHJpeCIKICAgICAgICB2YWx1ZXM9IjAuNjE4LCAwLjMyMCwgMC4wNjIsIDAsIDAKICAgICAgICAgICAgICAgIDAuMTYzLCAwLjc3NSwgMC4wNjIsIDAsIDAKICAgICAgICAgICAgICAgIDAuMTYzLCAwLjMyMCwgMC41MTYsIDAsIDAKICAgICAgICAgICAgICAgIDAsICAgICAwLCAgICAgMCwgICAgIDEsIDAiLz4KICAgIDwvZmlsdGVyPgogIDwvZGVmcz4KPC9zdmc+Cg=='; + const css = ` + body { + -webkit-filter: url('${xsltPath}#${colorDeficiency}'); + filter: url('${xsltPath}#${colorDeficiency}'); + } + `; + return applyCss(colorDeficiency, css); + }; + + const applySunlight = async (condition: string) => { + const css = 'body {backdrop-filter: brightness(0.5) !important;}'; + return applyCss(condition, css); + }; + + const applyVisualImpairment = async (visualImpairment: string) => { + const blur = + 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8ZGVmcz4KICAgICAgICA8ZmlsdGVyIGlkPSJnYXVzc2lhbl9ibHVyIj4KICAgICAgICAgICAgPGZlR2F1c3NpYW5CbHVyIGluPSJTb3VyY2VHcmFwaGljIiBzdGREZXZpYXRpb249IjEwIiAvPgogICAgICAgIDwvZmlsdGVyPgogICAgPC9kZWZzPgo8L3N2Zz4='; + + const impairments: { [key: string]: string } = { + cataract: `body { + -webkit-filter: url('${blur}#gaussian_blur'); + filter: url('${blur}#gaussian_blur'); + }`, + glaucome: `#bigoverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; +} + +#spotlight { + border-radius: 50%; + width: 300vmax; + height: 300vmax; + box-shadow: 0 0 5vmax 110vmax inset black; + position: absolute; + z-index: -1; + left: -75vmax; + top: -75vmax; +}`, + farsightedness: `body { filter: blur(2px); }`, + }; + const css = impairments[visualImpairment.toLowerCase()]; + let js = null; + if (visualImpairment.toLowerCase() === 'glaucome') { + js = String(`var div = document.createElement('div'); + div.innerHTML ='
'; + var body = document.body; + body.appendChild(div); + function handleMouseMove(){ + var eventDoc, doc, body; + eventDoc = (event.target && event.target.ownerDocument) || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + event.pageX = event.clientX + + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - + (doc && doc.clientLeft || body && body.clientLeft || 0); + event.pageY = event.clientY + + (doc && doc.scrollTop || body && body.scrollTop || 0) - + (doc && doc.clientTop || body && body.clientTop || 0 ); + const spotlight = document.getElementById("spotlight"); + const boundingRect = spotlight.getBoundingClientRect(); + spotlight.style.left = (event.pageX - boundingRect.width / 2) + "px" + spotlight.style.top = (event.pageY - boundingRect.height / 2) + "px" + };document.onmousemove = handleMouseMove;0`); + } + return applyCss(visualImpairment, css, js); + }; + const fullScreenshot = async () => { - if (webview == null) { + if (webview === null) { return; } setFullScreenshotLoading(true); try { const webviewTag = window.document.getElementById(device.name); - if (webviewTag == null) { + if (webviewTag === null) { return; } setScreenshotInProgress(true); @@ -168,7 +304,7 @@ const Toolbar = ({ - + + } + options={[ + { + label: ( +
+ A11y Tools +
+ ), + onClick: null, + }, + { + label: ( +
+ Visual deficiency +
+ ), + onClick: null, + }, + { + label: ( +
+ + Red-green deficiency + +
+ ), + onClick: null, + }, + ...redgreen.map((x: string) => { + return { + label: ( +
+ {injectCss?.name === x.toLowerCase() ? ( + + ) : ( + <> + )} + + {x} + +
+ ), + onClick: () => { + applyColorDeficiency(x.toLowerCase()); + }, + }; + }), + { + label: ( +
+ + Blue-yellow deficiency + +
+ ), + onClick: null, + }, + ...blueyellow.map((x: string) => { + return { + label: ( +
+ {injectCss?.name === x.toLowerCase() ? ( + + ) : ( + <> + )} + + {x} + +
+ ), + onClick: () => { + applyColorDeficiency(x.toLowerCase()); + }, + }; + }), + { + label: ( +
+ + Full color deficiency + +
+ ), + onClick: null, + }, + ...full.map((x: string) => { + return { + label: ( +
+ {injectCss?.name === x.toLowerCase() ? ( + + ) : ( + <> + )} + + {x} + +
+ ), + onClick: () => { + applyColorDeficiency(x.toLowerCase()); + }, + }; + }), + { + label: ( +
+ Visual impairment +
+ ), + onClick: null, + }, + ...visualimpairments.map((x: string) => { + return { + label: ( +
+ {injectCss?.name === x.toLowerCase() ? ( + + ) : ( + <> + )} + + {x} + +
+ ), + onClick: () => { + applyVisualImpairment(x.toLowerCase()); + }, + }; + }), + { + label: ( +
+ + Temporary impairment + +
+ ), + onClick: null, + }, + ...sunlight.map((x: string) => { + return { + label: ( +
+ {injectCss?.name === x.toLowerCase() ? ( + + ) : ( + <> + )} + + {x} + +
+ ), + onClick: () => { + applySunlight(x.toLowerCase()); + }, + }; + }), + ]} + />