reactnatively-hooks 0.1.0
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/LICENSE +20 -0
- package/dist/index.d.mts +197 -0
- package/dist/index.d.ts +197 -0
- package/dist/index.js +355 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +322 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Hakizimana Fred
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Animated, View, LayoutChangeEvent } from 'react-native';
|
|
2
|
+
import { ScrollHandlerProcessed, SharedValue } from 'react-native-reanimated';
|
|
3
|
+
|
|
4
|
+
interface UseControllableOptions<T> {
|
|
5
|
+
/** The controlled value. When provided the hook operates in controlled mode. */
|
|
6
|
+
value?: T;
|
|
7
|
+
/** The default value for uncontrolled mode. */
|
|
8
|
+
defaultValue?: T;
|
|
9
|
+
/** Called when the value changes in uncontrolled mode. */
|
|
10
|
+
onChange?: (value: T) => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Bridges controlled and uncontrolled component patterns.
|
|
14
|
+
* Returns [resolvedValue, setValue] — handles both controlled and uncontrolled.
|
|
15
|
+
*/
|
|
16
|
+
declare function useControllable<T>({ value, defaultValue, onChange, }: UseControllableOptions<T>): [T | undefined, (next: T) => void];
|
|
17
|
+
|
|
18
|
+
interface UseDisclosureOptions {
|
|
19
|
+
/** Initial open state for uncontrolled mode. */
|
|
20
|
+
defaultIsOpen?: boolean;
|
|
21
|
+
/** Controlled open state. */
|
|
22
|
+
isOpen?: boolean;
|
|
23
|
+
/** Called when the open state changes in controlled mode. */
|
|
24
|
+
onChange?: (isOpen: boolean) => void;
|
|
25
|
+
}
|
|
26
|
+
interface UseDisclosureReturn {
|
|
27
|
+
isOpen: boolean;
|
|
28
|
+
onOpen: () => void;
|
|
29
|
+
onClose: () => void;
|
|
30
|
+
onToggle: () => void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Manages open/close/toggle state for modals, drawers, menus, etc.
|
|
34
|
+
* Supports both controlled (external isOpen + onChange) and uncontrolled modes.
|
|
35
|
+
*/
|
|
36
|
+
declare function useDisclosure(options?: UseDisclosureOptions): UseDisclosureReturn;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns a stable unique ID suitable for accessibility label pairing (aria-labelledby, etc.).
|
|
40
|
+
* Uses React.useId() in React 18+ and falls back to a module-level counter otherwise.
|
|
41
|
+
*/
|
|
42
|
+
declare function useId(prefix?: string): string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the value from the previous render.
|
|
46
|
+
* On the first render, returns `undefined`.
|
|
47
|
+
*/
|
|
48
|
+
declare function usePrevious<T>(value: T): T | undefined;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Debounces a value by the given delay (in milliseconds).
|
|
52
|
+
* The returned value will only update after the input value
|
|
53
|
+
* has stopped changing for `delay` ms.
|
|
54
|
+
*/
|
|
55
|
+
declare function useDebounce<T>(value: T, delay: number): T;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns a throttled version of the provided callback.
|
|
59
|
+
* The callback will fire at most once per `delay` ms.
|
|
60
|
+
* Subsequent calls within the delay window are ignored.
|
|
61
|
+
* The returned function is stable across renders.
|
|
62
|
+
*/
|
|
63
|
+
declare function useThrottle<T extends (...args: Parameters<T>) => ReturnType<T>>(fn: T, delay: number): T;
|
|
64
|
+
|
|
65
|
+
interface KeyboardState {
|
|
66
|
+
/** Whether the keyboard is currently visible. */
|
|
67
|
+
isVisible: boolean;
|
|
68
|
+
/** Current keyboard height in logical pixels. */
|
|
69
|
+
height: number;
|
|
70
|
+
/** Animated.Value tracking keyboard height — animates in sync with the keyboard. */
|
|
71
|
+
keyboardHeight: Animated.Value;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Tracks keyboard visibility and height.
|
|
75
|
+
* Uses React Native's built-in Keyboard API and Animated for smooth transitions.
|
|
76
|
+
* On web, keyboard events are not available — isVisible and height remain at defaults.
|
|
77
|
+
*/
|
|
78
|
+
declare function useKeyboard(): KeyboardState;
|
|
79
|
+
|
|
80
|
+
type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
81
|
+
interface DimensionsState {
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
scale: number;
|
|
85
|
+
fontScale: number;
|
|
86
|
+
/** Current breakpoint based on screen width. */
|
|
87
|
+
breakpoint: Breakpoint;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Returns live window dimensions and a responsive breakpoint derived from width.
|
|
91
|
+
* Breakpoints: xs (<480), sm (<768), md (<1024), lg (<1280), xl (>=1280).
|
|
92
|
+
* Re-renders automatically when the screen size changes (rotation, resize).
|
|
93
|
+
*/
|
|
94
|
+
declare function useDimensions(): DimensionsState;
|
|
95
|
+
|
|
96
|
+
interface AccessibilityState {
|
|
97
|
+
/** Whether the user has requested reduced motion. */
|
|
98
|
+
isReducedMotion: boolean;
|
|
99
|
+
/** Whether a screen reader (VoiceOver / TalkBack) is active. */
|
|
100
|
+
isScreenReader: boolean;
|
|
101
|
+
/** Whether bold text is enabled (iOS only; false on Android/Web). */
|
|
102
|
+
isBoldText: boolean;
|
|
103
|
+
/** Whether grayscale mode is enabled (iOS only; false on Android/Web). */
|
|
104
|
+
isGrayscale: boolean;
|
|
105
|
+
/** Whether colour inversion is enabled (iOS only; false on Android/Web). */
|
|
106
|
+
isInvertColors: boolean;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Subscribes to AccessibilityInfo and returns live accessibility flags.
|
|
110
|
+
* Handles graceful degradation: iOS-only flags always return false on Android/Web.
|
|
111
|
+
*/
|
|
112
|
+
declare function useAccessibility(): AccessibilityState;
|
|
113
|
+
|
|
114
|
+
type HapticImpactStyle = 'light' | 'medium' | 'heavy';
|
|
115
|
+
type HapticNotificationType = 'success' | 'warning' | 'error';
|
|
116
|
+
interface UseHapticReturn {
|
|
117
|
+
/** Trigger an impact haptic feedback. */
|
|
118
|
+
impact: (style?: HapticImpactStyle) => Promise<void>;
|
|
119
|
+
/** Trigger a notification haptic feedback. */
|
|
120
|
+
notification: (type?: HapticNotificationType) => Promise<void>;
|
|
121
|
+
/** Trigger a selection haptic feedback. */
|
|
122
|
+
selection: () => Promise<void>;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Haptic feedback wrapper.
|
|
126
|
+
* Uses expo-haptics when available; falls back to no-ops gracefully.
|
|
127
|
+
* Haptics are automatically skipped on web where they are unsupported.
|
|
128
|
+
*/
|
|
129
|
+
declare function useHaptic(): UseHapticReturn;
|
|
130
|
+
|
|
131
|
+
type ContainerBreakpoint = 'xs' | 'sm' | 'md' | 'lg';
|
|
132
|
+
/**
|
|
133
|
+
* Container-based responsive breakpoints derived from the component's own width,
|
|
134
|
+
* not the screen width. Uses `onLayout` to measure the container.
|
|
135
|
+
*
|
|
136
|
+
* Container breakpoints: xs (<320), sm (<480), md (<640), lg (>=640).
|
|
137
|
+
*
|
|
138
|
+
* @returns [ref, breakpoint] — attach `ref` to the View you want to measure.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* const [ref, bp] = useContainerQuery();
|
|
142
|
+
* return <View ref={ref} onLayout={...}>...</View>;
|
|
143
|
+
*/
|
|
144
|
+
declare function useContainerQuery(): [
|
|
145
|
+
ref: (node: View | null) => void,
|
|
146
|
+
breakpoint: ContainerBreakpoint
|
|
147
|
+
];
|
|
148
|
+
/**
|
|
149
|
+
* Extended return type that includes an `onLayout` handler for measurement.
|
|
150
|
+
* Prefer this form for explicit usage:
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* const { ref, breakpoint, onLayout } = useContainerQueryFull();
|
|
154
|
+
* return <View ref={ref} onLayout={onLayout}>...</View>;
|
|
155
|
+
*/
|
|
156
|
+
interface ContainerQueryResult {
|
|
157
|
+
ref: (node: View | null) => void;
|
|
158
|
+
breakpoint: ContainerBreakpoint;
|
|
159
|
+
onLayout: (event: LayoutChangeEvent) => void;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Object-API variant of `useContainerQuery` — easier to use when you need
|
|
163
|
+
* all three values without destructuring a tuple.
|
|
164
|
+
*/
|
|
165
|
+
declare function useContainerQueryFull(): ContainerQueryResult;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* useScrollHandler — scroll-driven animation values via Reanimated.
|
|
169
|
+
*
|
|
170
|
+
* This hook requires `react-native-reanimated` >= 3.6.0 as a peer dependency.
|
|
171
|
+
* It is guarded by a try/catch import so that the rest of the package remains
|
|
172
|
+
* importable when Reanimated is not installed (it will throw a clear error only
|
|
173
|
+
* when useScrollHandler itself is called).
|
|
174
|
+
*/
|
|
175
|
+
|
|
176
|
+
interface ScrollHandlerResult {
|
|
177
|
+
/** Pass this to the `onScroll` prop of Reanimated.ScrollView / FlatList. */
|
|
178
|
+
scrollHandler: ScrollHandlerProcessed<Record<string, unknown>>;
|
|
179
|
+
/** Shared value tracking current vertical scroll offset in pixels. */
|
|
180
|
+
scrollY: SharedValue<number>;
|
|
181
|
+
/** Shared value tracking scroll direction — updates when direction changes. */
|
|
182
|
+
scrollDirection: SharedValue<'up' | 'down'>;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Returns scroll-driven Reanimated shared values and a scroll handler.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* const { scrollHandler, scrollY, scrollDirection } = useScrollHandler();
|
|
189
|
+
* return (
|
|
190
|
+
* <Animated.ScrollView onScroll={scrollHandler} scrollEventThrottle={16}>
|
|
191
|
+
* ...
|
|
192
|
+
* </Animated.ScrollView>
|
|
193
|
+
* );
|
|
194
|
+
*/
|
|
195
|
+
declare function useScrollHandler(): ScrollHandlerResult;
|
|
196
|
+
|
|
197
|
+
export { type AccessibilityState, type Breakpoint, type ContainerBreakpoint, type ContainerQueryResult, type DimensionsState, type HapticImpactStyle, type HapticNotificationType, type KeyboardState, type ScrollHandlerResult, type UseControllableOptions, type UseDisclosureOptions, type UseDisclosureReturn, type UseHapticReturn, useAccessibility, useContainerQuery, useContainerQueryFull, useControllable, useDebounce, useDimensions, useDisclosure, useHaptic, useId, useKeyboard, usePrevious, useScrollHandler, useThrottle };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Animated, View, LayoutChangeEvent } from 'react-native';
|
|
2
|
+
import { ScrollHandlerProcessed, SharedValue } from 'react-native-reanimated';
|
|
3
|
+
|
|
4
|
+
interface UseControllableOptions<T> {
|
|
5
|
+
/** The controlled value. When provided the hook operates in controlled mode. */
|
|
6
|
+
value?: T;
|
|
7
|
+
/** The default value for uncontrolled mode. */
|
|
8
|
+
defaultValue?: T;
|
|
9
|
+
/** Called when the value changes in uncontrolled mode. */
|
|
10
|
+
onChange?: (value: T) => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Bridges controlled and uncontrolled component patterns.
|
|
14
|
+
* Returns [resolvedValue, setValue] — handles both controlled and uncontrolled.
|
|
15
|
+
*/
|
|
16
|
+
declare function useControllable<T>({ value, defaultValue, onChange, }: UseControllableOptions<T>): [T | undefined, (next: T) => void];
|
|
17
|
+
|
|
18
|
+
interface UseDisclosureOptions {
|
|
19
|
+
/** Initial open state for uncontrolled mode. */
|
|
20
|
+
defaultIsOpen?: boolean;
|
|
21
|
+
/** Controlled open state. */
|
|
22
|
+
isOpen?: boolean;
|
|
23
|
+
/** Called when the open state changes in controlled mode. */
|
|
24
|
+
onChange?: (isOpen: boolean) => void;
|
|
25
|
+
}
|
|
26
|
+
interface UseDisclosureReturn {
|
|
27
|
+
isOpen: boolean;
|
|
28
|
+
onOpen: () => void;
|
|
29
|
+
onClose: () => void;
|
|
30
|
+
onToggle: () => void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Manages open/close/toggle state for modals, drawers, menus, etc.
|
|
34
|
+
* Supports both controlled (external isOpen + onChange) and uncontrolled modes.
|
|
35
|
+
*/
|
|
36
|
+
declare function useDisclosure(options?: UseDisclosureOptions): UseDisclosureReturn;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns a stable unique ID suitable for accessibility label pairing (aria-labelledby, etc.).
|
|
40
|
+
* Uses React.useId() in React 18+ and falls back to a module-level counter otherwise.
|
|
41
|
+
*/
|
|
42
|
+
declare function useId(prefix?: string): string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the value from the previous render.
|
|
46
|
+
* On the first render, returns `undefined`.
|
|
47
|
+
*/
|
|
48
|
+
declare function usePrevious<T>(value: T): T | undefined;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Debounces a value by the given delay (in milliseconds).
|
|
52
|
+
* The returned value will only update after the input value
|
|
53
|
+
* has stopped changing for `delay` ms.
|
|
54
|
+
*/
|
|
55
|
+
declare function useDebounce<T>(value: T, delay: number): T;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns a throttled version of the provided callback.
|
|
59
|
+
* The callback will fire at most once per `delay` ms.
|
|
60
|
+
* Subsequent calls within the delay window are ignored.
|
|
61
|
+
* The returned function is stable across renders.
|
|
62
|
+
*/
|
|
63
|
+
declare function useThrottle<T extends (...args: Parameters<T>) => ReturnType<T>>(fn: T, delay: number): T;
|
|
64
|
+
|
|
65
|
+
interface KeyboardState {
|
|
66
|
+
/** Whether the keyboard is currently visible. */
|
|
67
|
+
isVisible: boolean;
|
|
68
|
+
/** Current keyboard height in logical pixels. */
|
|
69
|
+
height: number;
|
|
70
|
+
/** Animated.Value tracking keyboard height — animates in sync with the keyboard. */
|
|
71
|
+
keyboardHeight: Animated.Value;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Tracks keyboard visibility and height.
|
|
75
|
+
* Uses React Native's built-in Keyboard API and Animated for smooth transitions.
|
|
76
|
+
* On web, keyboard events are not available — isVisible and height remain at defaults.
|
|
77
|
+
*/
|
|
78
|
+
declare function useKeyboard(): KeyboardState;
|
|
79
|
+
|
|
80
|
+
type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
81
|
+
interface DimensionsState {
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
scale: number;
|
|
85
|
+
fontScale: number;
|
|
86
|
+
/** Current breakpoint based on screen width. */
|
|
87
|
+
breakpoint: Breakpoint;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Returns live window dimensions and a responsive breakpoint derived from width.
|
|
91
|
+
* Breakpoints: xs (<480), sm (<768), md (<1024), lg (<1280), xl (>=1280).
|
|
92
|
+
* Re-renders automatically when the screen size changes (rotation, resize).
|
|
93
|
+
*/
|
|
94
|
+
declare function useDimensions(): DimensionsState;
|
|
95
|
+
|
|
96
|
+
interface AccessibilityState {
|
|
97
|
+
/** Whether the user has requested reduced motion. */
|
|
98
|
+
isReducedMotion: boolean;
|
|
99
|
+
/** Whether a screen reader (VoiceOver / TalkBack) is active. */
|
|
100
|
+
isScreenReader: boolean;
|
|
101
|
+
/** Whether bold text is enabled (iOS only; false on Android/Web). */
|
|
102
|
+
isBoldText: boolean;
|
|
103
|
+
/** Whether grayscale mode is enabled (iOS only; false on Android/Web). */
|
|
104
|
+
isGrayscale: boolean;
|
|
105
|
+
/** Whether colour inversion is enabled (iOS only; false on Android/Web). */
|
|
106
|
+
isInvertColors: boolean;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Subscribes to AccessibilityInfo and returns live accessibility flags.
|
|
110
|
+
* Handles graceful degradation: iOS-only flags always return false on Android/Web.
|
|
111
|
+
*/
|
|
112
|
+
declare function useAccessibility(): AccessibilityState;
|
|
113
|
+
|
|
114
|
+
type HapticImpactStyle = 'light' | 'medium' | 'heavy';
|
|
115
|
+
type HapticNotificationType = 'success' | 'warning' | 'error';
|
|
116
|
+
interface UseHapticReturn {
|
|
117
|
+
/** Trigger an impact haptic feedback. */
|
|
118
|
+
impact: (style?: HapticImpactStyle) => Promise<void>;
|
|
119
|
+
/** Trigger a notification haptic feedback. */
|
|
120
|
+
notification: (type?: HapticNotificationType) => Promise<void>;
|
|
121
|
+
/** Trigger a selection haptic feedback. */
|
|
122
|
+
selection: () => Promise<void>;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Haptic feedback wrapper.
|
|
126
|
+
* Uses expo-haptics when available; falls back to no-ops gracefully.
|
|
127
|
+
* Haptics are automatically skipped on web where they are unsupported.
|
|
128
|
+
*/
|
|
129
|
+
declare function useHaptic(): UseHapticReturn;
|
|
130
|
+
|
|
131
|
+
type ContainerBreakpoint = 'xs' | 'sm' | 'md' | 'lg';
|
|
132
|
+
/**
|
|
133
|
+
* Container-based responsive breakpoints derived from the component's own width,
|
|
134
|
+
* not the screen width. Uses `onLayout` to measure the container.
|
|
135
|
+
*
|
|
136
|
+
* Container breakpoints: xs (<320), sm (<480), md (<640), lg (>=640).
|
|
137
|
+
*
|
|
138
|
+
* @returns [ref, breakpoint] — attach `ref` to the View you want to measure.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* const [ref, bp] = useContainerQuery();
|
|
142
|
+
* return <View ref={ref} onLayout={...}>...</View>;
|
|
143
|
+
*/
|
|
144
|
+
declare function useContainerQuery(): [
|
|
145
|
+
ref: (node: View | null) => void,
|
|
146
|
+
breakpoint: ContainerBreakpoint
|
|
147
|
+
];
|
|
148
|
+
/**
|
|
149
|
+
* Extended return type that includes an `onLayout` handler for measurement.
|
|
150
|
+
* Prefer this form for explicit usage:
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* const { ref, breakpoint, onLayout } = useContainerQueryFull();
|
|
154
|
+
* return <View ref={ref} onLayout={onLayout}>...</View>;
|
|
155
|
+
*/
|
|
156
|
+
interface ContainerQueryResult {
|
|
157
|
+
ref: (node: View | null) => void;
|
|
158
|
+
breakpoint: ContainerBreakpoint;
|
|
159
|
+
onLayout: (event: LayoutChangeEvent) => void;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Object-API variant of `useContainerQuery` — easier to use when you need
|
|
163
|
+
* all three values without destructuring a tuple.
|
|
164
|
+
*/
|
|
165
|
+
declare function useContainerQueryFull(): ContainerQueryResult;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* useScrollHandler — scroll-driven animation values via Reanimated.
|
|
169
|
+
*
|
|
170
|
+
* This hook requires `react-native-reanimated` >= 3.6.0 as a peer dependency.
|
|
171
|
+
* It is guarded by a try/catch import so that the rest of the package remains
|
|
172
|
+
* importable when Reanimated is not installed (it will throw a clear error only
|
|
173
|
+
* when useScrollHandler itself is called).
|
|
174
|
+
*/
|
|
175
|
+
|
|
176
|
+
interface ScrollHandlerResult {
|
|
177
|
+
/** Pass this to the `onScroll` prop of Reanimated.ScrollView / FlatList. */
|
|
178
|
+
scrollHandler: ScrollHandlerProcessed<Record<string, unknown>>;
|
|
179
|
+
/** Shared value tracking current vertical scroll offset in pixels. */
|
|
180
|
+
scrollY: SharedValue<number>;
|
|
181
|
+
/** Shared value tracking scroll direction — updates when direction changes. */
|
|
182
|
+
scrollDirection: SharedValue<'up' | 'down'>;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Returns scroll-driven Reanimated shared values and a scroll handler.
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* const { scrollHandler, scrollY, scrollDirection } = useScrollHandler();
|
|
189
|
+
* return (
|
|
190
|
+
* <Animated.ScrollView onScroll={scrollHandler} scrollEventThrottle={16}>
|
|
191
|
+
* ...
|
|
192
|
+
* </Animated.ScrollView>
|
|
193
|
+
* );
|
|
194
|
+
*/
|
|
195
|
+
declare function useScrollHandler(): ScrollHandlerResult;
|
|
196
|
+
|
|
197
|
+
export { type AccessibilityState, type Breakpoint, type ContainerBreakpoint, type ContainerQueryResult, type DimensionsState, type HapticImpactStyle, type HapticNotificationType, type KeyboardState, type ScrollHandlerResult, type UseControllableOptions, type UseDisclosureOptions, type UseDisclosureReturn, type UseHapticReturn, useAccessibility, useContainerQuery, useContainerQueryFull, useControllable, useDebounce, useDimensions, useDisclosure, useHaptic, useId, useKeyboard, usePrevious, useScrollHandler, useThrottle };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
var reactNative = require('react-native');
|
|
5
|
+
|
|
6
|
+
function _interopNamespace(e) {
|
|
7
|
+
if (e && e.__esModule) return e;
|
|
8
|
+
var n = Object.create(null);
|
|
9
|
+
if (e) {
|
|
10
|
+
Object.keys(e).forEach(function (k) {
|
|
11
|
+
if (k !== 'default') {
|
|
12
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
13
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
get: function () { return e[k]; }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
n.default = e;
|
|
21
|
+
return Object.freeze(n);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
25
|
+
|
|
26
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
27
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
28
|
+
}) : x)(function(x) {
|
|
29
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
30
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
31
|
+
});
|
|
32
|
+
function useControllable({
|
|
33
|
+
value,
|
|
34
|
+
defaultValue,
|
|
35
|
+
onChange
|
|
36
|
+
}) {
|
|
37
|
+
const isControlled = value !== void 0;
|
|
38
|
+
const [internalValue, setInternalValue] = React.useState(defaultValue);
|
|
39
|
+
const onChangeRef = React.useRef(onChange);
|
|
40
|
+
onChangeRef.current = onChange;
|
|
41
|
+
const setValue = React.useCallback(
|
|
42
|
+
(next) => {
|
|
43
|
+
if (!isControlled) {
|
|
44
|
+
setInternalValue(next);
|
|
45
|
+
}
|
|
46
|
+
onChangeRef.current?.(next);
|
|
47
|
+
},
|
|
48
|
+
[isControlled]
|
|
49
|
+
);
|
|
50
|
+
return [isControlled ? value : internalValue, setValue];
|
|
51
|
+
}
|
|
52
|
+
function useDisclosure(options = {}) {
|
|
53
|
+
const { defaultIsOpen = false, isOpen: controlledIsOpen, onChange } = options;
|
|
54
|
+
const [isOpen, setIsOpen] = useControllable({
|
|
55
|
+
value: controlledIsOpen,
|
|
56
|
+
defaultValue: defaultIsOpen,
|
|
57
|
+
onChange
|
|
58
|
+
});
|
|
59
|
+
const resolvedIsOpen = isOpen ?? false;
|
|
60
|
+
const onOpen = React.useCallback(() => {
|
|
61
|
+
setIsOpen(true);
|
|
62
|
+
}, [setIsOpen]);
|
|
63
|
+
const onClose = React.useCallback(() => {
|
|
64
|
+
setIsOpen(false);
|
|
65
|
+
}, [setIsOpen]);
|
|
66
|
+
const onToggle = React.useCallback(() => {
|
|
67
|
+
setIsOpen(!resolvedIsOpen);
|
|
68
|
+
}, [setIsOpen, resolvedIsOpen]);
|
|
69
|
+
return { isOpen: resolvedIsOpen, onOpen, onClose, onToggle };
|
|
70
|
+
}
|
|
71
|
+
var counter = 0;
|
|
72
|
+
function generateId(prefix) {
|
|
73
|
+
counter += 1;
|
|
74
|
+
return `${prefix}-${counter}`;
|
|
75
|
+
}
|
|
76
|
+
function useId2(prefix = "rn") {
|
|
77
|
+
if (typeof React__namespace.useId === "function") {
|
|
78
|
+
const id = React__namespace.useId();
|
|
79
|
+
return `${prefix}${id}`;
|
|
80
|
+
}
|
|
81
|
+
const idRef = React__namespace.useRef(null);
|
|
82
|
+
if (idRef.current === null) {
|
|
83
|
+
idRef.current = generateId(prefix);
|
|
84
|
+
}
|
|
85
|
+
return idRef.current;
|
|
86
|
+
}
|
|
87
|
+
function usePrevious(value) {
|
|
88
|
+
const ref = React.useRef(void 0);
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
ref.current = value;
|
|
91
|
+
});
|
|
92
|
+
return ref.current;
|
|
93
|
+
}
|
|
94
|
+
function useDebounce(value, delay) {
|
|
95
|
+
const [debouncedValue, setDebouncedValue] = React.useState(value);
|
|
96
|
+
React.useEffect(() => {
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
setDebouncedValue(value);
|
|
99
|
+
}, delay);
|
|
100
|
+
return () => {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
};
|
|
103
|
+
}, [value, delay]);
|
|
104
|
+
return debouncedValue;
|
|
105
|
+
}
|
|
106
|
+
function useThrottle(fn, delay) {
|
|
107
|
+
const lastCallRef = React.useRef(0);
|
|
108
|
+
const fnRef = React.useRef(fn);
|
|
109
|
+
fnRef.current = fn;
|
|
110
|
+
return React.useCallback(
|
|
111
|
+
(...args) => {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
if (now - lastCallRef.current >= delay) {
|
|
114
|
+
lastCallRef.current = now;
|
|
115
|
+
return fnRef.current(...args);
|
|
116
|
+
}
|
|
117
|
+
return void 0;
|
|
118
|
+
},
|
|
119
|
+
[delay]
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
function useKeyboard() {
|
|
123
|
+
const [isVisible, setIsVisible] = React.useState(false);
|
|
124
|
+
const [height, setHeight] = React.useState(0);
|
|
125
|
+
const keyboardHeight = React.useRef(new reactNative.Animated.Value(0)).current;
|
|
126
|
+
React.useEffect(() => {
|
|
127
|
+
if (reactNative.Platform.OS === "web") return;
|
|
128
|
+
function handleShow(event) {
|
|
129
|
+
const keyHeight = event.endCoordinates.height;
|
|
130
|
+
setIsVisible(true);
|
|
131
|
+
setHeight(keyHeight);
|
|
132
|
+
reactNative.Animated.timing(keyboardHeight, {
|
|
133
|
+
toValue: keyHeight,
|
|
134
|
+
duration: event.duration ?? 250,
|
|
135
|
+
useNativeDriver: false
|
|
136
|
+
}).start();
|
|
137
|
+
}
|
|
138
|
+
function handleHide(event) {
|
|
139
|
+
setIsVisible(false);
|
|
140
|
+
setHeight(0);
|
|
141
|
+
reactNative.Animated.timing(keyboardHeight, {
|
|
142
|
+
toValue: 0,
|
|
143
|
+
duration: event.duration ?? 200,
|
|
144
|
+
useNativeDriver: false
|
|
145
|
+
}).start();
|
|
146
|
+
}
|
|
147
|
+
const showEvent = reactNative.Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
|
|
148
|
+
const hideEvent = reactNative.Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide";
|
|
149
|
+
const showSub = reactNative.Keyboard.addListener(showEvent, handleShow);
|
|
150
|
+
const hideSub = reactNative.Keyboard.addListener(hideEvent, handleHide);
|
|
151
|
+
return () => {
|
|
152
|
+
showSub.remove();
|
|
153
|
+
hideSub.remove();
|
|
154
|
+
};
|
|
155
|
+
}, [keyboardHeight]);
|
|
156
|
+
return { isVisible, height, keyboardHeight };
|
|
157
|
+
}
|
|
158
|
+
function getBreakpoint(width) {
|
|
159
|
+
if (width < 480) return "xs";
|
|
160
|
+
if (width < 768) return "sm";
|
|
161
|
+
if (width < 1024) return "md";
|
|
162
|
+
if (width < 1280) return "lg";
|
|
163
|
+
return "xl";
|
|
164
|
+
}
|
|
165
|
+
function useDimensions() {
|
|
166
|
+
const { width, height, scale, fontScale } = reactNative.useWindowDimensions();
|
|
167
|
+
const breakpoint = React.useMemo(() => getBreakpoint(width), [width]);
|
|
168
|
+
return { width, height, scale, fontScale, breakpoint };
|
|
169
|
+
}
|
|
170
|
+
var defaults = {
|
|
171
|
+
isReducedMotion: false,
|
|
172
|
+
isScreenReader: false,
|
|
173
|
+
isBoldText: false,
|
|
174
|
+
isGrayscale: false,
|
|
175
|
+
isInvertColors: false
|
|
176
|
+
};
|
|
177
|
+
function useAccessibility() {
|
|
178
|
+
const [state, setState] = React.useState(defaults);
|
|
179
|
+
React.useEffect(() => {
|
|
180
|
+
let cancelled = false;
|
|
181
|
+
async function init() {
|
|
182
|
+
const [
|
|
183
|
+
isReducedMotion,
|
|
184
|
+
isScreenReader,
|
|
185
|
+
isBoldText,
|
|
186
|
+
isGrayscale,
|
|
187
|
+
isInvertColors
|
|
188
|
+
] = await Promise.all([
|
|
189
|
+
reactNative.AccessibilityInfo.isReduceMotionEnabled(),
|
|
190
|
+
reactNative.AccessibilityInfo.isScreenReaderEnabled(),
|
|
191
|
+
reactNative.Platform.OS === "ios" ? reactNative.AccessibilityInfo.isBoldTextEnabled() : Promise.resolve(false),
|
|
192
|
+
reactNative.Platform.OS === "ios" ? reactNative.AccessibilityInfo.isGrayscaleEnabled() : Promise.resolve(false),
|
|
193
|
+
reactNative.Platform.OS === "ios" ? reactNative.AccessibilityInfo.isInvertColorsEnabled() : Promise.resolve(false)
|
|
194
|
+
]);
|
|
195
|
+
if (!cancelled) {
|
|
196
|
+
setState({ isReducedMotion, isScreenReader, isBoldText, isGrayscale, isInvertColors });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
void init();
|
|
200
|
+
const reduceMotionSub = reactNative.AccessibilityInfo.addEventListener(
|
|
201
|
+
"reduceMotionChanged",
|
|
202
|
+
(isReducedMotion) => setState((prev) => ({ ...prev, isReducedMotion }))
|
|
203
|
+
);
|
|
204
|
+
const screenReaderSub = reactNative.AccessibilityInfo.addEventListener(
|
|
205
|
+
"screenReaderChanged",
|
|
206
|
+
(isScreenReader) => setState((prev) => ({ ...prev, isScreenReader }))
|
|
207
|
+
);
|
|
208
|
+
const boldTextSub = reactNative.Platform.OS === "ios" ? reactNative.AccessibilityInfo.addEventListener(
|
|
209
|
+
"boldTextChanged",
|
|
210
|
+
(isBoldText) => setState((prev) => ({ ...prev, isBoldText }))
|
|
211
|
+
) : null;
|
|
212
|
+
const grayscaleSub = reactNative.Platform.OS === "ios" ? reactNative.AccessibilityInfo.addEventListener(
|
|
213
|
+
"grayscaleChanged",
|
|
214
|
+
(isGrayscale) => setState((prev) => ({ ...prev, isGrayscale }))
|
|
215
|
+
) : null;
|
|
216
|
+
const invertColorsSub = reactNative.Platform.OS === "ios" ? reactNative.AccessibilityInfo.addEventListener(
|
|
217
|
+
"invertColorsChanged",
|
|
218
|
+
(isInvertColors) => setState((prev) => ({ ...prev, isInvertColors }))
|
|
219
|
+
) : null;
|
|
220
|
+
return () => {
|
|
221
|
+
cancelled = true;
|
|
222
|
+
reduceMotionSub.remove();
|
|
223
|
+
screenReaderSub.remove();
|
|
224
|
+
boldTextSub?.remove();
|
|
225
|
+
grayscaleSub?.remove();
|
|
226
|
+
invertColorsSub?.remove();
|
|
227
|
+
};
|
|
228
|
+
}, []);
|
|
229
|
+
return state;
|
|
230
|
+
}
|
|
231
|
+
var expoHaptics = void 0;
|
|
232
|
+
function loadExpoHaptics() {
|
|
233
|
+
if (expoHaptics !== void 0) return expoHaptics;
|
|
234
|
+
try {
|
|
235
|
+
const mod = __require("expo-haptics");
|
|
236
|
+
expoHaptics = mod;
|
|
237
|
+
return mod;
|
|
238
|
+
} catch {
|
|
239
|
+
expoHaptics = null;
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
var noop = () => Promise.resolve();
|
|
244
|
+
function useHaptic() {
|
|
245
|
+
const impact = React.useCallback(async (style = "medium") => {
|
|
246
|
+
if (reactNative.Platform.OS === "web") return;
|
|
247
|
+
const haptics = loadExpoHaptics();
|
|
248
|
+
if (!haptics) return;
|
|
249
|
+
const styleMap = {
|
|
250
|
+
light: haptics.ImpactFeedbackStyle.Light,
|
|
251
|
+
medium: haptics.ImpactFeedbackStyle.Medium,
|
|
252
|
+
heavy: haptics.ImpactFeedbackStyle.Heavy
|
|
253
|
+
};
|
|
254
|
+
await haptics.impactAsync(styleMap[style]);
|
|
255
|
+
}, []);
|
|
256
|
+
const notification = React.useCallback(
|
|
257
|
+
async (type = "success") => {
|
|
258
|
+
if (reactNative.Platform.OS === "web") return;
|
|
259
|
+
const haptics = loadExpoHaptics();
|
|
260
|
+
if (!haptics) return;
|
|
261
|
+
const typeMap = {
|
|
262
|
+
success: haptics.NotificationFeedbackType.Success,
|
|
263
|
+
warning: haptics.NotificationFeedbackType.Warning,
|
|
264
|
+
error: haptics.NotificationFeedbackType.Error
|
|
265
|
+
};
|
|
266
|
+
await haptics.notificationAsync(typeMap[type]);
|
|
267
|
+
},
|
|
268
|
+
[]
|
|
269
|
+
);
|
|
270
|
+
const selection = React.useCallback(async () => {
|
|
271
|
+
if (reactNative.Platform.OS === "web") return;
|
|
272
|
+
const haptics = loadExpoHaptics();
|
|
273
|
+
if (!haptics) return;
|
|
274
|
+
await haptics.selectionAsync();
|
|
275
|
+
}, []);
|
|
276
|
+
if (reactNative.Platform.OS === "web") {
|
|
277
|
+
return { impact: noop, notification: noop, selection: noop };
|
|
278
|
+
}
|
|
279
|
+
return { impact, notification, selection };
|
|
280
|
+
}
|
|
281
|
+
function getContainerBreakpoint(width) {
|
|
282
|
+
if (width < 320) return "xs";
|
|
283
|
+
if (width < 480) return "sm";
|
|
284
|
+
if (width < 640) return "md";
|
|
285
|
+
return "lg";
|
|
286
|
+
}
|
|
287
|
+
function useContainerQuery() {
|
|
288
|
+
const [breakpoint, setBreakpoint] = React.useState("xs");
|
|
289
|
+
const ref = React.useCallback((_node) => {
|
|
290
|
+
}, []);
|
|
291
|
+
const onLayout = React.useCallback((event) => {
|
|
292
|
+
const { width } = event.nativeEvent.layout;
|
|
293
|
+
setBreakpoint(getContainerBreakpoint(width));
|
|
294
|
+
}, []);
|
|
295
|
+
return Object.assign([ref, breakpoint], { onLayout });
|
|
296
|
+
}
|
|
297
|
+
function useContainerQueryFull() {
|
|
298
|
+
const [breakpoint, setBreakpoint] = React.useState("xs");
|
|
299
|
+
const ref = React.useCallback((_node) => {
|
|
300
|
+
}, []);
|
|
301
|
+
const onLayout = React.useCallback((event) => {
|
|
302
|
+
const { width } = event.nativeEvent.layout;
|
|
303
|
+
setBreakpoint(getContainerBreakpoint(width));
|
|
304
|
+
}, []);
|
|
305
|
+
return { ref, breakpoint, onLayout };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/useScrollHandler.ts
|
|
309
|
+
var reanimated = null;
|
|
310
|
+
function loadReanimated() {
|
|
311
|
+
if (reanimated) return reanimated;
|
|
312
|
+
try {
|
|
313
|
+
reanimated = __require("react-native-reanimated");
|
|
314
|
+
return reanimated;
|
|
315
|
+
} catch {
|
|
316
|
+
throw new Error(
|
|
317
|
+
"[reactnatively-hooks] useScrollHandler requires react-native-reanimated >= 3.6.0. Install it with: npx expo install react-native-reanimated"
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function useScrollHandler() {
|
|
322
|
+
const rn = loadReanimated();
|
|
323
|
+
const { useSharedValue, useAnimatedScrollHandler } = rn;
|
|
324
|
+
const scrollY = useSharedValue(0);
|
|
325
|
+
const scrollDirection = useSharedValue("down");
|
|
326
|
+
const scrollHandler = useAnimatedScrollHandler({
|
|
327
|
+
onScroll(event) {
|
|
328
|
+
"worklet";
|
|
329
|
+
const y = event.contentOffset.y;
|
|
330
|
+
if (y > scrollY.value) {
|
|
331
|
+
scrollDirection.value = "down";
|
|
332
|
+
} else if (y < scrollY.value) {
|
|
333
|
+
scrollDirection.value = "up";
|
|
334
|
+
}
|
|
335
|
+
scrollY.value = y;
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
return { scrollHandler, scrollY, scrollDirection };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
exports.useAccessibility = useAccessibility;
|
|
342
|
+
exports.useContainerQuery = useContainerQuery;
|
|
343
|
+
exports.useContainerQueryFull = useContainerQueryFull;
|
|
344
|
+
exports.useControllable = useControllable;
|
|
345
|
+
exports.useDebounce = useDebounce;
|
|
346
|
+
exports.useDimensions = useDimensions;
|
|
347
|
+
exports.useDisclosure = useDisclosure;
|
|
348
|
+
exports.useHaptic = useHaptic;
|
|
349
|
+
exports.useId = useId2;
|
|
350
|
+
exports.useKeyboard = useKeyboard;
|
|
351
|
+
exports.usePrevious = usePrevious;
|
|
352
|
+
exports.useScrollHandler = useScrollHandler;
|
|
353
|
+
exports.useThrottle = useThrottle;
|
|
354
|
+
//# sourceMappingURL=index.js.map
|
|
355
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useControllable.ts","../src/useDisclosure.ts","../src/useId.ts","../src/usePrevious.ts","../src/useDebounce.ts","../src/useThrottle.ts","../src/useKeyboard.ts","../src/useDimensions.ts","../src/useAccessibility.ts","../src/useHaptic.ts","../src/useContainerQuery.ts","../src/useScrollHandler.ts"],"names":["useState","useRef","useCallback","useId","React","useEffect","Animated","Platform","Keyboard","useWindowDimensions","useMemo","AccessibilityInfo"],"mappings":"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","file":"index.js","sourcesContent":["import { useState, useCallback, useRef } from 'react';\n\nexport interface UseControllableOptions<T> {\n /** The controlled value. When provided the hook operates in controlled mode. */\n value?: T;\n /** The default value for uncontrolled mode. */\n defaultValue?: T;\n /** Called when the value changes in uncontrolled mode. */\n onChange?: (value: T) => void;\n}\n\n/**\n * Bridges controlled and uncontrolled component patterns.\n * Returns [resolvedValue, setValue] — handles both controlled and uncontrolled.\n */\nexport function useControllable<T>({\n value,\n defaultValue,\n onChange,\n}: UseControllableOptions<T>): [T | undefined, (next: T) => void] {\n const isControlled = value !== undefined;\n const [internalValue, setInternalValue] = useState<T | undefined>(defaultValue);\n\n // Keep a stable ref to onChange to avoid re-creating setValue\n const onChangeRef = useRef(onChange);\n onChangeRef.current = onChange;\n\n const setValue = useCallback(\n (next: T) => {\n if (!isControlled) {\n setInternalValue(next);\n }\n onChangeRef.current?.(next);\n },\n [isControlled],\n );\n\n return [isControlled ? value : internalValue, setValue];\n}\n","import { useCallback } from 'react';\nimport { useControllable } from './useControllable';\n\nexport interface UseDisclosureOptions {\n /** Initial open state for uncontrolled mode. */\n defaultIsOpen?: boolean;\n /** Controlled open state. */\n isOpen?: boolean;\n /** Called when the open state changes in controlled mode. */\n onChange?: (isOpen: boolean) => void;\n}\n\nexport interface UseDisclosureReturn {\n isOpen: boolean;\n onOpen: () => void;\n onClose: () => void;\n onToggle: () => void;\n}\n\n/**\n * Manages open/close/toggle state for modals, drawers, menus, etc.\n * Supports both controlled (external isOpen + onChange) and uncontrolled modes.\n */\nexport function useDisclosure(options: UseDisclosureOptions = {}): UseDisclosureReturn {\n const { defaultIsOpen = false, isOpen: controlledIsOpen, onChange } = options;\n\n const [isOpen, setIsOpen] = useControllable<boolean>({\n value: controlledIsOpen,\n defaultValue: defaultIsOpen,\n onChange,\n });\n\n const resolvedIsOpen = isOpen ?? false;\n\n const onOpen = useCallback(() => {\n setIsOpen(true);\n }, [setIsOpen]);\n\n const onClose = useCallback(() => {\n setIsOpen(false);\n }, [setIsOpen]);\n\n const onToggle = useCallback(() => {\n setIsOpen(!resolvedIsOpen);\n }, [setIsOpen, resolvedIsOpen]);\n\n return { isOpen: resolvedIsOpen, onOpen, onClose, onToggle };\n}\n","import * as React from 'react';\n\nlet counter = 0;\n\nfunction generateId(prefix: string): string {\n counter += 1;\n return `${prefix}-${counter}`;\n}\n\n/**\n * Returns a stable unique ID suitable for accessibility label pairing (aria-labelledby, etc.).\n * Uses React.useId() in React 18+ and falls back to a module-level counter otherwise.\n */\nexport function useId(prefix: string = 'rn'): string {\n // React 18+ provides useId natively\n if (typeof (React as { useId?: () => string }).useId === 'function') {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const id = (React as { useId: () => string }).useId();\n return `${prefix}${id}`;\n }\n\n // Fallback: stable counter-based ID (not SSR-safe, but RN has no SSR)\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const idRef = React.useRef<string | null>(null);\n if (idRef.current === null) {\n idRef.current = generateId(prefix);\n }\n return idRef.current;\n}\n","import { useRef, useEffect } from 'react';\n\n/**\n * Returns the value from the previous render.\n * On the first render, returns `undefined`.\n */\nexport function usePrevious<T>(value: T): T | undefined {\n const ref = useRef<T | undefined>(undefined);\n\n useEffect(() => {\n ref.current = value;\n });\n\n return ref.current;\n}\n","import { useState, useEffect } from 'react';\n\n/**\n * Debounces a value by the given delay (in milliseconds).\n * The returned value will only update after the input value\n * has stopped changing for `delay` ms.\n */\nexport function useDebounce<T>(value: T, delay: number): T {\n const [debouncedValue, setDebouncedValue] = useState<T>(value);\n\n useEffect(() => {\n const timer = setTimeout(() => {\n setDebouncedValue(value);\n }, delay);\n\n return () => {\n clearTimeout(timer);\n };\n }, [value, delay]);\n\n return debouncedValue;\n}\n","import { useCallback, useRef } from 'react';\n\n/**\n * Returns a throttled version of the provided callback.\n * The callback will fire at most once per `delay` ms.\n * Subsequent calls within the delay window are ignored.\n * The returned function is stable across renders.\n */\nexport function useThrottle<T extends (...args: Parameters<T>) => ReturnType<T>>(\n fn: T,\n delay: number,\n): T {\n const lastCallRef = useRef<number>(0);\n const fnRef = useRef<T>(fn);\n fnRef.current = fn;\n\n return useCallback(\n (...args: Parameters<T>): ReturnType<T> | undefined => {\n const now = Date.now();\n if (now - lastCallRef.current >= delay) {\n lastCallRef.current = now;\n return fnRef.current(...args);\n }\n return undefined;\n },\n [delay],\n ) as T;\n}\n","import { useState, useEffect, useRef } from 'react';\nimport { Keyboard, Animated, Platform } from 'react-native';\nimport type { KeyboardEvent } from 'react-native';\n\nexport interface KeyboardState {\n /** Whether the keyboard is currently visible. */\n isVisible: boolean;\n /** Current keyboard height in logical pixels. */\n height: number;\n /** Animated.Value tracking keyboard height — animates in sync with the keyboard. */\n keyboardHeight: Animated.Value;\n}\n\n/**\n * Tracks keyboard visibility and height.\n * Uses React Native's built-in Keyboard API and Animated for smooth transitions.\n * On web, keyboard events are not available — isVisible and height remain at defaults.\n */\nexport function useKeyboard(): KeyboardState {\n const [isVisible, setIsVisible] = useState(false);\n const [height, setHeight] = useState(0);\n const keyboardHeight = useRef(new Animated.Value(0)).current;\n\n useEffect(() => {\n // Web does not support Keyboard events in RN\n if (Platform.OS === 'web') return;\n\n function handleShow(event: KeyboardEvent) {\n const keyHeight = event.endCoordinates.height;\n setIsVisible(true);\n setHeight(keyHeight);\n Animated.timing(keyboardHeight, {\n toValue: keyHeight,\n duration: event.duration ?? 250,\n useNativeDriver: false,\n }).start();\n }\n\n function handleHide(event: KeyboardEvent) {\n setIsVisible(false);\n setHeight(0);\n Animated.timing(keyboardHeight, {\n toValue: 0,\n duration: event.duration ?? 200,\n useNativeDriver: false,\n }).start();\n }\n\n // iOS uses keyboardWillShow/Hide for smooth animation;\n // Android uses keyboardDidShow/Hide.\n const showEvent =\n Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';\n const hideEvent =\n Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';\n\n const showSub = Keyboard.addListener(showEvent, handleShow);\n const hideSub = Keyboard.addListener(hideEvent, handleHide);\n\n return () => {\n showSub.remove();\n hideSub.remove();\n };\n }, [keyboardHeight]);\n\n return { isVisible, height, keyboardHeight };\n}\n","import { useMemo } from 'react';\nimport { useWindowDimensions } from 'react-native';\n\nexport type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n\nexport interface DimensionsState {\n width: number;\n height: number;\n scale: number;\n fontScale: number;\n /** Current breakpoint based on screen width. */\n breakpoint: Breakpoint;\n}\n\nfunction getBreakpoint(width: number): Breakpoint {\n if (width < 480) return 'xs';\n if (width < 768) return 'sm';\n if (width < 1024) return 'md';\n if (width < 1280) return 'lg';\n return 'xl';\n}\n\n/**\n * Returns live window dimensions and a responsive breakpoint derived from width.\n * Breakpoints: xs (<480), sm (<768), md (<1024), lg (<1280), xl (>=1280).\n * Re-renders automatically when the screen size changes (rotation, resize).\n */\nexport function useDimensions(): DimensionsState {\n const { width, height, scale, fontScale } = useWindowDimensions();\n\n const breakpoint = useMemo(() => getBreakpoint(width), [width]);\n\n return { width, height, scale, fontScale, breakpoint };\n}\n","import { useState, useEffect } from 'react';\nimport { AccessibilityInfo, Platform } from 'react-native';\n\nexport interface AccessibilityState {\n /** Whether the user has requested reduced motion. */\n isReducedMotion: boolean;\n /** Whether a screen reader (VoiceOver / TalkBack) is active. */\n isScreenReader: boolean;\n /** Whether bold text is enabled (iOS only; false on Android/Web). */\n isBoldText: boolean;\n /** Whether grayscale mode is enabled (iOS only; false on Android/Web). */\n isGrayscale: boolean;\n /** Whether colour inversion is enabled (iOS only; false on Android/Web). */\n isInvertColors: boolean;\n}\n\nconst defaults: AccessibilityState = {\n isReducedMotion: false,\n isScreenReader: false,\n isBoldText: false,\n isGrayscale: false,\n isInvertColors: false,\n};\n\n/**\n * Subscribes to AccessibilityInfo and returns live accessibility flags.\n * Handles graceful degradation: iOS-only flags always return false on Android/Web.\n */\nexport function useAccessibility(): AccessibilityState {\n const [state, setState] = useState<AccessibilityState>(defaults);\n\n useEffect(() => {\n let cancelled = false;\n\n async function init() {\n const [\n isReducedMotion,\n isScreenReader,\n isBoldText,\n isGrayscale,\n isInvertColors,\n ] = await Promise.all([\n AccessibilityInfo.isReduceMotionEnabled(),\n AccessibilityInfo.isScreenReaderEnabled(),\n Platform.OS === 'ios'\n ? AccessibilityInfo.isBoldTextEnabled()\n : Promise.resolve(false),\n Platform.OS === 'ios'\n ? AccessibilityInfo.isGrayscaleEnabled()\n : Promise.resolve(false),\n Platform.OS === 'ios'\n ? AccessibilityInfo.isInvertColorsEnabled()\n : Promise.resolve(false),\n ]);\n\n if (!cancelled) {\n setState({ isReducedMotion, isScreenReader, isBoldText, isGrayscale, isInvertColors });\n }\n }\n\n void init();\n\n const reduceMotionSub = AccessibilityInfo.addEventListener(\n 'reduceMotionChanged',\n (isReducedMotion) => setState((prev) => ({ ...prev, isReducedMotion })),\n );\n\n const screenReaderSub = AccessibilityInfo.addEventListener(\n 'screenReaderChanged',\n (isScreenReader) => setState((prev) => ({ ...prev, isScreenReader })),\n );\n\n // iOS-only events — addEventListener returns undefined for unknown events on Android\n const boldTextSub =\n Platform.OS === 'ios'\n ? AccessibilityInfo.addEventListener('boldTextChanged', (isBoldText) =>\n setState((prev) => ({ ...prev, isBoldText })),\n )\n : null;\n\n const grayscaleSub =\n Platform.OS === 'ios'\n ? AccessibilityInfo.addEventListener('grayscaleChanged', (isGrayscale) =>\n setState((prev) => ({ ...prev, isGrayscale })),\n )\n : null;\n\n const invertColorsSub =\n Platform.OS === 'ios'\n ? AccessibilityInfo.addEventListener('invertColorsChanged', (isInvertColors) =>\n setState((prev) => ({ ...prev, isInvertColors })),\n )\n : null;\n\n return () => {\n cancelled = true;\n reduceMotionSub.remove();\n screenReaderSub.remove();\n boldTextSub?.remove();\n grayscaleSub?.remove();\n invertColorsSub?.remove();\n };\n }, []);\n\n return state;\n}\n","import { useCallback } from 'react';\nimport { Platform } from 'react-native';\n\nexport type HapticImpactStyle = 'light' | 'medium' | 'heavy';\nexport type HapticNotificationType = 'success' | 'warning' | 'error';\n\nexport interface UseHapticReturn {\n /** Trigger an impact haptic feedback. */\n impact: (style?: HapticImpactStyle) => Promise<void>;\n /** Trigger a notification haptic feedback. */\n notification: (type?: HapticNotificationType) => Promise<void>;\n /** Trigger a selection haptic feedback. */\n selection: () => Promise<void>;\n}\n\n// Lazily resolved expo-haptics module (null when unavailable)\nlet expoHaptics: ExpoHapticsModule | null | undefined = undefined;\n\ninterface ExpoHapticsModule {\n ImpactFeedbackStyle: {\n Light: string;\n Medium: string;\n Heavy: string;\n };\n NotificationFeedbackType: {\n Success: string;\n Warning: string;\n Error: string;\n };\n impactAsync: (style: string) => Promise<void>;\n notificationAsync: (type: string) => Promise<void>;\n selectionAsync: () => Promise<void>;\n}\n\nfunction loadExpoHaptics(): ExpoHapticsModule | null {\n if (expoHaptics !== undefined) return expoHaptics;\n try {\n // Dynamic require so bundlers can tree-shake when expo-haptics is absent\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const mod = require('expo-haptics') as ExpoHapticsModule;\n expoHaptics = mod;\n return mod;\n } catch {\n expoHaptics = null;\n return null;\n }\n}\n\nconst noop = () => Promise.resolve();\n\n/**\n * Haptic feedback wrapper.\n * Uses expo-haptics when available; falls back to no-ops gracefully.\n * Haptics are automatically skipped on web where they are unsupported.\n */\nexport function useHaptic(): UseHapticReturn {\n const impact = useCallback(async (style: HapticImpactStyle = 'medium'): Promise<void> => {\n if (Platform.OS === 'web') return;\n const haptics = loadExpoHaptics();\n if (!haptics) return;\n\n const styleMap: Record<HapticImpactStyle, string> = {\n light: haptics.ImpactFeedbackStyle.Light,\n medium: haptics.ImpactFeedbackStyle.Medium,\n heavy: haptics.ImpactFeedbackStyle.Heavy,\n };\n await haptics.impactAsync(styleMap[style]);\n }, []);\n\n const notification = useCallback(\n async (type: HapticNotificationType = 'success'): Promise<void> => {\n if (Platform.OS === 'web') return;\n const haptics = loadExpoHaptics();\n if (!haptics) return;\n\n const typeMap: Record<HapticNotificationType, string> = {\n success: haptics.NotificationFeedbackType.Success,\n warning: haptics.NotificationFeedbackType.Warning,\n error: haptics.NotificationFeedbackType.Error,\n };\n await haptics.notificationAsync(typeMap[type]);\n },\n [],\n );\n\n const selection = useCallback(async (): Promise<void> => {\n if (Platform.OS === 'web') return;\n const haptics = loadExpoHaptics();\n if (!haptics) return;\n await haptics.selectionAsync();\n }, []);\n\n if (Platform.OS === 'web') {\n return { impact: noop, notification: noop, selection: noop };\n }\n\n return { impact, notification, selection };\n}\n","import { useState, useCallback } from 'react';\nimport type { View, LayoutChangeEvent } from 'react-native';\n\nexport type ContainerBreakpoint = 'xs' | 'sm' | 'md' | 'lg';\n\nfunction getContainerBreakpoint(width: number): ContainerBreakpoint {\n if (width < 320) return 'xs';\n if (width < 480) return 'sm';\n if (width < 640) return 'md';\n return 'lg';\n}\n\n/**\n * Container-based responsive breakpoints derived from the component's own width,\n * not the screen width. Uses `onLayout` to measure the container.\n *\n * Container breakpoints: xs (<320), sm (<480), md (<640), lg (>=640).\n *\n * @returns [ref, breakpoint] — attach `ref` to the View you want to measure.\n *\n * @example\n * const [ref, bp] = useContainerQuery();\n * return <View ref={ref} onLayout={...}>...</View>;\n */\nexport function useContainerQuery(): [\n ref: (node: View | null) => void,\n breakpoint: ContainerBreakpoint,\n] {\n const [breakpoint, setBreakpoint] = useState<ContainerBreakpoint>('xs');\n\n // We use an onLayout callback rather than a ref callback with a MutationObserver\n // because React Native measures happen through the layout event system.\n // The ref here is returned as a convenience so consumers can attach it, but the\n // actual measurement is done via the onLayout prop that we also expose.\n const ref = useCallback((_node: View | null) => {\n // The ref callback is a no-op — measurement happens via onLayout below.\n }, []);\n\n const onLayout = useCallback((event: LayoutChangeEvent) => {\n const { width } = event.nativeEvent.layout;\n setBreakpoint(getContainerBreakpoint(width));\n }, []);\n\n // We augment the returned tuple with an onLayout so callers can use it.\n // Return value is [ref, breakpoint, onLayout] for ergonomics.\n // Keep the public API as [ref, breakpoint] per the spec, but attach onLayout\n // to the ref callback so callers that spread the returned ref also get measurement.\n // Actually — return three values and document properly.\n return Object.assign([ref, breakpoint] as [typeof ref, ContainerBreakpoint], { onLayout });\n}\n\n/**\n * Extended return type that includes an `onLayout` handler for measurement.\n * Prefer this form for explicit usage:\n *\n * @example\n * const { ref, breakpoint, onLayout } = useContainerQueryFull();\n * return <View ref={ref} onLayout={onLayout}>...</View>;\n */\nexport interface ContainerQueryResult {\n ref: (node: View | null) => void;\n breakpoint: ContainerBreakpoint;\n onLayout: (event: LayoutChangeEvent) => void;\n}\n\n/**\n * Object-API variant of `useContainerQuery` — easier to use when you need\n * all three values without destructuring a tuple.\n */\nexport function useContainerQueryFull(): ContainerQueryResult {\n const [breakpoint, setBreakpoint] = useState<ContainerBreakpoint>('xs');\n\n const ref = useCallback((_node: View | null) => {\n // Measurement is driven by onLayout, not the ref callback.\n }, []);\n\n const onLayout = useCallback((event: LayoutChangeEvent) => {\n const { width } = event.nativeEvent.layout;\n setBreakpoint(getContainerBreakpoint(width));\n }, []);\n\n return { ref, breakpoint, onLayout };\n}\n","/**\n * useScrollHandler — scroll-driven animation values via Reanimated.\n *\n * This hook requires `react-native-reanimated` >= 3.6.0 as a peer dependency.\n * It is guarded by a try/catch import so that the rest of the package remains\n * importable when Reanimated is not installed (it will throw a clear error only\n * when useScrollHandler itself is called).\n */\n\n// We import types only here so TypeScript can check the rest of this file even\n// when the peer is absent at runtime. The actual module is loaded lazily.\nimport type {\n SharedValue,\n ScrollHandlerProcessed,\n} from 'react-native-reanimated';\n\nexport interface ScrollHandlerResult {\n /** Pass this to the `onScroll` prop of Reanimated.ScrollView / FlatList. */\n scrollHandler: ScrollHandlerProcessed<Record<string, unknown>>;\n /** Shared value tracking current vertical scroll offset in pixels. */\n scrollY: SharedValue<number>;\n /** Shared value tracking scroll direction — updates when direction changes. */\n scrollDirection: SharedValue<'up' | 'down'>;\n}\n\nlet reanimated: typeof import('react-native-reanimated') | null = null;\n\nfunction loadReanimated(): typeof import('react-native-reanimated') {\n if (reanimated) return reanimated;\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n reanimated = require('react-native-reanimated') as typeof import('react-native-reanimated');\n return reanimated;\n } catch {\n throw new Error(\n '[reactnatively-hooks] useScrollHandler requires react-native-reanimated >= 3.6.0. ' +\n 'Install it with: npx expo install react-native-reanimated',\n );\n }\n}\n\n/**\n * Returns scroll-driven Reanimated shared values and a scroll handler.\n *\n * @example\n * const { scrollHandler, scrollY, scrollDirection } = useScrollHandler();\n * return (\n * <Animated.ScrollView onScroll={scrollHandler} scrollEventThrottle={16}>\n * ...\n * </Animated.ScrollView>\n * );\n */\nexport function useScrollHandler(): ScrollHandlerResult {\n const rn = loadReanimated();\n const { useSharedValue, useAnimatedScrollHandler } = rn;\n\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const scrollY = useSharedValue<number>(0);\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const scrollDirection = useSharedValue<'up' | 'down'>('down');\n\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const scrollHandler = useAnimatedScrollHandler({\n onScroll(event) {\n 'worklet';\n const y = event.contentOffset.y;\n if (y > scrollY.value) {\n scrollDirection.value = 'down';\n } else if (y < scrollY.value) {\n scrollDirection.value = 'up';\n }\n scrollY.value = y;\n },\n });\n\n return { scrollHandler, scrollY, scrollDirection };\n}\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
|
3
|
+
import { Animated, Platform, Keyboard, useWindowDimensions, AccessibilityInfo } from 'react-native';
|
|
4
|
+
|
|
5
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
6
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
7
|
+
}) : x)(function(x) {
|
|
8
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
9
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
10
|
+
});
|
|
11
|
+
function useControllable({
|
|
12
|
+
value,
|
|
13
|
+
defaultValue,
|
|
14
|
+
onChange
|
|
15
|
+
}) {
|
|
16
|
+
const isControlled = value !== void 0;
|
|
17
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
18
|
+
const onChangeRef = useRef(onChange);
|
|
19
|
+
onChangeRef.current = onChange;
|
|
20
|
+
const setValue = useCallback(
|
|
21
|
+
(next) => {
|
|
22
|
+
if (!isControlled) {
|
|
23
|
+
setInternalValue(next);
|
|
24
|
+
}
|
|
25
|
+
onChangeRef.current?.(next);
|
|
26
|
+
},
|
|
27
|
+
[isControlled]
|
|
28
|
+
);
|
|
29
|
+
return [isControlled ? value : internalValue, setValue];
|
|
30
|
+
}
|
|
31
|
+
function useDisclosure(options = {}) {
|
|
32
|
+
const { defaultIsOpen = false, isOpen: controlledIsOpen, onChange } = options;
|
|
33
|
+
const [isOpen, setIsOpen] = useControllable({
|
|
34
|
+
value: controlledIsOpen,
|
|
35
|
+
defaultValue: defaultIsOpen,
|
|
36
|
+
onChange
|
|
37
|
+
});
|
|
38
|
+
const resolvedIsOpen = isOpen ?? false;
|
|
39
|
+
const onOpen = useCallback(() => {
|
|
40
|
+
setIsOpen(true);
|
|
41
|
+
}, [setIsOpen]);
|
|
42
|
+
const onClose = useCallback(() => {
|
|
43
|
+
setIsOpen(false);
|
|
44
|
+
}, [setIsOpen]);
|
|
45
|
+
const onToggle = useCallback(() => {
|
|
46
|
+
setIsOpen(!resolvedIsOpen);
|
|
47
|
+
}, [setIsOpen, resolvedIsOpen]);
|
|
48
|
+
return { isOpen: resolvedIsOpen, onOpen, onClose, onToggle };
|
|
49
|
+
}
|
|
50
|
+
var counter = 0;
|
|
51
|
+
function generateId(prefix) {
|
|
52
|
+
counter += 1;
|
|
53
|
+
return `${prefix}-${counter}`;
|
|
54
|
+
}
|
|
55
|
+
function useId2(prefix = "rn") {
|
|
56
|
+
if (typeof React.useId === "function") {
|
|
57
|
+
const id = React.useId();
|
|
58
|
+
return `${prefix}${id}`;
|
|
59
|
+
}
|
|
60
|
+
const idRef = React.useRef(null);
|
|
61
|
+
if (idRef.current === null) {
|
|
62
|
+
idRef.current = generateId(prefix);
|
|
63
|
+
}
|
|
64
|
+
return idRef.current;
|
|
65
|
+
}
|
|
66
|
+
function usePrevious(value) {
|
|
67
|
+
const ref = useRef(void 0);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
ref.current = value;
|
|
70
|
+
});
|
|
71
|
+
return ref.current;
|
|
72
|
+
}
|
|
73
|
+
function useDebounce(value, delay) {
|
|
74
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const timer = setTimeout(() => {
|
|
77
|
+
setDebouncedValue(value);
|
|
78
|
+
}, delay);
|
|
79
|
+
return () => {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
};
|
|
82
|
+
}, [value, delay]);
|
|
83
|
+
return debouncedValue;
|
|
84
|
+
}
|
|
85
|
+
function useThrottle(fn, delay) {
|
|
86
|
+
const lastCallRef = useRef(0);
|
|
87
|
+
const fnRef = useRef(fn);
|
|
88
|
+
fnRef.current = fn;
|
|
89
|
+
return useCallback(
|
|
90
|
+
(...args) => {
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
if (now - lastCallRef.current >= delay) {
|
|
93
|
+
lastCallRef.current = now;
|
|
94
|
+
return fnRef.current(...args);
|
|
95
|
+
}
|
|
96
|
+
return void 0;
|
|
97
|
+
},
|
|
98
|
+
[delay]
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
function useKeyboard() {
|
|
102
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
103
|
+
const [height, setHeight] = useState(0);
|
|
104
|
+
const keyboardHeight = useRef(new Animated.Value(0)).current;
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (Platform.OS === "web") return;
|
|
107
|
+
function handleShow(event) {
|
|
108
|
+
const keyHeight = event.endCoordinates.height;
|
|
109
|
+
setIsVisible(true);
|
|
110
|
+
setHeight(keyHeight);
|
|
111
|
+
Animated.timing(keyboardHeight, {
|
|
112
|
+
toValue: keyHeight,
|
|
113
|
+
duration: event.duration ?? 250,
|
|
114
|
+
useNativeDriver: false
|
|
115
|
+
}).start();
|
|
116
|
+
}
|
|
117
|
+
function handleHide(event) {
|
|
118
|
+
setIsVisible(false);
|
|
119
|
+
setHeight(0);
|
|
120
|
+
Animated.timing(keyboardHeight, {
|
|
121
|
+
toValue: 0,
|
|
122
|
+
duration: event.duration ?? 200,
|
|
123
|
+
useNativeDriver: false
|
|
124
|
+
}).start();
|
|
125
|
+
}
|
|
126
|
+
const showEvent = Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
|
|
127
|
+
const hideEvent = Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide";
|
|
128
|
+
const showSub = Keyboard.addListener(showEvent, handleShow);
|
|
129
|
+
const hideSub = Keyboard.addListener(hideEvent, handleHide);
|
|
130
|
+
return () => {
|
|
131
|
+
showSub.remove();
|
|
132
|
+
hideSub.remove();
|
|
133
|
+
};
|
|
134
|
+
}, [keyboardHeight]);
|
|
135
|
+
return { isVisible, height, keyboardHeight };
|
|
136
|
+
}
|
|
137
|
+
function getBreakpoint(width) {
|
|
138
|
+
if (width < 480) return "xs";
|
|
139
|
+
if (width < 768) return "sm";
|
|
140
|
+
if (width < 1024) return "md";
|
|
141
|
+
if (width < 1280) return "lg";
|
|
142
|
+
return "xl";
|
|
143
|
+
}
|
|
144
|
+
function useDimensions() {
|
|
145
|
+
const { width, height, scale, fontScale } = useWindowDimensions();
|
|
146
|
+
const breakpoint = useMemo(() => getBreakpoint(width), [width]);
|
|
147
|
+
return { width, height, scale, fontScale, breakpoint };
|
|
148
|
+
}
|
|
149
|
+
var defaults = {
|
|
150
|
+
isReducedMotion: false,
|
|
151
|
+
isScreenReader: false,
|
|
152
|
+
isBoldText: false,
|
|
153
|
+
isGrayscale: false,
|
|
154
|
+
isInvertColors: false
|
|
155
|
+
};
|
|
156
|
+
function useAccessibility() {
|
|
157
|
+
const [state, setState] = useState(defaults);
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
let cancelled = false;
|
|
160
|
+
async function init() {
|
|
161
|
+
const [
|
|
162
|
+
isReducedMotion,
|
|
163
|
+
isScreenReader,
|
|
164
|
+
isBoldText,
|
|
165
|
+
isGrayscale,
|
|
166
|
+
isInvertColors
|
|
167
|
+
] = await Promise.all([
|
|
168
|
+
AccessibilityInfo.isReduceMotionEnabled(),
|
|
169
|
+
AccessibilityInfo.isScreenReaderEnabled(),
|
|
170
|
+
Platform.OS === "ios" ? AccessibilityInfo.isBoldTextEnabled() : Promise.resolve(false),
|
|
171
|
+
Platform.OS === "ios" ? AccessibilityInfo.isGrayscaleEnabled() : Promise.resolve(false),
|
|
172
|
+
Platform.OS === "ios" ? AccessibilityInfo.isInvertColorsEnabled() : Promise.resolve(false)
|
|
173
|
+
]);
|
|
174
|
+
if (!cancelled) {
|
|
175
|
+
setState({ isReducedMotion, isScreenReader, isBoldText, isGrayscale, isInvertColors });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
void init();
|
|
179
|
+
const reduceMotionSub = AccessibilityInfo.addEventListener(
|
|
180
|
+
"reduceMotionChanged",
|
|
181
|
+
(isReducedMotion) => setState((prev) => ({ ...prev, isReducedMotion }))
|
|
182
|
+
);
|
|
183
|
+
const screenReaderSub = AccessibilityInfo.addEventListener(
|
|
184
|
+
"screenReaderChanged",
|
|
185
|
+
(isScreenReader) => setState((prev) => ({ ...prev, isScreenReader }))
|
|
186
|
+
);
|
|
187
|
+
const boldTextSub = Platform.OS === "ios" ? AccessibilityInfo.addEventListener(
|
|
188
|
+
"boldTextChanged",
|
|
189
|
+
(isBoldText) => setState((prev) => ({ ...prev, isBoldText }))
|
|
190
|
+
) : null;
|
|
191
|
+
const grayscaleSub = Platform.OS === "ios" ? AccessibilityInfo.addEventListener(
|
|
192
|
+
"grayscaleChanged",
|
|
193
|
+
(isGrayscale) => setState((prev) => ({ ...prev, isGrayscale }))
|
|
194
|
+
) : null;
|
|
195
|
+
const invertColorsSub = Platform.OS === "ios" ? AccessibilityInfo.addEventListener(
|
|
196
|
+
"invertColorsChanged",
|
|
197
|
+
(isInvertColors) => setState((prev) => ({ ...prev, isInvertColors }))
|
|
198
|
+
) : null;
|
|
199
|
+
return () => {
|
|
200
|
+
cancelled = true;
|
|
201
|
+
reduceMotionSub.remove();
|
|
202
|
+
screenReaderSub.remove();
|
|
203
|
+
boldTextSub?.remove();
|
|
204
|
+
grayscaleSub?.remove();
|
|
205
|
+
invertColorsSub?.remove();
|
|
206
|
+
};
|
|
207
|
+
}, []);
|
|
208
|
+
return state;
|
|
209
|
+
}
|
|
210
|
+
var expoHaptics = void 0;
|
|
211
|
+
function loadExpoHaptics() {
|
|
212
|
+
if (expoHaptics !== void 0) return expoHaptics;
|
|
213
|
+
try {
|
|
214
|
+
const mod = __require("expo-haptics");
|
|
215
|
+
expoHaptics = mod;
|
|
216
|
+
return mod;
|
|
217
|
+
} catch {
|
|
218
|
+
expoHaptics = null;
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
var noop = () => Promise.resolve();
|
|
223
|
+
function useHaptic() {
|
|
224
|
+
const impact = useCallback(async (style = "medium") => {
|
|
225
|
+
if (Platform.OS === "web") return;
|
|
226
|
+
const haptics = loadExpoHaptics();
|
|
227
|
+
if (!haptics) return;
|
|
228
|
+
const styleMap = {
|
|
229
|
+
light: haptics.ImpactFeedbackStyle.Light,
|
|
230
|
+
medium: haptics.ImpactFeedbackStyle.Medium,
|
|
231
|
+
heavy: haptics.ImpactFeedbackStyle.Heavy
|
|
232
|
+
};
|
|
233
|
+
await haptics.impactAsync(styleMap[style]);
|
|
234
|
+
}, []);
|
|
235
|
+
const notification = useCallback(
|
|
236
|
+
async (type = "success") => {
|
|
237
|
+
if (Platform.OS === "web") return;
|
|
238
|
+
const haptics = loadExpoHaptics();
|
|
239
|
+
if (!haptics) return;
|
|
240
|
+
const typeMap = {
|
|
241
|
+
success: haptics.NotificationFeedbackType.Success,
|
|
242
|
+
warning: haptics.NotificationFeedbackType.Warning,
|
|
243
|
+
error: haptics.NotificationFeedbackType.Error
|
|
244
|
+
};
|
|
245
|
+
await haptics.notificationAsync(typeMap[type]);
|
|
246
|
+
},
|
|
247
|
+
[]
|
|
248
|
+
);
|
|
249
|
+
const selection = useCallback(async () => {
|
|
250
|
+
if (Platform.OS === "web") return;
|
|
251
|
+
const haptics = loadExpoHaptics();
|
|
252
|
+
if (!haptics) return;
|
|
253
|
+
await haptics.selectionAsync();
|
|
254
|
+
}, []);
|
|
255
|
+
if (Platform.OS === "web") {
|
|
256
|
+
return { impact: noop, notification: noop, selection: noop };
|
|
257
|
+
}
|
|
258
|
+
return { impact, notification, selection };
|
|
259
|
+
}
|
|
260
|
+
function getContainerBreakpoint(width) {
|
|
261
|
+
if (width < 320) return "xs";
|
|
262
|
+
if (width < 480) return "sm";
|
|
263
|
+
if (width < 640) return "md";
|
|
264
|
+
return "lg";
|
|
265
|
+
}
|
|
266
|
+
function useContainerQuery() {
|
|
267
|
+
const [breakpoint, setBreakpoint] = useState("xs");
|
|
268
|
+
const ref = useCallback((_node) => {
|
|
269
|
+
}, []);
|
|
270
|
+
const onLayout = useCallback((event) => {
|
|
271
|
+
const { width } = event.nativeEvent.layout;
|
|
272
|
+
setBreakpoint(getContainerBreakpoint(width));
|
|
273
|
+
}, []);
|
|
274
|
+
return Object.assign([ref, breakpoint], { onLayout });
|
|
275
|
+
}
|
|
276
|
+
function useContainerQueryFull() {
|
|
277
|
+
const [breakpoint, setBreakpoint] = useState("xs");
|
|
278
|
+
const ref = useCallback((_node) => {
|
|
279
|
+
}, []);
|
|
280
|
+
const onLayout = useCallback((event) => {
|
|
281
|
+
const { width } = event.nativeEvent.layout;
|
|
282
|
+
setBreakpoint(getContainerBreakpoint(width));
|
|
283
|
+
}, []);
|
|
284
|
+
return { ref, breakpoint, onLayout };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/useScrollHandler.ts
|
|
288
|
+
var reanimated = null;
|
|
289
|
+
function loadReanimated() {
|
|
290
|
+
if (reanimated) return reanimated;
|
|
291
|
+
try {
|
|
292
|
+
reanimated = __require("react-native-reanimated");
|
|
293
|
+
return reanimated;
|
|
294
|
+
} catch {
|
|
295
|
+
throw new Error(
|
|
296
|
+
"[reactnatively-hooks] useScrollHandler requires react-native-reanimated >= 3.6.0. Install it with: npx expo install react-native-reanimated"
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function useScrollHandler() {
|
|
301
|
+
const rn = loadReanimated();
|
|
302
|
+
const { useSharedValue, useAnimatedScrollHandler } = rn;
|
|
303
|
+
const scrollY = useSharedValue(0);
|
|
304
|
+
const scrollDirection = useSharedValue("down");
|
|
305
|
+
const scrollHandler = useAnimatedScrollHandler({
|
|
306
|
+
onScroll(event) {
|
|
307
|
+
"worklet";
|
|
308
|
+
const y = event.contentOffset.y;
|
|
309
|
+
if (y > scrollY.value) {
|
|
310
|
+
scrollDirection.value = "down";
|
|
311
|
+
} else if (y < scrollY.value) {
|
|
312
|
+
scrollDirection.value = "up";
|
|
313
|
+
}
|
|
314
|
+
scrollY.value = y;
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
return { scrollHandler, scrollY, scrollDirection };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export { useAccessibility, useContainerQuery, useContainerQueryFull, useControllable, useDebounce, useDimensions, useDisclosure, useHaptic, useId2 as useId, useKeyboard, usePrevious, useScrollHandler, useThrottle };
|
|
321
|
+
//# sourceMappingURL=index.mjs.map
|
|
322
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/useControllable.ts","../src/useDisclosure.ts","../src/useId.ts","../src/usePrevious.ts","../src/useDebounce.ts","../src/useThrottle.ts","../src/useKeyboard.ts","../src/useDimensions.ts","../src/useAccessibility.ts","../src/useHaptic.ts","../src/useContainerQuery.ts","../src/useScrollHandler.ts"],"names":["useCallback","useId","useRef","useState","useEffect","Platform"],"mappings":"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","file":"index.mjs","sourcesContent":["import { useState, useCallback, useRef } from 'react';\n\nexport interface UseControllableOptions<T> {\n /** The controlled value. When provided the hook operates in controlled mode. */\n value?: T;\n /** The default value for uncontrolled mode. */\n defaultValue?: T;\n /** Called when the value changes in uncontrolled mode. */\n onChange?: (value: T) => void;\n}\n\n/**\n * Bridges controlled and uncontrolled component patterns.\n * Returns [resolvedValue, setValue] — handles both controlled and uncontrolled.\n */\nexport function useControllable<T>({\n value,\n defaultValue,\n onChange,\n}: UseControllableOptions<T>): [T | undefined, (next: T) => void] {\n const isControlled = value !== undefined;\n const [internalValue, setInternalValue] = useState<T | undefined>(defaultValue);\n\n // Keep a stable ref to onChange to avoid re-creating setValue\n const onChangeRef = useRef(onChange);\n onChangeRef.current = onChange;\n\n const setValue = useCallback(\n (next: T) => {\n if (!isControlled) {\n setInternalValue(next);\n }\n onChangeRef.current?.(next);\n },\n [isControlled],\n );\n\n return [isControlled ? value : internalValue, setValue];\n}\n","import { useCallback } from 'react';\nimport { useControllable } from './useControllable';\n\nexport interface UseDisclosureOptions {\n /** Initial open state for uncontrolled mode. */\n defaultIsOpen?: boolean;\n /** Controlled open state. */\n isOpen?: boolean;\n /** Called when the open state changes in controlled mode. */\n onChange?: (isOpen: boolean) => void;\n}\n\nexport interface UseDisclosureReturn {\n isOpen: boolean;\n onOpen: () => void;\n onClose: () => void;\n onToggle: () => void;\n}\n\n/**\n * Manages open/close/toggle state for modals, drawers, menus, etc.\n * Supports both controlled (external isOpen + onChange) and uncontrolled modes.\n */\nexport function useDisclosure(options: UseDisclosureOptions = {}): UseDisclosureReturn {\n const { defaultIsOpen = false, isOpen: controlledIsOpen, onChange } = options;\n\n const [isOpen, setIsOpen] = useControllable<boolean>({\n value: controlledIsOpen,\n defaultValue: defaultIsOpen,\n onChange,\n });\n\n const resolvedIsOpen = isOpen ?? false;\n\n const onOpen = useCallback(() => {\n setIsOpen(true);\n }, [setIsOpen]);\n\n const onClose = useCallback(() => {\n setIsOpen(false);\n }, [setIsOpen]);\n\n const onToggle = useCallback(() => {\n setIsOpen(!resolvedIsOpen);\n }, [setIsOpen, resolvedIsOpen]);\n\n return { isOpen: resolvedIsOpen, onOpen, onClose, onToggle };\n}\n","import * as React from 'react';\n\nlet counter = 0;\n\nfunction generateId(prefix: string): string {\n counter += 1;\n return `${prefix}-${counter}`;\n}\n\n/**\n * Returns a stable unique ID suitable for accessibility label pairing (aria-labelledby, etc.).\n * Uses React.useId() in React 18+ and falls back to a module-level counter otherwise.\n */\nexport function useId(prefix: string = 'rn'): string {\n // React 18+ provides useId natively\n if (typeof (React as { useId?: () => string }).useId === 'function') {\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const id = (React as { useId: () => string }).useId();\n return `${prefix}${id}`;\n }\n\n // Fallback: stable counter-based ID (not SSR-safe, but RN has no SSR)\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const idRef = React.useRef<string | null>(null);\n if (idRef.current === null) {\n idRef.current = generateId(prefix);\n }\n return idRef.current;\n}\n","import { useRef, useEffect } from 'react';\n\n/**\n * Returns the value from the previous render.\n * On the first render, returns `undefined`.\n */\nexport function usePrevious<T>(value: T): T | undefined {\n const ref = useRef<T | undefined>(undefined);\n\n useEffect(() => {\n ref.current = value;\n });\n\n return ref.current;\n}\n","import { useState, useEffect } from 'react';\n\n/**\n * Debounces a value by the given delay (in milliseconds).\n * The returned value will only update after the input value\n * has stopped changing for `delay` ms.\n */\nexport function useDebounce<T>(value: T, delay: number): T {\n const [debouncedValue, setDebouncedValue] = useState<T>(value);\n\n useEffect(() => {\n const timer = setTimeout(() => {\n setDebouncedValue(value);\n }, delay);\n\n return () => {\n clearTimeout(timer);\n };\n }, [value, delay]);\n\n return debouncedValue;\n}\n","import { useCallback, useRef } from 'react';\n\n/**\n * Returns a throttled version of the provided callback.\n * The callback will fire at most once per `delay` ms.\n * Subsequent calls within the delay window are ignored.\n * The returned function is stable across renders.\n */\nexport function useThrottle<T extends (...args: Parameters<T>) => ReturnType<T>>(\n fn: T,\n delay: number,\n): T {\n const lastCallRef = useRef<number>(0);\n const fnRef = useRef<T>(fn);\n fnRef.current = fn;\n\n return useCallback(\n (...args: Parameters<T>): ReturnType<T> | undefined => {\n const now = Date.now();\n if (now - lastCallRef.current >= delay) {\n lastCallRef.current = now;\n return fnRef.current(...args);\n }\n return undefined;\n },\n [delay],\n ) as T;\n}\n","import { useState, useEffect, useRef } from 'react';\nimport { Keyboard, Animated, Platform } from 'react-native';\nimport type { KeyboardEvent } from 'react-native';\n\nexport interface KeyboardState {\n /** Whether the keyboard is currently visible. */\n isVisible: boolean;\n /** Current keyboard height in logical pixels. */\n height: number;\n /** Animated.Value tracking keyboard height — animates in sync with the keyboard. */\n keyboardHeight: Animated.Value;\n}\n\n/**\n * Tracks keyboard visibility and height.\n * Uses React Native's built-in Keyboard API and Animated for smooth transitions.\n * On web, keyboard events are not available — isVisible and height remain at defaults.\n */\nexport function useKeyboard(): KeyboardState {\n const [isVisible, setIsVisible] = useState(false);\n const [height, setHeight] = useState(0);\n const keyboardHeight = useRef(new Animated.Value(0)).current;\n\n useEffect(() => {\n // Web does not support Keyboard events in RN\n if (Platform.OS === 'web') return;\n\n function handleShow(event: KeyboardEvent) {\n const keyHeight = event.endCoordinates.height;\n setIsVisible(true);\n setHeight(keyHeight);\n Animated.timing(keyboardHeight, {\n toValue: keyHeight,\n duration: event.duration ?? 250,\n useNativeDriver: false,\n }).start();\n }\n\n function handleHide(event: KeyboardEvent) {\n setIsVisible(false);\n setHeight(0);\n Animated.timing(keyboardHeight, {\n toValue: 0,\n duration: event.duration ?? 200,\n useNativeDriver: false,\n }).start();\n }\n\n // iOS uses keyboardWillShow/Hide for smooth animation;\n // Android uses keyboardDidShow/Hide.\n const showEvent =\n Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';\n const hideEvent =\n Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';\n\n const showSub = Keyboard.addListener(showEvent, handleShow);\n const hideSub = Keyboard.addListener(hideEvent, handleHide);\n\n return () => {\n showSub.remove();\n hideSub.remove();\n };\n }, [keyboardHeight]);\n\n return { isVisible, height, keyboardHeight };\n}\n","import { useMemo } from 'react';\nimport { useWindowDimensions } from 'react-native';\n\nexport type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n\nexport interface DimensionsState {\n width: number;\n height: number;\n scale: number;\n fontScale: number;\n /** Current breakpoint based on screen width. */\n breakpoint: Breakpoint;\n}\n\nfunction getBreakpoint(width: number): Breakpoint {\n if (width < 480) return 'xs';\n if (width < 768) return 'sm';\n if (width < 1024) return 'md';\n if (width < 1280) return 'lg';\n return 'xl';\n}\n\n/**\n * Returns live window dimensions and a responsive breakpoint derived from width.\n * Breakpoints: xs (<480), sm (<768), md (<1024), lg (<1280), xl (>=1280).\n * Re-renders automatically when the screen size changes (rotation, resize).\n */\nexport function useDimensions(): DimensionsState {\n const { width, height, scale, fontScale } = useWindowDimensions();\n\n const breakpoint = useMemo(() => getBreakpoint(width), [width]);\n\n return { width, height, scale, fontScale, breakpoint };\n}\n","import { useState, useEffect } from 'react';\nimport { AccessibilityInfo, Platform } from 'react-native';\n\nexport interface AccessibilityState {\n /** Whether the user has requested reduced motion. */\n isReducedMotion: boolean;\n /** Whether a screen reader (VoiceOver / TalkBack) is active. */\n isScreenReader: boolean;\n /** Whether bold text is enabled (iOS only; false on Android/Web). */\n isBoldText: boolean;\n /** Whether grayscale mode is enabled (iOS only; false on Android/Web). */\n isGrayscale: boolean;\n /** Whether colour inversion is enabled (iOS only; false on Android/Web). */\n isInvertColors: boolean;\n}\n\nconst defaults: AccessibilityState = {\n isReducedMotion: false,\n isScreenReader: false,\n isBoldText: false,\n isGrayscale: false,\n isInvertColors: false,\n};\n\n/**\n * Subscribes to AccessibilityInfo and returns live accessibility flags.\n * Handles graceful degradation: iOS-only flags always return false on Android/Web.\n */\nexport function useAccessibility(): AccessibilityState {\n const [state, setState] = useState<AccessibilityState>(defaults);\n\n useEffect(() => {\n let cancelled = false;\n\n async function init() {\n const [\n isReducedMotion,\n isScreenReader,\n isBoldText,\n isGrayscale,\n isInvertColors,\n ] = await Promise.all([\n AccessibilityInfo.isReduceMotionEnabled(),\n AccessibilityInfo.isScreenReaderEnabled(),\n Platform.OS === 'ios'\n ? AccessibilityInfo.isBoldTextEnabled()\n : Promise.resolve(false),\n Platform.OS === 'ios'\n ? AccessibilityInfo.isGrayscaleEnabled()\n : Promise.resolve(false),\n Platform.OS === 'ios'\n ? AccessibilityInfo.isInvertColorsEnabled()\n : Promise.resolve(false),\n ]);\n\n if (!cancelled) {\n setState({ isReducedMotion, isScreenReader, isBoldText, isGrayscale, isInvertColors });\n }\n }\n\n void init();\n\n const reduceMotionSub = AccessibilityInfo.addEventListener(\n 'reduceMotionChanged',\n (isReducedMotion) => setState((prev) => ({ ...prev, isReducedMotion })),\n );\n\n const screenReaderSub = AccessibilityInfo.addEventListener(\n 'screenReaderChanged',\n (isScreenReader) => setState((prev) => ({ ...prev, isScreenReader })),\n );\n\n // iOS-only events — addEventListener returns undefined for unknown events on Android\n const boldTextSub =\n Platform.OS === 'ios'\n ? AccessibilityInfo.addEventListener('boldTextChanged', (isBoldText) =>\n setState((prev) => ({ ...prev, isBoldText })),\n )\n : null;\n\n const grayscaleSub =\n Platform.OS === 'ios'\n ? AccessibilityInfo.addEventListener('grayscaleChanged', (isGrayscale) =>\n setState((prev) => ({ ...prev, isGrayscale })),\n )\n : null;\n\n const invertColorsSub =\n Platform.OS === 'ios'\n ? AccessibilityInfo.addEventListener('invertColorsChanged', (isInvertColors) =>\n setState((prev) => ({ ...prev, isInvertColors })),\n )\n : null;\n\n return () => {\n cancelled = true;\n reduceMotionSub.remove();\n screenReaderSub.remove();\n boldTextSub?.remove();\n grayscaleSub?.remove();\n invertColorsSub?.remove();\n };\n }, []);\n\n return state;\n}\n","import { useCallback } from 'react';\nimport { Platform } from 'react-native';\n\nexport type HapticImpactStyle = 'light' | 'medium' | 'heavy';\nexport type HapticNotificationType = 'success' | 'warning' | 'error';\n\nexport interface UseHapticReturn {\n /** Trigger an impact haptic feedback. */\n impact: (style?: HapticImpactStyle) => Promise<void>;\n /** Trigger a notification haptic feedback. */\n notification: (type?: HapticNotificationType) => Promise<void>;\n /** Trigger a selection haptic feedback. */\n selection: () => Promise<void>;\n}\n\n// Lazily resolved expo-haptics module (null when unavailable)\nlet expoHaptics: ExpoHapticsModule | null | undefined = undefined;\n\ninterface ExpoHapticsModule {\n ImpactFeedbackStyle: {\n Light: string;\n Medium: string;\n Heavy: string;\n };\n NotificationFeedbackType: {\n Success: string;\n Warning: string;\n Error: string;\n };\n impactAsync: (style: string) => Promise<void>;\n notificationAsync: (type: string) => Promise<void>;\n selectionAsync: () => Promise<void>;\n}\n\nfunction loadExpoHaptics(): ExpoHapticsModule | null {\n if (expoHaptics !== undefined) return expoHaptics;\n try {\n // Dynamic require so bundlers can tree-shake when expo-haptics is absent\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const mod = require('expo-haptics') as ExpoHapticsModule;\n expoHaptics = mod;\n return mod;\n } catch {\n expoHaptics = null;\n return null;\n }\n}\n\nconst noop = () => Promise.resolve();\n\n/**\n * Haptic feedback wrapper.\n * Uses expo-haptics when available; falls back to no-ops gracefully.\n * Haptics are automatically skipped on web where they are unsupported.\n */\nexport function useHaptic(): UseHapticReturn {\n const impact = useCallback(async (style: HapticImpactStyle = 'medium'): Promise<void> => {\n if (Platform.OS === 'web') return;\n const haptics = loadExpoHaptics();\n if (!haptics) return;\n\n const styleMap: Record<HapticImpactStyle, string> = {\n light: haptics.ImpactFeedbackStyle.Light,\n medium: haptics.ImpactFeedbackStyle.Medium,\n heavy: haptics.ImpactFeedbackStyle.Heavy,\n };\n await haptics.impactAsync(styleMap[style]);\n }, []);\n\n const notification = useCallback(\n async (type: HapticNotificationType = 'success'): Promise<void> => {\n if (Platform.OS === 'web') return;\n const haptics = loadExpoHaptics();\n if (!haptics) return;\n\n const typeMap: Record<HapticNotificationType, string> = {\n success: haptics.NotificationFeedbackType.Success,\n warning: haptics.NotificationFeedbackType.Warning,\n error: haptics.NotificationFeedbackType.Error,\n };\n await haptics.notificationAsync(typeMap[type]);\n },\n [],\n );\n\n const selection = useCallback(async (): Promise<void> => {\n if (Platform.OS === 'web') return;\n const haptics = loadExpoHaptics();\n if (!haptics) return;\n await haptics.selectionAsync();\n }, []);\n\n if (Platform.OS === 'web') {\n return { impact: noop, notification: noop, selection: noop };\n }\n\n return { impact, notification, selection };\n}\n","import { useState, useCallback } from 'react';\nimport type { View, LayoutChangeEvent } from 'react-native';\n\nexport type ContainerBreakpoint = 'xs' | 'sm' | 'md' | 'lg';\n\nfunction getContainerBreakpoint(width: number): ContainerBreakpoint {\n if (width < 320) return 'xs';\n if (width < 480) return 'sm';\n if (width < 640) return 'md';\n return 'lg';\n}\n\n/**\n * Container-based responsive breakpoints derived from the component's own width,\n * not the screen width. Uses `onLayout` to measure the container.\n *\n * Container breakpoints: xs (<320), sm (<480), md (<640), lg (>=640).\n *\n * @returns [ref, breakpoint] — attach `ref` to the View you want to measure.\n *\n * @example\n * const [ref, bp] = useContainerQuery();\n * return <View ref={ref} onLayout={...}>...</View>;\n */\nexport function useContainerQuery(): [\n ref: (node: View | null) => void,\n breakpoint: ContainerBreakpoint,\n] {\n const [breakpoint, setBreakpoint] = useState<ContainerBreakpoint>('xs');\n\n // We use an onLayout callback rather than a ref callback with a MutationObserver\n // because React Native measures happen through the layout event system.\n // The ref here is returned as a convenience so consumers can attach it, but the\n // actual measurement is done via the onLayout prop that we also expose.\n const ref = useCallback((_node: View | null) => {\n // The ref callback is a no-op — measurement happens via onLayout below.\n }, []);\n\n const onLayout = useCallback((event: LayoutChangeEvent) => {\n const { width } = event.nativeEvent.layout;\n setBreakpoint(getContainerBreakpoint(width));\n }, []);\n\n // We augment the returned tuple with an onLayout so callers can use it.\n // Return value is [ref, breakpoint, onLayout] for ergonomics.\n // Keep the public API as [ref, breakpoint] per the spec, but attach onLayout\n // to the ref callback so callers that spread the returned ref also get measurement.\n // Actually — return three values and document properly.\n return Object.assign([ref, breakpoint] as [typeof ref, ContainerBreakpoint], { onLayout });\n}\n\n/**\n * Extended return type that includes an `onLayout` handler for measurement.\n * Prefer this form for explicit usage:\n *\n * @example\n * const { ref, breakpoint, onLayout } = useContainerQueryFull();\n * return <View ref={ref} onLayout={onLayout}>...</View>;\n */\nexport interface ContainerQueryResult {\n ref: (node: View | null) => void;\n breakpoint: ContainerBreakpoint;\n onLayout: (event: LayoutChangeEvent) => void;\n}\n\n/**\n * Object-API variant of `useContainerQuery` — easier to use when you need\n * all three values without destructuring a tuple.\n */\nexport function useContainerQueryFull(): ContainerQueryResult {\n const [breakpoint, setBreakpoint] = useState<ContainerBreakpoint>('xs');\n\n const ref = useCallback((_node: View | null) => {\n // Measurement is driven by onLayout, not the ref callback.\n }, []);\n\n const onLayout = useCallback((event: LayoutChangeEvent) => {\n const { width } = event.nativeEvent.layout;\n setBreakpoint(getContainerBreakpoint(width));\n }, []);\n\n return { ref, breakpoint, onLayout };\n}\n","/**\n * useScrollHandler — scroll-driven animation values via Reanimated.\n *\n * This hook requires `react-native-reanimated` >= 3.6.0 as a peer dependency.\n * It is guarded by a try/catch import so that the rest of the package remains\n * importable when Reanimated is not installed (it will throw a clear error only\n * when useScrollHandler itself is called).\n */\n\n// We import types only here so TypeScript can check the rest of this file even\n// when the peer is absent at runtime. The actual module is loaded lazily.\nimport type {\n SharedValue,\n ScrollHandlerProcessed,\n} from 'react-native-reanimated';\n\nexport interface ScrollHandlerResult {\n /** Pass this to the `onScroll` prop of Reanimated.ScrollView / FlatList. */\n scrollHandler: ScrollHandlerProcessed<Record<string, unknown>>;\n /** Shared value tracking current vertical scroll offset in pixels. */\n scrollY: SharedValue<number>;\n /** Shared value tracking scroll direction — updates when direction changes. */\n scrollDirection: SharedValue<'up' | 'down'>;\n}\n\nlet reanimated: typeof import('react-native-reanimated') | null = null;\n\nfunction loadReanimated(): typeof import('react-native-reanimated') {\n if (reanimated) return reanimated;\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n reanimated = require('react-native-reanimated') as typeof import('react-native-reanimated');\n return reanimated;\n } catch {\n throw new Error(\n '[reactnatively-hooks] useScrollHandler requires react-native-reanimated >= 3.6.0. ' +\n 'Install it with: npx expo install react-native-reanimated',\n );\n }\n}\n\n/**\n * Returns scroll-driven Reanimated shared values and a scroll handler.\n *\n * @example\n * const { scrollHandler, scrollY, scrollDirection } = useScrollHandler();\n * return (\n * <Animated.ScrollView onScroll={scrollHandler} scrollEventThrottle={16}>\n * ...\n * </Animated.ScrollView>\n * );\n */\nexport function useScrollHandler(): ScrollHandlerResult {\n const rn = loadReanimated();\n const { useSharedValue, useAnimatedScrollHandler } = rn;\n\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const scrollY = useSharedValue<number>(0);\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const scrollDirection = useSharedValue<'up' | 'down'>('down');\n\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const scrollHandler = useAnimatedScrollHandler({\n onScroll(event) {\n 'worklet';\n const y = event.contentOffset.y;\n if (y > scrollY.value) {\n scrollDirection.value = 'down';\n } else if (y < scrollY.value) {\n scrollDirection.value = 'up';\n }\n scrollY.value = y;\n },\n });\n\n return { scrollHandler, scrollY, scrollDirection };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reactnatively-hooks",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared UI hooks for the Reactnatively framework",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"reactnatively-theme": "0.1.0",
|
|
21
|
+
"reactnatively-utils": "0.1.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"react": ">=18.0.0",
|
|
25
|
+
"react-native": ">=0.73.0",
|
|
26
|
+
"react-native-reanimated": ">=3.6.0",
|
|
27
|
+
"react-native-safe-area-context": ">=4.0.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"react-native-reanimated": {
|
|
31
|
+
"optional": true
|
|
32
|
+
},
|
|
33
|
+
"react-native-safe-area-context": {
|
|
34
|
+
"optional": true
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"tsup": "^8.2.4",
|
|
39
|
+
"typescript": "^5.5.3",
|
|
40
|
+
"@reactnatively/tsconfig": "0.0.0"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"react-native",
|
|
44
|
+
"expo",
|
|
45
|
+
"hooks",
|
|
46
|
+
"ui",
|
|
47
|
+
"design-system",
|
|
48
|
+
"typescript"
|
|
49
|
+
],
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "https://github.com/hakizimana-fred/reactnatively.git",
|
|
53
|
+
"directory": "packages/hooks"
|
|
54
|
+
},
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsup",
|
|
61
|
+
"dev": "tsup --watch",
|
|
62
|
+
"typecheck": "tsc --noEmit",
|
|
63
|
+
"clean": "rm -rf dist"
|
|
64
|
+
}
|
|
65
|
+
}
|