Skip to content
Open
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.
41 changes: 30 additions & 11 deletions packages/react-native-ui-lite/src/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ import { Text, TouchableOpacity, useWindowDimensions, View, ViewStyle } from 're
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 +124,12 @@ export const Layout = ({
'desktopPanelState',
true
);

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

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

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

{isDesktop ? (
{isDesktop && hasEnabledPanels ? (
<>
{desktopAddonsPanelOpen ? (
<ResizeHandle
Expand All @@ -329,7 +340,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 +375,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 +412,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
248 changes: 138 additions & 110 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,130 +29,132 @@ export interface MobileAddonsPanelRef {
setAddonsPanelOpen: (isOpen: boolean) => void;
}

export const MobileAddonsPanel = forwardRef<MobileAddonsPanelRef, { storyId?: string }>(
({ storyId }, ref) => {
const theme = useTheme();
const { height } = useWindowDimensions();
const defaultPanelHeight = height / 2;
const positionBottomAnimation = useAnimatedValue(height / 2);
const [panelHeight, setPanelHeight] = useState(defaultPanelHeight);
const [isOpen, setIsOpen] = useState(false);
export const MobileAddonsPanel = forwardRef<
MobileAddonsPanelRef,
{ storyId?: string; parameters?: Parameters }
>(({ storyId, parameters }, ref) => {
const theme = useTheme();
const { height } = useWindowDimensions();
const defaultPanelHeight = height / 2;
const positionBottomAnimation = useAnimatedValue(height / 2);
const [panelHeight, setPanelHeight] = useState(defaultPanelHeight);
const [isOpen, setIsOpen] = useState(false);

useEffect(() => {
setPanelHeight(defaultPanelHeight);
}, [defaultPanelHeight]);
useEffect(() => {
setPanelHeight(defaultPanelHeight);
}, [defaultPanelHeight]);

const setMobileMenuOpen = useCallback(
(open: boolean) => {
setIsOpen(open);
const setMobileMenuOpen = useCallback(
(open: boolean) => {
setIsOpen(open);

if (open) {
if (open) {
setPanelHeight(defaultPanelHeight);
positionBottomAnimation.setValue(defaultPanelHeight);
Animated.timing(positionBottomAnimation, {
toValue: 0,
duration: 350,
useNativeDriver: true,
easing: Easing.inOut(Easing.cubic),
}).start();
} else {
Animated.timing(positionBottomAnimation, {
toValue: defaultPanelHeight,
duration: 350,
useNativeDriver: true,
easing: Easing.inOut(Easing.cubic),
}).start(() => {
setPanelHeight(defaultPanelHeight);
positionBottomAnimation.setValue(defaultPanelHeight);
Animated.timing(positionBottomAnimation, {
toValue: 0,
duration: 350,
useNativeDriver: true,
easing: Easing.inOut(Easing.cubic),
}).start();
} else {
Animated.timing(positionBottomAnimation, {
toValue: defaultPanelHeight,
duration: 350,
useNativeDriver: true,
easing: Easing.inOut(Easing.cubic),
}).start(() => {
setPanelHeight(defaultPanelHeight);
});
}
},
[defaultPanelHeight, positionBottomAnimation]
});
}
},
[defaultPanelHeight, positionBottomAnimation]
);

useEffect(() => {
const handleKeyboardShow = ({ endCoordinates }: KeyboardEvent) => {
if (isOpen) {
setPanelHeight((height - endCoordinates.height) / 2);
positionBottomAnimation.setValue(-endCoordinates.height);
}
};

const handleKeyboardHide = () => {
if (isOpen) {
setPanelHeight(defaultPanelHeight);
positionBottomAnimation.setValue(0);
}
};

const showSubscription = Keyboard.addListener(
Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow',
handleKeyboardShow
);
const hideSubscription = Keyboard.addListener(
Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide',
handleKeyboardHide
);

useEffect(() => {
const handleKeyboardShow = ({ endCoordinates }: KeyboardEvent) => {
if (isOpen) {
setPanelHeight((height - endCoordinates.height) / 2);
positionBottomAnimation.setValue(-endCoordinates.height);
}
};
// Clean up subscriptions on unmount
return () => {
showSubscription.remove();
hideSubscription.remove();
};
}, [defaultPanelHeight, height, positionBottomAnimation, isOpen]);

const handleKeyboardHide = () => {
if (isOpen) {
setPanelHeight(defaultPanelHeight);
positionBottomAnimation.setValue(0);
}
};

const showSubscription = Keyboard.addListener(
Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow',
handleKeyboardShow
);
const hideSubscription = Keyboard.addListener(
Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide',
handleKeyboardHide
);

// Clean up subscriptions on unmount
return () => {
showSubscription.remove();
hideSubscription.remove();
};
}, [defaultPanelHeight, height, positionBottomAnimation, isOpen]);

useImperativeHandle(ref, () => ({
setAddonsPanelOpen: (open: boolean) => {
if (open) {
setMobileMenuOpen(true);
} else {
setMobileMenuOpen(false);
}
},
}));

return (
<Animated.View
useImperativeHandle(ref, () => ({
setAddonsPanelOpen: (open: boolean) => {
if (open) {
setMobileMenuOpen(true);
} else {
setMobileMenuOpen(false);
}
},
}));

return (
<Animated.View
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: panelHeight,
transform: [{ translateY: positionBottomAnimation }],
}}
pointerEvents={isOpen ? 'auto' : 'none'}
accessibilityElementsHidden={!isOpen}
importantForAccessibility={isOpen ? 'auto' : 'no-hide-descendants'}
>
<View
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: panelHeight,
transform: [{ translateY: positionBottomAnimation }],
flex: 1,
justifyContent: 'flex-end',
}}
pointerEvents={isOpen ? 'auto' : 'none'}
accessibilityElementsHidden={!isOpen}
importantForAccessibility={isOpen ? 'auto' : 'no-hide-descendants'}
>
<View
style={{
flex: 1,
justifyContent: 'flex-end',
height: '100%',
backgroundColor: theme.background.content,
paddingTop: 10,
borderTopColor: theme.appBorderColor,
borderTopWidth: 1,
paddingBottom: Platform.OS === 'android' ? 16 : 0,
}}
>
<View
style={{
height: '100%',
backgroundColor: theme.background.content,
paddingTop: 10,
borderTopColor: theme.appBorderColor,
borderTopWidth: 1,
paddingBottom: Platform.OS === 'android' ? 16 : 0,
<AddonsTabs
onClose={() => {
setMobileMenuOpen(false);
Keyboard.dismiss();
}}
>
<AddonsTabs
onClose={() => {
setMobileMenuOpen(false);
Keyboard.dismiss();
}}
storyId={storyId}
/>
</View>
storyId={storyId}
parameters={parameters}
/>
</View>
</Animated.View>
);
}
);
</View>
</Animated.View>
);
});

MobileAddonsPanel.displayName = 'MobileAddonsPanel';

Expand Down Expand Up @@ -191,11 +194,36 @@ 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);
export const AddonsTabs = ({
onClose,
storyId,
parameters,
}: {
onClose?: () => void;
storyId?: string;
parameters?: Parameters;
}) => {
const allPanels: Addon_Collection<Addon_BaseType> = addons.getElements(Addon_TypesEnum.PANEL);

const panels = useMemo<Addon_Collection<Addon_BaseType>>(
() =>
Object.fromEntries(
Object.entries(allPanels).filter(
([, p]) => !p.paramKey || !parameters?.[p.paramKey]?.disable
)
),
[allPanels, parameters]
);

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

useEffect(() => {
if (!panels[addonSelected] && Object.keys(panels).length > 0) {
setAddonSelected(Object.keys(panels)[0]);
}
}, [panels, addonSelected]);

const panelEntries = useMemo(() => Object.entries(panels), [panels]);

const scrollContentContainerStyle = useStyle(
Expand Down
Loading
Loading