256 lines
11 KiB
JavaScript
256 lines
11 KiB
JavaScript
"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 (
|
|
* <>
|
|
* <Stack.Toolbar placement="right">
|
|
* <Stack.Toolbar.Menu icon="ellipsis.circle">
|
|
* <Stack.Toolbar.MenuAction onPress={() => Alert.alert('Action pressed!')}>
|
|
* Action 1
|
|
* </Stack.Toolbar.MenuAction>
|
|
* </Stack.Toolbar.Menu>
|
|
* </Stack.Toolbar>
|
|
* <ScreenContent />
|
|
* </>
|
|
* );
|
|
* }
|
|
* ```
|
|
*
|
|
* @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 (<NativeToolbarMenu {...props} icon={icon} xcassetName={xcassetName} image={props.image} imageRenderingMode={imageRenderingMode} label={computedLabel} title={computedMenuTitle} children={validChildren}/>);
|
|
};
|
|
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 (
|
|
* <>
|
|
* <Stack.Toolbar placement="right">
|
|
* <Stack.Toolbar.Menu icon="ellipsis.circle">
|
|
* <Stack.Toolbar.MenuAction onPress={() => alert('Action pressed!')}>
|
|
* Action 1
|
|
* </Stack.Toolbar.MenuAction>
|
|
* </Stack.Toolbar.Menu>
|
|
* </Stack.Toolbar>
|
|
* <ScreenContent />
|
|
* </>
|
|
* );
|
|
* }
|
|
* ```
|
|
*
|
|
* @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 (<NativeToolbarMenuAction {...props} icon={icon} image={props.image} imageRenderingMode={props.iconRenderingMode}/>);
|
|
};
|
|
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 (<native_1.NativeLinkPreviewAction sharesBackground={!separateBackground} hidesSharedBackground={hidesSharedBackground} hidden={hidden} icon={icon} xcassetName={xcassetName}
|
|
// TODO(@ubax): Handle image loading using useImage in a follow-up PR.
|
|
image={image} imageRenderingMode={renderingMode} destructive={destructive} subtitle={subtitle} accessibilityLabel={accessibilityLabel} accessibilityHint={accessibilityHint} displayAsPalette={palette} displayInline={inline} preferredElementSize={elementSize} tintColor={tintColor} titleStyle={titleStyle} barButtonItemStyle={variant === 'done' ? 'prominent' : variant} title={title ?? ''} label={label} onSelected={() => { }} 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
|