react-native-screens
Version:
Native navigation primitives for your React Native app.
392 lines (388 loc) • 14.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireWildcard(require("react"));
var _reactNative = require("react-native");
var _StackHeaderConfigAndroidNativeComponent = _interopRequireWildcard(require("../../../../fabric/gamma/stack/StackHeaderConfigAndroidNativeComponent"));
var _StackHeaderSubview = _interopRequireDefault(require("./android/StackHeaderSubview.android"));
var _shared = require("../../../shared");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
/**
* EXPERIMENTAL API, MIGHT CHANGE W/O ANY NOTICE
*/
function StackHeaderConfig(props, forwardedRef) {
// ios props are safely dropped
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {
android,
ios,
...baseProps
} = props;
const ref = useHeaderConfigRef(forwardedRef);
const {
backgroundSubview,
leadingSubview,
centerSubview,
trailingSubview,
backButtonIcon,
scrollFlagScroll,
scrollFlagEnterAlways,
scrollFlagEnterAlwaysCollapsed,
scrollFlagExitUntilCollapsed,
scrollFlagSnap,
toolbarMenu,
toolbarMenuGroupDividerEnabled,
...filteredAndroidProps
} = android ?? {};
const parsedToolbarMenu = parseToolbarMenuToNativeProps(toolbarMenu);
const handleToolbarMenuItemPress = event => {
const element = findToolbarMenuElementById(toolbarMenu?.children, event.nativeEvent.id);
if (element?.type === 'menuItem') {
element.onPress?.();
}
};
const handleToolbarMenuGroupSelectionChange = event => {
const {
groupId,
selectedIds
} = event.nativeEvent;
const group = findToolbarMenuGroupById(toolbarMenu, groupId);
group?.onSelectionChange?.(selectedIds);
};
const backButtonIconProps = parseBackButtonIconToNativeProps(backButtonIcon);
const scrollFlagProps = resolveScrollFlags(filteredAndroidProps.type, {
scrollFlagScroll,
scrollFlagEnterAlways,
scrollFlagEnterAlwaysCollapsed,
scrollFlagExitUntilCollapsed,
scrollFlagSnap
});
return /*#__PURE__*/_react.default.createElement(_StackHeaderConfigAndroidNativeComponent.default, _extends({
ref: ref,
collapsable: false,
style: _reactNative.StyleSheet.absoluteFill,
toolbarMenu: parsedToolbarMenu,
toolbarMenuGroupDividerEnabled: toolbarMenuGroupDividerEnabled,
onToolbarMenuItemPress: handleToolbarMenuItemPress,
onToolbarMenuGroupSelectionChange: handleToolbarMenuGroupSelectionChange
}, baseProps, filteredAndroidProps, backButtonIconProps, scrollFlagProps), backgroundSubview && /*#__PURE__*/_react.default.createElement(_StackHeaderSubview.default, {
type: "background",
collapseMode: backgroundSubview.collapseMode
}, backgroundSubview.render()), leadingSubview && /*#__PURE__*/_react.default.createElement(_StackHeaderSubview.default, {
type: "leading"
}, leadingSubview.render()), centerSubview && /*#__PURE__*/_react.default.createElement(_StackHeaderSubview.default, {
type: "center"
}, centerSubview.render()), trailingSubview && /*#__PURE__*/_react.default.createElement(_StackHeaderSubview.default, {
type: "trailing"
}, trailingSubview.render()));
}
function parseBackButtonIconToNativeProps(icon) {
if (!icon) {
return {};
}
if (icon.type === 'imageSource') {
const resolved = _reactNative.Image.resolveAssetSource(icon.imageSource);
if (!resolved) {
console.error('[RNScreens] failed to resolve an asset for back button icon');
}
return {
backButtonImageIconResource: resolved || undefined
};
} else if (icon.type === 'drawableResource') {
return {
backButtonDrawableIconResourceName: icon.name
};
} else {
throw new Error('[RNScreens] Incorrect icon format for Android. You must provide `imageSource` or `drawableResource`.');
}
}
const SCROLL_FLAG_DEFAULTS_BY_TYPE = {
small: {
scrollFlagScroll: false,
scrollFlagEnterAlways: false,
scrollFlagEnterAlwaysCollapsed: false,
scrollFlagExitUntilCollapsed: false,
scrollFlagSnap: false
},
medium: {
scrollFlagScroll: true,
scrollFlagEnterAlways: false,
scrollFlagEnterAlwaysCollapsed: false,
scrollFlagExitUntilCollapsed: true,
scrollFlagSnap: true
},
large: {
scrollFlagScroll: true,
scrollFlagEnterAlways: false,
scrollFlagEnterAlwaysCollapsed: false,
scrollFlagExitUntilCollapsed: true,
scrollFlagSnap: true
}
};
function resolveScrollFlags(type, overrides) {
const defaults = SCROLL_FLAG_DEFAULTS_BY_TYPE[type ?? 'small'];
return {
scrollFlagScroll: overrides.scrollFlagScroll ?? defaults.scrollFlagScroll,
scrollFlagEnterAlways: overrides.scrollFlagEnterAlways ?? defaults.scrollFlagEnterAlways,
scrollFlagEnterAlwaysCollapsed: overrides.scrollFlagEnterAlwaysCollapsed ?? defaults.scrollFlagEnterAlwaysCollapsed,
scrollFlagExitUntilCollapsed: overrides.scrollFlagExitUntilCollapsed ?? defaults.scrollFlagExitUntilCollapsed,
scrollFlagSnap: overrides.scrollFlagSnap ?? defaults.scrollFlagSnap
};
}
function useHeaderConfigRef(forwardedRef) {
const ref = (0, _react.useRef)(null);
(0, _react.useImperativeHandle)(forwardedRef, () => ({
android: {
setToolbarMenuElementOptions: (id, options) => {
if (!ref.current) {
console.warn('[RNScreens] Reference to native header config component has not been updated yet.');
return;
}
_StackHeaderConfigAndroidNativeComponent.Commands.setToolbarMenuElementOptions(ref.current, id, parseToolbarMenuElementOptionsToNativeProps(options));
}
}
}));
return ref;
}
function findToolbarMenuGroupById(menu, groupId) {
if (!menu) {
return null;
}
for (const group of menu.groups ?? []) {
if (group.groupId === groupId) {
return group;
}
}
for (const element of menu.children ?? []) {
if (element.type === 'menu') {
const found = findToolbarMenuGroupById(element, groupId);
if (found) {
return found;
}
}
}
return null;
}
function findToolbarMenuElementById(elements, id) {
if (!elements) {
return null;
}
for (const element of elements) {
if (element.id === id) {
return element;
}
if (element.type === 'menu') {
const found = findToolbarMenuElementById(element.children, id);
if (found) {
return found;
}
}
}
return null;
}
function parseToolbarMenuToNativeProps(menu) {
if (!menu?.children?.length) {
return undefined;
}
assertUniqueItemIds(menu.children);
assertUniqueGroupIds(menu);
assertGroupIdReferencesExist(menu);
assertRadioInitialSelection(menu);
return {
groups: parseGroupsToNativeProps(menu.groups),
children: menu.children.map(parseElementToNativeProps)
};
}
function parseGroupsToNativeProps(groups) {
if (!groups?.length) {
return undefined;
}
return groups.map(({
groupId,
singleSelection
}) => ({
groupId,
singleSelection
}));
}
function parseElementToNativeProps(element) {
if (element.type === 'menu') {
const {
type,
children,
groups,
...baseProps
} = element;
return {
type,
...parseBaseItemToNativeProps(baseProps),
groups: parseGroupsToNativeProps(groups),
children: children?.map(parseElementToNativeProps)
};
}
assertItemTypeGroupIdConsistency(element);
assertNoOnPressOnToggleItem(element);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {
type,
onPress,
groupId,
itemType,
initialToggleState,
...baseProps
} = element;
return {
type,
groupId,
itemType,
initialToggleState,
...parseBaseItemToNativeProps(baseProps)
};
}
function assertUniqueItemIds(elements, seen = new Set()) {
for (const element of elements) {
if (seen.has(element.id)) {
throw new Error(`[RNScreens] Duplicate toolbar menu item id: '${element.id}'. ` + `Item IDs must be unique across the entire menu.`);
}
seen.add(element.id);
if (element.type === 'menu' && element.children) {
assertUniqueItemIds(element.children, seen);
}
}
}
function assertUniqueGroupIds(menu, seen = new Set()) {
if (menu.groups) {
for (const group of menu.groups) {
if (seen.has(group.groupId)) {
throw new Error(`[RNScreens] Duplicate toolbar menu group id: '${group.groupId}'. ` + `Group IDs must be unique across the entire menu.`);
}
seen.add(group.groupId);
}
}
if (menu.children) {
for (const element of menu.children) {
if (element.type === 'menu') {
assertUniqueGroupIds(element, seen);
}
}
}
}
function assertGroupIdReferencesExist(menu) {
const localGroupIds = new Set(menu.groups?.map(g => g.groupId));
if (menu.children) {
for (const element of menu.children) {
if (element.type === 'menuItem' && element.groupId != null) {
if (!localGroupIds.has(element.groupId)) {
throw new Error(`[RNScreens] Menu item '${element.id}' references group ` + `'${element.groupId}' which is not defined at the same ` + `menu level. Groups cannot span submenus.`);
}
}
if (element.type === 'menu') {
assertGroupIdReferencesExist(element);
}
}
}
}
function assertRadioInitialSelection(menu) {
if (menu.groups && menu.children) {
for (const group of menu.groups) {
if (!group.singleSelection) {
continue;
}
let count = 0;
for (const element of menu.children) {
if (element.type === 'menuItem' && element.groupId === group.groupId && element.initialToggleState) {
count++;
}
}
if (count > 1) {
throw new Error(`[RNScreens] Radio group '${group.groupId}' has ${count} items ` + `with initialToggleState=true. At most 1 is allowed for ` + `single-selection groups.`);
}
}
}
if (menu.children) {
for (const element of menu.children) {
if (element.type === 'menu') {
assertRadioInitialSelection(element);
}
}
}
}
function assertItemTypeGroupIdConsistency(element) {
if (element.itemType === 'toggle' && element.groupId == null) {
throw new Error(`[RNScreens] Menu item '${element.id}' has itemType='toggle' ` + `but no groupId. Toggle items must belong to a group.`);
}
if (element.itemType === 'action' && element.groupId != null) {
throw new Error(`[RNScreens] Menu item '${element.id}' has itemType='action' ` + `and belongs to group '${element.groupId}'. ` + `Action items cannot belong to groups.`);
}
}
function assertNoOnPressOnToggleItem(element) {
if (!element.onPress) {
return;
}
const effectiveItemType = element.itemType ?? 'automatic';
if (effectiveItemType === 'toggle') {
throw new Error(`[RNScreens] Menu item '${element.id}' has itemType='toggle' and defines onPress. ` + `Toggle items do not emit press events. Use onSelectionChange on the group instead.`);
}
if (effectiveItemType === 'automatic' && element.groupId != null) {
throw new Error(`[RNScreens] Menu item '${element.id}' belongs to group '${element.groupId}' ` + `and defines onPress. Items in a group behave as toggles and do not emit press events. ` + `Use onSelectionChange on the group instead.`);
}
}
function parseBaseItemToNativeProps({
icon,
iconTintColorNormal,
iconTintColorPressed,
iconTintColorFocused,
iconTintColorDisabled,
...rest
}) {
return {
...rest,
...(0, _shared.parseAndroidIconToNativeProps)(icon),
iconTintColorNormal: (0, _reactNative.processColor)(iconTintColorNormal),
iconTintColorPressed: (0, _reactNative.processColor)(iconTintColorPressed),
iconTintColorFocused: (0, _reactNative.processColor)(iconTintColorFocused),
iconTintColorDisabled: (0, _reactNative.processColor)(iconTintColorDisabled)
};
}
function parseToolbarMenuElementOptionsToNativeProps(options) {
const nativeOptions = Object.fromEntries(Object.entries(options).flatMap(([key, value]) => {
const typedKey = key;
switch (typedKey) {
case 'iconTintColorNormal':
case 'iconTintColorPressed':
case 'iconTintColorFocused':
case 'iconTintColorDisabled':
return [[key, (0, _reactNative.processColor)(value) ?? null]];
case 'icon':
{
const iconValue = value;
// Explicit `undefined` means "reset the icon". The native side treats
// an absent key as "no change", so to clear the icon we must send every
// native icon key explicitly as `null`.
if (iconValue === undefined) {
const noIcon = {
imageIconResource: null,
drawableIconResourceName: null
};
return Object.entries(noIcon);
}
return Object.entries((0, _shared.parseAndroidIconToNativeProps)(iconValue));
}
}
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
throw new Error(`[RNScreens] Unexpected nested object.`);
}
return [[key,
// We need to replace explicit `undefined` with `null`
// so that we're able to read that information on the native side.
value === undefined ? null : value]];
}));
// For some reason Codegen requires passing an array (we can't use plain object).
return [nativeOptions];
}
var _default = exports.default = /*#__PURE__*/(0, _react.forwardRef)(StackHeaderConfig);
//# sourceMappingURL=StackHeaderConfig.android.js.map