From 81b926af861e9bf0eb7f93485ae0be1763b76963 Mon Sep 17 00:00:00 2001 From: Fernando Rojo Date: Thu, 7 Jul 2022 14:31:13 -0400 Subject: [PATCH] add context menu to ios / web --- examples/expo/alias.js | 22 +- examples/expo/babel.config.js | 14 +- examples/expo/src/App.tsx | 115 ++++++--- examples/expo/tsconfig.json | 6 +- examples/expo/webpack.config.js | 7 +- package.json | 3 + .../src/context-menu/__tests__/index.test.tsx | 1 - .../src/context-menu/context-menu.android.tsx | 2 + .../src/context-menu/context-menu.ios.tsx | 2 + .../zeego/src/context-menu/context-menu.tsx | 9 +- .../src/context-menu/context-menu.web.tsx | 226 ++++++++++++++++++ packages/zeego/src/context-menu/web/label.tsx | 16 ++ .../dropdown-menu/__tests__/index.test.tsx | 49 +++- .../src/dropdown-menu/dropdown-menu.web.tsx | 2 +- .../src/dropdown-menu/web/item-image.tsx | 28 --- .../zeego/src/menu/__tests__/index.test.tsx | 1 - .../create-android-menu/index.android.tsx | 39 ++- .../src/menu/create-ios-menu/index.ios.tsx | 80 ++++++- packages/zeego/src/menu/display-names.tsx | 1 + packages/zeego/src/menu/types.ts | 17 ++ .../zeego/src/menu/web-primitives/index.tsx | 12 +- .../src/menu/web-primitives/item-image.tsx | 30 +++ tsconfig.build.json | 2 +- 23 files changed, 549 insertions(+), 135 deletions(-) delete mode 100644 packages/zeego/src/context-menu/__tests__/index.test.tsx create mode 100644 packages/zeego/src/context-menu/context-menu.web.tsx create mode 100644 packages/zeego/src/context-menu/web/label.tsx delete mode 100644 packages/zeego/src/dropdown-menu/web/item-image.tsx delete mode 100644 packages/zeego/src/menu/__tests__/index.test.tsx create mode 100644 packages/zeego/src/menu/web-primitives/item-image.tsx diff --git a/examples/expo/alias.js b/examples/expo/alias.js index 6b61268..1e1d54e 100644 --- a/examples/expo/alias.js +++ b/examples/expo/alias.js @@ -1,24 +1,10 @@ -// const fs = require('fs') const path = require('path') -// const packages = path.resolve(__dirname, '../..', 'packages') - -// const alias = {} - -// fs.readdirSync(packages).map((name) => { -// const pak = require(path.join(packages, name, 'package.json')) -// alias[pak.name] = path.resolve(packages, name, pak.source) -// }) - -const packagePath = '../../packages/zeego/' - module.exports = { alias: { - 'zeego/dropdown-menu': path.resolve( - __dirname, - packagePath, - 'dropdown-menu' - ), - 'zeego/context-menu': path.resolve(__dirname, packagePath, 'context-menu'), + // zeego$: path.resolve(__dirname, '../../packages/zeego/src'), + zeego: ([, name]) => + // https://github.com/tleunen/babel-plugin-module-resolver/blob/HEAD/DOCS.md#passing-a-substitute-function + path.resolve(__dirname, `../../packages/zeego/src${name}`), }, } diff --git a/examples/expo/babel.config.js b/examples/expo/babel.config.js index b34d7f4..000efe5 100644 --- a/examples/expo/babel.config.js +++ b/examples/expo/babel.config.js @@ -1,7 +1,7 @@ -const { alias } = require('./alias'); +const path = require('path') module.exports = function (api) { - api.cache(true); + api.cache(false) return { presets: [['babel-preset-expo', { jsxRuntime: 'automatic' }]], @@ -10,9 +10,13 @@ module.exports = function (api) { 'module-resolver', { extensions: ['.tsx', '.ts', '.js', '.json'], - alias, + alias: { + // https://github.com/tleunen/babel-plugin-module-resolver/blob/HEAD/DOCS.md#passing-a-substitute-function + zeego: ([, name]) => + path.resolve(__dirname, `../../packages/zeego/src${name}`), + }, }, ], ], - }; -}; + } +} diff --git a/examples/expo/src/App.tsx b/examples/expo/src/App.tsx index 80cce7d..bb15fd9 100644 --- a/examples/expo/src/App.tsx +++ b/examples/expo/src/App.tsx @@ -8,26 +8,34 @@ const select = (val: unknown) => () => alert(val) const itemHeight = 25 +const contentStyle = { + minWidth: 220, + backgroundColor: 'white', + borderRadius: 6, + padding: 5, + borderWidth: 1, + borderColor: '#fff8', + ...Platform.select({ + web: { + animationDuration: '400ms', + animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)', + willChange: 'transform, opacity', + animationKeyframes: { + '0%': { opacity: 0, transform: [{ scale: 0.5 }] }, + '100%': { opacity: 1, transform: [{ scale: 1 }] }, + }, + boxShadow: + '0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)', + }, + }), +} + const dropdownStyles = StyleSheet.create({ content: { - minWidth: 220, - backgroundColor: 'white', - borderRadius: 6, - padding: 5, - borderWidth: 1, - borderColor: '#fff8', + ...contentStyle, ...Platform.select({ web: { - animationDuration: '400ms', - animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)', - willChange: 'transform, opacity', - animationKeyframes: { - '0%': { opacity: 0, transform: [{ scale: 0.5 }] }, - '100%': { opacity: 1, transform: [{ scale: 1 }] }, - }, transformOrigin: 'var(--radix-dropdown-menu-content-transform-origin)', - boxShadow: - '0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)', }, }), }, @@ -37,6 +45,10 @@ const dropdownStyles = StyleSheet.create({ paddingRight: 5, paddingLeft: itemHeight, height: itemHeight, + transformOrigin: 'var(--radix-dropdown-menu-item-transform-origin)', + }, + itemWithSubtitle: { + height: itemHeight * 2, }, itemFocused: { // a nice background gray @@ -89,6 +101,18 @@ const dropdownStyles = StyleSheet.create({ }, }) +const contextStyles = StyleSheet.create({ + content: { + ...contentStyle, + ...Platform.select({ + web: { + transformOrigin: 'var(--radix-context-menu-content-transform-origin)', + }, + }), + // no animations here yet, since I don't know how to style based on data-side attributes + }, +}) + const DropdownMenuItem = DropdownMenu.menuify( (props: ComponentProps) => { const [focused, setFocused] = useState(false) @@ -280,7 +304,7 @@ const DropdownMenuExample = () => { Group Submenu - + { {``} - - + + + {() => ( + + )} + + + Action #1 Description! - - Action #2 + + + Action #2 - - Action #3 + + Action #3 - + Submenu - + Submenu Option 1 @@ -347,14 +392,23 @@ const ContextMenuExample = () => { - + Group Submenu - - + + Group Submenu Option 3 - + Group Submenu Option 4 @@ -368,10 +422,9 @@ const ContextMenuExample = () => { export default function App() { return ( - + {/* */} - {/* - */} + ) } diff --git a/examples/expo/tsconfig.json b/examples/expo/tsconfig.json index d5ee86f..ba21aaa 100644 --- a/examples/expo/tsconfig.json +++ b/examples/expo/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { - "jsx": "react-jsx" + "jsx": "react-jsx", + "paths": { + "zeego/*": ["../../packages/zeego/src/*"] + }, + "baseUrl": "." }, "extends": "../../tsconfig" } diff --git a/examples/expo/webpack.config.js b/examples/expo/webpack.config.js index e128e4b..e3a6d37 100644 --- a/examples/expo/webpack.config.js +++ b/examples/expo/webpack.config.js @@ -13,9 +13,9 @@ module.exports = async function (env, argv) { config.module.rules.push({ test: /\.(js|ts|tsx)$/, - include: /(packages|example)\/.+/, + include: /(packages|examples)\/.+/, exclude: /node_modules/, - use: 'babel-loader', + use: require.resolve('babel-loader'), }) Object.assign(config.resolve.alias, { @@ -23,9 +23,8 @@ module.exports = async function (env, argv) { 'react-native': path.resolve(node_modules, 'react-native-web'), 'react-native-web': path.resolve(node_modules, 'react-native-web'), '@expo/vector-icons': path.resolve(node_modules, '@expo/vector-icons'), + 'zeego$': path.resolve(__dirname, '../../packages/zeego/src'), }) - config.resolve.alias = alias - return config } diff --git a/package.json b/package.json index a4b039d..b9a0f85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "description": "Logical UI primitives, made for screens.", "private": true, + "nohoist": [ + "**/*/babel-loader" + ], "workspaces": { "packages": [ "packages/*", diff --git a/packages/zeego/src/context-menu/__tests__/index.test.tsx b/packages/zeego/src/context-menu/__tests__/index.test.tsx deleted file mode 100644 index bf84291..0000000 --- a/packages/zeego/src/context-menu/__tests__/index.test.tsx +++ /dev/null @@ -1 +0,0 @@ -it.todo('write a test'); diff --git a/packages/zeego/src/context-menu/context-menu.android.tsx b/packages/zeego/src/context-menu/context-menu.android.tsx index 8997aca..3a8a936 100644 --- a/packages/zeego/src/context-menu/context-menu.android.tsx +++ b/packages/zeego/src/context-menu/context-menu.android.tsx @@ -15,6 +15,7 @@ const { CheckboxItem, ItemIndicator, Label, + Preview, } = createAndroidMenu('ContextMenu') export { @@ -32,4 +33,5 @@ export { CheckboxItem, ItemIndicator, Label, + Preview, } diff --git a/packages/zeego/src/context-menu/context-menu.ios.tsx b/packages/zeego/src/context-menu/context-menu.ios.tsx index 4fb39af..42a1372 100644 --- a/packages/zeego/src/context-menu/context-menu.ios.tsx +++ b/packages/zeego/src/context-menu/context-menu.ios.tsx @@ -15,6 +15,7 @@ const { ItemIndicator, ItemImage, Label, + Preview, } = createIosMenu('ContextMenu') export { @@ -32,4 +33,5 @@ export { ItemIndicator, ItemImage, Label, + Preview, } diff --git a/packages/zeego/src/context-menu/context-menu.tsx b/packages/zeego/src/context-menu/context-menu.tsx index 1f6800e..1ff8900 100644 --- a/packages/zeego/src/context-menu/context-menu.tsx +++ b/packages/zeego/src/context-menu/context-menu.tsx @@ -1,6 +1,7 @@ import { + ContextMenuContentProps, + ContextMenuPreviewProps, MenuCheckboxItemProps, - MenuContentProps, MenuDisplayName, MenuGroupProps, MenuItemIconProps, @@ -25,7 +26,7 @@ Root.displayName = MenuDisplayName.Root const Trigger: FC = ({ children }) => <>{children} Trigger.displayName = MenuDisplayName.Trigger -const Content: FC = ({ children }) => { +const Content: FC = ({ children }) => { return <>{children} } Content.displayName = MenuDisplayName.Content @@ -79,6 +80,9 @@ ItemIndicator.displayName = MenuDisplayName.ItemIndicator const Label: FC = () => <> Label.displayName = MenuDisplayName.Label +const Preview: FC = () => <> +Preview.displayName = MenuDisplayName.Preview + export { Root, Trigger, @@ -94,4 +98,5 @@ export { ItemIndicator, ItemImage, Label, + Preview, } diff --git a/packages/zeego/src/context-menu/context-menu.web.tsx b/packages/zeego/src/context-menu/context-menu.web.tsx new file mode 100644 index 0000000..eeeb53e --- /dev/null +++ b/packages/zeego/src/context-menu/context-menu.web.tsx @@ -0,0 +1,226 @@ +import { + ItemPrimitive, + ContextMenuContentProps, + MenuGroupProps, + MenuItemProps, + MenuRootProps, + MenuSeparatorProps, + MenuTriggerItemProps, + MenuTriggerProps, + MenuCheckboxItemProps, + MenuItemIndicatorProps, + MenuItemIconProps, + menuify, +} from '../menu' +import { View } from 'react-native' +import React, { forwardRef } from 'react' + +import * as ContextMenu from '@radix-ui/react-context-menu' + +const Root = menuify(({ children }: MenuRootProps) => { + return {children} +}, 'Root') + +const TriggerView = forwardRef((props, ref) => { + return ( + + {props.children} + + ) +}) + +const Trigger = menuify(({ children, style }: MenuTriggerProps) => { + return ( + + {children} + + ) +}, 'Trigger') + +const ContentView = forwardRef((props, ref) => { + return ( + + {props.children} + + ) +}) + +const Content = menuify( + ({ + children, + style, + loop, + alignOffset, + avoidCollisions, + collisionTolerance, + sideOffset, + }: ContextMenuContentProps) => { + return ( + + {children} + + ) + }, + 'Content' +) + +const itemStyleReset = { + outlineWidth: 0, +} + +const Item = menuify( + ({ + children, + disabled, + onSelect, + style, + onBlur, + onFocus, + textValue, + }: MenuItemProps) => { + return ( + + + {children} + + + ) + }, + 'Item' +) + +const TriggerItem = menuify( + ({ + children, + style, + textValue, + disabled, + onBlur, + onFocus, + }: MenuTriggerItemProps) => { + return ( + + + {children} + + + ) + }, + 'TriggerItem' +) + +const Group = menuify(({ children }: MenuGroupProps) => { + return {children} +}, 'Group') + +const Separator = menuify(({ style }: MenuSeparatorProps) => { + return ( + + + + ) +}, 'Separator') + +const CheckboxItem = menuify( + ({ + onValueChange, + value, + disabled, + textValue, + onBlur, + onFocus, + style, + children, + }: MenuCheckboxItemProps) => { + return ( + onValueChange?.(next ? 'on' : 'off', value)} + style={itemStyleReset} + > + + {children} + + + ) + }, + 'CheckboxItem' +) + +const ItemIndicator = menuify( + ({ style, children }: MenuItemIndicatorProps) => ( + + {children} + + ), + 'ItemIndicator' +) + +const ItemIcon = menuify(({ children, style }: MenuItemIconProps) => { + return {children} +}, 'ItemIcon') + +const Preview = menuify(() => <>, 'Preview') + +export { + Root, + Trigger, + Content, + Item, + TriggerItem, + Group, + Separator, + CheckboxItem, + ItemIndicator, + ItemIcon, + Preview, +} + +export { ItemImage } from '../menu/web-primitives/item-image' +export { Label } from './web/label' + +export { ItemTitle, ItemSubtitle } from '../menu' diff --git a/packages/zeego/src/context-menu/web/label.tsx b/packages/zeego/src/context-menu/web/label.tsx new file mode 100644 index 0000000..8487667 --- /dev/null +++ b/packages/zeego/src/context-menu/web/label.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { Text } from 'react-native' +import * as ContextMenu from '@radix-ui/react-context-menu' +import { menuify } from '../../menu/display-names' +import type { MenuLabelProps } from '../../menu' + +const Label = menuify( + ({ children, style }: MenuLabelProps) => ( + + {children} + + ), + 'Label' +) + +export { Label } diff --git a/packages/zeego/src/dropdown-menu/__tests__/index.test.tsx b/packages/zeego/src/dropdown-menu/__tests__/index.test.tsx index bf84291..458465d 100644 --- a/packages/zeego/src/dropdown-menu/__tests__/index.test.tsx +++ b/packages/zeego/src/dropdown-menu/__tests__/index.test.tsx @@ -1 +1,48 @@ -it.todo('write a test'); +// import * as ios from '../dropdown-menu.ios' +// import * as android from '../dropdown-menu.android' +// import * as web from '../dropdown-menu.web' + +jest.mock('react-native-ios-context-menu', () => { + const RN = jest.requireActual('react-native') + + return { + RCTContextMenuView: () => RN.View, + ContextMenuButton: 'TouchableOpacity', + } +}) + +describe('imports', () => { + it('should have same imports across platforms', () => { + jest.resetModules() + jest.doMock('react-native', () => ({ + Platform: { + OS: 'ios', + select(arg) { + return arg.ios || arg.native || arg.default + }, + }, + })) + + // TODO how do we mock platform file extensions like index.android.ts here? + // https://github.com/facebook/jest/issues/1370 + const ios = require('../dropdown-menu.ios') + + const iosKeys = Object.keys(ios).sort().join(',') + + jest.resetModules() + jest.doMock('react-native', () => ({ + Platform: { + OS: 'android', + select(arg) { + return arg.android || arg.native || arg.default + }, + }, + })) + + const android = require('../dropdown-menu.android') + + const androidKeys = Object.keys(android).sort().join(',') + + expect(iosKeys).toEqual(androidKeys) + }) +}) diff --git a/packages/zeego/src/dropdown-menu/dropdown-menu.web.tsx b/packages/zeego/src/dropdown-menu/dropdown-menu.web.tsx index cf6b62e..8ecdd49 100644 --- a/packages/zeego/src/dropdown-menu/dropdown-menu.web.tsx +++ b/packages/zeego/src/dropdown-menu/dropdown-menu.web.tsx @@ -217,7 +217,7 @@ export { ItemIcon, } -export { ItemImage } from './web/item-image' +export { ItemImage } from '../menu/web-primitives/item-image' export { Label } from './web/label' export { ItemTitle, ItemSubtitle } from '../menu' diff --git a/packages/zeego/src/dropdown-menu/web/item-image.tsx b/packages/zeego/src/dropdown-menu/web/item-image.tsx deleted file mode 100644 index f67602b..0000000 --- a/packages/zeego/src/dropdown-menu/web/item-image.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { MenuDisplayName, MenuItemImageProps } from '../../menu' -import { Image } from 'react-native' - -import React from 'react' - -const ItemImage = ({ - source, - style, - height, - width, - fadeDuration = 0, - resizeMode, -}: MenuItemImageProps) => { - return ( - - ) -} - -ItemImage.displayName = MenuDisplayName.ItemImage - -export { ItemImage } diff --git a/packages/zeego/src/menu/__tests__/index.test.tsx b/packages/zeego/src/menu/__tests__/index.test.tsx deleted file mode 100644 index bf84291..0000000 --- a/packages/zeego/src/menu/__tests__/index.test.tsx +++ /dev/null @@ -1 +0,0 @@ -it.todo('write a test'); diff --git a/packages/zeego/src/menu/create-android-menu/index.android.tsx b/packages/zeego/src/menu/create-android-menu/index.android.tsx index 5a27272..aa67e15 100644 --- a/packages/zeego/src/menu/create-android-menu/index.android.tsx +++ b/packages/zeego/src/menu/create-android-menu/index.android.tsx @@ -24,6 +24,8 @@ import type { MenuItemImageProps, MenuItemIndicatorProps, MenuLabelProps, + ContextMenuPreviewProps, + ContextMenuContentProps, } from '../types' import { View } from 'react-native' @@ -38,9 +40,10 @@ const createAndroidMenu = (Menu: 'ContextMenu' | 'DropdownMenu') => { return <>{children} }, 'Group') - const Content = menuify(({ children }: MenuContentProps) => { - if (!children) { - console.error(`[zeego] children must be written directly inline. + const Content = menuify( + ({ children }: MenuContentProps | ContextMenuContentProps) => { + if (!children) { + console.error(`[zeego] children must be written directly inline. You cannot wrap this component into its own component. It should look like this: @@ -54,9 +57,11 @@ You cannot wrap this component into its own component. It should look like this: Notice that the are all children of the component. That's important. If you want to use a custom component as your , you can use the menuify() method. But you still need to pass all items as children of .`) - } - return <>{children} - }, 'Content') + } + return <>{children} + }, + 'Content' + ) const ItemTitle = menuify(({ children }: MenuItemTitleProps) => { if (typeof children != 'string') { @@ -168,8 +173,10 @@ If you want to use a custom component as your , you can use the menui const Root = menuify((props: MenuRootProps) => { const trigger = pickChildren(props.children, Trigger) - const content = pickChildren(props.children, Content) - .targetChildren?.[0] + const content = pickChildren( + props.children, + Content + ).targetChildren?.[0] const callbacks: Record void> = {} @@ -374,19 +381,6 @@ If you want to use a custom component as your , you can use the menui ) } - // let menuItems: (MenuItem | MenuConfig)[] = [] - - // Children.forEach(flattenChildren(props.children), (_child) => { - // const child = _child as ReactElement - // if (isInstanceOfComponent(child, Content)) { - // menuItems.push( - // ...mapItemsChildren( - // (child as ReactElement).props.children - // ).filter(filterNull) - // ) - // } - // }) - const menuItems = mapItemsChildren(content?.props.children).filter( filterNull ) @@ -421,6 +415,8 @@ If you want to use a custom component as your , you can use the menui 'ItemIndicator' ) + const Preview = menuify((_: ContextMenuPreviewProps) => <>, 'Preview') + return { Root, Trigger, @@ -436,6 +432,7 @@ If you want to use a custom component as your , you can use the menui CheckboxItem, ItemImage, Label, + Preview, } } diff --git a/packages/zeego/src/menu/create-ios-menu/index.ios.tsx b/packages/zeego/src/menu/create-ios-menu/index.ios.tsx index d0298c2..de3416c 100644 --- a/packages/zeego/src/menu/create-ios-menu/index.ios.tsx +++ b/packages/zeego/src/menu/create-ios-menu/index.ios.tsx @@ -13,6 +13,8 @@ import type { MenuItemImageProps, MenuItemIndicatorProps, MenuLabelProps, + ContextMenuPreviewProps, + ContextMenuContentProps, } from '../types' import React, { Children, ReactElement } from 'react' import { @@ -39,9 +41,10 @@ const createIosMenu = (Menu: 'ContextMenu' | 'DropdownMenu') => { return <>{children} }, 'Group') - const Content = menuify(({ children }: MenuContentProps) => { - if (!children) { - console.error(`[zeego] children must be written directly inline. + const Content = menuify( + ({ children }: MenuContentProps | ContextMenuContentProps) => { + if (!children) { + console.error(`[zeego] children must be written directly inline. You cannot wrap this component into its own component. It should look like this: @@ -55,9 +58,11 @@ You cannot wrap this component into its own component. It should look like this: Notice that the are all children of the component. That's important. If you want to use a custom component as your , you can use the menuify() method. But you still need to pass all items as children of .`) - } - return <>{children} - }, 'Content') + } + return <>{children} + }, + 'Content' + ) const ItemTitle = menuify(({ children }: MenuItemTitleProps) => { if (typeof children != 'string') { @@ -126,6 +131,14 @@ If you want to use a custom component as your , you can use the menui return <>{children} }, 'TriggerItem') + const Preview = menuify((_: ContextMenuPreviewProps) => { + return <> + }, 'Preview') + + Preview.defaultProps = { + isResizeAnimated: true, + } + const CheckboxItem = menuify(({}: MenuCheckboxItemProps) => { return <> }, 'CheckboxItem') @@ -163,8 +176,10 @@ If you want to use a custom component as your , you can use the menui const Root = menuify((props: MenuRootProps) => { const trigger = pickChildren(props.children, Trigger) - const content = pickChildren(props.children, Content) - .targetChildren?.[0] + const content = pickChildren( + props.children, + Content + ).targetChildren?.[0] const callbacks: Record void> = {} @@ -333,10 +348,9 @@ If you want to use a custom component as your , you can use the menui const triggerItem = triggerItemChild && getItemFromChild(triggerItemChild, index) if (triggerItem) { - const nestedContent = pickChildren( - child.props.children, - Content - ).targetChildren?.[0] + const nestedContent = pickChildren< + MenuContentProps | ContextMenuContentProps + >(child.props.children, Content).targetChildren?.[0] if (nestedContent) { const nestedItems = mapItemsChildren( @@ -387,6 +401,11 @@ If you want to use a custom component as your , you can use the menui const Component = Menu === 'ContextMenu' ? ContextMenuView : ContextMenuButton + const preview = pickChildren(content?.props.children, Preview) + .targetChildren?.[0] + + const previewProps = preview?.props as ContextMenuPreviewProps | undefined + return ( { @@ -398,8 +417,42 @@ If you want to use a custom component as your , you can use the menui style={[{ flexGrow: 0 }, props.style]} menuConfig={{ menuTitle, - menuItems: menuItems, + menuItems, }} + renderPreview={ + Menu == 'ContextMenu' && preview && previewProps?.children + ? () => { + return ( + <> + {typeof previewProps?.children == 'function' + ? previewProps.children() + : previewProps?.children} + + ) + } + : undefined + } + lazyPreview={ + Menu === 'ContextMenu' + ? typeof previewProps?.children == 'function' + : undefined + } + onPressMenuPreview={ + Menu == 'ContextMenu' ? previewProps?.onPress : undefined + } + previewConfig={ + preview + ? { + // ...previewProps, + previewType: 'CUSTOM', + previewSize: previewProps?.size, + backgroundColor: previewProps?.backgroundColor, + borderRadius: previewProps?.borderRadius, + isResizeAnimated: previewProps?.isResizeAnimated, + preferredCommitStyle: previewProps?.preferredCommitStyle, + } + : undefined + } > {trigger.targetChildren?.[0]} @@ -430,6 +483,7 @@ If you want to use a custom component as your , you can use the menui CheckboxItem, ItemImage, Label, + Preview, } } diff --git a/packages/zeego/src/menu/display-names.tsx b/packages/zeego/src/menu/display-names.tsx index 65c9eb5..b295787 100644 --- a/packages/zeego/src/menu/display-names.tsx +++ b/packages/zeego/src/menu/display-names.tsx @@ -15,6 +15,7 @@ export const MenuDisplayName = { CheckboxItem: 'CheckboxItem', Label: 'Label', ItemIndicator: 'ItemIndicator', + Preview: 'Preview', } as const type DisplayNames = typeof MenuDisplayName diff --git a/packages/zeego/src/menu/types.ts b/packages/zeego/src/menu/types.ts index bc436f6..65fced3 100644 --- a/packages/zeego/src/menu/types.ts +++ b/packages/zeego/src/menu/types.ts @@ -1,5 +1,6 @@ import type { Text, View, ImageRequireSource, ImageProps } from 'react-native' import type { MenuContentProps as RadixContentProps } from '@radix-ui/react-dropdown-menu' +import type { ContextMenuView } from 'react-native-ios-context-menu' type ViewStyle = React.ComponentProps['style'] type TextStyle = React.ComponentProps['style'] @@ -12,6 +13,7 @@ export type MenuTriggerProps = { children: React.ReactElement style?: ViewStyle } + export type MenuContentProps = { children: React.ReactNode style?: ViewStyle @@ -26,6 +28,8 @@ export type MenuContentProps = { | 'sideOffset' > +export type ContextMenuContentProps = Omit + export type MenuGroupProps = { children: React.ReactNode style?: ViewStyle @@ -130,3 +134,16 @@ export type MenuLabelProps = { children: string style?: TextStyle } + +type Not> = Omit + +export type ContextMenuPreviewProps = { + children: React.ReactNode | (() => React.ReactNode) + size?: NonNullable< + React.ComponentProps['previewConfig'] + >['previewSize'] + onPress?: React.ComponentProps['onPressMenuPreview'] +} & Not< + NonNullable['previewConfig']>, + 'targetViewNode' | 'previewSize' | 'previewType' +> diff --git a/packages/zeego/src/menu/web-primitives/index.tsx b/packages/zeego/src/menu/web-primitives/index.tsx index 86764f4..de78759 100644 --- a/packages/zeego/src/menu/web-primitives/index.tsx +++ b/packages/zeego/src/menu/web-primitives/index.tsx @@ -6,7 +6,7 @@ import type { import { Text, View } from 'react-native' import { pickChildren } from '../children' import React from 'react' -import { MenuDisplayName } from '../display-names' +import { menuify } from '../display-names' const ItemPrimitive = ({ children, style }: MenuItemProps) => { const titleChildren = pickChildren(children, ItemTitle) @@ -30,22 +30,20 @@ const ItemPrimitive = ({ children, style }: MenuItemProps) => { ) } -const ItemTitle = ({ children, style }: MenuItemTitleProps) => { +const ItemTitle = menuify(({ children, style }: MenuItemTitleProps) => { return ( {children} ) -} -ItemTitle.displayName = MenuDisplayName.ItemTitle +}, 'ItemTitle') -const ItemSubtitle = ({ children, style }: MenuItemSubtitleProps) => { +const ItemSubtitle = menuify(({ children, style }: MenuItemSubtitleProps) => { return ( {children} ) -} -ItemSubtitle.displayName = MenuDisplayName.ItemSubtitle +}, 'ItemSubtitle') export { ItemPrimitive, ItemSubtitle, ItemTitle } diff --git a/packages/zeego/src/menu/web-primitives/item-image.tsx b/packages/zeego/src/menu/web-primitives/item-image.tsx new file mode 100644 index 0000000..4186993 --- /dev/null +++ b/packages/zeego/src/menu/web-primitives/item-image.tsx @@ -0,0 +1,30 @@ +import { Image } from 'react-native' +import type { MenuItemImageProps } from '../types' + +import React from 'react' +import { menuify } from '../display-names' + +const ItemImage = menuify( + ({ + source, + style, + height, + width, + fadeDuration = 0, + resizeMode, + }: MenuItemImageProps) => { + return ( + + ) + }, + 'ItemImage' +) + +export { ItemImage } diff --git a/tsconfig.build.json b/tsconfig.build.json index b48b5a4..24ddb5a 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,6 +1,6 @@ { "extends": "./tsconfig", - "exclude": ["example"], + "exclude": ["example", "**/*/__tests__/**/*", "**/*.test.ts"], "compilerOptions": { "noEmit": false }