mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-02 12:47:41 +02:00
Desktop: Fix errors found by automated accessibility testing (#11246)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
This commit is contained in:
parent
b248700e28
commit
a616dc3cd2
@ -511,10 +511,12 @@ packages/app-desktop/integration-tests/util/createStartupArgs.js
|
||||
packages/app-desktop/integration-tests/util/extendedExpect.js
|
||||
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
|
||||
packages/app-desktop/integration-tests/util/getImageSourceSize.js
|
||||
packages/app-desktop/integration-tests/util/setDarkMode.js
|
||||
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
|
||||
packages/app-desktop/integration-tests/util/setMessageBoxResponse.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/integration-tests/util/waitForNextOpenPath.js
|
||||
packages/app-desktop/integration-tests/wcag.spec.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
packages/app-desktop/services/autoUpdater/AutoUpdaterService.test.js
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -487,10 +487,12 @@ packages/app-desktop/integration-tests/util/createStartupArgs.js
|
||||
packages/app-desktop/integration-tests/util/extendedExpect.js
|
||||
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
|
||||
packages/app-desktop/integration-tests/util/getImageSourceSize.js
|
||||
packages/app-desktop/integration-tests/util/setDarkMode.js
|
||||
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
|
||||
packages/app-desktop/integration-tests/util/setMessageBoxResponse.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/integration-tests/util/waitForNextOpenPath.js
|
||||
packages/app-desktop/integration-tests/wcag.spec.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
packages/app-desktop/services/autoUpdater/AutoUpdaterService.test.js
|
||||
|
@ -224,7 +224,8 @@ const Button = React.forwardRef((props: Props, ref: any) => {
|
||||
function renderIcon() {
|
||||
if (!props.iconName) return null;
|
||||
return <StyledIcon
|
||||
aria-label={props.iconLabel ?? ''}
|
||||
aria-label={props.iconLabel ?? undefined}
|
||||
aria-hidden={!props.iconLabel}
|
||||
animation={props.iconAnimation}
|
||||
mr={iconOnly ? '0' : '6px'}
|
||||
color={props.color}
|
||||
|
@ -438,7 +438,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="config-screen" style={{ display: 'flex', flexDirection: 'row', height: this.props.style.height }}>
|
||||
<div className="config-screen" role="main" style={{ display: 'flex', flexDirection: 'row', height: this.props.style.height }}>
|
||||
<Sidebar
|
||||
selection={this.state.selectedSectionName}
|
||||
onSelectionChange={this.sidebar_selectionChange}
|
||||
|
@ -56,7 +56,7 @@ export const StyledDivider = styled.div`
|
||||
border-bottom: 1px solid ${(props: StyleProps) => props.theme.dividerColor};
|
||||
background-color: ${(props: StyleProps) => props.theme.selectedColor2};
|
||||
font-size: ${(props: StyleProps) => Math.round(props.theme.fontSize)}px;
|
||||
opacity: 0.5;
|
||||
opacity: 0.58;
|
||||
`;
|
||||
|
||||
export const StyledListItemLabel = styled.span`
|
||||
@ -131,9 +131,9 @@ export default function Sidebar(props: Props) {
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<StyledListItemIcon
|
||||
aria-label=''
|
||||
className={Setting.sectionNameToIcon(section.name, AppType.Desktop)}
|
||||
role='img'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
<StyledListItemLabel>
|
||||
{Setting.sectionNameToLabel(section.name)}
|
||||
|
@ -6,7 +6,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const SettingDescription: React.FC<Props> = props => {
|
||||
return props.text ? <div className='setting-description' id={props.id}>{props.text}</div> : null;
|
||||
return <div className={`setting-description ${!props.text ? '-empty' : ''}`} id={props.id}>{props.text}</div>;
|
||||
};
|
||||
|
||||
export default SettingDescription;
|
||||
|
@ -6,4 +6,9 @@
|
||||
font-style: italic;
|
||||
max-width: 70em;
|
||||
margin-top: 5px;
|
||||
|
||||
&.-empty {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ export default function(props: Props) {
|
||||
} else if (folderIcon.type === FolderIconType.DataUrl) {
|
||||
return <img style={{ width, height, opacity }} src={folderIcon.dataUrl} />;
|
||||
} else if (folderIcon.type === FolderIconType.FontAwesome) {
|
||||
return <i style={{ fontSize: 18, width, opacity }} className={folderIcon.name} role='img'></i>;
|
||||
return <i style={{ fontSize: 18, width, opacity }} className={folderIcon.name} role='img' aria-hidden={true}></i>;
|
||||
} else {
|
||||
throw new Error(`Unsupported folder icon type: ${folderIcon.type}`);
|
||||
}
|
||||
|
@ -211,7 +211,6 @@ class ItemList<ItemType> extends React.Component<Props<ItemType>, State> {
|
||||
id={this.props.id}
|
||||
role={this.props.role}
|
||||
aria-label={this.props['aria-label']}
|
||||
aria-setsize={items.length}
|
||||
|
||||
onScroll={this.onScroll}
|
||||
onKeyDown={this.onKeyDown}
|
||||
|
@ -633,10 +633,12 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
},
|
||||
|
||||
editor: () => {
|
||||
return <NoteEditor
|
||||
windowId={defaultWindowId}
|
||||
key={key}
|
||||
/>;
|
||||
return <div className='note-editor-wrapper' role='main' aria-label={_('Note')}>
|
||||
<NoteEditor
|
||||
windowId={defaultWindowId}
|
||||
key={key}
|
||||
/>
|
||||
</div>;
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -391,6 +391,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
spellcheckEnabled: Setting.value('editor.spellcheckBeta'),
|
||||
keymap: keyboardMode,
|
||||
indentWithTabs: true,
|
||||
editorLabel: _('Markdown editor'),
|
||||
};
|
||||
}, [
|
||||
props.contentMarkupLanguage, props.disabled, props.keyboardMode, styles.globalTheme,
|
||||
|
@ -116,6 +116,7 @@ export default function NoteTitleBar(props: Props) {
|
||||
type="text"
|
||||
ref={props.titleInputRef}
|
||||
placeholder={props.isProvisional ? (props.noteIsTodo ? _('Creating new to-do...') : _('Creating new note...')) : ''}
|
||||
aria-label={props.isProvisional ? undefined : _('Note title')}
|
||||
style={styles.titleInput}
|
||||
readOnly={props.disabled}
|
||||
onChange={props.onTitleChange}
|
||||
|
@ -282,12 +282,13 @@ const NoteList = (props: Props) => {
|
||||
onItemContextMenu({ itemId: activeNoteId });
|
||||
}, [onItemContextMenu, activeNoteId]);
|
||||
|
||||
const hasNotes = !!props.notes.length;
|
||||
return (
|
||||
<div
|
||||
role='listbox'
|
||||
aria-label={_('Notes')}
|
||||
aria-activedescendant={getNoteElementIdFromJoplinId(activeNoteId)}
|
||||
aria-multiselectable={true}
|
||||
role={hasNotes ? 'listbox' : 'group'}
|
||||
aria-label={hasNotes ? _('Notes') : null}
|
||||
aria-activedescendant={activeNoteId ? getNoteElementIdFromJoplinId(activeNoteId) : undefined}
|
||||
aria-multiselectable={hasNotes ? true : undefined}
|
||||
tabIndex={0}
|
||||
|
||||
onFocus={onFocus}
|
||||
|
@ -178,7 +178,7 @@ export default function NoteListWrapper(props: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledRoot>
|
||||
<StyledRoot role='navigation' aria-label={_('Note list')}>
|
||||
<NoteListControls
|
||||
height={controlHeight}
|
||||
width={noteListSize.width}
|
||||
|
@ -6,6 +6,7 @@ import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import { ForwardedRef, forwardRef, RefObject, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import { WindowIdContext } from './NewWindowOrIFrame';
|
||||
import useDocument from './hooks/useDocument';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
interface Props {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
@ -224,6 +225,7 @@ const NoteTextViewer = forwardRef((props: Props, ref: ForwardedRef<NoteViewerCon
|
||||
style={viewerStyle}
|
||||
allow='clipboard-write=(self) fullscreen=(self) autoplay=(self) local-fonts=(self) encrypted-media=(self)'
|
||||
allowFullScreen={true}
|
||||
aria-label={_('Note editor')}
|
||||
src={`joplin-content://note-viewer/${__dirname}/note-viewer/index.html`}
|
||||
></iframe>
|
||||
);
|
||||
|
@ -74,7 +74,7 @@ const SidebarComponent = (props: Props) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledRoot className="sidebar">
|
||||
<StyledRoot className="sidebar" role='navigation' aria-label={_('Sidebar')}>
|
||||
<div style={{ flex: 1 }}><FolderAndTagList/></div>
|
||||
<div style={{ flex: 0, padding: theme.mainPadding }}>
|
||||
{syncReportComp}
|
||||
|
@ -33,7 +33,6 @@ const useOnRenderListWrapper = ({ selectedIndex, onKeyDown }: Props) => {
|
||||
<div
|
||||
role='tree'
|
||||
className='sidebar-list-items-wrapper'
|
||||
aria-setsize={listItems.length}
|
||||
tabIndex={allowContainerFocus ? 0 : undefined}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
|
@ -60,7 +60,7 @@ const AllNotesItem: React.FC<Props> = props => {
|
||||
itemCount={props.itemCount}
|
||||
>
|
||||
<EmptyExpandLink/>
|
||||
<StyledAllNotesIcon aria-label='' role='img' className='icon-notes'/>
|
||||
<StyledAllNotesIcon aria-hidden='true' role='img' className='icon-notes'/>
|
||||
<StyledListItemAnchor
|
||||
className="list-item"
|
||||
isSpecialItem={true}
|
||||
|
@ -59,7 +59,7 @@ const HeaderItem: React.FC<Props> = props => {
|
||||
onDrop={props.onDrop}
|
||||
>
|
||||
<StyledHeader onClick={onClick}>
|
||||
<StyledHeaderIcon aria-label='' role='img' className={item.iconName}/>
|
||||
<StyledHeaderIcon aria-hidden='true' role='img' className={item.iconName}/>
|
||||
<StyledHeaderLabel>{item.label}</StyledHeaderLabel>
|
||||
</StyledHeader>
|
||||
</ListItemWrapper>
|
||||
|
@ -36,7 +36,7 @@ export default function ToolbarButton(props: Props) {
|
||||
const iconName = getProp(props, 'iconName');
|
||||
if (iconName) {
|
||||
const iconProps: React.HTMLProps<HTMLDivElement> = {
|
||||
'aria-label': '',
|
||||
'aria-hidden': true,
|
||||
role: 'img',
|
||||
className: `toolbar-icon ${title ? '-has-title' : ''} ${iconName}`,
|
||||
};
|
||||
|
@ -9,4 +9,5 @@
|
||||
@use './editor-toolbar.scss';
|
||||
@use './user-webview-dialog-container.scss';
|
||||
@use './dialog-anchor-node.scss';
|
||||
@use './note-editor-wrapper.scss';
|
||||
@use './text-input.scss';
|
||||
|
6
packages/app-desktop/gui/styles/note-editor-wrapper.scss
Normal file
6
packages/app-desktop/gui/styles/note-editor-wrapper.scss
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
.note-editor-wrapper {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
@ -4,9 +4,11 @@ import { ElectronApplication, Locator, Page } from '@playwright/test';
|
||||
|
||||
export default class Sidebar {
|
||||
public readonly container: Locator;
|
||||
public readonly allNotes: Locator;
|
||||
|
||||
public constructor(page: Page, private mainScreen: MainScreen) {
|
||||
this.container = page.locator('.rli-sideBar');
|
||||
this.allNotes = this.container.getByText('All notes');
|
||||
}
|
||||
|
||||
public async createNewFolder(title: string) {
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { ElectronApplication } from '@playwright/test';
|
||||
|
||||
const setDarkMode = (app: ElectronApplication, darkMode: boolean) => {
|
||||
return app.evaluate(({ nativeTheme }, darkMode) => {
|
||||
nativeTheme.themeSource = darkMode ? 'dark' : 'light';
|
||||
}, darkMode);
|
||||
};
|
||||
|
||||
export default setDarkMode;
|
@ -4,6 +4,7 @@ import { _electron as electron, Page, ElectronApplication, test as base } from '
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
import createStartupArgs from './createStartupArgs';
|
||||
import firstNonDevToolsWindow from './firstNonDevToolsWindow';
|
||||
import setDarkMode from './setDarkMode';
|
||||
|
||||
|
||||
type StartWithPluginsResult = { app: ElectronApplication; mainWindow: Page };
|
||||
@ -61,6 +62,7 @@ export const test = base.extend<JoplinFixtures>({
|
||||
electronApp: async ({ profileDirectory }, use) => {
|
||||
const startupArgs = createStartupArgs(profileDirectory);
|
||||
const electronApp = await electron.launch({ args: startupArgs });
|
||||
await setDarkMode(electronApp, false);
|
||||
|
||||
await use(electronApp);
|
||||
|
||||
|
68
packages/app-desktop/integration-tests/wcag.spec.ts
Normal file
68
packages/app-desktop/integration-tests/wcag.spec.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { test, expect } from './util/test';
|
||||
import MainScreen from './models/MainScreen';
|
||||
import SettingsScreen from './models/SettingsScreen';
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
const createScanner = (page: Page) => {
|
||||
return new AxeBuilder({ page })
|
||||
.disableRules(['page-has-heading-one'])
|
||||
.setLegacyMode(true);
|
||||
};
|
||||
|
||||
// Fade-in transitions can cause color contrast issues if still running
|
||||
// during a scan.
|
||||
// See https://github.com/dequelabs/axe-core-npm/issues/952
|
||||
const waitForAnimationsToEnd = (page: Page) => {
|
||||
return page.locator('body').evaluate(element => {
|
||||
const animationPromises = element
|
||||
.getAnimations({ subtree: true })
|
||||
.map(animation => animation.finished);
|
||||
return Promise.all(animationPromises);
|
||||
});
|
||||
};
|
||||
|
||||
const expectNoViolations = async (page: Page) => {
|
||||
await waitForAnimationsToEnd(page);
|
||||
const results = await createScanner(page).analyze();
|
||||
expect(results.violations).toEqual([]);
|
||||
};
|
||||
|
||||
|
||||
test.describe('wcag', () => {
|
||||
for (const tabName of ['General', 'Plugins']) {
|
||||
test(`should not detect significant issues in the settings screen ${tabName} tab`, async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.waitFor();
|
||||
|
||||
await mainScreen.openSettings(electronApp);
|
||||
|
||||
// Should be on the settings screen
|
||||
const settingsScreen = new SettingsScreen(mainWindow);
|
||||
await settingsScreen.waitFor();
|
||||
|
||||
const tabLocator = settingsScreen.getTabLocator(tabName);
|
||||
await tabLocator.click();
|
||||
await expect(tabLocator).toBeFocused();
|
||||
|
||||
await expectNoViolations(mainWindow);
|
||||
});
|
||||
}
|
||||
|
||||
test('should not detect significant issues in the main screen with an open note', async ({ mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.waitFor();
|
||||
|
||||
await mainScreen.createNewNote('Test');
|
||||
|
||||
// For now, activate all notes to make it active. When inactive, it causes a contrast warning.
|
||||
// This seems to be allowed under WCAG 2.2 SC 1.4.3 under the "Incidental" exception.
|
||||
await mainScreen.sidebar.allNotes.click();
|
||||
|
||||
// Ensure that `:hover` styling is consistent between tests:
|
||||
await mainScreen.noteEditor.noteTitleInput.hover();
|
||||
|
||||
await expectNoViolations(mainWindow);
|
||||
});
|
||||
});
|
||||
|
@ -124,6 +124,7 @@
|
||||
"homepage": "https://github.com/laurent22/joplin#readme",
|
||||
"devDependencies": {
|
||||
"7zip-bin": "5.2.0",
|
||||
"@axe-core/playwright": "4.10.0",
|
||||
"@electron/rebuild": "3.6.0",
|
||||
"@joplin/default-plugins": "~3.2",
|
||||
"@joplin/tools": "~3.2",
|
||||
|
@ -348,6 +348,8 @@ function NoteEditor(props: Props, ref: any) {
|
||||
autocompleteMarkup: Setting.value('editor.autocompleteMarkup'),
|
||||
|
||||
indentWithTabs: true,
|
||||
|
||||
editorLabel: _('Markdown editor'),
|
||||
}), [props.themeId, props.readOnly]);
|
||||
|
||||
const injectedJavaScript = `
|
||||
|
@ -56,6 +56,7 @@ const configFromSettings = (settings: EditorSettings) => {
|
||||
autocapitalize: 'sentence',
|
||||
autocorrect: settings.spellcheckEnabled ? 'true' : 'false',
|
||||
spellcheck: settings.spellcheckEnabled ? 'true' : 'false',
|
||||
'aria-label': settings.editorLabel,
|
||||
}),
|
||||
EditorState.readOnly.of(settings.readOnly),
|
||||
indentUnit.of(settings.indentWithTabs ? '\t' : ' '),
|
||||
|
@ -17,6 +17,7 @@ const createEditorSettings = (themeId: number) => {
|
||||
themeData,
|
||||
|
||||
indentWithTabs: true,
|
||||
editorLabel: 'Markdown editor',
|
||||
};
|
||||
|
||||
return editorSettings;
|
||||
|
@ -168,6 +168,8 @@ export interface EditorSettings {
|
||||
readOnly: boolean;
|
||||
|
||||
indentWithTabs: boolean;
|
||||
|
||||
editorLabel: string;
|
||||
}
|
||||
|
||||
export type LogMessageCallback = (message: string)=> void;
|
||||
|
@ -14,7 +14,7 @@ const theme: Theme = {
|
||||
colorCorrect: 'green', // Opposite of colorError
|
||||
colorWarn: 'rgb(228,86,0)',
|
||||
colorWarnUrl: '#155BDA',
|
||||
colorFaded: '#7C8B9E', // For less important text
|
||||
colorFaded: '#627184', // For less important text
|
||||
dividerColor: '#dddddd',
|
||||
selectedColor: '#e5e5e5',
|
||||
urlColor: '#155BDA',
|
||||
@ -32,7 +32,7 @@ const theme: Theme = {
|
||||
// It's dark text over gray background.
|
||||
backgroundColor3: '#F4F5F6',
|
||||
backgroundColorHover3: '#CBDAF1',
|
||||
color3: '#738598',
|
||||
color3: '#627284',
|
||||
|
||||
// Color scheme "4" is used for secondary-style buttons. It makes a white
|
||||
// button with blue text.
|
||||
|
@ -137,6 +137,7 @@ runtimes
|
||||
onnx
|
||||
onnxruntime
|
||||
treeitem
|
||||
WCAG
|
||||
qrcode
|
||||
Rocketbook
|
||||
datamatrix
|
||||
|
19
yarn.lock
19
yarn.lock
@ -1434,6 +1434,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@axe-core/playwright@npm:4.10.0":
|
||||
version: 4.10.0
|
||||
resolution: "@axe-core/playwright@npm:4.10.0"
|
||||
dependencies:
|
||||
axe-core: ~4.10.0
|
||||
peerDependencies:
|
||||
playwright-core: ">= 1.0.0"
|
||||
checksum: cba9b7f625c8ed510e661d8d014f7e924fb85175b918b818cb808eec848c3e8a2cbbe2960ccf8dd4b9f84a4a4ea33eeeace48de2560a4a5777dd0ee6281d0c81
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/code-frame@npm:7.10.4, @babel/code-frame@npm:~7.10.4":
|
||||
version: 7.10.4
|
||||
resolution: "@babel/code-frame@npm:7.10.4"
|
||||
@ -8168,6 +8179,7 @@ __metadata:
|
||||
resolution: "@joplin/app-desktop@workspace:packages/app-desktop"
|
||||
dependencies:
|
||||
7zip-bin: 5.2.0
|
||||
"@axe-core/playwright": 4.10.0
|
||||
"@electron/notarize": 2.3.2
|
||||
"@electron/rebuild": 3.6.0
|
||||
"@electron/remote": 2.1.2
|
||||
@ -16062,6 +16074,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"axe-core@npm:~4.10.0":
|
||||
version: 4.10.1
|
||||
resolution: "axe-core@npm:4.10.1"
|
||||
checksum: 1e71bc4b7cdad6e99dad9e4098a174932ed69052e7400e0fb57b585fff1764cc541580db375c643755250da7d68f811ba05fe0636c31d4238aa16e3f31587869
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"axios@npm:^0.25.0":
|
||||
version: 0.25.0
|
||||
resolution: "axios@npm:0.25.0"
|
||||
|
Loading…
Reference in New Issue
Block a user