From 532b339893b250c238c8db260be9e22ccfde8160 Mon Sep 17 00:00:00 2001 From: Len Date: Thu, 2 Nov 2023 10:23:18 +0100 Subject: [PATCH 1/4] Accessibility menu and simulators. --- .../renderer/components/DropDown/index.tsx | 45 ++- .../components/Previewer/Device/Toolbar.tsx | 361 +++++++++++++++++- .../renderer/store/features/renderer/index.ts | 18 + 3 files changed, 403 insertions(+), 21 deletions(-) diff --git a/desktop-app/src/renderer/components/DropDown/index.tsx b/desktop-app/src/renderer/components/DropDown/index.tsx index acfca8de1..ff9dfc285 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,12 +18,13 @@ 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..7968d0544 100644 --- a/desktop-app/src/renderer/components/Previewer/Device/Toolbar.tsx +++ b/desktop-app/src/renderer/components/Previewer/Device/Toolbar.tsx @@ -8,6 +8,13 @@ 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'; +import { + InjectedCss, + selectCss, + setCss, +} from '../../../store/features/renderer'; interface Props { webview: Electron.WebviewTag | null; @@ -28,6 +35,9 @@ const Toolbar = ({ onIndividualLayoutHandler, isIndividualLayout, }: Props) => { + const dispatch = useDispatch(); + const cssSelector: InjectedCss | undefined = useSelector(selectCss); + const [eventMirroringOff, setEventMirroringOff] = useState(false); const [playScreenshotDone] = useSound(screenshotSfx, { volume: 0.5 }); const [screenshotLoading, setScreenshotLoading] = useState(false); @@ -35,6 +45,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 +63,7 @@ const Toolbar = ({ }; const toggleEventMirroring = async () => { - if (webview == null) { + if (webview === null) { return; } try { @@ -64,7 +85,7 @@ const Toolbar = ({ }; const quickScreenshot = async () => { - if (webview == null) { + if (webview === null) { return; } setScreenshotLoading(true); @@ -84,14 +105,170 @@ const Toolbar = ({ setScreenshotLoading(false); }; + const applyColorDeficiency = async (colorDeficiency: string | undefined) => { + if (webview === null) { + return; + } + if (colorDeficiency === undefined) { + return; + } + + 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}'); + } + `; + if (cssSelector !== undefined) { + if (cssSelector.css === css) { + await webview.removeInsertedCSS(cssSelector.key); + dispatch(setCss(undefined)); + return; + } + await webview.removeInsertedCSS(cssSelector.key); + dispatch(setCss(undefined)); + } + + try { + const key = await webview.insertCSS(css); + dispatch(setCss({ key, css, name: colorDeficiency })); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error inserting css', error); + dispatch(setCss(undefined)); + } + }; + + const applySunlight = async (condition: string | undefined) => { + if (webview === null) { + return; + } + if (condition === undefined) { + return; + } + + const css = 'body {backdrop-filter: brightness(0.5) !important;}'; + + if (cssSelector !== undefined) { + if (cssSelector.css === css) { + await webview.removeInsertedCSS(cssSelector.key); + dispatch(setCss(undefined)); + return; + } + await webview.removeInsertedCSS(cssSelector.key); + dispatch(setCss(undefined)); + } + + try { + const key = await webview.insertCSS(css); + dispatch(setCss({ key, css, name: condition })); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error inserting css', error); + dispatch(setCss(undefined)); + } + }; + + const applyVisualImpairment = async ( + visualImpairment: string | undefined + ) => { + if (webview === null) { + return; + } + if (visualImpairment === undefined) { + return; + } + 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()]; + if (cssSelector !== undefined) { + if (cssSelector.css === css) { + await webview.removeInsertedCSS(cssSelector.key); + dispatch(setCss(undefined)); + return; + } + await webview.removeInsertedCSS(cssSelector.key); + dispatch(setCss(undefined)); + } + try { + const key = await webview.insertCSS(css); + if (visualImpairment.toLowerCase() === 'glaucome') { + try { + await webview.executeJavaScript( + 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`) + ); + // const listener = await webview.executeJavaScript() + } catch (e) { + // eslint-disable-next-line no-console + console.error({ e }); + } + } + + dispatch(setCss({ key, css, name: visualImpairment })); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error inserting css', error); + dispatch(setCss(undefined)); + } + }; + 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 +345,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: ( +
+ + {x} + +
+ ), + onClick: () => { + applyColorDeficiency(x.toLowerCase()); + }, + }; + }), + { + label: ( +
+ + Blue-yellow deficiency + +
+ ), + onClick: null, + }, + ...blueyellow.map((x: string) => { + return { + label: ( +
+ + {cssSelector?.name === x.toLowerCase() ? ( + + ) : ( + <> + )}{' '} + {x} + +
+ ), + onClick: () => { + applyColorDeficiency(x.toLowerCase()); + }, + }; + }), + { + label: ( +
+ + Full color deficiency + +
+ ), + onClick: null, + }, + ...full.map((x: string) => { + return { + label: ( +
+ + {x} + +
+ ), + onClick: () => { + applyColorDeficiency(x.toLowerCase()); + }, + }; + }), + { + label: ( +
+ Visual impairment +
+ ), + onClick: null, + }, + ...visualimpairments.map((x: string) => { + return { + label: ( +
+ + {x} + +
+ ), + onClick: () => { + applyVisualImpairment(x.toLowerCase()); + }, + }; + }), + { + label: ( +
+ Visual impairment +
+ ), + onClick: null, + }, + ...sunlight.map((x: string) => { + return { + label: ( +
+ + {x} + +
+ ), + onClick: () => { + applySunlight(x.toLowerCase()); + }, + }; + }), + ]} + />