@idealyst/components 1.0.94 → 1.0.96

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/components",
3
- "version": "1.0.94",
3
+ "version": "1.0.96",
4
4
  "description": "Shared component library for React and React Native",
5
5
  "documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/components#readme",
6
6
  "readme": "README.md",
@@ -41,7 +41,7 @@
41
41
  "publish:npm": "npm publish"
42
42
  },
43
43
  "peerDependencies": {
44
- "@idealyst/theme": "^1.0.94",
44
+ "@idealyst/theme": "^1.0.96",
45
45
  "@mdi/js": ">=7.0.0",
46
46
  "@mdi/react": ">=1.0.0",
47
47
  "@react-native-vector-icons/common": ">=12.0.0",
@@ -91,7 +91,7 @@
91
91
  }
92
92
  },
93
93
  "devDependencies": {
94
- "@idealyst/theme": "^1.0.94",
94
+ "@idealyst/theme": "^1.0.96",
95
95
  "@mdi/react": "^1.6.1",
96
96
  "@types/react": "^19.1.0",
97
97
  "react": "^19.1.0",
@@ -9,6 +9,7 @@ const Input = React.forwardRef<TextInput, InputProps>(({
9
9
  onChangeText,
10
10
  onFocus,
11
11
  onBlur,
12
+ onPress,
12
13
  placeholder,
13
14
  disabled = false,
14
15
  inputType = 'text',
@@ -50,6 +51,12 @@ const Input = React.forwardRef<TextInput, InputProps>(({
50
51
  }
51
52
  };
52
53
 
54
+ const handlePress = () => {
55
+ if (onPress) {
56
+ onPress();
57
+ }
58
+ }
59
+
53
60
  const handleBlur = () => {
54
61
  setIsFocused(false);
55
62
  if (onBlur) {
@@ -121,6 +128,7 @@ const Input = React.forwardRef<TextInput, InputProps>(({
121
128
 
122
129
  {/* Input */}
123
130
  <TextInput
131
+ onPress={handlePress}
124
132
  ref={ref}
125
133
  value={value}
126
134
  onChangeText={onChangeText}
@@ -86,7 +86,8 @@ function createFocusedCompoundVariants(theme: Theme) {
86
86
  styles: {
87
87
  borderColor: focusColor,
88
88
  _web: {
89
- border: `2px solid ${focusColor}`,
89
+ border: `1px solid ${focusColor}`,
90
+ boxShadow: `0 0 0 2px ${focusColor}20`,
90
91
  },
91
92
  },
92
93
  });
@@ -1,16 +1,17 @@
1
- import React, { useState, isValidElement, useRef } from 'react';
1
+ import React, { isValidElement, useState } from 'react';
2
2
  import { getWebProps } from 'react-native-unistyles/web';
3
- import { InputProps } from './types';
4
- import { inputStyles } from './Input.styles';
5
3
  import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
6
- import { resolveIconPath, isIconName } from '../Icon/icon-resolver';
4
+ import { isIconName, resolveIconPath } from '../Icon/icon-resolver';
7
5
  import useMergeRefs from '../hooks/useMergeRefs';
6
+ import { inputStyles } from './Input.styles';
7
+ import { InputProps } from './types';
8
8
 
9
9
  const Input = React.forwardRef<HTMLInputElement, InputProps>(({
10
10
  value,
11
11
  onChangeText,
12
12
  onFocus,
13
13
  onBlur,
14
+ onPress,
14
15
  placeholder,
15
16
  disabled = false,
16
17
  inputType = 'text',
@@ -58,7 +59,18 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
58
59
  }
59
60
  };
60
61
 
61
- const handleFocus = () => {
62
+ const handlePress = (e: React.MouseEvent<HTMLDivElement>) => {
63
+ // For web compatibility, we can trigger onFocus when pressed
64
+ e.preventDefault();
65
+ e.stopPropagation();
66
+ if (onPress) {
67
+ onPress();
68
+ }
69
+ }
70
+
71
+ const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
72
+ e.preventDefault();
73
+ e.stopPropagation();
62
74
  setIsFocused(true);
63
75
  if (onFocus) {
64
76
  onFocus();
@@ -97,6 +109,12 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
97
109
  // Get input props
98
110
  const inputWebProps = getWebProps([inputStyles.input]);
99
111
 
112
+ const handleContainerPress = (e: React.MouseEvent<HTMLDivElement>) => {
113
+ e.preventDefault();
114
+ e.stopPropagation();
115
+ inputWebProps.ref?.current?.focus();
116
+ }
117
+
100
118
  // Merge the forwarded ref with unistyles ref for the input
101
119
  const mergedInputRef = useMergeRefs(ref, inputWebProps.ref);
102
120
 
@@ -154,7 +172,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
154
172
  };
155
173
 
156
174
  return (
157
- <div {...containerProps} data-testid={testID}>
175
+ <div onClick={handleContainerPress} {...containerProps} data-testid={testID}>
158
176
  {/* Left Icon */}
159
177
  {leftIcon && (
160
178
  <span {...leftIconContainerProps}>
@@ -168,6 +186,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
168
186
  ref={mergedInputRef}
169
187
  type={getInputType()}
170
188
  value={value}
189
+ onClick={handlePress}
171
190
  onChange={handleChange}
172
191
  onFocus={handleFocus}
173
192
  onBlur={handleBlur}
@@ -29,6 +29,12 @@ export interface InputProps {
29
29
  */
30
30
  onBlur?: () => void;
31
31
 
32
+ /**
33
+ * Called when the input is pressed (for web compatibility)
34
+ * @returns
35
+ */
36
+ onPress?: () => void;
37
+
32
38
  /**
33
39
  * Placeholder text
34
40
  */
@@ -113,7 +113,8 @@ function buildDynamicTriggerStyles(theme: Theme) {
113
113
  true: {
114
114
  borderColor: theme.intents.primary.primary,
115
115
  _web: {
116
- border: `2px solid ${theme.intents.primary.primary}`,
116
+ border: `1px solid ${theme.intents.primary.primary}`,
117
+ boxShadow: `0 0 0 2px ${theme.intents.primary.primary}20`,
117
118
  outline: 'none',
118
119
  },
119
120
  },
@@ -137,7 +138,6 @@ export const selectStyles = StyleSheet.create((theme: Theme) => {
137
138
  return {
138
139
  container: {
139
140
  position: 'relative',
140
- backgroundColor: theme.colors.surface.primary,
141
141
  },
142
142
  label: {
143
143
  fontSize: 14,
@@ -256,35 +256,28 @@ export const selectStyles = StyleSheet.create((theme: Theme) => {
256
256
  flexDirection: 'row',
257
257
  alignItems: 'center',
258
258
  minHeight: 36,
259
- variants: {
260
- selected: {
261
- true: {
262
- backgroundColor: theme.intents.primary.light,
263
- },
264
- false: {},
259
+ _web: {
260
+ display: 'flex',
261
+ cursor: 'pointer',
262
+ _hover: {
263
+ backgroundColor: theme.colors.surface.secondary,
265
264
  },
266
- disabled: {
267
- true: {
268
- opacity: 0.5,
269
- _web: {
270
- cursor: 'not-allowed',
271
- },
272
- },
273
- false: {
274
- _web: {
275
- cursor: 'pointer',
276
- _hover: {
277
- backgroundColor: theme.colors.surface.secondary,
278
- },
279
- _active: {
280
- opacity: 0.8,
281
- },
282
- },
283
- },
265
+ _active: {
266
+ opacity: 0.8,
284
267
  },
285
268
  },
269
+ },
270
+ optionFocused: {
271
+ backgroundColor: theme.interaction.focusedBackground,
286
272
  _web: {
287
- display: 'flex',
273
+ outline: `1px solid ${theme.interaction.focusBorder}`,
274
+ outlineOffset: -1,
275
+ },
276
+ },
277
+ optionDisabled: {
278
+ opacity: theme.interaction.opacity.disabled,
279
+ _web: {
280
+ cursor: 'not-allowed',
288
281
  },
289
282
  },
290
283
  optionContent: {
@@ -1,12 +1,12 @@
1
- import React, { useState, useRef, useEffect, forwardRef } from 'react';
1
+ import React, { forwardRef, useEffect, useRef, useState } from 'react';
2
2
  // @ts-ignore - web-specific import
3
3
  import { getWebProps } from 'react-native-unistyles/web';
4
- import { SelectProps, SelectOption } from './types';
5
- import { selectStyles } from './Select.styles';
6
4
  import { IconSvg } from '../Icon/IconSvg/IconSvg.web';
7
5
  import { resolveIconPath } from '../Icon/icon-resolver';
8
- import { PositionedPortal } from '../internal/PositionedPortal';
9
6
  import useMergeRefs from '../hooks/useMergeRefs';
7
+ import { PositionedPortal } from '../internal/PositionedPortal';
8
+ import { selectStyles } from './Select.styles';
9
+ import { SelectOption, SelectProps } from './types';
10
10
 
11
11
  const Select = forwardRef<HTMLDivElement, SelectProps>(({
12
12
  options,
@@ -45,6 +45,13 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(({
45
45
  })
46
46
  : options;
47
47
 
48
+ // Get the index of the currently selected option
49
+ const getSelectedIndex = () => {
50
+ if (!value) return 0;
51
+ const index = filteredOptions.findIndex(option => option.value === value);
52
+ return index >= 0 ? index : 0;
53
+ };
54
+
48
55
  // Apply styles with variants
49
56
  selectStyles.useVariants({
50
57
  type,
@@ -54,10 +61,17 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(({
54
61
  focused: isOpen,
55
62
  });
56
63
 
57
-
58
- // Handle keyboard navigation
59
- const handleKeyDown = (event: KeyboardEvent) => {
60
- if (!isOpen) return;
64
+ // Handle keyboard navigation on the trigger button
65
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
66
+ // Handle opening with arrow keys when closed
67
+ if (!isOpen) {
68
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter' || event.key === ' ') {
69
+ event.preventDefault();
70
+ setIsOpen(true);
71
+ setFocusedIndex(getSelectedIndex());
72
+ }
73
+ return;
74
+ }
61
75
 
62
76
  switch (event.key) {
63
77
  case 'ArrowDown':
@@ -72,7 +86,34 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(({
72
86
  prev > 0 ? prev - 1 : filteredOptions.length - 1
73
87
  );
74
88
  break;
89
+ case 'Tab':
90
+ if (event.shiftKey) {
91
+ // Shift+Tab: go to previous option or exit
92
+ if (focusedIndex <= 0) {
93
+ setIsOpen(false);
94
+ } else {
95
+ event.preventDefault();
96
+ setFocusedIndex(prev => prev - 1);
97
+ }
98
+ } else {
99
+ // Tab: go to next option or exit
100
+ if (focusedIndex >= filteredOptions.length - 1) {
101
+ setIsOpen(false);
102
+ } else {
103
+ event.preventDefault();
104
+ setFocusedIndex(prev => prev < 0 ? 0 : prev + 1);
105
+ }
106
+ }
107
+ break;
75
108
  case 'Enter':
109
+ event.preventDefault();
110
+ if (focusedIndex >= 0 && focusedIndex < filteredOptions.length) {
111
+ const option = filteredOptions[focusedIndex];
112
+ if (!option.disabled) {
113
+ handleOptionSelect(option);
114
+ }
115
+ }
116
+ break;
76
117
  case ' ':
77
118
  event.preventDefault();
78
119
  if (focusedIndex >= 0 && focusedIndex < filteredOptions.length) {
@@ -82,36 +123,39 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(({
82
123
  }
83
124
  }
84
125
  break;
126
+ case 'Escape':
127
+ event.preventDefault();
128
+ setIsOpen(false);
129
+ break;
85
130
  }
86
131
  };
87
132
 
88
- useEffect(() => {
89
- if (!isOpen) return;
90
- document.addEventListener('keydown', handleKeyDown);
91
- return () => document.removeEventListener('keydown', handleKeyDown);
92
- }, [isOpen, focusedIndex, filteredOptions]);
93
-
94
133
  // Focus search input when dropdown opens
95
134
  useEffect(() => {
96
135
  if (isOpen && searchable && searchInputRef.current) {
97
- // Delay to ensure dropdown is positioned
98
136
  setTimeout(() => {
99
137
  searchInputRef.current?.focus();
100
138
  }, 50);
101
139
  }
102
140
  }, [isOpen, searchable]);
103
141
 
104
- const handleTriggerClick = () => {
105
- if (!disabled) {
106
- setIsOpen(!isOpen);
142
+ const handleTriggerClick = (e: React.MouseEvent<HTMLButtonElement>) => {
143
+ e.preventDefault();
144
+ e.stopPropagation();
145
+ };
146
+
147
+ const handleTriggerFocus = () => {
148
+ if (!disabled && !isOpen) {
149
+ setIsOpen(true);
107
150
  setSearchTerm('');
108
- setFocusedIndex(-1);
151
+ // Focus on selected option, or first option if none selected
152
+ setFocusedIndex(getSelectedIndex());
109
153
  }
110
154
  };
111
155
 
112
156
  const handleOptionSelect = (option: SelectOption) => {
113
157
  if (!option.disabled) {
114
- onValueChange(option.value);
158
+ onValueChange?.(option.value);
115
159
  setIsOpen(false);
116
160
  setSearchTerm('');
117
161
  triggerRef.current?.focus();
@@ -151,6 +195,8 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(({
151
195
  <button
152
196
  {...triggerWebProps}
153
197
  onClick={handleTriggerClick}
198
+ onFocus={handleTriggerFocus}
199
+ onKeyDown={handleKeyDown}
154
200
  disabled={disabled}
155
201
  aria-label={accessibilityLabel || label}
156
202
  aria-expanded={isOpen}
@@ -196,7 +242,6 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(({
196
242
  {...getWebProps([selectStyles.dropdown])}
197
243
  style={{
198
244
  maxHeight: maxHeight,
199
- // Override positioning since PositionedPortal handles it
200
245
  position: 'relative',
201
246
  top: 'auto',
202
247
  left: 'auto',
@@ -219,16 +264,20 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(({
219
264
 
220
265
  <div {...getWebProps([selectStyles.optionsList])}>
221
266
  {filteredOptions.map((option, index) => {
222
- const isSelected = option.value === value;
267
+ const isFocused = index === focusedIndex;
223
268
 
224
269
  return (
225
270
  <div
226
271
  key={option.value}
227
272
  onClick={() => handleOptionSelect(option)}
228
273
  role="option"
229
- aria-selected={isSelected}
274
+ aria-selected={option.value === value}
230
275
  onMouseEnter={() => setFocusedIndex(index)}
231
- {...getWebProps([selectStyles.option])}
276
+ {...getWebProps([
277
+ selectStyles.option,
278
+ isFocused && selectStyles.optionFocused,
279
+ option.disabled && selectStyles.optionDisabled,
280
+ ])}
232
281
  >
233
282
  <div {...getWebProps([selectStyles.optionContent])}>
234
283
  {option.icon && (
@@ -273,4 +322,4 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(({
273
322
 
274
323
  Select.displayName = 'Select';
275
324
 
276
- export default Select;
325
+ export default Select;
@@ -134,4 +134,7 @@ export type { SkeletonProps, SkeletonGroupProps, SkeletonShape, SkeletonAnimatio
134
134
  export type { ChipProps, ChipSize, ChipIntent } from './Chip/types';
135
135
  export type { BreadcrumbProps, BreadcrumbItem } from './Breadcrumb/types';
136
136
 
137
+ // Event utilities
138
+ export * from './utils/events';
139
+
137
140
  export type { AppTheme } from '@idealyst/theme';
package/src/index.ts CHANGED
@@ -142,4 +142,7 @@ export type { BreadcrumbProps, BreadcrumbItem } from './Breadcrumb/types';
142
142
 
143
143
  export { useMergeRefs };
144
144
 
145
+ // Event utilities
146
+ export * from './utils/events';
147
+
145
148
  export type { AppTheme } from '@idealyst/theme';
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Native-specific event wrappers
3
+ *
4
+ * Converts React Native events to standardized cross-platform events.
5
+ * Note: preventDefault() and stopPropagation() are no-ops on native since
6
+ * React Native's event system doesn't have these concepts in the same way.
7
+ */
8
+
9
+ import type {
10
+ GestureResponderEvent,
11
+ NativeSyntheticEvent,
12
+ NativeScrollEvent,
13
+ TextInputFocusEventData,
14
+ TextInputChangeEventData,
15
+ } from 'react-native';
16
+
17
+ import type {
18
+ PressEvent,
19
+ FocusEvent,
20
+ ChangeEvent,
21
+ TextChangeEvent,
22
+ ToggleEvent,
23
+ KeyboardEvent,
24
+ ScrollEvent,
25
+ SubmitEvent,
26
+ } from './types';
27
+
28
+ // No-op functions for native (these concepts don't apply)
29
+ const noop = () => {};
30
+
31
+ /**
32
+ * Wraps a React Native GestureResponderEvent into a standardized PressEvent
33
+ */
34
+ export function createPressEvent(
35
+ event: GestureResponderEvent,
36
+ type: PressEvent['type'] = 'press'
37
+ ): PressEvent {
38
+ return {
39
+ nativeEvent: event.nativeEvent,
40
+ timestamp: event.nativeEvent.timestamp,
41
+ defaultPrevented: false,
42
+ propagationStopped: false,
43
+ type,
44
+ preventDefault: noop,
45
+ stopPropagation: noop,
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Wraps a React Native focus event into a standardized FocusEvent
51
+ */
52
+ export function createFocusEvent(
53
+ event: NativeSyntheticEvent<TextInputFocusEventData>,
54
+ type: FocusEvent['type']
55
+ ): FocusEvent {
56
+ return {
57
+ nativeEvent: event.nativeEvent,
58
+ timestamp: Date.now(),
59
+ defaultPrevented: false,
60
+ propagationStopped: false,
61
+ type,
62
+ preventDefault: noop,
63
+ stopPropagation: noop,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Creates a standardized FocusEvent without a native event (for simple focus tracking)
69
+ */
70
+ export function createSimpleFocusEvent(type: FocusEvent['type']): FocusEvent {
71
+ return {
72
+ nativeEvent: null,
73
+ timestamp: Date.now(),
74
+ defaultPrevented: false,
75
+ propagationStopped: false,
76
+ type,
77
+ preventDefault: noop,
78
+ stopPropagation: noop,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Wraps a value change into a standardized ChangeEvent
84
+ */
85
+ export function createChangeEvent<V = string>(value: V): ChangeEvent<V> {
86
+ return {
87
+ nativeEvent: null,
88
+ timestamp: Date.now(),
89
+ defaultPrevented: false,
90
+ propagationStopped: false,
91
+ type: 'change',
92
+ value,
93
+ preventDefault: noop,
94
+ stopPropagation: noop,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Wraps a React Native text change event into a standardized TextChangeEvent
100
+ */
101
+ export function createTextChangeEvent(
102
+ event: NativeSyntheticEvent<TextInputChangeEventData>
103
+ ): TextChangeEvent {
104
+ const text = event.nativeEvent.text;
105
+ return {
106
+ nativeEvent: event.nativeEvent,
107
+ timestamp: Date.now(),
108
+ defaultPrevented: false,
109
+ propagationStopped: false,
110
+ type: 'change',
111
+ value: text,
112
+ text,
113
+ preventDefault: noop,
114
+ stopPropagation: noop,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Creates a TextChangeEvent from a simple text value (for onChangeText)
120
+ */
121
+ export function createSimpleTextChangeEvent(text: string): TextChangeEvent {
122
+ return {
123
+ nativeEvent: null,
124
+ timestamp: Date.now(),
125
+ defaultPrevented: false,
126
+ propagationStopped: false,
127
+ type: 'change',
128
+ value: text,
129
+ text,
130
+ preventDefault: noop,
131
+ stopPropagation: noop,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Creates a standardized ToggleEvent for switch/checkbox changes
137
+ */
138
+ export function createToggleEvent(checked: boolean): ToggleEvent {
139
+ return {
140
+ nativeEvent: null,
141
+ timestamp: Date.now(),
142
+ defaultPrevented: false,
143
+ propagationStopped: false,
144
+ type: 'change',
145
+ value: checked,
146
+ checked,
147
+ preventDefault: noop,
148
+ stopPropagation: noop,
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Creates a standardized KeyboardEvent
154
+ * Note: React Native's keyboard events are more limited than web
155
+ */
156
+ export function createKeyboardEvent(
157
+ key: string,
158
+ type: KeyboardEvent['type']
159
+ ): KeyboardEvent {
160
+ return {
161
+ nativeEvent: null,
162
+ timestamp: Date.now(),
163
+ defaultPrevented: false,
164
+ propagationStopped: false,
165
+ type,
166
+ key,
167
+ keyCode: 0,
168
+ altKey: false,
169
+ ctrlKey: false,
170
+ metaKey: false,
171
+ shiftKey: false,
172
+ preventDefault: noop,
173
+ stopPropagation: noop,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Wraps a React Native scroll event into a standardized ScrollEvent
179
+ */
180
+ export function createScrollEvent(
181
+ event: NativeSyntheticEvent<NativeScrollEvent>,
182
+ type: ScrollEvent['type'] = 'scroll'
183
+ ): ScrollEvent {
184
+ const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
185
+ return {
186
+ nativeEvent: event.nativeEvent,
187
+ timestamp: Date.now(),
188
+ defaultPrevented: false,
189
+ propagationStopped: false,
190
+ type,
191
+ contentOffset,
192
+ contentSize,
193
+ layoutMeasurement,
194
+ preventDefault: noop,
195
+ stopPropagation: noop,
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Creates a standardized SubmitEvent
201
+ */
202
+ export function createSubmitEvent(): SubmitEvent {
203
+ return {
204
+ nativeEvent: null,
205
+ timestamp: Date.now(),
206
+ defaultPrevented: false,
207
+ propagationStopped: false,
208
+ type: 'submit',
209
+ preventDefault: noop,
210
+ stopPropagation: noop,
211
+ };
212
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Web-specific event wrappers
3
+ *
4
+ * Converts React web events to standardized cross-platform events
5
+ */
6
+
7
+ import type {
8
+ PressEvent,
9
+ FocusEvent,
10
+ ChangeEvent,
11
+ TextChangeEvent,
12
+ ToggleEvent,
13
+ KeyboardEvent,
14
+ ScrollEvent,
15
+ SubmitEvent,
16
+ } from './types';
17
+
18
+ type ReactMouseEvent = React.MouseEvent<HTMLElement>;
19
+ type ReactFocusEvent = React.FocusEvent<HTMLElement>;
20
+ type ReactChangeEvent = React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>;
21
+ type ReactKeyboardEvent = React.KeyboardEvent<HTMLElement>;
22
+ type ReactFormEvent = React.FormEvent<HTMLFormElement>;
23
+ type ReactUIEvent = React.UIEvent<HTMLElement>;
24
+
25
+ /**
26
+ * Wraps a React mouse/click event into a standardized PressEvent
27
+ */
28
+ export function createPressEvent(
29
+ event: ReactMouseEvent,
30
+ type: PressEvent['type'] = 'press'
31
+ ): PressEvent {
32
+ return {
33
+ nativeEvent: event.nativeEvent,
34
+ timestamp: event.timeStamp,
35
+ defaultPrevented: event.defaultPrevented,
36
+ propagationStopped: false,
37
+ type,
38
+ preventDefault: () => event.preventDefault(),
39
+ stopPropagation: () => event.stopPropagation(),
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Wraps a React focus event into a standardized FocusEvent
45
+ */
46
+ export function createFocusEvent(
47
+ event: ReactFocusEvent,
48
+ type: FocusEvent['type']
49
+ ): FocusEvent {
50
+ return {
51
+ nativeEvent: event.nativeEvent,
52
+ timestamp: event.timeStamp,
53
+ defaultPrevented: event.defaultPrevented,
54
+ propagationStopped: false,
55
+ type,
56
+ preventDefault: () => event.preventDefault(),
57
+ stopPropagation: () => event.stopPropagation(),
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Wraps a React change event into a standardized ChangeEvent
63
+ */
64
+ export function createChangeEvent<V = string>(
65
+ event: ReactChangeEvent,
66
+ value: V
67
+ ): ChangeEvent<V> {
68
+ return {
69
+ nativeEvent: event.nativeEvent,
70
+ timestamp: event.timeStamp,
71
+ defaultPrevented: event.defaultPrevented,
72
+ propagationStopped: false,
73
+ type: 'change',
74
+ value,
75
+ preventDefault: () => event.preventDefault(),
76
+ stopPropagation: () => event.stopPropagation(),
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Wraps a React change event for text inputs into a standardized TextChangeEvent
82
+ */
83
+ export function createTextChangeEvent(event: ReactChangeEvent): TextChangeEvent {
84
+ const value = event.target.value;
85
+ return {
86
+ nativeEvent: event.nativeEvent,
87
+ timestamp: event.timeStamp,
88
+ defaultPrevented: event.defaultPrevented,
89
+ propagationStopped: false,
90
+ type: 'change',
91
+ value,
92
+ text: value,
93
+ preventDefault: () => event.preventDefault(),
94
+ stopPropagation: () => event.stopPropagation(),
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Wraps a React change event for checkboxes/switches into a standardized ToggleEvent
100
+ */
101
+ export function createToggleEvent(event: ReactChangeEvent): ToggleEvent {
102
+ const checked = (event.target as HTMLInputElement).checked;
103
+ return {
104
+ nativeEvent: event.nativeEvent,
105
+ timestamp: event.timeStamp,
106
+ defaultPrevented: event.defaultPrevented,
107
+ propagationStopped: false,
108
+ type: 'change',
109
+ value: checked,
110
+ checked,
111
+ preventDefault: () => event.preventDefault(),
112
+ stopPropagation: () => event.stopPropagation(),
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Wraps a React keyboard event into a standardized KeyboardEvent
118
+ */
119
+ export function createKeyboardEvent(
120
+ event: ReactKeyboardEvent,
121
+ type: KeyboardEvent['type']
122
+ ): KeyboardEvent {
123
+ return {
124
+ nativeEvent: event.nativeEvent,
125
+ timestamp: event.timeStamp,
126
+ defaultPrevented: event.defaultPrevented,
127
+ propagationStopped: false,
128
+ type,
129
+ key: event.key,
130
+ keyCode: event.keyCode,
131
+ altKey: event.altKey,
132
+ ctrlKey: event.ctrlKey,
133
+ metaKey: event.metaKey,
134
+ shiftKey: event.shiftKey,
135
+ preventDefault: () => event.preventDefault(),
136
+ stopPropagation: () => event.stopPropagation(),
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Wraps a React scroll event into a standardized ScrollEvent
142
+ */
143
+ export function createScrollEvent(
144
+ event: ReactUIEvent,
145
+ type: ScrollEvent['type'] = 'scroll'
146
+ ): ScrollEvent {
147
+ const target = event.target as HTMLElement;
148
+ return {
149
+ nativeEvent: event.nativeEvent,
150
+ timestamp: event.timeStamp,
151
+ defaultPrevented: event.defaultPrevented,
152
+ propagationStopped: false,
153
+ type,
154
+ contentOffset: {
155
+ x: target.scrollLeft,
156
+ y: target.scrollTop,
157
+ },
158
+ contentSize: {
159
+ width: target.scrollWidth,
160
+ height: target.scrollHeight,
161
+ },
162
+ layoutMeasurement: {
163
+ width: target.clientWidth,
164
+ height: target.clientHeight,
165
+ },
166
+ preventDefault: () => event.preventDefault(),
167
+ stopPropagation: () => event.stopPropagation(),
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Wraps a React form submit event into a standardized SubmitEvent
173
+ */
174
+ export function createSubmitEvent(event: ReactFormEvent): SubmitEvent {
175
+ return {
176
+ nativeEvent: event.nativeEvent,
177
+ timestamp: event.timeStamp,
178
+ defaultPrevented: event.defaultPrevented,
179
+ propagationStopped: false,
180
+ type: 'submit',
181
+ preventDefault: () => event.preventDefault(),
182
+ stopPropagation: () => event.stopPropagation(),
183
+ };
184
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Cross-platform event utilities
3
+ *
4
+ * This module provides standardized event types and creation functions
5
+ * that work consistently across web and native platforms.
6
+ */
7
+
8
+ // Re-export all types
9
+ export type {
10
+ SyntheticEvent,
11
+ PressEvent,
12
+ FocusEvent,
13
+ ChangeEvent,
14
+ TextChangeEvent,
15
+ SelectChangeEvent,
16
+ ToggleEvent,
17
+ KeyboardEvent,
18
+ ScrollEvent,
19
+ SubmitEvent,
20
+ PressEventHandler,
21
+ FocusEventHandler,
22
+ ChangeEventHandler,
23
+ TextChangeEventHandler,
24
+ SelectChangeEventHandler,
25
+ ToggleEventHandler,
26
+ KeyboardEventHandler,
27
+ ScrollEventHandler,
28
+ SubmitEventHandler,
29
+ } from './types';
30
+
31
+ // Re-export base utility
32
+ export { createBaseSyntheticEvent } from './types';
33
+
34
+ // Re-export platform-specific functions (native by default)
35
+ export {
36
+ createPressEvent,
37
+ createFocusEvent,
38
+ createSimpleFocusEvent,
39
+ createChangeEvent,
40
+ createTextChangeEvent,
41
+ createSimpleTextChangeEvent,
42
+ createToggleEvent,
43
+ createKeyboardEvent,
44
+ createScrollEvent,
45
+ createSubmitEvent,
46
+ } from './events.native';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Cross-platform event utilities (Web)
3
+ *
4
+ * This module provides standardized event types and creation functions
5
+ * that work consistently across web and native platforms.
6
+ */
7
+
8
+ // Re-export all types
9
+ export type {
10
+ SyntheticEvent,
11
+ PressEvent,
12
+ FocusEvent,
13
+ ChangeEvent,
14
+ TextChangeEvent,
15
+ SelectChangeEvent,
16
+ ToggleEvent,
17
+ KeyboardEvent,
18
+ ScrollEvent,
19
+ SubmitEvent,
20
+ PressEventHandler,
21
+ FocusEventHandler,
22
+ ChangeEventHandler,
23
+ TextChangeEventHandler,
24
+ SelectChangeEventHandler,
25
+ ToggleEventHandler,
26
+ KeyboardEventHandler,
27
+ ScrollEventHandler,
28
+ SubmitEventHandler,
29
+ } from './types';
30
+
31
+ // Re-export base utility
32
+ export { createBaseSyntheticEvent } from './types';
33
+
34
+ // Re-export platform-specific functions (web)
35
+ export {
36
+ createPressEvent,
37
+ createFocusEvent,
38
+ createChangeEvent,
39
+ createTextChangeEvent,
40
+ createToggleEvent,
41
+ createKeyboardEvent,
42
+ createScrollEvent,
43
+ createSubmitEvent,
44
+ } from './events.web';
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Standardized cross-platform event system
3
+ *
4
+ * Provides unified event interfaces that work consistently across web and native.
5
+ * On native, methods like preventDefault() and stopPropagation() are no-ops since
6
+ * React Native's event system doesn't have these concepts in the same way.
7
+ */
8
+
9
+ /**
10
+ * Base synthetic event interface that normalizes web and native events
11
+ */
12
+ export interface SyntheticEvent<T = unknown> {
13
+ /**
14
+ * The native event (platform-specific)
15
+ * - Web: React.SyntheticEvent
16
+ * - Native: GestureResponderEvent or NativeSyntheticEvent
17
+ */
18
+ nativeEvent: T;
19
+
20
+ /**
21
+ * Prevents the default behavior of the event.
22
+ * On native, this is a no-op as there's no default behavior concept.
23
+ */
24
+ preventDefault: () => void;
25
+
26
+ /**
27
+ * Stops the event from propagating to parent elements.
28
+ * On native, this is a no-op.
29
+ */
30
+ stopPropagation: () => void;
31
+
32
+ /**
33
+ * Whether preventDefault() has been called
34
+ */
35
+ defaultPrevented: boolean;
36
+
37
+ /**
38
+ * Whether stopPropagation() has been called
39
+ */
40
+ propagationStopped: boolean;
41
+
42
+ /**
43
+ * Timestamp of when the event occurred
44
+ */
45
+ timestamp: number;
46
+ }
47
+
48
+ /**
49
+ * Press/Click event - for buttons, pressables, touchable elements
50
+ */
51
+ export interface PressEvent extends SyntheticEvent {
52
+ /**
53
+ * The type of press event
54
+ */
55
+ type: 'press' | 'pressIn' | 'pressOut' | 'longPress';
56
+ }
57
+
58
+ /**
59
+ * Focus event - for inputs and focusable elements
60
+ */
61
+ export interface FocusEvent extends SyntheticEvent {
62
+ /**
63
+ * The type of focus event
64
+ */
65
+ type: 'focus' | 'blur';
66
+ }
67
+
68
+ /**
69
+ * Change event - for inputs, selects, checkboxes, etc.
70
+ */
71
+ export interface ChangeEvent<V = string> extends SyntheticEvent {
72
+ /**
73
+ * The new value after the change
74
+ */
75
+ value: V;
76
+
77
+ /**
78
+ * The type of change event
79
+ */
80
+ type: 'change';
81
+ }
82
+
83
+ /**
84
+ * Text change event - specifically for text inputs
85
+ */
86
+ export interface TextChangeEvent extends ChangeEvent<string> {
87
+ /**
88
+ * The text value
89
+ */
90
+ text: string;
91
+ }
92
+
93
+ /**
94
+ * Selection change event - for selects, dropdowns
95
+ */
96
+ export interface SelectChangeEvent<V = string> extends ChangeEvent<V> {
97
+ /**
98
+ * The selected value(s)
99
+ */
100
+ selected: V;
101
+ }
102
+
103
+ /**
104
+ * Toggle event - for switches, checkboxes
105
+ */
106
+ export interface ToggleEvent extends ChangeEvent<boolean> {
107
+ /**
108
+ * Whether the toggle is now checked/on
109
+ */
110
+ checked: boolean;
111
+ }
112
+
113
+ /**
114
+ * Keyboard event - for keyboard interactions
115
+ */
116
+ export interface KeyboardEvent extends SyntheticEvent {
117
+ /**
118
+ * The key that was pressed
119
+ */
120
+ key: string;
121
+
122
+ /**
123
+ * The key code
124
+ */
125
+ keyCode: number;
126
+
127
+ /**
128
+ * Whether the alt/option key was pressed
129
+ */
130
+ altKey: boolean;
131
+
132
+ /**
133
+ * Whether the control key was pressed
134
+ */
135
+ ctrlKey: boolean;
136
+
137
+ /**
138
+ * Whether the meta/command key was pressed
139
+ */
140
+ metaKey: boolean;
141
+
142
+ /**
143
+ * Whether the shift key was pressed
144
+ */
145
+ shiftKey: boolean;
146
+
147
+ /**
148
+ * The type of keyboard event
149
+ */
150
+ type: 'keyDown' | 'keyUp' | 'keyPress';
151
+ }
152
+
153
+ /**
154
+ * Scroll event - for scrollable containers
155
+ */
156
+ export interface ScrollEvent extends SyntheticEvent {
157
+ /**
158
+ * Current scroll position
159
+ */
160
+ contentOffset: {
161
+ x: number;
162
+ y: number;
163
+ };
164
+
165
+ /**
166
+ * Size of the scrollable content
167
+ */
168
+ contentSize: {
169
+ width: number;
170
+ height: number;
171
+ };
172
+
173
+ /**
174
+ * Size of the visible container
175
+ */
176
+ layoutMeasurement: {
177
+ width: number;
178
+ height: number;
179
+ };
180
+
181
+ /**
182
+ * The type of scroll event
183
+ */
184
+ type: 'scroll' | 'scrollBegin' | 'scrollEnd';
185
+ }
186
+
187
+ /**
188
+ * Submit event - for forms
189
+ */
190
+ export interface SubmitEvent extends SyntheticEvent {
191
+ /**
192
+ * The type of submit event
193
+ */
194
+ type: 'submit';
195
+ }
196
+
197
+ // Event handler type aliases for convenience
198
+ export type PressEventHandler = (event: PressEvent) => void;
199
+ export type FocusEventHandler = (event: FocusEvent) => void;
200
+ export type ChangeEventHandler<V = string> = (event: ChangeEvent<V>) => void;
201
+ export type TextChangeEventHandler = (event: TextChangeEvent) => void;
202
+ export type SelectChangeEventHandler<V = string> = (event: SelectChangeEvent<V>) => void;
203
+ export type ToggleEventHandler = (event: ToggleEvent) => void;
204
+ export type KeyboardEventHandler = (event: KeyboardEvent) => void;
205
+ export type ScrollEventHandler = (event: ScrollEvent) => void;
206
+ export type SubmitEventHandler = (event: SubmitEvent) => void;
207
+
208
+ /**
209
+ * Creates a base synthetic event object with default implementations
210
+ */
211
+ export function createBaseSyntheticEvent<T>(nativeEvent: T): SyntheticEvent<T> {
212
+ let defaultPrevented = false;
213
+ let propagationStopped = false;
214
+
215
+ return {
216
+ nativeEvent,
217
+ timestamp: Date.now(),
218
+ get defaultPrevented() {
219
+ return defaultPrevented;
220
+ },
221
+ get propagationStopped() {
222
+ return propagationStopped;
223
+ },
224
+ preventDefault() {
225
+ defaultPrevented = true;
226
+ },
227
+ stopPropagation() {
228
+ propagationStopped = true;
229
+ },
230
+ };
231
+ }