From a863b6a537033df99d8a633c0f21a63fa68ee474 Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Wed, 11 Mar 2026 18:25:10 +0000 Subject: [PATCH 1/4] chore: add tags handling and filtering by tag - Added '@gorhom/portal' dependency to pnpm-lock.yaml. - Updated '@legendapp/list' version from 3.0.0-beta.38 to 3.0.0-beta.42 in pnpm-lock.yaml. - Modified storybook examples to include tags for better categorization, enhancing the organization of components in the storybook. - Adjusted the storybook configuration to improve the development experience with new tags and updated parameters. --- .../.rnstorybook/storybook.requires.ts | 3 +- .../ActionExample/Actions.stories.tsx | 1 + .../BackgroundCsf.stories.tsx | 1 + .../BackgroundExample/ThemedCard.stories.tsx | 1 + .../ControlExamples/Array/Array.stories.tsx | 1 + .../Boolean/Boolean.stories.tsx | 1 + .../ControlExamples/Color/Color.stories.tsx | 1 + .../ControlExample/ControlExample.stories.tsx | 1 + .../ControlExamples/Date/Date.stories.tsx | 1 + .../ControlExamples/Number/Number.stories.tsx | 1 + .../ControlExamples/Object/Object.stories.tsx | 1 + .../ControlExamples/Radio/Radio.stories.tsx | 1 + .../SelectWithNumber.stories.tsx | 1 + .../Reproductions/UseArgsLatency.stories.tsx | 1 + .../ControlExamples/Select/Select.stories.tsx | 1 + .../ControlExamples/Text/Text.stories.tsx | 1 + .../WebCompatibility.stories.tsx | 1 + .../DeepControls/DeepControls.stories.tsx | 2 + .../HiddenControls/HiddenControls.stories.tsx | 1 + .../InputExample/TextInput.stories.tsx | 1 + .../InteractionExample.stories.tsx | 1 + .../Button/Button.stories.tsx | 1 + .../LoginForm/LoginForm.stories.tsx | 1 + .../TextInput/TextInput.stories.tsx | 1 + .../NestingExample/ChatMessage.stories.tsx | 1 + .../ChatMessageBubble.stories.tsx | 1 + .../ChatMessageBubbleAgain.stories.tsx | 1 + .../ChatMessageMessageInput.stories.tsx | 1 + .../ChatMessageReactions.stories.tsx | 1 + .../NotesExample/NotesExample.stories.tsx | 1 + .../SafeAreaInside.stories.tsx | 1 + .../SafeAreaOutside.stories.tsx | 1 + .../SafeAreaExample/UsableArea.stories.tsx | 1 + .../StoryName/StoryName.stories.tsx | 1 + .../UserProfileCard.stories.tsx | 1 + .../TestCase/TestCase.stories.tsx | 1 + .../TestCase2/TestCase2.stories.tsx | 1 + packages/react-native-ui-common/package.json | 1 + .../react-native-ui-common/src/TagsFilter.tsx | 386 ++++++++++++++++++ packages/react-native-ui-common/src/index.tsx | 1 + .../react-native-ui-common/src/util/index.ts | 1 + .../react-native-ui-common/src/util/tags.ts | 202 +++++++++ packages/react-native-ui-lite/package.json | 2 +- packages/react-native-ui-lite/src/Layout.tsx | 2 +- packages/react-native-ui-lite/src/Search.tsx | 61 ++- packages/react-native-ui-lite/src/Sidebar.tsx | 39 +- .../src/icon/iconDataUris.tsx | 21 + packages/react-native-ui/src/Layout.tsx | 20 +- packages/react-native-ui/src/Search.tsx | 62 ++- packages/react-native-ui/src/Sidebar.tsx | 40 +- .../react-native-ui/src/icon/FilterIcon.tsx | 17 + packages/react-native/src/metro/buildIndex.ts | 16 +- .../react-native/src/prepareStories.test.ts | 40 +- packages/react-native/src/prepareStories.ts | 7 +- packages/react-native/src/storyTags.ts | 32 ++ pnpm-lock.yaml | 19 +- 56 files changed, 935 insertions(+), 74 deletions(-) create mode 100644 packages/react-native-ui-common/src/TagsFilter.tsx create mode 100644 packages/react-native-ui-common/src/util/tags.ts create mode 100644 packages/react-native-ui/src/icon/FilterIcon.tsx create mode 100644 packages/react-native/src/storyTags.ts diff --git a/examples/expo-example/.rnstorybook/storybook.requires.ts b/examples/expo-example/.rnstorybook/storybook.requires.ts index 1c3ca9365c..e2a1116775 100644 --- a/examples/expo-example/.rnstorybook/storybook.requires.ts +++ b/examples/expo-example/.rnstorybook/storybook.requires.ts @@ -64,8 +64,9 @@ const annotations = [ globalThis.STORIES = normalizedStories; globalThis.STORYBOOK_WEBSOCKET = { + host: '192.168.1.172', port: 7007, - secured: true, + secured: false, }; module?.hot?.accept?.(); diff --git a/examples/expo-example/components/ActionExample/Actions.stories.tsx b/examples/expo-example/components/ActionExample/Actions.stories.tsx index 9933e415d5..381ca71b94 100644 --- a/examples/expo-example/components/ActionExample/Actions.stories.tsx +++ b/examples/expo-example/components/ActionExample/Actions.stories.tsx @@ -4,6 +4,7 @@ import { fn } from 'storybook/test'; const meta = { component: ActionButton, + tags: ['actions'], parameters: { notes: ` # Button diff --git a/examples/expo-example/components/BackgroundExample/BackgroundCsf.stories.tsx b/examples/expo-example/components/BackgroundExample/BackgroundCsf.stories.tsx index 103317e2fb..4c9fef05b6 100644 --- a/examples/expo-example/components/BackgroundExample/BackgroundCsf.stories.tsx +++ b/examples/expo-example/components/BackgroundExample/BackgroundCsf.stories.tsx @@ -11,6 +11,7 @@ const styles = StyleSheet.create({ const meta = { component: Background, + tags: ['backgrounds'], // parameters: { // backgrounds: { // default: 'warm', diff --git a/examples/expo-example/components/BackgroundExample/ThemedCard.stories.tsx b/examples/expo-example/components/BackgroundExample/ThemedCard.stories.tsx index bf0795104b..085ddd9667 100644 --- a/examples/expo-example/components/BackgroundExample/ThemedCard.stories.tsx +++ b/examples/expo-example/components/BackgroundExample/ThemedCard.stories.tsx @@ -23,6 +23,7 @@ const withAppTheme: Meta['decorators'] = [ const meta = { title: 'BackgroundExample/ThemedCard', component: ThemedCard, + tags: ['backgrounds', 'theme'], decorators: withAppTheme, args: { title: 'Themed Card', diff --git a/examples/expo-example/components/ControlExamples/Array/Array.stories.tsx b/examples/expo-example/components/ControlExamples/Array/Array.stories.tsx index f6746c6565..a870dcd25e 100644 --- a/examples/expo-example/components/ControlExamples/Array/Array.stories.tsx +++ b/examples/expo-example/components/ControlExamples/Array/Array.stories.tsx @@ -3,6 +3,7 @@ import { Array } from './Array'; const meta = { component: Array, + tags: ['controls', 'array'], args: { list: ['a', 'b', 'c'], }, diff --git a/examples/expo-example/components/ControlExamples/Boolean/Boolean.stories.tsx b/examples/expo-example/components/ControlExamples/Boolean/Boolean.stories.tsx index c8cf8f6f31..764de5ad4d 100644 --- a/examples/expo-example/components/ControlExamples/Boolean/Boolean.stories.tsx +++ b/examples/expo-example/components/ControlExamples/Boolean/Boolean.stories.tsx @@ -3,6 +3,7 @@ import { Switch } from './Boolean'; const meta = { component: Switch, + tags: ['controls', 'boolean'], } satisfies Meta; export default meta; diff --git a/examples/expo-example/components/ControlExamples/Color/Color.stories.tsx b/examples/expo-example/components/ControlExamples/Color/Color.stories.tsx index d1734cf6f8..8ac836c65b 100644 --- a/examples/expo-example/components/ControlExamples/Color/Color.stories.tsx +++ b/examples/expo-example/components/ControlExamples/Color/Color.stories.tsx @@ -9,6 +9,7 @@ const meta = { }, }, component: Color, + tags: ['controls', 'color'], } satisfies Meta; export default meta; diff --git a/examples/expo-example/components/ControlExamples/ControlExample/ControlExample.stories.tsx b/examples/expo-example/components/ControlExamples/ControlExample/ControlExample.stories.tsx index 65070ba30b..07ba723d8c 100644 --- a/examples/expo-example/components/ControlExamples/ControlExample/ControlExample.stories.tsx +++ b/examples/expo-example/components/ControlExamples/ControlExample/ControlExample.stories.tsx @@ -3,6 +3,7 @@ import { ControlExample } from './ControlExample'; const meta = { component: ControlExample, + tags: ['controls'], args: { name: 'Storyteller', age: 70, diff --git a/examples/expo-example/components/ControlExamples/Date/Date.stories.tsx b/examples/expo-example/components/ControlExamples/Date/Date.stories.tsx index 0f9d275e49..564f5ec583 100644 --- a/examples/expo-example/components/ControlExamples/Date/Date.stories.tsx +++ b/examples/expo-example/components/ControlExamples/Date/Date.stories.tsx @@ -5,6 +5,7 @@ const date = new Date(1983, 1, 25); const meta = { component: DateString, + tags: ['controls', 'date'], args: { date: date }, argTypes: { date: { control: { type: 'date' } } }, } satisfies Meta; diff --git a/examples/expo-example/components/ControlExamples/Number/Number.stories.tsx b/examples/expo-example/components/ControlExamples/Number/Number.stories.tsx index 4d037a652a..309b0f57ee 100644 --- a/examples/expo-example/components/ControlExamples/Number/Number.stories.tsx +++ b/examples/expo-example/components/ControlExamples/Number/Number.stories.tsx @@ -3,6 +3,7 @@ import { Multiply } from './Number'; const meta = { component: Multiply, + tags: ['controls', 'number'], } satisfies Meta; export default meta; diff --git a/examples/expo-example/components/ControlExamples/Object/Object.stories.tsx b/examples/expo-example/components/ControlExamples/Object/Object.stories.tsx index 9fadedc0a5..94329f0eeb 100644 --- a/examples/expo-example/components/ControlExamples/Object/Object.stories.tsx +++ b/examples/expo-example/components/ControlExamples/Object/Object.stories.tsx @@ -3,6 +3,7 @@ import { Movie } from './Object'; const meta = { component: Movie, + tags: ['controls', 'object'], args: { filmInfo: { releaseYear: 1982, diff --git a/examples/expo-example/components/ControlExamples/Radio/Radio.stories.tsx b/examples/expo-example/components/ControlExamples/Radio/Radio.stories.tsx index cd2abb2d9d..8cd1202f80 100644 --- a/examples/expo-example/components/ControlExamples/Radio/Radio.stories.tsx +++ b/examples/expo-example/components/ControlExamples/Radio/Radio.stories.tsx @@ -5,6 +5,7 @@ const radio_stations = ['104.8MHz', '909 kHz', '90FM']; const meta = { component: Radio, + tags: ['controls', 'radio'], argTypes: { selection: { diff --git a/examples/expo-example/components/ControlExamples/Reproductions/SelectWithNumber.stories.tsx b/examples/expo-example/components/ControlExamples/Reproductions/SelectWithNumber.stories.tsx index b2eb8ebdb4..e0e121fa69 100644 --- a/examples/expo-example/components/ControlExamples/Reproductions/SelectWithNumber.stories.tsx +++ b/examples/expo-example/components/ControlExamples/Reproductions/SelectWithNumber.stories.tsx @@ -4,6 +4,7 @@ import { MyButton } from './SelectWithNumber'; const meta = { component: MyButton, + tags: ['controls', 'reproduction'], } satisfies Meta; export default meta; diff --git a/examples/expo-example/components/ControlExamples/Reproductions/UseArgsLatency.stories.tsx b/examples/expo-example/components/ControlExamples/Reproductions/UseArgsLatency.stories.tsx index 206e1cfdf8..f95eeb42ef 100644 --- a/examples/expo-example/components/ControlExamples/Reproductions/UseArgsLatency.stories.tsx +++ b/examples/expo-example/components/ControlExamples/Reproductions/UseArgsLatency.stories.tsx @@ -59,6 +59,7 @@ const StorySurface = ({ const meta = { title: 'ControlExamples/Reproductions/UseArgsLatency', component: StorySurface, + tags: ['controls', 'reproduction', 'useargs'], args: { title: 'useArgs latency repro', description: 'Compare how this segmented control feels under different update strategies.', diff --git a/examples/expo-example/components/ControlExamples/Select/Select.stories.tsx b/examples/expo-example/components/ControlExamples/Select/Select.stories.tsx index 84a6058553..1c7f074702 100644 --- a/examples/expo-example/components/ControlExamples/Select/Select.stories.tsx +++ b/examples/expo-example/components/ControlExamples/Select/Select.stories.tsx @@ -5,6 +5,7 @@ const arrows = { ArrowUp: '⬆', ArrowDown: '⬇', ArrowLeft: '⬅️', ArrowRig const meta = { component: SelectExample, + tags: ['controls', 'select'], argTypes: { arrow: { options: Object.values(arrows), diff --git a/examples/expo-example/components/ControlExamples/Text/Text.stories.tsx b/examples/expo-example/components/ControlExamples/Text/Text.stories.tsx index 2162fac250..60c80dc930 100644 --- a/examples/expo-example/components/ControlExamples/Text/Text.stories.tsx +++ b/examples/expo-example/components/ControlExamples/Text/Text.stories.tsx @@ -3,6 +3,7 @@ import { Heading } from './Text'; const meta = { component: Heading, + tags: ['controls', 'text'], args: { text: 'Hello world!' }, } satisfies Meta; diff --git a/examples/expo-example/components/ControlExamples/WebCompatibility/WebCompatibility.stories.tsx b/examples/expo-example/components/ControlExamples/WebCompatibility/WebCompatibility.stories.tsx index 2f646968e4..3081f2f239 100644 --- a/examples/expo-example/components/ControlExamples/WebCompatibility/WebCompatibility.stories.tsx +++ b/examples/expo-example/components/ControlExamples/WebCompatibility/WebCompatibility.stories.tsx @@ -12,6 +12,7 @@ const ComponentExample = (props: any) => { export default { component: ComponentExample, + tags: ['controls', 'web'], argTypes: { boolean: { control: 'boolean' }, color: { control: 'color' }, diff --git a/examples/expo-example/components/DeepControls/DeepControls.stories.tsx b/examples/expo-example/components/DeepControls/DeepControls.stories.tsx index f5e19f8946..eb74cf87a8 100644 --- a/examples/expo-example/components/DeepControls/DeepControls.stories.tsx +++ b/examples/expo-example/components/DeepControls/DeepControls.stories.tsx @@ -22,6 +22,7 @@ const DeepControls = ({ const meta = { component: DeepControls, + tags: ['controls', 'deep-controls'], } satisfies Meta; export default meta; @@ -72,4 +73,5 @@ export const Basic: DeepControlsStory = { control: 'number', }, }, + tags: ['controls', 'deep-controls'], }; diff --git a/examples/expo-example/components/HiddenControls/HiddenControls.stories.tsx b/examples/expo-example/components/HiddenControls/HiddenControls.stories.tsx index 3e72313ce8..d2ae118a8d 100644 --- a/examples/expo-example/components/HiddenControls/HiddenControls.stories.tsx +++ b/examples/expo-example/components/HiddenControls/HiddenControls.stories.tsx @@ -20,6 +20,7 @@ const HiddenControls = ({ value, padding }: HiddenControlsProps) => { const meta = { component: HiddenControls, + tags: ['controls', 'conditional'], } satisfies Meta; export default meta; diff --git a/examples/expo-example/components/InputExample/TextInput.stories.tsx b/examples/expo-example/components/InputExample/TextInput.stories.tsx index 6de0a40da7..f9dc75969f 100644 --- a/examples/expo-example/components/InputExample/TextInput.stories.tsx +++ b/examples/expo-example/components/InputExample/TextInput.stories.tsx @@ -3,6 +3,7 @@ import { Input } from './TextInput'; const meta = { component: Input, + tags: ['forms', 'input'], parameters: { notes: 'Use this example to test the software keyboard related issues.', }, diff --git a/examples/expo-example/components/InteractionExample/InteractionExample.stories.tsx b/examples/expo-example/components/InteractionExample/InteractionExample.stories.tsx index 6861749ad3..d0ab0a3ae9 100644 --- a/examples/expo-example/components/InteractionExample/InteractionExample.stories.tsx +++ b/examples/expo-example/components/InteractionExample/InteractionExample.stories.tsx @@ -6,6 +6,7 @@ const Placeholder = () => ; const meta = { component: Placeholder, + tags: ['interaction', 'keyboard'], parameters: { notes: ` Use these example to test that tapping the story view will dismiss the keyboard, diff --git a/examples/expo-example/components/LoginDocsExample/Button/Button.stories.tsx b/examples/expo-example/components/LoginDocsExample/Button/Button.stories.tsx index c8ed4e14ed..4ba115bb0e 100644 --- a/examples/expo-example/components/LoginDocsExample/Button/Button.stories.tsx +++ b/examples/expo-example/components/LoginDocsExample/Button/Button.stories.tsx @@ -4,6 +4,7 @@ import { Button } from './Button'; const meta = { component: Button, + tags: ['docs', 'forms', 'button'], args: { onPress: fn(), }, diff --git a/examples/expo-example/components/LoginDocsExample/LoginForm/LoginForm.stories.tsx b/examples/expo-example/components/LoginDocsExample/LoginForm/LoginForm.stories.tsx index 6f45d5301b..d11542b9af 100644 --- a/examples/expo-example/components/LoginDocsExample/LoginForm/LoginForm.stories.tsx +++ b/examples/expo-example/components/LoginDocsExample/LoginForm/LoginForm.stories.tsx @@ -4,6 +4,7 @@ import { LoginForm } from './LoginForm'; const meta = { component: LoginForm, + tags: ['docs', 'forms', 'login'], args: { onSubmit: fn(), emailLabel: 'Email Address', diff --git a/examples/expo-example/components/LoginDocsExample/TextInput/TextInput.stories.tsx b/examples/expo-example/components/LoginDocsExample/TextInput/TextInput.stories.tsx index 330eae778e..c26cb09cf8 100644 --- a/examples/expo-example/components/LoginDocsExample/TextInput/TextInput.stories.tsx +++ b/examples/expo-example/components/LoginDocsExample/TextInput/TextInput.stories.tsx @@ -3,6 +3,7 @@ import { TextInput } from './TextInput'; const meta = { component: TextInput, + tags: ['docs', 'forms', 'input'], args: { placeholder: 'Enter text...', }, diff --git a/examples/expo-example/components/NestingExample/ChatMessage.stories.tsx b/examples/expo-example/components/NestingExample/ChatMessage.stories.tsx index 72aab9a882..389c032697 100644 --- a/examples/expo-example/components/NestingExample/ChatMessage.stories.tsx +++ b/examples/expo-example/components/NestingExample/ChatMessage.stories.tsx @@ -3,6 +3,7 @@ import { ChatMessage, ChatMessageProps } from './ChatComponents'; const meta = { component: ChatMessage, + tags: ['chat', 'message'], argTypes: { isOwn: { control: 'boolean' }, showAvatar: { control: 'boolean' }, diff --git a/examples/expo-example/components/NestingExample/ChatMessageBubble.stories.tsx b/examples/expo-example/components/NestingExample/ChatMessageBubble.stories.tsx index 96ba44a113..331a60c4e0 100644 --- a/examples/expo-example/components/NestingExample/ChatMessageBubble.stories.tsx +++ b/examples/expo-example/components/NestingExample/ChatMessageBubble.stories.tsx @@ -4,6 +4,7 @@ import { ChatBubble, ChatBubbleProps } from './ChatComponents'; const meta = { title: 'NestingExample/Message/bubble', component: ChatBubble, + tags: ['chat', 'bubble'], argTypes: { isOwn: { control: 'boolean' }, status: { diff --git a/examples/expo-example/components/NestingExample/ChatMessageBubbleAgain.stories.tsx b/examples/expo-example/components/NestingExample/ChatMessageBubbleAgain.stories.tsx index c980aa10d1..fb00ad9929 100644 --- a/examples/expo-example/components/NestingExample/ChatMessageBubbleAgain.stories.tsx +++ b/examples/expo-example/components/NestingExample/ChatMessageBubbleAgain.stories.tsx @@ -39,6 +39,7 @@ const meta = { title: 'NestingExample/Message/bubble/a very long name for a title that just keeps going and going', component: BubbleWithReactions, + tags: ['chat', 'bubble'], argTypes: { isOwn: { control: 'boolean' }, showReactions: { control: 'boolean' }, diff --git a/examples/expo-example/components/NestingExample/ChatMessageMessageInput.stories.tsx b/examples/expo-example/components/NestingExample/ChatMessageMessageInput.stories.tsx index b10f02b166..fcc824100f 100644 --- a/examples/expo-example/components/NestingExample/ChatMessageMessageInput.stories.tsx +++ b/examples/expo-example/components/NestingExample/ChatMessageMessageInput.stories.tsx @@ -47,6 +47,7 @@ const styles = StyleSheet.create({ const meta = { title: 'NestingExample/MessageInput', component: InputDemo, + tags: ['chat', 'input'], argTypes: { disabled: { control: 'boolean' }, }, diff --git a/examples/expo-example/components/NestingExample/ChatMessageReactions.stories.tsx b/examples/expo-example/components/NestingExample/ChatMessageReactions.stories.tsx index c161786ecb..e79f9b3e31 100644 --- a/examples/expo-example/components/NestingExample/ChatMessageReactions.stories.tsx +++ b/examples/expo-example/components/NestingExample/ChatMessageReactions.stories.tsx @@ -30,6 +30,7 @@ const styles = StyleSheet.create({ const meta = { title: 'NestingExample/Message/Reactions', component: ReactionsDemo, + tags: ['chat', 'reactions'], } satisfies Meta; export default meta; diff --git a/examples/expo-example/components/NotesExample/NotesExample.stories.tsx b/examples/expo-example/components/NotesExample/NotesExample.stories.tsx index 54f6149321..b0adff262f 100644 --- a/examples/expo-example/components/NotesExample/NotesExample.stories.tsx +++ b/examples/expo-example/components/NotesExample/NotesExample.stories.tsx @@ -2,6 +2,7 @@ import type { StoryObj, Meta } from '@storybook/react-native'; import { View, StyleSheet, Text } from 'react-native'; const NotesExampleMeta: Meta = { + tags: ['notes'], parameters: { notes: ` # H1 diff --git a/examples/expo-example/components/SafeAreaExample/SafeAreaInside.stories.tsx b/examples/expo-example/components/SafeAreaExample/SafeAreaInside.stories.tsx index f629598521..2a6a466905 100644 --- a/examples/expo-example/components/SafeAreaExample/SafeAreaInside.stories.tsx +++ b/examples/expo-example/components/SafeAreaExample/SafeAreaInside.stories.tsx @@ -4,6 +4,7 @@ import { ScrollView } from 'react-native'; const InsideSafeAreaMeta: Meta = { component: AButton, + tags: ['safe-area', 'inside'], argTypes: { onPress: { action: 'pressed the button' }, }, diff --git a/examples/expo-example/components/SafeAreaExample/SafeAreaOutside.stories.tsx b/examples/expo-example/components/SafeAreaExample/SafeAreaOutside.stories.tsx index 3d22ff1d01..e4d8b2e8ab 100644 --- a/examples/expo-example/components/SafeAreaExample/SafeAreaOutside.stories.tsx +++ b/examples/expo-example/components/SafeAreaExample/SafeAreaOutside.stories.tsx @@ -4,6 +4,7 @@ import { ScrollView } from 'react-native'; const OutsideSafeAreaMeta: Meta = { component: AButton, + tags: ['safe-area', 'outside'], parameters: { noSafeArea: true }, argTypes: { onPress: { action: 'pressed the button' }, diff --git a/examples/expo-example/components/SafeAreaExample/UsableArea.stories.tsx b/examples/expo-example/components/SafeAreaExample/UsableArea.stories.tsx index 6e99f9d7ae..32e757323e 100644 --- a/examples/expo-example/components/SafeAreaExample/UsableArea.stories.tsx +++ b/examples/expo-example/components/SafeAreaExample/UsableArea.stories.tsx @@ -18,6 +18,7 @@ function UsableAreaContent() { } const meta = { component: UsableAreaContent, + tags: ['safe-area', 'layout'], } satisfies Meta; export default meta; diff --git a/examples/expo-example/components/StoryName/StoryName.stories.tsx b/examples/expo-example/components/StoryName/StoryName.stories.tsx index 597b2c0cfa..3f35b14cf3 100644 --- a/examples/expo-example/components/StoryName/StoryName.stories.tsx +++ b/examples/expo-example/components/StoryName/StoryName.stories.tsx @@ -5,6 +5,7 @@ const StoryName = ({ name }: { name: string }) => {name}; const meta = { component: StoryName, + tags: ['story-name'], } satisfies Meta; export default meta; diff --git a/examples/expo-example/components/UserProfileCard/UserProfileCard.stories.tsx b/examples/expo-example/components/UserProfileCard/UserProfileCard.stories.tsx index 6d368862dd..2a823d6b9f 100644 --- a/examples/expo-example/components/UserProfileCard/UserProfileCard.stories.tsx +++ b/examples/expo-example/components/UserProfileCard/UserProfileCard.stories.tsx @@ -5,6 +5,7 @@ import { UserProfileCard } from './UserProfileCard'; const meta = { title: 'UserProfileCard', component: UserProfileCard, + tags: ['profile', 'card'], args: { name: 'Jane Doe', email: 'jane.doe@example.com', diff --git a/examples/expo-example/other_components/TestCase/TestCase.stories.tsx b/examples/expo-example/other_components/TestCase/TestCase.stories.tsx index d8a3b27476..a2e3107915 100644 --- a/examples/expo-example/other_components/TestCase/TestCase.stories.tsx +++ b/examples/expo-example/other_components/TestCase/TestCase.stories.tsx @@ -9,6 +9,7 @@ const TestCase = () => { const meta = { component: TestCase, + tags: ['testing'], } satisfies Meta; export default meta; diff --git a/examples/expo-example/other_components/TestCase2/TestCase2.stories.tsx b/examples/expo-example/other_components/TestCase2/TestCase2.stories.tsx index 4aab97065c..e511fad3d8 100644 --- a/examples/expo-example/other_components/TestCase2/TestCase2.stories.tsx +++ b/examples/expo-example/other_components/TestCase2/TestCase2.stories.tsx @@ -7,6 +7,7 @@ const TestCase2 = () => { const meta = { component: TestCase2, + tags: ['testing', 'globs'], } satisfies Meta; export default meta; diff --git a/packages/react-native-ui-common/package.json b/packages/react-native-ui-common/package.json index ff31ceedd9..99d77d5bd6 100644 --- a/packages/react-native-ui-common/package.json +++ b/packages/react-native-ui-common/package.json @@ -39,6 +39,7 @@ "typescript": "~5.9.3" }, "dependencies": { + "@gorhom/portal": "^1.0.14", "@nozbe/microfuzz": "^1.0.0", "@storybook/react": "10.3.0-alpha.14", "@storybook/react-native-theming": "^10.3.0-next.6", diff --git a/packages/react-native-ui-common/src/TagsFilter.tsx b/packages/react-native-ui-common/src/TagsFilter.tsx new file mode 100644 index 0000000000..d3d43aea5e --- /dev/null +++ b/packages/react-native-ui-common/src/TagsFilter.tsx @@ -0,0 +1,386 @@ +import { Portal } from '@gorhom/portal'; +import { styled } from '@storybook/react-native-theming'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Pressable, ScrollView, useWindowDimensions, View, ViewStyle } from 'react-native'; +import type { API_IndexHash } from 'storybook/internal/types'; +import { Button } from './Button'; +import { + type SidebarTagSelection, + createSidebarTagFilters, + getVisibleSidebarTagFilters, + hasActiveSidebarTagSelection, +} from './util/tags'; + +const Wrapper = styled.View<{ compact?: boolean }>(({ compact }) => ({ + marginBottom: compact ? 0 : 8, +})); + +const TriggerWrapper = styled.View({ + alignItems: 'flex-start', +}); + +const SelectedDot = styled.View(({ theme }) => ({ + position: 'absolute', + top: 5, + right: 5, + width: 6, + height: 6, + borderRadius: 999, + backgroundColor: theme.color.secondary, +})); + +const Panel = styled.View(({ theme }) => ({ + borderWidth: 1, + borderColor: theme.appBorderColor, + borderRadius: 8, + padding: 8, + backgroundColor: theme.background.content, + gap: 8, + boxShadow: `0 8px 24px 0 ${theme.color.border}`, + elevation: 12, +})); + +const PanelHeader = styled.View({ + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + gap: 8, +}); + +const HeaderActions = styled.View({ + minWidth: 56, + alignItems: 'flex-end', +}); + +const PanelTitle = styled.Text(({ theme }) => ({ + color: theme.color.defaultText, + fontSize: theme.typography.size.s1, + fontWeight: theme.typography.weight.bold, +})); + +const EmptyText = styled.Text(({ theme }) => ({ + color: theme.textMutedColor, + fontSize: theme.typography.size.s1, + lineHeight: 18, +})); + +const FiltersScrollView = styled(ScrollView)({ + maxHeight: 220, +}); + +const FilterRow = styled.View({ + minHeight: 40, + flexDirection: 'row', + alignItems: 'center', + gap: 8, + paddingVertical: 4, +}); + +const FilterToggle = styled.TouchableOpacity({ + flex: 1, + flexDirection: 'row', + alignItems: 'center', + gap: 10, + minHeight: 32, +}); + +const Checkbox = styled.View<{ checked: boolean }>(({ theme, checked }) => ({ + width: 18, + height: 18, + borderRadius: 4, + borderWidth: 1, + borderColor: checked ? theme.color.secondary : theme.appBorderColor, + backgroundColor: checked ? theme.color.secondary : 'transparent', + alignItems: 'center', + justifyContent: 'center', +})); + +const Checkmark = styled.Text<{ checked: boolean }>(({ theme, checked }) => ({ + color: checked ? theme.color.lightest : theme.color.defaultText, + fontSize: theme.typography.size.s1 - 1, + fontWeight: theme.typography.weight.bold, +})); + +const FilterMeta = styled.View({ + flex: 1, + gap: 2, +}); + +const FilterTitleRow = styled.View({ + flexDirection: 'row', + alignItems: 'center', + gap: 4, +}); + +const FilterTitle = styled.Text(({ theme }) => ({ + color: theme.color.defaultText, + fontSize: theme.typography.size.s1, +})); + +const FilterCount = styled.Text(({ theme }) => ({ + color: theme.textMutedColor, + fontSize: theme.typography.size.s1 - 1, +})); + +const ExcludedLabel = styled.Text(({ theme }) => ({ + color: theme.textMutedColor, + fontSize: theme.typography.size.s1 - 1, +})); + +const ExcludedCount = styled.Text(({ theme }) => ({ + color: theme.textMutedColor, + fontSize: theme.typography.size.s1 - 1, + textDecorationLine: 'line-through', +})); + +const RowActions = styled.View({ + width: 72, + alignItems: 'flex-end', +}); + +const PopoverContainer = styled.View({ + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + zIndex: 1000, +}); + +const Backdrop = styled(Pressable)({ + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, +}); + +const PANEL_GAP = 8; +const SCREEN_PADDING = 8; +const PANEL_WIDTH = 300; +const COMPACT_TRIGGER_SIZE = 24; +const compactTriggerHitSlop = { top: 6, right: 6, bottom: 6, left: 6 }; +const filtersScrollViewStyle = { + marginRight: -6, +} satisfies ViewStyle; +const filtersScrollContentStyle = { + paddingRight: 6, +} satisfies ViewStyle; +const compactTriggerStyle = { + width: COMPACT_TRIGGER_SIZE, + height: COMPACT_TRIGGER_SIZE, + paddingHorizontal: 0, +} satisfies ViewStyle; + +type TagsFilterProps = { + storiesHash?: API_IndexHash; + selection: SidebarTagSelection; + onSelectionChange: (selection: SidebarTagSelection) => void; + compact?: boolean; + TriggerIcon?: (props: { + color?: string; + width?: number; + height?: number; + size?: number; + }) => React.ReactElement; +}; + +export const TagsFilter = React.memo(function TagsFilter({ + storiesHash, + selection, + onSelectionChange, + compact = false, + TriggerIcon, +}: TagsFilterProps) { + const [isOpen, setIsOpen] = useState(false); + const [anchorRect, setAnchorRect] = useState<{ + x: number; + y: number; + width: number; + height: number; + } | null>(null); + const triggerRef = useRef(null); + const { width: windowWidth, height: windowHeight } = useWindowDimensions(); + + const filtersById = useMemo(() => createSidebarTagFilters(storiesHash), [storiesHash]); + const filters = useMemo(() => getVisibleSidebarTagFilters(filtersById), [filtersById]); + const activeCount = selection.included.size + selection.excluded.size; + const hasActiveSelection = hasActiveSidebarTagSelection(selection); + + const measureTrigger = useCallback(() => { + triggerRef.current?.measure((_x, _y, width, height, pageX, pageY) => { + setAnchorRect({ x: pageX, y: pageY, width, height }); + }); + }, []); + + useEffect(() => { + if (!isOpen) { + return; + } + + const frame = requestAnimationFrame(() => { + measureTrigger(); + }); + + return () => cancelAnimationFrame(frame); + }, [isOpen, measureTrigger, windowHeight, windowWidth]); + + const updateSelection = useCallback( + (id: string) => { + const included = new Set(selection.included); + const excluded = new Set(selection.excluded); + + if (included.has(id)) { + included.delete(id); + } else { + included.add(id); + excluded.delete(id); + } + + onSelectionChange({ included, excluded }); + }, + [onSelectionChange, selection.excluded, selection.included] + ); + + const toggleExcluded = useCallback( + (id: string) => { + const included = new Set(selection.included); + const excluded = new Set(selection.excluded); + + if (excluded.has(id)) { + excluded.delete(id); + } else { + excluded.add(id); + included.delete(id); + } + + onSelectionChange({ included, excluded }); + }, + [onSelectionChange, selection.excluded, selection.included] + ); + + const clearSelection = useCallback(() => { + onSelectionChange({ included: new Set(), excluded: new Set() }); + }, [onSelectionChange]); + + const popoverStyle = useMemo(() => { + if (!anchorRect) { + return undefined; + } + + const width = Math.min(PANEL_WIDTH, windowWidth - SCREEN_PADDING * 2); + const left = Math.min( + Math.max(SCREEN_PADDING, anchorRect.x), + windowWidth - width - SCREEN_PADDING + ); + const top = Math.min( + Math.max(SCREEN_PADDING, anchorRect.y + anchorRect.height + PANEL_GAP), + windowHeight - SCREEN_PADDING + ); + + return { + position: 'absolute', + top, + left, + width, + maxHeight: Math.max(180, windowHeight - top - SCREEN_PADDING), + }; + }, [anchorRect, windowHeight, windowWidth]); + + return ( + + + + + + {isOpen && popoverStyle ? ( + + + setIsOpen(false)} /> + + + Filter stories by tag + + diff --git a/packages/react-native-ui-lite/src/Sidebar.tsx b/packages/react-native-ui-lite/src/Sidebar.tsx index 886b7e24ad..3356b61bde 100644 --- a/packages/react-native-ui-lite/src/Sidebar.tsx +++ b/packages/react-native-ui-lite/src/Sidebar.tsx @@ -143,7 +143,6 @@ export const Sidebar = React.memo(function Sidebar({ searchFieldContent={ index ? ( Date: Wed, 11 Mar 2026 19:14:48 +0000 Subject: [PATCH 3/4] Refine search clear behavior and tags filter hit area - Show the search clear action only when focused input has text - Keep search open across blur events to avoid premature close - Adjust compact tags filter trigger hit slop on the left edge --- .../react-native-ui-common/src/TagsFilter.tsx | 2 +- packages/react-native-ui-lite/src/Search.tsx | 18 +++++------------- packages/react-native-ui/src/Search.tsx | 18 +++++------------- 3 files changed, 11 insertions(+), 27 deletions(-) diff --git a/packages/react-native-ui-common/src/TagsFilter.tsx b/packages/react-native-ui-common/src/TagsFilter.tsx index 98ddb458c8..a2997507a5 100644 --- a/packages/react-native-ui-common/src/TagsFilter.tsx +++ b/packages/react-native-ui-common/src/TagsFilter.tsx @@ -159,7 +159,7 @@ const PANEL_GAP = 8; const SCREEN_PADDING = 8; const PANEL_WIDTH = 300; const COMPACT_TRIGGER_SIZE = 24; -const compactTriggerHitSlop = { top: 6, right: 6, bottom: 6, left: 6 }; +const compactTriggerHitSlop = { top: 6, right: 6, bottom: 6, left: 0 }; const filtersScrollViewStyle = { marginRight: -6, } satisfies ViewStyle; diff --git a/packages/react-native-ui-lite/src/Search.tsx b/packages/react-native-ui-lite/src/Search.tsx index 0631268842..dd2baf7639 100644 --- a/packages/react-native-ui-lite/src/Search.tsx +++ b/packages/react-native-ui-lite/src/Search.tsx @@ -96,14 +96,7 @@ export const Search = React.memo<{ getLastViewed: () => Selection[]; initialQuery?: string; searchFieldContent?: React.ReactNode; -}>(function Search({ - children, - dataset, - setSelection, - getLastViewed, - initialQuery = '', - searchFieldContent, -}) { +}>(function Search({ children, dataset, setSelection, initialQuery = '', searchFieldContent }) { const theme = useTheme(); const inputRef = useRef(null); const [inputValue, setInputValue] = useState(initialQuery); @@ -259,24 +252,23 @@ export const Search = React.memo<{ ref={inputRef} onChangeText={setInputValue} onFocus={() => setIsOpen(true)} - onBlur={() => setIsOpen(false)} returnKeyType="search" style={{ paddingRight: 72 }} /> - - {isOpen ? ( + + {isOpen && inputValue.length > 0 && ( { setInputValue(''); - inputRef.current?.clear(); + inputRef.current.clear(); }} > - ) : null} + )} {searchFieldContent} diff --git a/packages/react-native-ui/src/Search.tsx b/packages/react-native-ui/src/Search.tsx index 70d2913933..9442fde9a9 100644 --- a/packages/react-native-ui/src/Search.tsx +++ b/packages/react-native-ui/src/Search.tsx @@ -98,14 +98,7 @@ export const Search = React.memo<{ getLastViewed: () => Selection[]; initialQuery?: string; searchFieldContent?: React.ReactNode; -}>(function Search({ - children, - dataset, - setSelection, - getLastViewed, - initialQuery = '', - searchFieldContent, -}) { +}>(function Search({ children, dataset, setSelection, initialQuery = '', searchFieldContent }) { const context = useBottomSheetInternal(true); const isBottomSheet = context !== null; @@ -262,7 +255,6 @@ export const Search = React.memo<{ ref={inputRef as any} // TODO find solution for this onChangeText={setInputValue} onFocus={() => setIsOpen(true)} - onBlur={() => setIsOpen(false)} style={{ paddingRight: 72 }} /> ) : ( @@ -270,22 +262,22 @@ export const Search = React.memo<{ ref={inputRef} onChangeText={setInputValue} onFocus={() => setIsOpen(true)} - onBlur={() => setIsOpen(false)} style={{ paddingRight: 72 }} /> )} - {isOpen ? ( + {isOpen && inputValue.length > 0 && ( { setInputValue(''); - inputRef.current?.clear(); + inputRef.current.clear(); }} + accessibilityLabel="Clear search" > - ) : null} + )} {searchFieldContent} From ff8230435fad0042489f3e46bf93cdcb7f848d5d Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Thu, 12 Mar 2026 00:58:23 +0000 Subject: [PATCH 4/4] fix: types --- packages/react-native/src/Start.tsx | 24 +++++---- packages/react-native/src/View.tsx | 54 +++++++++++++------ .../components/StoryView/ErrorBoundary.tsx | 9 ++-- .../src/components/StoryView/StoryView.tsx | 2 +- .../react-native/src/patchChannelForRN.ts | 10 +++- .../react-native/src/prepareStories.test.ts | 11 +++- packages/react-native/src/prepareStories.ts | 27 +++++++--- 7 files changed, 96 insertions(+), 41 deletions(-) diff --git a/packages/react-native/src/Start.tsx b/packages/react-native/src/Start.tsx index c186b5a308..4608798430 100644 --- a/packages/react-native/src/Start.tsx +++ b/packages/react-native/src/Start.tsx @@ -16,6 +16,11 @@ import { View } from './View'; import { prepareStories, type ReactNativeOptions } from './prepareStories'; export { prepareStories, type ReactNativeOptions } from './prepareStories'; +type StorybookGlobals = typeof globalThis & { + __STORYBOOK_ADDONS_CHANNEL__?: Channel; + __STORYBOOK_PREVIEW__?: PreviewWithSelection; +}; + /** * Since we aren't supporting these web addons yet in react native (or reimplement them) then we should disable them * to avoid running code for addons that are not supported. @@ -37,10 +42,10 @@ if (Platform.OS === 'web' && typeof globalThis.setImmediate === 'undefined') { export const getProjectAnnotations = (view: View, annotations: any[]) => async () => composeConfigs([ { - renderToCanvas: (context) => { + renderToCanvas: (context: any) => { view._setStory(context.storyContext); }, - render: (args, context) => { + render: (args: any, context: any) => { const { id, component: Component } = context; if (!Component) { @@ -107,10 +112,10 @@ export function start({ } satisfies PreviewView; const selectionStore = { - selection: null, + selection: undefined, selectionSpecifier: null, setQueryParams: () => {}, - setSelection: (selection) => { + setSelection: (selection: NonNullable) => { preview.selectionStore.selection = selection; }, } satisfies SelectionStore; @@ -118,10 +123,10 @@ export function start({ const getProjectAnnotationsInitial = async () => composeConfigs([ { - renderToCanvas: (context) => { + renderToCanvas: (context: any) => { view._setStory(context.storyContext); }, - render: (args, context) => { + render: (args: any, context: any) => { const { id, component: Component } = context; if (!Component) { @@ -145,10 +150,9 @@ export function start({ const view = new View(preview, channel); - if (global) { - global.__STORYBOOK_ADDONS_CHANNEL__ = channel; - global.__STORYBOOK_PREVIEW__ = preview; - } + const globalScope = globalThis as StorybookGlobals; + globalScope.__STORYBOOK_ADDONS_CHANNEL__ = channel; + globalScope.__STORYBOOK_PREVIEW__ = preview; view._storyIndex = index; diff --git a/packages/react-native/src/View.tsx b/packages/react-native/src/View.tsx index b0f75610b3..2cc4b490ee 100644 --- a/packages/react-native/src/View.tsx +++ b/packages/react-native/src/View.tsx @@ -27,6 +27,22 @@ import { STORYBOOK_STORY_ID_PARAM, } from './constants'; +type StorybookGlobals = typeof globalThis & { + STORYBOOK_WEBSOCKET?: { + host?: string; + port?: number; + secured?: boolean; + }; + FEATURES?: { + ondeviceBackgrounds?: boolean; + }; +}; + +const defaultStorage: Storage = { + getItem: async () => null, + setItem: async () => {}, +}; + function resolveStoryBackgroundColor( story?: StoryContext | null ): string | undefined { @@ -109,14 +125,14 @@ export type Params = { }; export class View { - _storyIndex: StoryIndex; + _storyIndex: StoryIndex = { v: 5, entries: {} }; _setStory: (story: StoryContext) => void = () => {}; _forceRerender: () => void = () => {}; _ready: boolean = false; _preview: PreviewWithSelection; - _asyncStorageStoryId: string; - _webUrl: string; - _storage: Storage; + _asyncStorageStoryId: string | null = null; + _webUrl: string = ''; + _storage: Storage = defaultStorage; _channel: Channel; _idToPrepared: Record> = {}; @@ -168,35 +184,41 @@ export class View { }; _getHost = (params: Partial = {}) => { + const globalScope = globalThis as StorybookGlobals; + if (params.host) { return params.host; } - if (globalThis.STORYBOOK_WEBSOCKET?.host) { - return globalThis.STORYBOOK_WEBSOCKET.host; + if (globalScope.STORYBOOK_WEBSOCKET?.host) { + return globalScope.STORYBOOK_WEBSOCKET.host; } return Platform.OS === 'android' ? '10.0.2.2' : 'localhost'; }; __getPort = (params: Partial = {}) => { + const globalScope = globalThis as StorybookGlobals; + if (params.port) { return params.port; } - if (globalThis.STORYBOOK_WEBSOCKET?.port) { - return globalThis.STORYBOOK_WEBSOCKET.port; + if (globalScope.STORYBOOK_WEBSOCKET?.port) { + return globalScope.STORYBOOK_WEBSOCKET.port; } return 7007; }; _isSecureConnection = (params: Partial = {}) => { + const globalScope = globalThis as StorybookGlobals; + if (typeof params.secured === 'boolean') { return params.secured; } - return globalThis.STORYBOOK_WEBSOCKET?.secured ?? false; + return globalScope.STORYBOOK_WEBSOCKET?.secured ?? false; }; _getServerChannel = (params: Partial = {}) => { @@ -260,7 +282,7 @@ export class View { const FullUI: SBUI = getFullUI(onDeviceUI && !CustomUIComponent); - this._storage = storage; + this._storage = storage ?? defaultStorage; const initialStory = this._getInitialStory(params); @@ -283,7 +305,7 @@ export class View { managerAddons.loadAddons({ store: () => ({ - fromId: (id) => { + fromId: (id: string) => { if (!this._ready) { throw new Error('Storybook is not ready yet'); } @@ -446,7 +468,7 @@ export class View { ); } - const storyBackgroundColor = globalThis.FEATURES?.ondeviceBackgrounds + const storyBackgroundColor = (globalThis as StorybookGlobals).FEATURES?.ondeviceBackgrounds ? resolveStoryBackgroundColor(story) : undefined; @@ -454,12 +476,12 @@ export class View { if (CustomUIComponent) { return ( self._channel.emit(SET_CURRENT_STORY, { storyId: newStoryId }) } - storage={storage} + storage={self._storage} theme={appliedTheme as Theme} storyBackgroundColor={storyBackgroundColor} > @@ -473,10 +495,10 @@ export class View { return ( self._channel.emit(SET_CURRENT_STORY, { storyId: newStoryId }) } diff --git a/packages/react-native/src/components/StoryView/ErrorBoundary.tsx b/packages/react-native/src/components/StoryView/ErrorBoundary.tsx index 6f172ca422..ef5d677f42 100644 --- a/packages/react-native/src/components/StoryView/ErrorBoundary.tsx +++ b/packages/react-native/src/components/StoryView/ErrorBoundary.tsx @@ -5,17 +5,20 @@ export class ErrorBoundary extends React.Component< { children: ReactNode | ReactNode[]; onError: (error: Error, stack: string) => void }, { hasError: boolean } > { - constructor(props) { + constructor(props: { + children: ReactNode | ReactNode[]; + onError: (error: Error, stack: string) => void; + }) { super(props); this.state = { hasError: false }; } - static getDerivedStateFromError(_error) { + static getDerivedStateFromError(_error: Error) { // Update state so the next render will show the fallback UI. return { hasError: true }; } - componentDidCatch(error, info) { + componentDidCatch(error: Error, info: React.ErrorInfo) { this.props.onError(error, info.componentStack); } diff --git a/packages/react-native/src/components/StoryView/StoryView.tsx b/packages/react-native/src/components/StoryView/StoryView.tsx index 70f9ff1114..35f979e0d1 100644 --- a/packages/react-native/src/components/StoryView/StoryView.tsx +++ b/packages/react-native/src/components/StoryView/StoryView.tsx @@ -52,7 +52,7 @@ const StoryView = ({ const theme = useTheme(); const containerStyle = useMemo(() => { - const layout = context?.parameters?.layout; + const layout = context?.parameters?.layout as keyof typeof layoutStyles | undefined; const layoutStyle = layout ? layoutStyles[layout] : {}; return { diff --git a/packages/react-native/src/patchChannelForRN.ts b/packages/react-native/src/patchChannelForRN.ts index c3ff7e5017..1444a33b08 100644 --- a/packages/react-native/src/patchChannelForRN.ts +++ b/packages/react-native/src/patchChannelForRN.ts @@ -1,5 +1,11 @@ import type { Channel } from 'storybook/internal/channels'; +type StorybookGlobals = typeof globalThis & { + CHANNEL_OPTIONS?: { + maxDepth?: number; + }; +}; + /** * React Native still pools synthetic events (unlike React DOM 17+). * Storybook's serializeArg only recognises web SyntheticBaseEvent, so RN @@ -63,7 +69,9 @@ export function patchChannelForRN(channel: Channel): void { * when action args contain React fiber nodes or other deep object graphs. * The default maxDepth of 15 is too deep and causes blocking + crashes. */ - globalThis.CHANNEL_OPTIONS = { + const globalScope = globalThis as StorybookGlobals; + + globalScope.CHANNEL_OPTIONS = { maxDepth: 5, }; diff --git a/packages/react-native/src/prepareStories.test.ts b/packages/react-native/src/prepareStories.test.ts index ef3a770ca7..eec9d9c1dc 100644 --- a/packages/react-native/src/prepareStories.test.ts +++ b/packages/react-native/src/prepareStories.test.ts @@ -32,6 +32,7 @@ describe('prepareStories', () => { parameters: { notes: 'Use this example to test the software keyboard related issues.', }, + tags: ['forms', 'input'], }, }, }, @@ -42,7 +43,7 @@ describe('prepareStories', () => { id: 'textinput--basic', importPath: './src/TextInput.stories.tsx', name: 'Basic', - tags: ['dev', 'test'], + tags: ['dev', 'test', 'forms', 'input'], title: 'TextInput', type: 'story', subtype: 'story', @@ -130,8 +131,14 @@ describe('prepareStories', () => { }, ], }); + // @ts-expect-error expect(result.importMap['./src/TextInput.stories.tsx'].Basic.play).toBeUndefined(); - expect(result.index.entries['textinput--basic'].tags).toEqual(['dev', 'test']); + expect(result.index.entries['textinput--basic'].tags).toEqual([ + 'dev', + 'test', + 'forms', + 'input', + ]); }); test('includes meta, story, and play tags in the generated index', () => { diff --git a/packages/react-native/src/prepareStories.ts b/packages/react-native/src/prepareStories.ts index 375131c013..fdf9368b62 100644 --- a/packages/react-native/src/prepareStories.ts +++ b/packages/react-native/src/prepareStories.ts @@ -7,6 +7,13 @@ import type { } from 'storybook/internal/types'; import { getStoryEntryTags } from './storyTags'; +type StoryModuleExport = Readonly> & { + play?: unknown; + name?: string; + storyName?: string; + tags?: unknown; +}; + /** Configuration options that are needed at startup, only serialisable values are possible */ export interface ReactNativeOptions { /** @@ -29,7 +36,7 @@ export function prepareStories({ entries: {}, }; - let importMap: Record = {}; + let importMap: Record> = {}; const makeTitle = ( fileName: string, @@ -99,17 +106,19 @@ export function prepareStories({ subtype: 'story', }; - const importedStories = req(filename); - const stories = Object.entries(importedStories).reduce( - (carry, [storyKey, story]: [string, Readonly>]) => { + const importedStories = req(filename) as Record; + const stories = Object.entries(importedStories).reduce>( + (carry, [storyKey, story]) => { if (!isExportStory(storyKey, fileExports.default)) return carry; - if (story.play && !options?.playFn) { + const typedStory = story as StoryModuleExport; + + if (typedStory.play && !options?.playFn) { // play functions are not yet fully supported on native. // There is a new option in main.js to turn them on for future use. - carry[storyKey] = { ...story, play: undefined }; + carry[storyKey] = { ...typedStory, play: undefined }; } else { - carry[storyKey] = story; + carry[storyKey] = typedStory; } return carry; }, @@ -123,7 +132,9 @@ export function prepareStories({ }); } catch (error) { const errorString = - error.message && error.stack ? `${error.message}\n ${error.stack}` : error.toString(); + error instanceof Error && error.message && error.stack + ? `${error.message}\n ${error.stack}` + : String(error); console.error(`Unexpected error while loading ${filename}: ${errorString}`); } });