Skip to content

Commit

Permalink
Merge pull request #13027 from storybookjs/13000-sidebar-rfcs
Browse files Browse the repository at this point in the history
UI: Fixes for Sidebar and Search
  • Loading branch information
shilman authored Nov 6, 2020
2 parents eac05fe + 59b61d3 commit 9d5071b
Show file tree
Hide file tree
Showing 9 changed files with 179 additions and 51 deletions.
6 changes: 4 additions & 2 deletions lib/ui/src/components/sidebar/Search.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -31,16 +32,17 @@ export const Simple = () => <Search {...baseProps}>{() => null}</Search>;

export const FilledIn = () => (
<Search {...baseProps} initialQuery="Search query">
{() => null}
{() => <SearchResults {...noResults} />}
</Search>
);

export const LastViewed = () => (
<Search {...baseProps} lastViewed={lastViewed}>
{({ query, results, getMenuProps, getItemProps, highlightedIndex }) => (
{({ query, results, closeMenu, getMenuProps, getItemProps, highlightedIndex }) => (
<SearchResults
query={query}
results={results}
closeMenu={closeMenu}
getMenuProps={getMenuProps}
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
Expand Down
9 changes: 9 additions & 0 deletions lib/ui/src/components/sidebar/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
isSearchResult,
isExpandType,
isClearType,
isCloseType,
} from './types';
import { searchItem } from './utils';

Expand Down Expand Up @@ -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;
}

Expand All @@ -294,6 +300,7 @@ export const Search: FunctionComponent<{
{({
isOpen,
openMenu,
closeMenu,
inputValue,
clearSelection,
getInputProps,
Expand Down Expand Up @@ -322,6 +329,7 @@ export const Search: FunctionComponent<{
}
return acc;
}, []);
results.push({ closeMenu });
if (results.length > 0) {
results.push({ clearLastViewed });
}
Expand Down Expand Up @@ -354,6 +362,7 @@ export const Search: FunctionComponent<{
query: input,
results,
isBrowsing: !isOpen && document.activeElement !== inputRef.current,
closeMenu,
getMenuProps,
getItemProps,
highlightedIndex,
Expand Down
12 changes: 10 additions & 2 deletions lib/ui/src/components/sidebar/SearchResults.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,29 @@ 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,
closeMenu: () => {},
getMenuProps: passKey,
getItemProps: passKey,
highlightedIndex: 0,
};
const lastViewed = {
export const noResults = {
...searching,
results: [] as any,
};
export const lastViewed = {
query: '',
results: recents,
closeMenu: () => {},
getMenuProps: passKey,
getItemProps: passKey,
highlightedIndex: 0,
};

export const Searching = () => <SearchResults {...searching} />;

export const NoResults = () => <SearchResults {...noResults} />;

export const LastViewed = () => <SearchResults {...lastViewed} />;
83 changes: 78 additions & 5 deletions lib/ui/src/components/sidebar/SearchResults.tsx
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -24,18 +37,31 @@ const ResultRow = styled.li<{ isHighlighted: boolean }>(({ theme, isHighlighted
cursor: 'pointer',
}));

const NoResults = styled.div(({ theme }) => ({
marginTop: 20,
textAlign: 'center',
fontSize: `${theme.typography.size.s2 - 1}px`,
lineHeight: `18px`,
color: theme.color.defaultText,
small: {
color: theme.barTextColor,
fontSize: `${theme.typography.size.s1}px`,
},
}));

const Mark = styled.mark(({ theme }) => ({
background: 'transparent',
color: theme.color.secondary,
}));

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`,
}));
Expand All @@ -48,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;
Expand Down Expand Up @@ -122,18 +161,52 @@ const Result: FunctionComponent<
export const SearchResults: FunctionComponent<{
query: string;
results: DownshiftItem[];
closeMenu: (cb?: () => void) => void;
getMenuProps: ControllerStateAndHelpers<DownshiftItem>['getMenuProps'];
getItemProps: ControllerStateAndHelpers<DownshiftItem>['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 (
<ResultsList {...getMenuProps()}>
{results.length > 0 && !query && (
<li>
<RootNode>Recently opened</RootNode>
</li>
)}
{results.length === 0 && query && (
<li>
<NoResults>
<strong>No components found</strong>
<br />
<small>Find components by name or path.</small>
</NoResults>
</li>
)}
{results.map((result: DownshiftItem, index) => {
if (isCloseType(result)) {
return (
<ActionRow
{...result}
{...getItemProps({ key: index, index, item: result })}
isHighlighted={highlightedIndex === index}
>
<ActionIcon icon="arrowleft" />
<ActionLabel>Back</ActionLabel>
<ActionKey>Esc</ActionKey>
</ActionRow>
);
}
if (isClearType(result)) {
return (
<ActionRow
Expand Down
11 changes: 10 additions & 1 deletion lib/ui/src/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,15 @@ export const Sidebar: FunctionComponent<SidebarProps> = React.memo(
enableShortcuts={enableShortcuts}
{...lastViewed}
>
{({ query, results, isBrowsing, getMenuProps, getItemProps, highlightedIndex }) => (
{({
query,
results,
isBrowsing,
closeMenu,
getMenuProps,
getItemProps,
highlightedIndex,
}) => (
<Swap condition={isBrowsing}>
<Explorer
dataset={dataset}
Expand All @@ -130,6 +138,7 @@ export const Sidebar: FunctionComponent<SidebarProps> = React.memo(
<SearchResults
query={query}
results={results}
closeMenu={closeMenu}
getMenuProps={getMenuProps}
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
Expand Down
9 changes: 8 additions & 1 deletion lib/ui/src/components/sidebar/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export interface Match {
arrayIndex: number;
}

export function isCloseType(x: any): x is CloseType {
return !!(x && x.closeMenu);
}
export function isClearType(x: any): x is ClearType {
return !!(x && x.clearLastViewed);
}
Expand All @@ -40,6 +43,9 @@ export function isSearchResult(x: any): x is SearchResult {
return !!(x && x.item);
}

export interface CloseType {
closeMenu: () => void;
}
export interface ClearType {
clearLastViewed: () => void;
}
Expand All @@ -55,12 +61,13 @@ export type SearchItem = Item & { refId: string; path: string[] };
export type SearchResult = Fuse.FuseResultWithMatches<SearchItem> &
Fuse.FuseResultWithScore<SearchItem>;

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<DownshiftItem>['getMenuProps'];
getItemProps: ControllerStateAndHelpers<DownshiftItem>['getItemProps'];
highlightedIndex: number | null;
Expand Down
43 changes: 28 additions & 15 deletions lib/ui/src/components/sidebar/useExpanded.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -108,21 +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') {
setExpanded({ ids: [parentId], value: false });
highlightElement(parentElement);
} else {
setExpanded({
ids: getDescendantIds(data, parentId, 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') {
Expand All @@ -146,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];
};
Loading

0 comments on commit 9d5071b

Please sign in to comment.