@idealyst/components 1.0.95 → 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.95",
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.95",
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.95",
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}
@@ -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,6 +59,15 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
58
59
  }
59
60
  };
60
61
 
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
+
61
71
  const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
62
72
  e.preventDefault();
63
73
  e.stopPropagation();
@@ -99,6 +109,12 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
99
109
  // Get input props
100
110
  const inputWebProps = getWebProps([inputStyles.input]);
101
111
 
112
+ const handleContainerPress = (e: React.MouseEvent<HTMLDivElement>) => {
113
+ e.preventDefault();
114
+ e.stopPropagation();
115
+ inputWebProps.ref?.current?.focus();
116
+ }
117
+
102
118
  // Merge the forwarded ref with unistyles ref for the input
103
119
  const mergedInputRef = useMergeRefs(ref, inputWebProps.ref);
104
120
 
@@ -156,7 +172,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
156
172
  };
157
173
 
158
174
  return (
159
- <div {...containerProps} data-testid={testID}>
175
+ <div onClick={handleContainerPress} {...containerProps} data-testid={testID}>
160
176
  {/* Left Icon */}
161
177
  {leftIcon && (
162
178
  <span {...leftIconContainerProps}>
@@ -170,6 +186,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({
170
186
  ref={mergedInputRef}
171
187
  type={getInputType()}
172
188
  value={value}
189
+ onClick={handlePress}
173
190
  onChange={handleChange}
174
191
  onFocus={handleFocus}
175
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
  */
@@ -138,7 +138,6 @@ export const selectStyles = StyleSheet.create((theme: Theme) => {
138
138
  return {
139
139
  container: {
140
140
  position: 'relative',
141
- backgroundColor: theme.colors.surface.primary,
142
141
  },
143
142
  label: {
144
143
  fontSize: 14,
@@ -257,35 +256,28 @@ export const selectStyles = StyleSheet.create((theme: Theme) => {
257
256
  flexDirection: 'row',
258
257
  alignItems: 'center',
259
258
  minHeight: 36,
260
- variants: {
261
- selected: {
262
- true: {
263
- backgroundColor: theme.intents.primary.light,
264
- },
265
- false: {},
259
+ _web: {
260
+ display: 'flex',
261
+ cursor: 'pointer',
262
+ _hover: {
263
+ backgroundColor: theme.colors.surface.secondary,
266
264
  },
267
- disabled: {
268
- true: {
269
- opacity: 0.5,
270
- _web: {
271
- cursor: 'not-allowed',
272
- },
273
- },
274
- false: {
275
- _web: {
276
- cursor: 'pointer',
277
- _hover: {
278
- backgroundColor: theme.colors.surface.secondary,
279
- },
280
- _active: {
281
- opacity: 0.8,
282
- },
283
- },
284
- },
265
+ _active: {
266
+ opacity: 0.8,
285
267
  },
286
268
  },
269
+ },
270
+ optionFocused: {
271
+ backgroundColor: theme.interaction.focusedBackground,
287
272
  _web: {
288
- 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',
289
281
  },
290
282
  },
291
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,44 +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);
107
- setSearchTerm('');
108
- setFocusedIndex(-1);
109
- }
142
+ const handleTriggerClick = (e: React.MouseEvent<HTMLButtonElement>) => {
143
+ e.preventDefault();
144
+ e.stopPropagation();
110
145
  };
111
146
 
112
147
  const handleTriggerFocus = () => {
113
148
  if (!disabled && !isOpen) {
114
149
  setIsOpen(true);
115
150
  setSearchTerm('');
116
- setFocusedIndex(-1);
151
+ // Focus on selected option, or first option if none selected
152
+ setFocusedIndex(getSelectedIndex());
117
153
  }
118
154
  };
119
155
 
120
156
  const handleOptionSelect = (option: SelectOption) => {
121
157
  if (!option.disabled) {
122
- onValueChange(option.value);
158
+ onValueChange?.(option.value);
123
159
  setIsOpen(false);
124
160
  setSearchTerm('');
125
161
  triggerRef.current?.focus();
@@ -160,6 +196,7 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(({
160
196
  {...triggerWebProps}
161
197
  onClick={handleTriggerClick}
162
198
  onFocus={handleTriggerFocus}
199
+ onKeyDown={handleKeyDown}
163
200
  disabled={disabled}
164
201
  aria-label={accessibilityLabel || label}
165
202
  aria-expanded={isOpen}
@@ -205,7 +242,6 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(({
205
242
  {...getWebProps([selectStyles.dropdown])}
206
243
  style={{
207
244
  maxHeight: maxHeight,
208
- // Override positioning since PositionedPortal handles it
209
245
  position: 'relative',
210
246
  top: 'auto',
211
247
  left: 'auto',
@@ -228,16 +264,20 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(({
228
264
 
229
265
  <div {...getWebProps([selectStyles.optionsList])}>
230
266
  {filteredOptions.map((option, index) => {
231
- const isSelected = option.value === value;
267
+ const isFocused = index === focusedIndex;
232
268
 
233
269
  return (
234
270
  <div
235
271
  key={option.value}
236
272
  onClick={() => handleOptionSelect(option)}
237
273
  role="option"
238
- aria-selected={isSelected}
274
+ aria-selected={option.value === value}
239
275
  onMouseEnter={() => setFocusedIndex(index)}
240
- {...getWebProps([selectStyles.option])}
276
+ {...getWebProps([
277
+ selectStyles.option,
278
+ isFocused && selectStyles.optionFocused,
279
+ option.disabled && selectStyles.optionDisabled,
280
+ ])}
241
281
  >
242
282
  <div {...getWebProps([selectStyles.optionContent])}>
243
283
  {option.icon && (
@@ -282,4 +322,4 @@ const Select = forwardRef<HTMLDivElement, SelectProps>(({
282
322
 
283
323
  Select.displayName = 'Select';
284
324
 
285
- 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
+ }