Merge pull request #1160 from responsively-org/accessibility-polish

Accessibility UI polish & Global toolbar controls
This commit is contained in:
Manoj Vivek 2023-11-14 20:04:07 +05:30 committed by GitHub
commit 598892e785
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 458 additions and 335 deletions

View file

@ -59,7 +59,7 @@ export function DropDown({ label, options, className }: Props) {
option.onClick !== null ? (
<button
className={cx(
'group flex w-full items-center rounded-md px-2 py-2',
'group flex w-full items-center rounded-md px-2 py-2 text-sm',
{ 'bg-slate-200 dark:bg-slate-800': active }
)}
type="button"
@ -70,8 +70,7 @@ export function DropDown({ label, options, className }: Props) {
) : (
<div
className={cx(
'group mt-2 flex w-full items-center rounded-md px-2',
{ 'bg-slate-200 dark:bg-slate-800': active }
'group mt-2 flex w-full items-center rounded-md px-2'
)}
>
{option.label}

View file

@ -0,0 +1,237 @@
import { Icon } from '@iconify/react';
import cx from 'classnames';
import { useCallback, useEffect, useState } from 'react';
import { DropDown } from 'renderer/components/DropDown';
import { COLOR_BLINDNESS_CHANNEL } from 'renderer/components/ToolBar/ColorBlindnessControls';
import {
BLUE_YELLOW,
FULL,
RED_GREEN,
SIMULATIONS,
SUNLIGHT,
VISUAL_IMPAIRMENTS,
VisionSimulationDropDown,
} from 'renderer/components/VisionSimulationDropDown';
import { webViewPubSub } from 'renderer/lib/pubsub';
interface InjectedCss {
key: string;
css: string;
js: string | null;
name: string;
}
interface Props {
webview: Electron.WebviewTag | null;
}
export const ColorBlindnessTools = ({ webview }: Props) => {
const [injectCss, setInjectCss] = useState<InjectedCss>();
const reApplyCss = useCallback(async () => {
if (webview === null) {
return;
}
if (injectCss === undefined) {
return;
}
const key = await webview.insertCSS(injectCss.css);
if (injectCss.js != null) {
await webview.executeJavaScript(injectCss.js);
}
setInjectCss({ ...injectCss, key });
}, [webview, injectCss, setInjectCss]);
const applyCss = useCallback(
async (debugType: string, css: string, js: string | null = null) => {
if (webview === null) {
return;
}
if (css === undefined) {
return;
}
if (injectCss !== undefined) {
if (injectCss.name === debugType) {
return;
}
if (injectCss.js !== null) {
webview.reload();
}
await webview.removeInsertedCSS(injectCss.key);
setInjectCss(undefined);
}
try {
const key = await webview.insertCSS(css);
if (js !== null) {
await webview.executeJavaScript(js);
}
setInjectCss({ key, css, name: debugType, js });
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error inserting css', error);
// dispatch(setCss(undefined));
setInjectCss(undefined);
}
},
[setInjectCss, webview, injectCss]
);
const clearSimulation = useCallback(async () => {
if (webview === null) {
return;
}
if (injectCss === undefined) {
return;
}
await webview.removeInsertedCSS(injectCss.key);
setInjectCss(undefined);
}, [webview, injectCss, setInjectCss]);
useEffect(() => {
if (webview === null) {
return () => {};
}
const handler = async () => {
reApplyCss();
};
webview.addEventListener('did-navigate', handler);
return () => {
webview.removeEventListener('did-navigate', handler);
};
}, [webview, reApplyCss]);
const applyColorDeficiency = useCallback(
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);
},
[applyCss]
);
const applySunlight = useCallback(
async (condition: string) => {
const css = 'body {backdrop-filter: brightness(0.5) !important;}';
return applyCss(condition, css);
},
[applyCss]
);
const applyVisualImpairment = useCallback(
async (visualImpairment: string) => {
const blur =
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8ZGVmcz4KICAgICAgICA8ZmlsdGVyIGlkPSJnYXVzc2lhbl9ibHVyIj4KICAgICAgICAgICAgPGZlR2F1c3NpYW5CbHVyIGluPSJTb3VyY2VHcmFwaGljIiBzdGREZXZpYXRpb249IjEwIiAvPgogICAgICAgIDwvZmlsdGVyPgogICAgPC9kZWZzPgo8L3N2Zz4=';
const impairments: { [key: string]: string } = {
[SIMULATIONS.CATARACT]: `body {
-webkit-filter: url('${blur}#gaussian_blur');
filter: url('${blur}#gaussian_blur');
}`,
[SIMULATIONS.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;
}`,
[SIMULATIONS.FAR]: `body { filter: blur(2px); }`,
};
const css = impairments[visualImpairment.toLowerCase()];
let js = null;
if (visualImpairment.toLowerCase() === SIMULATIONS.GLAUCOME) {
js = String(`var div = document.createElement('div');
div.innerHTML ='<div class="bigoverlay" id="bigoverlay"><div class="spotlight" id="spotlight"></div></div>';
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);
},
[applyCss]
);
const applySimulation = useCallback(
async (simulation = '') => {
if (
RED_GREEN.indexOf(simulation) !== -1 ||
BLUE_YELLOW.indexOf(simulation) !== -1 ||
FULL.indexOf(simulation) !== -1
) {
return applyColorDeficiency(simulation);
}
if (VISUAL_IMPAIRMENTS.indexOf(simulation) !== -1) {
return applyVisualImpairment(simulation);
}
if (SUNLIGHT.indexOf(simulation) !== -1) {
return applySunlight(simulation);
}
return clearSimulation();
},
[
applyColorDeficiency,
applyVisualImpairment,
applySunlight,
clearSimulation,
]
);
useEffect(() => {
const handler = ({ simulationName }: { simulationName: string }) => {
applySimulation(simulationName);
};
webViewPubSub.subscribe(COLOR_BLINDNESS_CHANNEL, handler);
return () => {
webViewPubSub.unsubscribe(COLOR_BLINDNESS_CHANNEL, handler);
};
}, [applySimulation]);
return (
<VisionSimulationDropDown
simulationName={injectCss?.name}
onChange={applySimulation}
/>
);
};

View file

@ -8,14 +8,7 @@ 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;
}
import { ColorBlindnessTools } from './ColorBlindnessTools';
interface Props {
webview: Electron.WebviewTag | null;
@ -36,9 +29,6 @@ const Toolbar = ({
onIndividualLayoutHandler,
isIndividualLayout,
}: Props) => {
const dispatch = useDispatch();
// const cssSelector: InjectedCss | undefined = useSelector(selectCss);
const [injectCss, setInjectCss] = useState<InjectedCss>();
const [eventMirroringOff, setEventMirroringOff] = useState<boolean>(false);
const [playScreenshotDone] = useSound(screenshotSfx, { volume: 0.5 });
const [screenshotLoading, setScreenshotLoading] = useState<boolean>(false);
@ -46,17 +36,6 @@ const Toolbar = ({
useState<boolean>(false);
const [rotated, setRotated] = useState<boolean>(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();
@ -106,120 +85,6 @@ 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 ='<div class="bigoverlay" id="bigoverlay"><div class="spotlight" id="spotlight"></div></div>';
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) {
return;
@ -313,202 +178,7 @@ const Toolbar = ({
}
/>
</Button>
<DropDown
className="text-xs"
label={<Icon icon="codicon:debug-line-by-line" fontSize={18} />}
options={[
{
label: (
<div className="flex w-full flex-shrink-0 items-center justify-between gap-12 whitespace-nowrap">
<span className="font-bold">A11y Tools</span>
</div>
),
onClick: null,
},
{
label: (
<div className="flex w-full flex-shrink-0 items-center justify-between gap-12 whitespace-nowrap">
<span>Visual deficiency</span>
</div>
),
onClick: null,
},
{
label: (
<div className="flex w-full flex-shrink-0 items-center justify-start gap-12 whitespace-nowrap">
<span className="ml-1 font-semibold">
Red-green deficiency
</span>
</div>
),
onClick: null,
},
...redgreen.map((x: string) => {
return {
label: (
<div className="justify-normal flex w-full flex-shrink-0 items-center gap-1 whitespace-nowrap">
{injectCss?.name === x.toLowerCase() ? (
<Icon icon="ic:round-check" />
) : (
<></>
)}
<span
className={`ml-2 ${
injectCss?.name === x.toLowerCase()
? 'font-semibold text-black'
: ''
}`}
>
{x}
</span>
</div>
),
onClick: () => {
applyColorDeficiency(x.toLowerCase());
},
};
}),
{
label: (
<div className="flex w-full flex-shrink-0 items-center justify-between gap-12 whitespace-nowrap">
<span className="ml-1 font-semibold">
Blue-yellow deficiency
</span>
</div>
),
onClick: null,
},
...blueyellow.map((x: string) => {
return {
label: (
<div className="justify-normal flex w-full flex-shrink-0 items-center gap-1 whitespace-nowrap">
{injectCss?.name === x.toLowerCase() ? (
<Icon icon="ic:round-check" />
) : (
<></>
)}
<span
className={`ml-2 ${
injectCss?.name === x.toLowerCase()
? 'font-semibold text-black'
: ''
}`}
>
{x}
</span>
</div>
),
onClick: () => {
applyColorDeficiency(x.toLowerCase());
},
};
}),
{
label: (
<div className="flex w-full flex-shrink-0 items-center justify-between gap-1 whitespace-nowrap">
<span className="ml-1 font-semibold">
Full color deficiency
</span>
</div>
),
onClick: null,
},
...full.map((x: string) => {
return {
label: (
<div className="justify-normal flex w-full flex-shrink-0 items-center gap-1 whitespace-nowrap">
{injectCss?.name === x.toLowerCase() ? (
<Icon icon="ic:round-check" />
) : (
<></>
)}
<span
className={`ml-2 ${
injectCss?.name === x.toLowerCase()
? 'font-semibold text-black'
: ''
}`}
>
{x}
</span>
</div>
),
onClick: () => {
applyColorDeficiency(x.toLowerCase());
},
};
}),
{
label: (
<div className="flex w-full flex-shrink-0 items-center justify-between gap-1 whitespace-nowrap">
<span className="ml-1 font-semibold">Visual impairment</span>
</div>
),
onClick: null,
},
...visualimpairments.map((x: string) => {
return {
label: (
<div className="justify-normal flex w-full flex-shrink-0 items-center gap-1 whitespace-nowrap">
{injectCss?.name === x.toLowerCase() ? (
<Icon icon="ic:round-check" />
) : (
<></>
)}
<span
className={`ml-2 ${
injectCss?.name === x.toLowerCase()
? 'font-semibold text-black'
: ''
}`}
>
{x}
</span>
</div>
),
onClick: () => {
applyVisualImpairment(x.toLowerCase());
},
};
}),
{
label: (
<div className="flex w-full flex-shrink-0 items-center justify-between gap-1 whitespace-nowrap">
<span className="ml-1 font-semibold">
Temporary impairment
</span>
</div>
),
onClick: null,
},
...sunlight.map((x: string) => {
return {
label: (
<div className="justify-normal flex w-full flex-shrink-0 items-center gap-1 whitespace-nowrap">
{injectCss?.name === x.toLowerCase() ? (
<Icon icon="ic:round-check" />
) : (
<></>
)}
<span
className={`ml-2 ${
injectCss?.name === x.toLowerCase()
? 'font-semibold text-black'
: ''
}`}
>
{x}
</span>
</div>
),
onClick: () => {
applySunlight(x.toLowerCase());
},
};
}),
]}
/>
<ColorBlindnessTools webview={webview} />
</div>
<Button
onClick={() => onIndividualLayoutHandler(device)}

View file

@ -0,0 +1,22 @@
import { useEffect, useState } from 'react';
import { VisionSimulationDropDown } from 'renderer/components/VisionSimulationDropDown';
import { webViewPubSub } from 'renderer/lib/pubsub';
export const COLOR_BLINDNESS_CHANNEL = 'color-blindness';
export const ColorBlindnessControls = () => {
const [simulationName, setSimulationName] = useState<string | undefined>(
undefined
);
useEffect(() => {
webViewPubSub.publish(COLOR_BLINDNESS_CHANNEL, { simulationName });
}, [simulationName]);
return (
<VisionSimulationDropDown
simulationName={simulationName}
onChange={setSimulationName}
/>
);
};

View file

@ -24,6 +24,7 @@ import useKeyboardShortcut, {
SHORTCUT_CHANNEL,
} from '../KeyboardShortcutsManager/useKeyboardShortcut';
import Shortcuts from './Shortcuts';
import { ColorBlindnessControls } from './ColorBlindnessControls';
const Divider = () => <div className="h-6 w-px bg-gray-300 dark:bg-gray-700" />;
@ -129,6 +130,7 @@ const ToolBar = () => {
</Button>
<ColorSchemeToggle />
<Shortcuts />
<ColorBlindnessControls />
<Divider />
<PreviewSuiteSelector />
<Menu />

View file

@ -0,0 +1,193 @@
import cx from 'classnames';
import { Icon } from '@iconify/react';
import { DropDown } from '../DropDown';
const MenuItemLabel = ({
label,
isActive,
}: {
label: string;
isActive: boolean;
}) => {
return (
<div className="justify-normal flex w-full flex-shrink-0 items-center gap-1 whitespace-nowrap">
<Icon
icon="ic:round-check"
className={cx('opacity-0', {
'opacity-100': isActive,
})}
/>
<span
className={cx({
'font-semibold text-black dark:text-white': isActive,
})}
>
{label}
</span>
</div>
);
};
const MenuItemHeader = ({ label }: { label: string }) => {
return (
<div className="relative flex w-full min-w-44 items-center justify-between gap-1 whitespace-nowrap">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-300 dark:border-gray-600" />
</div>
<span className="mxl-1 z-10 flex-shrink-0 bg-slate-100 pr-2 dark:bg-slate-900">
{label}
</span>
</div>
);
};
export const SIMULATIONS = {
DEUTERANOPIA: 'deuteranopia',
DEUTERANOMALY: 'deuteranomaly',
PROTANOPIA: 'protanopia',
PROTANOMALY: 'protanomaly',
TRITANOPIA: 'tritanopia',
TRITANOMALY: 'tritanomaly',
ACHROMATOMALY: 'achromatomaly',
ACHROMATOPSIA: 'achromatopsia',
CATARACT: 'cataract',
FAR: 'farsightedness',
GLAUCOME: 'glaucoma',
SOLARIZE: 'solarize',
};
export const RED_GREEN = [
SIMULATIONS.DEUTERANOPIA,
SIMULATIONS.DEUTERANOMALY,
SIMULATIONS.PROTANOPIA,
SIMULATIONS.PROTANOMALY,
];
export const BLUE_YELLOW = [SIMULATIONS.TRITANOPIA, SIMULATIONS.TRITANOMALY];
export const FULL = [SIMULATIONS.ACHROMATOMALY, SIMULATIONS.ACHROMATOPSIA];
export const VISUAL_IMPAIRMENTS = [
SIMULATIONS.CATARACT,
SIMULATIONS.FAR,
SIMULATIONS.GLAUCOME,
];
export const SUNLIGHT = [SIMULATIONS.SOLARIZE];
interface Props {
simulationName: string | undefined;
onChange: (name: string | undefined) => void;
}
export const VisionSimulationDropDown = ({
simulationName,
onChange,
}: Props) => {
return (
<DropDown
className={cx('rounded-lg text-xs', {
'bg-slate-400/60': simulationName != null,
})}
label={<Icon icon="bx:low-vision" fontSize={18} />}
options={[
{
label: <MenuItemHeader label="No deficiency" />,
onClick: null,
},
{
label: (
<MenuItemLabel
label="Normal Vision"
isActive={simulationName === undefined}
/>
),
onClick: () => {
onChange(undefined);
},
},
{
label: <MenuItemHeader label="Red-green deficiency" />,
onClick: null,
},
...RED_GREEN.map((x: string) => {
return {
label: (
<MenuItemLabel
label={x}
isActive={simulationName === x.toLowerCase()}
/>
),
onClick: () => {
onChange(x.toLowerCase());
},
};
}),
{
label: <MenuItemHeader label="Blue-yellow deficiency" />,
onClick: null,
},
...BLUE_YELLOW.map((x: string) => {
return {
label: (
<MenuItemLabel
label={x}
isActive={simulationName === x.toLowerCase()}
/>
),
onClick: () => {
onChange(x.toLowerCase());
},
};
}),
{
label: <MenuItemHeader label="Full color deficiency" />,
onClick: null,
},
...FULL.map((x: string) => {
return {
label: (
<MenuItemLabel
label={x}
isActive={simulationName === x.toLowerCase()}
/>
),
onClick: () => {
onChange(x.toLowerCase());
},
};
}),
{
label: <MenuItemHeader label="Visual impairment" />,
onClick: null,
},
...VISUAL_IMPAIRMENTS.map((x: string) => {
return {
label: (
<MenuItemLabel
label={x}
isActive={simulationName === x.toLowerCase()}
/>
),
onClick: () => {
onChange(x.toLowerCase());
},
};
}),
{
label: <MenuItemHeader label="Temporary impairment" />,
onClick: null,
},
...SUNLIGHT.map((x: string) => {
return {
label: (
<MenuItemLabel
label={x}
isActive={simulationName === x.toLowerCase()}
/>
),
onClick: () => {
onChange(x.toLowerCase());
},
};
}),
]}
/>
);
};