"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 ( { }} children={children} identifier={identifier}/>);
};
// #endregion
// #region NativeToolbarMenuAction
/**
* Native toolbar menu action - reuses LinkMenuAction.
*/
const NativeToolbarMenuAction = elements_1.LinkMenuAction;
// #endregion
const ALLOWED_CHILDREN = [
exports.StackToolbarMenu,
exports.StackToolbarMenuAction,
NativeToolbarMenu,
NativeToolbarMenuAction,
toolbar_primitives_1.StackToolbarLabel,
toolbar_primitives_1.StackToolbarIcon,
toolbar_primitives_1.StackToolbarBadge,
];
//# sourceMappingURL=StackToolbarMenu.js.map