Skip to content

Commit e19c99a

Browse files
committed
Refactor MenuBar to use proper ARIA rules
1 parent 394ce2b commit e19c99a

File tree

4 files changed

+144
-93
lines changed

4 files changed

+144
-93
lines changed

client/finder/DropDownMenu.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,17 @@ export default function DropDownMenu(props) {
2222

2323
return (
2424
<Wrapper
25-
aria-haspopup="true"
25+
ref={ref}
26+
role={props.role ? `combobox ${props.role}` : 'combobox'}
27+
aria-haspopup="listbox"
28+
aria-expanded="false"
2629
onClick={toggleSubmenu}
2730
className={props.className}
2831
data-tooltip-id="django-finder-tooltip"
2932
data-tooltip-content={props.tooltip}
3033
>
3134
{props.icon}
32-
<ul ref={ref} role="combobox" aria-expanded="false">
35+
<ul role="listbox">
3336
{props.children}
3437
</ul>
3538
</Wrapper>

client/finder/FileAdmin.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import React, {useContext, useEffect, useMemo, useRef, lazy, Suspense} from 'react';
1+
import React, {useContext, useMemo, useRef, lazy, Suspense, useEffect} from 'react';
2+
import {createRoot} from 'react-dom/client';
23
import FinderSettings from './FinderSettings';
34
import FolderTabs from './FolderTabs';
45
import FileDetails from './FileDetails';
6+
import SelectLabels from "./SelectLabels";
57

68

79
export function FileAdmin() {
@@ -25,14 +27,35 @@ export function FileAdmin() {
2527
const editorRef = useRef(null);
2628

2729
useEffect(() => {
28-
editorRef.current.insertAdjacentHTML('afterbegin', settings.mainContent.innerHTML);
30+
if (settings.labels) {
31+
const labelsElement = document.getElementById('id_labels');
32+
if (labelsElement instanceof HTMLSelectElement) {
33+
// extract selected values from the original <select multiple name="labels"> element
34+
const initial = [];
35+
for (const option of labelsElement.selectedOptions) {
36+
const found = settings.labels.find(label => label.value == option.value);
37+
if (found) {
38+
initial.push(found);
39+
}
40+
}
41+
console.log('initialValues', initial);
42+
43+
// replace the original <select multiple name="labels"> element with the "downshift" component
44+
const divElement = document.createElement('div');
45+
divElement.classList.add('select-labels-container');
46+
labelsElement.insertAdjacentElement('afterend', divElement);
47+
labelsElement.style.display = 'none';
48+
const root = createRoot(divElement);
49+
root.render(<SelectLabels labels={settings.labels} initial={initial} original={labelsElement} />);
50+
}
51+
}
2952
}, []);
3053

3154
return (<>
3255
<FolderTabs settings={settings} />
3356
<div className="detail-editor">
3457
<FileEditor editorRef={editorRef} settings={settings} />
35-
<div ref={editorRef}></div>
58+
<div dangerouslySetInnerHTML={{__html: settings.mainContent.innerHTML}} ref={editorRef}></div>
3659
</div>
3760
</>);
3861
}

client/finder/MenuBar.tsx

Lines changed: 96 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import React, {
99
useState,
1010
} from 'react';
1111
import {useCookie} from './Storage';
12-
import {SearchField} from './Search';
12+
import SearchField from './SearchField';
1313
import DropDownMenu from './DropDownMenu';
1414
import MoreVerticalIcon from 'icons/more-vertical.svg';
1515
import CopyIcon from 'icons/copy.svg';
@@ -39,42 +39,53 @@ export function SortingOptionsItem(props: any) {
3939
const {refreshColumns} = props;
4040
const [sorting, setSorting] = useSorting();
4141

42-
function isActive(value) {
43-
return sorting === value ? 'active' : null;
44-
}
45-
4642
function changeSorting(value) {
4743
if (value !== sorting) {
4844
setSorting(value);
4945
refreshColumns();
5046
}
5147
}
5248

49+
function getItemProps(value: string) {
50+
return {
51+
role: 'option',
52+
'aria-selected': sorting === value,
53+
onClick: () => changeSorting(value),
54+
};
55+
}
56+
5357
return (
54-
<DropDownMenu icon={<SortingIcon/>} className="with-caret" tooltip={gettext("Change sorting order")}>
55-
<li onClick={() => changeSorting('')} className={isActive('')}><span>{gettext("Unsorted")}</span></li>
56-
<li onClick={() => changeSorting('name_asc')} className={isActive('name_asc')}>
58+
<DropDownMenu
59+
icon={<SortingIcon/>}
60+
role="menuitem"
61+
className="sorting-options with-caret"
62+
tooltip={gettext("Change sorting order")}
63+
>
64+
<li {...getItemProps('')}>
65+
<span>{gettext("Unsorted")}</span>
66+
</li>
67+
<li {...getItemProps('name_asc')}>
5768
<SortDescIcon/><span>{gettext("Name")}</span>
5869
</li>
59-
<li onClick={() => changeSorting('name_desc')} className={isActive('name_desc')}>
70+
<li {...getItemProps('name_desc')}>
6071
<SortAscIcon /><span>{gettext("Name")}</span>
6172
</li>
62-
<li onClick={() => changeSorting('date_asc')} className={isActive('date_asc')}>
73+
<li {...getItemProps('date_asc')}>
6374
<SortDescIcon /><span>{gettext("Date")}</span>
6475
</li>
65-
<li onClick={() => changeSorting('date_desc')} className={isActive('date_desc')}>
76+
<li {...getItemProps('date_desc')}>
6677
<SortAscIcon /><span>{gettext("Date")}</span>
6778
</li>
68-
<li onClick={() => changeSorting('size_asc')} className={isActive('size_asc')}>
79+
<li {...getItemProps('size_asc')}>
6980
<SortDescIcon /><span>{gettext("Size")}</span>
7081
</li>
71-
<li onClick={() => changeSorting('size_desc')} className={isActive('size_desc')}>
82+
<li {...getItemProps('size_desc')}>
7283
<SortAscIcon /><span>{gettext("Size")}</span>
7384
</li>
74-
<li onClick={() => changeSorting('type_asc')} className={isActive('type_asc')}>
85+
<li {...getItemProps('type_asc')}>
7586
<SortDescIcon /><span>{gettext("Type")}</span>
7687
</li>
77-
<li onClick={() => changeSorting('type_desc')} className={isActive('type_desc')}>
88+
<li {...getItemProps('type_desc')}>
7889
<SortAscIcon /><span>{gettext("Type")}</span>
7990
</li>
8091
</DropDownMenu>
@@ -100,24 +111,26 @@ export function FilterByLabel(props: any) {
100111
return (
101112
<DropDownMenu
102113
icon={<FilterIcon/>}
103-
className={`filter-by-label with-caret${filter.length ? ' active' : ''}`}
114+
role="menuitem"
115+
aria-selected={filter.length}
116+
className="filter-by-label with-caret"
104117
tooltip={gettext("Filter by label")}
105118
>
106-
<li><span onClick={() => changeFilter(null)}>{gettext("Clear all")}</span></li>
107-
{labels.map((label, index) => (
108-
<li key={label.value}>
109-
<label htmlFor={`filter-${label.value}`}>
110-
<input
111-
type="checkbox"
112-
id={`filter-${label.value}`}
113-
name={label.value}
114-
checked={filter.includes(label.value)}
115-
onChange={() => changeFilter(label.value)}
116-
/>
117-
<span className="label-dot" style={{backgroundColor: label.color}}></span>
118-
{label.label}
119-
</label>
120-
</li>
119+
<li role="option"><span onClick={() => changeFilter(null)}>{gettext("Clear all")}</span></li>
120+
<hr/>{labels.map((label, index) => (
121+
<li key={label.value} role="option">
122+
<label htmlFor={`filter-${label.value}`}>
123+
<input
124+
type="checkbox"
125+
id={`filter-${label.value}`}
126+
name={label.value}
127+
checked={filter.includes(label.value)}
128+
onChange={() => changeFilter(label.value)}
129+
/>
130+
<span className="label-dot" style={{backgroundColor: label.color}}></span>
131+
{label.label}
132+
</label>
133+
</li>
121134
))}
122135
</DropDownMenu>
123136
);
@@ -188,20 +201,25 @@ function ExtraMenu(props) {
188201
}
189202

190203
return (
191-
<DropDownMenu className="extra-menu" icon={<MoreVerticalIcon/>} tooltip={gettext("Extra options")}>
192-
<li onClick={addFolder}>
204+
<DropDownMenu
205+
role="menuitem"
206+
className="extra-menu"
207+
icon={<MoreVerticalIcon/>}
208+
tooltip={gettext("Extra options")}
209+
>
210+
<li role="option" onClick={addFolder}>
193211
<AddFolderIcon/><span>{gettext("Add new folder")}</span>
194212
</li>
195-
<li className={numSelectedFiles ? null : "disabled"} onClick={downloadSelectedFiles}>
213+
<li role="option" aria-disabled={numSelectedFiles === 0} onClick={downloadSelectedFiles}>
196214
<DownloadIcon/><span>{gettext("Download selected files")}</span>
197215
</li>
198-
<li onClick={openUploader}>
216+
<li role="option" onClick={openUploader}>
199217
<UploadIcon/><span>{gettext("Upload local files")}</span>
200218
</li>
201-
<li className={numSelectedInodes ? null : "disabled"} onClick={copyInodes}>
219+
<li role="option" aria-disabled={numSelectedInodes === 0} onClick={copyInodes}>
202220
<CopyIcon/><span>{gettext("Copy selected to clipboard")}</span>
203221
</li>
204-
<li className={numClippedInodes ? null : "disabled"} onClick={clearClipboard}>
222+
<li role="option" aria-disabled={numClippedInodes === 0} onClick={clearClipboard}>
205223
<ClipboardIcon/><span>{gettext("Clear clipboard")}</span>
206224
</li>
207225
{settings.menu_extensions.length && <hr/>}
@@ -412,48 +430,54 @@ const MenuBar = forwardRef((props: any, forwardedRef) => {
412430
}
413431
}
414432

415-
function isActive(value) {
416-
return layout === value ? 'active' : null;
417-
}
418-
419433
return (
420-
<nav role="menubar">
421-
<menu>
422-
<li className="search-field">
434+
// <nav aria-label={gettext("Finder List View")}>
435+
<ul role="menubar">
436+
<li className="search-field" role="menuitem" style={{marginRight: 'auto'}}>
423437
<SearchField columnRefs={columnRefs} setSearchResult={setSearchResult} settings={settings}/>
424438
</li>
425-
<li style={{marginLeft: 'auto'}} className={isActive('tiles')} onClick={() => setLayout('tiles')}
426-
data-tooltip-id="django-finder-tooltip" data-tooltip-content={gettext("Tiles view")}><TilesIcon/>
439+
<li aria-selected={layout === 'tiles'} onClick={() => setLayout('tiles')}
440+
role="menuitem" data-tooltip-id="django-finder-tooltip" data-tooltip-content={gettext("Tiles view")}>
441+
<TilesIcon/>
427442
</li>
428-
<li className={isActive('mosaic')} onClick={() => setLayout('mosaic')}
429-
data-tooltip-id="django-finder-tooltip" data-tooltip-content={gettext("Mosaic view")}><MosaicIcon/>
443+
<li aria-selected={layout === 'mosaic'} onClick={() => setLayout('mosaic')}
444+
role="menuitem" data-tooltip-id="django-finder-tooltip" data-tooltip-content={gettext("Mosaic view")}><MosaicIcon/>
430445
</li>
431-
<li className={isActive('list')} onClick={() => setLayout('list')}
432-
data-tooltip-id="django-finder-tooltip" data-tooltip-content={gettext("List view")}><ListIcon/>
446+
<li aria-selected={layout === 'list'} onClick={() => setLayout('list')}
447+
role="menuitem" data-tooltip-id="django-finder-tooltip" data-tooltip-content={gettext("List view")}><ListIcon/>
448+
</li>
449+
<li aria-selected={layout === 'columns'} onClick={() => setLayout('columns')}
450+
role="menuitem" data-tooltip-id="django-finder-tooltip" data-tooltip-content={gettext("Columns view")}>
451+
<ColumnsIcon/>
433452
</li>
434-
<li className={isActive('columns')} onClick={() => setLayout('columns')}
435-
data-tooltip-id="django-finder-tooltip" data-tooltip-content={gettext("Columns view")}>
436-
<ColumnsIcon/></li>
437-
<li style={{marginLeft: 'auto'}}></li>
438-
{settings.labels && <FilterByLabel refreshFilesList={refreshColumns} labels={settings.labels}/>}
439453
<SortingOptionsItem refreshColumns={refreshColumns} />
440-
<li style={{marginRight: 'auto'}}></li>
441-
<li className={numSelectedInodes ? null : "disabled"} onClick={cutInodes}
442-
data-tooltip-id="django-finder-tooltip"
443-
data-tooltip-content={gettext("Cut selected to clipboard")}><CutIcon/></li>
454+
{settings.labels && <FilterByLabel refreshFilesList={refreshColumns} labels={settings.labels} />}
455+
<li aria-disabled={numSelectedInodes === 0} onClick={cutInodes}
456+
role="menuitem" data-tooltip-id="django-finder-tooltip"
457+
data-tooltip-content={gettext("Cut selected to clipboard")}>
458+
<CutIcon/>
459+
</li>
444460
{settings.is_trash ? (<>
445-
<li className={numSelectedInodes ? null : "disabled"} onClick={undoDiscardInodes}
446-
data-tooltip-id="django-finder-tooltip"
447-
data-tooltip-content={gettext("Undo discarding files/folders")}><UndoIcon/></li>
448-
<li className="erase" onClick={confirmEraseTrashFolder} data-tooltip-id="django-finder-tooltip"
449-
data-tooltip-content={gettext("Empty trash folder")}><EraseIcon/></li>
461+
<li aria-disabled={numSelectedInodes === 0} onClick={undoDiscardInodes}
462+
role="menuitem" data-tooltip-id="django-finder-tooltip"
463+
data-tooltip-content={gettext("Undo discarding files/folders")}>
464+
<UndoIcon/>
465+
</li>
466+
<li className="erase" onClick={confirmEraseTrashFolder} data-tooltip-id="django-finder-tooltip"
467+
role="menuitem" data-tooltip-content={gettext("Empty trash folder")}>
468+
<EraseIcon/>
469+
</li>
450470
</>) : (<>
451-
<li className={clipboard.length === 0 ? "disabled" : null} onClick={pasteInodes}
452-
data-tooltip-id="django-finder-tooltip" data-tooltip-content={gettext("Paste from clipboard")}>
453-
<PasteIcon/></li>
454-
<li className={numSelectedInodes ? null : "disabled"} onClick={deleteInodes}
455-
data-tooltip-id="django-finder-tooltip"
456-
data-tooltip-content={gettext("Move selected to trash folder")}><TrashIcon/></li>
471+
<li aria-disabled={clipboard.length === 0} onClick={pasteInodes}
472+
role="menuitem" data-tooltip-id="django-finder-tooltip"
473+
data-tooltip-content={gettext("Paste from clipboard")}>
474+
<PasteIcon/>
475+
</li>
476+
<li aria-disabled={numSelectedInodes === 0} onClick={deleteInodes}
477+
role="menuitem" data-tooltip-id="django-finder-tooltip"
478+
data-tooltip-content={gettext("Move selected to trash folder")}>
479+
<TrashIcon/>
480+
</li>
457481
<ExtraMenu
458482
numSelectedFiles={numSelectedFiles}
459483
numSelectedInodes={numSelectedInodes}
@@ -463,8 +487,8 @@ const MenuBar = forwardRef((props: any, forwardedRef) => {
463487
{...props}
464488
/>
465489
</>)}
466-
</menu>
467-
</nav>
490+
</ul>
491+
//</nav>
468492
);
469493
});
470494

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function useSearchParam(key) : [string, (value: string) => any] {
3131
}
3232

3333

34-
export function SearchField(props) {
34+
export default function SearchField(props) {
3535
const {columnRefs, setSearchResult, settings} = props;
3636
const searchRef = useRef(null);
3737
const [searchQuery, setSearchQuery] = useSearchParam('q');
@@ -73,8 +73,12 @@ export function SearchField(props) {
7373
}
7474
}
7575

76-
function isActive(value) {
77-
return searchRealm === value ? 'active' : null;
76+
function getItemProps(value: string) {
77+
return {
78+
role: 'option',
79+
'aria-selected': searchRealm === value,
80+
onClick: () => changeSearchRealm(value),
81+
};
7882
}
7983

8084
return (<>
@@ -88,20 +92,17 @@ export function SearchField(props) {
8892
/>
8993
<div>
9094
<span className="search-icon" onClick={handleSearch}><SearchIcon/></span>
91-
<DropDownMenu wrapperElement="span" className="search-realm with-caret" tooltip={gettext("Restrict search")}>
92-
<li onClick={() => changeSearchRealm('current')}
93-
className={isActive('current')}>{gettext("From current folder")}
94-
</li>
95-
<li onClick={() => changeSearchRealm('everywhere')}
96-
className={isActive('everywhere')}>{gettext("In all folders")}
97-
</li>
95+
<DropDownMenu
96+
wrapperElement="span"
97+
role="menuitem"
98+
className="search-realm with-caret"
99+
tooltip={gettext("Restrict search")}
100+
>
101+
<li {...getItemProps('current')}>{gettext("From current folder")}</li>
102+
<li {...getItemProps('everywhere')}>{gettext("In all folders")}</li>
98103
<hr/>
99-
<li onClick={() => changeSearchRealm('filename')}
100-
className={isActive('filename')}>{gettext("Filename only")}
101-
</li>
102-
<li onClick={() => changeSearchRealm('content')}
103-
className={isActive('content')}><s>{gettext("Also file content")}</s>
104-
</li>
104+
<li {...getItemProps('filename')}>{gettext("Filename only")}</li>
105+
<li {...getItemProps('content')}><s>{gettext("Also file content")}</s></li>
105106
</DropDownMenu>
106107
</div>
107108
</>

0 commit comments

Comments
 (0)