Skip to content
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
6 changes: 6 additions & 0 deletions .changeset/panel-disable-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@storybook/react-native-ui': minor
'@storybook/react-native-ui-lite': minor
---

feat: honor `parameters[paramKey].disable` on addon panels — matches web Storybook. When every panel is disabled for the current story, the addons UI is hidden.
22 changes: 22 additions & 0 deletions docs/docs/intro/addons/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,30 @@ export default main;
On-device addons contain React Native code that can only run on the device. When listed in the regular `addons` array, Storybook Core tries to evaluate them as presets during server-side operations (like `extract` or `build`), which fails because Node.js can't load React Native modules. The `deviceAddons` property ensures they're only loaded at runtime on the device.

For backwards compatibility, on-device addons in the `addons` array still work — they're detected by the "ondevice" substring in their name and handled correctly. However, `deviceAddons` is the recommended approach.

:::

## Hiding addon panels per story

On-device addon panels can be hidden for a story with the same `parameters[addonKey].disable` convention used by Storybook web. When every registered panel is disabled for the selected story, the addons button and panel are hidden.

```ts
export const WithoutControls = {
parameters: {
controls: { disable: true },
},
};

export const WithoutAnyAddonPanels = {
parameters: {
actions: { disable: true },
backgrounds: { disable: true },
controls: { disable: true },
notes: { disable: true },
},
};
```

## Actions

The Actions addon lets you log events and actions inside your stories. It's useful for verifying component interactions and event handling.
Expand Down
12 changes: 12 additions & 0 deletions docs/docs/intro/writing-stories.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ React Native Storybook provides several built-in parameters to control the on-de
| `hideFullScreenButton` | `boolean` | When `true`, hides the fullscreen toggle button |
| `layout` | `'padded'` \| `'centered'` \| `'fullscreen'` | Controls the layout of the story container. `'padded'` adds padding, `'centered'` centers the content, `'fullscreen'` removes any default spacing |

On-device addon panels can also be hidden per story with `parameters[addonKey].disable`. This follows the same convention as Storybook web. For example, `parameters: { controls: { disable: true } }` hides the Controls panel for that story. When every addon panel is disabled for the selected story, the addons UI is hidden.

```tsx
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-native';
Expand Down Expand Up @@ -186,6 +188,16 @@ export const EdgeToEdge: Story = {
noSafeArea: true,
},
};

// Story without the Controls addon panel
export const WithoutControls: Story = {
args: {
label: 'Button',
},
parameters: {
controls: { disable: true },
},
};
```

### Using decorators
Expand Down
4 changes: 2 additions & 2 deletions examples/expo-example/.rnstorybook/storybook.requires.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* do not change this file, it is auto generated by storybook. */
/// <reference types="@storybook/react-native/metro-env" />
import { start, updateView, View, type Features } from '@storybook/react-native';
import { start, updateView, type View, type Features } from '@storybook/react-native';


import "storybook-addon-deep-controls/register";
Expand Down Expand Up @@ -65,7 +65,7 @@ const annotations = [

globalThis.STORIES = normalizedStories;
globalThis.STORYBOOK_WEBSOCKET = {
host: '192.168.1.171',
host: '192.168.1.210',
port: 7007,
secured: false,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import type { Meta, StoryObj } from '@storybook/react-native';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { fn } from 'storybook/test';

type AddonPanelDisableDemoProps = {
label: string;
count: number;
enabled: boolean;
onPress: () => void;
};

const AddonPanelDisableDemo = ({ label, count, enabled, onPress }: AddonPanelDisableDemoProps) => (
<View style={styles.container}>
<Text style={styles.heading}>{label}</Text>
<Text style={styles.body}>Count: {count}</Text>
<Text style={styles.body}>Enabled: {enabled ? 'true' : 'false'}</Text>
<Pressable
style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
onPress={onPress}
>
<Text style={styles.buttonText}>Log action</Text>
</Pressable>
</View>
);

const meta = {
title: 'AddonPanelDisable',
component: AddonPanelDisableDemo,
args: {
label: 'Addon panel disable test',
count: 1,
enabled: true,
onPress: fn(),
},
argTypes: {
label: { control: 'text' },
count: { control: 'number' },
enabled: { control: 'boolean' },
},
parameters: {
notes: `
# Addon panel disable test

Use these stories to manually verify \`parameters[paramKey].disable\`.
`,
backgrounds: {
options: {
panelDisableLight: { name: 'Panel disable light', value: '#f7f2e8' },
panelDisableDark: { name: 'Panel disable dark', value: '#20232a' },
},
},
},
} satisfies Meta<typeof AddonPanelDisableDemo>;

export default meta;

type Story = StoryObj<typeof meta>;

export const AllPanelsEnabled: Story = {};

export const ControlsDisabled: Story = {
parameters: {
controls: { disable: true },
},
};

export const ActionsDisabled: Story = {
parameters: {
actions: { disable: true },
},
};

export const BackgroundsDisabled: Story = {
parameters: {
backgrounds: { disable: true },
},
};

export const NotesDisabled: Story = {
parameters: {
notes: { disable: true },
},
};

export const FirstPanelDisabled: Story = {
parameters: {
controls: { disable: true },
},
};

export const LastPanelOnly: Story = {
parameters: {
actions: { disable: true },
backgrounds: { disable: true },
controls: { disable: true },
},
};

export const AllPanelsDisabled: Story = {
parameters: {
actions: { disable: true },
backgrounds: { disable: true },
controls: { disable: true },
notes: { disable: true },
},
};

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 12,
padding: 24,
},
heading: {
fontSize: 20,
fontWeight: '700',
textAlign: 'center',
},
body: {
fontSize: 16,
},
button: {
backgroundColor: '#1ea7fd',
borderRadius: 4,
paddingHorizontal: 16,
paddingVertical: 10,
},
buttonText: {
color: '#ffffff',
fontWeight: '700',
},
buttonPressed: {
opacity: 0.8,
},
});
53 changes: 41 additions & 12 deletions packages/react-native-ui-lite/src/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,25 @@ import {
useStoreNumberState,
useStyle,
} from '@storybook/react-native-ui-common';
import { ReactElement, ReactNode, useCallback, useLayoutEffect, useRef, useState } from 'react';
import {
ReactElement,
ReactNode,
useCallback,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import { Text, TouchableOpacity, useWindowDimensions, View, ViewStyle } from 'react-native';
import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context';
import { SET_CURRENT_STORY } from 'storybook/internal/core-events';
import type { Args, StoryContext } from 'storybook/internal/csf';
import { type API_IndexHash } from 'storybook/internal/types';
import {
Addon_TypesEnum,
type Addon_BaseType,
type Addon_Collection,
type API_IndexHash,
} from 'storybook/internal/types';
import { addons } from 'storybook/manager-api';
import { AddonsTabs, MobileAddonsPanel, MobileAddonsPanelRef } from './MobileAddonsPanel';
import { MobileMenuDrawer, MobileMenuDrawerRef } from './MobileMenuDrawer';
Expand Down Expand Up @@ -119,6 +132,14 @@ export const Layout = ({
'desktopPanelState',
true
);

const hasEnabledPanels = useMemo(() => {
const allPanels: Addon_Collection<Addon_BaseType> = addons.getElements(Addon_TypesEnum.PANEL);
return Object.values(allPanels).some(
(p) => !p.paramKey || !story?.parameters?.[p.paramKey]?.disable
);
}, [story?.parameters]);

const [isMobileSearchActive, setIsMobileSearchActive] = useState(false);

const [sidebarWidth, setSidebarWidth] = useStoreNumberState('desktopSidebarWidth', 240);
Expand Down Expand Up @@ -317,7 +338,7 @@ export const Layout = ({
</TouchableOpacity>
)}

{isDesktop ? (
{isDesktop && hasEnabledPanels ? (
<>
{desktopAddonsPanelOpen ? (
<ResizeHandle
Expand All @@ -329,7 +350,11 @@ export const Layout = ({
) : null}
<View style={desktopAddonsPanelStyle} pointerEvents={isResizing ? 'none' : 'auto'}>
{desktopAddonsPanelOpen ? (
<AddonsTabs storyId={story?.id} onClose={() => setDesktopAddonsPanelOpen(false)} />
<AddonsTabs
storyId={story?.id}
parameters={story?.parameters}
onClose={() => setDesktopAddonsPanelOpen(false)}
/>
) : (
<IconButton
style={iconFloatRightStyle}
Expand Down Expand Up @@ -360,13 +385,15 @@ export const Layout = ({
</Text>
</Button>

<IconButton
testID="mobile-addons-button"
hitSlop={addonButtonHitSlop}
onPress={() => addonPanelRef.current.setAddonsPanelOpen(true)}
Icon={BottomBarToggleIcon}
accessibilityLabel="Open addons panel"
/>
{hasEnabledPanels && (
<IconButton
testID="mobile-addons-button"
hitSlop={addonButtonHitSlop}
onPress={() => addonPanelRef.current.setAddonsPanelOpen(true)}
Icon={BottomBarToggleIcon}
accessibilityLabel="Open addons panel"
/>
)}
</Nav>
</Container>
) : null}
Expand Down Expand Up @@ -395,7 +422,9 @@ export const Layout = ({
</MobileMenuDrawer>
)}

{isDesktop ? null : <MobileAddonsPanel ref={addonPanelRef} storyId={story?.id} />}
{!isDesktop && hasEnabledPanels ? (
<MobileAddonsPanel ref={addonPanelRef} storyId={story?.id} parameters={story?.parameters} />
) : null}
</View>
);
};
Expand Down
31 changes: 24 additions & 7 deletions packages/react-native-ui-lite/src/MobileAddonsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { styled, useTheme } from '@storybook/react-native-theming';
import { IconButton, useStyle } from '@storybook/react-native-ui-common';
import type { Parameters } from 'storybook/internal/csf';
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react';
import {
Animated,
Expand Down Expand Up @@ -28,8 +29,10 @@ export interface MobileAddonsPanelRef {
setAddonsPanelOpen: (isOpen: boolean) => void;
}

export const MobileAddonsPanel = forwardRef<MobileAddonsPanelRef, { storyId?: string }>(
({ storyId }, ref) => {
type MobileAddonsPanelProps = { storyId?: string; parameters?: Parameters };

export const MobileAddonsPanel = forwardRef<MobileAddonsPanelRef, MobileAddonsPanelProps>(
({ storyId, parameters }, ref) => {
const theme = useTheme();
const { height } = useWindowDimensions();
const defaultPanelHeight = height / 2;
Expand Down Expand Up @@ -145,6 +148,7 @@ export const MobileAddonsPanel = forwardRef<MobileAddonsPanelRef, { storyId?: st
Keyboard.dismiss();
}}
storyId={storyId}
parameters={parameters}
/>
</View>
</View>
Expand Down Expand Up @@ -191,12 +195,25 @@ const hiddenStyle = {

const hitSlop = { top: 10, right: 10, bottom: 10, left: 10 };

export const AddonsTabs = ({ onClose, storyId }: { onClose?: () => void; storyId?: string }) => {
const panels: Addon_Collection<Addon_BaseType> = addons.getElements(Addon_TypesEnum.PANEL);
type AddonsTabsProps = {
onClose?: () => void;
storyId?: string;
parameters?: Parameters;
};

export const AddonsTabs = ({ onClose, storyId, parameters }: AddonsTabsProps) => {
const panels = useMemo<Addon_Collection<Addon_BaseType>>(() => {
const allPanels: Addon_Collection<Addon_BaseType> = addons.getElements(Addon_TypesEnum.PANEL);

return Object.fromEntries(
Object.entries(allPanels).filter(([, p]) => !p.paramKey || !parameters?.[p.paramKey]?.disable)
);
}, [parameters]);

const insets = useSafeAreaInsets();
const [addonSelected, setAddonSelected] = useState(Object.keys(panels)[0]);

const panelEntries = useMemo(() => Object.entries(panels), [panels]);
const activeAddonId = panels[addonSelected] ? addonSelected : panelEntries[0]?.[0];

const scrollContentContainerStyle = useStyle(
() => ({
Expand All @@ -220,7 +237,7 @@ export const AddonsTabs = ({ onClose, storyId }: { onClose?: () => void; storyId
return (
<Tab
key={id}
active={id === addonSelected}
active={id === activeAddonId}
onPress={() => setAddonSelected(id)}
text={String(resolvedTitle)}
/>
Expand Down Expand Up @@ -252,7 +269,7 @@ export const AddonsTabs = ({ onClose, storyId }: { onClose?: () => void; storyId
</View>
) : (
panelEntries.map(([id, p]) => (
<View key={id} style={id === addonSelected ? undefined : hiddenStyle}>
<View key={id} style={id === activeAddonId ? undefined : hiddenStyle}>
<PanelRenderer panel={p} />
</View>
))
Expand Down
Loading