From 67e12489f3bc9edfe377dc5b3cbc76589c7c3344 Mon Sep 17 00:00:00 2001 From: Jonathan Poltak Samosir Date: Thu, 11 Jul 2024 14:05:43 +0700 Subject: [PATCH 1/4] Set up to have optionally controlled hover state - Previously was uncontrolled only. Uncontrolled still exists if props.isFocused is not set, but becomes controlled if set --- .../lists-sidebar/components/sidebar-item.tsx | 77 ++++++++++--------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/src/dashboard-refactor/lists-sidebar/components/sidebar-item.tsx b/src/dashboard-refactor/lists-sidebar/components/sidebar-item.tsx index 6bc089d378..33221a88ba 100644 --- a/src/dashboard-refactor/lists-sidebar/components/sidebar-item.tsx +++ b/src/dashboard-refactor/lists-sidebar/components/sidebar-item.tsx @@ -23,23 +23,39 @@ export interface Props { spaceSidebarWidth: string sidebarItemRef?: (el: any) => void isMenuDisplayed?: boolean + + /** This overrides `state.isHovering` if set. If setting you must also set `props.setFocused` to control it. */ + isFocused?: boolean + setFocused?: (isFocused: boolean) => void } export interface State { - isHovering: boolean - canDisableHover: boolean + /** Don't access this directly. Use `isFocused` calculated getter. */ + _isFocused: boolean } export default class ListsSidebarItem extends React.PureComponent< Props, State > { - state: State = { isHovering: false, canDisableHover: false } + state: State = { _isFocused: false } + + private setFocused = (isFocused: boolean) => { + if (this.props.setFocused) { + this.props.setFocused(isFocused) + } else { + this.setState({ _isFocused: isFocused }) + } + } + + private get isFocused(): boolean { + return this.props.isFocused ?? this.state._isFocused + } private get shouldShowRightSideIcon(): boolean { return ( + this.isFocused || this.props.isShared || - this.state.isHovering || this.props.dragNDropActions?.isDraggedOver || this.props.dragNDropActions?.wasPageDropped || this.props.forceRightSidePermanentDisplay @@ -47,24 +63,14 @@ export default class ListsSidebarItem extends React.PureComponent< } render() { - if (!this.props.areAnyMenusDisplayed && this.state.canDisableHover) { - this.setState({ isHovering: false, canDisableHover: false }) - } - return ( this.setState({ isHovering: true })} - isHovering={this.state.isHovering} + isHovering={this.isFocused} + onMouseEnter={() => this.setFocused(true)} onMouseLeave={() => { if (!this.props.areAnyMenusDisplayed) { - this.setState({ - isHovering: false, - canDisableHover: true, - }) + this.setFocused(false) } - this.setState({ - canDisableHover: true, - }) }} spaceSidebarWidth={this.props.spaceSidebarWidth} onDragEnter={this.props.dragNDropActions?.onDragEnter} @@ -83,13 +89,14 @@ export default class ListsSidebarItem extends React.PureComponent< isSelected={this.props.isSelected} dragNDropActions={this.props.dragNDropActions} name={this.props.name} // Add this line + isFocused={this.isFocused} > - {(this.state.isHovering || + {(this.isFocused || this.props.alwaysShowLeftSideIcon || this.props.alwaysShowRightSideIcon) && this.props.renderLeftSideIcon?.()} @@ -102,6 +109,7 @@ export default class ListsSidebarItem extends React.PureComponent< this.props.dragNDropActions?.onDragStart } onDragEnd={this.props.dragNDropActions?.onDragEnd} + isFocused={this.isFocused} draggable > @@ -115,7 +123,7 @@ export default class ListsSidebarItem extends React.PureComponent< this.props.alwaysShowRightSideIcon } > - + {this.props.renderEditIcon?.()} {this.shouldShowRightSideIcon && @@ -197,6 +205,7 @@ const Name = styled.div` const TitleBox = styled.div<{ draggable: boolean + isFocused: boolean }>` display: flex; flex: 0 1 100%; @@ -204,6 +213,7 @@ const TitleBox = styled.div<{ height: 100%; align-items: center; color: ${(props) => props.theme.colors.greyScale5}; + ${(props) => (props.isFocused ? 'width: 30%;' : '')} ` const SidebarItem = styled.div` @@ -227,10 +237,6 @@ const SidebarItem = styled.div` : 'transparent'}; - &:hover ${TitleBox} { - width: 30%; - } - ${({ isSelected }) => isSelected && css` @@ -241,20 +247,17 @@ const SidebarItem = styled.div` cursor: 'pointer'; - &:hover { - background: ${(props) => props.theme.colors.greyScale1_5}; + ${(props) => + props.isFocused && + ` + background: ${props.theme.colors.greyScale1_5}; - ${({ isSelected }: Props) => - isSelected && - css` - background: ${(props) => props.theme.colors.greyScale2}; - `} - ${(props) => + ${props.isSelected && `background: ${props.theme.colors.greyScale2};`}} + ${ props.theme.variant === 'light' && - css` - background: ${(props) => props.theme.colors.greyScale3}; - `}; - } + `background: ${props.theme.colors.greyScale3};` + } + `} ${(props) => props.dragNDropActions?.isDraggedOver && @@ -282,7 +285,7 @@ const ListTitle = styled.span` pointer-events: none; ` -const IconBox = styled.div` +const IconBox = styled.div` display: none; height: 100%; align-items: center; @@ -294,7 +297,7 @@ const IconBox = styled.div` // List all states in which to display ${(props) => (props.alwaysShowRightSideIcon || - props.isHovering || + props.isFocused || props.dragNDropActions?.isDraggedOver || props.dragNDropActions?.wasPageDropped || props.isMenuDisplayed) && From 4b47d8e3553476fb836111b6466baed2eac0f3f7 Mon Sep 17 00:00:00 2001 From: Jonathan Poltak Samosir Date: Thu, 11 Jul 2024 14:25:07 +0700 Subject: [PATCH 2/4] Get base keyboard nav working in list sidebar --- src/dashboard-refactor/index.tsx | 36 ++++--- .../lists-sidebar/components/search-bar.tsx | 10 +- .../lists-sidebar/index.tsx | 14 ++- .../lists-sidebar/types.tsx | 3 + src/dashboard-refactor/logic.test.util.ts | 6 ++ src/dashboard-refactor/logic.ts | 102 +++++++++++++++++- src/dashboard-refactor/util.ts | 13 +++ 7 files changed, 158 insertions(+), 26 deletions(-) diff --git a/src/dashboard-refactor/index.tsx b/src/dashboard-refactor/index.tsx index 707385cb83..d54f859296 100644 --- a/src/dashboard-refactor/index.tsx +++ b/src/dashboard-refactor/index.tsx @@ -18,7 +18,12 @@ import SidebarToggle from './header/sidebar-toggle' import { Rnd } from 'react-rnd' import { AnnotationsSidebarInDashboardResults as NotesSidebar } from 'src/sidebar/annotations-sidebar/containers/AnnotationsSidebarInDashboardResults' import { AnnotationsSidebarContainer as NotesSidebarContainer } from 'src/sidebar/annotations-sidebar/containers/AnnotationsSidebarContainer' -import { updatePickerValues, stateToSearchParams, getListData } from './util' +import { + updatePickerValues, + stateToSearchParams, + getListData, + getOwnLists, +} from './util' import analytics from 'src/analytics' import { copyToClipboard } from 'src/annotations/content_script/utils' import { deriveStatusIconColor } from './header/sync-status-menu/util' @@ -72,6 +77,7 @@ import { UpdateNotifBanner } from 'src/common-ui/containers/UpdateNotifBanner' import { defaultOrderableSorter } from '@worldbrain/memex-common/lib/utils/item-ordering' import type { HighlightColor } from '@worldbrain/memex-common/lib/common-ui/components/highlightColorPicker/types' import ImagePreviewModal from '@worldbrain/memex-common/lib/common-ui/image-preview-modal' +import type { ListTrees } from 'src/custom-lists/ui/list-trees' export type Props = DashboardDependencies & { getRootElement: () => HTMLElement @@ -166,6 +172,7 @@ export class DashboardContainer extends StatefulUIElement< imageSupportBG: runInBackground(), } + private listTreesRef = React.createRef() private notesSidebarRef = React.createRef() private syncStatusButtonRef = React.createRef() youtubeService: YoutubeService @@ -175,7 +182,13 @@ export class DashboardContainer extends StatefulUIElement< } constructor(props: Props) { - super(props, new DashboardLogic(props)) + super( + props, + new DashboardLogic({ + ...props, + getListTreeState: () => this.listTreesRef.current?.state, + }), + ) this.youtubeService = new YoutubeService(createYoutubeServiceOptions()) ;(window as any)['_state'] = () => ({ ...this.state }) @@ -658,16 +671,9 @@ export class DashboardContainer extends StatefulUIElement< ? { type: 'user-reference', id: currentUser.id } : undefined - const ownListsData = allLists - .filter( - (list) => - list.type === 'user-list' && - cacheUtils.deriveListOwnershipStatus( - list, - userReference, - ) === 'Creator', - ) - .sort(defaultOrderableSorter) + const ownListsData = getOwnLists(allLists, currentUser).sort( + defaultOrderableSorter, + ) const followedListsData = allLists.filter( (list) => list.type === 'user-list' && @@ -686,6 +692,7 @@ export class DashboardContainer extends StatefulUIElement< 0, }} + setFocusedListId={(listId) => + this.processEvent('setFocusedListId', { listId }) + } spaceSidebarWidth={this.state.listsSidebar.spaceSidebarWidth} openRemoteListPage={(remoteListId) => this.props.openSpaceInWebUI(remoteListId) @@ -726,6 +736,8 @@ export class DashboardContainer extends StatefulUIElement< this.processEvent('confirmListCreate', { value }), onSearchQueryChange: (query) => this.processEvent('setListQueryValue', { query }), + onInputKeyDown: (key) => + this.processEvent('handleListQueryKeyPress', { key }), onInputClear: () => this.processEvent('setListQueryValue', { query: '' }), areLocalListsEmpty: !ownListsData.length, diff --git a/src/dashboard-refactor/lists-sidebar/components/search-bar.tsx b/src/dashboard-refactor/lists-sidebar/components/search-bar.tsx index e26b42eb64..f99d686abc 100644 --- a/src/dashboard-refactor/lists-sidebar/components/search-bar.tsx +++ b/src/dashboard-refactor/lists-sidebar/components/search-bar.tsx @@ -114,6 +114,7 @@ const CreateBox = styled.div` export interface ListsSidebarSearchBarProps { onInputClear(): void + onInputKeyDown(key: string): void onCreateNew(newListName: string): void onSearchQueryChange(inputString: string): void areLocalListsEmpty: boolean @@ -164,14 +165,7 @@ export default class ListsSidebarSearchBar extends PureComponent< e, ) => { e.stopPropagation() - if (e.key === 'Escape') { - this.handleClearSearch() - } - - if (e.key === 'Enter') { - this.props.onCreateNew(this.props.searchQuery) - this.handleClearSearch() - } + this.props.onInputKeyDown(e.key) } render(): JSX.Element { diff --git a/src/dashboard-refactor/lists-sidebar/index.tsx b/src/dashboard-refactor/lists-sidebar/index.tsx index ae9aeab62e..0156413c84 100644 --- a/src/dashboard-refactor/lists-sidebar/index.tsx +++ b/src/dashboard-refactor/lists-sidebar/index.tsx @@ -44,6 +44,7 @@ export interface ListsSidebarProps extends ListsSidebarState { onConfirmAddList: (value: string) => void setSidebarPeekState: (isPeeking: boolean) => () => void initDNDActions: (listId: string) => DragNDropActions + setFocusedListId: (listId: UnifiedList['unifiedId'] | null) => void initContextMenuBtnProps: ( listId: string, ) => Omit< @@ -70,7 +71,9 @@ export interface ListsSidebarProps extends ListsSidebarState { spaceSidebarWidth: string getRootElement: () => HTMLElement isInPageMode: boolean - listTreesDeps: Omit + listTreesDeps: Omit & { + ref: React.RefObject + } } export default class ListsSidebar extends PureComponent { @@ -225,6 +228,15 @@ export default class ListsSidebar extends PureComponent { key={list.unifiedId} indentSteps={list.pathUnifiedIds.length} name={`${list.name}`} + isFocused={ + this.props.focusedListId === + list.unifiedId + } + setFocused={(isFocused) => + this.props.setFocusedListId( + isFocused ? list.unifiedId : null, + ) + } isSelected={ this.props.selectedListId === list.unifiedId diff --git a/src/dashboard-refactor/lists-sidebar/types.tsx b/src/dashboard-refactor/lists-sidebar/types.tsx index a135c9433c..8d1ffd83cb 100644 --- a/src/dashboard-refactor/lists-sidebar/types.tsx +++ b/src/dashboard-refactor/lists-sidebar/types.tsx @@ -23,6 +23,7 @@ export type RootState = Pick & { dragOverListId?: string editingListId?: string selectedListId: string | null + focusedListId: UnifiedList['unifiedId'] | null showMoreMenuListId?: string editMenuListId?: string isSidebarToggleHovered?: boolean @@ -46,6 +47,8 @@ export type Events = UIEvent<{ setSidebarPeeking: { isPeeking: boolean } setSidebarToggleHovered: { isHovered: boolean } setListQueryValue: { query: string } + handleListQueryKeyPress: { key: string } + setFocusedListId: { listId: UnifiedList['unifiedId'] | null } setAddListInputShown: { isShown: boolean } cancelListCreate: null diff --git a/src/dashboard-refactor/logic.test.util.ts b/src/dashboard-refactor/logic.test.util.ts index a0b334add6..24018d7207 100644 --- a/src/dashboard-refactor/logic.test.util.ts +++ b/src/dashboard-refactor/logic.test.util.ts @@ -18,6 +18,7 @@ import { } from 'src/content-sharing/background/types' import { PageAnnotationsCache } from 'src/annotations/cache' import { RemoteCopyPasterInterface } from 'src/copy-paster/background/types' +import { initNormalizedState } from '@worldbrain/memex-common/lib/common-ui/utils/normalized-state' type DataSeeder = ( logic: TestLogicContainer, @@ -209,6 +210,11 @@ export async function setupTest( device.backgroundModules.bgScript.remoteFunctions, ) as any, browserAPIs: device.browserAPIs, + getListTreeState: () => ({ + draggedListId: null, + dragOverListId: null, + listTrees: initNormalizedState(), + }), }) if (args.overrideSearchTrigger) { diff --git a/src/dashboard-refactor/logic.ts b/src/dashboard-refactor/logic.ts index 023d2b0e0d..46a776e0b8 100644 --- a/src/dashboard-refactor/logic.ts +++ b/src/dashboard-refactor/logic.ts @@ -15,7 +15,12 @@ import { MISSING_PDF_QUERY_PARAM, } from 'src/dashboard-refactor/constants' import { STORAGE_KEYS as CLOUD_STORAGE_KEYS } from 'src/personal-cloud/constants' -import { updatePickerValues, stateToSearchParams, getListData } from './util' +import { + updatePickerValues, + stateToSearchParams, + getListData, + getOwnLists, +} from './util' import { SPECIAL_LIST_IDS } from '@worldbrain/memex-common/lib/storage/modules/lists/constants' import type { NoResultsType, SelectableBlock } from './search-results/types' import { filterListsByQuery } from './lists-sidebar/util' @@ -45,7 +50,10 @@ import { ACTIVITY_INDICATOR_ACTIVE_CACHE_KEY } from 'src/activity-indicator/cons import { validateSpaceName } from '@worldbrain/memex-common/lib/utils/space-name-validation' import { HIGHLIGHT_COLORS_DEFAULT } from '@worldbrain/memex-common/lib/common-ui/components/highlightColorPicker/constants' import { openPDFInViewer } from 'src/pdf/util' -import { hydrateCacheForListUsage } from 'src/annotations/cache/utils' +import { + deriveListOwnershipStatus, + hydrateCacheForListUsage, +} from 'src/annotations/cache/utils' import type { PageAnnotationsCacheEvents } from 'src/annotations/cache/types' import { SPECIAL_LIST_STRING_IDS } from './lists-sidebar/constants' import { normalizeUrl } from '@worldbrain/memex-common/lib/url-utils/normalize' @@ -64,7 +72,8 @@ import { processCommentForImageUpload } from '@worldbrain/memex-common/lib/annot import type { UnifiedSearchResult } from 'src/search/background/types' import type { BulkEditCollection } from 'src/bulk-edit/types' import checkBrowser from 'src/util/check-browser' -import { HighlightColor } from '@worldbrain/memex-common/lib/common-ui/components/highlightColorPicker/types' +import { getVisibleTreeNodesInOrder } from 'src/custom-lists/ui/list-trees/util' +import type { State as ListTreesState } from 'src/custom-lists/ui/list-trees/types' type EventHandler = UIEventHandler< State, @@ -119,7 +128,12 @@ export class DashboardLogic extends UILogic { currentSearchID = 0 observer: MutationObserver // This line explicitly declares the observer property for clarity. - constructor(private options: DashboardDependencies) { + constructor( + private options: DashboardDependencies & { + /** Allows direct access to list tree state encapsulated in ListTrees container component. */ + getListTreeState: () => ListTreesState + }, + ) { super() this.syncSettings = createSyncSettingsStore({ syncSettingsBG: options.syncSettingsBG, @@ -287,6 +301,7 @@ export class DashboardLogic extends UILogic { spaceSidebarWidth: sizeConstants.listsSidebar.width + 'px', addListErrorMessage: null, editListErrorMessage: null, + focusedListId: null, listShareLoadingState: 'pristine', listDropReceiveState: 'pristine', listCreateState: 'pristine', @@ -4096,6 +4111,78 @@ export class DashboardLogic extends UILogic { }) } + private calcNextFocusedList(state: State, change: -1 | 1 = 1): string { + let lists = getOwnLists( + normalizedStateToArray(state.listsSidebar.lists), + state.currentUser, + ) + .filter((list) => + state.listsSidebar.filteredListIds.length + ? state.listsSidebar.filteredListIds.includes( + list.unifiedId, + ) + : true, + ) + .sort(defaultOrderableSorter) + + let visibleTreeNodes = getVisibleTreeNodesInOrder( + lists, + this.options.getListTreeState(), + { + areListsBeingFiltered: + state.listsSidebar.filteredListIds.length > 0, + }, + ) + + let currentIndex = -1 + if (state.listsSidebar.focusedListId != null) { + currentIndex = visibleTreeNodes.findIndex( + (node) => node.unifiedId === state.listsSidebar.focusedListId, + ) + } + + let nextIndex = currentIndex === -1 ? 0 : currentIndex + change + + // Loop back around if going out-of-bounds + if (nextIndex < 0) { + nextIndex = visibleTreeNodes.length - 1 + } else if (nextIndex >= visibleTreeNodes.length) { + nextIndex = 0 + } + + let nextFocusedListId = visibleTreeNodes[nextIndex]?.unifiedId + if (nextFocusedListId != null) { + this.emitMutation({ + listsSidebar: { focusedListId: { $set: nextFocusedListId } }, + }) + } + return nextFocusedListId + } + + handleListQueryKeyPress: EventHandler<'handleListQueryKeyPress'> = async ({ + event, + previousState, + }) => { + if (event.key === 'Escape') { + this.emitMutation({ listsSidebar: { searchQuery: { $set: '' } } }) + } else if (event.key === 'Enter') { + await this.createNewList( + previousState, + previousState.listsSidebar.searchQuery, + ) + } else if (event.key === 'ArrowUp') { + this.calcNextFocusedList(previousState, -1) + } else if (event.key === 'ArrowDown') { + this.calcNextFocusedList(previousState, 1) + } + } + + setFocusedListId: EventHandler<'setFocusedListId'> = async ({ event }) => { + this.emitMutation({ + listsSidebar: { focusedListId: { $set: event.listId } }, + }) + } + setAddListInputShown: EventHandler<'setAddListInputShown'> = async ({ event, }) => { @@ -4120,7 +4207,12 @@ export class DashboardLogic extends UILogic { event, previousState, }) => { - const newListName = event.value.trim() + await this.createNewList(previousState, event.value) + this.emitMutation({ listsSidebar: { searchQuery: { $set: '' } } }) + } + + private async createNewList(previousState: State, name: string) { + const newListName = name.trim() const validationResult = validateSpaceName( newListName, normalizedStateToArray(previousState.listsSidebar.lists) diff --git a/src/dashboard-refactor/util.ts b/src/dashboard-refactor/util.ts index 8fd58af46b..b2a6f3be5b 100644 --- a/src/dashboard-refactor/util.ts +++ b/src/dashboard-refactor/util.ts @@ -10,6 +10,8 @@ import type { } from 'src/annotations/cache/types' import { SPECIAL_LIST_STRING_IDS } from './lists-sidebar/constants' import { SPECIAL_LIST_NAMES } from '@worldbrain/memex-common/lib/storage/modules/lists/constants' +import { deriveListOwnershipStatus } from 'src/annotations/cache/utils' +import type { AuthenticatedUser } from '@worldbrain/memex-common/lib/authentication/types' export const updatePickerValues = (event: { added?: string @@ -140,3 +142,14 @@ export const stateToSearchParams = ( ...termsSearchOpts, } } + +export function getOwnLists(allLists: UnifiedList[], user?: AuthenticatedUser) { + return allLists.filter( + (list) => + list.type === 'user-list' && + deriveListOwnershipStatus( + list, + user ? { type: 'user-reference', id: user.id } : undefined, + ) === 'Creator', + ) +} From 81803fc3e7b0cf6f291ed80c80e6e129ce379d7a Mon Sep 17 00:00:00 2001 From: Jonathan Poltak Samosir Date: Fri, 12 Jul 2024 09:48:02 +0700 Subject: [PATCH 3/4] Allow Enter to both create new lists and select existing lists - A bit more complicated than I first thought. See changes and in-code comments --- src/dashboard-refactor/logic.ts | 69 +++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/src/dashboard-refactor/logic.ts b/src/dashboard-refactor/logic.ts index 46a776e0b8..e510faa507 100644 --- a/src/dashboard-refactor/logic.ts +++ b/src/dashboard-refactor/logic.ts @@ -4086,26 +4086,40 @@ export class DashboardLogic extends UILogic { event, previousState, }) => { - const filteredLists = filterListsByQuery( + let filteredLists = filterListsByQuery( event.query, normalizedStateToArray(previousState.listsSidebar.lists), ) + let trimmedQuery = event.query.trim() + let nextFilteredListIds = + trimmedQuery.length > 0 + ? [ + ...new Set( + filteredLists.flatMap((list) => [ + list.unifiedId, + ...list.pathUnifiedIds, // Include ancestors of matched lists + ]), + ), + ] + : [] this.emitMutation({ listsSidebar: { searchQuery: { $set: event.query }, - filteredListIds: { - $set: - event.query.trim().length > 0 - ? [ - ...new Set( - filteredLists.flatMap((list) => [ - list.unifiedId, - ...list.pathUnifiedIds, // Include ancestors of matched lists - ]), - ), - ] - : [], + filteredListIds: { $set: nextFilteredListIds }, + // Basically we want to reset state.focusedListId to null if the prev list no longer shows up in the filtered lists. + // This is so "Enter" press on focused list can easily distingush the actions of "select list" and "create new list" + // - the latter only working when there is NO focused list AND some query exists AND no matches to the query. + focusedListId: { + $apply: (prev) => { + if ( + !nextFilteredListIds.length && + !trimmedQuery.length + ) { + return prev + } + return nextFilteredListIds.includes(prev) ? prev : null + }, }, }, }) @@ -4166,10 +4180,21 @@ export class DashboardLogic extends UILogic { if (event.key === 'Escape') { this.emitMutation({ listsSidebar: { searchQuery: { $set: '' } } }) } else if (event.key === 'Enter') { - await this.createNewList( - previousState, - previousState.listsSidebar.searchQuery, - ) + let canCreateNewList = + previousState.listsSidebar.searchQuery.trim().length && + !previousState.listsSidebar.filteredListIds.length + + if (previousState.listsSidebar.focusedListId != null) { + await this._setSelectedListId( + previousState, + previousState.listsSidebar.focusedListId, + ) + } else if (canCreateNewList) { + await this.createNewList( + previousState, + previousState.listsSidebar.searchQuery, + ) + } } else if (event.key === 'ArrowUp') { this.calcNextFocusedList(previousState, -1) } else if (event.key === 'ArrowDown') { @@ -4208,7 +4233,6 @@ export class DashboardLogic extends UILogic { previousState, }) => { await this.createNewList(previousState, event.value) - this.emitMutation({ listsSidebar: { searchQuery: { $set: '' } } }) } private async createNewList(previousState: State, name: string) { @@ -4259,6 +4283,7 @@ export class DashboardLogic extends UILogic { isAddListInputShown: { $set: false }, areLocalListsExpanded: { $set: true }, addListErrorMessage: { $set: null }, + searchQuery: { $set: '' }, }, }) const { @@ -4312,10 +4337,12 @@ export class DashboardLogic extends UILogic { event, previousState, }) => { + await this._setSelectedListId(previousState, event.listId) + } + + private async _setSelectedListId(previousState: State, listId: string) { const listIdToSet = - previousState.listsSidebar.selectedListId === event.listId - ? null - : event.listId + previousState.listsSidebar.selectedListId === listId ? null : listId if (listIdToSet != null) { if (listIdToSet === '20201014' || listIdToSet === '20201015') { From bc5602cd72ea53167d66eb4f911e6c78db8e1df8 Mon Sep 17 00:00:00 2001 From: Jonathan Poltak Samosir Date: Fri, 12 Jul 2024 10:01:37 +0700 Subject: [PATCH 4/4] Toggle trees by < > arrow keys in lists sidebar KB nav --- src/dashboard-refactor/index.tsx | 2 +- src/dashboard-refactor/logic.test.util.ts | 6 +---- src/dashboard-refactor/logic.ts | 33 ++++++++++++++++++----- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/dashboard-refactor/index.tsx b/src/dashboard-refactor/index.tsx index d54f859296..2c660e7027 100644 --- a/src/dashboard-refactor/index.tsx +++ b/src/dashboard-refactor/index.tsx @@ -186,7 +186,7 @@ export class DashboardContainer extends StatefulUIElement< props, new DashboardLogic({ ...props, - getListTreeState: () => this.listTreesRef.current?.state, + getListTreesRef: () => this.listTreesRef.current, }), ) diff --git a/src/dashboard-refactor/logic.test.util.ts b/src/dashboard-refactor/logic.test.util.ts index 24018d7207..7949f2151f 100644 --- a/src/dashboard-refactor/logic.test.util.ts +++ b/src/dashboard-refactor/logic.test.util.ts @@ -210,11 +210,7 @@ export async function setupTest( device.backgroundModules.bgScript.remoteFunctions, ) as any, browserAPIs: device.browserAPIs, - getListTreeState: () => ({ - draggedListId: null, - dragOverListId: null, - listTrees: initNormalizedState(), - }), + getListTreesRef: () => undefined, }) if (args.overrideSearchTrigger) { diff --git a/src/dashboard-refactor/logic.ts b/src/dashboard-refactor/logic.ts index e510faa507..e4ad3ab4ff 100644 --- a/src/dashboard-refactor/logic.ts +++ b/src/dashboard-refactor/logic.ts @@ -74,6 +74,7 @@ import type { BulkEditCollection } from 'src/bulk-edit/types' import checkBrowser from 'src/util/check-browser' import { getVisibleTreeNodesInOrder } from 'src/custom-lists/ui/list-trees/util' import type { State as ListTreesState } from 'src/custom-lists/ui/list-trees/types' +import type { ListTrees } from 'src/custom-lists/ui/list-trees' type EventHandler = UIEventHandler< State, @@ -131,7 +132,7 @@ export class DashboardLogic extends UILogic { constructor( private options: DashboardDependencies & { /** Allows direct access to list tree state encapsulated in ListTrees container component. */ - getListTreeState: () => ListTreesState + getListTreesRef: () => ListTrees | undefined }, ) { super() @@ -4125,7 +4126,11 @@ export class DashboardLogic extends UILogic { }) } - private calcNextFocusedList(state: State, change: -1 | 1 = 1): string { + private calcNextFocusedList( + state: State, + listTreesState: ListTreesState, + change: -1 | 1 = 1, + ): string { let lists = getOwnLists( normalizedStateToArray(state.listsSidebar.lists), state.currentUser, @@ -4141,7 +4146,7 @@ export class DashboardLogic extends UILogic { let visibleTreeNodes = getVisibleTreeNodesInOrder( lists, - this.options.getListTreeState(), + listTreesState, { areListsBeingFiltered: state.listsSidebar.filteredListIds.length > 0, @@ -4177,6 +4182,7 @@ export class DashboardLogic extends UILogic { event, previousState, }) => { + let listTreesRef = this.options.getListTreesRef() if (event.key === 'Escape') { this.emitMutation({ listsSidebar: { searchQuery: { $set: '' } } }) } else if (event.key === 'Enter') { @@ -4195,10 +4201,23 @@ export class DashboardLogic extends UILogic { previousState.listsSidebar.searchQuery, ) } - } else if (event.key === 'ArrowUp') { - this.calcNextFocusedList(previousState, -1) - } else if (event.key === 'ArrowDown') { - this.calcNextFocusedList(previousState, 1) + } else if (event.key === 'ArrowUp' && listTreesRef) { + this.calcNextFocusedList(previousState, listTreesRef.state, -1) + } else if (event.key === 'ArrowDown' && listTreesRef) { + this.calcNextFocusedList(previousState, listTreesRef.state, 1) + } else if ( + (event.key === 'ArrowRight' && + listTreesRef?.state.listTrees.byId[ + previousState.listsSidebar.focusedListId + ]?.areChildrenShown === false) || + (event.key === 'ArrowLeft' && + listTreesRef?.state.listTrees.byId[ + previousState.listsSidebar.focusedListId + ]?.areChildrenShown === true) + ) { + listTreesRef.processEvent('toggleShowChildren', { + listId: previousState.listsSidebar.focusedListId, + }) } }