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/NotyfContext.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/types.js
packages/app-desktop/gui/PdfViewer.js
packages/app-desktop/gui/PromptDialog.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/NotyfContext.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/types.js
packages/app-desktop/gui/PdfViewer.js
packages/app-desktop/gui/PromptDialog.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 KvStore from '@joplin/lib/services/KvStore';
import ShareService from '@joplin/lib/services/share/ShareService';
import { PasswordInput } from '../PasswordInput/PasswordInput';
import LabelledPasswordInput from '../PasswordInput/LabelledPasswordInput';
interface Props {
themeId: number;
@ -136,11 +136,6 @@ export default function(props: Props) {
setCurrentPasswordIsValid(isValid);
}, [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() {
const renderCurrentPassword = () => {
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
// been encrypted with it).
const showValidIcon = currentPassword && status !== MasterPasswordStatus.NotSet;
return (
<div className="form-input-group">
<label>{'Current password'}</label>
<div className="current-password-wrapper">
<PasswordInput value={currentPassword} onChange={onCurrentPasswordChange}/>
{renderCurrentPasswordIcon()}
</div>
</div>
<LabelledPasswordInput
labelText={_('Current password')}
value={currentPassword}
onChange={onCurrentPasswordChange}
valid={showValidIcon ? currentPasswordIsValid : undefined}
/>
);
};
@ -175,15 +170,17 @@ export default function(props: Props) {
<div>
<div className="form">
{renderCurrentPassword()}
<div className="form-input-group">
<label>{enterPasswordLabel}</label>
<PasswordInput value={password1} onChange={onPasswordChange1}/>
</div>
<LabelledPasswordInput
labelText={enterPasswordLabel}
value={password1}
onChange={onPasswordChange1}
/>
{needToRepeatPassword && (
<div className="form-input-group">
<label>{'Re-enter password'}</label>
<PasswordInput value={password2} onChange={onPasswordChange2}/>
</div>
<LabelledPasswordInput
labelText={_('Re-enter password')}
value={password2}
onChange={onPasswordChange2}
/>
)}
</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>

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

View File

@ -1,19 +1,3 @@
.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;
}
}
@use "styles/password-input.scss";
@use "styles/labelled-password-input.scss";
@use "styles/password-status-icon.scss";

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 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 = {
...theme.textStyle,
textOverflow: 'ellipsis',
@ -96,12 +87,28 @@ const ResourceTableComp = (props: ResourceTable) => {
(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 (
<table style={{ width: '100%' }}>
<thead>
<tr>
<th style={headerStyle}>{_('Title')} {sortOrderEngagedMarker('name')}</th>
<th style={headerStyle}>{_('Size')} {sortOrderEngagedMarker('size')}</th>
{renderSortableHeader(_('Title'), 'name')}
{renderSortableHeader(_('Size'), 'size')}
<th style={headerStyle}>{_('ID')}</th>
<th style={headerStyle}>{_('Action')}</th>
</tr>

View File

@ -289,22 +289,9 @@ Component-specific classes
display: flex;
flex-direction: row;
align-items: center;
> .password-valid-icon {
margin-left: 10px;
}
}
// .master-password-dialog .current-password-wrapper input {
// flex: 1;
// 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;
}