Merge pull request #3466 from overleaf/ta-accessible-modal

Fix Aria-Hidden Modals

GitOrigin-RevId: bcce61104220ebcf04e9c348d9f3ab84bff8804a
This commit is contained in:
Timothée Alby 2021-01-05 11:57:45 +01:00 committed by Copybot
parent 204d9a7138
commit f36269ef0a
5 changed files with 59 additions and 27 deletions

View file

@ -5,6 +5,8 @@ import { Button, Modal } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { useRefWithAutoFocus } from '../../../../infrastructure/auto-focus'
import AccessibleModal from '../../../../shared/components/accessible-modal'
import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
import { DuplicateFilenameError } from '../../errors'
@ -44,7 +46,7 @@ function FileTreeModalCreateFolder() {
}
return (
<Modal show={isCreatingFolder} onHide={handleHide}>
<AccessibleModal show={isCreatingFolder} onHide={handleHide}>
<Modal.Header>
<Modal.Title>{t('new_folder')}</Modal.Title>
</Modal.Header>
@ -95,7 +97,7 @@ function FileTreeModalCreateFolder() {
</>
)}
</Modal.Footer>
</Modal>
</AccessibleModal>
)
}

View file

@ -2,6 +2,8 @@ import React from 'react'
import { Button, Modal } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import AccessibleModal from '../../../../shared/components/accessible-modal'
import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
function FileTreeModalDelete() {
@ -27,7 +29,7 @@ function FileTreeModalDelete() {
}
return (
<Modal show={isDeleting} onHide={handleHide}>
<AccessibleModal show={isDeleting} onHide={handleHide}>
<Modal.Header>
<Modal.Title>{t('delete')}</Modal.Title>
</Modal.Header>
@ -60,7 +62,7 @@ function FileTreeModalDelete() {
</>
)}
</Modal.Footer>
</Modal>
</AccessibleModal>
)
}

View file

@ -0,0 +1,36 @@
import React, { useCallback } from 'react'
import PropTypes from 'prop-types'
import { Modal } from 'react-bootstrap'
// a bootstrap Modal with its `aria-hidden` attribute removed. Visisble modals
// should not have their `aria-hidden` attribute set but that's a bug in our
// version of react-bootstrap.
function AccessibleModal({ show, ...otherProps }) {
// use a callback ref to track the modal. This will re-run the function
// when the element node or any of the dependencies are updated
const setModalRef = useCallback(
element => {
if (!element) return
const modalNode = element._modal && element._modal.modalNode
if (!modalNode) return
if (show) {
modalNode.removeAttribute('aria-hidden')
} else {
modalNode.setAttribute('aria-hidden', 'true')
}
},
// `show` is necessary as a dependency, but eslint thinks it is not
// eslint-disable-next-line react-hooks/exhaustive-deps
[show]
)
return <Modal show={show} {...otherProps} ref={setModalRef} />
}
AccessibleModal.propTypes = {
show: PropTypes.bool
}
export default AccessibleModal

View file

@ -1,7 +1,7 @@
import { expect } from 'chai'
import React from 'react'
import sinon from 'sinon'
import { screen, render, fireEvent } from '@testing-library/react'
import { screen, render, fireEvent, waitFor } from '@testing-library/react'
import fetchMock from 'fetch-mock'
import MockedSocket from 'socket.io-mock'
@ -56,7 +56,7 @@ describe('FileTree Create Folder Flow', function() {
}
fetchMock.post(matcher, response)
fireCreateFolder(newFolderName)
await fireCreateFolder(newFolderName)
const lastCallBody = JSON.parse(fetchMock.lastCall(matcher)[1].body)
expect(lastCallBody.name).to.equal(newFolderName)
@ -114,7 +114,7 @@ describe('FileTree Create Folder Flow', function() {
}
fetchMock.post(matcher, response)
fireCreateFolder(newFolderName)
await fireCreateFolder(newFolderName)
const lastCallBody = JSON.parse(fetchMock.lastCall(matcher)[1].body)
expect(lastCallBody.name).to.equal(newFolderName)
@ -178,7 +178,7 @@ describe('FileTree Create Folder Flow', function() {
}
fetchMock.post(matcher, response)
fireCreateFolder(newFolderName)
await fireCreateFolder(newFolderName)
const lastCallBody = JSON.parse(fetchMock.lastCall(matcher)[1].body)
expect(lastCallBody.name).to.equal(newFolderName)
@ -239,7 +239,7 @@ describe('FileTree Create Folder Flow', function() {
})
})
function fireCreateFolder(name) {
async function fireCreateFolder(name) {
const createFolderButton = screen.getByRole('button', {
name: 'New Folder'
})
@ -247,14 +247,12 @@ describe('FileTree Create Folder Flow', function() {
setFolderName(name)
const modalCreateButton = getModalCreateButton()
const modalCreateButton = await getModalCreateButton()
fireEvent.click(modalCreateButton)
}
function setFolderName(name) {
const input = screen.getByRole('textbox', {
hidden: true // FIXME: modal should not be hidden but it has the aria-hidden label due to a react-bootstrap bug
})
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: name } })
}
@ -264,10 +262,7 @@ describe('FileTree Create Folder Flow', function() {
.replace(/0\./, 'random-test-id-')
}
function getModalCreateButton() {
return screen.getAllByRole('button', {
name: 'Create',
hidden: true // FIXME: modal should not be hidden but it has the aria-hidden label due to a react-bootstrap bug
})[0] // the first matched button is the toolbar button
async function getModalCreateButton() {
return waitFor(() => screen.getByRole('button', { name: 'Create' }))
}
})

View file

@ -55,7 +55,7 @@ describe('FileTree Delete Entity Flow', function() {
const fetchMatcher = /\/project\/\w+\/doc\/\w+/
fetchMock.delete(fetchMatcher, 204)
const modalDeleteButton = getModalDeleteButton()
const modalDeleteButton = await getModalDeleteButton()
fireEvent.click(modalDeleteButton)
window._ide.socket.socketClient.emit('removeEntity', '456def')
@ -82,7 +82,7 @@ describe('FileTree Delete Entity Flow', function() {
it('continues delete on 404s', async function() {
fetchMock.delete(/\/project\/\w+\/doc\/\w+/, 404)
const modalDeleteButton = getModalDeleteButton()
const modalDeleteButton = await getModalDeleteButton()
fireEvent.click(modalDeleteButton)
window._ide.socket.socketClient.emit('removeEntity', '456def')
@ -107,7 +107,7 @@ describe('FileTree Delete Entity Flow', function() {
const fetchMatcher = /\/project\/\w+\/doc\/\w+/
fetchMock.delete(fetchMatcher, 500)
const modalDeleteButton = getModalDeleteButton()
const modalDeleteButton = await getModalDeleteButton()
fireEvent.click(modalDeleteButton)
// The modal should still be open, but the file should not be deleted
@ -150,7 +150,7 @@ describe('FileTree Delete Entity Flow', function() {
const fetchMatcher = /\/project\/\w+\/(doc|file)\/\w+/
fetchMock.delete(fetchMatcher, 204)
const modalDeleteButton = getModalDeleteButton()
const modalDeleteButton = await getModalDeleteButton()
fireEvent.click(modalDeleteButton)
window._ide.socket.socketClient.emit('removeEntity', '456def')
@ -181,10 +181,7 @@ describe('FileTree Delete Entity Flow', function() {
})
})
function getModalDeleteButton() {
return screen.getAllByRole('button', {
name: 'Delete',
hidden: true // FIXME: modal should not be hidden but it has the aria-hidden label due to a react-bootstrap bug
})[1] // the first matched button is the toolbar button
async function getModalDeleteButton() {
return waitFor(() => screen.getByRole('button', { name: 'Delete' }))
}
})