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:
parent
2d9c2d533d
commit
f07e4e9b5a
@ -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
2
.gitignore
vendored
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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;
|
@ -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;
|
||||
|
@ -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";
|
@ -0,0 +1,10 @@
|
||||
.labelled-password-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .password {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
6
packages/app-desktop/gui/PasswordInput/types.ts
Normal file
6
packages/app-desktop/gui/PasswordInput/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
export interface ChangeEvent {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type ChangeEventHandler = (event: ChangeEvent)=> void;
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user