mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-02 12:47:41 +02:00
Desktop: Accessibility: Improve settings screen keyboard navigation and screen reader accessibility (#10812)
This commit is contained in:
parent
65ef700fdc
commit
14cc053094
@ -167,9 +167,13 @@ packages/app-desktop/gui/Button/Button.js
|
|||||||
packages/app-desktop/gui/ClipperConfigScreen.js
|
packages/app-desktop/gui/ClipperConfigScreen.js
|
||||||
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
|
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
|
||||||
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
|
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
|
||||||
packages/app-desktop/gui/ConfigScreen/FontSearch.js
|
|
||||||
packages/app-desktop/gui/ConfigScreen/Sidebar.js
|
packages/app-desktop/gui/ConfigScreen/Sidebar.js
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/FontSearch.js
|
||||||
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
|
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.js
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/SettingDescription.js
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/SettingHeader.js
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/SettingLabel.js
|
||||||
packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
||||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
||||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
||||||
@ -463,6 +467,7 @@ packages/app-desktop/integration-tests/models/SettingsScreen.js
|
|||||||
packages/app-desktop/integration-tests/models/Sidebar.js
|
packages/app-desktop/integration-tests/models/Sidebar.js
|
||||||
packages/app-desktop/integration-tests/noteList.spec.js
|
packages/app-desktop/integration-tests/noteList.spec.js
|
||||||
packages/app-desktop/integration-tests/richTextEditor.spec.js
|
packages/app-desktop/integration-tests/richTextEditor.spec.js
|
||||||
|
packages/app-desktop/integration-tests/settings.spec.js
|
||||||
packages/app-desktop/integration-tests/sidebar.spec.js
|
packages/app-desktop/integration-tests/sidebar.spec.js
|
||||||
packages/app-desktop/integration-tests/simpleBackup.spec.js
|
packages/app-desktop/integration-tests/simpleBackup.spec.js
|
||||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||||
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -146,9 +146,13 @@ packages/app-desktop/gui/Button/Button.js
|
|||||||
packages/app-desktop/gui/ClipperConfigScreen.js
|
packages/app-desktop/gui/ClipperConfigScreen.js
|
||||||
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
|
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
|
||||||
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
|
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
|
||||||
packages/app-desktop/gui/ConfigScreen/FontSearch.js
|
|
||||||
packages/app-desktop/gui/ConfigScreen/Sidebar.js
|
packages/app-desktop/gui/ConfigScreen/Sidebar.js
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/FontSearch.js
|
||||||
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
|
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.js
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/SettingDescription.js
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/SettingHeader.js
|
||||||
|
packages/app-desktop/gui/ConfigScreen/controls/SettingLabel.js
|
||||||
packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
||||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
||||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
||||||
@ -442,6 +446,7 @@ packages/app-desktop/integration-tests/models/SettingsScreen.js
|
|||||||
packages/app-desktop/integration-tests/models/Sidebar.js
|
packages/app-desktop/integration-tests/models/Sidebar.js
|
||||||
packages/app-desktop/integration-tests/noteList.spec.js
|
packages/app-desktop/integration-tests/noteList.spec.js
|
||||||
packages/app-desktop/integration-tests/richTextEditor.spec.js
|
packages/app-desktop/integration-tests/richTextEditor.spec.js
|
||||||
|
packages/app-desktop/integration-tests/settings.spec.js
|
||||||
packages/app-desktop/integration-tests/sidebar.spec.js
|
packages/app-desktop/integration-tests/sidebar.spec.js
|
||||||
packages/app-desktop/integration-tests/simpleBackup.spec.js
|
packages/app-desktop/integration-tests/simpleBackup.spec.js
|
||||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||||
|
@ -36,6 +36,9 @@ interface Props {
|
|||||||
isSquare?: boolean;
|
isSquare?: boolean;
|
||||||
iconOnly?: boolean;
|
iconOnly?: boolean;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
|
|
||||||
|
'aria-controls'?: string;
|
||||||
|
'aria-expanded'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledTitle = styled.span`
|
const StyledTitle = styled.span`
|
||||||
@ -220,7 +223,14 @@ const Button = React.forwardRef((props: Props, ref: any) => {
|
|||||||
|
|
||||||
function renderIcon() {
|
function renderIcon() {
|
||||||
if (!props.iconName) return null;
|
if (!props.iconName) return null;
|
||||||
return <StyledIcon aria-label={props.iconLabel} animation={props.iconAnimation} mr={iconOnly ? '0' : '6px'} color={props.color} className={props.iconName}/>;
|
return <StyledIcon
|
||||||
|
aria-label={props.iconLabel ?? ''}
|
||||||
|
animation={props.iconAnimation}
|
||||||
|
mr={iconOnly ? '0' : '6px'}
|
||||||
|
color={props.color}
|
||||||
|
className={props.iconName}
|
||||||
|
role='img'
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTitle() {
|
function renderTitle() {
|
||||||
@ -234,7 +244,22 @@ const Button = React.forwardRef((props: Props, ref: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledButton ref={ref} fontSize={props.fontSize} isSquare={props.isSquare} size={props.size} style={props.style} disabled={props.disabled} title={props.tooltip} className={props.className} iconOnly={iconOnly} onClick={onClick}>
|
<StyledButton
|
||||||
|
ref={ref}
|
||||||
|
fontSize={props.fontSize}
|
||||||
|
isSquare={props.isSquare}
|
||||||
|
size={props.size}
|
||||||
|
style={props.style}
|
||||||
|
disabled={props.disabled}
|
||||||
|
title={props.tooltip}
|
||||||
|
className={props.className}
|
||||||
|
iconOnly={iconOnly}
|
||||||
|
onClick={onClick}
|
||||||
|
|
||||||
|
aria-disabled={props.disabled}
|
||||||
|
aria-expanded={props['aria-expanded']}
|
||||||
|
aria-controls={props['aria-controls']}
|
||||||
|
>
|
||||||
{renderIcon()}
|
{renderIcon()}
|
||||||
{renderTitle()}
|
{renderTitle()}
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Sidebar from './Sidebar';
|
import Sidebar from './Sidebar';
|
||||||
import ButtonBar from './ButtonBar';
|
import ButtonBar from './ButtonBar';
|
||||||
import Button, { ButtonLevel, ButtonSize } from '../Button/Button';
|
import Button, { ButtonLevel } from '../Button/Button';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import bridge from '../../services/bridge';
|
import bridge from '../../services/bridge';
|
||||||
import Setting, { AppType, SettingItemSubType, SyncStartupOperation } from '@joplin/lib/models/Setting';
|
import Setting, { AppType, SettingValueType, SyncStartupOperation } from '@joplin/lib/models/Setting';
|
||||||
import control_PluginsStates from './controls/plugins/PluginsStates';
|
|
||||||
import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen';
|
import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen';
|
||||||
import { reg } from '@joplin/lib/registry';
|
import { reg } from '@joplin/lib/registry';
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const { themeStyle } = require('@joplin/lib/theme');
|
import { themeStyle } from '@joplin/lib/theme';
|
||||||
import * as pathUtils from '@joplin/lib/path-utils';
|
|
||||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||||
import * as shared from '@joplin/lib/components/shared/config/config-shared.js';
|
import * as shared from '@joplin/lib/components/shared/config/config-shared.js';
|
||||||
import ClipperConfigScreen from '../ClipperConfigScreen';
|
import ClipperConfigScreen from '../ClipperConfigScreen';
|
||||||
@ -20,12 +18,8 @@ import ToggleAdvancedSettingsButton from './controls/ToggleAdvancedSettingsButto
|
|||||||
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
|
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
|
||||||
import MacOSMissingPasswordHelpLink from './controls/MissingPasswordHelpLink';
|
import MacOSMissingPasswordHelpLink from './controls/MissingPasswordHelpLink';
|
||||||
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
|
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
|
||||||
import FontSearch from './FontSearch';
|
import SettingComponent, { UpdateSettingValueEvent } from './controls/SettingComponent';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
const settingKeyToControl: any = {
|
|
||||||
'plugins.states': control_PluginsStates,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Font {
|
interface Font {
|
||||||
family: string;
|
family: string;
|
||||||
@ -67,9 +61,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
|||||||
this.onCancelClick = this.onCancelClick.bind(this);
|
this.onCancelClick = this.onCancelClick.bind(this);
|
||||||
this.onSaveClick = this.onSaveClick.bind(this);
|
this.onSaveClick = this.onSaveClick.bind(this);
|
||||||
this.onApplyClick = this.onApplyClick.bind(this);
|
this.onApplyClick = this.onApplyClick.bind(this);
|
||||||
this.renderLabel = this.renderLabel.bind(this);
|
|
||||||
this.renderDescription = this.renderDescription.bind(this);
|
|
||||||
this.renderHeader = this.renderHeader.bind(this);
|
|
||||||
this.handleSettingButton = this.handleSettingButton.bind(this);
|
this.handleSettingButton = this.handleSettingButton.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,7 +228,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
|||||||
if (syncTargetMd.supportsConfigCheck) {
|
if (syncTargetMd.supportsConfigCheck) {
|
||||||
const messages = shared.checkSyncConfigMessages(this);
|
const messages = shared.checkSyncConfigMessages(this);
|
||||||
const statusComp = !messages.length ? null : (
|
const statusComp = !messages.length ? null : (
|
||||||
<div style={statusStyle}>
|
<div style={statusStyle} aria-live='polite'>
|
||||||
{messages[0]}
|
{messages[0]}
|
||||||
{messages.length >= 1 ? <p>{messages[1]}</p> : null}
|
{messages.length >= 1 ? <p>{messages[1]}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
@ -277,12 +268,14 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
|||||||
|
|
||||||
let advancedSettingsButton = null;
|
let advancedSettingsButton = null;
|
||||||
const advancedSettingsSectionStyle = { display: 'none' };
|
const advancedSettingsSectionStyle = { display: 'none' };
|
||||||
|
const advancedSettingsGroupId = `advanced_settings_${key}`;
|
||||||
|
|
||||||
if (advancedSettingComps.length) {
|
if (advancedSettingComps.length) {
|
||||||
advancedSettingsButton = (
|
advancedSettingsButton = (
|
||||||
<ToggleAdvancedSettingsButton
|
<ToggleAdvancedSettingsButton
|
||||||
onClick={() => shared.advancedSettingsButton_click(this)}
|
onClick={() => shared.advancedSettingsButton_click(this)}
|
||||||
advancedSettingsVisible={this.state.showAdvancedSettings}
|
advancedSettingsVisible={this.state.showAdvancedSettings}
|
||||||
|
aria-controls={advancedSettingsGroupId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none';
|
advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none';
|
||||||
@ -293,425 +286,35 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
|||||||
{this.renderSectionDescription(section)}
|
{this.renderSectionDescription(section)}
|
||||||
<div>{settingComps}</div>
|
<div>{settingComps}</div>
|
||||||
{advancedSettingsButton}
|
{advancedSettingsButton}
|
||||||
<div style={advancedSettingsSectionStyle}>{advancedSettingComps}</div>
|
<div
|
||||||
|
style={advancedSettingsSectionStyle}
|
||||||
|
id={advancedSettingsGroupId}
|
||||||
|
role='group'
|
||||||
|
>{advancedSettingComps}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private labelStyle(themeId: number) {
|
private onUpdateSettingValue = ({ key, value }: UpdateSettingValueEvent) => {
|
||||||
const theme = themeStyle(themeId);
|
|
||||||
return { ...theme.textStyle, display: 'block',
|
|
||||||
color: theme.color,
|
|
||||||
fontSize: theme.fontSize * 1.083333,
|
|
||||||
fontWeight: 500,
|
|
||||||
marginBottom: theme.mainPadding / 2 };
|
|
||||||
}
|
|
||||||
|
|
||||||
private descriptionStyle(themeId: number) {
|
|
||||||
const theme = themeStyle(themeId);
|
|
||||||
return { ...theme.textStyle, color: theme.colorFaded,
|
|
||||||
fontStyle: 'italic',
|
|
||||||
maxWidth: '70em',
|
|
||||||
marginTop: 5 };
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderLabel(themeId: number, label: string) {
|
|
||||||
const labelStyle = this.labelStyle(themeId);
|
|
||||||
return (
|
|
||||||
<div style={labelStyle}>
|
|
||||||
<label>{label}</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
private renderHeader(themeId: number, label: string, style: any = null) {
|
|
||||||
const theme = themeStyle(themeId);
|
|
||||||
|
|
||||||
const labelStyle = { ...theme.textStyle, display: 'block',
|
|
||||||
color: theme.color,
|
|
||||||
fontSize: theme.fontSize * 1.25,
|
|
||||||
fontWeight: 500,
|
|
||||||
marginBottom: theme.mainPadding,
|
|
||||||
...style };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={labelStyle}>
|
|
||||||
<label>{label}</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderDescription(themeId: number, description: string) {
|
|
||||||
return description ? <div style={this.descriptionStyle(themeId)}>{description}</div> : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
public settingToComponent(key: string, value: any) {
|
|
||||||
const theme = themeStyle(this.props.themeId);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
const output: any = null;
|
|
||||||
|
|
||||||
const rowStyle = {
|
|
||||||
marginBottom: theme.mainPadding * 1.5,
|
|
||||||
};
|
|
||||||
|
|
||||||
const labelStyle = this.labelStyle(this.props.themeId);
|
|
||||||
|
|
||||||
const subLabel = { ...labelStyle, display: 'block',
|
|
||||||
opacity: 0.7,
|
|
||||||
marginBottom: labelStyle.marginBottom };
|
|
||||||
|
|
||||||
const checkboxLabelStyle = { ...labelStyle, marginLeft: 8,
|
|
||||||
display: 'inline',
|
|
||||||
backgroundColor: 'transparent' };
|
|
||||||
|
|
||||||
const controlStyle = {
|
|
||||||
display: 'inline-block',
|
|
||||||
color: theme.color,
|
|
||||||
fontFamily: theme.fontFamily,
|
|
||||||
backgroundColor: theme.backgroundColor,
|
|
||||||
};
|
|
||||||
|
|
||||||
const textInputBaseStyle = { ...controlStyle, fontFamily: theme.fontFamily,
|
|
||||||
border: '1px solid',
|
|
||||||
padding: '4px 6px',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
borderColor: theme.borderColor4,
|
|
||||||
borderRadius: 3,
|
|
||||||
paddingLeft: 6,
|
|
||||||
paddingRight: 6,
|
|
||||||
paddingTop: 4,
|
|
||||||
paddingBottom: 4 };
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
const updateSettingValue = (key: string, value: any) => {
|
|
||||||
const md = Setting.settingMetadata(key);
|
|
||||||
if (md.needRestart) {
|
|
||||||
this.setState({ needRestart: true });
|
|
||||||
}
|
|
||||||
shared.updateSettingValue(this, key, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const md = Setting.settingMetadata(key);
|
const md = Setting.settingMetadata(key);
|
||||||
|
if (md.needRestart) {
|
||||||
const descriptionText = Setting.keyDescription(key, AppType.Desktop);
|
this.setState({ needRestart: true });
|
||||||
const descriptionComp = this.renderDescription(this.props.themeId, descriptionText);
|
|
||||||
|
|
||||||
if (settingKeyToControl[key]) {
|
|
||||||
const SettingComponent = settingKeyToControl[key];
|
|
||||||
const label = md.label ? this.renderLabel(this.props.themeId, md.label()) : null;
|
|
||||||
return (
|
|
||||||
<div key={key} style={rowStyle}>
|
|
||||||
{label}
|
|
||||||
{this.renderDescription(this.props.themeId, md.description ? md.description(AppType.Desktop) : null)}
|
|
||||||
<SettingComponent
|
|
||||||
metadata={md}
|
|
||||||
value={value}
|
|
||||||
themeId={this.props.themeId}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
onChange={(event: any) => {
|
|
||||||
updateSettingValue(key, event.value);
|
|
||||||
}}
|
|
||||||
renderLabel={this.renderLabel}
|
|
||||||
renderDescription={this.renderDescription}
|
|
||||||
renderHeader={this.renderHeader}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (md.isEnum) {
|
|
||||||
const items = [];
|
|
||||||
const settingOptions = md.options();
|
|
||||||
const array = Setting.enumOptionsToValueLabels(settingOptions, md.optionsOrder ? md.optionsOrder() : [], {
|
|
||||||
valueKey: 'key',
|
|
||||||
labelKey: 'label',
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i < array.length; i++) {
|
|
||||||
const e = array[i];
|
|
||||||
items.push(
|
|
||||||
<option value={e.key.toString()} key={e.key}>
|
|
||||||
{settingOptions[e.key]}
|
|
||||||
</option>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectStyle = { ...controlStyle, paddingLeft: 6,
|
|
||||||
paddingRight: 6,
|
|
||||||
paddingTop: 4,
|
|
||||||
paddingBottom: 4,
|
|
||||||
borderColor: theme.borderColor4,
|
|
||||||
borderRadius: 3 };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={key} style={rowStyle}>
|
|
||||||
<div style={labelStyle}>
|
|
||||||
<label>{md.label()}</label>
|
|
||||||
</div>
|
|
||||||
<select
|
|
||||||
value={value}
|
|
||||||
style={selectStyle}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
onChange={(event: any) => {
|
|
||||||
updateSettingValue(key, event.target.value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{items}
|
|
||||||
</select>
|
|
||||||
{descriptionComp}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (md.type === Setting.TYPE_BOOL) {
|
|
||||||
const onCheckboxClick = () => {
|
|
||||||
updateSettingValue(key, !value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkboxSize = theme.fontSize * 1.1666666666666;
|
|
||||||
|
|
||||||
// Hack: The {key+value.toString()} is needed as otherwise the checkbox doesn't update when the state changes.
|
|
||||||
// There's probably a better way to do this but can't figure it out.
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={key + (`${value}`).toString()} style={rowStyle}>
|
|
||||||
<div style={{ ...controlStyle, backgroundColor: 'transparent', display: 'flex', alignItems: 'center' }}>
|
|
||||||
<input
|
|
||||||
id={`setting_checkbox_${key}`}
|
|
||||||
type="checkbox"
|
|
||||||
checked={!!value}
|
|
||||||
onChange={() => {
|
|
||||||
onCheckboxClick();
|
|
||||||
}}
|
|
||||||
style={{ marginLeft: 0, width: checkboxSize, height: checkboxSize }}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
onClick={() => {
|
|
||||||
onCheckboxClick();
|
|
||||||
}}
|
|
||||||
style={{ ...checkboxLabelStyle, marginLeft: 5, marginBottom: 0 }}
|
|
||||||
htmlFor={`setting_checkbox_${key}`}
|
|
||||||
>
|
|
||||||
{md.label()}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{descriptionComp}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (md.type === Setting.TYPE_STRING) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
const inputStyle: any = { ...textInputBaseStyle, width: '50%',
|
|
||||||
minWidth: '20em' };
|
|
||||||
const inputType = md.secure === true ? 'password' : 'text';
|
|
||||||
|
|
||||||
if (md.subType === 'file_path_and_args' || md.subType === 'file_path' || md.subType === 'directory_path') {
|
|
||||||
inputStyle.marginBottom = subLabel.marginBottom;
|
|
||||||
|
|
||||||
const splitCmd = (cmdString: string) => {
|
|
||||||
// Normally not necessary but certain plugins found a way to
|
|
||||||
// set the set the value to "undefined", leading to a crash.
|
|
||||||
// This is now fixed at the model level but to be sure we
|
|
||||||
// check here too, to handle any already existing data.
|
|
||||||
// https://github.com/laurent22/joplin/issues/7621
|
|
||||||
if (!cmdString) cmdString = '';
|
|
||||||
const path = pathUtils.extractExecutablePath(cmdString);
|
|
||||||
const args = cmdString.substr(path.length + 1);
|
|
||||||
return [pathUtils.unquotePath(path), args];
|
|
||||||
};
|
|
||||||
|
|
||||||
const joinCmd = (cmdArray: string[]) => {
|
|
||||||
if (!cmdArray[0] && !cmdArray[1]) return '';
|
|
||||||
let cmdString = pathUtils.quotePath(cmdArray[0]);
|
|
||||||
if (!cmdString) cmdString = '""';
|
|
||||||
if (cmdArray[1]) cmdString += ` ${cmdArray[1]}`;
|
|
||||||
return cmdString;
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
const onPathChange = (event: any) => {
|
|
||||||
if (md.subType === 'file_path_and_args') {
|
|
||||||
const cmd = splitCmd(this.state.settings[key]);
|
|
||||||
cmd[0] = event.target.value;
|
|
||||||
updateSettingValue(key, joinCmd(cmd));
|
|
||||||
} else {
|
|
||||||
updateSettingValue(key, event.target.value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
const onArgsChange = (event: any) => {
|
|
||||||
const cmd = splitCmd(this.state.settings[key]);
|
|
||||||
cmd[1] = event.target.value;
|
|
||||||
updateSettingValue(key, joinCmd(cmd));
|
|
||||||
};
|
|
||||||
|
|
||||||
const browseButtonClick = async () => {
|
|
||||||
if (md.subType === 'directory_path') {
|
|
||||||
const paths = await bridge().showOpenDialog({
|
|
||||||
properties: ['openDirectory'],
|
|
||||||
});
|
|
||||||
if (!paths || !paths.length) return;
|
|
||||||
updateSettingValue(key, paths[0]);
|
|
||||||
} else {
|
|
||||||
const paths = await bridge().showOpenDialog();
|
|
||||||
if (!paths || !paths.length) return;
|
|
||||||
|
|
||||||
if (md.subType === 'file_path') {
|
|
||||||
updateSettingValue(key, paths[0]);
|
|
||||||
} else {
|
|
||||||
const cmd = splitCmd(this.state.settings[key]);
|
|
||||||
cmd[0] = paths[0];
|
|
||||||
updateSettingValue(key, joinCmd(cmd));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const cmd = splitCmd(this.state.settings[key]);
|
|
||||||
const path = md.subType === 'file_path_and_args' ? cmd[0] : this.state.settings[key];
|
|
||||||
|
|
||||||
const argComp = md.subType !== 'file_path_and_args' ? null : (
|
|
||||||
<div style={{ ...rowStyle, marginBottom: 5 }}>
|
|
||||||
<div style={subLabel}>{_('Arguments:')}</div>
|
|
||||||
<input
|
|
||||||
type={inputType}
|
|
||||||
style={inputStyle}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
onChange={(event: any) => {
|
|
||||||
onArgsChange(event);
|
|
||||||
}}
|
|
||||||
value={cmd[1]}
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
|
|
||||||
{descriptionComp}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={key} style={rowStyle}>
|
|
||||||
<div style={labelStyle}>
|
|
||||||
<label>{md.label()}</label>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex' }}>
|
|
||||||
<div style={{ flex: 1 }}>
|
|
||||||
<div style={{ ...rowStyle, marginBottom: 5 }}>
|
|
||||||
<div style={subLabel}>{_('Path:')}</div>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', marginBottom: inputStyle.marginBottom }}>
|
|
||||||
<input
|
|
||||||
type={inputType}
|
|
||||||
style={{ ...inputStyle, marginBottom: 0, marginRight: 5 }}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
onChange={(event: any) => {
|
|
||||||
onPathChange(event);
|
|
||||||
}}
|
|
||||||
value={path}
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
level={ButtonLevel.Secondary}
|
|
||||||
title={_('Browse...')}
|
|
||||||
onClick={browseButtonClick}
|
|
||||||
size={ButtonSize.Small}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
|
|
||||||
{descriptionComp}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{argComp}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
const onTextChange = (event: any) => {
|
|
||||||
updateSettingValue(key, event.target.value);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div key={key} style={rowStyle}>
|
|
||||||
<div style={labelStyle}>
|
|
||||||
<label>{md.label()}</label>
|
|
||||||
</div>
|
|
||||||
{
|
|
||||||
md.subType === SettingItemSubType.FontFamily || md.subType === SettingItemSubType.MonospaceFontFamily ?
|
|
||||||
<FontSearch
|
|
||||||
type={inputType}
|
|
||||||
style={inputStyle}
|
|
||||||
value={this.state.settings[key]}
|
|
||||||
availableFonts={this.state.fonts}
|
|
||||||
onChange={fontFamily => updateSettingValue(key, fontFamily)}
|
|
||||||
subtype={md.subType}
|
|
||||||
/> :
|
|
||||||
<input
|
|
||||||
type={inputType}
|
|
||||||
style={inputStyle}
|
|
||||||
value={this.state.settings[key]}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
onChange={(event: any) => {
|
|
||||||
onTextChange(event);
|
|
||||||
}}
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
|
|
||||||
{descriptionComp}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (md.type === Setting.TYPE_INT) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
const onNumChange = (event: any) => {
|
|
||||||
updateSettingValue(key, event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const label = [md.label()];
|
|
||||||
if (md.unitLabel) label.push(`(${md.unitLabel(md.value)})`);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
const inputStyle: any = { ...textInputBaseStyle };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={key} style={rowStyle}>
|
|
||||||
<div style={labelStyle}>
|
|
||||||
<label>{label.join(' ')}</label>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
style={inputStyle}
|
|
||||||
value={this.state.settings[key]}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
onChange={(event: any) => {
|
|
||||||
onNumChange(event);
|
|
||||||
}}
|
|
||||||
min={md.minimum}
|
|
||||||
max={md.maximum}
|
|
||||||
step={md.step}
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
{descriptionComp}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (md.type === Setting.TYPE_BUTTON) {
|
|
||||||
const labelComp = md.hideLabel ? null : (
|
|
||||||
<div style={labelStyle}>
|
|
||||||
<label>{md.label()}</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={key} style={rowStyle}>
|
|
||||||
{labelComp}
|
|
||||||
<Button level={ButtonLevel.Secondary} title={md.label()} onClick={md.onClick ? md.onClick : () => this.handleSettingButton(key)}/>
|
|
||||||
{descriptionComp}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.warn(`Type not implemented: ${key}`);
|
|
||||||
}
|
}
|
||||||
|
shared.updateSettingValue(this, key, value);
|
||||||
|
};
|
||||||
|
|
||||||
return output;
|
public settingToComponent<T extends string>(key: T, value: SettingValueType<T>) {
|
||||||
|
return (
|
||||||
|
<SettingComponent
|
||||||
|
themeId={this.props.themeId}
|
||||||
|
key={key}
|
||||||
|
settingKey={key}
|
||||||
|
value={value}
|
||||||
|
fonts={this.state.fonts}
|
||||||
|
onUpdateSettingValue={this.onUpdateSettingValue}
|
||||||
|
onSettingButtonClick={this.handleSettingButton}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private restartMessage() {
|
private restartMessage() {
|
||||||
@ -768,7 +371,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
|||||||
|
|
||||||
const settings = this.state.settings;
|
const settings = this.state.settings;
|
||||||
|
|
||||||
const containerStyle = {
|
const containerStyle: React.CSSProperties = {
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
padding: theme.configScreenPadding,
|
padding: theme.configScreenPadding,
|
||||||
paddingTop: 0,
|
paddingTop: 0,
|
||||||
@ -800,6 +403,35 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
|||||||
const rightStyle = { ...style, flex: 1 };
|
const rightStyle = { ...style, flex: 1 };
|
||||||
delete style.width;
|
delete style.width;
|
||||||
|
|
||||||
|
const tabComponents: React.ReactNode[] = [];
|
||||||
|
for (const section of sections) {
|
||||||
|
const sectionId = `setting-section-${section.name}`;
|
||||||
|
let content = null;
|
||||||
|
const visible = section.name === this.state.selectedSectionName;
|
||||||
|
if (visible) {
|
||||||
|
content = (
|
||||||
|
<>
|
||||||
|
{screenComp}
|
||||||
|
<div style={containerStyle}>{settingComps}</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tabComponents.push(
|
||||||
|
<div
|
||||||
|
key={sectionId}
|
||||||
|
id={sectionId}
|
||||||
|
className={`setting-tab-panel ${!visible ? '-hidden' : ''}`}
|
||||||
|
hidden={!visible}
|
||||||
|
aria-labelledby={`setting-tab-${section.name}`}
|
||||||
|
tabIndex={0}
|
||||||
|
role='tabpanel'
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="config-screen" style={{ display: 'flex', flexDirection: 'row', height: this.props.style.height }}>
|
<div className="config-screen" style={{ display: 'flex', flexDirection: 'row', height: this.props.style.height }}>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
@ -808,9 +440,8 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
|||||||
sections={sections}
|
sections={sections}
|
||||||
/>
|
/>
|
||||||
<div style={rightStyle}>
|
<div style={rightStyle}>
|
||||||
{screenComp}
|
|
||||||
{needRestartComp}
|
{needRestartComp}
|
||||||
<div style={containerStyle}>{settingComps}</div>
|
{tabComponents}
|
||||||
<ButtonBar
|
<ButtonBar
|
||||||
hasChanges={hasChanges}
|
hasChanges={hasChanges}
|
||||||
backButtonTitle={hasChanges && !screenComp ? _('Cancel') : _('Back')}
|
backButtonTitle={hasChanges && !screenComp ? _('Cancel') : _('Back')}
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
import { AppType, SettingSectionSource } from '@joplin/lib/models/Setting';
|
import { AppType, MetadataBySection, SettingMetadataSection, SettingSectionSource } from '@joplin/lib/models/Setting';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||||
const styled = require('styled-components').default;
|
const styled = require('styled-components').default;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied;
|
||||||
type StyleProps = any;
|
type StyleProps = any;
|
||||||
|
|
||||||
|
interface SectionChangeEvent {
|
||||||
|
section: SettingMetadataSection;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selection: string;
|
selection: string;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
onSelectionChange: (event: SectionChangeEvent)=> void;
|
||||||
onSelectionChange: Function;
|
sections: MetadataBySection;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied;
|
|
||||||
sections: any[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StyledRoot = styled.div`
|
export const StyledRoot = styled.div`
|
||||||
@ -73,24 +77,63 @@ export const StyledListItemIcon = styled.i`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export default function Sidebar(props: Props) {
|
export default function Sidebar(props: Props) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied;
|
const buttonRefs = useRef<HTMLElement[]>([]);
|
||||||
const buttons: any[] = [];
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied;
|
// Making a tabbed region accessible involves supporting keyboard interaction.
|
||||||
function renderButton(section: any) {
|
// See https://www.w3.org/WAI/ARIA/apg/patterns/tabs/ for details
|
||||||
|
const onKeyDown: React.KeyboardEventHandler<HTMLElement> = useCallback((event) => {
|
||||||
|
const selectedIndex = props.sections.findIndex(section => section.name === props.selection);
|
||||||
|
let newIndex = selectedIndex;
|
||||||
|
|
||||||
|
if (event.code === 'ArrowUp') {
|
||||||
|
newIndex --;
|
||||||
|
} else if (event.code === 'ArrowDown') {
|
||||||
|
newIndex ++;
|
||||||
|
} else if (event.code === 'Home') {
|
||||||
|
newIndex = 0;
|
||||||
|
} else if (event.code === 'End') {
|
||||||
|
newIndex = props.sections.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newIndex < 0) newIndex += props.sections.length;
|
||||||
|
newIndex %= props.sections.length;
|
||||||
|
|
||||||
|
if (newIndex !== selectedIndex) {
|
||||||
|
event.preventDefault();
|
||||||
|
props.onSelectionChange({ section: props.sections[newIndex] });
|
||||||
|
|
||||||
|
const targetButton = buttonRefs.current[newIndex];
|
||||||
|
if (targetButton) {
|
||||||
|
focus('Sidebar', targetButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [props.sections, props.selection, props.onSelectionChange]);
|
||||||
|
|
||||||
|
const buttons: React.ReactNode[] = [];
|
||||||
|
|
||||||
|
function renderButton(section: SettingMetadataSection, index: number) {
|
||||||
const selected = props.selection === section.name;
|
const selected = props.selection === section.name;
|
||||||
return (
|
return (
|
||||||
<StyledListItem
|
<StyledListItem
|
||||||
key={section.name}
|
key={section.name}
|
||||||
href='#'
|
href='#'
|
||||||
role='tab'
|
role='tab'
|
||||||
|
ref={(item: HTMLElement) => { buttonRefs.current[index] = item; }}
|
||||||
|
|
||||||
|
id={`setting-tab-${section.name}`}
|
||||||
|
aria-controls={`setting-section-${section.name}`}
|
||||||
aria-selected={selected}
|
aria-selected={selected}
|
||||||
|
tabIndex={selected ? 0 : -1}
|
||||||
|
|
||||||
isSubSection={Setting.isSubSection(section.name)}
|
isSubSection={Setting.isSubSection(section.name)}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
onClick={() => { props.onSelectionChange({ section: section }); }}
|
onClick={() => { props.onSelectionChange({ section: section }); }}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
>
|
>
|
||||||
<StyledListItemIcon
|
<StyledListItemIcon
|
||||||
|
aria-label=''
|
||||||
className={Setting.sectionNameToIcon(section.name, AppType.Desktop)}
|
className={Setting.sectionNameToIcon(section.name, AppType.Desktop)}
|
||||||
|
role='img'
|
||||||
/>
|
/>
|
||||||
<StyledListItemLabel>
|
<StyledListItemLabel>
|
||||||
{Setting.sectionNameToLabel(section.name)}
|
{Setting.sectionNameToLabel(section.name)}
|
||||||
@ -109,13 +152,15 @@ export default function Sidebar(props: Props) {
|
|||||||
|
|
||||||
let pluginDividerAdded = false;
|
let pluginDividerAdded = false;
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
for (const section of props.sections) {
|
for (const section of props.sections) {
|
||||||
if (section.source === SettingSectionSource.Plugin && !pluginDividerAdded) {
|
if (section.source === SettingSectionSource.Plugin && !pluginDividerAdded) {
|
||||||
buttons.push(renderDivider('divider-plugins'));
|
buttons.push(renderDivider('divider-plugins'));
|
||||||
pluginDividerAdded = true;
|
pluginDividerAdded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
buttons.push(renderButton(section));
|
buttons.push(renderButton(section, index));
|
||||||
|
index ++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -9,6 +9,7 @@ interface Props {
|
|||||||
style: CSSProperties;
|
style: CSSProperties;
|
||||||
value: string;
|
value: string;
|
||||||
availableFonts: string[];
|
availableFonts: string[];
|
||||||
|
inputId: string;
|
||||||
onChange: (font: string)=> void;
|
onChange: (font: string)=> void;
|
||||||
subtype: string;
|
subtype: string;
|
||||||
}
|
}
|
||||||
@ -108,6 +109,7 @@ const FontSearch = (props: Props) => {
|
|||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
id={props.inputId}
|
||||||
ref={fontInputRef}
|
ref={fontInputRef}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
@ -0,0 +1,381 @@
|
|||||||
|
import Setting, { AppType, SettingItemSubType } from '@joplin/lib/models/Setting';
|
||||||
|
import { themeStyle } from '@joplin/lib/theme';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useCallback, useId } from 'react';
|
||||||
|
import control_PluginsStates from './plugins/PluginsStates';
|
||||||
|
import bridge from '../../../services/bridge';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import Button, { ButtonLevel, ButtonSize } from '../../Button/Button';
|
||||||
|
import FontSearch from './FontSearch';
|
||||||
|
import * as pathUtils from '@joplin/lib/path-utils';
|
||||||
|
import SettingLabel from './SettingLabel';
|
||||||
|
import SettingDescription from './SettingDescription';
|
||||||
|
|
||||||
|
const settingKeyToControl: Record<string, typeof control_PluginsStates> = {
|
||||||
|
'plugins.states': control_PluginsStates,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface UpdateSettingValueEvent {
|
||||||
|
key: string;
|
||||||
|
value: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
themeId: number;
|
||||||
|
settingKey: string;
|
||||||
|
value: unknown;
|
||||||
|
fonts: string[];
|
||||||
|
onUpdateSettingValue: (event: UpdateSettingValueEvent)=> void;
|
||||||
|
onSettingButtonClick: (key: string)=> void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingComponent: React.FC<Props> = props => {
|
||||||
|
const theme = themeStyle(props.themeId);
|
||||||
|
|
||||||
|
const output: React.ReactNode = null;
|
||||||
|
|
||||||
|
const updateSettingValue = useCallback((key: string, value: unknown) => {
|
||||||
|
props.onUpdateSettingValue({ key, value });
|
||||||
|
}, [props.onUpdateSettingValue]);
|
||||||
|
|
||||||
|
const rowStyle = {
|
||||||
|
marginBottom: theme.mainPadding * 1.5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const controlStyle = {
|
||||||
|
display: 'inline-block',
|
||||||
|
color: theme.color,
|
||||||
|
fontFamily: theme.fontFamily,
|
||||||
|
backgroundColor: theme.backgroundColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
const textInputBaseStyle: React.CSSProperties = {
|
||||||
|
...controlStyle,
|
||||||
|
fontFamily: theme.fontFamily,
|
||||||
|
border: '1px solid',
|
||||||
|
padding: '4px 6px',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
borderColor: theme.borderColor4,
|
||||||
|
borderRadius: 3,
|
||||||
|
paddingLeft: 6,
|
||||||
|
paddingRight: 6,
|
||||||
|
paddingTop: 4,
|
||||||
|
paddingBottom: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
const key = props.settingKey;
|
||||||
|
const md = Setting.settingMetadata(key);
|
||||||
|
|
||||||
|
const descriptionText = Setting.keyDescription(key, AppType.Desktop);
|
||||||
|
const inputId = useId();
|
||||||
|
const descriptionId = useId();
|
||||||
|
const descriptionComp = <SettingDescription id={descriptionId} text={descriptionText}/>;
|
||||||
|
|
||||||
|
if (key in settingKeyToControl) {
|
||||||
|
const CustomSettingComponent = settingKeyToControl[key];
|
||||||
|
const label = md.label ? <SettingLabel text={md.label()} htmlFor={null} /> : null;
|
||||||
|
return (
|
||||||
|
<div style={rowStyle}>
|
||||||
|
{label}
|
||||||
|
<SettingDescription id={descriptionId} text={md.description ? md.description(AppType.Desktop) : null}/>
|
||||||
|
<CustomSettingComponent
|
||||||
|
value={props.value}
|
||||||
|
themeId={props.themeId}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
onChange={(event: any) => {
|
||||||
|
updateSettingValue(key, event.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (md.isEnum) {
|
||||||
|
const value = props.value as string;
|
||||||
|
|
||||||
|
const items = [];
|
||||||
|
const settingOptions = md.options();
|
||||||
|
const array = Setting.enumOptionsToValueLabels(settingOptions, md.optionsOrder ? md.optionsOrder() : [], {
|
||||||
|
valueKey: 'key',
|
||||||
|
labelKey: 'label',
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < array.length; i++) {
|
||||||
|
const e = array[i];
|
||||||
|
items.push(
|
||||||
|
<option value={e.key.toString()} key={e.key}>
|
||||||
|
{settingOptions[e.key]}
|
||||||
|
</option>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectStyle = { ...controlStyle, paddingLeft: 6,
|
||||||
|
paddingRight: 6,
|
||||||
|
paddingTop: 4,
|
||||||
|
paddingBottom: 4,
|
||||||
|
borderColor: theme.borderColor4,
|
||||||
|
borderRadius: 3 };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<SettingLabel htmlFor={inputId} text={md.label()}/>
|
||||||
|
<select
|
||||||
|
value={value}
|
||||||
|
style={selectStyle}
|
||||||
|
onChange={(event) => {
|
||||||
|
updateSettingValue(key, event.target.value);
|
||||||
|
}}
|
||||||
|
id={inputId}
|
||||||
|
aria-describedby={descriptionId}
|
||||||
|
>
|
||||||
|
{items}
|
||||||
|
</select>
|
||||||
|
{descriptionComp}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (md.type === Setting.TYPE_BOOL) {
|
||||||
|
const value = props.value as boolean;
|
||||||
|
|
||||||
|
const checkboxSize = theme.fontSize * 1.1666666666666;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<div style={{ ...controlStyle, backgroundColor: 'transparent', display: 'flex', alignItems: 'center' }}>
|
||||||
|
<input
|
||||||
|
id={inputId}
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!value}
|
||||||
|
onChange={event => updateSettingValue(key, event.target.checked)}
|
||||||
|
style={{ marginLeft: 0, width: checkboxSize, height: checkboxSize }}
|
||||||
|
|
||||||
|
// Prefer aria-details to aria-describedby for checkbox inputs --
|
||||||
|
// on MacOS, VoiceOver reads "checked"/"unchecked" only after reading the
|
||||||
|
// potentially-lengthy description. For other input types, the input value
|
||||||
|
// is read first.
|
||||||
|
aria-details={descriptionId}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className='setting-label -for-checkbox'
|
||||||
|
htmlFor={inputId}
|
||||||
|
>
|
||||||
|
{md.label()}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{descriptionComp}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (md.type === Setting.TYPE_STRING) {
|
||||||
|
const value = props.value as string;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
const inputStyle: any = { ...textInputBaseStyle, width: '50%',
|
||||||
|
minWidth: '20em' };
|
||||||
|
const inputType = md.secure === true ? 'password' : 'text';
|
||||||
|
|
||||||
|
if (md.subType === 'file_path_and_args' || md.subType === 'file_path' || md.subType === 'directory_path') {
|
||||||
|
inputStyle.marginBottom = theme.mainPadding / 2;
|
||||||
|
|
||||||
|
const splitCmd = (cmdString: string) => {
|
||||||
|
// Normally not necessary but certain plugins found a way to
|
||||||
|
// set the set the value to "undefined", leading to a crash.
|
||||||
|
// This is now fixed at the model level but to be sure we
|
||||||
|
// check here too, to handle any already existing data.
|
||||||
|
// https://github.com/laurent22/joplin/issues/7621
|
||||||
|
if (!cmdString) cmdString = '';
|
||||||
|
const path = pathUtils.extractExecutablePath(cmdString);
|
||||||
|
const args = cmdString.substr(path.length + 1);
|
||||||
|
return [pathUtils.unquotePath(path), args];
|
||||||
|
};
|
||||||
|
|
||||||
|
const joinCmd = (cmdArray: string[]) => {
|
||||||
|
if (!cmdArray[0] && !cmdArray[1]) return '';
|
||||||
|
let cmdString = pathUtils.quotePath(cmdArray[0]);
|
||||||
|
if (!cmdString) cmdString = '""';
|
||||||
|
if (cmdArray[1]) cmdString += ` ${cmdArray[1]}`;
|
||||||
|
return cmdString;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPathChange: React.ChangeEventHandler<HTMLInputElement> = event => {
|
||||||
|
if (md.subType === 'file_path_and_args') {
|
||||||
|
const cmd = splitCmd(value);
|
||||||
|
cmd[0] = event.target.value;
|
||||||
|
updateSettingValue(key, joinCmd(cmd));
|
||||||
|
} else {
|
||||||
|
updateSettingValue(key, event.target.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onArgsChange: React.ChangeEventHandler<HTMLInputElement> = event => {
|
||||||
|
const cmd = splitCmd(value);
|
||||||
|
cmd[1] = event.target.value;
|
||||||
|
updateSettingValue(key, joinCmd(cmd));
|
||||||
|
};
|
||||||
|
|
||||||
|
const browseButtonClick = async () => {
|
||||||
|
if (md.subType === 'directory_path') {
|
||||||
|
const paths = await bridge().showOpenDialog({
|
||||||
|
properties: ['openDirectory'],
|
||||||
|
});
|
||||||
|
if (!paths || !paths.length) return;
|
||||||
|
updateSettingValue(key, paths[0]);
|
||||||
|
} else {
|
||||||
|
const paths = await bridge().showOpenDialog();
|
||||||
|
if (!paths || !paths.length) return;
|
||||||
|
|
||||||
|
if (md.subType === 'file_path') {
|
||||||
|
updateSettingValue(key, paths[0]);
|
||||||
|
} else {
|
||||||
|
const cmd = splitCmd(value);
|
||||||
|
cmd[0] = paths[0];
|
||||||
|
updateSettingValue(key, joinCmd(cmd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cmd = splitCmd(value);
|
||||||
|
const path = md.subType === 'file_path_and_args' ? cmd[0] : value;
|
||||||
|
|
||||||
|
const argInputId = `setting_path_arg_${key}`;
|
||||||
|
const argComp = md.subType !== 'file_path_and_args' ? null : (
|
||||||
|
<div style={{ ...rowStyle, marginBottom: 5 }}>
|
||||||
|
<label
|
||||||
|
className='setting-label -sub-label'
|
||||||
|
htmlFor={argInputId}
|
||||||
|
>{_('Arguments:')}</label>
|
||||||
|
<input
|
||||||
|
type={inputType}
|
||||||
|
style={inputStyle}
|
||||||
|
onChange={onArgsChange}
|
||||||
|
value={cmd[1]}
|
||||||
|
spellCheck={false}
|
||||||
|
id={argInputId}
|
||||||
|
aria-describedby={descriptionId}
|
||||||
|
/>
|
||||||
|
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
|
||||||
|
{descriptionComp}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const pathDescriptionId = `setting_path_label_${key}`;
|
||||||
|
return (
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<SettingLabel text={md.label()} htmlFor={inputId}/>
|
||||||
|
<div style={{ display: 'flex' }}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ ...rowStyle, marginBottom: 5 }}>
|
||||||
|
<div
|
||||||
|
className='setting-label -sub-label'
|
||||||
|
id={pathDescriptionId}
|
||||||
|
>{_('Path:')}</div>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', marginBottom: inputStyle.marginBottom }}>
|
||||||
|
<input
|
||||||
|
type={inputType}
|
||||||
|
style={{ ...inputStyle, marginBottom: 0, marginRight: 5 }}
|
||||||
|
onChange={onPathChange}
|
||||||
|
value={path}
|
||||||
|
spellCheck={false}
|
||||||
|
id={inputId}
|
||||||
|
aria-describedby={pathDescriptionId}
|
||||||
|
aria-details={descriptionId}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
level={ButtonLevel.Secondary}
|
||||||
|
title={_('Browse...')}
|
||||||
|
onClick={browseButtonClick}
|
||||||
|
size={ButtonSize.Small}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
|
||||||
|
{descriptionComp}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{argComp}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const onTextChange: React.ChangeEventHandler<HTMLInputElement> = event => {
|
||||||
|
updateSettingValue(key, event.target.value);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<SettingLabel text={md.label()} htmlFor={inputId}/>
|
||||||
|
{
|
||||||
|
md.subType === SettingItemSubType.FontFamily || md.subType === SettingItemSubType.MonospaceFontFamily ?
|
||||||
|
<FontSearch
|
||||||
|
type={inputType}
|
||||||
|
style={inputStyle}
|
||||||
|
value={props.value as string}
|
||||||
|
availableFonts={props.fonts}
|
||||||
|
onChange={fontFamily => updateSettingValue(key, fontFamily)}
|
||||||
|
subtype={md.subType}
|
||||||
|
inputId={inputId}
|
||||||
|
/> :
|
||||||
|
<input
|
||||||
|
type={inputType}
|
||||||
|
style={inputStyle}
|
||||||
|
value={props.value as string|number}
|
||||||
|
onChange={onTextChange}
|
||||||
|
spellCheck={false}
|
||||||
|
id={inputId}
|
||||||
|
aria-describedby={descriptionId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
|
||||||
|
{descriptionComp}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (md.type === Setting.TYPE_INT) {
|
||||||
|
const value = props.value as number;
|
||||||
|
|
||||||
|
const onNumChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||||
|
updateSettingValue(key, event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const label = [md.label()];
|
||||||
|
if (md.unitLabel) label.push(`(${md.unitLabel(md.value)})`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<SettingLabel htmlFor={inputId} text={label.join(' ')}/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
style={textInputBaseStyle}
|
||||||
|
value={value}
|
||||||
|
onChange={onNumChange}
|
||||||
|
min={md.minimum}
|
||||||
|
max={md.maximum}
|
||||||
|
step={md.step}
|
||||||
|
spellCheck={false}
|
||||||
|
id={inputId}
|
||||||
|
aria-describedby={descriptionId}
|
||||||
|
/>
|
||||||
|
{descriptionComp}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (md.type === Setting.TYPE_BUTTON) {
|
||||||
|
const labelComp = md.hideLabel ? null : (
|
||||||
|
<SettingLabel text={md.label()} htmlFor={null} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={rowStyle}>
|
||||||
|
{labelComp}
|
||||||
|
<Button
|
||||||
|
level={ButtonLevel.Secondary}
|
||||||
|
title={md.label()}
|
||||||
|
onClick={md.onClick ? md.onClick : () => props.onSettingButtonClick(key)}
|
||||||
|
/>
|
||||||
|
{descriptionComp}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.warn(`Type not implemented: ${key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingComponent;
|
@ -0,0 +1,12 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingDescription: React.FC<Props> = props => {
|
||||||
|
return props.text ? <div className='setting-description' id={props.id}>{props.text}</div> : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingDescription;
|
@ -0,0 +1,15 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingHeader: React.FC<Props> = props => {
|
||||||
|
return (
|
||||||
|
<div className='setting-header'>
|
||||||
|
<label>{props.text}</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingHeader;
|
@ -0,0 +1,16 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
htmlFor: string|null;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingLabel: React.FC<Props> = props => {
|
||||||
|
return (
|
||||||
|
<div className='setting-label'>
|
||||||
|
<label htmlFor={props.htmlFor}>{props.text}</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingLabel;
|
@ -6,6 +6,7 @@ import { _ } from '@joplin/lib/locale';
|
|||||||
interface Props {
|
interface Props {
|
||||||
onClick: ()=> void;
|
onClick: ()=> void;
|
||||||
advancedSettingsVisible: boolean;
|
advancedSettingsVisible: boolean;
|
||||||
|
'aria-controls': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToggleAdvancedSettingsButton: React.FunctionComponent<Props> = props => {
|
const ToggleAdvancedSettingsButton: React.FunctionComponent<Props> = props => {
|
||||||
@ -16,6 +17,10 @@ const ToggleAdvancedSettingsButton: React.FunctionComponent<Props> = props => {
|
|||||||
level={ButtonLevel.Secondary}
|
level={ButtonLevel.Secondary}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
iconName={iconName}
|
iconName={iconName}
|
||||||
|
|
||||||
|
aria-controls={props['aria-controls']}
|
||||||
|
aria-expanded={props.advancedSettingsVisible}
|
||||||
|
|
||||||
title={_('Show Advanced Settings')}
|
title={_('Show Advanced Settings')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useId, useMemo } from 'react';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import ToggleButton from '../../../lib/ToggleButton/ToggleButton';
|
import ToggleButton from '../../../lib/ToggleButton/ToggleButton';
|
||||||
@ -173,6 +173,7 @@ export default function(props: Props) {
|
|||||||
themeId={props.themeId}
|
themeId={props.themeId}
|
||||||
value={item.enabled}
|
value={item.enabled}
|
||||||
onToggle={() => props.onToggle({ item })}
|
onToggle={() => props.onToggle({ item })}
|
||||||
|
aria-label={_('Enabled')}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,10 +257,17 @@ export default function(props: Props) {
|
|||||||
return <RecommendedBadge href="#" title={_('The Joplin team has vetted this plugin and it meets our standards for security and performance.')} onClick={onRecommendedClick}><i className="fas fa-crown"></i></RecommendedBadge>;
|
return <RecommendedBadge href="#" title={_('The Joplin team has vetted this plugin and it meets our standards for security and performance.')} onClick={onRecommendedClick}><i className="fas fa-crown"></i></RecommendedBadge>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nameLabelId = useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellRoot isCompatible={props.isCompatible}>
|
<CellRoot isCompatible={props.isCompatible} role='group' aria-labelledby={nameLabelId}>
|
||||||
<CellTop>
|
<CellTop>
|
||||||
<StyledNameAndVersion mb={'5px'}><StyledName onClick={onNameClick} href="#" style={{ marginRight: 5 }}>{item.manifest.name} {item.deleted ? _('(%s)', 'Deleted') : ''}</StyledName><StyledVersion>v{item.manifest.version}</StyledVersion></StyledNameAndVersion>
|
<StyledNameAndVersion mb={'5px'}>
|
||||||
|
<StyledName onClick={onNameClick} href="#" style={{ marginRight: 5 }} id={nameLabelId}>
|
||||||
|
{item.manifest.name} {item.deleted ? _('(%s)', 'Deleted') : ''}
|
||||||
|
</StyledName>
|
||||||
|
<StyledVersion>v{item.manifest.version}</StyledVersion>
|
||||||
|
</StyledNameAndVersion>
|
||||||
{renderToggleButton()}
|
{renderToggleButton()}
|
||||||
{renderRecommendedBadge()}
|
{renderRecommendedBadge()}
|
||||||
</CellTop>
|
</CellTop>
|
||||||
|
@ -17,6 +17,8 @@ import useOnDeleteHandler from '@joplin/lib/components/shared/config/plugins/use
|
|||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
import StyledMessage from '../../../style/StyledMessage';
|
import StyledMessage from '../../../style/StyledMessage';
|
||||||
import StyledLink from '../../../style/StyledLink';
|
import StyledLink from '../../../style/StyledLink';
|
||||||
|
import SettingHeader from '../SettingHeader';
|
||||||
|
import SettingDescription from '../SettingDescription';
|
||||||
const { space } = require('styled-system');
|
const { space } = require('styled-system');
|
||||||
|
|
||||||
const logger = Logger.create('PluginState');
|
const logger = Logger.create('PluginState');
|
||||||
@ -51,12 +53,6 @@ interface Props {
|
|||||||
themeId: number;
|
themeId: number;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
onChange: Function;
|
onChange: Function;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
|
||||||
renderLabel: Function;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
|
||||||
renderDescription: Function;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
|
||||||
renderHeader: Function;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let repoApi_: RepositoryApi = null;
|
let repoApi_: RepositoryApi = null;
|
||||||
@ -281,7 +277,7 @@ export default function(props: Props) {
|
|||||||
if (!pluginItems.length || allDeleted) {
|
if (!pluginItems.length || allDeleted) {
|
||||||
return (
|
return (
|
||||||
<UserPluginsRoot mb={'10px'}>
|
<UserPluginsRoot mb={'10px'}>
|
||||||
{props.renderDescription(props.themeId, _('You do not have any installed plugin.'))}
|
<SettingDescription text={_('You do not have any installed plugin.')}/>
|
||||||
</UserPluginsRoot>
|
</UserPluginsRoot>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -311,7 +307,6 @@ export default function(props: Props) {
|
|||||||
pluginSettings={pluginSettings}
|
pluginSettings={pluginSettings}
|
||||||
onSearchQueryChange={onSearchQueryChange}
|
onSearchQueryChange={onSearchQueryChange}
|
||||||
onPluginSettingsChange={onSearchPluginSettingsChange}
|
onPluginSettingsChange={onSearchPluginSettingsChange}
|
||||||
renderDescription={props.renderDescription}
|
|
||||||
repoApi={repoApi}
|
repoApi={repoApi}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -333,7 +328,7 @@ export default function(props: Props) {
|
|||||||
<div style={{ display: 'flex', flexDirection: 'row', maxWidth }}>
|
<div style={{ display: 'flex', flexDirection: 'row', maxWidth }}>
|
||||||
<ToolsButton size={ButtonSize.Small} tooltip={_('Plugin tools')} iconName="fas fa-cog" level={ButtonLevel.Secondary} onClick={onToolsClick}/>
|
<ToolsButton size={ButtonSize.Small} tooltip={_('Plugin tools')} iconName="fas fa-cog" level={ButtonLevel.Secondary} onClick={onToolsClick}/>
|
||||||
<div style={{ display: 'flex', flex: 1 }}>
|
<div style={{ display: 'flex', flex: 1 }}>
|
||||||
{props.renderHeader(props.themeId, _('Manage your plugins'))}
|
<SettingHeader text={_('Manage your plugins')}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{renderUserPlugins(pluginItems)}
|
{renderUserPlugins(pluginItems)}
|
||||||
|
@ -10,6 +10,7 @@ import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/Plug
|
|||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import useOnInstallHandler from '@joplin/lib/components/shared/config/plugins/useOnInstallHandler';
|
import useOnInstallHandler from '@joplin/lib/components/shared/config/plugins/useOnInstallHandler';
|
||||||
import { themeStyle } from '@joplin/lib/theme';
|
import { themeStyle } from '@joplin/lib/theme';
|
||||||
|
import SettingDescription from '../SettingDescription';
|
||||||
|
|
||||||
const Root = styled.div`
|
const Root = styled.div`
|
||||||
`;
|
`;
|
||||||
@ -26,8 +27,6 @@ interface Props {
|
|||||||
pluginSettings: PluginSettings;
|
pluginSettings: PluginSettings;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
onPluginSettingsChange(event: any): void;
|
onPluginSettingsChange(event: any): void;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
|
||||||
renderDescription: Function;
|
|
||||||
maxWidth: number;
|
maxWidth: number;
|
||||||
repoApi(): RepositoryApi;
|
repoApi(): RepositoryApi;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
@ -81,7 +80,7 @@ export default function(props: Props) {
|
|||||||
function renderResults(query: string, manifests: PluginManifest[]) {
|
function renderResults(query: string, manifests: PluginManifest[]) {
|
||||||
if (query && !manifests.length) {
|
if (query && !manifests.length) {
|
||||||
if (searchResultCount === null) return ''; // Search in progress
|
if (searchResultCount === null) return ''; // Search in progress
|
||||||
return props.renderDescription(props.themeId, _('No results'));
|
return <SettingDescription text={_('No results')}/>;
|
||||||
} else {
|
} else {
|
||||||
const output = [];
|
const output = [];
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@use "./styles/index.scss";
|
||||||
|
|
||||||
.config-screen-content-wrapper {
|
.config-screen-content-wrapper {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
5
packages/app-desktop/gui/ConfigScreen/styles/index.scss
Normal file
5
packages/app-desktop/gui/ConfigScreen/styles/index.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
@use "./setting-description.scss";
|
||||||
|
@use "./setting-label.scss";
|
||||||
|
@use "./setting-header.scss";
|
||||||
|
@use "./setting-tab-panel.scss";
|
@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
.setting-description {
|
||||||
|
color: var(--joplin-color-faded);
|
||||||
|
font-size: var(--joplin-font-size);
|
||||||
|
line-height: var(--joplin-line-height);
|
||||||
|
font-style: italic;
|
||||||
|
max-width: 70em;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
.setting-header {
|
||||||
|
display: block;
|
||||||
|
color: var(--joplin-color);
|
||||||
|
font-size: calc(var(--joplin-font-size) * 1.25);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: var(--joplin-main-padding);
|
||||||
|
line-height: var(--joplin-line-height);
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
.setting-label {
|
||||||
|
display: block;
|
||||||
|
color: var(--joplin-color);
|
||||||
|
font-size: calc(var(--joplin-font-size) * 1.083333);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: calc(var(--joplin-main-padding) / 2);
|
||||||
|
line-height: var(--joplin-line-height);
|
||||||
|
|
||||||
|
&.-sub-label {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-for-checkbox {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
display: inline;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
.setting-tab-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
&.-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
// Use a border rather than an outline -- an outline would be shown outside of the screen
|
||||||
|
// and thus invisible.
|
||||||
|
border: 1px solid var(--joplin-focus-outline-color);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
@ -10,7 +10,7 @@ import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
|||||||
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||||
import { getDefaultMasterKey, getMasterPasswordStatusMessage, masterPasswordIsValid, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
import { getDefaultMasterKey, getMasterPasswordStatusMessage, masterPasswordIsValid, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||||
import Button, { ButtonLevel } from '../Button/Button';
|
import Button, { ButtonLevel } from '../Button/Button';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useId, useMemo, useState } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
@ -350,7 +350,7 @@ const EncryptionConfigScreen = (props: Props) => {
|
|||||||
t = `<p>${t}</p>`;
|
t = `<p>${t}</p>`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<h2>{_('Re-encryption')}</h2>
|
<h2>{_('Re-encryption')}</h2>
|
||||||
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
|
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
|
||||||
<span style={{ marginRight: 10 }}>
|
<span style={{ marginRight: 10 }}>
|
||||||
@ -358,7 +358,7 @@ const EncryptionConfigScreen = (props: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
{ !props.shouldReencrypt ? null : <button onClick={() => dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
|
{ !props.shouldReencrypt ? null : <button onClick={() => dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -368,6 +368,7 @@ const EncryptionConfigScreen = (props: Props) => {
|
|||||||
setShowAdvanced(!showAdvanced);
|
setShowAdvanced(!showAdvanced);
|
||||||
}, [showAdvanced]);
|
}, [showAdvanced]);
|
||||||
|
|
||||||
|
const advancedSettingsId = useId();
|
||||||
const renderAdvancedSection = () => {
|
const renderAdvancedSection = () => {
|
||||||
const reEncryptSection = renderReencryptData();
|
const reEncryptSection = renderReencryptData();
|
||||||
|
|
||||||
@ -378,8 +379,12 @@ const EncryptionConfigScreen = (props: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<ToggleAdvancedSettingsButton
|
<ToggleAdvancedSettingsButton
|
||||||
onClick={toggleAdvanced}
|
onClick={toggleAdvanced}
|
||||||
advancedSettingsVisible={showAdvanced}/>
|
advancedSettingsVisible={showAdvanced}
|
||||||
{ showAdvanced ? reEncryptSection : null }
|
aria-controls={advancedSettingsId}
|
||||||
|
/>
|
||||||
|
<div id={advancedSettingsId}>
|
||||||
|
{ showAdvanced ? reEncryptSection : null }
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { themeStyle } from '@joplin/lib/theme';
|
import { themeStyle } from '@joplin/lib/theme';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
const ReactToggleButton = require('react-toggle-button');
|
const ReactToggleButton = require('react-toggle-button');
|
||||||
const Color = require('color');
|
const Color = require('color');
|
||||||
|
|
||||||
@ -8,11 +9,27 @@ interface Props {
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||||
onToggle: Function;
|
onToggle: Function;
|
||||||
themeId: number;
|
themeId: number;
|
||||||
|
'aria-label': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function(props: Props) {
|
export default function(props: Props) {
|
||||||
const theme = themeStyle(props.themeId);
|
const theme = themeStyle(props.themeId);
|
||||||
|
|
||||||
|
const ariaLabel = props['aria-label'];
|
||||||
|
|
||||||
|
const passThroughInputProps = useMemo(() => {
|
||||||
|
return {
|
||||||
|
'aria-label': ariaLabel,
|
||||||
|
|
||||||
|
// Works around a bug in ReactToggleButton -- the hidden checkbox input associated
|
||||||
|
// with the toggle is always read as "unchecked" by screen readers.
|
||||||
|
checked: props.value,
|
||||||
|
// Silences a ReactJS warning: "You provided a `checked` prop to a form field without an `onChange` handler."
|
||||||
|
// Change events are handled by ReactToggleButton.
|
||||||
|
onChange: ()=>{},
|
||||||
|
};
|
||||||
|
}, [ariaLabel, props.value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactToggleButton
|
<ReactToggleButton
|
||||||
value={props.value}
|
value={props.value}
|
||||||
@ -33,6 +50,7 @@ export default function(props: Props) {
|
|||||||
}}
|
}}
|
||||||
inactiveLabel=""
|
inactiveLabel=""
|
||||||
activeLabel=""
|
activeLabel=""
|
||||||
|
passThroughInputProps={passThroughInputProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { test, expect } from './util/test';
|
import { test, expect } from './util/test';
|
||||||
import MainScreen from './models/MainScreen';
|
import MainScreen from './models/MainScreen';
|
||||||
import SettingsScreen from './models/SettingsScreen';
|
|
||||||
import { _electron as electron } from '@playwright/test';
|
import { _electron as electron } from '@playwright/test';
|
||||||
import { writeFile } from 'fs-extra';
|
import { writeFile } from 'fs-extra';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
@ -130,36 +129,6 @@ test.describe('main', () => {
|
|||||||
expect(fullSize[0] / resizedSize[0]).toBeCloseTo(fullSize[1] / resizedSize[1]);
|
expect(fullSize[0] / resizedSize[0]).toBeCloseTo(fullSize[1] / resizedSize[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should be possible to remove sort order buttons in settings', async ({ electronApp, mainWindow }) => {
|
|
||||||
const mainScreen = new MainScreen(mainWindow);
|
|
||||||
await mainScreen.waitFor();
|
|
||||||
|
|
||||||
// Sort order buttons should be visible by default
|
|
||||||
await expect(mainScreen.noteListContainer.locator('[title^="Toggle sort order"]')).toBeVisible();
|
|
||||||
|
|
||||||
await mainScreen.openSettings(electronApp);
|
|
||||||
|
|
||||||
// Should be on the settings screen
|
|
||||||
const settingsScreen = new SettingsScreen(mainWindow);
|
|
||||||
await settingsScreen.waitFor();
|
|
||||||
|
|
||||||
// Open the appearance tab
|
|
||||||
await settingsScreen.appearanceTabButton.click();
|
|
||||||
|
|
||||||
// Find the sort order visible checkbox
|
|
||||||
const sortOrderVisibleCheckbox = mainWindow.getByLabel(/^Show sort order/);
|
|
||||||
|
|
||||||
await expect(sortOrderVisibleCheckbox).toBeChecked();
|
|
||||||
await sortOrderVisibleCheckbox.click();
|
|
||||||
await expect(sortOrderVisibleCheckbox).not.toBeChecked();
|
|
||||||
|
|
||||||
// Save settings & close
|
|
||||||
await settingsScreen.okayButton.click();
|
|
||||||
await mainScreen.waitFor();
|
|
||||||
|
|
||||||
await expect(mainScreen.noteListContainer.locator('[title^="Toggle sort order"]')).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('clicking on an external link should try to launch a browser', async ({ electronApp, mainWindow }) => {
|
test('clicking on an external link should try to launch a browser', async ({ electronApp, mainWindow }) => {
|
||||||
const mainScreen = new MainScreen(mainWindow);
|
const mainScreen = new MainScreen(mainWindow);
|
||||||
await mainScreen.waitFor();
|
await mainScreen.waitFor();
|
||||||
|
@ -17,7 +17,7 @@ export default class MainScreen {
|
|||||||
this.newNoteButton = page.locator('.new-note-button');
|
this.newNoteButton = page.locator('.new-note-button');
|
||||||
this.noteListContainer = page.locator('.rli-noteList');
|
this.noteListContainer = page.locator('.rli-noteList');
|
||||||
this.sidebar = new Sidebar(page, this);
|
this.sidebar = new Sidebar(page, this);
|
||||||
this.dialog = page.locator('.dialog-root');
|
this.dialog = page.locator('.dialog-modal-layer');
|
||||||
this.noteEditor = new NoteEditorScreen(page);
|
this.noteEditor = new NoteEditorScreen(page);
|
||||||
this.goToAnything = new GoToAnything(page, this);
|
this.goToAnything = new GoToAnything(page, this);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export default class SettingsScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getTabLocator(tabName: string) {
|
public getTabLocator(tabName: string) {
|
||||||
return this.page.locator('a[role="tab"] > span', { hasText: tabName });
|
return this.page.getByRole('tab', { name: tabName });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async waitFor() {
|
public async waitFor() {
|
||||||
|
96
packages/app-desktop/integration-tests/settings.spec.ts
Normal file
96
packages/app-desktop/integration-tests/settings.spec.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { test, expect } from './util/test';
|
||||||
|
import MainScreen from './models/MainScreen';
|
||||||
|
import SettingsScreen from './models/SettingsScreen';
|
||||||
|
|
||||||
|
test.describe('settings', () => {
|
||||||
|
test('should be possible to remove sort order buttons in settings', async ({ electronApp, mainWindow }) => {
|
||||||
|
const mainScreen = new MainScreen(mainWindow);
|
||||||
|
await mainScreen.waitFor();
|
||||||
|
|
||||||
|
// Sort order buttons should be visible by default
|
||||||
|
const sortOrderLocator = mainScreen.noteListContainer.locator('[title^="Toggle sort order"]');
|
||||||
|
await expect(sortOrderLocator).toBeVisible();
|
||||||
|
|
||||||
|
await mainScreen.openSettings(electronApp);
|
||||||
|
|
||||||
|
// Should be on the settings screen
|
||||||
|
const settingsScreen = new SettingsScreen(mainWindow);
|
||||||
|
await settingsScreen.waitFor();
|
||||||
|
|
||||||
|
// Open the appearance tab
|
||||||
|
await settingsScreen.appearanceTabButton.click();
|
||||||
|
|
||||||
|
// Find the sort order visible checkbox
|
||||||
|
const sortOrderVisibleCheckbox = mainWindow.getByLabel(/^Show sort order/);
|
||||||
|
|
||||||
|
await expect(sortOrderVisibleCheckbox).toBeChecked();
|
||||||
|
await sortOrderVisibleCheckbox.click();
|
||||||
|
await expect(sortOrderVisibleCheckbox).not.toBeChecked();
|
||||||
|
|
||||||
|
// Save settings & close
|
||||||
|
await settingsScreen.okayButton.click();
|
||||||
|
await mainScreen.waitFor();
|
||||||
|
|
||||||
|
await expect(sortOrderLocator).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking the sync wizard button in settings should open a dialog', async ({ electronApp, mainWindow }) => {
|
||||||
|
const mainScreen = new MainScreen(mainWindow);
|
||||||
|
await mainScreen.waitFor();
|
||||||
|
await mainScreen.openSettings(electronApp);
|
||||||
|
|
||||||
|
const settingsScreen = new SettingsScreen(mainWindow);
|
||||||
|
const generalTab = settingsScreen.getTabLocator('Synchronisation');
|
||||||
|
await generalTab.click();
|
||||||
|
|
||||||
|
await expect(mainScreen.dialog).not.toBeVisible();
|
||||||
|
|
||||||
|
const syncWizardButton = mainWindow.getByRole('button', { name: 'Open Sync Wizard' });
|
||||||
|
await syncWizardButton.click();
|
||||||
|
|
||||||
|
await expect(mainScreen.dialog).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be possible to navigate settings screen tabs with the arrow keys', async ({ electronApp, mainWindow }) => {
|
||||||
|
const mainScreen = new MainScreen(mainWindow);
|
||||||
|
await mainScreen.waitFor();
|
||||||
|
await mainScreen.openSettings(electronApp);
|
||||||
|
|
||||||
|
const settingsScreen = new SettingsScreen(mainWindow);
|
||||||
|
await settingsScreen.waitFor();
|
||||||
|
|
||||||
|
const generalTab = settingsScreen.getTabLocator('General');
|
||||||
|
await generalTab.click();
|
||||||
|
|
||||||
|
const focusedItem = mainWindow.locator(':focus');
|
||||||
|
|
||||||
|
// Up/Down arrows should move to the next and previous items
|
||||||
|
await expect(focusedItem).toHaveText('General');
|
||||||
|
await mainWindow.keyboard.press('ArrowDown');
|
||||||
|
await expect(focusedItem).toHaveText('Application');
|
||||||
|
await mainWindow.keyboard.press('ArrowUp');
|
||||||
|
await expect(focusedItem).toHaveText('General');
|
||||||
|
|
||||||
|
// Pressing Up when the first item is focused should focus the last item
|
||||||
|
await mainWindow.keyboard.press('ArrowUp');
|
||||||
|
await expect(focusedItem).toHaveText('Backup');
|
||||||
|
|
||||||
|
await mainWindow.keyboard.press('ArrowDown');
|
||||||
|
await mainWindow.keyboard.press('ArrowDown');
|
||||||
|
|
||||||
|
await expect(focusedItem).toHaveText('Application');
|
||||||
|
|
||||||
|
// Pressing Tab should focus the tab container
|
||||||
|
await mainWindow.keyboard.press('Tab');
|
||||||
|
await expect(focusedItem).toHaveAttribute('role', 'tabpanel');
|
||||||
|
|
||||||
|
// The correct tab should be visible
|
||||||
|
await expect(mainWindow.getByLabel('Show tray icon')).toBeVisible();
|
||||||
|
|
||||||
|
// Shift+Tab should focus the sidebar again
|
||||||
|
await mainWindow.keyboard.press('Shift+Tab');
|
||||||
|
await expect(focusedItem).toHaveAttribute('role', 'tab');
|
||||||
|
await expect(focusedItem).toHaveText('Application');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -143,7 +143,7 @@ a {
|
|||||||
|
|
||||||
|
|
||||||
*:focus-visible {
|
*:focus-visible {
|
||||||
outline: 1px solid var(--joplin-color-warn);
|
outline: 1px solid var(--joplin-focus-outline-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The browser-default focus-visible indicator was originally removed for aesthetic
|
// The browser-default focus-visible indicator was originally removed for aesthetic
|
||||||
|
@ -17,7 +17,7 @@ const logger = Logger.create('models/Setting');
|
|||||||
|
|
||||||
export * from './settings/types';
|
export * from './settings/types';
|
||||||
|
|
||||||
type SettingValueType<T extends string> = (
|
export type SettingValueType<T extends string> = (
|
||||||
T extends BuiltInMetadataKeys
|
T extends BuiltInMetadataKeys
|
||||||
? BuiltInMetadataValues[T]
|
? BuiltInMetadataValues[T]
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of old code before rule was applied
|
||||||
|
@ -110,6 +110,7 @@ export function extraStyles(theme: Theme) {
|
|||||||
const bgColor4 = theme.backgroundColor4;
|
const bgColor4 = theme.backgroundColor4;
|
||||||
const borderColor4: string = Color(theme.color).alpha(0.3);
|
const borderColor4: string = Color(theme.color).alpha(0.3);
|
||||||
const iconColor = Color(theme.color).alpha(0.8);
|
const iconColor = Color(theme.color).alpha(0.8);
|
||||||
|
const focusOutlineColor = theme.colorWarn;
|
||||||
|
|
||||||
const backgroundColor5 = theme.backgroundColor5 ?? theme.color4;
|
const backgroundColor5 = theme.backgroundColor5 ?? theme.color4;
|
||||||
const backgroundColorHover5 = Color(backgroundColor5).darken(0.2).hex();
|
const backgroundColorHover5 = Color(backgroundColor5).darken(0.2).hex();
|
||||||
@ -230,6 +231,7 @@ export function extraStyles(theme: Theme) {
|
|||||||
backgroundColor5,
|
backgroundColor5,
|
||||||
backgroundColorHover5,
|
backgroundColorHover5,
|
||||||
backgroundColorActive5,
|
backgroundColorActive5,
|
||||||
|
focusOutlineColor,
|
||||||
|
|
||||||
icon: {
|
icon: {
|
||||||
...globalStyle.icon,
|
...globalStyle.icon,
|
||||||
|
Loading…
Reference in New Issue
Block a user