Skip to content

Commit

Permalink
Use useId to generate unique ids for accessibility attributes.
Browse files Browse the repository at this point in the history
  • Loading branch information
cbeer committed Dec 19, 2024
1 parent 05194d8 commit 5e49588
Show file tree
Hide file tree
Showing 13 changed files with 60 additions and 44 deletions.
2 changes: 1 addition & 1 deletion __tests__/src/components/WindowTopMenuButton.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ describe('WindowTopMenuButton', () => {
render(<Subject />);
await user.click(screen.getByLabelText('Window views & thumbnail display'));
// when 'open' is true, aria-owns is set to the id of the window
expect(screen.getByLabelText('Window views & thumbnail display')).toHaveAttribute('aria-owns', 'window-menu_xyz'); // eslint-disable-line testing-library/no-node-access
expect(screen.getByLabelText('Window views & thumbnail display')).toHaveAttribute('aria-owns'); // eslint-disable-line testing-library/no-node-access
});
});
14 changes: 7 additions & 7 deletions __tests__/src/components/WorkspaceOptionsMenu.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,18 @@ describe('WorkspaceOptionsMenu', () => {

it('renders the export dialog when export option is clicked', async () => {
render(<Subject anchorEl={screen.getByTestId('menu-trigger-button')} open />);
expect(document.querySelector('#workspace-export')).not.toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access
expect(screen.queryByRole('heading', { name: 'Export workspace' })).not.toBeInTheDocument();

await user.click(screen.getAllByRole('menuitem')[0]);
expect(document.querySelector('#workspace-export')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access
await user.click(screen.getByRole('menuitem', { name: 'Export workspace' }));
expect(screen.getByRole('heading', { name: 'Export workspace' })).toBeInTheDocument();
});

it('renders the import dialog when imporrt option is clicked', async () => {
it('renders the import dialog when import option is clicked', async () => {
render(<Subject anchorEl={screen.getByTestId('menu-trigger-button')} open />);
expect(document.querySelector('#workspace-import')).not.toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access
expect(screen.queryByRole('heading', { name: 'Import workspace' })).not.toBeInTheDocument();

await user.click(screen.getAllByRole('menuitem')[1]);
expect(document.querySelector('#workspace-import')).toBeInTheDocument(); // eslint-disable-line testing-library/no-node-access
await user.click(screen.getByRole('menuitem', { name: 'Import workspace' }));
expect(screen.getByRole('heading', { name: 'Import workspace' })).toBeInTheDocument();
});

it('fires the correct callbacks on menu close', async () => {
Expand Down
8 changes: 5 additions & 3 deletions src/components/ManifestRelatedLinks.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Typography from '@mui/material/Typography';
import Link from '@mui/material/Link';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { useId } from 'react';
import CollapsibleSection from '../containers/CollapsibleSection';
import ns from '../config/css-ns';
import { PluginHook } from './PluginHook';
Expand All @@ -28,18 +29,19 @@ export function ManifestRelatedLinks({
...rest
}) {
const { t } = useTranslation();
const titleId = useId();

const pluginProps = {
homepage, id, manifestUrl, related, renderings, seeAlso, t, ...rest,
};

return (
<CollapsibleSection
id={`${id}-related`}
aria-labelledby={titleId}
label={t('related')}
>
<Typography
aria-labelledby={`${id}-related ${id}-related-heading`}
id={`${id}-related-heading`}
id={titleId}
variant="h4"
component="h5"
>
Expand Down
5 changes: 3 additions & 2 deletions src/components/SearchHit.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react';
import { useEffect, useId, useMemo } from 'react';
import { useEffectEvent } from 'use-effect-event';
import PropTypes from 'prop-types';
import Button from '@mui/material/Button';
Expand Down Expand Up @@ -86,11 +86,12 @@ export function SearchHit({
);
});

const canvasLabelHtmlId = useId();

if (focused && !selected) return null;

const renderedHit = focused ? hit : hit && truncatedHit;
const truncated = hit && (renderedHit.before !== hit.before || renderedHit.after !== hit.after);
const canvasLabelHtmlId = `${companionWindowId}-${index}`;
const ownerState = {
adjacent, focused, selected, windowSelected,
};
Expand Down
7 changes: 4 additions & 3 deletions src/components/WindowListButton.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useId, useState } from 'react';
import PropTypes from 'prop-types';
import BookmarksIcon from '@mui/icons-material/BookmarksSharp';
import { useTranslation } from 'react-i18next';
Expand All @@ -11,6 +11,7 @@ import MiradorMenuButton from '../containers/MiradorMenuButton';
export function WindowListButton({ disabled = false, windowCount }) {
const { t } = useTranslation();
const [windowListAnchor, setWindowListAnchor] = useState(null);
const id = useId();

/** */
const handleClose = () => { setWindowListAnchor(null); };
Expand All @@ -22,7 +23,7 @@ export function WindowListButton({ disabled = false, windowCount }) {
<MiradorMenuButton
aria-haspopup="true"
aria-label={t('listAllOpenWindows')}
aria-owns={windowListAnchor ? 'window-list' : null}
aria-owns={windowListAnchor ? id : null}
selected={Boolean(windowListAnchor)}
disabled={disabled}
badge
Expand All @@ -42,7 +43,7 @@ export function WindowListButton({ disabled = false, windowCount }) {
{Boolean(windowListAnchor) && (
<WindowList
anchorEl={windowListAnchor}
id="window-list"
id={id}
open={Boolean(windowListAnchor)}
handleClose={handleClose}
/>
Expand Down
11 changes: 6 additions & 5 deletions src/components/WindowSideBarCanvasPanel.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef } from 'react';
import { useId, useRef } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
import Tabs from '@mui/material/Tabs';
Expand Down Expand Up @@ -49,6 +49,7 @@ export function WindowSideBarCanvasPanel({
}) {
const { t } = useTranslation();
const containerRef = useRef();
const tabPanelId = useId();

/** */
const handleSequenceChange = (event) => {
Expand Down Expand Up @@ -126,15 +127,15 @@ export function WindowSideBarCanvasPanel({
textColor="primary"
>
{showToc && (
<Tooltip title={t('tableOfContentsList')} value="tableOfContents"><Tab sx={{ minWidth: 'auto' }} value="tableOfContents" aria-label={t('tableOfContentsList')} aria-controls={`tab-panel-${id}`} icon={<TocIcon style={{ transform: 'scale(-1, 1)' }} />} /></Tooltip>
<Tooltip title={t('tableOfContentsList')} value="tableOfContents"><Tab sx={{ minWidth: 'auto' }} value="tableOfContents" aria-label={t('tableOfContentsList')} aria-controls={tabPanelId} icon={<TocIcon style={{ transform: 'scale(-1, 1)' }} />} /></Tooltip>
)}
<Tooltip title={t('itemList')} value="item"><Tab sx={{ minWidth: 'auto' }} value="item" aria-label={t('itemList')} aria-controls={`tab-panel-${id}`} icon={<ItemListIcon />} /></Tooltip>
<Tooltip title={t('thumbnailList')} value="thumbnail"><Tab sx={{ minWidth: 'auto' }} value="thumbnail" aria-label={t('thumbnailList')} aria-controls={`tab-panel-${id}`} icon={<ThumbnailListIcon />} /></Tooltip>
<Tooltip title={t('itemList')} value="item"><Tab sx={{ minWidth: 'auto' }} value="item" aria-label={t('itemList')} aria-controls={tabPanelId} icon={<ItemListIcon />} /></Tooltip>
<Tooltip title={t('thumbnailList')} value="thumbnail"><Tab sx={{ minWidth: 'auto' }} value="thumbnail" aria-label={t('thumbnailList')} aria-controls={tabPanelId} icon={<ThumbnailListIcon />} /></Tooltip>
</Tabs>
</>
)}
>
<div id={`tab-panel-${id}`}>
<div id={tabPanelId}>
{ collection && (
<Button
fullWidth
Expand Down
4 changes: 2 additions & 2 deletions src/components/WindowTopBarPluginMenu.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useContext, useState } from 'react';
import { useContext, useId, useState } from 'react';
import PropTypes from 'prop-types';
import MoreVertIcon from '@mui/icons-material/MoreVertSharp';
import Menu from '@mui/material/Menu';
Expand All @@ -18,6 +18,7 @@ export function WindowTopBarPluginMenu({
const pluginProps = arguments[0]; // eslint-disable-line prefer-rest-params
const [anchorEl, setAnchorEl] = useState(null);
const [open, setOpen] = useState(false);
const windowPluginMenuId = useId();

/** */
const handleMenuClick = (event) => {
Expand All @@ -31,7 +32,6 @@ export function WindowTopBarPluginMenu({
setOpen(false);
};

const windowPluginMenuId = `window-plugin-menu_${windowId}`;
if (!PluginComponents || PluginComponents.length === 0) return null;

return (
Expand Down
4 changes: 2 additions & 2 deletions src/components/WindowTopMenuButton.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useId, useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import WindowTopMenu from '../containers/WindowTopMenu';
Expand All @@ -11,6 +11,7 @@ export function WindowTopMenuButton({ classes = {}, windowId }) {
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState(null);
const [open, setOpen] = useState(false);
const menuId = useId();

/** */
const handleMenuClick = (event) => {
Expand All @@ -24,7 +25,6 @@ export function WindowTopMenuButton({ classes = {}, windowId }) {
setOpen(false);
};

const menuId = `window-menu_${windowId}`;
return (
<>
<MiradorMenuButton
Expand Down
11 changes: 7 additions & 4 deletions src/components/WorkspaceExport.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useId, useState } from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import DialogTitle from '@mui/material/DialogTitle';
Expand All @@ -19,10 +19,11 @@ import { WorkspaceDialog } from './WorkspaceDialog';
/**
*/
export function WorkspaceExport({
children = null, container = null, open = false, handleClose, exportableState,
children = null, container = null, id = undefined, open = false, handleClose, exportableState,
}) {
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
const titleId = useId();
const exportedState = JSON.stringify(exportableState, null, 2);

if (copied) {
Expand All @@ -47,15 +48,16 @@ export function WorkspaceExport({

return (
<WorkspaceDialog
id="workspace-export"
aria-labelledby={titleId}
id={id}
container={container}
open={open}
onClose={handleClose}
scroll="paper"
fullWidth
maxWidth="sm"
>
<DialogTitle id="form-dialog-title">
<DialogTitle id={titleId}>
{t('downloadExport')}
</DialogTitle>

Expand Down Expand Up @@ -93,5 +95,6 @@ WorkspaceExport.propTypes = {
container: PropTypes.object, // eslint-disable-line react/forbid-prop-types
exportableState: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
handleClose: PropTypes.func.isRequired,
id: PropTypes.string,
open: PropTypes.bool,
};
12 changes: 7 additions & 5 deletions src/components/WorkspaceImport.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useId, useState } from 'react';
import DialogTitle from '@mui/material/DialogTitle';
import PropTypes from 'prop-types';
import {
Expand All @@ -13,10 +13,11 @@ import ScrollIndicatedDialogContent from '../containers/ScrollIndicatedDialogCon
/**
*/
export function WorkspaceImport({
addError, importConfig, classes = {}, handleClose, open = false,
addError, id = undefined, importConfig, classes = {}, handleClose, open = false,
}) {
const { t } = useTranslation();
const [configImportValue, setConfigImportValue] = useState('');
const titleId = useId();

/** */
const handleChange = (event) => {
Expand All @@ -37,14 +38,14 @@ export function WorkspaceImport({

return (
<WorkspaceDialog
aria-labelledby="workspace-import-title"
id="workspace-import"
aria-labelledby={titleId}
id={id}
onClose={handleClose}
open={open}
fullWidth
maxWidth="sm"
>
<DialogTitle id="workspace-import-title">
<DialogTitle id={titleId}>
{t('importWorkspace')}
</DialogTitle>
<ScrollIndicatedDialogContent>
Expand Down Expand Up @@ -79,6 +80,7 @@ WorkspaceImport.propTypes = {
addError: PropTypes.func.isRequired,
classes: PropTypes.objectOf(PropTypes.string),
handleClose: PropTypes.func.isRequired,
id: PropTypes.string,
importConfig: PropTypes.func.isRequired,
open: PropTypes.bool,
};
7 changes: 4 additions & 3 deletions src/components/WorkspaceMenuButton.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useId, useState } from 'react';
import SettingsIcon from '@mui/icons-material/SettingsSharp';
import { useTranslation } from 'react-i18next';
import WorkspaceMenu from '../containers/WorkspaceMenu';
Expand All @@ -10,6 +10,7 @@ export function WorkspaceMenuButton() {
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState(null);
const [open, setOpen] = useState(false);
const id = useId();

/** */
const handleMenuClick = (event) => {
Expand All @@ -28,7 +29,7 @@ export function WorkspaceMenuButton() {
<MiradorMenuButton
aria-haspopup="true"
aria-label={t('workspaceMenu')}
aria-owns={open ? 'workspace-menu' : undefined}
aria-owns={open ? id : undefined}
selected={open}
id="menuBtn"
onClick={handleMenuClick}
Expand All @@ -37,7 +38,7 @@ export function WorkspaceMenuButton() {
</MiradorMenuButton>
<WorkspaceMenu
anchorEl={anchorEl}
id="workspace-menu"
id={id}
handleClose={handleMenuClose}
open={open}
/>
Expand Down
11 changes: 7 additions & 4 deletions src/components/WorkspaceOptionsMenu.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useContext, useState } from 'react';
import { useContext, useId, useState } from 'react';
import PropTypes from 'prop-types';
import ImportIcon from '@mui/icons-material/Input';
import SaveAltIcon from '@mui/icons-material/SaveAltSharp';
Expand All @@ -21,6 +21,8 @@ export function WorkspaceOptionsMenu({
const { t } = useTranslation();
const container = useContext(WorkspaceContext);
const [selectedOption, setSelectedOption] = useState(null);
const exportId = useId();
const importId = useId();

const pluginProps = {
anchorEl, container, handleClose, open, t, ...rest,
Expand Down Expand Up @@ -57,7 +59,7 @@ export function WorkspaceOptionsMenu({
<MenuItem
aria-haspopup="true"
onClick={() => { handleClick('exportWorkspace'); }}
aria-owns={selectedOption === 'exportWorkspace' ? 'workspace-export' : undefined}
aria-owns={selectedOption === 'exportWorkspace' ? exportId : undefined}
>
<ListItemIcon>
<SaveAltIcon />
Expand All @@ -66,9 +68,8 @@ export function WorkspaceOptionsMenu({
</MenuItem>
<MenuItem
aria-haspopup="true"
id="workspace-menu-import"
onClick={() => { handleClick('importWorkspace'); }}
aria-owns={selectedOption === 'importWorkspace' ? 'workspace-import' : undefined}
aria-owns={selectedOption === 'importWorkspace' ? importId : undefined}
>
<ListItemIcon>
<ImportIcon />
Expand All @@ -79,13 +80,15 @@ export function WorkspaceOptionsMenu({
</Menu>
{selectedOption === 'exportWorkspace' && (
<WorkspaceExport
id={exportId}
open={selectedOption === 'exportWorkspace'}
container={container?.current}
handleClose={handleDialogClose}
/>
)}
{selectedOption === 'importWorkspace' && (
<WorkspaceImport
id={importId}
open={selectedOption === 'importWorkspace'}
container={container?.current}
handleClose={handleDialogClose}
Expand Down
Loading

0 comments on commit 5e49588

Please sign in to comment.