diff --git a/.changeset/panel-disable-support.md b/.changeset/panel-disable-support.md new file mode 100644 index 0000000000..ef5f8fb75f --- /dev/null +++ b/.changeset/panel-disable-support.md @@ -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. diff --git a/packages/react-native-ui-lite/src/Layout.tsx b/packages/react-native-ui-lite/src/Layout.tsx index 4580f83b2d..bdf5a1e180 100644 --- a/packages/react-native-ui-lite/src/Layout.tsx +++ b/packages/react-native-ui-lite/src/Layout.tsx @@ -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'; @@ -119,6 +124,12 @@ export const Layout = ({ 'desktopPanelState', true ); + + const allPanels: Addon_Collection = 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); @@ -317,7 +328,7 @@ export const Layout = ({ )} - {isDesktop ? ( + {isDesktop && hasEnabledPanels ? ( <> {desktopAddonsPanelOpen ? ( {desktopAddonsPanelOpen ? ( - setDesktopAddonsPanelOpen(false)} /> + setDesktopAddonsPanelOpen(false)} + /> ) : ( - addonPanelRef.current.setAddonsPanelOpen(true)} - Icon={BottomBarToggleIcon} - accessibilityLabel="Open addons panel" - /> + {hasEnabledPanels && ( + addonPanelRef.current.setAddonsPanelOpen(true)} + Icon={BottomBarToggleIcon} + accessibilityLabel="Open addons panel" + /> + )} ) : null} @@ -395,7 +412,9 @@ export const Layout = ({ )} - {isDesktop ? null : } + {!isDesktop && hasEnabledPanels ? ( + + ) : null} ); }; diff --git a/packages/react-native-ui-lite/src/MobileAddonsPanel.tsx b/packages/react-native-ui-lite/src/MobileAddonsPanel.tsx index abc7934553..7ae472e5c9 100644 --- a/packages/react-native-ui-lite/src/MobileAddonsPanel.tsx +++ b/packages/react-native-ui-lite/src/MobileAddonsPanel.tsx @@ -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, @@ -28,130 +29,132 @@ export interface MobileAddonsPanelRef { setAddonsPanelOpen: (isOpen: boolean) => void; } -export const MobileAddonsPanel = forwardRef( - ({ 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 ( - ({ + setAddonsPanelOpen: (open: boolean) => { + if (open) { + setMobileMenuOpen(true); + } else { + setMobileMenuOpen(false); + } + }, + })); + + return ( + + - { + setMobileMenuOpen(false); + Keyboard.dismiss(); }} - > - { - setMobileMenuOpen(false); - Keyboard.dismiss(); - }} - storyId={storyId} - /> - + storyId={storyId} + parameters={parameters} + /> - - ); - } -); + + + ); +}); MobileAddonsPanel.displayName = 'MobileAddonsPanel'; @@ -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 = addons.getElements(Addon_TypesEnum.PANEL); +export const AddonsTabs = ({ + onClose, + storyId, + parameters, +}: { + onClose?: () => void; + storyId?: string; + parameters?: Parameters; +}) => { + const allPanels: Addon_Collection = addons.getElements(Addon_TypesEnum.PANEL); + + const panels = useMemo>( + () => + 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( diff --git a/packages/react-native-ui/src/Layout.tsx b/packages/react-native-ui/src/Layout.tsx index ae808b9160..a1a96b4de0 100644 --- a/packages/react-native-ui/src/Layout.tsx +++ b/packages/react-native-ui/src/Layout.tsx @@ -17,7 +17,12 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; 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 { DEFAULT_REF_ID } from './constants'; import { BottomBarToggleIcon } from './icon/BottomBarToggleIcon'; @@ -117,6 +122,11 @@ export const Layout = ({ true ); + const allPanels: Addon_Collection = addons.getElements(Addon_TypesEnum.PANEL); + const hasEnabledPanels = Object.values(allPanels).some( + (p) => !p.paramKey || !story?.parameters?.[p.paramKey]?.disable + ); + const [uiHidden, setUiHidden] = useState(false); useLayoutEffect(() => { @@ -270,10 +280,14 @@ export const Layout = ({ )} - {isDesktop ? ( + {isDesktop && hasEnabledPanels ? ( {desktopAddonsPanelOpen ? ( - setDesktopAddonsPanelOpen(false)} /> + setDesktopAddonsPanelOpen(false)} + /> ) : ( - addonPanelRef.current.setAddonsPanelOpen(true)} - Icon={BottomBarToggleIcon} - /> + {hasEnabledPanels && ( + addonPanelRef.current.setAddonsPanelOpen(true)} + Icon={BottomBarToggleIcon} + /> + )} ) : null} @@ -330,7 +346,9 @@ export const Layout = ({ ) : null} - {!isDesktop ? : null} + {!isDesktop && hasEnabledPanels ? ( + + ) : null} ); }; diff --git a/packages/react-native-ui/src/MobileAddonsPanel.tsx b/packages/react-native-ui/src/MobileAddonsPanel.tsx index 6a845170b4..bc632486f6 100644 --- a/packages/react-native-ui/src/MobileAddonsPanel.tsx +++ b/packages/react-native-ui/src/MobileAddonsPanel.tsx @@ -1,12 +1,13 @@ import { BottomSheetModal } from '@gorhom/bottom-sheet'; import { addons } from 'storybook/manager-api'; import { styled, useTheme } from '@storybook/react-native-theming'; +import type { Parameters } from 'storybook/internal/csf'; import { Addon_TypesEnum, type Addon_BaseType, type Addon_Collection, } from 'storybook/internal/types'; -import { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { Platform, StyleProp, Text, View, ViewStyle, useWindowDimensions } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import Animated, { @@ -31,84 +32,86 @@ const contentStyle = { flex: 1, } satisfies StyleProp; -export const MobileAddonsPanel = forwardRef( - ({ storyId }, ref) => { - const theme = useTheme(); - const reducedMotion = useReducedMotion(); - - const addonsPanelBottomSheetRef = useRef(null); - const insets = useSafeAreaInsets(); - - const animatedPosition = useSharedValue(0); - - // bringing in animated keyboard disables android resizing - // TODO replicate functionality without this - useAnimatedKeyboard(); - - useImperativeHandle(ref, () => ({ - setAddonsPanelOpen: (open: boolean) => { - if (open) { - addonsPanelBottomSheetRef.current?.present(); - } else { - addonsPanelBottomSheetRef.current?.dismiss(); - } - }, - })); - - const { height } = useWindowDimensions(); - - const adjustedBottomSheetSize = useAnimatedStyle(() => { - const extraPadding = Platform.OS === 'android' ? 32 : 16 + insets.bottom; - return { - maxHeight: height - animatedPosition.value - extraPadding, - }; - }, [animatedPosition, height, insets.bottom]); - - const backgroundStyle = useStyle(() => { - return { - borderRadius: 0, - borderTopColor: theme.appBorderColor, - borderTopWidth: 1, - backgroundColor: theme.background.content, - }; - }); - - const handleIndicatorStyle = useStyle(() => { - return { - backgroundColor: theme.textMutedColor, - }; - }); - - return ( - - - { - addonsPanelBottomSheetRef.current?.dismiss(); - }} - storyId={storyId} - /> - - - ); - } -); +export const MobileAddonsPanel = forwardRef< + MobileAddonsPanelRef, + { storyId?: string; parameters?: Parameters } +>(({ storyId, parameters }, ref) => { + const theme = useTheme(); + const reducedMotion = useReducedMotion(); + + const addonsPanelBottomSheetRef = useRef(null); + const insets = useSafeAreaInsets(); + + const animatedPosition = useSharedValue(0); + + // bringing in animated keyboard disables android resizing + // TODO replicate functionality without this + useAnimatedKeyboard(); + + useImperativeHandle(ref, () => ({ + setAddonsPanelOpen: (open: boolean) => { + if (open) { + addonsPanelBottomSheetRef.current?.present(); + } else { + addonsPanelBottomSheetRef.current?.dismiss(); + } + }, + })); + + const { height } = useWindowDimensions(); + + const adjustedBottomSheetSize = useAnimatedStyle(() => { + const extraPadding = Platform.OS === 'android' ? 32 : 16 + insets.bottom; + return { + maxHeight: height - animatedPosition.value - extraPadding, + }; + }, [animatedPosition, height, insets.bottom]); + + const backgroundStyle = useStyle(() => { + return { + borderRadius: 0, + borderTopColor: theme.appBorderColor, + borderTopWidth: 1, + backgroundColor: theme.background.content, + }; + }); + + const handleIndicatorStyle = useStyle(() => { + return { + backgroundColor: theme.textMutedColor, + }; + }); + + return ( + + + { + addonsPanelBottomSheetRef.current?.dismiss(); + }} + storyId={storyId} + parameters={parameters} + /> + + + ); +}); MobileAddonsPanel.displayName = 'MobileAddonsPanel'; @@ -148,11 +151,35 @@ 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 = addons.getElements(Addon_TypesEnum.PANEL); +export const AddonsTabs = ({ + onClose, + storyId, + parameters, +}: { + onClose?: () => void; + storyId?: string; + parameters?: Parameters; +}) => { + const allPanels: Addon_Collection = addons.getElements(Addon_TypesEnum.PANEL); + + const panels = useMemo>( + () => + Object.fromEntries( + Object.entries(allPanels).filter( + ([, p]) => !p.paramKey || !parameters?.[p.paramKey]?.disable + ) + ), + [allPanels, parameters] + ); 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 insets = useSafeAreaInsets(); const scrollContentContainerStyle = useStyle(() => {