diff --git a/.eslintignore b/.eslintignore index 569cda587dc..6f4b65cfa3b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -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 diff --git a/.gitignore b/.gitignore index 8145c35252a..5aeb7099fe7 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/packages/app-desktop/gui/Button/Button.tsx b/packages/app-desktop/gui/Button/Button.tsx index 58f4f7ec1b9..26d844da1f5 100644 --- a/packages/app-desktop/gui/Button/Button.tsx +++ b/packages/app-desktop/gui/Button/Button.tsx @@ -18,28 +18,21 @@ export enum ButtonSize { Normal = 2, } -interface Props { +type ReactButtonProps = React.DetailedHTMLProps, HTMLButtonElement>; +interface Props extends Omit { 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 ; } function renderTitle() { - if (!props.title) return null; - return {props.title}; + if (!title) return null; + return {title}; } function onClick() { - if (props.disabled) return; - props.onClick(); + if (disabled) return; + propsOnClick(); } return ( {renderIcon()} {renderTitle()} diff --git a/packages/app-desktop/gui/MainScreen.tsx b/packages/app-desktop/gui/MainScreen.tsx index 195c689e9c5..63a0b35a7d3 100644 --- a/packages/app-desktop/gui/MainScreen.tsx +++ b/packages/app-desktop/gui/MainScreen.tsx @@ -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 { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied @@ -728,6 +741,10 @@ class MainScreenComponent extends React.Component { ); } + 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 { 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.')} /> diff --git a/packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.tsx b/packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.tsx new file mode 100644 index 00000000000..0012d387531 --- /dev/null +++ b/packages/app-desktop/gui/ResizableLayout/LayoutItemContainer.tsx @@ -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 = ({ + 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 ( + + {children} + + ); + } else { + return ( +
+ {children} +
+ ); + } +}; + +export default LayoutItemContainer; diff --git a/packages/app-desktop/gui/ResizableLayout/MoveButtons.tsx b/packages/app-desktop/gui/ResizableLayout/MoveButtons.tsx index 26905c67fd6..515040f8416 100644 --- a/packages/app-desktop/gui/ResizableLayout/MoveButtons.tsx +++ b/packages/app-desktop/gui/ResizableLayout/MoveButtons.tsx @@ -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 onButtonClick(dir)} />; } return ( - + {renderButton(MoveDirection.Up)} {renderButton(MoveDirection.Left)} - + {renderButton(MoveDirection.Right)} {renderButton(MoveDirection.Down)} +
{props.itemLabel}
); } diff --git a/packages/app-desktop/gui/ResizableLayout/ResizableLayout.tsx b/packages/app-desktop/gui/ResizableLayout/ResizableLayout.tsx index 79d1d7d2688..6f964696981 100644 --- a/packages/app-desktop/gui/ResizableLayout/ResizableLayout.tsx +++ b/packages/app-desktop/gui/ResizableLayout/ResizableLayout.tsx @@ -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, - }; +function ResizableLayout(props: Props) { + const eventEmitter = useRef(new EventEmitter()); - 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, - }; + const [resizedItem, setResizedItem] = useState(null); + const lastUsedMoveButtonKey = useRef(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 ( - - {children} - - ); - } else { - return ( -
- {children} -
+ + + + + ); - } -} - -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(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 ? ( - - - - ) : null; + }; + function renderItemWrapper(comp: React.ReactNode, item: LayoutItem, size: Size) { return ( - {moveOverlay} {comp} ); } - // 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 + {wrapper} + ; } 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 + {childrenComponents} + ; } } @@ -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 ( - + const renderRoot = (moveControlsOnly: boolean) => { + return renderLayoutItem(props.layout, null, sizes, itemVisible(props.layout, props.moveMode), true, moveControlsOnly); + }; + + function renderMoveModeBox() { + return
+ {props.moveModeMessage} - {rootComp} - - ); + {renderRoot(true)} + + {renderRoot(false)} +
; } - 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); } } diff --git a/packages/app-desktop/gui/ResizableLayout/utils/style.ts b/packages/app-desktop/gui/ResizableLayout/utils/style.ts index 5637cbd6ee8..fabc5e83417 100644 --- a/packages/app-desktop/gui/ResizableLayout/utils/style.ts +++ b/packages/app-desktop/gui/ResizableLayout/utils/style.ts @@ -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; `; diff --git a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/resetLayout.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/resetLayout.ts index 6bb2f6002fa..950822793b9 100644 --- a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/resetLayout.ts +++ b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/resetLayout.ts @@ -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; diff --git a/packages/app-desktop/gui/styles/change-app-layout-dialog.scss b/packages/app-desktop/gui/styles/change-app-layout-dialog.scss new file mode 100644 index 00000000000..5b445c1838d --- /dev/null +++ b/packages/app-desktop/gui/styles/change-app-layout-dialog.scss @@ -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; + } +} \ No newline at end of file diff --git a/packages/app-desktop/gui/styles/dialog-modal-layer.scss b/packages/app-desktop/gui/styles/dialog-modal-layer.scss index d8c81838cd3..9b5bdd8a894 100644 --- a/packages/app-desktop/gui/styles/dialog-modal-layer.scss +++ b/packages/app-desktop/gui/styles/dialog-modal-layer.scss @@ -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; diff --git a/packages/app-desktop/gui/styles/index.scss b/packages/app-desktop/gui/styles/index.scss index 0217913a6bb..e21bc051199 100644 --- a/packages/app-desktop/gui/styles/index.scss +++ b/packages/app-desktop/gui/styles/index.scss @@ -11,3 +11,4 @@ @use './dialog-anchor-node.scss'; @use './note-editor-wrapper.scss'; @use './text-input.scss'; +@use './change-app-layout-dialog.scss'; diff --git a/packages/app-desktop/integration-tests/models/ChangeAppLayoutScreen.ts b/packages/app-desktop/integration-tests/models/ChangeAppLayoutScreen.ts new file mode 100644 index 00000000000..681bd9a123e --- /dev/null +++ b/packages/app-desktop/integration-tests/models/ChangeAppLayoutScreen.ts @@ -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(); + } +} diff --git a/packages/app-desktop/integration-tests/models/MainScreen.ts b/packages/app-desktop/integration-tests/models/MainScreen.ts index 56f86a10e9a..f0fd733f390 100644 --- a/packages/app-desktop/integration-tests/models/MainScreen.ts +++ b/packages/app-desktop/integration-tests/models/MainScreen.ts @@ -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() { diff --git a/packages/app-desktop/integration-tests/resizableLayout.spec.ts b/packages/app-desktop/integration-tests/resizableLayout.spec.ts new file mode 100644 index 00000000000..d448bcad230 --- /dev/null +++ b/packages/app-desktop/integration-tests/resizableLayout.spec.ts @@ -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(); + }); +}); diff --git a/packages/app-desktop/integration-tests/wcag.spec.ts b/packages/app-desktop/integration-tests/wcag.spec.ts index 600c8c5aeb1..3f851dcf32e 100644 --- a/packages/app-desktop/integration-tests/wcag.spec.ts +++ b/packages/app-desktop/integration-tests/wcag.spec.ts @@ -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); + }); }); diff --git a/packages/lib/services/plugins/PluginService.ts b/packages/lib/services/plugins/PluginService.ts index 1b9338279a7..d2cf81f1113 100644 --- a/packages/lib/services/plugins/PluginService.ts +++ b/packages/lib/services/plugins/PluginService.ts @@ -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);