diff --git a/NEWS.md b/NEWS.md index d05d11a5..97ed53c7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -24,6 +24,8 @@ * Certain graphical properties, e.g `lty` & `lwd`, now work correctly in the webR `canvas()` graphics device. (#289, #304). +* Various updates for the webR Demo App to improve accessibility. (#267, #269, #270, #271, #272, #273, #274). + # webR 0.2.1 ## New features diff --git a/src/repl/App.css b/src/repl/App.css index 6d1e5098..91d24379 100644 --- a/src/repl/App.css +++ b/src/repl/App.css @@ -28,6 +28,7 @@ body { border-style: solid; border-color: var(--bg-secondary); border-width: 0px 2px 2px 0px; + padding-top: 5px; } .term { @@ -53,7 +54,7 @@ body { border-color: var(--bg-secondary); border-width: 0px 0px 2px 2px; overflow: auto; - padding: 4px 0; + padding: 5px 0; } .plot { diff --git a/src/repl/App.tsx b/src/repl/App.tsx index e2c7039f..06c2c630 100644 --- a/src/repl/App.tsx +++ b/src/repl/App.tsx @@ -78,8 +78,8 @@ function App() { terminalInterface={terminalInterface} filesInterface={filesInterface} /> - + ); diff --git a/src/repl/components/Editor.css b/src/repl/components/Editor.css index f737a52c..fb37b623 100644 --- a/src/repl/components/Editor.css +++ b/src/repl/components/Editor.css @@ -1,4 +1,5 @@ .editor { + position: relative; display: grid; grid-template-rows: auto 1fr; } @@ -8,7 +9,13 @@ justify-content: space-between; align-items: center; border-bottom: 1px solid var(--bg-dark); - padding: 5px 5px 0 5px; + padding: 0 5px; +} + +.editor-actions { + position: absolute; + top: 5px; + right: 0; } .editor-actions > button { @@ -16,33 +23,44 @@ border: none; color: var(--secondary); font-size: 14px; + padding: 5px; } .editor-actions > button:hover { color: var(--primary); } -.editor-files > button { - font-size: 14px; - height: 26px; +.editor-files { + display: flex; +} + +.editor-file { + position: relative; + display: flex; + padding: 0 5px; + gap: 5px; background-color: var(--bg-secondary); color: var(--secondary); border: 1px solid var(--bg-dark); border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; - padding: 0 5px; } -.editor-files > button.active { +.editor-file > .editor-filename { + font-size: 14px; + height: 26px; + line-height: 26px; +} + +.editor-file.active { color: var(--primary); background-color: var(--bg-primary); border-color: var(--bg-dark); border-bottom-color: transparent; - position: relative; } -.editor-files > button.active::after { +.editor-file.active::after { content: ""; position: absolute; width: 100%; @@ -52,14 +70,27 @@ background-color: var(--bg-primary); } -.editor-files .editor-closebutton { - padding-left: 5px; - display: inline-block; +.editor-file > button { + background: transparent; + border: none; + padding: 0; +} + +.editor-file > button.editor-switch { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; +} + +.editor-file > button.editor-close { color: var(--secondary); font-weight: bold; + z-index: 1; } -.editor-files .editor-closebutton:hover { +.editor-file > button.editor-close:hover { color: var(--primary); } diff --git a/src/repl/components/Editor.tsx b/src/repl/components/Editor.tsx index 3e4f00bd..3bdcfdd2 100644 --- a/src/repl/components/Editor.tsx +++ b/src/repl/components/Editor.tsx @@ -37,17 +37,34 @@ export function FileTabs({ closeFile: (e: React.SyntheticEvent, index: number) => void; }) { return ( -
+
{files.map((f, index) => - + + + + +
)}
); @@ -292,7 +309,11 @@ export function Editor({ return editorView.domAtPos(0).node; } }); - editorView.focus(); + + // Update accessibility labelling + const container = editorView.contentDOM.parentElement; + container?.setAttribute('role', 'tabpanel'); + container?.setAttribute('aria-labelledby', `filetab-${activeFileIdx}`); // Before switching to a new file, save the state and scroll position return function cleanup() { @@ -303,7 +324,11 @@ export function Editor({ const displayStyle = files.length === 0 ? { display: 'none' } : undefined; return ( -
+
-
+
+
+
+

+ This component is an instance of the CodeMirror interactive text editor. + The editor has been configured so that the Tab key controls the indentation of code. + To move focus away from the editor, press the Escape key, and then press the Tab key directly after it. + Escape and then Shift-Tab can also be used to move focus backwards. +

+
+ {isRFile && } {!isReadOnly && } - {isRFile && }
-
-
); } diff --git a/src/repl/components/Files.css b/src/repl/components/Files.css index ad968e8b..717d3548 100644 --- a/src/repl/components/Files.css +++ b/src/repl/components/Files.css @@ -56,14 +56,14 @@ .directory .tree-node { cursor: pointer; + border-left: 2px solid transparent; } -.directory .tree-node:hover { - background: var(--bg-secondary); -} - -.directory .tree .tree-node--focused { - background: var(--bg-secondary); +.directory .tree-node:hover, +.directory .tree-node:focus, +.directory .tree-branch-wrapper:focus > .tree-node +{ + border-left: 2px solid var(--secondary); } .directory .tree .tree-node--selected { diff --git a/src/repl/components/Files.tsx b/src/repl/components/Files.tsx index 65eec324..f5b6b07a 100644 --- a/src/repl/components/Files.tsx +++ b/src/repl/components/Files.tsx @@ -49,6 +49,7 @@ export function Files({ const [treeData, setTreeData] = React.useState(initialData); const [selectedNode, setSelectedNode] = React.useState(); const [isFileSelected, setIsFileSelected] = React.useState(true); + const [selectedIds, setSelectedIds] = React.useState([1]); const uploadRef = React.useRef(null); const uploadButtonRef = React.useRef(null); const downloadButtonRef = React.useRef(null); @@ -56,12 +57,15 @@ export function Files({ const nodeRenderer: ITreeViewProps['nodeRenderer'] = ({ element, isExpanded, - isBranch, getNodeProps, level, }) => ( -
- { isBranch ? : } +
+ { + element.metadata!.type === 'folder' + ? + : + } { element.name }
); @@ -76,7 +80,13 @@ export function Files({ const onNodeSelect: ITreeViewProps['onNodeSelect'] = ({ element }) => { setSelectedNode(element); - setIsFileSelected(!element.isBranch); + setIsFileSelected(element.metadata!.type === 'file'); + }; + + const onExpand: ITreeViewProps['onExpand'] = ({ element }) => { + setSelectedNode(element); + setIsFileSelected(element.metadata!.type === 'file'); + setSelectedIds([Number(element.id)]); }; const onUpload: ChangeEventHandler = () => { @@ -177,7 +187,7 @@ export function Files({ } try { - if (selectedNode.isBranch) { + if (selectedNode.metadata!.type === 'folder') { await webR.FS.rmdir(path); } else { await webR.FS.unlink(path); @@ -204,7 +214,7 @@ export function Files({ metadata: {type: 'folder'}} ); data.forEach((node) => { - if ( node.metadata!.type === 'folder') { + if ( node.metadata!.type === 'folder' && node.children.length > 0) { node.isBranch = true; } }); @@ -212,29 +222,44 @@ export function Files({ }; }, [filesInterface]); + const treeView = ; + return ( -
+
-
+
-
- +
+ {treeData[0].name ? treeView : undefined}
); diff --git a/src/repl/components/Plot.tsx b/src/repl/components/Plot.tsx index 000b441c..f53f32af 100644 --- a/src/repl/components/Plot.tsx +++ b/src/repl/components/Plot.tsx @@ -8,7 +8,7 @@ export function Plot({ }: { plotInterface: PlotInterface; }) { - const plotContainterRef = React.useRef(null); + const plotContainerRef = React.useRef(null); const canvasRef = React.useRef(null); const canvasElements = React.useRef([]); const [selectedCanvas, setSelectedCanvas] = React.useState(null); @@ -25,25 +25,27 @@ export function Plot({ // If a new plot is created in R, add it to the list of canvas elements plotInterface.newPlot = () => { + const plotNumber = canvasElements.current.length + 1; const canvas = document.createElement('canvas'); canvas.setAttribute('width', '1008'); canvas.setAttribute('height', '1008'); + canvas.setAttribute('aria-label', `R Plot ${plotNumber}`); canvasRef.current = canvas; canvasElements.current.push(canvas); - setSelectedCanvas(canvasElements.current.length - 1); + setSelectedCanvas(plotNumber - 1); }; }, [plotInterface]); // Update the plot container to display the currently selected canvas element React.useEffect(() => { - if (!plotContainterRef.current) { + if (!plotContainerRef.current) { return; } if (selectedCanvas === null) { - plotContainterRef.current.replaceChildren(); + plotContainerRef.current.replaceChildren(); } else { const canvas = canvasElements.current[selectedCanvas]; - plotContainterRef.current.replaceChildren(canvas); + plotContainerRef.current.replaceChildren(canvas); } }, [selectedCanvas]); @@ -65,30 +67,49 @@ export function Plot({ const prevPlot = () => setSelectedCanvas((selectedCanvas === null) ? null : selectedCanvas - 1); return ( -
+
-
- - -
-
+
); diff --git a/src/repl/components/Terminal.tsx b/src/repl/components/Terminal.tsx index cef1de0b..997b309e 100644 --- a/src/repl/components/Terminal.tsx +++ b/src/repl/components/Terminal.tsx @@ -17,19 +17,25 @@ export function Terminal({ const termRef = React.useRef(null); const [readline, setReadline] = React.useState(); - const handleCtrlC = React.useCallback((event: KeyboardEvent) => { + const handleShortcuts = React.useCallback((event: KeyboardEvent) => { + // Allow escaping the terminal with Tab navigation + if (event.key === 'Tab') { + event.stopPropagation(); + } + + // Interrupt R code executed by the Editor if (event.key === 'c' && event.ctrlKey) { webR.interrupt(); } }, []); - // Handle ctrl-c here so that code executed by the Editor can be interrupted + // Add additional keyboard shortcut handlers React.useEffect(() => { - divRef.current!.addEventListener('keydown', handleCtrlC, true); + divRef.current!.addEventListener('keydown', handleShortcuts, true); return () => { - divRef.current!.removeEventListener('keydown', handleCtrlC); + divRef.current!.removeEventListener('keydown', handleShortcuts); }; - }, [handleCtrlC]); + }, [handleShortcuts]); React.useEffect(() => { // Don't reinitialise XTerminal after an instance has been created @@ -55,6 +61,8 @@ export function Terminal({ term.loadAddon(fitAddon); term.loadAddon(readline); term.open(divRef.current); + term.element?.setAttribute('aria-label', 'R Terminal'); + term.element?.setAttribute('tabindex', '-1'); fitAddon.fit(); const resizeObserver = new ResizeObserver(() => { @@ -92,7 +100,12 @@ export function Terminal({ }; }, [readline, terminalInterface]); - return
; + return
; } export default Terminal;