1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

Desktop: Accessibility: Add missing labels to the note attachments screen and master password dialog (#11231)

This commit is contained in:
Henry Heino 2024-10-26 13:06:09 -07:00 committed by GitHub
parent 2d9c2d533d
commit f07e4e9b5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 170 additions and 73 deletions

View File

@ -386,7 +386,9 @@ packages/app-desktop/gui/NoteTextViewer.js
packages/app-desktop/gui/NoteToolbar/NoteToolbar.js packages/app-desktop/gui/NoteToolbar/NoteToolbar.js
packages/app-desktop/gui/NotyfContext.js packages/app-desktop/gui/NotyfContext.js
packages/app-desktop/gui/OneDriveLoginScreen.js packages/app-desktop/gui/OneDriveLoginScreen.js
packages/app-desktop/gui/PasswordInput/LabelledPasswordInput.js
packages/app-desktop/gui/PasswordInput/PasswordInput.js packages/app-desktop/gui/PasswordInput/PasswordInput.js
packages/app-desktop/gui/PasswordInput/types.js
packages/app-desktop/gui/PdfViewer.js packages/app-desktop/gui/PdfViewer.js
packages/app-desktop/gui/PromptDialog.js packages/app-desktop/gui/PromptDialog.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js packages/app-desktop/gui/ResizableLayout/MoveButtons.js

2
.gitignore vendored
View File

@ -363,7 +363,9 @@ packages/app-desktop/gui/NoteTextViewer.js
packages/app-desktop/gui/NoteToolbar/NoteToolbar.js packages/app-desktop/gui/NoteToolbar/NoteToolbar.js
packages/app-desktop/gui/NotyfContext.js packages/app-desktop/gui/NotyfContext.js
packages/app-desktop/gui/OneDriveLoginScreen.js packages/app-desktop/gui/OneDriveLoginScreen.js
packages/app-desktop/gui/PasswordInput/LabelledPasswordInput.js
packages/app-desktop/gui/PasswordInput/PasswordInput.js packages/app-desktop/gui/PasswordInput/PasswordInput.js
packages/app-desktop/gui/PasswordInput/types.js
packages/app-desktop/gui/PdfViewer.js packages/app-desktop/gui/PdfViewer.js
packages/app-desktop/gui/PromptDialog.js packages/app-desktop/gui/PromptDialog.js
packages/app-desktop/gui/ResizableLayout/MoveButtons.js packages/app-desktop/gui/ResizableLayout/MoveButtons.js

View File

@ -10,7 +10,7 @@ import { reg } from '@joplin/lib/registry';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService'; import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import KvStore from '@joplin/lib/services/KvStore'; import KvStore from '@joplin/lib/services/KvStore';
import ShareService from '@joplin/lib/services/share/ShareService'; import ShareService from '@joplin/lib/services/share/ShareService';
import { PasswordInput } from '../PasswordInput/PasswordInput'; import LabelledPasswordInput from '../PasswordInput/LabelledPasswordInput';
interface Props { interface Props {
themeId: number; themeId: number;
@ -136,11 +136,6 @@ export default function(props: Props) {
setCurrentPasswordIsValid(isValid); setCurrentPasswordIsValid(isValid);
}, [currentPassword]); }, [currentPassword]);
function renderCurrentPasswordIcon() {
if (!currentPassword || status === MasterPasswordStatus.NotSet) return null;
return currentPasswordIsValid ? <i className="fas fa-check password-valid-icon"></i> : <i className="fas fa-times"></i>;
}
function renderPasswordForm() { function renderPasswordForm() {
const renderCurrentPassword = () => { const renderCurrentPassword = () => {
if (!showCurrentPassword) return null; if (!showCurrentPassword) return null;
@ -151,14 +146,14 @@ export default function(props: Props) {
// having to reset the password (and lose access to any data that's // having to reset the password (and lose access to any data that's
// been encrypted with it). // been encrypted with it).
const showValidIcon = currentPassword && status !== MasterPasswordStatus.NotSet;
return ( return (
<div className="form-input-group"> <LabelledPasswordInput
<label>{'Current password'}</label> labelText={_('Current password')}
<div className="current-password-wrapper"> value={currentPassword}
<PasswordInput value={currentPassword} onChange={onCurrentPasswordChange}/> onChange={onCurrentPasswordChange}
{renderCurrentPasswordIcon()} valid={showValidIcon ? currentPasswordIsValid : undefined}
</div> />
</div>
); );
}; };
@ -175,15 +170,17 @@ export default function(props: Props) {
<div> <div>
<div className="form"> <div className="form">
{renderCurrentPassword()} {renderCurrentPassword()}
<div className="form-input-group"> <LabelledPasswordInput
<label>{enterPasswordLabel}</label> labelText={enterPasswordLabel}
<PasswordInput value={password1} onChange={onPasswordChange1}/> value={password1}
</div> onChange={onPasswordChange1}
/>
{needToRepeatPassword && ( {needToRepeatPassword && (
<div className="form-input-group"> <LabelledPasswordInput
<label>{'Re-enter password'}</label> labelText={_('Re-enter password')}
<PasswordInput value={password2} onChange={onPasswordChange2}/> value={password2}
</div> onChange={onPasswordChange2}
/>
)} )}
</div> </div>
<p className="bold">Please make sure you remember your password. For security reasons, it is not possible to recover it if it is lost.</p> <p className="bold">Please make sure you remember your password. For security reasons, it is not possible to recover it if it is lost.</p>

View File

@ -0,0 +1,56 @@
import * as React from 'react';
import PasswordInput from './PasswordInput';
import { useId } from 'react';
import { ChangeEventHandler } from './types';
import { _ } from '@joplin/lib/locale';
interface Props {
labelText: string;
value: string;
onChange: ChangeEventHandler;
valid?: boolean;
}
const LabelledPasswordInput: React.FC<Props> = props => {
const inputId = useId();
const statusIconId = useId();
const canRenderStatusIcon = (props.valid ?? null) !== null && props.value;
const renderStatusIcon = () => {
if (!canRenderStatusIcon) return null;
let title, classNames;
if (props.valid) {
title = _('Valid');
classNames = 'fas fa-check -valid';
} else {
title = _('Invalid');
classNames = 'fas fa-times -invalid';
}
return <i
className={`password-status-icon ${classNames}`}
id={statusIconId}
role='img'
aria-label={title}
title={title}
aria-live='polite'
></i>;
};
return <div className='labelled-password-input form-input-group'>
<label htmlFor={inputId}>{props.labelText}</label>
<div className='password'>
<PasswordInput
inputId={inputId}
aria-invalid={canRenderStatusIcon ? !props.valid : undefined}
aria-errormessage={canRenderStatusIcon ? statusIconId : undefined}
value={props.value}
onChange={props.onChange}
/>
{renderStatusIcon()}
</div>
</div>;
};
export default LabelledPasswordInput;

View File

@ -1,22 +1,24 @@
import * as React from 'react';
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import StyledInput from '../style/StyledInput'; import StyledInput from '../style/StyledInput';
import { _ } from '@joplin/lib/locale';
export interface ChangeEvent { import { ChangeEventHandler } from './types';
value: string;
}
type ChangeEventHandler = (event: ChangeEvent)=> void;
interface Props { interface Props {
value: string; value: string;
inputId: string;
onChange: ChangeEventHandler; onChange: ChangeEventHandler;
'aria-invalid'?: boolean;
'aria-errormessage'?: string;
} }
export const PasswordInput = (props: Props) => { const PasswordInput = (props: Props) => {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const inputType = showPassword ? 'text' : 'password'; const inputType = showPassword ? 'text' : 'password';
const icon = showPassword ? 'far fa-eye-slash' : 'far fa-eye'; const icon = showPassword ? 'far fa-eye-slash' : 'far fa-eye';
const title = showPassword ? _('Hide password') : _('Show password');
const onShowPassword = useCallback(() => { const onShowPassword = useCallback(() => {
setShowPassword(current => !current); setShowPassword(current => !current);
@ -24,8 +26,20 @@ export const PasswordInput = (props: Props) => {
return ( return (
<div className="password-input"> <div className="password-input">
<StyledInput className="field" type={inputType} value={props.value} onChange={props.onChange}/> <StyledInput
<button onClick={onShowPassword} className="showpasswordbutton"><i className={icon}></i></button> id={props.inputId}
aria-errormessage={props['aria-errormessage']}
aria-invalid={props['aria-invalid']}
className="field"
type={inputType}
value={props.value}
onChange={props.onChange}
/>
<button onClick={onShowPassword} className="showpasswordbutton">
<i className={icon} role='img' aria-label={title} title={title}></i>
</button>
</div> </div>
); );
}; };
export default PasswordInput;

View File

@ -1,19 +1,3 @@
.password-input { @use "styles/password-input.scss";
display: flex; @use "styles/labelled-password-input.scss";
position: relative; @use "styles/password-status-icon.scss";
flex: 1;
> .field {
display: flex;
flex: 1;
width: 100%;
}
> .showpasswordbutton {
position: absolute;
right: 5px;
top: 4px;
border: none;
background: none;
}
}

View File

@ -0,0 +1,10 @@
.labelled-password-input {
display: flex;
flex-direction: column;
> .password {
display: flex;
flex-direction: row;
align-items: center;
}
}

View File

@ -0,0 +1,19 @@
.password-input {
display: flex;
position: relative;
flex: 1;
> .field {
display: flex;
flex: 1;
width: 100%;
}
> .showpasswordbutton {
position: absolute;
right: 5px;
top: 4px;
border: none;
background: none;
}
}

View File

@ -0,0 +1,13 @@
.password-status-icon {
margin-left: 10px;
&.-valid {
color: var(--joplin-color-correct);
}
&.-invalid {
color: var(--joplin-color-error);
margin-left: 5px;
}
}

View File

@ -0,0 +1,6 @@
export interface ChangeEvent {
value: string;
}
export type ChangeEventHandler = (event: ChangeEvent)=> void;

View File

@ -60,15 +60,6 @@ interface ActiveSorting {
const ResourceTableComp = (props: ResourceTable) => { const ResourceTableComp = (props: ResourceTable) => {
const theme = themeStyle(props.themeId); const theme = themeStyle(props.themeId);
const sortOrderEngagedMarker = (s: SortingOrder) => {
return (
<a href="#"
style={{ color: theme.urlColor }}
onClick={() => props.onToggleSorting(s)}>{
(props.sorting.order === s && props.sorting.type === 'desc') ? '▾' : '▴'}</a>
);
};
const titleCellStyle = { const titleCellStyle = {
...theme.textStyle, ...theme.textStyle,
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
@ -96,12 +87,28 @@ const ResourceTableComp = (props: ResourceTable) => {
(resource: InnerResource) => !props.filter || resource.title?.includes(props.filter) || resource.id.includes(props.filter), (resource: InnerResource) => !props.filter || resource.title?.includes(props.filter) || resource.id.includes(props.filter),
); );
const renderSortableHeader = (title: string, order: SortingOrder) => {
const sortedDescending = props.sorting.order === order && props.sorting.type === 'desc';
const sortButtonLabel = sortedDescending ? _('Sort "%s" in ascending order', title) : _('Sort "%s" in descending order', title);
const reverseSortButton = (
<a
href="#"
style={{ color: theme.urlColor }}
onClick={() => props.onToggleSorting(order)}
aria-label={sortButtonLabel}
title={sortButtonLabel}
role='button'
>{sortedDescending ? '▾' : '▴'}</a>
);
return <th key={`header-${title}`} style={headerStyle}>{title} {reverseSortButton}</th>;
};
return ( return (
<table style={{ width: '100%' }}> <table style={{ width: '100%' }}>
<thead> <thead>
<tr> <tr>
<th style={headerStyle}>{_('Title')} {sortOrderEngagedMarker('name')}</th> {renderSortableHeader(_('Title'), 'name')}
<th style={headerStyle}>{_('Size')} {sortOrderEngagedMarker('size')}</th> {renderSortableHeader(_('Size'), 'size')}
<th style={headerStyle}>{_('ID')}</th> <th style={headerStyle}>{_('ID')}</th>
<th style={headerStyle}>{_('Action')}</th> <th style={headerStyle}>{_('Action')}</th>
</tr> </tr>

View File

@ -289,22 +289,9 @@ Component-specific classes
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
> .password-valid-icon {
margin-left: 10px;
}
} }
// .master-password-dialog .current-password-wrapper input { // .master-password-dialog .current-password-wrapper input {
// flex: 1; // flex: 1;
// margin-right: 10px; // margin-right: 10px;
// } // }
.master-password-dialog .fa-check {
color: var(--joplin-color-correct);
}
.master-password-dialog .fa-times {
color: var(--joplin-color-error);
margin-left: 5px;
}