"use strict"; 'use client'; Object.defineProperty(exports, "__esModule", { value: true }); exports.StackToolbarMenuAction = exports.StackToolbarMenu = void 0; exports.convertStackToolbarMenuPropsToRNHeaderItem = convertStackToolbarMenuPropsToRNHeaderItem; exports.convertStackToolbarMenuActionPropsToRNHeaderItem = convertStackToolbarMenuActionPropsToRNHeaderItem; const react_1 = require("react"); const react_native_1 = require("react-native"); const context_1 = require("./context"); const shared_1 = require("./shared"); const toolbar_primitives_1 = require("./toolbar-primitives"); const elements_1 = require("../../../link/elements"); const native_1 = require("../../../link/preview/native"); const children_1 = require("../../../utils/children"); /** * Computes the label and menu title from children and title prop. * * - If only `title` prop is provided, it is used for both the label (button text) and menu title * - If only `.Label` child is provided, it is used for the label and the menu title is an empty string * - If both `.Label` child and `title` prop are provided. `.Label` is used for the label, and `title` is used for the menu title */ function computeMenuLabelAndTitle(children, title) { const labelChild = (0, children_1.getFirstChildOfType)(children, toolbar_primitives_1.StackToolbarLabel); const labelFromChild = labelChild?.props.children; return { label: labelFromChild ?? title ?? '', menuTitle: title ?? '', }; } /** * Use as `Stack.Toolbar.Menu` to provide menus in iOS toolbar. * It accepts `Stack.Toolbar.MenuAction` and nested `Stack.Toolbar.Menu` * elements. Menu can be configured using both component props and child * elements. * * @example * ```tsx * import { Stack } from 'expo-router'; * import { Alert } from 'react-native'; * * export default function Page() { * return ( * <> * * * Alert.alert('Action pressed!')}> * Action 1 * * * * * * ); * } * ``` * * @see [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/menus) for more information about menus on iOS. * * @platform ios */ const StackToolbarMenu = (props) => { const placement = (0, context_1.useToolbarPlacement)(); if (placement !== 'bottom') { // For placement other than bottom, this component will not render, and should be // converted to RN header item using convertStackToolbarMenuPropsToRNHeaderItem. // So if we reach here, it means we're not inside a toolbar or something else is wrong. throw new Error('Stack.Toolbar.Menu must be used inside a Stack.Toolbar'); } const validChildren = (0, react_1.useMemo)(() => (0, children_1.filterAllowedChildrenElements)(props.children, ALLOWED_CHILDREN), [props.children]); const sharedProps = convertStackToolbarMenuPropsToRNHeaderItem(props, true); const computedLabel = sharedProps?.label; const computedMenuTitle = sharedProps?.menu?.title; const icon = sharedProps?.icon?.type === 'sfSymbol' ? sharedProps.icon.name : undefined; const xcassetName = (0, shared_1.extractXcassetName)(props); const imageRenderingMode = (0, shared_1.extractIconRenderingMode)(props) ?? props.iconRenderingMode; if (process.env.NODE_ENV !== 'production') { const allChildren = react_1.Children.toArray(props.children); if (allChildren.length !== validChildren.length) { throw new Error(`Stack.Toolbar.Menu only accepts Stack.Toolbar.Menu, Stack.Toolbar.MenuAction, Stack.Toolbar.Label, Stack.Toolbar.Icon, and Stack.Toolbar.Badge as its children.`); } } if (process.env.NODE_ENV !== 'production') { const hasBadge = (0, children_1.getFirstChildOfType)(props.children, toolbar_primitives_1.StackToolbarBadge); if (hasBadge) { console.warn('Stack.Toolbar.Badge is not supported in bottom toolbar (iOS limitation). The badge will be ignored.'); } } // TODO(@ubax): Handle image loading using useImage in a follow-up PR. return (); }; exports.StackToolbarMenu = StackToolbarMenu; function convertStackToolbarMenuPropsToRNHeaderItem(props, isBottomPlacement = false) { if (props.hidden) { return undefined; } const { title, ...rest } = props; const actions = react_1.Children.toArray(props.children).filter((child) => (0, children_1.isChildOfType)(child, exports.StackToolbarMenuAction) || (0, children_1.isChildOfType)(child, exports.StackToolbarMenu)); const { label: computedLabel, menuTitle: computedMenuTitle } = computeMenuLabelAndTitle(props.children, title); const sharedProps = (0, shared_1.convertStackHeaderSharedPropsToRNSharedHeaderItem)(rest, isBottomPlacement); const item = { ...sharedProps, label: computedLabel, type: 'menu', menu: { multiselectable: true, items: actions .map((action) => { if ((0, children_1.isChildOfType)(action, exports.StackToolbarMenu)) { return convertStackToolbarSubmenuMenuPropsToRNHeaderItem(action.props); } return convertStackToolbarMenuActionPropsToRNHeaderItem(action.props); }) .filter((i) => !!i), }, }; if (computedMenuTitle) { item.menu.title = computedMenuTitle; } return item; } // Custom menu action icons are not supported in react-navigation yet // But they are supported in react-native-screens // TODO(@ubax): Remove this workaround once react-navigation supports custom icons for menu actions. // https://linear.app/expo/issue/ENG-19853/remove-custom-conversion-logic-for-icon-from-packagesexpo function convertImageIconToPlatformIcon(icon) { return icon.tinted ? { type: 'templateSource', templateSource: icon.source } : { type: 'imageSource', imageSource: icon.source }; } function convertStackToolbarSubmenuMenuPropsToRNHeaderItem(props) { if (props.hidden) { return undefined; } const sharedProps = (0, shared_1.convertStackHeaderSharedPropsToRNSharedHeaderItem)(props); const actions = react_1.Children.toArray(props.children).filter((child) => (0, children_1.isChildOfType)(child, exports.StackToolbarMenuAction) || (0, children_1.isChildOfType)(child, exports.StackToolbarMenu)); const item = { type: 'submenu', items: actions .map((action) => { if ((0, children_1.isChildOfType)(action, exports.StackToolbarMenu)) { return convertStackToolbarSubmenuMenuPropsToRNHeaderItem(action.props); } return convertStackToolbarMenuActionPropsToRNHeaderItem(action.props); }) .filter((i) => !!i), label: sharedProps.label || props.title || '', multiselectable: true, }; if (props.inline !== undefined) { item.inline = props.inline; } if (props.palette !== undefined) { item.layout = props.palette ? 'palette' : 'default'; } if (props.destructive !== undefined) { item.destructive = props.destructive; } // TODO: Add elementSize to react-native-screens if (sharedProps.icon) { if (sharedProps.icon.type === 'sfSymbol') { item.icon = sharedProps.icon; } else { item.icon = convertImageIconToPlatformIcon(sharedProps.icon); } } return item; } /** * An action item for a `Stack.Toolbar.Menu`. * * @example * ```tsx * import { Stack } from 'expo-router'; * * export default function Page() { * return ( * <> * * * alert('Action pressed!')}> * Action 1 * * * * * * ); * } * ``` * * @platform ios */ const StackToolbarMenuAction = (props) => { const placement = (0, context_1.useToolbarPlacement)(); if (placement !== 'bottom') { throw new Error('Stack.Toolbar.MenuAction must be used inside a Stack.Toolbar.Menu'); } // TODO(@ubax): Handle image loading using useImage in a follow-up PR. const icon = typeof props.icon === 'string' ? props.icon : undefined; return (); }; exports.StackToolbarMenuAction = StackToolbarMenuAction; function convertStackToolbarMenuActionPropsToRNHeaderItem(props) { const { children, isOn, unstable_keepPresented, icon, ...rest } = props; const sharedProps = (0, shared_1.convertStackHeaderSharedPropsToRNSharedHeaderItem)(props); const item = { ...rest, description: props.subtitle, type: 'action', label: sharedProps.label, state: isOn ? 'on' : 'off', onPress: props.onPress ?? (() => { }), }; if (unstable_keepPresented !== undefined) { item.keepsMenuPresented = unstable_keepPresented; } if (sharedProps.icon) { if (sharedProps.icon.type === 'sfSymbol') { item.icon = sharedProps.icon; } else { item.icon = convertImageIconToPlatformIcon(sharedProps.icon); } } return item; } /** * Native toolbar menu component for bottom toolbar. * Renders as NativeLinkPreviewAction. */ const NativeToolbarMenu = ({ accessibilityHint, accessibilityLabel, separateBackground, hidesSharedBackground, palette, inline, hidden, subtitle, title, label, destructive, children, icon, xcassetName, image, imageRenderingMode, tintColor, variant, style, elementSize, }) => { const identifier = (0, react_1.useId)(); const titleStyle = react_native_1.StyleSheet.flatten(style); const renderingMode = imageRenderingMode ?? (tintColor !== undefined ? 'template' : 'original'); return (