one
Version:
One is a React framework for web, iOS, and Android apps.
455 lines (452 loc) • 18 kB
JavaScript
import { Children, isValidElement } from "react";
const TOOLBAR_KIND = "__oneStackToolbarKind";
function kindOf(element) {
if (!isValidElement(element)) return void 0;
return element.type?.[TOOLBAR_KIND];
}
function isKind(element, kind) {
return kindOf(element) === kind;
}
function warn(message) {
if (process.env.NODE_ENV !== "production") {
console.warn(message);
}
}
function childName(child) {
if (!isValidElement(child)) return "unknown";
const type = child.type;
if (typeof type === "string") return type;
if (typeof type === "symbol") return "Fragment";
return type?.name ?? "unknown";
}
function fallbackIdentifier(slot, index, title) {
const slug = (title ?? "item").toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24) || "item";
return `stack-toolbar-${slot}-${slug}-${index}`;
}
function firstChildOfKind(children, kind) {
let found = null;
Children.forEach(children, (child) => {
if (found || !isValidElement(child)) return;
if (isKind(child, kind)) found = child;
});
return found;
}
function toolbarChildrenToString(children) {
return Children.toArray(children).filter((child) => typeof child === "string" || typeof child === "number").join("");
}
function resolveIcon(iconProp, children) {
const iconChild = firstChildOfKind(children, "icon");
if (iconChild) {
const props = iconChild.props;
if ("sf" in props) return {
form: "sf",
name: props.sf
};
if ("xcasset" in props) return {
form: "xcasset",
name: props.xcasset,
renderingMode: props.renderingMode
};
return {
form: "src",
source: props.src,
renderingMode: props.renderingMode
};
}
if (typeof iconProp === "string") return {
form: "sf",
name: iconProp
};
if (iconProp) return {
form: "src",
source: iconProp
};
return void 0;
}
function checkButtonChildren(children) {
if (process.env.NODE_ENV === "production" || typeof children === "string") return;
const all = Children.toArray(children);
const valid = all.filter((child) => typeof child === "string" || typeof child === "number" || isValidElement(child) && (isKind(child, "label") || isKind(child, "icon") || isKind(child, "badge")));
if (all.length !== valid.length) {
throw new Error("Stack.Toolbar.Button only accepts a single string or Stack.Toolbar.Label, Stack.Toolbar.Icon, and Stack.Toolbar.Badge as its children.");
}
}
function checkMenuChildren(children) {
if (process.env.NODE_ENV === "production") return;
const all = Children.toArray(children);
const valid = all.filter((child) => isValidElement(child) && (isKind(child, "menu") || isKind(child, "menuAction") || isKind(child, "label") || isKind(child, "icon") || isKind(child, "badge")));
if (all.length !== valid.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.");
}
}
function labelFromChildren(children) {
const labelChild = firstChildOfKind(children, "label");
if (labelChild) return labelChild.props.children ?? "";
return toolbarChildrenToString(children);
}
function badgeFromChildren(children) {
const badgeChild = firstChildOfKind(children, "badge");
if (!badgeChild) return void 0;
const props = badgeChild.props;
const style = props.style;
return {
value: props.children ?? "",
...style?.backgroundColor !== void 0 && { backgroundColor: style.backgroundColor },
...style?.color !== void 0 && { color: style.color },
...style?.fontFamily !== void 0 && { fontFamily: style.fontFamily },
...style?.fontSize !== void 0 && { fontSize: style.fontSize },
...style?.fontWeight !== void 0 && { fontWeight: style.fontWeight }
};
}
function labelStyleFromStyle(style) {
if (!style) return void 0;
const flat = style;
const picked = {
...flat.fontFamily !== void 0 && { fontFamily: flat.fontFamily },
...flat.fontSize !== void 0 && { fontSize: flat.fontSize },
...flat.fontWeight !== void 0 && { fontWeight: flat.fontWeight },
...flat.color !== void 0 && { color: flat.color }
};
return Object.keys(picked).length ? picked : void 0;
}
function headerIconFromResolution(resolution, iconRenderingMode, tintColor) {
if (!resolution) return void 0;
if (resolution.form === "sf") {
return {
type: "sfSymbol",
name: resolution.name
};
}
const source = resolution.form === "src" ? resolution.source : { uri: resolution.name };
const effectiveMode = resolution.renderingMode ?? iconRenderingMode ?? (tintColor ? "template" : "original");
return {
type: "image",
source,
tinted: effectiveMode === "template"
};
}
function buttonPropsToHeaderButton(props) {
if (props.hidden) return null;
checkButtonChildren(props.children);
const label = labelFromChildren(props.children);
const icon = headerIconFromResolution(resolveIcon(props.icon, props.children), props.iconRenderingMode, props.tintColor);
const badge = badgeFromChildren(props.children);
return {
type: "button",
label,
...icon && { icon },
...props.variant !== void 0 && { variant: props.variant },
...props.tintColor !== void 0 && { tintColor: props.tintColor },
...props.disabled !== void 0 && { disabled: props.disabled },
sharesBackground: !props.separateBackground,
...props.hidesSharedBackground !== void 0 && { hidesSharedBackground: props.hidesSharedBackground },
...badge && { badge: {
value: badge.value,
...(badge.color !== void 0 || badge.backgroundColor !== void 0 || badge.fontFamily !== void 0 || badge.fontSize !== void 0 || badge.fontWeight !== void 0) && { style: {
...badge.color !== void 0 && { color: badge.color },
...badge.backgroundColor !== void 0 && { backgroundColor: badge.backgroundColor },
...badge.fontFamily !== void 0 && { fontFamily: badge.fontFamily },
...badge.fontSize !== void 0 && { fontSize: badge.fontSize },
...badge.fontWeight !== void 0 && { fontWeight: badge.fontWeight }
} }
} },
...labelStyleFromStyle(props.style) && { labelStyle: labelStyleFromStyle(props.style) },
...props.accessibilityLabel !== void 0 && { accessibilityLabel: props.accessibilityLabel },
...props.accessibilityHint !== void 0 && { accessibilityHint: props.accessibilityHint },
onPress: props.onPress ?? (() => {}),
selected: !!props.selected
};
}
function menuLabelAndTitle(children, title) {
const labelChild = firstChildOfKind(children, "label");
const labelFromChild = labelChild?.props?.children;
return {
label: labelFromChild ?? title ?? "",
menuTitle: title ?? ""
};
}
function menuActionPropsToHeaderAction(props) {
const { children, subtitle, isOn, unstable_keepPresented, icon, image, iconRenderingMode, ...rest } = props;
void image;
const shared = labelFromChildren(children);
const iconResolution = resolveIcon(icon, children);
const item = {
...rest,
description: subtitle,
type: "action",
label: shared,
state: isOn ? "on" : "off",
onPress: props.onPress ?? (() => {})
};
if (unstable_keepPresented !== void 0) {
item.keepsMenuPresented = unstable_keepPresented;
}
const headerIcon = headerIconFromResolution(iconResolution, iconRenderingMode, void 0);
if (headerIcon) item.icon = headerIcon;
return item;
}
function menuElementToHeaderSubmenu(element, placement) {
const props = element.props;
if (props.hidden) return null;
checkMenuChildren(props.children);
const items = menuChildrenToHeaderMenuItems(props.children, placement);
return {
type: "submenu",
label: labelFromChildren(props.children) || props.title || "",
multiselectable: true,
...props.inline !== void 0 && { inline: props.inline },
...props.palette !== void 0 && { layout: props.palette ? "palette" : "default" },
...props.destructive !== void 0 && { destructive: props.destructive },
...headerIconFromResolution(resolveIcon(props.icon, props.children), props.iconRenderingMode, props.tintColor) && { icon: headerIconFromResolution(resolveIcon(props.icon, props.children), props.iconRenderingMode, props.tintColor) },
items
};
}
function menuChildrenToHeaderMenuItems(children, placement) {
const items = [];
Children.forEach(children, (child) => {
if (!isValidElement(child)) return;
if (isKind(child, "menuAction")) {
items.push(menuActionPropsToHeaderAction(child.props));
} else if (isKind(child, "menu")) {
const submenu = menuElementToHeaderSubmenu(child, placement);
if (submenu) items.push(submenu);
}
});
return items;
}
function menuPropsToHeaderMenu(element, placement) {
const props = element.props;
if (props.hidden) return null;
checkMenuChildren(props.children);
const { label, menuTitle } = menuLabelAndTitle(props.children, props.title);
const icon = headerIconFromResolution(resolveIcon(props.icon, props.children), props.iconRenderingMode, props.tintColor);
const badge = badgeFromChildren(props.children);
return {
type: "menu",
label,
...icon && { icon },
...props.variant !== void 0 && { variant: props.variant },
...props.tintColor !== void 0 && { tintColor: props.tintColor },
...props.disabled !== void 0 && { disabled: props.disabled },
sharesBackground: !props.separateBackground,
...props.hidesSharedBackground !== void 0 && { hidesSharedBackground: props.hidesSharedBackground },
...badge && { badge: { value: badge.value } },
...props.accessibilityLabel !== void 0 && { accessibilityLabel: props.accessibilityLabel },
...props.accessibilityHint !== void 0 && { accessibilityHint: props.accessibilityHint },
menu: {
...menuTitle ? { title: menuTitle } : null,
multiselectable: true,
...props.palette !== void 0 && { layout: props.palette ? "palette" : "default" },
items: menuChildrenToHeaderMenuItems(props.children, placement)
}
};
}
function spacerPropsToHeaderSpacing(props, placement) {
if (props.hidden) return null;
if (props.width === void 0) {
warn(`Warning: Stack.Toolbar.Spacer requires \`width\` when used in ${placement} placement. Flexible spacers are only supported in bottom placement.`);
return null;
}
return {
type: "spacing",
spacing: props.width
};
}
function toolbarChildrenToHeaderItems(children, placement) {
const items = [];
Children.forEach(children, (child) => {
if (!isValidElement(child)) return;
if (isKind(child, "button")) {
const item = buttonPropsToHeaderButton(child.props);
if (item) items.push(item);
} else if (isKind(child, "menu")) {
const menu = menuPropsToHeaderMenu(child, placement);
if (menu) items.push(menu);
} else if (isKind(child, "spacer")) {
const spacing = spacerPropsToHeaderSpacing(child.props, placement);
if (spacing) items.push(spacing);
} else {
warn(`Warning: Stack.Toolbar with placement="${placement}" only accepts <Stack.Toolbar.Button>, <Stack.Toolbar.Menu>, and <Stack.Toolbar.Spacer> as children. Found invalid child: ${childName(child)}`);
}
});
return items;
}
function bottomIconFields(iconProp, imageProp, iconRenderingMode, tintColor, children, component) {
const resolution = resolveIcon(iconProp, children);
if (!resolution) {
return { ...imageProp !== void 0 && {
image: imageProp,
imageRenderingMode: iconRenderingMode ?? (tintColor !== void 0 ? "template" : "original")
} };
}
if (resolution.form === "sf") return { sfName: resolution.name };
if (resolution.form === "xcasset") {
return {
xcassetName: resolution.name,
imageRenderingMode: resolution.renderingMode ?? iconRenderingMode
};
}
warn(`Warning: Stack.Toolbar.${component} in bottom placement does not support image icons via the \`icon\` prop or <Stack.Toolbar.Icon src={...} />; the image will not render. Use the \`icon\` prop with an SF Symbol name, the \`image\` prop, or <Stack.Toolbar.Icon xcasset="..." />.`);
return {};
}
function bottomButtonData(props, index) {
checkButtonChildren(props.children);
const title = labelFromChildren(props.children) || void 0;
const { sfName, ...iconRest } = bottomIconFields(props.icon, props.image, props.iconRenderingMode, props.tintColor, props.children, "Button");
return {
kind: "button",
identifier: fallbackIdentifier("bottom", index, title),
...title !== void 0 && { title },
...sfName !== void 0 && { systemImageName: sfName },
...iconRest,
tintColor: props.tintColor,
...props.variant !== void 0 && { barButtonItemStyle: props.variant },
sharesBackground: !props.separateBackground,
hidesSharedBackground: props.hidesSharedBackground,
hidden: props.hidden,
selected: props.selected,
disabled: props.disabled,
badgeConfiguration: badgeFromChildren(props.children),
...labelStyleFromStyle(props.style) && { titleStyle: labelStyleFromStyle(props.style) },
accessibilityLabel: props.accessibilityLabel,
accessibilityHint: props.accessibilityHint,
handler: props.onPress ?? (() => {})
};
}
function bottomMenuActionData(props, index, path) {
const title = labelFromChildren(props.children);
const { sfName, ...iconRest } = bottomIconFields(props.icon, props.image, props.iconRenderingMode, void 0, props.children, "MenuAction");
return {
kind: "action",
identifier: fallbackIdentifier(path, index, title),
title,
...sfName !== void 0 && { icon: sfName },
...iconRest,
disabled: props.disabled,
destructive: props.destructive,
hidden: props.hidden,
isOn: props.isOn,
keepPresented: props.unstable_keepPresented,
discoverabilityLabel: props.discoverabilityLabel,
subtitle: props.subtitle,
accessibilityLabel: props.accessibilityLabel,
accessibilityHint: props.accessibilityHint,
handler: props.onPress ?? (() => {})
};
}
function bottomMenuChildren(children, path) {
const data = [];
Children.forEach(children, (child, index) => {
if (!isValidElement(child)) return;
if (isKind(child, "menuAction")) {
data.push(bottomMenuActionData(child.props, index, path));
} else if (isKind(child, "menu")) {
data.push(bottomSubmenuData(child, index, path));
}
});
return data;
}
function bottomSubmenuData(element, index, path) {
const props = element.props;
checkMenuChildren(props.children);
const title = labelFromChildren(props.children) || props.title || "";
const { sfName, ...iconRest } = bottomIconFields(props.icon, props.image, props.iconRenderingMode, props.tintColor, props.children, "Menu");
return {
kind: "submenu",
identifier: fallbackIdentifier(path, index, title),
title,
...sfName !== void 0 && { icon: sfName },
...iconRest,
destructive: props.destructive,
hidden: props.hidden,
inline: props.inline,
palette: props.palette,
accessibilityLabel: props.accessibilityLabel,
accessibilityHint: props.accessibilityHint,
children: bottomMenuChildren(props.children, `${path}-submenu-${index}`)
};
}
function bottomMenuData(props, index) {
checkMenuChildren(props.children);
const { label, menuTitle } = menuLabelAndTitle(props.children, props.title);
const { sfName, ...iconRest } = bottomIconFields(props.icon, props.image, props.iconRenderingMode, props.tintColor, props.children, "Menu");
return {
kind: "menu",
identifier: fallbackIdentifier("bottom", index, label || menuTitle),
title: menuTitle,
label,
...sfName !== void 0 && { systemImageName: sfName },
...iconRest,
tintColor: props.tintColor,
...props.variant !== void 0 && { barButtonItemStyle: props.variant },
sharesBackground: !props.separateBackground,
hidesSharedBackground: props.hidesSharedBackground,
disabled: props.disabled,
destructive: props.destructive,
hidden: props.hidden,
inline: props.inline,
palette: props.palette,
elementSize: props.elementSize,
accessibilityLabel: props.accessibilityLabel,
accessibilityHint: props.accessibilityHint,
children: bottomMenuChildren(props.children, `bottom-menu-${index}`)
};
}
function toolbarChildrenToBottomData(children) {
const data = [];
Children.forEach(children, (child, index) => {
if (!isValidElement(child)) return;
if (isKind(child, "button")) {
data.push(bottomButtonData(child.props, index));
} else if (isKind(child, "menu")) {
data.push(bottomMenuData(child.props, index));
} else if (isKind(child, "spacer")) {
const props = child.props;
data.push({
kind: "spacer",
identifier: fallbackIdentifier("bottom", index, "spacer"),
...props.width !== void 0 && { width: props.width },
...props.sharesBackground !== void 0 && { sharesBackground: props.sharesBackground },
...props.hidden !== void 0 && { hidden: props.hidden }
});
} else if (isKind(child, "searchBarSlot")) {
const props = child.props;
data.push({
kind: "searchBar",
identifier: fallbackIdentifier("bottom", index, "search"),
sharesBackground: !props.separateBackground,
hidesSharedBackground: props.hidesSharedBackground,
hidden: props.hidden
});
} else {
warn(`Warning: Stack.Toolbar with bottom placement only accepts <Stack.Toolbar.Button>, <Stack.Toolbar.Menu>, <Stack.Toolbar.Spacer>, and <Stack.Toolbar.SearchBarSlot> as children. Found invalid child: ${childName(child)}`);
}
});
return data;
}
function appendStackToolbarPropsToOptions(options, props) {
const placement = props.placement ?? "bottom";
if (placement === "bottom") {
if (process.env.NODE_ENV !== "production") {
warn("Warning: Stack.Toolbar with bottom placement in layout config has no effect. Mount it in screen content so the toolbar host sits inside the screen view controller.");
}
return options;
}
if (props.asChild) {
const key2 = placement === "left" ? "headerLeft" : "headerRight";
return {
...options,
headerShown: true,
[key2]: () => props.children
};
}
const items = toolbarChildrenToHeaderItems(props.children, placement);
const key = placement === "left" ? "unstable_headerLeftItems" : "unstable_headerRightItems";
return {
...options,
headerShown: true,
[key]: () => items
};
}
export { TOOLBAR_KIND, appendStackToolbarPropsToOptions, isKind as isToolbarKind, toolbarChildrenToBottomData, toolbarChildrenToHeaderItems, toolbarChildrenToString };
//# sourceMappingURL=stackToolbarDescriptors.mjs.map