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/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
2
.gitignore
vendored
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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 { 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;
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 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>
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user