diff --git a/.eslintignore b/.eslintignore index a4fa65b1b7a..051d21292cb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -429,6 +429,7 @@ packages/app-desktop/gui/Sidebar/listItemComponents/ExpandIcon.js packages/app-desktop/gui/Sidebar/listItemComponents/ExpandLink.js packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.js packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.js +packages/app-desktop/gui/Sidebar/listItemComponents/ListItemWrapper.js packages/app-desktop/gui/Sidebar/listItemComponents/NoteCount.js packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.js packages/app-desktop/gui/Sidebar/styles/index.js diff --git a/.gitignore b/.gitignore index 3698426c866..d756b4c0dc8 100644 --- a/.gitignore +++ b/.gitignore @@ -406,6 +406,7 @@ packages/app-desktop/gui/Sidebar/listItemComponents/ExpandIcon.js packages/app-desktop/gui/Sidebar/listItemComponents/ExpandLink.js packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.js packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.js +packages/app-desktop/gui/Sidebar/listItemComponents/ListItemWrapper.js packages/app-desktop/gui/Sidebar/listItemComponents/NoteCount.js packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.js packages/app-desktop/gui/Sidebar/styles/index.js diff --git a/packages/app-desktop/gui/ItemList.tsx b/packages/app-desktop/gui/ItemList.tsx index 0ffd582a166..8bfd6b48db8 100644 --- a/packages/app-desktop/gui/ItemList.tsx +++ b/packages/app-desktop/gui/ItemList.tsx @@ -14,6 +14,7 @@ interface Props { id?: string; role?: string; 'aria-label'?: string; + tabIndex?: number; } interface State { @@ -177,6 +178,7 @@ class ItemList extends React.Component, State> { role={this.props.role} aria-label={this.props['aria-label']} aria-setsize={items.length} + tabIndex={this.props.tabIndex} onScroll={this.onScroll} onKeyDown={this.onKeyDown} diff --git a/packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx b/packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx index 85bb4606981..c149671dc4c 100644 --- a/packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx +++ b/packages/app-desktop/gui/Sidebar/FolderAndTagList.tsx @@ -44,6 +44,7 @@ const FolderAndTagList: React.FC = props => { ...props, selectedIndex, onSelectedElementShown: setSelectedListElement, + listItems, }); const onKeyEventHandler = useOnSidebarKeyDownHandler({ @@ -75,6 +76,8 @@ const FolderAndTagList: React.FC = props => { items={listItems} itemRenderer={onRenderItem} onKeyDown={onKeyEventHandler} + tabIndex={0} + role='tree' itemHeight={30} /> diff --git a/packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.ts b/packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.ts index 9fad738dc1e..da335eba7a6 100644 --- a/packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.ts +++ b/packages/app-desktop/gui/Sidebar/hooks/useFocusHandler.ts @@ -1,4 +1,4 @@ -import { MutableRefObject, RefObject, useCallback, useEffect, useMemo, useRef } from 'react'; +import { RefObject, useCallback, useEffect, useMemo, useRef } from 'react'; import { ListItem } from '../types'; import ItemList from '../../ItemList'; import { focus } from '@joplin/lib/utils/focusHandler'; @@ -10,20 +10,8 @@ interface Props { listItems: ListItem[]; } -const useFocusAfterNextRenderHandler = ( - shouldFocusAfterNextRender: MutableRefObject, - selectedListElement: HTMLElement|null, -) => { - useEffect(() => { - if (!shouldFocusAfterNextRender.current || !selectedListElement) return; - focus('FolderAndTagList/useFocusHandler/afterRender', selectedListElement); - shouldFocusAfterNextRender.current = false; - }, [selectedListElement, shouldFocusAfterNextRender]); -}; - -const useRefocusOnSelectionChangeHandler = ( +const useScrollToSelectionHandler = ( itemListRef: RefObject>, - shouldFocusAfterNextRender: MutableRefObject, listItems: ListItem[], selectedIndex: number, ) => { @@ -49,31 +37,24 @@ const useRefocusOnSelectionChangeHandler = ( useEffect(() => { if (!itemListRef.current || !selectedItemKey) return; - const hasFocus = !!itemListRef.current.container.querySelector(':scope :focus'); - shouldFocusAfterNextRender.current = hasFocus; + const hasFocus = !!itemListRef.current.container.contains(document.activeElement); if (hasFocus) { itemListRef.current.makeItemIndexVisible(selectedIndexRef.current); } - }, [selectedItemKey, itemListRef, shouldFocusAfterNextRender]); + }, [selectedItemKey, itemListRef]); }; const useFocusHandler = (props: Props) => { const { itemListRef, selectedListElement, selectedIndex, listItems } = props; - // When set to true, when selectedListElement next changes, select it. - const shouldFocusAfterNextRender = useRef(false); - - useRefocusOnSelectionChangeHandler(itemListRef, shouldFocusAfterNextRender, listItems, selectedIndex); - useFocusAfterNextRenderHandler(shouldFocusAfterNextRender, selectedListElement); + useScrollToSelectionHandler(itemListRef, listItems, selectedIndex); const focusSidebar = useCallback(() => { if (!selectedListElement || !itemListRef.current.isIndexVisible(selectedIndex)) { itemListRef.current.makeItemIndexVisible(selectedIndex); - shouldFocusAfterNextRender.current = true; - } else { - focus('FolderAndTagList/useFocusHandler/focusSidebar', selectedListElement); } + focus('FolderAndTagList/useFocusHandler/focusSidebar', itemListRef.current.container); }, [selectedListElement, selectedIndex, itemListRef]); return { focusSidebar }; diff --git a/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx b/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx index aad5bf709e7..07d4f78d6ed 100644 --- a/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx +++ b/packages/app-desktop/gui/Sidebar/hooks/useOnRenderItem.tsx @@ -29,6 +29,7 @@ import Logger from '@joplin/utils/Logger'; import onFolderDrop from '@joplin/lib/models/utils/onFolderDrop'; import HeaderItem from '../listItemComponents/HeaderItem'; import AllNotesItem from '../listItemComponents/AllNotesItem'; +import ListItemWrapper from '../listItemComponents/ListItemWrapper'; const Menu = bridge().Menu; const MenuItem = bridge().MenuItem; @@ -44,6 +45,7 @@ interface Props { selectedIndex: number; onSelectedElementShown: (element: HTMLElement)=> void; + listItems: ListItem[]; } type ItemContextMenuListener = MouseEventHandler; @@ -326,26 +328,21 @@ const useOnRenderItem = (props: Props) => { const selectedIndexRef = useRef(props.selectedIndex); selectedIndexRef.current = props.selectedIndex; + const itemCount = props.listItems.length; return useCallback((item: ListItem, index: number) => { const selected = props.selectedIndex === index; - const anchorRefCallback = selected ? ( - (element: HTMLElement) => { - if (selectedIndexRef.current === index) { - props.onSelectedElementShown(element); - } - } - ) : null; if (item.kind === ListItemType.Tag) { const tag = item.tag; return ; } else if (item.kind === ListItemType.Folder) { const folder = item.folder; @@ -368,7 +365,6 @@ const useOnRenderItem = (props: Props) => { } return { shareId={folder.share_id} parentId={folder.parent_id} showFolderIcon={showFolderIcons} + index={index} + itemCount={itemCount} />; } else if (item.kind === ListItemType.Header) { return ; } else if (item.kind === ListItemType.AllNotes) { return ; } else if (item.kind === ListItemType.Spacer) { return ( - + +
+
); } else { const exhaustivenessCheck: never = item; @@ -422,6 +431,7 @@ const useOnRenderItem = (props: Props) => { tagItem_click, props.selectedIndex, props.onSelectedElementShown, + itemCount, ]); }; diff --git a/packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.ts b/packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.ts index acd335c3e8c..485b77b52cb 100644 --- a/packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.ts +++ b/packages/app-desktop/gui/Sidebar/hooks/useOnSidebarKeyDownHandler.ts @@ -29,6 +29,8 @@ const useOnSidebarKeyDownHandler = (props: Props) => { return useCallback>((event) => { const selectedItem = listItems[selectedIndex]; + let indexChange = 0; + if (selectedItem?.kind === ListItemType.Folder && isToggleShortcut(event.code, selectedItem, collapsedFolderIds)) { event.preventDefault(); @@ -36,18 +38,13 @@ const useOnSidebarKeyDownHandler = (props: Props) => { type: 'FOLDER_TOGGLE', id: selectedItem.folder.id, }); - } - - if ((event.ctrlKey || event.metaKey) && event.code === 'KeyA') { // ctrl+a or cmd+a + } else if ((event.ctrlKey || event.metaKey) && event.code === 'KeyA') { // ctrl+a or cmd+a event.preventDefault(); - } - - let indexChange = 0; - if (event.code === 'ArrowUp') { + } else if (event.code === 'ArrowUp') { indexChange = -1; } else if (event.code === 'ArrowDown') { indexChange = 1; - } else if (event.code === 'Tab') { + } else if (event.code === 'ArrowRight') { event.preventDefault(); if (event.shiftKey) { diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx index 70a5e51307c..bdd94fce38d 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/AllNotesItem.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { StyledAllNotesIcon, StyledListItem, StyledListItemAnchor } from '../styles'; +import { StyledAllNotesIcon, StyledListItemAnchor } from '../styles'; import { useCallback } from 'react'; import { Dispatch } from 'redux'; import bridge from '../../../services/bridge'; @@ -10,6 +10,7 @@ import PerFolderSortOrderService from '../../../services/sortOrder/PerFolderSort import { _ } from '@joplin/lib/locale'; import { connect } from 'react-redux'; import EmptyExpandLink from './EmptyExpandLink'; +import ListItemWrapper from './ListItemWrapper'; const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids'); const Menu = bridge().Menu; @@ -18,7 +19,8 @@ const MenuItem = bridge().MenuItem; interface Props { dispatch: Dispatch; selected: boolean; - anchorRef: React.Ref; + index: number; + itemCount: number; } const menuUtils = new MenuUtils(CommandService.instance()); @@ -46,21 +48,25 @@ const AllNotesItem: React.FC = props => { }, []); return ( - + {_('All notes')} - + ); }; diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/ExpandLink.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/ExpandLink.tsx index 74d1d02ea8a..e8841e449cb 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/ExpandLink.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/ExpandLink.tsx @@ -13,7 +13,7 @@ interface ExpandLinkProps { const ExpandLink: React.FC = props => { return props.hasChildren ? ( - + ) : ( diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.tsx index dd7af6140d4..457604c7e45 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/FolderItem.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types'; import ExpandLink from './ExpandLink'; -import { StyledListItem, StyledListItemAnchor, StyledShareIcon, StyledSpanFix } from '../styles'; +import { StyledListItemAnchor, StyledShareIcon, StyledSpanFix } from '../styles'; import { ItemClickListener, ItemContextMenuListener, ItemDragListener } from '../types'; import FolderIconBox from '../../FolderIconBox'; import { getTrashFolderIcon, getTrashFolderId } from '@joplin/lib/services/trash'; @@ -10,6 +10,7 @@ import Folder from '@joplin/lib/models/Folder'; import { ModelType } from '@joplin/lib/BaseModel'; import { _ } from '@joplin/lib/locale'; import NoteCount from './NoteCount'; +import ListItemWrapper from './ListItemWrapper'; const renderFolderIcon = (folderIcon: FolderIcon) => { if (!folderIcon) { @@ -43,7 +44,9 @@ interface FolderItemProps { onFolderToggleClick_: ItemClickListener; shareId: string; selected: boolean; - anchorRef: React.Ref; + + index: number; + itemCount: number; } function FolderItem(props: FolderItemProps) { @@ -63,29 +66,39 @@ function FolderItem(props: FolderItemProps) { }; return ( - + { folderItem_click(folderId); }} - onDoubleClick={onFolderToggleClick_} > {doRenderFolderIcon()}{folderTitle} {shareIcon} - + ); } diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx index 357a17a7ead..9a70f43b17e 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/HeaderItem.tsx @@ -7,6 +7,7 @@ import { _ } from '@joplin/lib/locale'; import bridge from '../../../services/bridge'; import MenuUtils from '@joplin/lib/services/commands/MenuUtils'; import CommandService from '@joplin/lib/services/CommandService'; +import ListItemWrapper from './ListItemWrapper'; const Menu = bridge().Menu; const MenuItem = bridge().MenuItem; @@ -15,8 +16,10 @@ const menuUtils = new MenuUtils(CommandService.instance()); interface Props { item: HeaderListItem; + isSelected: boolean; onDrop: React.DragEventHandler|null; - anchorRef: React.Ref; + index: number; + itemCount: number; } const HeaderItem: React.FC = props => { @@ -50,7 +53,10 @@ const HeaderItem: React.FC = props => { />; return ( -
= props => { {item.label} { item.onPlusButtonClick && addButton } -
+ ); }; diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/ListItemWrapper.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/ListItemWrapper.tsx new file mode 100644 index 00000000000..b5b3d52953b --- /dev/null +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/ListItemWrapper.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { _ } from '@joplin/lib/locale'; +import { useMemo } from 'react'; + +interface Props { + selected: boolean; + itemIndex: number; + itemCount: number; + depth?: number; + className?: string; + children: (React.ReactNode[])|React.ReactNode; + + onDrag?: React.DragEventHandler; + onDragStart?: React.DragEventHandler; + onDragOver?: React.DragEventHandler; + onDrop?: React.DragEventHandler; + draggable?: boolean; +} + +const ListItemWrapper: React.FC = props => { + const style = useMemo(() => { + return { + '--depth': props.depth, + } as React.CSSProperties; + }, [props.depth]); + + return ( +
+ {props.children} +
+ ); +}; + +export default ListItemWrapper; diff --git a/packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.tsx b/packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.tsx index 6730173f02c..3c0b9e0b6fd 100644 --- a/packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.tsx +++ b/packages/app-desktop/gui/Sidebar/listItemComponents/TagItem.tsx @@ -1,22 +1,25 @@ import Setting from '@joplin/lib/models/Setting'; import * as React from 'react'; import { useCallback } from 'react'; -import { StyledListItem, StyledListItemAnchor, StyledSpanFix } from '../styles'; +import { StyledListItemAnchor, StyledSpanFix } from '../styles'; import { TagsWithNoteCountEntity } from '@joplin/lib/services/database/types'; import BaseModel from '@joplin/lib/BaseModel'; import NoteCount from './NoteCount'; import Tag from '@joplin/lib/models/Tag'; import EmptyExpandLink from './EmptyExpandLink'; +import ListItemWrapper from './ListItemWrapper'; export type TagLinkClickEvent = { tag: TagsWithNoteCountEntity|undefined }; interface Props { selected: boolean; - anchorRef: React.Ref; tag: TagsWithNoteCountEntity; onTagDrop: React.DragEventHandler; onContextMenu: React.MouseEventHandler; onClick: (event: TagLinkClickEvent)=> void; + + itemCount: number; + index: number; } const TagItem = (props: Props) => { @@ -33,18 +36,18 @@ const TagItem = (props: Props) => { }, [props.onClick, tag]); return ( - { {Tag.displayTitle(tag)} {noteCount} - + ); }; diff --git a/packages/app-desktop/gui/Sidebar/style.scss b/packages/app-desktop/gui/Sidebar/style.scss index 59247115da7..04c07981f09 100644 --- a/packages/app-desktop/gui/Sidebar/style.scss +++ b/packages/app-desktop/gui/Sidebar/style.scss @@ -1,4 +1,5 @@ @use 'styles/folder-and-tag-list.scss'; +@use 'styles/list-item-wrapper.scss'; @use 'styles/note-count-label.scss'; @use 'styles/sidebar-expand-icon.scss'; @use 'styles/sidebar-expand-link.scss'; diff --git a/packages/app-desktop/gui/Sidebar/styles/index.ts b/packages/app-desktop/gui/Sidebar/styles/index.ts index 37ba9fa9eb8..ff59ce8a47b 100644 --- a/packages/app-desktop/gui/Sidebar/styles/index.ts +++ b/packages/app-desktop/gui/Sidebar/styles/index.ts @@ -49,22 +49,6 @@ export const StyledHeaderLabel = styled.span` font-weight: bold; `; -export const StyledListItem = styled.div` - box-sizing: border-box; - height: 30px; - display: flex; - flex-direction: row; - align-items: center; - padding-left: ${(props: StyleProps) => props.theme.mainPadding + ('depth' in props ? props.depth : 0) * 16}px; - background: ${(props: StyleProps) => props.selected ? props.theme.selectedColor2 : 'none'}; - /*text-transform: ${(props: StyleProps) => props.isSpecialItem ? 'uppercase' : 'none'};*/ - transition: 0.1s; - - &:hover { - background-color: ${(props: StyleProps) => props.theme.backgroundColorHover2}; - } -`; - function listItemTextColor(props: StyleProps) { if (props.isConflictFolder) return props.theme.colorError2; if (props.isSpecialItem) return props.theme.colorFaded2; diff --git a/packages/app-desktop/gui/Sidebar/styles/list-item-wrapper.scss b/packages/app-desktop/gui/Sidebar/styles/list-item-wrapper.scss new file mode 100644 index 00000000000..440d21bf930 --- /dev/null +++ b/packages/app-desktop/gui/Sidebar/styles/list-item-wrapper.scss @@ -0,0 +1,19 @@ + +.list-item-wrapper { + box-sizing: border-box; + height: 30px; + display: flex; + flex-direction: row; + align-items: center; + padding-left: calc(var(--joplin-main-padding) + calc(var(--depth) * 16px)); + background: none; + transition: 0.1s; + + &.-selected { + background: var(--joplin-selected-color2); + } + + &:hover { + background-color: var(--joplin-background-color-hover2); + } +} \ No newline at end of file