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
31 changes: 27 additions & 4 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 = 'filter';
43081j marked this conversation as resolved.
Show resolved Hide resolved

const options = {
shouldSort: true,
tokenize: true,
Expand Down Expand Up @@ -261,8 +263,26 @@ export const Search = React.memo<{
[api]
);

const onInputValueChange = useCallback((inputValue: string, stateAndHelpers: any) => {
const onInputValueChange = useCallback((inputValue: string, stateAndHelpers: ControllerStateAndHelpers<DownshiftItem>) => {
showAllComponents(false);
const isBrowsing = !stateAndHelpers.isOpen && document.activeElement !== inputRef.current;
api.setQueryParams({
filter: 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(
Expand Down Expand Up @@ -318,6 +338,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 +361,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 @@ -374,7 +397,7 @@ export const Search = React.memo<{
// The stateReducer will handle returning to the tree view
inputRef.current.blur();
}
},
}
});

const labelProps = getLabelProps({
Expand Down Expand Up @@ -437,7 +460,7 @@ export const Search = React.memo<{
{children({
query: input,
results,
isBrowsing: !isOpen && document.activeElement !== inputRef.current,
isBrowsing,
closeMenu,
getMenuProps,
getItemProps,
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
136 changes: 136 additions & 0 deletions code/ui/manager/src/components/sidebar/__tests__/Search.test.tsx
43081j marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { type Mock, beforeEach, afterEach, describe, test, expect, vi } from 'vitest';
import React from 'react';
import { render, screen, cleanup, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as api from '@storybook/manager-api';
import { ThemeProvider, themes, convert } from '@storybook/theming';

import { FILTER_KEY, Search } from '../Search';
import type {
CombinedDataset,
SearchChildrenFn,
Selection,
} from '../types';

const FILTER_VALUE = 'filter';
const PLACEHOLDER = 'Find components';

const renderSearch = (props?: Partial<Parameters<typeof Search>[0]>) => {
const dataset: CombinedDataset = {
hash: {},
entries: []
};
const getLastViewed = (): Selection[] => [];
const children: SearchChildrenFn = () => (
<></>
);
render(
<ThemeProvider theme={convert(themes.light)}>
<Search
{...({
dataset,
getLastViewed,
children,
...props
})}
>
</Search>
</ThemeProvider>
);
};

vi.mock('@storybook/manager-api');

describe('Search', () => {
let setQueryParams: Mock;
let getQueryParams: Mock;
let queryParams: Record<string, string>;

beforeEach(() => {
vi.mocked(api.useStorybookApi).mockReset();
queryParams = {};
setQueryParams = vi.fn((params) => {
queryParams = params;
});
getQueryParams = vi.fn(() => queryParams);
const mockApi: Partial<api.API> = {
setQueryParams,
getQueryParams,
getShortcutKeys: () => ({
aboutPage: [],
collapseAll: [],
escape: [],
expandAll: [],
focusIframe: [],
focusNav: [],
focusPanel: [],
fullScreen: [],
nextComponent: [],
nextStory: [],
panelPosition: [],
prevComponent: [],
prevStory: [],
remount: [],
search: [],
shortcutsPage: [],
toggleNav: [],
togglePanel: [],
toolbar: []
})
};
vi.mocked(api.useStorybookApi).mockReturnValue(mockApi as any);
vi.mocked(api.useStorybookState).mockReset();
});

afterEach(() => {
cleanup();
});

test('renders OK', async () => {
renderSearch();
const INPUT = screen.getByPlaceholderText(PLACEHOLDER) as HTMLInputElement;
expect(INPUT.value).toBe('');
});

test('prefills input with initial query', async () => {
const state: Partial<api.State> = {
storyId: 'jest',
customQueryParams: {
[FILTER_KEY]: 'filter',
},
ui: { enableShortcuts: true },
};
vi.mocked(api.useStorybookState).mockReturnValue(state as any);
renderSearch({initialQuery: FILTER_VALUE});
const INPUT = screen.getByPlaceholderText(PLACEHOLDER) as HTMLInputElement;
expect(INPUT.value).toBe(FILTER_VALUE);
expect(setQueryParams).not.toHaveBeenCalled();
expect(queryParams).toEqual({});
});

test('updates location on input update with current query', async () => {
renderSearch({initialQuery: 'foo'});
const INPUT = screen.getByPlaceholderText(PLACEHOLDER) as HTMLInputElement;
await act(async () => {
await userEvent.clear(INPUT);
await userEvent.type(INPUT, FILTER_VALUE);
});
expect(setQueryParams).toHaveBeenCalled();
expect(queryParams).toEqual({
[FILTER_KEY]: FILTER_VALUE
});
});

test('updates location on input update without current query', async () => {
renderSearch();
const INPUT = screen.getByPlaceholderText(PLACEHOLDER) as HTMLInputElement;
await act(async () => {
await userEvent.clear(INPUT);
await userEvent.type(INPUT, FILTER_VALUE);
});
expect(setQueryParams).toHaveBeenCalled();
expect(queryParams).toEqual({
[FILTER_KEY]: FILTER_VALUE
});
});
});
1 change: 1 addition & 0 deletions code/ui/manager/src/container/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const Sidebar = React.memo(function Sideber({ onMenuClick }: SidebarProps) {
enableShortcuts,
bottom,
extra: top,
initialQuery: api.getQueryParam('filter'),
};
};

Expand Down
16 changes: 1 addition & 15 deletions code/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6285,7 +6285,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 @@ -7326,7 +7325,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 @@ -7421,19 +7420,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