react-native-molecules 0.5.0-beta.1 → 0.5.0-beta.11

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.
Files changed (165) hide show
  1. package/README.md +87 -0
  2. package/components/Accordion/index.tsx +1 -6
  3. package/components/Accordion/utils.ts +17 -14
  4. package/components/ActivityIndicator/ActivityIndicator.tsx +12 -20
  5. package/components/ActivityIndicator/index.tsx +1 -5
  6. package/components/Appbar/index.tsx +1 -4
  7. package/components/Appbar/utils.ts +33 -21
  8. package/components/Avatar/index.tsx +1 -5
  9. package/components/Avatar/utils.ts +2 -6
  10. package/components/Backdrop/Backdrop.tsx +2 -2
  11. package/components/Backdrop/index.tsx +1 -5
  12. package/components/Backdrop/utils.ts +5 -6
  13. package/components/Badge/index.tsx +1 -5
  14. package/components/Badge/utils.ts +2 -6
  15. package/components/Button/Button.tsx +211 -264
  16. package/components/Button/index.tsx +9 -7
  17. package/components/Button/types.ts +16 -2
  18. package/components/Button/utils.ts +231 -210
  19. package/components/Card/Card.tsx +8 -4
  20. package/components/Card/CardContent.tsx +5 -4
  21. package/components/Card/CardHeader.tsx +5 -3
  22. package/components/Card/CardMedia.tsx +5 -3
  23. package/components/Card/CardTypography.tsx +5 -3
  24. package/components/Card/index.tsx +1 -5
  25. package/components/Card/utils.ts +5 -6
  26. package/components/Checkbox/Checkbox.tsx +1 -0
  27. package/components/Checkbox/CheckboxBase.ios.tsx +1 -0
  28. package/components/Checkbox/CheckboxBase.tsx +24 -128
  29. package/components/Checkbox/index.tsx +1 -5
  30. package/components/Checkbox/utils.ts +6 -31
  31. package/components/Chip/Chip.tsx +40 -52
  32. package/components/Chip/index.tsx +1 -5
  33. package/components/Chip/utils.ts +5 -13
  34. package/components/DatePickerDocked/index.tsx +1 -5
  35. package/components/DatePickerDocked/utils.ts +21 -19
  36. package/components/DatePickerInline/index.tsx +1 -5
  37. package/components/DatePickerInline/utils.ts +41 -28
  38. package/components/DatePickerInput/DatePickerInput.tsx +4 -2
  39. package/components/DatePickerInput/DatePickerInputWithoutModal.tsx +0 -4
  40. package/components/DatePickerInput/index.tsx +1 -5
  41. package/components/DatePickerInput/types.ts +1 -3
  42. package/components/DatePickerInput/utils.ts +5 -6
  43. package/components/DatePickerModal/CalendarEdit.tsx +10 -9
  44. package/components/DatePickerModal/DatePickerModalHeader.tsx +1 -1
  45. package/components/DatePickerModal/index.tsx +1 -5
  46. package/components/DatePickerModal/utils.ts +17 -16
  47. package/components/DateTimePicker/index.tsx +1 -5
  48. package/components/DateTimePicker/utils.ts +5 -6
  49. package/components/Dialog/index.tsx +1 -5
  50. package/components/Dialog/utils.ts +22 -16
  51. package/components/Drawer/Collapsible/utils.ts +13 -13
  52. package/components/Drawer/Drawer.tsx +2 -3
  53. package/components/Drawer/DrawerContent.tsx +5 -3
  54. package/components/Drawer/DrawerFooter.tsx +5 -4
  55. package/components/Drawer/DrawerHeader.tsx +5 -4
  56. package/components/Drawer/DrawerItem.tsx +5 -3
  57. package/components/Drawer/DrawerItemGroup.tsx +5 -4
  58. package/components/Drawer/index.tsx +1 -5
  59. package/components/Drawer/utils.ts +7 -7
  60. package/components/ElementGroup/ElementGroup.tsx +16 -14
  61. package/components/ElementGroup/index.tsx +1 -5
  62. package/components/ElementGroup/utils.ts +5 -6
  63. package/components/FAB/index.tsx +1 -5
  64. package/components/FAB/utils.ts +2 -6
  65. package/components/FilePicker/FilePicker.tsx +47 -76
  66. package/components/FilePicker/index.tsx +1 -5
  67. package/components/FilePicker/utils.ts +5 -6
  68. package/components/HelperText/HelperText.tsx +0 -35
  69. package/components/HelperText/index.tsx +1 -5
  70. package/components/HelperText/utils.ts +5 -7
  71. package/components/HorizontalDivider/HorizontalDivider.tsx +5 -3
  72. package/components/HorizontalDivider/index.tsx +1 -5
  73. package/components/Icon/CrossFadeIcon.tsx +3 -5
  74. package/components/Icon/Icon.tsx +2 -4
  75. package/components/Icon/iconFactory.tsx +3 -3
  76. package/components/Icon/index.tsx +2 -6
  77. package/components/Icon/types.ts +17 -6
  78. package/components/IconButton/IconButton.tsx +45 -58
  79. package/components/IconButton/index.tsx +1 -5
  80. package/components/IconButton/utils.ts +153 -49
  81. package/components/If/index.tsx +1 -5
  82. package/components/InputAddon/index.tsx +1 -5
  83. package/components/InputAddon/utils.ts +5 -6
  84. package/components/Link/index.tsx +1 -5
  85. package/components/Link/utils.ts +2 -6
  86. package/components/ListItem/index.tsx +1 -5
  87. package/components/ListItem/utils.ts +13 -11
  88. package/components/LoadingIndicator/LoadingIndicator.tsx +253 -0
  89. package/components/LoadingIndicator/LoadingIndicator.web.tsx +136 -0
  90. package/components/LoadingIndicator/index.tsx +13 -0
  91. package/components/LoadingIndicator/utils.ts +117 -0
  92. package/components/Menu/index.tsx +1 -5
  93. package/components/Menu/utils.ts +6 -8
  94. package/components/Modal/index.tsx +1 -5
  95. package/components/Modal/utils.ts +2 -6
  96. package/components/NavigationRail/NavigationRailHeader.tsx +1 -1
  97. package/components/NavigationRail/index.tsx +1 -5
  98. package/components/NavigationRail/utils.ts +21 -17
  99. package/components/NavigationStack/index.tsx +1 -5
  100. package/components/NavigationStack/utils.tsx +7 -1
  101. package/components/Portal/index.tsx +1 -5
  102. package/components/RadioButton/index.ts +1 -5
  103. package/components/RadioButton/utils.ts +9 -8
  104. package/components/Rating/index.tsx +1 -5
  105. package/components/Rating/utils.ts +6 -8
  106. package/components/Select/Select.tsx +369 -507
  107. package/components/Select/index.ts +7 -14
  108. package/components/Select/types.ts +2 -4
  109. package/components/Select/utils.ts +215 -0
  110. package/components/Slot/Slot.tsx +244 -0
  111. package/components/Slot/compose-refs.tsx +60 -0
  112. package/components/Slot/index.tsx +8 -0
  113. package/components/StateLayer/index.tsx +1 -5
  114. package/components/StateLayer/utils.ts +5 -6
  115. package/components/Surface/Surface.android.tsx +34 -8
  116. package/components/Surface/Surface.ios.tsx +36 -29
  117. package/components/Surface/Surface.tsx +31 -4
  118. package/components/Surface/index.tsx +1 -5
  119. package/components/Surface/utils.ts +49 -36
  120. package/components/Switch/Switch.tsx +8 -2
  121. package/components/Switch/index.tsx +1 -5
  122. package/components/Switch/utils.ts +2 -6
  123. package/components/Tabs/TabItem.tsx +35 -58
  124. package/components/Tabs/TabLabel.tsx +5 -9
  125. package/components/Tabs/Tabs.tsx +154 -149
  126. package/components/Tabs/index.tsx +1 -5
  127. package/components/Tabs/utils.ts +25 -12
  128. package/components/Text/Text.tsx +2 -8
  129. package/components/TextInput/TextInput.tsx +655 -571
  130. package/components/TextInput/index.tsx +19 -7
  131. package/components/TextInput/types.ts +76 -27
  132. package/components/TextInput/utils.ts +232 -159
  133. package/components/TextInputWithMask/index.tsx +1 -5
  134. package/components/TimePicker/AmPmSwitcher.tsx +1 -1
  135. package/components/TimePicker/index.tsx +1 -5
  136. package/components/TimePicker/utils.ts +29 -21
  137. package/components/TimePickerField/TimePickerField.tsx +7 -5
  138. package/components/TimePickerField/index.tsx +1 -5
  139. package/components/TimePickerField/utils.ts +5 -6
  140. package/components/TimePickerModal/TimePickerModal.tsx +6 -2
  141. package/components/TimePickerModal/index.tsx +1 -5
  142. package/components/TimePickerModal/utils.ts +5 -6
  143. package/components/Tooltip/TooltipTrigger.tsx +25 -16
  144. package/components/Tooltip/index.tsx +1 -5
  145. package/components/Tooltip/utils.ts +5 -6
  146. package/components/TouchableRipple/TouchableRipple.native.tsx +50 -14
  147. package/components/TouchableRipple/TouchableRipple.tsx +137 -47
  148. package/components/TouchableRipple/index.tsx +1 -5
  149. package/components/TouchableRipple/utils.ts +5 -6
  150. package/components/VerticalDivider/VerticalDivider.tsx +9 -8
  151. package/components/VerticalDivider/index.tsx +1 -5
  152. package/core/componentsRegistry.ts +31 -19
  153. package/hocs/withPortal.tsx +1 -1
  154. package/hooks/index.tsx +0 -5
  155. package/hooks/useControlledValue.tsx +20 -4
  156. package/hooks/useSubcomponents.tsx +63 -31
  157. package/hooks/useWhatHasUpdated.tsx +48 -0
  158. package/package.json +12 -15
  159. package/shortcuts-manager/ShortcutsManager/ShortcutsManager.tsx +5 -2
  160. package/styles/shadow.ts +2 -1
  161. package/styles/themes/LightTheme.tsx +1 -1
  162. package/utils/extractPropertiesFromStyles.ts +25 -0
  163. package/utils/lodash.ts +77 -6
  164. package/utils/repository.ts +2 -52
  165. package/hooks/useSearchable.tsx +0 -74
@@ -1,4 +1,4 @@
1
- import { Children, forwardRef, memo, type ReactNode, useCallback, useMemo } from 'react';
1
+ import { forwardRef, memo, type ReactNode, useCallback, useMemo, useRef } from 'react';
2
2
  import {
3
3
  type GestureResponderEvent,
4
4
  Pressable,
@@ -9,6 +9,7 @@ import {
9
9
  } from 'react-native';
10
10
  import { StyleSheet } from 'react-native-unistyles';
11
11
 
12
+ import { Slot } from '../Slot';
12
13
  import { touchableRippleStyles } from './utils';
13
14
 
14
15
  export type Props = PressableProps & {
@@ -50,6 +51,28 @@ export type Props = PressableProps & {
50
51
  */
51
52
  children: ReactNode;
52
53
  style?: StyleProp<ViewStyle>;
54
+ /**
55
+ * When `true`, the component will not render a wrapper element. Instead, it will
56
+ * merge its props (styles, event handlers, ref) onto its immediate child element.
57
+ * This follows the Radix UI "Slot" pattern for flexible component composition.
58
+ *
59
+ * @example
60
+ * ```tsx
61
+ * // Without asChild - renders a Pressable wrapper
62
+ * <TouchableRipple onPress={handlePress}>
63
+ * <View><Text>Click me</Text></View>
64
+ * </TouchableRipple>
65
+ *
66
+ * // With asChild - merges props onto the child
67
+ * <TouchableRipple asChild onPress={handlePress}>
68
+ * <Link href="/page"><Text>Navigate</Text></Link>
69
+ * </TouchableRipple>
70
+ * ```
71
+ *
72
+ * @note When `asChild` is `true`, only a single child element is allowed.
73
+ * @default false
74
+ */
75
+ asChild?: boolean;
53
76
  };
54
77
 
55
78
  /**
@@ -96,12 +119,13 @@ const TouchableRipple = (
96
119
  onPressIn: onPressInProp,
97
120
  onPressOut: onPressOutProp,
98
121
  centered,
122
+ asChild = false,
99
123
  ...rest
100
124
  }: Props,
101
125
  ref: any,
102
126
  ) => {
103
127
  // TODO - enable ripple onLongPress, need to check for mobile as well
104
- const disabled = disabledProp || !onPress;
128
+ const disabled = disabledProp;
105
129
 
106
130
  const componentStyles = touchableRippleStyles;
107
131
 
@@ -121,29 +145,52 @@ const TouchableRipple = (
121
145
  };
122
146
  }, [borderless, componentStyles.root, rippleColorProp, style]);
123
147
 
124
- const handlePressIn = useCallback(
148
+ // Track whether pointer is currently down for handling pointer leave
149
+ const isPointerDownRef = useRef(false);
150
+ // Store current target element to clean up ripples on pointer up/leave
151
+ const currentTargetRef = useRef<HTMLElement | null>(null);
152
+
153
+ // Using 'any' for event types to support both React DOM PointerEvent and React Native events
154
+ // This is a web-only file, so we primarily handle DOM pointer events
155
+ const handlePointerDown = useCallback(
125
156
  (e: any) => {
126
- onPressInProp?.(e);
157
+ onPressInProp?.(e as GestureResponderEvent);
127
158
 
128
159
  if (disabled) return;
129
160
 
130
- const button = e.currentTarget;
161
+ isPointerDownRef.current = true;
162
+
163
+ const button = e.currentTarget as HTMLElement;
164
+ currentTargetRef.current = button;
131
165
  const computedStyle = window.getComputedStyle(button);
132
166
  const dimensions = button.getBoundingClientRect();
133
167
 
134
- let touchX;
135
- let touchY;
136
-
137
- const { changedTouches, touches } = e.nativeEvent;
138
- const touch = touches?.[0] ?? changedTouches?.[0];
168
+ let touchX: number;
169
+ let touchY: number;
139
170
 
140
- // If centered or it was pressed using keyboard - enter or space
141
- if (centered || !touch) {
171
+ if (centered) {
172
+ // If centered, always position ripple at center
142
173
  touchX = dimensions.width / 2;
143
174
  touchY = dimensions.height / 2;
175
+ } else if ('clientX' in e && 'clientY' in e) {
176
+ // Web pointer event - calculate position relative to element
177
+ touchX = e.clientX - dimensions.left;
178
+ touchY = e.clientY - dimensions.top;
179
+ } else if (e.nativeEvent) {
180
+ // React Native gesture event
181
+ const { changedTouches, touches } = e.nativeEvent;
182
+ const touch = touches?.[0] ?? changedTouches?.[0];
183
+ if (touch) {
184
+ touchX = touch.locationX ?? dimensions.width / 2;
185
+ touchY = touch.locationY ?? dimensions.height / 2;
186
+ } else {
187
+ touchX = dimensions.width / 2;
188
+ touchY = dimensions.height / 2;
189
+ }
144
190
  } else {
145
- touchX = touch.locationX ?? e.pageX;
146
- touchY = touch.locationY ?? e.pageY;
191
+ // Fallback to center (keyboard activation)
192
+ touchX = dimensions.width / 2;
193
+ touchY = dimensions.height / 2;
147
194
  }
148
195
 
149
196
  // Get the size of the button to determine how big the ripple should be
@@ -156,7 +203,7 @@ const TouchableRipple = (
156
203
  // Create a container for our ripple effect so we don't need to change the parent's style
157
204
  const container = document.createElement('span');
158
205
 
159
- container.setAttribute('data-paper-ripple', '');
206
+ container.setAttribute('data-molecules-ripple', '');
160
207
 
161
208
  Object.assign(container.style, {
162
209
  position: 'absolute',
@@ -217,42 +264,86 @@ const TouchableRipple = (
217
264
  [onPressInProp, disabled, centered, rippleColor],
218
265
  );
219
266
 
220
- const handlePressOut = useCallback(
221
- (e: any) => {
222
- onPressOutProp?.(e);
223
-
224
- if (disabled) return;
225
-
226
- const containers = e.currentTarget.querySelectorAll(
227
- '[data-paper-ripple]',
228
- ) as HTMLElement[];
267
+ const fadeOutRipples = useCallback((target: HTMLElement) => {
268
+ const containers = target.querySelectorAll(
269
+ '[data-molecules-ripple]',
270
+ ) as NodeListOf<HTMLElement>;
229
271
 
272
+ requestAnimationFrame(() => {
230
273
  requestAnimationFrame(() => {
231
- requestAnimationFrame(() => {
232
- containers.forEach(container => {
233
- const ripple = container.firstChild as HTMLSpanElement;
234
-
235
- Object.assign(ripple.style, {
236
- transitionDuration: '250ms',
237
- opacity: 0,
238
- });
239
-
240
- // Finally remove the span after the transition
241
- setTimeout(() => {
242
- const { parentNode } = container;
243
-
244
- if (parentNode) {
245
- parentNode.removeChild(container);
246
- }
247
- }, 500);
274
+ containers.forEach(container => {
275
+ const ripple = container.firstChild as HTMLSpanElement;
276
+
277
+ Object.assign(ripple.style, {
278
+ transitionDuration: '250ms',
279
+ opacity: 0,
248
280
  });
281
+
282
+ // Finally remove the span after the transition
283
+ setTimeout(() => {
284
+ const { parentNode } = container;
285
+
286
+ if (parentNode) {
287
+ parentNode.removeChild(container);
288
+ }
289
+ }, 500);
249
290
  });
250
291
  });
292
+ });
293
+ }, []);
294
+
295
+ const handlePointerUp = useCallback(
296
+ (e: any) => {
297
+ onPressOutProp?.(e as GestureResponderEvent);
298
+
299
+ if (disabled || !isPointerDownRef.current) return;
300
+
301
+ isPointerDownRef.current = false;
302
+ currentTargetRef.current = null;
303
+
304
+ const target = e.currentTarget as HTMLElement;
305
+ fadeOutRipples(target);
251
306
  },
252
- [onPressOutProp, disabled],
307
+ [onPressOutProp, disabled, fadeOutRipples],
253
308
  );
254
309
 
255
- const Component = onPress ? Pressable : View;
310
+ const handlePointerLeave = useCallback(
311
+ (e: any) => {
312
+ // Only fade out if pointer was down (dragging out of element)
313
+ if (disabled || !isPointerDownRef.current) return;
314
+
315
+ isPointerDownRef.current = false;
316
+ currentTargetRef.current = null;
317
+
318
+ const target = e.currentTarget as HTMLElement;
319
+ fadeOutRipples(target);
320
+ },
321
+ [disabled, fadeOutRipples],
322
+ );
323
+
324
+ const handlePointerCancel = useCallback(
325
+ (e: any) => {
326
+ if (disabled || !isPointerDownRef.current) return;
327
+
328
+ isPointerDownRef.current = false;
329
+ currentTargetRef.current = null;
330
+
331
+ const target = e.currentTarget as HTMLElement;
332
+ fadeOutRipples(target);
333
+ },
334
+ [disabled, fadeOutRipples],
335
+ );
336
+
337
+ const Component = asChild ? Slot : onPress ? Pressable : View;
338
+
339
+ // Use pointer events for universal compatibility (works on any HTML element)
340
+ // These events work with mouse, touch, and stylus inputs
341
+ const pointerEventProps = {
342
+ onPointerDown: handlePointerDown,
343
+ onPointerUp: handlePointerUp,
344
+ onPointerLeave: handlePointerLeave,
345
+ onPointerCancel: handlePointerCancel,
346
+ };
256
347
 
257
348
  return (
258
349
  <Component
@@ -261,10 +352,9 @@ const TouchableRipple = (
261
352
  style={containerStyle}
262
353
  ref={ref}
263
354
  onPress={onPress}
264
- onPressIn={handlePressIn}
265
- onPressOut={handlePressOut}
266
- disabled={disabled}>
267
- {Children.only(children)}
355
+ disabled={disabled}
356
+ {...pointerEventProps}>
357
+ {children}
268
358
  </Component>
269
359
  );
270
360
  };
@@ -1,10 +1,6 @@
1
- import { getRegisteredComponentWithFallback, registerMoleculesComponents } from '../../core';
1
+ import { getRegisteredComponentWithFallback } from '../../core';
2
2
  import TouchableRippleDefault from './TouchableRipple';
3
3
 
4
- registerMoleculesComponents({
5
- TouchableRipple: TouchableRippleDefault,
6
- });
7
-
8
4
  export const TouchableRipple = getRegisteredComponentWithFallback(
9
5
  'TouchableRipple',
10
6
  TouchableRippleDefault,
@@ -1,6 +1,6 @@
1
1
  import { StyleSheet } from 'react-native-unistyles';
2
2
 
3
- import { getRegisteredMoleculesComponentStyles, registerComponentsStyles } from '../../core';
3
+ import { getRegisteredComponentStylesWithFallback } from '../../core';
4
4
 
5
5
  const touchableRippleStylesDefault = StyleSheet.create(theme => ({
6
6
  root: {
@@ -8,8 +8,7 @@ const touchableRippleStylesDefault = StyleSheet.create(theme => ({
8
8
  } as any,
9
9
  }));
10
10
 
11
- registerComponentsStyles({
12
- TouchableRipple: touchableRippleStylesDefault,
13
- });
14
-
15
- export const touchableRippleStyles = getRegisteredMoleculesComponentStyles('TouchableRipple');
11
+ export const touchableRippleStyles = getRegisteredComponentStylesWithFallback(
12
+ 'TouchableRipple',
13
+ touchableRippleStylesDefault,
14
+ );
@@ -1,8 +1,8 @@
1
1
  import { memo } from 'react';
2
- import { type StyleProp, View, type ViewProps, type ViewStyle } from 'react-native';
2
+ import { View, type ViewProps } from 'react-native';
3
3
  import { StyleSheet } from 'react-native-unistyles';
4
4
 
5
- import { getRegisteredMoleculesComponentStyles, registerComponentStyles } from '../../core';
5
+ import { getRegisteredComponentStylesWithFallback } from '../../core';
6
6
 
7
7
  export type Props = Omit<ViewProps, 'children'> & {
8
8
  /**
@@ -21,7 +21,6 @@ export type Props = Omit<ViewProps, 'children'> & {
21
21
  * Horizontal spacing of the Divider
22
22
  */
23
23
  spacing?: number;
24
- style?: StyleProp<ViewStyle>;
25
24
  };
26
25
 
27
26
  /**
@@ -70,9 +69,9 @@ const VerticalDivider = ({
70
69
  style={[
71
70
  verticalDividerStyles.root,
72
71
  style,
73
- topInset && { marginTop: topInset },
74
- bottomInset && { marginBottom: bottomInset },
75
- spacing && { marginHorizontal: spacing },
72
+ topInset ? { marginTop: topInset } : undefined,
73
+ bottomInset ? { marginBottom: bottomInset } : undefined,
74
+ spacing ? { marginHorizontal: spacing } : undefined,
76
75
  ]}
77
76
  />
78
77
  );
@@ -93,7 +92,9 @@ export const verticalDividerStylesDefault = StyleSheet.create(theme => ({
93
92
  },
94
93
  }));
95
94
 
96
- registerComponentStyles('VerticalDivider', verticalDividerStylesDefault);
97
- export const verticalDividerStyles = getRegisteredMoleculesComponentStyles('HorizontalDivider');
95
+ export const verticalDividerStyles = getRegisteredComponentStylesWithFallback(
96
+ 'VerticalDivider',
97
+ verticalDividerStylesDefault,
98
+ );
98
99
 
99
100
  export default memo(VerticalDivider);
@@ -1,10 +1,6 @@
1
- import { getRegisteredComponentWithFallback, registerMoleculesComponents } from '../../core';
1
+ import { getRegisteredComponentWithFallback } from '../../core';
2
2
  import VerticalDividerDefault from './VerticalDivider';
3
3
 
4
- registerMoleculesComponents({
5
- VerticalDivider: VerticalDividerDefault,
6
- });
7
-
8
4
  export const VerticalDivider = getRegisteredComponentWithFallback(
9
5
  'VerticalDivider',
10
6
  VerticalDividerDefault,
@@ -1,18 +1,13 @@
1
- import EventEmitter, {
2
- type ConstructorOptions,
3
- type event as Event,
4
- type eventNS,
5
- } from 'eventemitter2';
6
1
  import type { ComponentType } from 'react';
7
2
 
8
- interface RepositoryConstructor<T> extends ConstructorOptions {
3
+ interface RepositoryConstructor<T> {
9
4
  onRegister?: (arg: T, name: string, registery: Record<string, T>) => T;
10
5
  name?: string;
11
6
  }
12
7
 
13
8
  let id = Date.now();
14
9
 
15
- export class Repository<T> extends EventEmitter {
10
+ export class Repository<T> {
16
11
  private registry: Record<string, T> = {};
17
12
  readonly #name!: string;
18
13
 
@@ -29,9 +24,7 @@ export class Repository<T> extends EventEmitter {
29
24
  constructor({
30
25
  onRegister = arg => arg,
31
26
  name = Repository.uniqueId,
32
- ...options
33
27
  }: RepositoryConstructor<T> = {}) {
34
- super(options);
35
28
  this.#onRegister = onRegister;
36
29
  this.#name = name;
37
30
  }
@@ -40,11 +33,6 @@ export class Repository<T> extends EventEmitter {
40
33
  return !!this.registry[itemName];
41
34
  };
42
35
 
43
- emit(event: eventNS | Event, ...values: any[]) {
44
- event = typeof event === 'string' ? `${this.#name}::event` : event;
45
- return super.emit(event, ...values);
46
- }
47
-
48
36
  /**
49
37
  * Register a item with the src.
50
38
  */
@@ -58,8 +46,6 @@ export class Repository<T> extends EventEmitter {
58
46
  ...this.registry,
59
47
  [itemName]: updatedItem,
60
48
  };
61
-
62
- this.emit('item_registered', itemName);
63
49
  };
64
50
 
65
51
  /**
@@ -92,17 +78,14 @@ export class Repository<T> extends EventEmitter {
92
78
 
93
79
  export const componentsRepository = new Repository<Record<string, any>>({
94
80
  name: 'Components_Repository',
95
- maxListeners: 0,
96
81
  });
97
82
 
98
83
  export const componentsStylesRepository = new Repository<Record<string, any>>({
99
84
  name: 'Components_Styles_Repository',
100
- maxListeners: 0,
101
85
  });
102
86
 
103
87
  export const componentsUtilsRepository = new Repository<Record<string, any>>({
104
88
  name: 'Components_Utils_Repository',
105
- maxListeners: 0,
106
89
  });
107
90
 
108
91
  export const registerMoleculesComponent = componentsRepository.registerOne;
@@ -162,3 +145,32 @@ export function getRegisteredComponentWithFallback<T extends ComponentType<any>>
162
145
  ): T {
163
146
  return (getRegisteredMoleculesComponent(name) ?? defaultComponent) as T;
164
147
  }
148
+
149
+ /**
150
+ * Gets a registered component with a fallback to the default component
151
+ * @param name The name of the component to retrieve
152
+ * @param defaultStyles The default styles to use as fallback
153
+ * @returns The registered styles or the default styles
154
+ */
155
+ export function getRegisteredComponentStylesWithFallback<T extends unknown>(
156
+ name: string,
157
+ defaultStyles: T,
158
+ ): T {
159
+ return (getRegisteredMoleculesComponentStyles(name) ?? defaultStyles) as T;
160
+ }
161
+
162
+ /**
163
+ * Gets a registered component with a fallback to the default component
164
+ * @param name The name of the component to retrieve
165
+ * @param defaultUtils The default utils to use as fallback
166
+ * @returns The registered utils or the default utils
167
+ */
168
+ export function getRegisteredComponentUtilsWithFallback<T extends unknown>(
169
+ name: string,
170
+ defaultUtils: T,
171
+ key?: string,
172
+ ): T {
173
+ return key
174
+ ? (((getRegisteredComponentUtils(name) as Record<string, any>)?.[key] ?? defaultUtils) as T)
175
+ : ((getRegisteredComponentUtils(name) ?? defaultUtils) as T);
176
+ }
@@ -6,7 +6,7 @@ const withPortal =
6
6
  <T,>(Component: ComponentType<T>) =>
7
7
  (props: T) => {
8
8
  return (
9
- <Portal name={'withPortal' + (Component.displayName ?? '')}>
9
+ <Portal>
10
10
  {/* @ts-ignore */}
11
11
  <Component {...(props as T)} />
12
12
  </Portal>
package/hooks/index.tsx CHANGED
@@ -18,11 +18,6 @@ export { useMediaQuery } from './useMediaQuery';
18
18
  export { useMergedRefs } from './useMergedRefs';
19
19
  export { default as usePrevious } from './usePrevious';
20
20
  export * from './useQueryFilter';
21
- export {
22
- default as useSearchable,
23
- type UseSearchableProps,
24
- useSearchInputProps,
25
- } from './useSearchable';
26
21
  export { default as useSubcomponents, type UseSubcomponentsProps } from './useSubcomponents';
27
22
  export * from './useTheme';
28
23
  export { default as useToggle } from './useToggle';
@@ -1,5 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
 
3
+ import useLatest from './useLatest';
4
+
3
5
  type ReturnType<T> = [T, (value: T, ...args: any[]) => void];
4
6
 
5
7
  type Args<T> = {
@@ -31,21 +33,35 @@ const useControlledValue = <T,>({
31
33
 
32
34
  const isUncontrolled = useRef(valueProp).current === undefined;
33
35
  const [uncontrolledValue, setValue] = useState(value);
36
+ const valuePropRef = useLatest(valueProp);
37
+ const onChangeRef = useLatest(onChange);
38
+ const manipulateValueRef = useLatest(manipulateValue);
39
+ const uncontrolledValueRef = useLatest(uncontrolledValue);
34
40
 
35
41
  const updateValue = useCallback(
36
42
  (val: T, ...rest: any[]) => {
37
43
  if (disabled) return;
38
44
 
39
45
  if (isUncontrolled) {
40
- setValue(manipulateValue(val, uncontrolledValue));
46
+ setValue(manipulateValueRef.current(val, uncontrolledValueRef.current));
41
47
  }
42
48
 
43
- onChange?.(
44
- manipulateValue(val, isUncontrolled ? uncontrolledValue : valueProp),
49
+ onChangeRef.current?.(
50
+ manipulateValueRef.current(
51
+ val,
52
+ isUncontrolled ? uncontrolledValueRef.current : valuePropRef.current,
53
+ ),
45
54
  ...rest,
46
55
  );
47
56
  },
48
- [disabled, isUncontrolled, manipulateValue, onChange, uncontrolledValue, valueProp],
57
+ [
58
+ disabled,
59
+ isUncontrolled,
60
+ manipulateValueRef,
61
+ onChangeRef,
62
+ uncontrolledValueRef,
63
+ valuePropRef,
64
+ ],
49
65
  );
50
66
 
51
67
  useEffect(() => {
@@ -1,14 +1,23 @@
1
- import type { ReactElement } from 'react';
1
+ import type { ReactElement, ReactNode } from 'react';
2
2
  import { Children, type FC, isValidElement, useMemo } from 'react';
3
3
 
4
4
  export type UseSubcomponentsProps<T extends string> = {
5
- children: ReactElement | ReactElement[];
5
+ children: ReactNode;
6
6
  /**
7
7
  * array of displayName as string
8
8
  * */
9
- allowedChildren: T[];
9
+ allowedChildren: (T | { name: T; allowMultiple?: boolean })[];
10
+ /**
11
+ * If true, also returns the remaining children that don't match any of the allowedChildren
12
+ * in a `rest` property
13
+ */
14
+ includeRest?: boolean;
10
15
  };
11
16
 
17
+ export type UseSubcomponentsResult<T extends string, IncludeRest extends boolean = false> = {
18
+ [key in T]: ReactElement[];
19
+ } & (IncludeRest extends true ? { rest: ReactNode[] } : {});
20
+
12
21
  /**
13
22
  * This will return an object with the displayNames as the property names
14
23
  * eg. allowedChildren: ['Drawer_Header', 'Drawer_Content', 'Drawer_Footer', 'DrawerItem'];
@@ -19,41 +28,64 @@ export type UseSubcomponentsProps<T extends string> = {
19
28
  * Drawer_Footer: [],
20
29
  * DrawerItem: [],
21
30
  * }
31
+ *
32
+ * If includeRest is true, also returns:
33
+ * {
34
+ * ...above,
35
+ * rest: [remaining children that don't match allowedChildren]
36
+ * }
22
37
  * */
23
- const useSubcomponents = <T extends string = string>({
38
+ function useSubcomponents<T extends string = string, IncludeRest extends boolean = false>({
24
39
  children,
25
40
  allowedChildren,
26
- }: UseSubcomponentsProps<T>) => {
41
+ includeRest,
42
+ }: UseSubcomponentsProps<T> & { includeRest?: IncludeRest }): UseSubcomponentsResult<
43
+ T,
44
+ IncludeRest
45
+ > {
27
46
  return useMemo(() => {
28
- // this will create properties with default empty array values even if they don't exist in the children
29
- const defaultContext = allowedChildren.reduce((context, childName) => {
30
- return {
31
- ...context,
32
- [childName]: [],
33
- };
34
- }, {}) as {
35
- [key in T]: ReactElement[];
36
- };
37
-
38
- return Children.map(children, child => child).reduce((context, child) => {
39
- if (!isValidElement(child)) return context;
40
-
41
- if (
42
- !allowedChildren.find(name => name === ((child.type as FC).displayName as string))
43
- ) {
44
- return context;
47
+ const configs = allowedChildren.map(entry =>
48
+ typeof entry === 'string'
49
+ ? { name: entry, allowMultiple: true as boolean }
50
+ : { name: entry.name, allowMultiple: entry.allowMultiple ?? true },
51
+ );
52
+
53
+ const nameSet = new Set(configs.map(c => c.name));
54
+ const allowMultipleMap = new Map(configs.map(c => [c.name, c.allowMultiple]));
55
+
56
+ const result = configs.reduce((acc, { name }) => {
57
+ (acc as any)[name] = [];
58
+ return acc;
59
+ }, (includeRest ? { rest: [] } : {}) as UseSubcomponentsResult<T, IncludeRest>);
60
+
61
+ Children.forEach(children, child => {
62
+ if (!isValidElement(child)) {
63
+ if (includeRest) {
64
+ (result as any).rest.push(child);
65
+ }
66
+ return;
45
67
  }
46
68
 
47
- const name = (child.type as FC).displayName as T;
69
+ const displayName = (child.type as FC)?.displayName as T | undefined;
48
70
 
49
- if (!name) return context;
71
+ if (displayName && nameSet.has(displayName)) {
72
+ if (allowMultipleMap.get(displayName)) {
73
+ (result as any)[displayName].push(child);
74
+ } else {
75
+ // Only keep the last matching child
76
+ (result as any)[displayName] = [child];
77
+ }
50
78
 
51
- return {
52
- ...context,
53
- [name]: [...context[name], child],
54
- };
55
- }, defaultContext);
56
- }, [allowedChildren, children]);
57
- };
79
+ return;
80
+ }
81
+
82
+ if (includeRest) {
83
+ (result as any).rest.push(child);
84
+ }
85
+ });
86
+
87
+ return result;
88
+ }, [allowedChildren, children, includeRest]);
89
+ }
58
90
 
59
91
  export default useSubcomponents;