From e4aafbcb9fc811f2bcbbe05679b373da90938ebd Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 5 Nov 2020 16:09:20 +0100 Subject: [PATCH 01/11] Show 'no results' message when query yields nothing. --- .../src/components/sidebar/Search.stories.tsx | 3 ++- .../sidebar/SearchResults.stories.tsx | 10 ++++++++-- .../src/components/sidebar/SearchResults.tsx | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/ui/src/components/sidebar/Search.stories.tsx b/lib/ui/src/components/sidebar/Search.stories.tsx index 871fc0f4f89e..ad989c881dcd 100644 --- a/lib/ui/src/components/sidebar/Search.stories.tsx +++ b/lib/ui/src/components/sidebar/Search.stories.tsx @@ -4,6 +4,7 @@ import { action } from '@storybook/addon-actions'; import { stories } from './mockdata.large'; import { Search } from './Search'; import { SearchResults } from './SearchResults'; +import { noResults } from './SearchResults.stories'; import { DEFAULT_REF_ID } from './data'; import { Selection } from './types'; @@ -31,7 +32,7 @@ export const Simple = () => {() => null}; export const FilledIn = () => ( - {() => null} + {() => } ); diff --git a/lib/ui/src/components/sidebar/SearchResults.stories.tsx b/lib/ui/src/components/sidebar/SearchResults.stories.tsx index d3d29310eba7..98b0bb2ceeca 100644 --- a/lib/ui/src/components/sidebar/SearchResults.stories.tsx +++ b/lib/ui/src/components/sidebar/SearchResults.stories.tsx @@ -58,14 +58,18 @@ const recents = stories // We need this to prevent react key warnings const passKey = (props: any = {}) => ({ key: props.key }); -const searching = { +export const searching = { query: 'query', results, getMenuProps: passKey, getItemProps: passKey, highlightedIndex: 0, }; -const lastViewed = { +export const noResults = { + ...searching, + results: [] as any, +}; +export const lastViewed = { query: '', results: recents, getMenuProps: passKey, @@ -75,4 +79,6 @@ const lastViewed = { export const Searching = () => ; +export const NoResults = () => ; + export const LastViewed = () => ; diff --git a/lib/ui/src/components/sidebar/SearchResults.tsx b/lib/ui/src/components/sidebar/SearchResults.tsx index 5b666e26ce56..99d153b9ad5b 100644 --- a/lib/ui/src/components/sidebar/SearchResults.tsx +++ b/lib/ui/src/components/sidebar/SearchResults.tsx @@ -24,6 +24,16 @@ const ResultRow = styled.li<{ isHighlighted: boolean }>(({ theme, isHighlighted cursor: 'pointer', })); +const NoResults = styled.div(({ theme }) => ({ + marginTop: 20, + textAlign: 'center', + fontSize: `${theme.typography.size.s2}px`, + color: theme.color.defaultText, + small: { + color: theme.barTextColor, + }, +})); + const Mark = styled.mark(({ theme }) => ({ background: 'transparent', color: theme.color.secondary, @@ -133,6 +143,15 @@ export const SearchResults: FunctionComponent<{ Recently opened )} + {results.length === 0 && query && ( +
  • + + No components found +
    + Find components by name or path. +
    +
  • + )} {results.map((result: DownshiftItem, index) => { if (isClearType(result)) { return ( From fda2d9172b41378a306c9ab3c16004a438cbfde2 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 5 Nov 2020 16:35:58 +0100 Subject: [PATCH 02/11] Keep currently selected item in history when clearing it. --- lib/ui/src/components/sidebar/useLastViewed.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/ui/src/components/sidebar/useLastViewed.ts b/lib/ui/src/components/sidebar/useLastViewed.ts index 2cd30bf45404..6e1fa4808336 100644 --- a/lib/ui/src/components/sidebar/useLastViewed.ts +++ b/lib/ui/src/components/sidebar/useLastViewed.ts @@ -31,9 +31,10 @@ export const useLastViewed = (selection: Selection) => { ); const clearLastViewed = useCallback(() => { - setLastViewed([]); - store.set('lastViewedStoryIds', []); - }, []); + const update = selection ? [selection] : []; + setLastViewed(update); + store.set('lastViewedStoryIds', update); + }, [selection]); useEffect(() => { if (selection) updateLastViewed(selection); From e3303e62cd66f125238e66c30142b7643b75addc Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 5 Nov 2020 17:01:30 +0100 Subject: [PATCH 03/11] Remove focus from expand/collapse all buttons when using arrow keys to navigate the tree. --- lib/ui/src/components/sidebar/useExpanded.ts | 5 ++- .../src/components/sidebar/useHighlighted.ts | 32 +++++++++---------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/ui/src/components/sidebar/useExpanded.ts b/lib/ui/src/components/sidebar/useExpanded.ts index b4f97805e6d0..d57bdaf6cf19 100644 --- a/lib/ui/src/components/sidebar/useExpanded.ts +++ b/lib/ui/src/components/sidebar/useExpanded.ts @@ -94,8 +94,11 @@ export const useExpanded = ({ if (!highlightedElement || highlightedElement.getAttribute('data-ref-id') !== refId) return; const target = event.target as Element; - if (target.hasAttribute('data-action')) return; if (!isAncestor(menuElement, target) && !isAncestor(target, menuElement)) return; + if (target.hasAttribute('data-action')) { + if (['Enter', ' '].includes(event.key)) return; + (target as HTMLButtonElement).blur(); + } event.preventDefault(); diff --git a/lib/ui/src/components/sidebar/useHighlighted.ts b/lib/ui/src/components/sidebar/useHighlighted.ts index ae79b35b0d44..a87b195aaffd 100644 --- a/lib/ui/src/components/sidebar/useHighlighted.ts +++ b/lib/ui/src/components/sidebar/useHighlighted.ts @@ -70,26 +70,26 @@ export const useHighlighted = ({ const navigateTree = throttle((event) => { if (isLoading || !isBrowsing || !event.key || !containerRef || !containerRef.current) return; if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return; + if (!['ArrowUp', 'ArrowDown'].includes(event.key)) return; const target = event.target as Element; if (!isAncestor(menuElement, target) && !isAncestor(target, menuElement)) return; + if (target.hasAttribute('data-action')) (target as HTMLButtonElement).blur(); - if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { - event.preventDefault(); - const highlightable = Array.from( - containerRef.current.querySelectorAll('[data-highlightable=true]') - ); - const currentIndex = highlightable.findIndex( - (el) => - el.getAttribute('data-item-id') === highlightedRef.current?.itemId && - el.getAttribute('data-ref-id') === highlightedRef.current?.refId - ); - const nextIndex = cycle(highlightable, currentIndex, event.key === 'ArrowUp' ? -1 : 1); - const didRunAround = - (event.key === 'ArrowDown' && nextIndex === 0) || - (event.key === 'ArrowUp' && nextIndex === highlightable.length - 1); - highlightElement(highlightable[nextIndex], didRunAround); - } + event.preventDefault(); + const highlightable = Array.from( + containerRef.current.querySelectorAll('[data-highlightable=true]') + ); + const currentIndex = highlightable.findIndex( + (el) => + el.getAttribute('data-item-id') === highlightedRef.current?.itemId && + el.getAttribute('data-ref-id') === highlightedRef.current?.refId + ); + const nextIndex = cycle(highlightable, currentIndex, event.key === 'ArrowUp' ? -1 : 1); + const didRunAround = + (event.key === 'ArrowDown' && nextIndex === 0) || + (event.key === 'ArrowUp' && nextIndex === highlightable.length - 1); + highlightElement(highlightable[nextIndex], didRunAround); }, 30); document.addEventListener('keydown', navigateTree); From 44e6a4508e59df2706be0dcedbc0dd64cf528531 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 5 Nov 2020 21:08:39 +0100 Subject: [PATCH 04/11] Highlight parent instead of closing it right away, and fix closing descendants. --- lib/ui/src/components/sidebar/useExpanded.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/ui/src/components/sidebar/useExpanded.ts b/lib/ui/src/components/sidebar/useExpanded.ts index d57bdaf6cf19..ba945f039273 100644 --- a/lib/ui/src/components/sidebar/useExpanded.ts +++ b/lib/ui/src/components/sidebar/useExpanded.ts @@ -117,11 +117,10 @@ export const useExpanded = ({ if (!parentId) return; const parentElement = getElementByDataItemId(parentId); if (parentElement && parentElement.getAttribute('data-highlightable') === 'true') { - setExpanded({ ids: [parentId], value: false }); highlightElement(parentElement); } else { setExpanded({ - ids: getDescendantIds(data, parentId, true), + ids: getDescendantIds(data, highlightedItemId, true), value: false, }); } From 8a30b98b7ed00b5bac1c7c03f82b435d7781d5b7 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 5 Nov 2020 21:10:48 +0100 Subject: [PATCH 05/11] Fix highlighting prev/next node after collapsing and reopening a node. --- .../src/components/sidebar/useHighlighted.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/ui/src/components/sidebar/useHighlighted.ts b/lib/ui/src/components/sidebar/useHighlighted.ts index a87b195aaffd..c668dbc27958 100644 --- a/lib/ui/src/components/sidebar/useHighlighted.ts +++ b/lib/ui/src/components/sidebar/useHighlighted.ts @@ -35,24 +35,30 @@ export const useHighlighted = ({ const highlightedRef = useRef(initialHighlight); const [highlighted, setHighlighted] = useState(initialHighlight); + const updateHighlighted = useCallback( + (highlight) => { + highlightedRef.current = highlight; + setHighlighted(highlight); + }, + [highlightedRef] + ); + // Sets the highlighted node and scrolls it into view, using DOM elements as reference const highlightElement = useCallback( (element: Element, center = false) => { const itemId = element.getAttribute('data-item-id'); const refId = element.getAttribute('data-ref-id'); if (!itemId || !refId) return; - highlightedRef.current = { itemId, refId }; - setHighlighted(highlightedRef.current); + updateHighlighted({ itemId, refId }); scrollIntoView(element, center); }, - [highlightedRef, setHighlighted] + [updateHighlighted] ); // Highlight and scroll to the selected story whenever the selection or dataset changes useEffect(() => { const highlight = fromSelection(selected); - setHighlighted(highlight); - highlightedRef.current = highlight; + updateHighlighted(highlight); if (highlight) { const { itemId, refId } = highlight; setTimeout(() => { @@ -96,5 +102,5 @@ export const useHighlighted = ({ return () => document.removeEventListener('keydown', navigateTree); }, [isLoading, isBrowsing, highlightedRef, highlightElement]); - return [highlighted, setHighlighted]; + return [highlighted, updateHighlighted]; }; From e4dd53410968ad3c54bc4ebe8fe5c0c8cfb54975 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 5 Nov 2020 21:45:59 +0100 Subject: [PATCH 06/11] Highlight a node when it gets expanded or collapsed. --- lib/ui/src/components/sidebar/useExpanded.ts | 37 +++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/lib/ui/src/components/sidebar/useExpanded.ts b/lib/ui/src/components/sidebar/useExpanded.ts index ba945f039273..cd1ddb2d56e3 100644 --- a/lib/ui/src/components/sidebar/useExpanded.ts +++ b/lib/ui/src/components/sidebar/useExpanded.ts @@ -111,20 +111,23 @@ export const useExpanded = ({ if (event.key === 'ArrowLeft') { if (isExpanded === 'true') { + // The highlighted node is expanded, so we collapse it. setExpanded({ ids: [highlightedItemId], value: false }); - } else { - const parentId = highlightedElement.getAttribute('data-parent-id'); - if (!parentId) return; - const parentElement = getElementByDataItemId(parentId); - if (parentElement && parentElement.getAttribute('data-highlightable') === 'true') { - highlightElement(parentElement); - } else { - setExpanded({ - ids: getDescendantIds(data, highlightedItemId, true), - value: false, - }); - } + return; } + + const parentId = highlightedElement.getAttribute('data-parent-id'); + const parentElement = parentId && getElementByDataItemId(parentId); + if (parentElement && parentElement.getAttribute('data-highlightable') === 'true') { + // The highlighted node isn't expanded, so we move the highlight to its parent instead. + highlightElement(parentElement); + return; + } + + // The parent can't be highlighted, which means it must be a root. + // The highlighted node is already collapsed, so we collapse its descendants. + setExpanded({ ids: getDescendantIds(data, highlightedItemId, true), value: false }); + return; } if (event.key === 'ArrowRight') { @@ -148,5 +151,13 @@ export const useExpanded = ({ onSelectStoryId, ]); - return [expanded, setExpanded]; + const updateExpanded = useCallback( + ({ ids, value }) => { + setExpanded({ ids, value }); + if (ids.length === 1) setHighlightedItemId(ids[0]); + }, + [setHighlightedItemId] + ); + + return [expanded, updateExpanded]; }; From 080f3351d48510d59d1103488f8fcc1d7d9c876b Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 5 Nov 2020 23:12:45 +0100 Subject: [PATCH 07/11] Update lib/ui/src/components/sidebar/SearchResults.tsx Co-authored-by: Dominic Nguyen --- lib/ui/src/components/sidebar/SearchResults.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/ui/src/components/sidebar/SearchResults.tsx b/lib/ui/src/components/sidebar/SearchResults.tsx index 99d153b9ad5b..3315ab09f356 100644 --- a/lib/ui/src/components/sidebar/SearchResults.tsx +++ b/lib/ui/src/components/sidebar/SearchResults.tsx @@ -27,7 +27,8 @@ const ResultRow = styled.li<{ isHighlighted: boolean }>(({ theme, isHighlighted const NoResults = styled.div(({ theme }) => ({ marginTop: 20, textAlign: 'center', - fontSize: `${theme.typography.size.s2}px`, + fontSize: `${theme.typography.size.s2 - 1}px`, + lineHeight: `18px`, color: theme.color.defaultText, small: { color: theme.barTextColor, From 99bff118d0b4f941ac90f43e508c840312c9f876 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 5 Nov 2020 23:12:51 +0100 Subject: [PATCH 08/11] Update lib/ui/src/components/sidebar/SearchResults.tsx Co-authored-by: Dominic Nguyen --- lib/ui/src/components/sidebar/SearchResults.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/ui/src/components/sidebar/SearchResults.tsx b/lib/ui/src/components/sidebar/SearchResults.tsx index 3315ab09f356..63cbf0792ed2 100644 --- a/lib/ui/src/components/sidebar/SearchResults.tsx +++ b/lib/ui/src/components/sidebar/SearchResults.tsx @@ -32,6 +32,7 @@ const NoResults = styled.div(({ theme }) => ({ color: theme.color.defaultText, small: { color: theme.barTextColor, + fontSize: `${theme.typography.size.s1}px`, }, })); From da337235ab8a5df344efa0fba38f51da25c0becc Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Thu, 5 Nov 2020 23:52:30 +0100 Subject: [PATCH 09/11] Add back button to recently viewed list, with keyboard shortcut. --- lib/ui/src/components/sidebar/Search.tsx | 9 +++ .../src/components/sidebar/SearchResults.tsx | 62 +++++++++++++++++-- lib/ui/src/components/sidebar/Sidebar.tsx | 11 +++- lib/ui/src/components/sidebar/types.ts | 9 ++- 4 files changed, 84 insertions(+), 7 deletions(-) diff --git a/lib/ui/src/components/sidebar/Search.tsx b/lib/ui/src/components/sidebar/Search.tsx index df42eddadaf4..72f5ac7e5218 100644 --- a/lib/ui/src/components/sidebar/Search.tsx +++ b/lib/ui/src/components/sidebar/Search.tsx @@ -18,6 +18,7 @@ import { isSearchResult, isExpandType, isClearType, + isCloseType, } from './types'; import { searchItem } from './utils'; @@ -268,6 +269,11 @@ export const Search: FunctionComponent<{ // Nothing to see anymore, so return to the tree view return { isOpen: false }; } + if (isCloseType(changes.selectedItem)) { + inputRef.current.blur(); + // Return to the tree view + return { isOpen: false }; + } return changes; } @@ -294,6 +300,7 @@ export const Search: FunctionComponent<{ {({ isOpen, openMenu, + closeMenu, inputValue, clearSelection, getInputProps, @@ -322,6 +329,7 @@ export const Search: FunctionComponent<{ } return acc; }, []); + results.push({ closeMenu }); if (results.length > 0) { results.push({ clearLastViewed }); } @@ -354,6 +362,7 @@ export const Search: FunctionComponent<{ query: input, results, isBrowsing: !isOpen && document.activeElement !== inputRef.current, + closeMenu, getMenuProps, getItemProps, highlightedIndex, diff --git a/lib/ui/src/components/sidebar/SearchResults.tsx b/lib/ui/src/components/sidebar/SearchResults.tsx index 63cbf0792ed2..c9dbed3f6abf 100644 --- a/lib/ui/src/components/sidebar/SearchResults.tsx +++ b/lib/ui/src/components/sidebar/SearchResults.tsx @@ -1,11 +1,24 @@ import { styled } from '@storybook/theming'; import { Icons } from '@storybook/components'; -import { DOCS_MODE } from 'global'; -import React, { FunctionComponent, MouseEventHandler, ReactNode, useCallback } from 'react'; +import { document, DOCS_MODE } from 'global'; +import React, { + FunctionComponent, + MouseEventHandler, + ReactNode, + useCallback, + useEffect, +} from 'react'; import { ControllerStateAndHelpers } from 'downshift'; import { ComponentNode, DocumentNode, Path, RootNode, StoryNode } from './TreeNode'; -import { Match, DownshiftItem, isClearType, isExpandType, SearchResult } from './types'; +import { + Match, + DownshiftItem, + isCloseType, + isClearType, + isExpandType, + SearchResult, +} from './types'; import { getLink } from './utils'; const ResultsList = styled.ol({ @@ -43,11 +56,12 @@ const Mark = styled.mark(({ theme }) => ({ const ActionRow = styled(ResultRow)({ display: 'flex', - padding: '5px 19px', + padding: '6px 19px', alignItems: 'center', }); const ActionLabel = styled.span(({ theme }) => ({ + flexGrow: 1, color: theme.color.mediumdark, fontSize: `${theme.typography.size.s1}px`, })); @@ -60,6 +74,19 @@ const ActionIcon = styled(Icons)(({ theme }) => ({ color: theme.color.mediumdark, })); +const ActionKey = styled.code(({ theme }) => ({ + minWidth: 16, + height: 16, + lineHeight: '17px', + textAlign: 'center', + fontSize: '11px', + background: 'rgba(0,0,0,0.1)', + color: theme.textMutedColor, + borderRadius: 2, + userSelect: 'none', + pointerEvents: 'none', +})); + const Highlight: FunctionComponent<{ match?: Match }> = React.memo(({ children, match }) => { if (!match) return <>{children}; const { value, indices } = match; @@ -134,10 +161,22 @@ const Result: FunctionComponent< export const SearchResults: FunctionComponent<{ query: string; results: DownshiftItem[]; + closeMenu: (cb?: () => void) => void; getMenuProps: ControllerStateAndHelpers['getMenuProps']; getItemProps: ControllerStateAndHelpers['getItemProps']; highlightedIndex: number | null; -}> = React.memo(({ query, results, getMenuProps, getItemProps, highlightedIndex }) => { +}> = React.memo(({ query, results, closeMenu, getMenuProps, getItemProps, highlightedIndex }) => { + useEffect(() => { + const handleEscape = (event: KeyboardEvent) => { + const target = event.target as Element; + if (target?.id === 'storybook-explorer-searchfield') return; // handled by downshift + closeMenu(); + }; + + document.addEventListener('keydown', handleEscape); + return () => document.removeEventListener('keydown', handleEscape); + }, []); + return ( {results.length > 0 && !query && ( @@ -155,6 +194,19 @@ export const SearchResults: FunctionComponent<{ )} {results.map((result: DownshiftItem, index) => { + if (isCloseType(result)) { + return ( + + + Back + Esc + + ); + } if (isClearType(result)) { return ( = React.memo( enableShortcuts={enableShortcuts} {...lastViewed} > - {({ query, results, isBrowsing, getMenuProps, getItemProps, highlightedIndex }) => ( + {({ + query, + results, + isBrowsing, + closeMenu, + getMenuProps, + getItemProps, + highlightedIndex, + }) => ( = React.memo( void; +} export interface ClearType { clearLastViewed: () => void; } @@ -55,12 +61,13 @@ export type SearchItem = Item & { refId: string; path: string[] }; export type SearchResult = Fuse.FuseResultWithMatches & Fuse.FuseResultWithScore; -export type DownshiftItem = SearchResult | ExpandType | ClearType; +export type DownshiftItem = SearchResult | ExpandType | ClearType | CloseType; export type SearchChildrenFn = (args: { query: string; results: DownshiftItem[]; isBrowsing: boolean; + closeMenu: (cb?: () => void) => void; getMenuProps: ControllerStateAndHelpers['getMenuProps']; getItemProps: ControllerStateAndHelpers['getItemProps']; highlightedIndex: number | null; From bb5a215f55c5a6b9bb6ae3323cea1d62dd0aa868 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 6 Nov 2020 09:58:56 +0100 Subject: [PATCH 10/11] Add missing property. --- lib/ui/src/components/sidebar/SearchResults.stories.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/ui/src/components/sidebar/SearchResults.stories.tsx b/lib/ui/src/components/sidebar/SearchResults.stories.tsx index 98b0bb2ceeca..fc7b1b471d05 100644 --- a/lib/ui/src/components/sidebar/SearchResults.stories.tsx +++ b/lib/ui/src/components/sidebar/SearchResults.stories.tsx @@ -61,6 +61,7 @@ const passKey = (props: any = {}) => ({ key: props.key }); export const searching = { query: 'query', results, + closeMenu: () => {}, getMenuProps: passKey, getItemProps: passKey, highlightedIndex: 0, @@ -72,6 +73,7 @@ export const noResults = { export const lastViewed = { query: '', results: recents, + closeMenu: () => {}, getMenuProps: passKey, getItemProps: passKey, highlightedIndex: 0, From 793ca8064b8df8d2947b301416885bbf31ee4ef4 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Fri, 6 Nov 2020 11:03:13 +0100 Subject: [PATCH 11/11] Add missing prop. --- lib/ui/src/components/sidebar/Search.stories.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/ui/src/components/sidebar/Search.stories.tsx b/lib/ui/src/components/sidebar/Search.stories.tsx index ad989c881dcd..2a5f19bb8e59 100644 --- a/lib/ui/src/components/sidebar/Search.stories.tsx +++ b/lib/ui/src/components/sidebar/Search.stories.tsx @@ -38,10 +38,11 @@ export const FilledIn = () => ( export const LastViewed = () => ( - {({ query, results, getMenuProps, getItemProps, highlightedIndex }) => ( + {({ query, results, closeMenu, getMenuProps, getItemProps, highlightedIndex }) => (