mirror of
https://github.com/responsively-org/responsively-app
synced 2024-11-10 06:44:13 +00:00
Merge pull request #1160 from responsively-org/accessibility-polish
Accessibility UI polish & Global toolbar controls
This commit is contained in:
commit
598892e785
6 changed files with 458 additions and 335 deletions
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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)}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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 />
|
||||
|
|
|
@ -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());
|
||||
},
|
||||
};
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
Loading…
Reference in a new issue