mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Desktop: Accessibility: Improve keyboard navigation in the Markdown and note toolbar (#10819)
This commit is contained in:
parent
19af6a8722
commit
9cf298ef44
@ -6,6 +6,7 @@ import { connect } from 'react-redux';
|
|||||||
import { AppState } from '../../../../app.reducer';
|
import { AppState } from '../../../../app.reducer';
|
||||||
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||||
import stateToWhenClauseContext from '../../../../services/commands/stateToWhenClauseContext';
|
import stateToWhenClauseContext from '../../../../services/commands/stateToWhenClauseContext';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
const { buildStyle } = require('@joplin/lib/theme');
|
const { buildStyle } = require('@joplin/lib/theme');
|
||||||
|
|
||||||
interface ToolbarProps {
|
interface ToolbarProps {
|
||||||
@ -29,7 +30,14 @@ const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
|
|||||||
|
|
||||||
function Toolbar(props: ToolbarProps) {
|
function Toolbar(props: ToolbarProps) {
|
||||||
const styles = styles_(props);
|
const styles = styles_(props);
|
||||||
return <ToolbarBase style={styles.root} items={props.toolbarButtonInfos} disabled={!!props.disabled} />;
|
return (
|
||||||
|
<ToolbarBase
|
||||||
|
style={styles.root}
|
||||||
|
items={props.toolbarButtonInfos}
|
||||||
|
disabled={!!props.disabled}
|
||||||
|
aria-label={_('Editor actions')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
const mapStateToProps = (state: AppState) => {
|
||||||
|
@ -6,6 +6,7 @@ import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/comm
|
|||||||
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
|
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
import { buildStyle } from '@joplin/lib/theme';
|
import { buildStyle } from '@joplin/lib/theme';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
|
||||||
interface NoteToolbarProps {
|
interface NoteToolbarProps {
|
||||||
themeId: number;
|
themeId: number;
|
||||||
@ -29,7 +30,14 @@ function styles_(props: NoteToolbarProps) {
|
|||||||
|
|
||||||
function NoteToolbar(props: NoteToolbarProps) {
|
function NoteToolbar(props: NoteToolbarProps) {
|
||||||
const styles = styles_(props);
|
const styles = styles_(props);
|
||||||
return <ToolbarBase style={styles.root} items={props.toolbarButtonInfos} disabled={props.disabled}/>;
|
return (
|
||||||
|
<ToolbarBase
|
||||||
|
style={styles.root}
|
||||||
|
items={props.toolbarButtonInfos}
|
||||||
|
disabled={props.disabled}
|
||||||
|
aria-label={_('Note')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
|
const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styles_ from './styles';
|
import styles_ from './styles';
|
||||||
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
|
||||||
export enum Value {
|
export enum Value {
|
||||||
Markdown = 'markdown',
|
Markdown = 'markdown',
|
||||||
@ -11,6 +12,8 @@ export interface Props {
|
|||||||
themeId: number;
|
themeId: number;
|
||||||
value: Value;
|
value: Value;
|
||||||
toolbarButtonInfo: ToolbarButtonInfo;
|
toolbarButtonInfo: ToolbarButtonInfo;
|
||||||
|
tabIndex?: number;
|
||||||
|
buttonRef?: React.Ref<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ToggleEditorsButton(props: Props) {
|
export default function ToggleEditorsButton(props: Props) {
|
||||||
@ -18,14 +21,17 @@ export default function ToggleEditorsButton(props: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
ref={props.buttonRef}
|
||||||
style={style.button}
|
style={style.button}
|
||||||
disabled={!props.toolbarButtonInfo.enabled}
|
disabled={!props.toolbarButtonInfo.enabled}
|
||||||
aria-label={props.toolbarButtonInfo.tooltip}
|
aria-label={props.toolbarButtonInfo.tooltip}
|
||||||
|
aria-description={_('Switch to the %s Editor', props.value !== Value.Markdown ? _('Markdown') : _('Rich Text'))}
|
||||||
title={props.toolbarButtonInfo.tooltip}
|
title={props.toolbarButtonInfo.tooltip}
|
||||||
type="button"
|
type="button"
|
||||||
className={`tox-tbtn ${props.value}-active`}
|
className={`tox-tbtn ${props.value}-active`}
|
||||||
aria-pressed="false"
|
aria-pressed="false"
|
||||||
onClick={props.toolbarButtonInfo.onClick}
|
onClick={props.toolbarButtonInfo.onClick}
|
||||||
|
tabIndex={props.tabIndex}
|
||||||
>
|
>
|
||||||
<div style={style.leftInnerButton}>
|
<div style={style.leftInnerButton}>
|
||||||
<i style={style.leftIcon} className="fab fa-markdown"></i>
|
<i style={style.leftIcon} className="fab fa-markdown"></i>
|
||||||
|
@ -2,98 +2,207 @@ import * as React from 'react';
|
|||||||
import ToolbarButton from './ToolbarButton/ToolbarButton';
|
import ToolbarButton from './ToolbarButton/ToolbarButton';
|
||||||
import ToggleEditorsButton, { Value } from './ToggleEditorsButton/ToggleEditorsButton';
|
import ToggleEditorsButton, { Value } from './ToggleEditorsButton/ToggleEditorsButton';
|
||||||
import ToolbarSpace from './ToolbarSpace';
|
import ToolbarSpace from './ToolbarSpace';
|
||||||
const { connect } = require('react-redux');
|
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||||
const { themeStyle } = require('@joplin/lib/theme');
|
import { AppState } from '../app.reducer';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||||
|
|
||||||
|
interface ToolbarItemInfo extends ToolbarButtonInfo {
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
themeId: number;
|
themeId: number;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
style: React.CSSProperties;
|
||||||
style: any;
|
items: ToolbarItemInfo[];
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
|
||||||
items: any[];
|
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
'aria-label': string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
const getItemType = (item: ToolbarItemInfo) => {
|
||||||
class ToolbarBaseComponent extends React.Component<Props, any> {
|
return item.type ?? 'button';
|
||||||
|
};
|
||||||
|
|
||||||
public render() {
|
const isFocusable = (item: ToolbarItemInfo) => {
|
||||||
const theme = themeStyle(this.props.themeId);
|
if (!item.enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
return getItemType(item) === 'button';
|
||||||
const style: any = { display: 'flex',
|
};
|
||||||
flexDirection: 'row',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
backgroundColor: theme.backgroundColor3,
|
|
||||||
padding: theme.toolbarPadding,
|
|
||||||
paddingRight: theme.mainPadding, ...this.props.style };
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
const useCategorizedItems = (items: ToolbarItemInfo[]) => {
|
||||||
const groupStyle: any = {
|
return useMemo(() => {
|
||||||
display: 'flex',
|
const itemsLeft: ToolbarItemInfo[] = [];
|
||||||
flexDirection: 'row',
|
const itemsCenter: ToolbarItemInfo[] = [];
|
||||||
boxSizing: 'border-box',
|
const itemsRight: ToolbarItemInfo[] = [];
|
||||||
minWidth: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
if (items) {
|
||||||
const leftItemComps: any[] = [];
|
for (const item of items) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
const type = getItemType(item);
|
||||||
const centerItemComps: any[] = [];
|
if (item.name === 'toggleEditors') {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
itemsRight.push(item);
|
||||||
const rightItemComps: any[] = [];
|
} else if (type === 'button') {
|
||||||
|
const target = ['historyForward', 'historyBackward', 'toggleExternalEditing'].includes(item.name) ? itemsLeft : itemsCenter;
|
||||||
if (this.props.items) {
|
target.push(item);
|
||||||
for (let i = 0; i < this.props.items.length; i++) {
|
} else if (type === 'separator') {
|
||||||
const o = this.props.items[i];
|
itemsCenter.push(item);
|
||||||
let key = o.iconName ? o.iconName : '';
|
|
||||||
key += o.title ? o.title : '';
|
|
||||||
key += o.name ? o.name : '';
|
|
||||||
const itemType = !('type' in o) ? 'button' : o.type;
|
|
||||||
|
|
||||||
if (!key) key = `${o.type}_${i}`;
|
|
||||||
|
|
||||||
const props = {
|
|
||||||
key: key,
|
|
||||||
themeId: this.props.themeId,
|
|
||||||
disabled: this.props.disabled,
|
|
||||||
...o,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (o.name === 'toggleEditors') {
|
|
||||||
rightItemComps.push(<ToggleEditorsButton
|
|
||||||
key={o.name}
|
|
||||||
value={Value.Markdown}
|
|
||||||
themeId={this.props.themeId}
|
|
||||||
toolbarButtonInfo={o}
|
|
||||||
/>);
|
|
||||||
} else if (itemType === 'button') {
|
|
||||||
const target = ['historyForward', 'historyBackward', 'toggleExternalEditing'].includes(o.name) ? leftItemComps : centerItemComps;
|
|
||||||
target.push(<ToolbarButton {...props} />);
|
|
||||||
} else if (itemType === 'separator') {
|
|
||||||
centerItemComps.push(<ToolbarSpace {...props} />);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return {
|
||||||
<div className="editor-toolbar" style={style}>
|
itemsLeft,
|
||||||
<div style={groupStyle}>
|
itemsCenter,
|
||||||
{leftItemComps}
|
itemsRight,
|
||||||
</div>
|
allItems: itemsLeft.concat(itemsCenter, itemsRight),
|
||||||
<div style={groupStyle}>
|
};
|
||||||
{centerItemComps}
|
}, [items]);
|
||||||
</div>
|
};
|
||||||
<div style={{ ...groupStyle, flex: 1, justifyContent: 'flex-end' }}>
|
|
||||||
{rightItemComps}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
const useKeyboardHandler = (
|
||||||
const mapStateToProps = (state: any) => {
|
setSelectedIndex: React.Dispatch<React.SetStateAction<number>>,
|
||||||
|
focusableItems: ToolbarItemInfo[],
|
||||||
|
) => {
|
||||||
|
const onKeyDown: React.KeyboardEventHandler<HTMLElement> = useCallback(event => {
|
||||||
|
let direction = 0;
|
||||||
|
if (event.code === 'ArrowRight') {
|
||||||
|
direction = 1;
|
||||||
|
} else if (event.code === 'ArrowLeft') {
|
||||||
|
direction = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let handled = true;
|
||||||
|
if (direction !== 0) {
|
||||||
|
setSelectedIndex(index => {
|
||||||
|
let newIndex = (index + direction) % focusableItems.length;
|
||||||
|
if (newIndex < 0) {
|
||||||
|
newIndex += focusableItems.length;
|
||||||
|
}
|
||||||
|
return newIndex;
|
||||||
|
});
|
||||||
|
} else if (event.code === 'End') {
|
||||||
|
setSelectedIndex(focusableItems.length - 1);
|
||||||
|
} else if (event.code === 'Home') {
|
||||||
|
setSelectedIndex(0);
|
||||||
|
} else {
|
||||||
|
handled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handled) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}, [focusableItems, setSelectedIndex]);
|
||||||
|
|
||||||
|
return onKeyDown;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolbarBaseComponent: React.FC<Props> = props => {
|
||||||
|
const { itemsLeft, itemsCenter, itemsRight, allItems } = useCategorizedItems(props.items);
|
||||||
|
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
const focusableItems = useMemo(() => {
|
||||||
|
return allItems.filter(isFocusable);
|
||||||
|
}, [allItems]);
|
||||||
|
const containerRef = useRef<HTMLDivElement|null>(null);
|
||||||
|
const containerHasFocus = !!containerRef.current?.contains(document.activeElement);
|
||||||
|
|
||||||
|
let keyCounter = 0;
|
||||||
|
const renderItem = (o: ToolbarItemInfo, indexInFocusable: number) => {
|
||||||
|
let key = o.iconName ? o.iconName : '';
|
||||||
|
key += o.title ? o.title : '';
|
||||||
|
key += o.name ? o.name : '';
|
||||||
|
const itemType = !('type' in o) ? 'button' : o.type;
|
||||||
|
|
||||||
|
if (!key) key = `${o.type}_${keyCounter++}`;
|
||||||
|
|
||||||
|
const buttonProps = {
|
||||||
|
key,
|
||||||
|
themeId: props.themeId,
|
||||||
|
disabled: props.disabled || !o.enabled,
|
||||||
|
...o,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabIndex = indexInFocusable === (selectedIndex % focusableItems.length) ? 0 : -1;
|
||||||
|
const setButtonRefCallback = (button: HTMLButtonElement) => {
|
||||||
|
if (tabIndex === 0 && containerHasFocus) {
|
||||||
|
focus('ToolbarBase', button);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (o.name === 'toggleEditors') {
|
||||||
|
return <ToggleEditorsButton
|
||||||
|
key={o.name}
|
||||||
|
buttonRef={setButtonRefCallback}
|
||||||
|
value={Value.Markdown}
|
||||||
|
themeId={props.themeId}
|
||||||
|
toolbarButtonInfo={o}
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
/>;
|
||||||
|
} else if (itemType === 'button') {
|
||||||
|
return (
|
||||||
|
<ToolbarButton
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
buttonRef={setButtonRefCallback}
|
||||||
|
{...buttonProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (itemType === 'separator') {
|
||||||
|
return <ToolbarSpace {...buttonProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let focusableIndex = 0;
|
||||||
|
const renderList = (items: ToolbarItemInfo[]) => {
|
||||||
|
const result: React.ReactNode[] = [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
result.push(renderItem(item, focusableIndex));
|
||||||
|
if (isFocusable(item)) {
|
||||||
|
focusableIndex ++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const leftItemComps = renderList(itemsLeft);
|
||||||
|
const centerItemComps = renderList(itemsCenter);
|
||||||
|
const rightItemComps = renderList(itemsRight);
|
||||||
|
|
||||||
|
const onKeyDown = useKeyboardHandler(
|
||||||
|
setSelectedIndex,
|
||||||
|
focusableItems,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className='editor-toolbar'
|
||||||
|
style={props.style}
|
||||||
|
|
||||||
|
role='toolbar'
|
||||||
|
aria-label={props['aria-label']}
|
||||||
|
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
>
|
||||||
|
<div className='group'>
|
||||||
|
{leftItemComps}
|
||||||
|
</div>
|
||||||
|
<div className='group'>
|
||||||
|
{centerItemComps}
|
||||||
|
</div>
|
||||||
|
<div className='group -right'>
|
||||||
|
{rightItemComps}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state: AppState) => {
|
||||||
return { themeId: state.settings.theme };
|
return { themeId: state.settings.theme };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||||
import { StyledRoot, StyledIconSpan, StyledIconI } from './styles';
|
import { StyledIconSpan, StyledIconI } from './styles';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
readonly themeId: number;
|
readonly themeId: number;
|
||||||
@ -10,6 +10,9 @@ interface Props {
|
|||||||
readonly iconName?: string;
|
readonly iconName?: string;
|
||||||
readonly disabled?: boolean;
|
readonly disabled?: boolean;
|
||||||
readonly backgroundHover?: boolean;
|
readonly backgroundHover?: boolean;
|
||||||
|
readonly tabIndex?: number;
|
||||||
|
|
||||||
|
buttonRef?: React.Ref<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFontAwesomeIcon(iconName: string) {
|
function isFontAwesomeIcon(iconName: string) {
|
||||||
@ -34,7 +37,7 @@ export default function ToolbarButton(props: Props) {
|
|||||||
const iconName = getProp(props, 'iconName');
|
const iconName = getProp(props, 'iconName');
|
||||||
if (iconName) {
|
if (iconName) {
|
||||||
const IconClass = isFontAwesomeIcon(iconName) ? StyledIconI : StyledIconSpan;
|
const IconClass = isFontAwesomeIcon(iconName) ? StyledIconI : StyledIconSpan;
|
||||||
icon = <IconClass className={iconName} title={title}/>;
|
icon = <IconClass className={iconName} aria-label='' hasTitle={!!title} role='img'/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep this for legacy compatibility but for consistency we should use "disabled" prop
|
// Keep this for legacy compatibility but for consistency we should use "disabled" prop
|
||||||
@ -42,8 +45,9 @@ export default function ToolbarButton(props: Props) {
|
|||||||
if (isEnabled === null) isEnabled = true;
|
if (isEnabled === null) isEnabled = true;
|
||||||
if (props.disabled) isEnabled = false;
|
if (props.disabled) isEnabled = false;
|
||||||
|
|
||||||
const classes = ['button'];
|
const classes = ['button', 'toolbar-button'];
|
||||||
if (!isEnabled) classes.push('disabled');
|
if (!isEnabled) classes.push('disabled');
|
||||||
|
if (title) classes.push('-has-title');
|
||||||
|
|
||||||
const onClick = getProp(props, 'onClick');
|
const onClick = getProp(props, 'onClick');
|
||||||
const style: React.CSSProperties = {
|
const style: React.CSSProperties = {
|
||||||
@ -51,25 +55,27 @@ export default function ToolbarButton(props: Props) {
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis' };
|
textOverflow: 'ellipsis' };
|
||||||
const disabled = !isEnabled;
|
const disabled = !isEnabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRoot
|
<button
|
||||||
className={classes.join(' ')}
|
className={classes.join(' ')}
|
||||||
title={tooltip}
|
title={tooltip}
|
||||||
href="#"
|
|
||||||
hasTitle={!!title}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isEnabled && onClick) onClick();
|
if (isEnabled && onClick) onClick();
|
||||||
}}
|
}}
|
||||||
|
ref={props.buttonRef}
|
||||||
|
|
||||||
// At least on MacOS, the disabled HTML prop isn't sufficient for the screen reader
|
// At least on MacOS, the disabled HTML prop isn't sufficient for the screen reader
|
||||||
// to read the element as disable. For this, aria-disabled is necessary.
|
// to read the element as disable. For this, aria-disabled is necessary.
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
aria-label={!title ? tooltip : undefined}
|
||||||
|
aria-description={title ? tooltip : undefined}
|
||||||
aria-disabled={!isEnabled}
|
aria-disabled={!isEnabled}
|
||||||
role='button'
|
tabIndex={props.tabIndex}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
<span style={style}>{title}</span>
|
<span style={style}>{title}</span>
|
||||||
</StyledRoot>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,46 +3,15 @@ import { ThemeStyle } from '@joplin/lib/theme';
|
|||||||
const styled = require('styled-components').default;
|
const styled = require('styled-components').default;
|
||||||
const { css } = require('styled-components');
|
const { css } = require('styled-components');
|
||||||
|
|
||||||
interface RootProps {
|
|
||||||
readonly theme: ThemeStyle;
|
|
||||||
readonly disabled: boolean;
|
|
||||||
readonly hasTitle: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StyledRoot = styled.a<RootProps>`
|
|
||||||
opacity: ${(props: RootProps) => props.disabled ? 0.3 : 1};
|
|
||||||
height: ${(props: RootProps) => props.theme.toolbarHeight}px;
|
|
||||||
min-height: ${(props: RootProps) => props.theme.toolbarHeight}px;
|
|
||||||
width: ${(props: RootProps) => props.hasTitle ? 'auto' : `${props.theme.toolbarHeight}px`};
|
|
||||||
max-width: ${(props: RootProps) => props.hasTitle ? 'auto' : `${props.theme.toolbarHeight}px`};
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: default;
|
|
||||||
border-radius: 3px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: ${(props: RootProps) => props.theme.color3};
|
|
||||||
font-size: ${(props: RootProps) => props.theme.toolbarIconSize * 0.8}px;
|
|
||||||
padding-left: 5px;
|
|
||||||
padding-right: 5px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: ${(props: RootProps) => props.disabled ? 'none' : props.theme.backgroundColorHover3};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface IconProps {
|
interface IconProps {
|
||||||
readonly theme: ThemeStyle;
|
readonly theme: ThemeStyle;
|
||||||
readonly title: string;
|
readonly hasTitle: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iconStyle = css<IconProps>`
|
const iconStyle = css<IconProps>`
|
||||||
font-size: ${(props: IconProps) => props.theme.toolbarIconSize}px;
|
font-size: ${(props: IconProps) => props.theme.toolbarIconSize}px;
|
||||||
color: ${(props: IconProps) => props.theme.color3};
|
color: ${(props: IconProps) => props.theme.color3};
|
||||||
margin-right: ${(props: IconProps) => props.title ? 5 : 0}px;
|
margin-right: ${(props: IconProps) => props.hasTitle ? 5 : 0}px;
|
||||||
pointer-events: none; /* Need this to get button tooltip to work */
|
pointer-events: none; /* Need this to get button tooltip to work */
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
21
packages/app-desktop/gui/styles/editor-toolbar.scss
Normal file
21
packages/app-desktop/gui/styles/editor-toolbar.scss
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
.editor-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: var(--joplin-background-color3);
|
||||||
|
padding: var(--joplin-toolbar-padding);
|
||||||
|
padding-right: var(--joplin-main-padding);
|
||||||
|
|
||||||
|
> .group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
&.-right {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
@use './dialog-modal-layer.scss';
|
@use './dialog-modal-layer.scss';
|
||||||
@use './user-webview-dialog.scss';
|
@use './user-webview-dialog.scss';
|
||||||
@use './prompt-dialog.scss';
|
@use './prompt-dialog.scss';
|
||||||
|
@use './toolbar-button.scss';
|
||||||
|
@use './editor-toolbar.scss';
|
||||||
|
42
packages/app-desktop/gui/styles/toolbar-button.scss
Normal file
42
packages/app-desktop/gui/styles/toolbar-button.scss
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
|
||||||
|
.toolbar-button {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
opacity: 1;
|
||||||
|
height: var(--joplin-toolbar-height);
|
||||||
|
min-height: var(--joplin-toolbar-height);
|
||||||
|
width: var(--joplin-toolbar-height);
|
||||||
|
max-width: var(--joplin-toolbar-height);
|
||||||
|
|
||||||
|
&.-has-title {
|
||||||
|
width: auto;
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: default;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--joplin-color3);
|
||||||
|
font-size: calc(var(--joplin-toolbar-icon-size) * 0.8);
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
|
||||||
|
&:not(:disabled) {
|
||||||
|
&:hover, &:focus-visible {
|
||||||
|
background-color: var(--joplin-background-color-hover3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -26,5 +26,42 @@ test.describe('markdownEditor', () => {
|
|||||||
await expect(image).toBeAttached();
|
await expect(image).toBeAttached();
|
||||||
await expect(await getImageSourceSize(image)).toMatchObject([117, 30]);
|
await expect(await getImageSourceSize(image)).toMatchObject([117, 30]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('arrow keys should navigate the toolbar', async ({ mainWindow }) => {
|
||||||
|
const mainScreen = new MainScreen(mainWindow);
|
||||||
|
await mainScreen.waitFor();
|
||||||
|
|
||||||
|
await mainScreen.createNewNote('Note 1');
|
||||||
|
await mainScreen.createNewNote('Note 2');
|
||||||
|
const noteEditor = mainScreen.noteEditor;
|
||||||
|
await noteEditor.focusCodeMirrorEditor();
|
||||||
|
|
||||||
|
// Escape, then Shift+Tab should focus the toolbar
|
||||||
|
await mainWindow.keyboard.press('Escape');
|
||||||
|
await mainWindow.keyboard.press('Shift+Tab');
|
||||||
|
|
||||||
|
// Should focus the first item by default, the "back" arrow (back to "Note 1")
|
||||||
|
const firstItemLocator = noteEditor.toolbarButtonLocator('Back');
|
||||||
|
await expect(firstItemLocator).toBeFocused();
|
||||||
|
|
||||||
|
// Left arrow should wrap to the end
|
||||||
|
await mainWindow.keyboard.press('ArrowLeft');
|
||||||
|
const lastItemLocator = noteEditor.toolbarButtonLocator('Toggle editors');
|
||||||
|
await expect(lastItemLocator).toBeFocused();
|
||||||
|
|
||||||
|
await mainWindow.keyboard.press('ArrowRight');
|
||||||
|
await expect(firstItemLocator).toBeFocused();
|
||||||
|
|
||||||
|
// ArrowRight should skip disabled items (Forward).
|
||||||
|
await mainWindow.keyboard.press('ArrowRight');
|
||||||
|
await expect(noteEditor.toolbarButtonLocator('Toggle external editing')).toBeFocused();
|
||||||
|
|
||||||
|
// Home/end should navigate to the first/last items
|
||||||
|
await mainWindow.keyboard.press('End');
|
||||||
|
await expect(lastItemLocator).toBeFocused();
|
||||||
|
|
||||||
|
await mainWindow.keyboard.press('Home');
|
||||||
|
await expect(firstItemLocator).toBeFocused();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -14,8 +14,12 @@ export default class NoteEditorPage {
|
|||||||
this.codeMirrorEditor = this.containerLocator.locator('.cm-editor');
|
this.codeMirrorEditor = this.containerLocator.locator('.cm-editor');
|
||||||
this.richTextEditor = this.containerLocator.locator('iframe[title="Rich Text Area"]');
|
this.richTextEditor = this.containerLocator.locator('iframe[title="Rich Text Area"]');
|
||||||
this.noteTitleInput = this.containerLocator.locator('.title-input');
|
this.noteTitleInput = this.containerLocator.locator('.title-input');
|
||||||
this.attachFileButton = this.containerLocator.locator('[title^="Attach file"]');
|
this.attachFileButton = this.containerLocator.getByRole('button', { name: 'Attach file' });
|
||||||
this.toggleEditorsButton = this.containerLocator.locator('[title^="Toggle editors"]');
|
this.toggleEditorsButton = this.containerLocator.getByRole('button', { name: 'Toggle editors' });
|
||||||
|
}
|
||||||
|
|
||||||
|
public toolbarButtonLocator(title: string) {
|
||||||
|
return this.containerLocator.getByRole('button', { name: title });
|
||||||
}
|
}
|
||||||
|
|
||||||
public getNoteViewerIframe() {
|
public getNoteViewerIframe() {
|
||||||
|
@ -8,7 +8,7 @@ test.describe('settings', () => {
|
|||||||
await mainScreen.waitFor();
|
await mainScreen.waitFor();
|
||||||
|
|
||||||
// Sort order buttons should be visible by default
|
// Sort order buttons should be visible by default
|
||||||
const sortOrderLocator = mainScreen.noteListContainer.locator('[title^="Toggle sort order"]');
|
const sortOrderLocator = mainScreen.noteListContainer.getByRole('button', { name: 'Toggle sort order' });
|
||||||
await expect(sortOrderLocator).toBeVisible();
|
await expect(sortOrderLocator).toBeVisible();
|
||||||
|
|
||||||
await mainScreen.openSettings(electronApp);
|
await mainScreen.openSettings(electronApp);
|
||||||
|
Loading…
Reference in New Issue
Block a user