diff --git a/windows/NuGet.Config b/NuGet.config similarity index 84% rename from windows/NuGet.Config rename to NuGet.config index 37da5d50a4..fe459fedd6 100644 --- a/windows/NuGet.Config +++ b/NuGet.config @@ -1,12 +1,9 @@ - - - - + diff --git a/example/NuGet.config b/example/NuGet.config new file mode 100644 index 0000000000..d990d66d51 --- /dev/null +++ b/example/NuGet.config @@ -0,0 +1,6 @@ + + + + diff --git a/example/jest.config.windows.js b/example/jest.config.windows.js new file mode 100644 index 0000000000..4ae04e8b48 --- /dev/null +++ b/example/jest.config.windows.js @@ -0,0 +1,3 @@ +const config = {}; + +module.exports = require('@rnx-kit/jest-preset')('windows', config); diff --git a/example/metro.config.js b/example/metro.config.js index 6066b4251c..53cf89058e 100644 --- a/example/metro.config.js +++ b/example/metro.config.js @@ -1,11 +1,44 @@ -const {makeMetroConfig} = require('@rnx-kit/metro-config'); -module.exports = makeMetroConfig({ - transformer: { - getTransformOptions: async () => ({ - transform: { - experimentalImportSupport: false, - inlineRequires: false, - }, - }), - }, -}); +const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); +const path = require('path'); + +const root = path.resolve(__dirname, '..'); + +const defaultConfig = getDefaultConfig(__dirname); + +/** + * Metro configuration + * https://reactnative.dev/docs/metro + * + * @type {import('metro-config').MetroConfig} + */ +const config = { + watchFolders: [root], + resolver: { + // Ensure .ts and .tsx files are resolved (for Windows-specific TypeScript files) + sourceExts: [...defaultConfig.resolver.sourceExts, 'ts', 'tsx'], + // Add 'windows' to platform extensions so Metro resolves .windows.ts/.windows.js files + platforms: [...(defaultConfig.resolver.platforms || []), 'windows'], + // Make sure Metro can resolve the picker package from the parent folder + extraNodeModules: { + '@react-native-picker/picker': root, + }, + // Block duplicate react-native packages + blockList: [ + new RegExp(`${root.replace(/[/\\]/g, '[/\\\\]')}/node_modules/react-native/.*`), + ], + nodeModulesPaths: [ + path.resolve(__dirname, 'node_modules'), + path.resolve(root, 'node_modules'), + ], + }, + transformer: { + getTransformOptions: async () => ({ + transform: { + experimentalImportSupport: false, + inlineRequires: true, + }, + }), + }, +}; + +module.exports = mergeConfig(defaultConfig, config); diff --git a/example/package.json b/example/package.json index 921af59da9..c61da7d6b6 100644 --- a/example/package.json +++ b/example/package.json @@ -14,38 +14,47 @@ "mkdist": "node -e \"require('node:fs').mkdirSync('dist', { recursive: true, mode: 0o755 })\"", "start": "react-native start", "test": "jest", - "windows": "react-native run-windows" + "windows": "npx @react-native-community/cli run-windows", + "test:windows": "jest --config jest.config.windows.js" }, "dependencies": { "@react-native-picker/picker": "workspace:^", - "react": "18.3.1", - "react-native": "0.76.3", + "react": "19.1.0", + "react-native": "0.82.0", "react-native-macos": "^0.75.0", - "react-native-windows": "^0.76.0" + "react-native-windows": "0.0.0-canary.1020" }, "devDependencies": { "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.3", "@babel/runtime": "^7.25.0", - "@react-native-community/cli": "15.0.1", - "@react-native-community/cli-platform-android": "15.0.1", - "@react-native-community/cli-platform-ios": "15.0.1", - "@react-native/babel-preset": "0.76.3", - "@react-native/eslint-config": "0.76.3", - "@react-native/metro-config": "0.76.3", - "@react-native/typescript-config": "0.76.3", + "@react-native-community/cli": "20.0.0", + "@react-native-community/cli-platform-android": "20.0.0", + "@react-native-community/cli-platform-ios": "20.0.0", + "@react-native/babel-preset": "0.82.0", + "@react-native/eslint-config": "0.82.0", + "@react-native/metro-config": "0.82.0", + "@react-native/typescript-config": "0.82.0", + "@rnx-kit/jest-preset": "^0.1.17", "@rnx-kit/metro-config": "^2.0.0", - "@types/react": "^18.2.6", - "@types/react-test-renderer": "^18.0.0", + "@types/react": "^19.1.0", + "@types/react-test-renderer": "^19.0.0", "babel-jest": "^29.6.3", "eslint": "^8.19.0", "jest": "^29.6.3", "prettier": "2.8.8", "react-native-test-app": "^4.0.4", - "react-test-renderer": "18.3.1", + "react-test-renderer": "19.1.0", "typescript": "5.0.4" }, "engines": { - "node": ">=18" + "node": ">=22" + }, + "react-native-windows": { + "init-windows": { + "name": "PickerExample", + "namespace": "PickerExample", + "template": "cpp-app" + } } } diff --git a/example/react-native.config.js b/example/react-native.config.js index f192ab33dd..f779ac0164 100644 --- a/example/react-native.config.js +++ b/example/react-native.config.js @@ -1,23 +1,47 @@ -const project = (() => { - try { - const {configureProjects} = require('react-native-test-app'); - return configureProjects({ - android: { - sourceDir: 'android', - }, - ios: { - sourceDir: 'ios', - }, - windows: { - sourceDir: 'windows', - solutionFile: 'windows/Example.sln', - }, - }); - } catch (_) { - return undefined; - } -})(); - -module.exports = { - ...(project ? {project} : undefined), -}; +const path = require('path'); + +const project = (() => { + try { + const {configureProjects} = require('react-native-test-app'); + return configureProjects({ + android: { + sourceDir: 'android', + }, + ios: { + sourceDir: 'ios', + }, + windows: { + sourceDir: 'windows', + solutionFile: 'PickerExample.sln', + }, + }); + } catch (_) { + return undefined; + } +})(); + +module.exports = { + ...(project ? {project} : undefined), + dependencies: { + '@react-native-picker/picker': { + root: path.resolve(__dirname, '..'), + platforms: { + windows: { + sourceDir: 'windows', + solutionFile: 'Picker.sln', + projects: [ + { + projectFile: 'Picker\\Picker.vcxproj', + projectName: 'Picker', + projectLang: 'cpp', + projectGuid: '{170F439F-1AC2-40F6-94D2-FB6511EDF052}', + directDependency: true, + cppHeaders: ['winrt/Picker.h'], + cppPackageProviders: ['Picker::ReactPackageProvider'], + }, + ], + }, + }, + }, + }, +}; diff --git a/example/src/App.tsx b/example/src/App.tsx index f3a79ba9b5..c16762028b 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,93 +1,94 @@ -import * as React from 'react'; -import { - Platform, - ScrollView, - StyleSheet, - Text, - View, - SafeAreaView, - I18nManager, - Switch, -} from 'react-native'; - -import * as PickerExamples from './PickerExample'; -import * as PickerIOSExamples from './PickerIOSExample'; -import * as PickerWindowsExamples from './PickerWindowsExamples'; - -export default function App() { - const [isRTL, setIsRTL] = React.useState(I18nManager.isRTL); - React.useEffect(() => { - I18nManager.allowRTL(true); - }, []); - return ( - - - - { - setIsRTL(newValue); - I18nManager.forceRTL(newValue); - }} - /> - {I18nManager.isRTL ? 'RTL' : 'LTR'} - - - Picker Examples - {PickerExamples.examples.map((element) => ( - - {element.title} - {element.render()} - - ))} - {Platform.OS === 'ios' && ( - PickerIOS Examples - )} - {Platform.OS === 'ios' && - PickerIOSExamples.examples.map((element) => ( - - {element.title} - {element.render()} - - ))} - {Platform.OS === 'windows' && ( - PickerWindows Examples - )} - {Platform.OS === 'windows' && - PickerWindowsExamples.examples.map((element) => ( - - {element.title} - {element.render()} - - ))} - - - - ); -} - -const styles = StyleSheet.create({ - main: { - backgroundColor: '#F5FCFF', - }, - container: { - padding: 24, - paddingBottom: 60, - }, - title: { - fontSize: 18, - }, - elementContainer: { - marginTop: 8, - }, - heading: { - fontSize: 22, - color: 'black', - }, - rtlSwitchContainer: { - flexDirection: 'row', - justifyContent: 'space-between', - paddingHorizontal: 40, - paddingTop: 20, - }, -}); +import * as React from 'react'; +import { + Platform, + ScrollView, + StyleSheet, + Text, + View, + SafeAreaView, + I18nManager, + Switch, +} from 'react-native'; + +import * as PickerExamples from './PickerExample'; +import * as PickerIOSExamples from './PickerIOSExample'; +import * as PickerWindowsExamples from './PickerWindowsExample'; + +export default function App() { + const [isRTL, setIsRTL] = React.useState(I18nManager.isRTL); + React.useEffect(() => { + I18nManager.allowRTL(true); + }, []); + return ( + + + + { + setIsRTL(newValue); + I18nManager.forceRTL(newValue); + }} + /> + {I18nManager.isRTL ? 'RTL' : 'LTR'} + + + {Platform.OS === 'android' && (Picker Examples)} + {Platform.OS === 'android' && + PickerExamples.examples.map((element) => ( + + {element.title} + {element.render()} + + ))} + {Platform.OS === 'ios' && ( + PickerIOS Examples + )} + {Platform.OS === 'ios' && + PickerIOSExamples.examples.map((element) => ( + + {element.title} + {element.render()} + + ))} + {Platform.OS === 'windows' && ( + PickerWindows Examples + )} + {Platform.OS === 'windows' && + PickerWindowsExamples.examples.map((element) => ( + + {element.title} + {element.render()} + + ))} + + + + ); +} + +const styles = StyleSheet.create({ + main: { + backgroundColor: '#F5FCFF', + }, + container: { + padding: 24, + paddingBottom: 60, + }, + title: { + fontSize: 18, + }, + elementContainer: { + marginTop: 8, + }, + heading: { + fontSize: 22, + color: 'black', + }, + rtlSwitchContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + paddingHorizontal: 40, + paddingTop: 20, + }, +}); diff --git a/example/src/PickerWindowsExample.tsx b/example/src/PickerWindowsExample.tsx new file mode 100644 index 0000000000..dab49ff013 --- /dev/null +++ b/example/src/PickerWindowsExample.tsx @@ -0,0 +1,705 @@ +import * as React from 'react'; +import {View, Text, Button, StyleSheet} from 'react-native'; +// @ts-ignore - module resolution works at runtime via workspace linking +import { + RNCPicker, + type RNCPickerItem, + type RNCPickerChangeEvent, +} from '@react-native-picker/picker'; + +// Reusable Card component for examples +function ExampleCard({children}: {children: React.ReactNode}) { + return {children}; +} + + + +// Example 1: Basic Picker with Placeholder +function PlaceholderPickerExample() { + const [selectedIndex, setSelectedIndex] = React.useState(-1); + const items: RNCPickerItem[] = [ + {label: 'JavaScript', value: 'js'}, + {label: 'TypeScript', value: 'ts'}, + {label: 'Python', value: 'py'}, + {label: 'C++', value: 'cpp'}, + ]; + + const handleChange = (event: {nativeEvent: RNCPickerChangeEvent}) => { + setSelectedIndex(event.nativeEvent.itemIndex); + }; + + return ( + + + Picker with placeholder text when nothing is selected + + + + + + Selected: + + {selectedIndex >= 0 ? items[selectedIndex]?.label : 'None'} + + + + ); +} + +// Example 2: Basic Picker +function BasicPickerExample() { + const [selectedIndex, setSelectedIndex] = React.useState(0); + const items: RNCPickerItem[] = [ + {label: 'Small', value: 'sm'}, + {label: 'Medium', value: 'md'}, + {label: 'Large', value: 'lg'}, + {label: 'Extra Large', value: 'xl'}, + ]; + + const handleChange = (event: {nativeEvent: RNCPickerChangeEvent}) => { + setSelectedIndex(event.nativeEvent.itemIndex); + }; + + return ( + + Basic picker with pre-selected value + + + + + Size: + {items[selectedIndex]?.label} + + + ); +} + +// Example 3: Disabled Picker +function DisabledPickerExample() { + const items: RNCPickerItem[] = [ + {label: 'Option A', value: 'a'}, + {label: 'Option B (Selected)', value: 'b'}, + {label: 'Option C', value: 'c'}, + ]; + + return ( + + + Disabled picker - user cannot interact + + + + + + + This picker is disabled and cannot be changed + + + + ); +} + +// Example 4: Editable Picker +function EditablePickerExample() { + const [selectedIndex, setSelectedIndex] = React.useState(-1); + const [text, setText] = React.useState(''); + const items: RNCPickerItem[] = [ + {label: 'Apple', value: 'apple'}, + {label: 'Banana', value: 'banana'}, + {label: 'Cherry', value: 'cherry'}, + {label: 'Date', value: 'date'}, + {label: 'Elderberry', value: 'elderberry'}, + ]; + + const handleChange = (event: {nativeEvent: RNCPickerChangeEvent}) => { + setSelectedIndex(event.nativeEvent.itemIndex); + setText(event.nativeEvent.text); + }; + + return ( + + + Editable picker - type to filter or enter custom text + + + + + + Text: + {text || '(empty)'} + + + Selected Index: + {selectedIndex} + + + ); +} + +// Example 5: Color Picker +function ColorPickerExample() { + const [selectedIndex, setSelectedIndex] = React.useState(0); + const items: RNCPickerItem[] = [ + {label: 'Red', value: 'red'}, + {label: 'Green', value: 'green'}, + {label: 'Blue', value: 'blue'}, + {label: 'Orange', value: 'orange'}, + {label: 'Purple', value: 'purple'}, + ]; + + const handleChange = (event: {nativeEvent: RNCPickerChangeEvent}) => { + setSelectedIndex(event.nativeEvent.itemIndex); + }; + + const selectedColor = items[selectedIndex]?.value || 'gray'; + + return ( + + Pick a color and see it applied + + + + + + Selected: {items[selectedIndex]?.label} + + + + ); +} + +// Example 6: Multiple Pickers in a Form +function FormPickerExample() { + const [countryIndex, setCountryIndex] = React.useState(0); + const [categoryIndex, setCategoryIndex] = React.useState(-1); + + const countries: RNCPickerItem[] = [ + {label: 'United States', value: 'us'}, + {label: 'Canada', value: 'ca'}, + {label: 'United Kingdom', value: 'uk'}, + {label: 'Germany', value: 'de'}, + {label: 'France', value: 'fr'}, + {label: 'Japan', value: 'jp'}, + ]; + + const categories: RNCPickerItem[] = [ + {label: 'Electronics', value: 'electronics'}, + {label: 'Clothing', value: 'clothing'}, + {label: 'Books', value: 'books'}, + {label: 'Home & Garden', value: 'home'}, + {label: 'Sports', value: 'sports'}, + ]; + + return ( + + Multiple pickers in a form layout + + + Country: + + + setCountryIndex(e.nativeEvent.itemIndex) + } + style={styles.picker} + /> + + + + + Category: + + + setCategoryIndex(e.nativeEvent.itemIndex) + } + style={styles.picker} + /> + + + + + Selection: + + {countries[countryIndex]?.label} /{' '} + {categoryIndex >= 0 ? categories[categoryIndex]?.label : 'None'} + + + + ); +} + +// Example 7: Picker with Many Items +function ManyItemsPickerExample() { + const [selectedIndex, setSelectedIndex] = React.useState(0); + const items: RNCPickerItem[] = Array.from({length: 50}, (_, i) => ({ + label: `Item ${i + 1}`, + value: `item_${i + 1}`, + })); + + return ( + + Picker with 50 items (scrollable) + + + setSelectedIndex(e.nativeEvent.itemIndex) + } + style={styles.picker} + /> + + + Selected: + {items[selectedIndex]?.label} + + + ); +} + +// Example 8: Dynamic Items +function DynamicItemsPickerExample() { + const INITIAL_ITEMS: RNCPickerItem[] = [ + { label: 'Apple', value: 'apple' }, + { label: 'Banana', value: 'banana' }, + { label: 'Cherry', value: 'cherry' }, + { label: 'Date', value: 'date' }, + { label: 'Elderberry', value: 'elderberry' }, + ]; + + const [items, setItems] = React.useState(INITIAL_ITEMS); + const [selectedIndex, setSelectedIndex] = React.useState(-1); + const [addedItems, setAddedItems] = React.useState([]); + const [removedItems, setRemovedItems] = React.useState([]); + const [addCounters, setAddCounters] = React.useState>({}); + + /* 🔍 Always log EXACT picker items */ + React.useEffect(() => { + console.log( + 'Picker Items:', + items.map(i => i.label) + ); + }, [items]); + + /* ➕ ADD */ + const addSelectedItem = () => { + if (selectedIndex < 0 || selectedIndex >= items.length) return; + + const baseItem = items[selectedIndex]; + + setAddCounters(prevCounters => { + const nextCount = (prevCounters[baseItem.label] || 0) + 1; + + const newItem: RNCPickerItem = { + label: `${baseItem.label} ${nextCount}`, + value: `${baseItem.value}-${nextCount}-${Date.now()}`, // unique + }; + + // add to picker items + setItems(prevItems => [...prevItems, newItem]); + + // log + setAddedItems(prev => [...prev, newItem.label]); + + return { + ...prevCounters, + [baseItem.label]: nextCount, + }; + }); + + setSelectedIndex(-1); + }; + + /* ➖ REMOVE (AFFECTS PICKER + LOG) */ + const removeSelectedItem = () => { + if ( + selectedIndex < 0 || + selectedIndex >= items.length || + items.length <= 1 + ) { + return; + } + + const removedItem = items[selectedIndex]; + + setRemovedItems(prev => [...prev, removedItem.label]); + setItems(prev => prev.filter((_, i) => i !== selectedIndex)); + + // Also remove from added list if present + setAddedItems(prev => + prev.filter(label => label !== removedItem.label) + ); + + setSelectedIndex(-1); + }; + + /* 🔄 RESET EVERYTHING */ + const resetItems = () => { + setItems(INITIAL_ITEMS); + setSelectedIndex(-1); + setAddedItems([]); + setRemovedItems([]); + setAddCounters({}); + }; + + return ( + + + Select an item, then Add (log only) or Remove (from picker) + + + + i.value).join('|')} + items={items} + selectedIndex={selectedIndex} + placeholder="Select a fruit..." + onPickerSelect={(e: { nativeEvent: RNCPickerChangeEvent }) => + setSelectedIndex(e.nativeEvent.itemIndex) + } + style={styles.picker} + /> + + + + +