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: Add preloading to stories highlighted in the sidebar #17964

Merged
merged 2 commits into from
Jul 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion code/lib/api/src/modules/channel.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-param-reassign */
import { STORIES_COLLAPSE_ALL, STORIES_EXPAND_ALL } from '@storybook/core-events';
import { Channel } from '@storybook/channels';
import type { Listener } from '@storybook/channels';
Expand Down Expand Up @@ -26,7 +27,19 @@ export const init: ModuleFn<SubAPI, SubState> = ({ provider }) => {
},
off: (type, cb) => provider.channel.removeListener(type, cb),
once: (type, cb) => provider.channel.once(type, cb),
emit: (type, ...args) => provider.channel.emit(type, ...args),
emit: (type, data, ...args) => {
if (
data?.options?.target &&
data.options.target !== 'storybook-preview-iframe' &&
!data.options.target.startsWith('storybook-ref-')
) {
data.options.target =
data.options.target !== 'storybook_internal'
? `storybook-ref-${data.options.target}`
: 'storybook-preview-iframe';
}
provider.channel.emit(type, data, ...args);
},

collapseAll: () => {
provider.channel.emit(STORIES_COLLAPSE_ALL, {});
Expand Down
17 changes: 8 additions & 9 deletions code/lib/api/src/modules/stories.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import global from 'global';
import { toId, sanitize } from '@storybook/csf';
import {
PRELOAD_STORIES,
PRELOAD_ENTRIES,
STORY_PREPARED,
UPDATE_STORY_ARGS,
RESET_STORY_ARGS,
Expand Down Expand Up @@ -320,19 +320,15 @@ export const init: ModuleFn<SubAPI, SubState, true> = ({
fullAPI.emit(UPDATE_STORY_ARGS, {
storyId,
updatedArgs,
options: {
target: refId ? `storybook-ref-${refId}` : 'storybook-preview-iframe',
},
options: { target: refId },
});
},
resetStoryArgs: (story, argNames?: [string]) => {
const { id: storyId, refId } = story;
fullAPI.emit(RESET_STORY_ARGS, {
storyId,
argNames,
options: {
target: refId ? `storybook-ref-${refId}` : 'storybook-preview-iframe',
},
options: { target: refId },
});
},
fetchStoryList: async () => {
Expand Down Expand Up @@ -448,7 +444,7 @@ export const init: ModuleFn<SubAPI, SubState, true> = ({
}

if (sourceType === 'local') {
const { storyId, storiesHash } = store.getState();
const { storyId, storiesHash, refId } = store.getState();

// create a list of related stories to be preloaded
const toBePreloaded = Array.from(
Expand All @@ -458,7 +454,10 @@ export const init: ModuleFn<SubAPI, SubState, true> = ({
])
).filter(Boolean);

fullAPI.emit(PRELOAD_STORIES, toBePreloaded);
fullAPI.emit(PRELOAD_ENTRIES, {
ids: toBePreloaded,
options: { target: refId },
});
}
});

Expand Down
10 changes: 5 additions & 5 deletions code/lib/api/src/tests/stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ describe('stories API', () => {
storyId: 'a--1',
updatedArgs: { foo: 'bar' },
options: {
target: 'storybook-preview-iframe',
target: undefined,
},
});

Expand Down Expand Up @@ -654,12 +654,12 @@ describe('stories API', () => {
storyId: 'a--1',
updatedArgs: { foo: 'bar' },
options: {
target: 'storybook-ref-refId',
target: 'refId',
},
});
});

it('resetStoryArgs emits RESET_STORY_ARGS to the local frame and does not change anything', () => {
it('refId to the local frame and does not change anything', () => {
const navigate = jest.fn();
const emit = jest.fn();
const on = jest.fn();
Expand All @@ -681,7 +681,7 @@ describe('stories API', () => {
storyId: 'a--1',
argNames: ['foo'],
options: {
target: 'storybook-preview-iframe',
target: undefined,
},
});

Expand Down Expand Up @@ -712,7 +712,7 @@ describe('stories API', () => {
storyId: 'a--1',
argNames: ['foo'],
options: {
target: 'storybook-ref-refId',
target: 'refId',
},
});
});
Expand Down
4 changes: 2 additions & 2 deletions code/lib/core-events/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ enum events {
// Force the current story to re-render from scratch, with its initial args
FORCE_REMOUNT = 'forceRemount',
// Request the story has been loaded into the store, ahead of time, before it's actually
PRELOAD_STORIES = 'preloadStories',
PRELOAD_ENTRIES = 'preloadStories',
// The story has been loaded into the store, we have parameters/args/etc
STORY_PREPARED = 'storyPrepared',
// The next 6 events are emitted by the StoryRenderer when rendering the current story
Expand Down Expand Up @@ -78,7 +78,7 @@ export const {
STORY_PREPARED,
STORY_CHANGED,
STORY_UNCHANGED,
PRELOAD_STORIES,
PRELOAD_ENTRIES,
STORY_RENDERED,
STORY_MISSING,
STORY_ERRORED,
Expand Down
8 changes: 4 additions & 4 deletions code/lib/preview-web/src/PreviewWeb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1104,7 +1104,7 @@ describe('PreviewWeb', () => {
await waitForRender();

importFn.mockClear();
await preview.onPreloadStories(['component-two--c']);
await preview.onPreloadStories({ ids: ['component-two--c'] });
expect(importFn).toHaveBeenCalledWith('./src/ComponentTwo.stories.js');
});

Expand All @@ -1114,7 +1114,7 @@ describe('PreviewWeb', () => {
await waitForRender();

importFn.mockClear();
await preview.onPreloadStories(['component-one--docs']);
await preview.onPreloadStories({ ids: ['component-one--docs'] });
expect(importFn).toHaveBeenCalledWith('./src/ComponentOne.stories.js');
});

Expand All @@ -1124,7 +1124,7 @@ describe('PreviewWeb', () => {
await waitForRender();

importFn.mockClear();
await preview.onPreloadStories(['introduction--docs']);
await preview.onPreloadStories({ ids: ['introduction--docs'] });
expect(importFn).toHaveBeenCalledWith('./src/Introduction.mdx');
});
it('loads imports of modern docs entries', async () => {
Expand All @@ -1133,7 +1133,7 @@ describe('PreviewWeb', () => {
await waitForRender();

importFn.mockClear();
await preview.onPreloadStories(['introduction--docs']);
await preview.onPreloadStories({ ids: ['introduction--docs'] });
expect(importFn).toHaveBeenCalledWith('./src/ComponentTwo.stories.js');
});
});
Expand Down
14 changes: 10 additions & 4 deletions code/lib/preview-web/src/PreviewWeb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import global from 'global';
import {
CURRENT_STORY_WAS_SET,
IGNORED_EXCEPTION,
PRELOAD_STORIES,
PRELOAD_ENTRIES,
PREVIEW_KEYDOWN,
SET_CURRENT_STORY,
SET_STORIES,
Expand Down Expand Up @@ -93,7 +93,7 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew

this.channel.on(SET_CURRENT_STORY, this.onSetCurrentStory.bind(this));
this.channel.on(UPDATE_QUERY_PARAMS, this.onUpdateQueryParams.bind(this));
this.channel.on(PRELOAD_STORIES, this.onPreloadStories.bind(this));
this.channel.on(PRELOAD_ENTRIES, this.onPreloadStories.bind(this));
}

initializeWithProjectAnnotations(projectAnnotations: WebProjectAnnotations<TFramework>) {
Expand Down Expand Up @@ -240,8 +240,14 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
super.onUpdateArgs({ storyId, updatedArgs });
}

async onPreloadStories(ids: string[]) {
await Promise.all(ids.map((id) => this.storyStore.loadEntry(id)));
async onPreloadStories({ ids }: { ids: string[] }) {
/**
* It's possible that we're trying to preload a story in a ref we haven't loaded the iframe for yet.
* Because of the way the targeting works, if we can't find the targeted iframe,
* we'll use the currently active iframe which can cause the event to be targeted
* to the wrong iframe, causing an error if the storyId does not exists there.
*/
await Promise.allSettled(ids.map((id) => this.storyStore.loadEntry(id)));
}

// RENDERING
Expand Down
4 changes: 1 addition & 3 deletions code/lib/ui/src/components/preview/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,7 @@ const Preview = React.memo<PreviewProps>((props) => {
api.emit(SET_CURRENT_STORY, {
storyId: id,
viewMode,
options: {
target: refId ? `storybook-ref-${refId}` : 'storybook-preview-iframe',
},
options: { target: refId },
});
}
}
Expand Down
31 changes: 31 additions & 0 deletions code/lib/ui/src/components/sidebar/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import global from 'global';
import React, { FC, MouseEventHandler, ReactNode, useCallback, useEffect } from 'react';
import { ControllerStateAndHelpers } from 'downshift';

import { useStorybookApi } from '@storybook/api';
import { PRELOAD_ENTRIES } from '@storybook/core-events';
import { ComponentNode, DocumentNode, Path, RootNode, StoryNode } from './TreeNode';
import {
Match,
Expand Down Expand Up @@ -121,6 +123,17 @@ const Result: FC<
[onClick]
);

const api = useStorybookApi();
useEffect(() => {
if (api && props.isHighlighted && item.isComponent) {
api.emit(
PRELOAD_ENTRIES,
{ ids: [item.isLeaf ? item.id : item.children[0]] },
{ options: { target: item.refId } }
);
}
}, [props.isHighlighted, item]);

const nameMatch = matches.find((match: Match) => match.key === 'name');
const pathMatches = matches.filter((match: Match) => match.key === 'path');
const label = (
Expand Down Expand Up @@ -175,6 +188,7 @@ export const SearchResults: FC<{
isLoading = false,
enableShortcuts = true,
}) => {
const api = useStorybookApi();
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (!enableShortcuts || isLoading || event.repeat) return;
Expand All @@ -190,6 +204,20 @@ export const SearchResults: FC<{
return () => document.removeEventListener('keydown', handleEscape);
}, [enableShortcuts, isLoading]);

const mouseOverHandler = useCallback((event: MouseEvent) => {
const currentTarget = event.currentTarget as HTMLElement;
const storyId = currentTarget.getAttribute('data-id');
const refId = currentTarget.getAttribute('data-refid');
const item = api.getData(storyId, refId === 'storybook_internal' ? undefined : refId);

if (item.isComponent) {
api.emit(PRELOAD_ENTRIES, {
ids: [item.isLeaf ? item.id : item.children[0]],
options: { target: refId },
});
}
}, []);

return (
<ResultsList {...getMenuProps()}>
{results.length > 0 && !query && (
Expand Down Expand Up @@ -255,6 +283,9 @@ export const SearchResults: FC<{
{...result}
{...getItemProps({ key, index, item: result })}
isHighlighted={highlightedIndex === index}
data-id={result.item.id}
data-refid={result.item.refId}
onMouseOver={mouseOverHandler}
className="search-result-item"
/>
);
Expand Down
11 changes: 11 additions & 0 deletions code/lib/ui/src/components/sidebar/Tree.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { useStorybookApi } from '@storybook/api';
import type { StoriesHash, GroupEntry, ComponentEntry, StoryEntry } from '@storybook/api';
import { styled } from '@storybook/theming';
import { Button, Icons } from '@storybook/components';
import { transparentize } from 'polished';
import React, { MutableRefObject, useCallback, useMemo, useRef } from 'react';

import { PRELOAD_ENTRIES } from '@storybook/core-events';
import {
ComponentNode,
DocumentNode,
Expand Down Expand Up @@ -147,6 +149,7 @@ const Node = React.memo<NodeProps>(
setExpanded,
onSelectStoryId,
}) => {
const api = useStorybookApi();
if (!isDisplayed) return null;

const id = createId(item.id, refId);
Expand Down Expand Up @@ -246,6 +249,14 @@ const Node = React.memo<NodeProps>(
setExpanded({ ids: [item.id], value: !isExpanded });
if (item.type === 'component' && !isExpanded) onSelectStoryId(item.id);
}}
onMouseEnter={() => {
if (item.isComponent) {
api.emit(PRELOAD_ENTRIES, {
ids: [item.children[0]],
options: { target: refId },
});
}
}}
>
{(item.renderLabel as (i: typeof item) => React.ReactNode)?.(item) || item.name}
</BranchNode>
Expand Down
14 changes: 14 additions & 0 deletions code/lib/ui/src/components/sidebar/useHighlighted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
useRef,
useState,
} from 'react';
import { useStorybookApi } from '@storybook/api';
import { PRELOAD_ENTRIES } from '@storybook/core-events';
import { matchesKeyCode, matchesModifiers } from '../../keybinding';

import { CombinedDataset, Highlight, Selection } from './types';
Expand Down Expand Up @@ -40,6 +42,7 @@ export const useHighlighted = ({
const initialHighlight = fromSelection(selected);
const highlightedRef = useRef<Highlight>(initialHighlight);
const [highlighted, setHighlighted] = useState<Highlight>(initialHighlight);
const api = useStorybookApi();

const updateHighlighted = useCallback(
(highlight) => {
Expand Down Expand Up @@ -109,6 +112,17 @@ export const useHighlighted = ({
const nextIndex = cycle(highlightable, currentIndex, isArrowUp ? -1 : 1);
const didRunAround = isArrowUp ? nextIndex === highlightable.length - 1 : nextIndex === 0;
highlightElement(highlightable[nextIndex], didRunAround);

if (highlightable[nextIndex].getAttribute('data-nodetype') === 'component') {
const { itemId, refId } = highlightedRef.current;
const item = api.getData(itemId, refId === 'storybook_internal' ? undefined : refId);
if (item.isComponent) {
api.emit(PRELOAD_ENTRIES, {
ids: [item.isLeaf ? item.id : item.children[0]],
options: { target: refId },
});
}
}
});
};

Expand Down
2 changes: 1 addition & 1 deletion code/lib/ui/src/globals/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export default {
'IGNORED_EXCEPTION',
'NAVIGATE_URL',
'PLAY_FUNCTION_THREW_EXCEPTION',
'PRELOAD_STORIES',
'PRELOAD_ENTRIES',
'PREVIEW_KEYDOWN',
'REGISTER_SUBSCRIPTION',
'RESET_STORY_ARGS',
Expand Down