Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: Reflect search in URL #16527

Open
wants to merge 11 commits into
base: next
Choose a base branch
from
1 change: 0 additions & 1 deletion code/ui/manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@
"@storybook/theming": "workspace:*",
"@storybook/types": "workspace:*",
"@tanstack/react-virtual": "^3.3.0",
"@testing-library/react": "^11.2.2",
"@types/react-transition-group": "^4",
"@types/semver": "^7.3.4",
"browser-dtector": "^3.4.0",
Expand Down
140 changes: 82 additions & 58 deletions code/ui/manager/src/components/sidebar/Search.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React from 'react';
import type { StoryFn, Meta } from '@storybook/react';
import type { API } from '@storybook/manager-api';
import type { Meta, StoryObj } from '@storybook/react';
import { ManagerContext } from '@storybook/manager-api';
import { action } from '@storybook/addon-actions';
import { within, userEvent, expect, fn } from '@storybook/test';

import { index } from './mockdata.large';
import { Search } from './Search';
Expand All @@ -11,6 +10,7 @@ import { noResults } from './SearchResults.stories';
import { DEFAULT_REF_ID } from './Sidebar';
import type { Selection } from './types';
import { IconSymbols } from './IconSymbols';
import { LayoutProvider } from '../layout/LayoutProvider';

const refId = DEFAULT_REF_ID;
const data = { [refId]: { id: refId, url: '/', index, previewInitialized: true } };
Expand All @@ -20,76 +20,100 @@ const getLastViewed = () =>
.filter((item, i) => item.type === 'component' && item.parent && i % 20 === 0)
.map((component) => ({ storyId: component.id, refId }));

const setQueryParams = fn().mockName('setQueryParams');
const getQueryParams = fn().mockName('setQueryParams');

const meta = {
component: Search,
title: 'Sidebar/Search',
parameters: { layout: 'fullscreen' },
args: {
dataset,
getLastViewed: (): Selection[] => [],
children: () => <SearchResults {...noResults} />,
},
render: ({ children, ...args }) => {
return <Search {...args}>{children}</Search>;
},
decorators: [
(storyFn: any) => (
<div style={{ padding: 20, maxWidth: '230px' }}>
<IconSymbols />
{storyFn()}
</div>
(storyFn) => (
<ManagerContext.Provider
value={
{
state: {},
api: {
emit: () => {},
on: () => {},
off: () => {},
getShortcutKeys: () => ({ search: ['control', 'shift', 's'] }),
selectStory: () => {},
setQueryParams,
getQueryParams,
},
} as any
}
>
<LayoutProvider>
<IconSymbols />
{storyFn()}
</LayoutProvider>
</ManagerContext.Provider>
),
],
} satisfies Meta<typeof Search>;

export default meta;

const baseProps = {
dataset,
clearLastViewed: action('clear'),
getLastViewed: () => [] as Selection[],
type Story = StoryObj<typeof meta>;

export const Simple: Story = {};

export const SimpleWithCreateButton: Story = {
args: {
showCreateStoryButton: true,
},
};

export const Simple: StoryFn = () => <Search {...baseProps}>{() => null}</Search>;
export const FilledIn: Story = {
args: {
initialQuery: 'Search query',
},
};

export const SimpleWithCreateButton: StoryFn = () => (
<Search {...baseProps} showCreateStoryButton={true}>
{() => null}
</Search>
);
export const LastViewed: Story = {
args: {
getLastViewed,
},
};

export const FilledIn: StoryFn = () => (
<Search {...baseProps} initialQuery="Search query">
{() => <SearchResults {...noResults} />}
</Search>
);
export const ShortcutsDisabled: Story = {
args: {
enableShortcuts: false,
},
};

export const LastViewed: StoryFn = () => (
<Search {...baseProps} getLastViewed={getLastViewed}>
{({ query, results, closeMenu, getMenuProps, getItemProps, highlightedIndex }) => (
<SearchResults
query={query}
results={results}
closeMenu={closeMenu}
getMenuProps={getMenuProps}
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
/>
)}
</Search>
);
export const Searching: Story = {
play: async ({ canvasElement }) => {
const canvas = await within(canvasElement);
const search = await canvas.findByPlaceholderText('Find components');
await userEvent.clear(search);
await userEvent.type(search, 'foo');
expect(setQueryParams).toHaveBeenCalledWith({
search: 'foo',
});
},
};

export const ShortcutsDisabled: StoryFn = () => (
<Search {...baseProps} enableShortcuts={false}>
{() => null}
</Search>
);
export const Clearing: Story = {
play: async ({ canvasElement }) => {
const canvas = await within(canvasElement);
const search = await canvas.findByPlaceholderText('Find components');
await userEvent.clear(search);
await userEvent.type(search, 'foo');

export const CustomShortcuts: StoryFn = () => <Search {...baseProps}>{() => null}</Search>;
const clearIcon = await canvas.findByTitle('Clear search');
await userEvent.click(clearIcon);

CustomShortcuts.decorators = [
(storyFn) => (
<ManagerContext.Provider
value={
{
api: {
getShortcutKeys: () => ({ search: ['control', 'shift', 's'] }),
} as API,
} as any
}
>
{storyFn()}
</ManagerContext.Provider>
),
];
expect(setQueryParams).toHaveBeenCalledWith({ search: null });
},
};
42 changes: 33 additions & 9 deletions code/ui/manager/src/components/sidebar/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useStorybookApi, shortcutToHumanString } from '@storybook/manager-api';
import { styled } from '@storybook/theming';
import type { DownshiftState, StateChangeOptions } from 'downshift';
import type { DownshiftState, StateChangeOptions, ControllerStateAndHelpers } from 'downshift';
import Downshift from 'downshift';
import type { FuseOptions } from 'fuse.js';
import Fuse from 'fuse.js';
Expand Down Expand Up @@ -28,6 +28,8 @@ const { document } = global;

const DEFAULT_MAX_SEARCH_RESULTS = 50;

export const FILTER_KEY = 'search';

const options = {
shouldSort: true,
tokenize: true,
Expand Down Expand Up @@ -167,21 +169,23 @@ const FocusContainer = styled.div({ outline: 0 });
const isDevelopment = global.CONFIG_TYPE === 'DEVELOPMENT';
const isRendererReact = global.STORYBOOK_RENDERER === 'react';

export const Search = React.memo<{
export interface SearchProps {
children: SearchChildrenFn;
dataset: CombinedDataset;
enableShortcuts?: boolean;
getLastViewed: () => Selection[];
initialQuery?: string;
showCreateStoryButton?: boolean;
}>(function Search({
}

export const Search = React.memo(function Search({
children,
dataset,
enableShortcuts = true,
getLastViewed,
initialQuery = '',
showCreateStoryButton = isDevelopment && isRendererReact,
}) {
}: SearchProps) {
const api = useStorybookApi();
const inputRef = useRef<HTMLInputElement>(null);
const [inputPlaceholder, setPlaceholder] = useState('Find components');
Expand Down Expand Up @@ -261,9 +265,26 @@ export const Search = React.memo<{
[api]
);

const onInputValueChange = useCallback((inputValue: string, stateAndHelpers: any) => {
showAllComponents(false);
}, []);
const onInputValueChange = useCallback(
(inputValue: string, stateAndHelpers: ControllerStateAndHelpers<DownshiftItem>) => {
showAllComponents(false);
const isBrowsing = !stateAndHelpers.isOpen && document.activeElement !== inputRef.current;
api.setQueryParams({
[FILTER_KEY]: isBrowsing ? null : inputValue,
});
const params = new URLSearchParams(window.location.search);
if (window.history.replaceState) {
if (inputValue) {
params.set(FILTER_KEY, inputValue);
} else {
params.delete(FILTER_KEY);
}
const paramsString = params.size > 0 ? `?${params.toString()}` : '';
window.history.replaceState({}, '', paramsString);
}
},
[]
);

const stateReducer = useCallback(
(state: DownshiftState<DownshiftItem>, changes: StateChangeOptions<DownshiftItem>) => {
Expand Down Expand Up @@ -318,6 +339,7 @@ export const Search = React.memo<{
<Downshift<DownshiftItem>
initialInputValue={initialQuery}
stateReducer={stateReducer}
initialIsOpen={initialQuery !== ''}
// @ts-expect-error (Converted from ts-ignore)
itemToString={(result) => result?.item?.name || ''}
scrollIntoView={(e) => scrollIntoView(e)}
Expand All @@ -340,6 +362,8 @@ export const Search = React.memo<{
const input = inputValue ? inputValue.trim() : '';
let results: DownshiftItem[] = input ? getResults(input) : [];

const isBrowsing = !isOpen && document.activeElement !== inputRef.current;

const lastViewed = !input && getLastViewed();
if (lastViewed && lastViewed.length) {
results = lastViewed.reduce((acc, { storyId, refId }) => {
Expand Down Expand Up @@ -405,7 +429,7 @@ export const Search = React.memo<{
</FocusKey>
)}
{isOpen && (
<ClearIcon onClick={() => clearSelection()}>
<ClearIcon onClick={() => clearSelection()} title="Clear search">
<CloseIcon />
</ClearIcon>
)}
Expand Down Expand Up @@ -437,7 +461,7 @@ export const Search = React.memo<{
{children({
query: input,
results,
isBrowsing: !isOpen && document.activeElement !== inputRef.current,
isBrowsing,
closeMenu,
getMenuProps,
getItemProps,
Expand Down
2 changes: 2 additions & 0 deletions code/ui/manager/src/components/sidebar/Sidebar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ const meta = {
'api::getShortcutKeys'
),
selectStory: fn().mockName('api::selectStory'),
setQueryParams: fn().mockName('api::setQueryParams'),
getQueryParams: fn().mockName('api::getQueryParams'),
},
} as any
}
Expand Down
3 changes: 3 additions & 0 deletions code/ui/manager/src/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export interface SidebarProps extends API_LoadedRefData {
refId?: string;
menuHighlighted?: boolean;
enableShortcuts?: boolean;
initialQuery?: string;
onMenuClick?: HeadingProps['onMenuClick'];
showCreateStoryButton?: boolean;
}
Expand All @@ -130,6 +131,7 @@ export const Sidebar = React.memo(function Sidebar({
menuHighlighted = false,
enableShortcuts = true,
refs = {},
initialQuery,
onMenuClick,
showCreateStoryButton,
}: SidebarProps) {
Expand All @@ -154,6 +156,7 @@ export const Sidebar = React.memo(function Sidebar({
<Search
dataset={dataset}
enableShortcuts={enableShortcuts}
initialQuery={initialQuery}
showCreateStoryButton={showCreateStoryButton}
{...lastViewedProps}
>
Expand Down
2 changes: 2 additions & 0 deletions code/ui/manager/src/container/Sidebar.tsx
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Addon_TypesEnum } from '@storybook/types';

import type { SidebarProps as SidebarComponentProps } from '../components/sidebar/Sidebar';
import { Sidebar as SidebarComponent } from '../components/sidebar/Sidebar';
import { FILTER_KEY } from '../components/sidebar/Search';
import { useMenu } from './Menu';

export type Item = StoriesHash[keyof StoriesHash];
Expand Down Expand Up @@ -65,6 +66,7 @@ const Sidebar = React.memo(function Sideber({ onMenuClick }: SidebarProps) {
enableShortcuts,
bottom,
extra: top,
initialQuery: api.getQueryParam(FILTER_KEY),
};
};

Expand Down
16 changes: 1 addition & 15 deletions code/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6286,7 +6286,6 @@ __metadata:
"@storybook/theming": "workspace:*"
"@storybook/types": "workspace:*"
"@tanstack/react-virtual": "npm:^3.3.0"
"@testing-library/react": "npm:^11.2.2"
"@types/react-transition-group": "npm:^4"
"@types/semver": "npm:^7.3.4"
browser-dtector: "npm:^3.4.0"
Expand Down Expand Up @@ -7339,7 +7338,7 @@ __metadata:
languageName: node
linkType: hard

"@testing-library/dom@npm:^7.28.1, @testing-library/dom@npm:^7.29.4":
"@testing-library/dom@npm:^7.29.4":
version: 7.31.2
resolution: "@testing-library/dom@npm:7.31.2"
dependencies:
Expand Down Expand Up @@ -7434,19 +7433,6 @@ __metadata:
languageName: node
linkType: hard

"@testing-library/react@npm:^11.2.2":
version: 11.2.7
resolution: "@testing-library/react@npm:11.2.7"
dependencies:
"@babel/runtime": "npm:^7.12.5"
"@testing-library/dom": "npm:^7.28.1"
peerDependencies:
react: "*"
react-dom: "*"
checksum: 10c0/5c97aa5fb28a867d674e292e9e556b0890385e729972f8e0c3386001903e5975f6798632a038558750101fc1ff20d5faf7a0fb4d382ee3afe28d0118e9bd2f36
languageName: node
linkType: hard

"@testing-library/react@npm:^14.0.0":
version: 14.1.2
resolution: "@testing-library/react@npm:14.1.2"
Expand Down