mirror of https://github.com/laurent22/joplin.git
Desktop: Accessibility: Improve "change application layout" screen keyboard accessibility (#11718)
parent
827233605e
commit
762daa5a68
|
@ -355,6 +355,7 @@ packages/app-desktop/gui/PasswordInput/types.js
|
|||
packages/app-desktop/gui/PdfViewer.js
|
||||
packages/app-desktop/gui/PluginNotification/PluginNotification.js
|
||||
packages/app-desktop/gui/PromptDialog.js
|
||||
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||
packages/app-desktop/gui/ResizableLayout/ResizableLayout.js
|
||||
packages/app-desktop/gui/ResizableLayout/utils/findItemByKey.js
|
||||
|
@ -500,6 +501,7 @@ packages/app-desktop/gulpfile.js
|
|||
packages/app-desktop/integration-tests/goToAnything.spec.js
|
||||
packages/app-desktop/integration-tests/main.spec.js
|
||||
packages/app-desktop/integration-tests/markdownEditor.spec.js
|
||||
packages/app-desktop/integration-tests/models/ChangeAppLayoutScreen.js
|
||||
packages/app-desktop/integration-tests/models/GoToAnything.js
|
||||
packages/app-desktop/integration-tests/models/MainScreen.js
|
||||
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
||||
|
@ -508,6 +510,7 @@ packages/app-desktop/integration-tests/models/SettingsScreen.js
|
|||
packages/app-desktop/integration-tests/models/Sidebar.js
|
||||
packages/app-desktop/integration-tests/noteList.spec.js
|
||||
packages/app-desktop/integration-tests/pluginApi.spec.js
|
||||
packages/app-desktop/integration-tests/resizableLayout.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
|
||||
|
|
|
@ -330,6 +330,7 @@ packages/app-desktop/gui/PasswordInput/types.js
|
|||
packages/app-desktop/gui/PdfViewer.js
|
||||
packages/app-desktop/gui/PluginNotification/PluginNotification.js
|
||||
packages/app-desktop/gui/PromptDialog.js
|
||||
packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.js
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||
packages/app-desktop/gui/ResizableLayout/ResizableLayout.js
|
||||
packages/app-desktop/gui/ResizableLayout/utils/findItemByKey.js
|
||||
|
@ -475,6 +476,7 @@ packages/app-desktop/gulpfile.js
|
|||
packages/app-desktop/integration-tests/goToAnything.spec.js
|
||||
packages/app-desktop/integration-tests/main.spec.js
|
||||
packages/app-desktop/integration-tests/markdownEditor.spec.js
|
||||
packages/app-desktop/integration-tests/models/ChangeAppLayoutScreen.js
|
||||
packages/app-desktop/integration-tests/models/GoToAnything.js
|
||||
packages/app-desktop/integration-tests/models/MainScreen.js
|
||||
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
||||
|
@ -483,6 +485,7 @@ packages/app-desktop/integration-tests/models/SettingsScreen.js
|
|||
packages/app-desktop/integration-tests/models/Sidebar.js
|
||||
packages/app-desktop/integration-tests/noteList.spec.js
|
||||
packages/app-desktop/integration-tests/pluginApi.spec.js
|
||||
packages/app-desktop/integration-tests/resizableLayout.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
|
||||
|
|
|
@ -18,28 +18,21 @@ export enum ButtonSize {
|
|||
Normal = 2,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
type ReactButtonProps = React.DetailedHTMLProps<React.HTMLAttributes<HTMLButtonElement>, HTMLButtonElement>;
|
||||
interface Props extends Omit<ReactButtonProps, 'onClick'> {
|
||||
title?: string;
|
||||
iconName?: string;
|
||||
level?: ButtonLevel;
|
||||
iconLabel?: string;
|
||||
className?: string;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onClick?: Function;
|
||||
onClick?: ()=> void;
|
||||
color?: string;
|
||||
iconAnimation?: string;
|
||||
tooltip?: string;
|
||||
disabled?: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied;
|
||||
style?: any;
|
||||
size?: ButtonSize;
|
||||
isSquare?: boolean;
|
||||
iconOnly?: boolean;
|
||||
fontSize?: number;
|
||||
|
||||
'aria-controls'?: string;
|
||||
'aria-describedby'?: string;
|
||||
'aria-expanded'?: string;
|
||||
}
|
||||
|
||||
const StyledTitle = styled.span`
|
||||
|
@ -216,55 +209,52 @@ function buttonClass(level: ButtonLevel) {
|
|||
return StyledButtonSecondary;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef(({
|
||||
iconName, iconLabel, iconAnimation, color, title, level, fontSize, isSquare, tooltip, disabled, onClick: propsOnClick, ...unusedProps
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied;
|
||||
const Button = React.forwardRef((props: Props, ref: any) => {
|
||||
const iconOnly = props.iconName && !props.title;
|
||||
}: Props, ref: any) => {
|
||||
const iconOnly = iconName && !title;
|
||||
|
||||
const StyledButton = buttonClass(props.level);
|
||||
const StyledButton = buttonClass(level);
|
||||
|
||||
function renderIcon() {
|
||||
if (!props.iconName) return null;
|
||||
if (!iconName) return null;
|
||||
return <StyledIcon
|
||||
aria-label={props.iconLabel ?? undefined}
|
||||
aria-hidden={!props.iconLabel}
|
||||
animation={props.iconAnimation}
|
||||
aria-label={iconLabel ?? undefined}
|
||||
aria-hidden={!iconLabel}
|
||||
animation={iconAnimation}
|
||||
mr={iconOnly ? '0' : '6px'}
|
||||
color={props.color}
|
||||
className={props.iconName}
|
||||
color={color}
|
||||
className={iconName}
|
||||
role='img'
|
||||
/>;
|
||||
}
|
||||
|
||||
function renderTitle() {
|
||||
if (!props.title) return null;
|
||||
return <StyledTitle color={props.color}>{props.title}</StyledTitle>;
|
||||
if (!title) return null;
|
||||
return <StyledTitle color={color}>{title}</StyledTitle>;
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
if (props.disabled) return;
|
||||
props.onClick();
|
||||
if (disabled) return;
|
||||
propsOnClick();
|
||||
}
|
||||
|
||||
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}
|
||||
fontSize={fontSize}
|
||||
isSquare={isSquare}
|
||||
disabled={disabled}
|
||||
title={tooltip}
|
||||
iconOnly={iconOnly}
|
||||
onClick={onClick}
|
||||
|
||||
// When there's no title, the button needs a label. In this case, fall back
|
||||
// to the tooltip.
|
||||
aria-label={props.title ? undefined : props.tooltip}
|
||||
aria-disabled={props.disabled}
|
||||
aria-expanded={props['aria-expanded']}
|
||||
aria-controls={props['aria-controls']}
|
||||
aria-describedby={props['aria-describedby']}
|
||||
aria-label={title ? undefined : tooltip}
|
||||
aria-disabled={disabled}
|
||||
{...unusedProps}
|
||||
>
|
||||
{renderIcon()}
|
||||
{renderTitle()}
|
||||
|
|
|
@ -43,6 +43,7 @@ import UpdateNotification from './UpdateNotification/UpdateNotification';
|
|||
import NoteEditor from './NoteEditor/NoteEditor';
|
||||
import PluginNotification from './PluginNotification/PluginNotification';
|
||||
import { Toast } from '@joplin/lib/services/plugins/api/types';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
|
||||
|
@ -121,6 +122,18 @@ const defaultLayout: LayoutItem = {
|
|||
],
|
||||
};
|
||||
|
||||
const layoutKeyToLabel = (key: string, plugins: PluginStates) => {
|
||||
if (key === 'sideBar') return _('Sidebar');
|
||||
if (key === 'noteList') return _('Note list');
|
||||
if (key === 'editor') return _('Editor');
|
||||
|
||||
const viewInfo = pluginUtils.viewInfoByViewId(plugins, key);
|
||||
if (viewInfo) {
|
||||
return PluginService.instance().safePluginNameById(viewInfo.plugin.id);
|
||||
}
|
||||
return key;
|
||||
};
|
||||
|
||||
class MainScreenComponent extends React.Component<Props, State> {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
|
@ -728,6 +741,10 @@ class MainScreenComponent extends React.Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
private layoutKeyToLabel = (key: string) => {
|
||||
return layoutKeyToLabel(key, this.props.plugins);
|
||||
};
|
||||
|
||||
public render() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const style = {
|
||||
|
@ -746,6 +763,7 @@ class MainScreenComponent extends React.Component<Props, State> {
|
|||
onResize={this.resizableLayout_resize}
|
||||
onMoveButtonClick={this.resizableLayout_moveButtonClick}
|
||||
renderItem={this.resizableLayout_renderItem}
|
||||
layoutKeyToLabel={this.layoutKeyToLabel}
|
||||
moveMode={this.props.layoutMoveMode}
|
||||
moveModeMessage={_('Use the arrows to move the layout items. Press "Escape" to exit.')}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import * as React from 'react';
|
||||
import { Resizable, ResizeCallback, ResizeStartCallback, Size } from 're-resizable';
|
||||
import { LayoutItem } from './utils/types';
|
||||
import { itemMinHeight, itemMinWidth, itemSize, LayoutItemSizes } from './utils/useLayoutItemSizes';
|
||||
|
||||
interface Props {
|
||||
item: LayoutItem;
|
||||
parent: LayoutItem|null;
|
||||
sizes: LayoutItemSizes;
|
||||
resizedItemMaxSize: Size|null;
|
||||
onResizeStart: ResizeStartCallback;
|
||||
onResize: ResizeCallback;
|
||||
onResizeStop: ResizeCallback;
|
||||
children: React.ReactNode;
|
||||
isLastChild: boolean;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const LayoutItemContainer: React.FC<Props> = ({
|
||||
item, visible, parent, sizes, resizedItemMaxSize, onResize, onResizeStart, onResizeStop, children, isLastChild,
|
||||
}) => {
|
||||
const style: React.CSSProperties = {
|
||||
display: visible ? 'flex' : 'none',
|
||||
flexDirection: item.direction,
|
||||
};
|
||||
|
||||
const size: Size = itemSize(item, parent, sizes, true);
|
||||
|
||||
const className = `resizableLayoutItem rli-${item.key}`;
|
||||
if (item.resizableRight || item.resizableBottom) {
|
||||
const enable = {
|
||||
top: false,
|
||||
right: !!item.resizableRight && !isLastChild,
|
||||
bottom: !!item.resizableBottom && !isLastChild,
|
||||
left: false,
|
||||
topRight: false,
|
||||
bottomRight: false,
|
||||
bottomLeft: false,
|
||||
topLeft: false,
|
||||
};
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
key={item.key}
|
||||
className={className}
|
||||
style={style}
|
||||
size={size}
|
||||
onResizeStart={onResizeStart}
|
||||
onResize={onResize}
|
||||
onResizeStop={onResizeStop}
|
||||
enable={enable}
|
||||
minWidth={'minWidth' in item ? item.minWidth : itemMinWidth}
|
||||
minHeight={'minHeight' in item ? item.minHeight : itemMinHeight}
|
||||
maxWidth={resizedItemMaxSize?.width}
|
||||
maxHeight={resizedItemMaxSize?.height}
|
||||
>
|
||||
{children}
|
||||
</Resizable>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={item.key} className={className} style={{ ...style, ...size }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default LayoutItemContainer;
|
|
@ -1,8 +1,9 @@
|
|||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useId } from 'react';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import { MoveDirection } from './utils/movements';
|
||||
import styled from 'styled-components';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
const StyledRoot = styled.div`
|
||||
display: flex;
|
||||
|
@ -10,6 +11,11 @@ const StyledRoot = styled.div`
|
|||
padding: 5px;
|
||||
background-color: ${props => props.theme.backgroundColor};
|
||||
border-radius: 5px;
|
||||
|
||||
> .label {
|
||||
// Used only for accessibility tools
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const ButtonRow = styled.div`
|
||||
|
@ -26,23 +32,32 @@ const ArrowButton = styled(Button)`
|
|||
opacity: ${props => props.disabled ? 0.2 : 1};
|
||||
`;
|
||||
|
||||
type ButtonKey = string;
|
||||
|
||||
export interface MoveButtonClickEvent {
|
||||
direction: MoveDirection;
|
||||
itemKey: string;
|
||||
buttonKey: ButtonKey;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onClick(event: MoveButtonClickEvent): void;
|
||||
itemKey: string;
|
||||
itemLabel: string;
|
||||
canMoveLeft: boolean;
|
||||
canMoveRight: boolean;
|
||||
canMoveUp: boolean;
|
||||
canMoveDown: boolean;
|
||||
|
||||
// Specifies which button to auto-focus (if any). Clicking a "Move ..." button changes the app's layout. By default, this
|
||||
// causes focus to jump to the start of the move dialog. Providing the key of the last-clicked button allows focus
|
||||
// to be restored after changing the app layout:
|
||||
autoFocusKey: ButtonKey|null;
|
||||
}
|
||||
|
||||
export default function MoveButtons(props: Props) {
|
||||
const onButtonClick = useCallback((direction: MoveDirection) => {
|
||||
props.onClick({ direction, itemKey: props.itemKey });
|
||||
props.onClick({ direction, itemKey: props.itemKey, buttonKey: `${props.itemKey}-${direction}` });
|
||||
}, [props.onClick, props.itemKey]);
|
||||
|
||||
function canMove(dir: MoveDirection) {
|
||||
|
@ -53,28 +68,64 @@ export default function MoveButtons(props: Props) {
|
|||
throw new Error('Unreachable');
|
||||
}
|
||||
|
||||
const iconLabel = (dir: MoveDirection) => {
|
||||
if (dir === MoveDirection.Up) return _('Move up');
|
||||
if (dir === MoveDirection.Down) return _('Move down');
|
||||
if (dir === MoveDirection.Left) return _('Move left');
|
||||
if (dir === MoveDirection.Right) return _('Move right');
|
||||
const unreachable: never = dir;
|
||||
throw new Error(`Invalid direction: ${unreachable}`);
|
||||
};
|
||||
|
||||
const descriptionId = useId();
|
||||
|
||||
const buttonKey = (dir: MoveDirection) => `${props.itemKey}-${dir}`;
|
||||
const autoFocusDirection = (() => {
|
||||
if (!props.autoFocusKey) return undefined;
|
||||
|
||||
const buttonDirections = [MoveDirection.Up, MoveDirection.Down, MoveDirection.Left, MoveDirection.Right];
|
||||
const autoFocusDirection = buttonDirections.find(
|
||||
direction => buttonKey(direction) === props.autoFocusKey,
|
||||
);
|
||||
|
||||
if (!autoFocusDirection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const autoFocusDirectionEnabled = autoFocusDirection && canMove(autoFocusDirection);
|
||||
if (autoFocusDirectionEnabled) {
|
||||
return autoFocusDirection;
|
||||
} else {
|
||||
// Select an enabled direction instead
|
||||
return buttonDirections.find(dir => canMove(dir));
|
||||
}
|
||||
})();
|
||||
|
||||
function renderButton(dir: MoveDirection) {
|
||||
return <ArrowButton
|
||||
disabled={!canMove(dir)}
|
||||
level={ButtonLevel.Primary}
|
||||
iconName={`fas fa-arrow-${dir}`}
|
||||
iconLabel={iconLabel(dir)}
|
||||
autoFocus={autoFocusDirection === dir}
|
||||
onClick={() => onButtonClick(dir)}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledRoot>
|
||||
<StyledRoot role='group' aria-labelledby={descriptionId}>
|
||||
<ButtonRow>
|
||||
{renderButton(MoveDirection.Up)}
|
||||
</ButtonRow>
|
||||
<ButtonRow>
|
||||
{renderButton(MoveDirection.Left)}
|
||||
<EmptyButton iconName="fas fa-arrow-down" disabled={true}/>
|
||||
<EmptyButton iconName="fas fa-arrow-down" aria-hidden={true} disabled={true}/>
|
||||
{renderButton(MoveDirection.Right)}
|
||||
</ButtonRow>
|
||||
<ButtonRow>
|
||||
{renderButton(MoveDirection.Down)}
|
||||
</ButtonRow>
|
||||
<div className='label' id={descriptionId}>{props.itemLabel}</div>
|
||||
</StyledRoot>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
import { useRef, useState, useEffect, useCallback } from 'react';
|
||||
import useWindowResizeEvent from './utils/useWindowResizeEvent';
|
||||
import setLayoutItemProps from './utils/setLayoutItemProps';
|
||||
import useLayoutItemSizes, { LayoutItemSizes, itemSize, calculateMaxSizeAvailableForItem, itemMinWidth, itemMinHeight } from './utils/useLayoutItemSizes';
|
||||
|
@ -7,16 +7,26 @@ import validateLayout from './utils/validateLayout';
|
|||
import { Size, LayoutItem } from './utils/types';
|
||||
import { canMove, MoveDirection } from './utils/movements';
|
||||
import MoveButtons, { MoveButtonClickEvent } from './MoveButtons';
|
||||
import { StyledWrapperRoot, StyledMoveOverlay, MoveModeRootWrapper, MoveModeRootMessage } from './utils/style';
|
||||
import { Resizable } from 're-resizable';
|
||||
const EventEmitter = require('events');
|
||||
import { StyledWrapperRoot, StyledMoveOverlay, MoveModeRootMessage } from './utils/style';
|
||||
import type { ResizeCallback, ResizeStartCallback } from 're-resizable';
|
||||
import Dialog from '../Dialog';
|
||||
import * as EventEmitter from 'events';
|
||||
import LayoutItemContainer from './LayoutItemContainer';
|
||||
|
||||
interface OnResizeEvent {
|
||||
layout: LayoutItem;
|
||||
}
|
||||
|
||||
interface ResizedItem {
|
||||
key: string;
|
||||
initialWidth: number;
|
||||
initialHeight: number;
|
||||
maxSize: Size;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
layout: LayoutItem;
|
||||
layoutKeyToLabel: (key: string)=> string;
|
||||
onResize(event: OnResizeEvent): void;
|
||||
width?: number;
|
||||
height?: number;
|
||||
|
@ -33,101 +43,57 @@ function itemVisible(item: LayoutItem, moveMode: boolean) {
|
|||
return item.visible !== false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
|
||||
function renderContainer(item: LayoutItem, parent: LayoutItem | null, sizes: LayoutItemSizes, resizedItemMaxSize: Size | null, onResizeStart: Function, onResize: Function, onResizeStop: Function, children: any[], isLastChild: boolean, moveMode: boolean): any {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const style: any = {
|
||||
display: itemVisible(item, moveMode) ? 'flex' : 'none',
|
||||
flexDirection: item.direction,
|
||||
};
|
||||
|
||||
const size: Size = itemSize(item, parent, sizes, true);
|
||||
|
||||
const className = `resizableLayoutItem rli-${item.key}`;
|
||||
if (item.resizableRight || item.resizableBottom) {
|
||||
const enable = {
|
||||
top: false,
|
||||
right: !!item.resizableRight && !isLastChild,
|
||||
bottom: !!item.resizableBottom && !isLastChild,
|
||||
left: false,
|
||||
topRight: false,
|
||||
bottomRight: false,
|
||||
bottomLeft: false,
|
||||
topLeft: false,
|
||||
};
|
||||
|
||||
return (
|
||||
<Resizable
|
||||
key={item.key}
|
||||
className={className}
|
||||
style={style}
|
||||
size={size}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onResizeStart={onResizeStart as any}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onResize={onResize as any}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onResizeStop={onResizeStop as any}
|
||||
enable={enable}
|
||||
minWidth={'minWidth' in item ? item.minWidth : itemMinWidth}
|
||||
minHeight={'minHeight' in item ? item.minHeight : itemMinHeight}
|
||||
maxWidth={resizedItemMaxSize?.width}
|
||||
maxHeight={resizedItemMaxSize?.height}
|
||||
>
|
||||
{children}
|
||||
</Resizable>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={item.key} className={className} style={{ ...style, ...size }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function ResizableLayout(props: Props) {
|
||||
const eventEmitter = useRef(new EventEmitter());
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const [resizedItem, setResizedItem] = useState<any>(null);
|
||||
const [resizedItem, setResizedItem] = useState<ResizedItem|null>(null);
|
||||
const lastUsedMoveButtonKey = useRef<string|null>(null);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function renderItemWrapper(comp: any, item: LayoutItem, parent: LayoutItem | null, size: Size, moveMode: boolean) {
|
||||
const moveOverlay = moveMode ? (
|
||||
<StyledMoveOverlay>
|
||||
<MoveButtons
|
||||
itemKey={item.key}
|
||||
onClick={props.onMoveButtonClick}
|
||||
canMoveLeft={canMove(MoveDirection.Left, item, parent)}
|
||||
canMoveRight={canMove(MoveDirection.Right, item, parent)}
|
||||
canMoveUp={canMove(MoveDirection.Up, item, parent)}
|
||||
canMoveDown={canMove(MoveDirection.Down, item, parent)}
|
||||
/>
|
||||
</StyledMoveOverlay>
|
||||
) : null;
|
||||
const onMoveButtonClick = useCallback((event: MoveButtonClickEvent) => {
|
||||
lastUsedMoveButtonKey.current = event.buttonKey;
|
||||
props.onMoveButtonClick(event);
|
||||
}, [props.onMoveButtonClick]);
|
||||
|
||||
const renderMoveControls = (item: LayoutItem, parent: LayoutItem | null, size: Size) => {
|
||||
return (
|
||||
<StyledWrapperRoot key={item.key} size={size}>
|
||||
<StyledMoveOverlay>
|
||||
<MoveButtons
|
||||
autoFocusKey={lastUsedMoveButtonKey.current}
|
||||
itemKey={item.key}
|
||||
itemLabel={props.layoutKeyToLabel(item.key)}
|
||||
onClick={onMoveButtonClick}
|
||||
canMoveLeft={canMove(MoveDirection.Left, item, parent)}
|
||||
canMoveRight={canMove(MoveDirection.Right, item, parent)}
|
||||
canMoveUp={canMove(MoveDirection.Up, item, parent)}
|
||||
canMoveDown={canMove(MoveDirection.Down, item, parent)}
|
||||
/>
|
||||
</StyledMoveOverlay>
|
||||
</StyledWrapperRoot>
|
||||
);
|
||||
};
|
||||
|
||||
function renderItemWrapper(comp: React.ReactNode, item: LayoutItem, size: Size) {
|
||||
return (
|
||||
<StyledWrapperRoot key={item.key} size={size}>
|
||||
{moveOverlay}
|
||||
{comp}
|
||||
</StyledWrapperRoot>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function renderLayoutItem(item: LayoutItem, parent: LayoutItem | null, sizes: LayoutItemSizes, isVisible: boolean, isLastChild: boolean): any {
|
||||
function onResizeStart() {
|
||||
function renderLayoutItem(
|
||||
item: LayoutItem, parent: LayoutItem | null, sizes: LayoutItemSizes, isVisible: boolean, isLastChild: boolean, onlyMoveControls: boolean,
|
||||
): React.ReactNode {
|
||||
const onResizeStart: ResizeStartCallback = () => {
|
||||
setResizedItem({
|
||||
key: item.key,
|
||||
initialWidth: sizes[item.key].width,
|
||||
initialHeight: sizes[item.key].height,
|
||||
maxSize: calculateMaxSizeAvailableForItem(item, parent, sizes),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function onResize(_event: any, direction: string, _refToElement: any, delta: any) {
|
||||
const onResize: ResizeCallback = (_event, direction, _refToElement, delta) => {
|
||||
const newWidth = Math.max(itemMinWidth, resizedItem.initialWidth + delta.width);
|
||||
const newHeight = Math.max(itemMinHeight, resizedItem.initialHeight + delta.height);
|
||||
|
||||
|
@ -147,15 +113,18 @@ function ResizableLayout(props: Props) {
|
|||
|
||||
props.onResize({ layout: newLayout });
|
||||
eventEmitter.current.emit('resize');
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function onResizeStop(_event: any, _direction: any, _refToElement: any, delta: any) {
|
||||
const onResizeStop: ResizeCallback = (_event, _direction, _refToElement, delta) => {
|
||||
onResize(_event, _direction, _refToElement, delta);
|
||||
setResizedItem(null);
|
||||
}
|
||||
};
|
||||
|
||||
const resizedItemMaxSize = resizedItem && item.key === resizedItem.key ? resizedItem.maxSize : null;
|
||||
const visible = itemVisible(item, props.moveMode);
|
||||
const itemContainerProps = {
|
||||
key: item.key, item, parent, sizes, resizedItemMaxSize, onResizeStart, onResizeStop, onResize, isLastChild, visible,
|
||||
};
|
||||
if (!item.children) {
|
||||
const size = itemSize(item, parent, sizes, false);
|
||||
|
||||
|
@ -166,17 +135,22 @@ function ResizableLayout(props: Props) {
|
|||
visible: isVisible,
|
||||
});
|
||||
|
||||
const wrapper = renderItemWrapper(comp, item, parent, size, props.moveMode);
|
||||
|
||||
return renderContainer(item, parent, sizes, resizedItemMaxSize, onResizeStart, onResize, onResizeStop, [wrapper], isLastChild, props.moveMode);
|
||||
const wrapper = onlyMoveControls ? renderMoveControls(item, parent, size) : renderItemWrapper(comp, item, size);
|
||||
return <LayoutItemContainer {...itemContainerProps}>
|
||||
{wrapper}
|
||||
</LayoutItemContainer>;
|
||||
} else {
|
||||
const childrenComponents = [];
|
||||
for (let i = 0; i < item.children.length; i++) {
|
||||
const child = item.children[i];
|
||||
childrenComponents.push(renderLayoutItem(child, item, sizes, isVisible && itemVisible(child, props.moveMode), i === item.children.length - 1));
|
||||
childrenComponents.push(
|
||||
renderLayoutItem(child, item, sizes, isVisible && itemVisible(child, props.moveMode), i === item.children.length - 1, onlyMoveControls),
|
||||
);
|
||||
}
|
||||
|
||||
return renderContainer(item, parent, sizes, resizedItemMaxSize, onResizeStart, onResize, onResizeStop, childrenComponents, isLastChild, props.moveMode);
|
||||
return <LayoutItemContainer {...itemContainerProps}>
|
||||
{childrenComponents}
|
||||
</LayoutItemContainer>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -187,22 +161,24 @@ function ResizableLayout(props: Props) {
|
|||
useWindowResizeEvent(eventEmitter);
|
||||
const sizes = useLayoutItemSizes(props.layout, props.moveMode);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function renderMoveModeBox(rootComp: any) {
|
||||
return (
|
||||
<MoveModeRootWrapper>
|
||||
const renderRoot = (moveControlsOnly: boolean) => {
|
||||
return renderLayoutItem(props.layout, null, sizes, itemVisible(props.layout, props.moveMode), true, moveControlsOnly);
|
||||
};
|
||||
|
||||
function renderMoveModeBox() {
|
||||
return <div>
|
||||
<Dialog contentFillsScreen={true} className='change-app-layout-dialog'>
|
||||
<MoveModeRootMessage>{props.moveModeMessage}</MoveModeRootMessage>
|
||||
{rootComp}
|
||||
</MoveModeRootWrapper>
|
||||
);
|
||||
{renderRoot(true)}
|
||||
</Dialog>
|
||||
{renderRoot(false)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
const rootComp = renderLayoutItem(props.layout, null, sizes, itemVisible(props.layout, props.moveMode), true);
|
||||
|
||||
if (props.moveMode) {
|
||||
return renderMoveModeBox(rootComp);
|
||||
return renderMoveModeBox();
|
||||
} else {
|
||||
return rootComp;
|
||||
return renderRoot(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,18 +28,12 @@ export const StyledMoveOverlay = styled.div`
|
|||
height: 100%;
|
||||
`;
|
||||
|
||||
export const MoveModeRootWrapper = styled.div`
|
||||
position:relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const MoveModeRootMessage = styled.div`
|
||||
position:absolute;
|
||||
export const MoveModeRootMessage = styled.h1`
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
font-size: 1em;
|
||||
|
||||
z-index:200;
|
||||
background-color: ${props => props.theme.backgroundColor};
|
||||
padding: 10px;
|
||||
border-radius: 5;
|
||||
`;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import dialogs from '../../dialogs';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'resetLayout',
|
||||
|
@ -12,7 +12,7 @@ export const runtime = (): CommandRuntime => {
|
|||
execute: async (context: CommandContext) => {
|
||||
|
||||
const message = _('Are you sure you want to return to the default layout? The current layout configuration will be lost.');
|
||||
const isConfirmed = await dialogs.confirm(message);
|
||||
const isConfirmed = await shim.showConfirmationDialog(message);
|
||||
|
||||
if (!isConfirmed) return;
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
.change-app-layout-dialog {
|
||||
padding: 0;
|
||||
|
||||
> .content {
|
||||
position:relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&::backdrop {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
|
@ -32,13 +32,16 @@
|
|||
}
|
||||
|
||||
&.-fullscreen {
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
|
||||
&::backdrop {
|
||||
background-color: var(--joplin-background-color);
|
||||
}
|
||||
|
||||
> .content {
|
||||
width: calc(100% - 20px);
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
|
|
|
@ -11,3 +11,4 @@
|
|||
@use './dialog-anchor-node.scss';
|
||||
@use './note-editor-wrapper.scss';
|
||||
@use './text-input.scss';
|
||||
@use './change-app-layout-dialog.scss';
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
|
||||
import { ElectronApplication, Locator, Page } from '@playwright/test';
|
||||
import MainScreen from './MainScreen';
|
||||
import activateMainMenuItem from '../util/activateMainMenuItem';
|
||||
|
||||
export default class ChangeAppLayoutScreen {
|
||||
public readonly containerLocator: Locator;
|
||||
|
||||
public constructor(page: Page, private readonly mainScreen: MainScreen) {
|
||||
this.containerLocator = page.locator('.change-app-layout-dialog[open]');
|
||||
}
|
||||
|
||||
public async open(electronApp: ElectronApplication) {
|
||||
await this.mainScreen.waitFor();
|
||||
await activateMainMenuItem(electronApp, 'Change application layout');
|
||||
|
||||
return this.waitFor();
|
||||
}
|
||||
|
||||
public async waitFor() {
|
||||
await this.containerLocator.waitFor();
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import GoToAnything from './GoToAnything';
|
|||
import setFilePickerResponse from '../util/setFilePickerResponse';
|
||||
import NoteList from './NoteList';
|
||||
import { expect } from '../util/test';
|
||||
import ChangeAppLayoutScreen from './ChangeAppLayoutScreen';
|
||||
|
||||
export default class MainScreen {
|
||||
public readonly newNoteButton: Locator;
|
||||
|
@ -14,6 +15,7 @@ export default class MainScreen {
|
|||
public readonly dialog: Locator;
|
||||
public readonly noteEditor: NoteEditorScreen;
|
||||
public readonly goToAnything: GoToAnything;
|
||||
public readonly changeLayoutScreen: ChangeAppLayoutScreen;
|
||||
|
||||
public constructor(private page: Page) {
|
||||
this.newNoteButton = page.locator('.new-note-button');
|
||||
|
@ -22,6 +24,7 @@ export default class MainScreen {
|
|||
this.dialog = page.locator('.dialog-modal-layer');
|
||||
this.noteEditor = new NoteEditorScreen(page);
|
||||
this.goToAnything = new GoToAnything(page, this);
|
||||
this.changeLayoutScreen = new ChangeAppLayoutScreen(page, this);
|
||||
}
|
||||
|
||||
public async setup() {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
import { test, expect } from './util/test';
|
||||
import MainScreen from './models/MainScreen';
|
||||
|
||||
test.describe('resizableLayout', () => {
|
||||
test('right/left buttons should retain keyboard focus after use', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
const changeLayoutScreen = mainScreen.changeLayoutScreen;
|
||||
await changeLayoutScreen.open(electronApp);
|
||||
|
||||
const moveSidebarControls = changeLayoutScreen.containerLocator.getByRole('group', { name: 'Sidebar' });
|
||||
const moveSidebarRight = moveSidebarControls.getByRole('button', { name: 'Move right' });
|
||||
|
||||
await expect(moveSidebarRight).not.toBeDisabled();
|
||||
|
||||
// Should refocus (or keep focused) after clicking
|
||||
await moveSidebarRight.click();
|
||||
await expect(moveSidebarRight).toBeFocused();
|
||||
await moveSidebarRight.click();
|
||||
await expect(moveSidebarRight).toBeFocused();
|
||||
});
|
||||
});
|
|
@ -67,5 +67,11 @@ test.describe('wcag', () => {
|
|||
|
||||
await expectNoViolations(mainWindow);
|
||||
});
|
||||
|
||||
test('should not detect significant issues in the change app layout screen', async ({ mainWindow, electronApp }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.changeLayoutScreen.open(electronApp);
|
||||
await expectNoViolations(mainWindow);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -203,6 +203,14 @@ export default class PluginService extends BaseService {
|
|||
return this.plugins_[id];
|
||||
}
|
||||
|
||||
public safePluginNameById(id: string) {
|
||||
if (!this.plugins_[id]) {
|
||||
return id;
|
||||
}
|
||||
|
||||
return this.pluginById(id).manifest?.name ?? 'Unknown';
|
||||
}
|
||||
|
||||
public viewControllerByViewId(id: string): ViewController|null {
|
||||
for (const [, plugin] of Object.entries(this.plugins_)) {
|
||||
if (plugin.hasViewController(id)) return plugin.viewController(id);
|
||||
|
|
Loading…
Reference in New Issue