mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-25 10:10:08 +02:00
Make editor popover toolbar keyboard focusable (#25169)
* Remove redundant class conflicting with focus styling * Make the toolbar in the popover focusable via keyboard * Focus to the first context menu item via keyboard only GitOrigin-RevId: 7d3e2af4ba96654b5b2312b3999483c2a439b406
This commit is contained in:
@@ -1025,6 +1025,7 @@
|
||||
"more_collabs_per_project": "",
|
||||
"more_comments": "",
|
||||
"more_compile_time": "",
|
||||
"more_editor_toolbar_item": "",
|
||||
"more_info": "",
|
||||
"more_options": "",
|
||||
"more_options_for_border_settings_coming_soon": "",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import React, { useCallback, useEffect, useRef } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import {
|
||||
Dropdown,
|
||||
@@ -13,6 +13,7 @@ function FileTreeContextMenu() {
|
||||
const { fileTreeReadOnly } = useFileTreeData()
|
||||
const { contextMenuCoords, setContextMenuCoords } = useFileTreeMainContext()
|
||||
const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
|
||||
const keyboardInputRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (contextMenuCoords) {
|
||||
@@ -22,12 +23,24 @@ function FileTreeContextMenu() {
|
||||
}
|
||||
}, [contextMenuCoords])
|
||||
|
||||
if (!contextMenuCoords || fileTreeReadOnly) return null
|
||||
useEffect(() => {
|
||||
if (contextMenuCoords && keyboardInputRef.current) {
|
||||
const firstDropdownMenuItem = document.querySelector(
|
||||
'#dropdown-file-tree-context-menu .dropdown-item:not([disabled])'
|
||||
) as HTMLButtonElement | null
|
||||
|
||||
if (firstDropdownMenuItem) {
|
||||
firstDropdownMenuItem.focus()
|
||||
}
|
||||
}
|
||||
}, [contextMenuCoords])
|
||||
|
||||
function close() {
|
||||
if (!contextMenuCoords) return
|
||||
setContextMenuCoords(null)
|
||||
|
||||
if (toggleButtonRef.current) {
|
||||
// A11y - Move the focus back to the toggle button when the context menu closes by pressing the Esc key
|
||||
// A11y - Focus moves back to the trigger button when the context menu is dismissed
|
||||
toggleButtonRef.current.focus()
|
||||
}
|
||||
}
|
||||
@@ -36,14 +49,33 @@ function FileTreeContextMenu() {
|
||||
if (!wantOpen) close()
|
||||
}
|
||||
|
||||
// A11y - Close the context menu when the user presses the Tab key
|
||||
// Focus should move to the next element in the filetree
|
||||
function handleKeyDown(event: React.KeyboardEvent<Element>) {
|
||||
if (event.key === 'Tab') {
|
||||
function handleClose(event: React.KeyboardEvent<Element>) {
|
||||
if (event.key === 'Tab' || event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = useCallback(() => {
|
||||
keyboardInputRef.current = true
|
||||
}, [])
|
||||
|
||||
const handleMouseDown = useCallback(() => {
|
||||
keyboardInputRef.current = false
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
document.addEventListener('mousedown', handleMouseDown)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
document.removeEventListener('mousedown', handleMouseDown)
|
||||
}
|
||||
}, [handleKeyDown, handleMouseDown])
|
||||
|
||||
if (!contextMenuCoords || fileTreeReadOnly) return null
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div style={contextMenuCoords} className="context-menu">
|
||||
<Dropdown
|
||||
@@ -54,8 +86,7 @@ function FileTreeContextMenu() {
|
||||
? 'up'
|
||||
: 'down'
|
||||
}
|
||||
focusFirstItemOnShow // A11y - Focus the first item in the context menu when it opens since the menu is rendered at the root level
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyDown={handleClose}
|
||||
onToggle={handleToggle}
|
||||
>
|
||||
<DropdownMenu
|
||||
|
||||
@@ -21,7 +21,7 @@ export const ToolbarButtonMenu: FC<{
|
||||
const button = (
|
||||
<button
|
||||
type="button"
|
||||
className="ol-cm-toolbar-button btn"
|
||||
className="ol-cm-toolbar-button"
|
||||
aria-label={label}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FC, useRef } from 'react'
|
||||
import { FC, useCallback, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import classnames from 'classnames'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { useCodeMirrorViewContext } from '../codemirror-context'
|
||||
@@ -11,7 +12,10 @@ export const ToolbarOverflow: FC<{
|
||||
setOverflowOpen: (open: boolean) => void
|
||||
overflowRef?: React.Ref<HTMLDivElement>
|
||||
}> = ({ overflowed, overflowOpen, setOverflowOpen, overflowRef, children }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const keyboardInputRef = useRef(false)
|
||||
const view = useCodeMirrorViewContext()
|
||||
|
||||
const className = classnames(
|
||||
@@ -22,6 +26,46 @@ export const ToolbarOverflow: FC<{
|
||||
}
|
||||
)
|
||||
|
||||
// A11y - Move the focus inside the popover to the first toolbar button when it opens
|
||||
const handlePopoverFocus = useCallback(() => {
|
||||
if (keyboardInputRef.current) {
|
||||
const firstToolbarItem = document.querySelector(
|
||||
'#popover-toolbar-overflow .ol-cm-toolbar-overflow button:not([disabled])'
|
||||
) as HTMLButtonElement | null
|
||||
|
||||
if (firstToolbarItem) {
|
||||
firstToolbarItem.focus()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleKeyDown = useCallback(() => {
|
||||
keyboardInputRef.current = true
|
||||
}, [])
|
||||
|
||||
const handleMouseDown = useCallback(() => {
|
||||
keyboardInputRef.current = false
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
document.addEventListener('mousedown', handleMouseDown)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
document.removeEventListener('mousedown', handleMouseDown)
|
||||
}
|
||||
}, [handleKeyDown, handleMouseDown])
|
||||
|
||||
// A11y - Move the focus back to the trigger when the popover is dismissed
|
||||
const handleCloseAndReturnFocus = useCallback(() => {
|
||||
setOverflowOpen(false)
|
||||
|
||||
if (keyboardInputRef.current && buttonRef.current) {
|
||||
buttonRef.current.focus()
|
||||
}
|
||||
}, [setOverflowOpen])
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
@@ -29,7 +73,9 @@ export const ToolbarOverflow: FC<{
|
||||
type="button"
|
||||
id="toolbar-more"
|
||||
className={className}
|
||||
aria-label="More"
|
||||
aria-label={t('more_editor_toolbar_item')}
|
||||
aria-expanded={overflowOpen}
|
||||
aria-controls="popover-toolbar-overflow"
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@@ -49,9 +95,14 @@ export const ToolbarOverflow: FC<{
|
||||
// containerPadding={0}
|
||||
transition
|
||||
rootClose
|
||||
onHide={() => setOverflowOpen(false)}
|
||||
onHide={handleCloseAndReturnFocus}
|
||||
onEntered={handlePopoverFocus}
|
||||
>
|
||||
<OLPopover id="popover-toolbar-overflow" ref={overflowRef}>
|
||||
<OLPopover
|
||||
id="popover-toolbar-overflow"
|
||||
ref={overflowRef}
|
||||
role="toolbar"
|
||||
>
|
||||
<div className="ol-cm-toolbar-overflow">{children}</div>
|
||||
</OLPopover>
|
||||
</OLOverlay>
|
||||
|
||||
@@ -36,7 +36,7 @@ export const LegacyTableDropdown = memo(() => {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="ol-cm-toolbar-button btn"
|
||||
className="ol-cm-toolbar-button"
|
||||
aria-label={t('toolbar_insert_table')}
|
||||
onMouseDown={event => {
|
||||
event.preventDefault()
|
||||
|
||||
@@ -1344,6 +1344,7 @@
|
||||
"more_collabs_per_project": "More collaborators per project",
|
||||
"more_comments": "More comments",
|
||||
"more_compile_time": "More compile time",
|
||||
"more_editor_toolbar_item": "More editor toolbar items",
|
||||
"more_info": "More Info",
|
||||
"more_options": "More options",
|
||||
"more_options_for_border_settings_coming_soon": "More options for border settings coming soon.",
|
||||
|
||||
@@ -121,7 +121,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
||||
mountEditor('test')
|
||||
selectAll()
|
||||
|
||||
clickToolbarButton('More')
|
||||
clickToolbarButton('More editor toolbar items')
|
||||
clickToolbarButton('Bullet List')
|
||||
|
||||
cy.get('.cm-content').should('have.text', ' test')
|
||||
@@ -134,7 +134,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
||||
mountEditor('test')
|
||||
selectAll()
|
||||
|
||||
clickToolbarButton('More')
|
||||
clickToolbarButton('More editor toolbar items')
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
cy.get('.cm-content').should('have.text', ' test')
|
||||
@@ -147,7 +147,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
||||
mountEditor('test')
|
||||
selectAll()
|
||||
|
||||
clickToolbarButton('More')
|
||||
clickToolbarButton('More editor toolbar items')
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
// expose the markup
|
||||
@@ -180,7 +180,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
||||
mountEditor('test')
|
||||
selectAll()
|
||||
|
||||
clickToolbarButton('More')
|
||||
clickToolbarButton('More editor toolbar items')
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
// expose the markup
|
||||
@@ -205,7 +205,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
||||
mountEditor('test\ntest')
|
||||
selectAll()
|
||||
|
||||
clickToolbarButton('More')
|
||||
clickToolbarButton('More editor toolbar items')
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
// expose the markup
|
||||
@@ -242,7 +242,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
||||
|
||||
cy.get('.cm-line').eq(1).click()
|
||||
|
||||
clickToolbarButton('More')
|
||||
clickToolbarButton('More editor toolbar items')
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
cy.get('.cm-line').eq(0).type('{upArrow}')
|
||||
@@ -263,7 +263,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
||||
mountEditor('test\ntest')
|
||||
selectAll()
|
||||
|
||||
clickToolbarButton('More')
|
||||
clickToolbarButton('More editor toolbar items')
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
// expose the markup
|
||||
@@ -300,7 +300,7 @@ describe('<CodeMirrorEditor/> toolbar in Rich Text mode', function () {
|
||||
|
||||
cy.get('.cm-line').eq(0).click()
|
||||
|
||||
clickToolbarButton('More')
|
||||
clickToolbarButton('More editor toolbar items')
|
||||
clickToolbarButton('Numbered List')
|
||||
|
||||
// expose the markup
|
||||
|
||||
Reference in New Issue
Block a user