@cdx-ui/primitives 0.0.1-beta.79 → 0.0.1-beta.80

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 (73) hide show
  1. package/lib/commonjs/full-window-overlay/index.js +47 -0
  2. package/lib/commonjs/full-window-overlay/index.js.map +1 -0
  3. package/lib/commonjs/index.js +36 -0
  4. package/lib/commonjs/index.js.map +1 -1
  5. package/lib/commonjs/list-item/index.js.map +1 -1
  6. package/lib/commonjs/portal/index.js +17 -0
  7. package/lib/commonjs/portal/index.js.map +1 -0
  8. package/lib/commonjs/portal/portal.js +110 -0
  9. package/lib/commonjs/portal/portal.js.map +1 -0
  10. package/lib/commonjs/slot/index.js +133 -11
  11. package/lib/commonjs/slot/index.js.map +1 -1
  12. package/lib/commonjs/toast/ToastRoot.js +41 -0
  13. package/lib/commonjs/toast/ToastRoot.js.map +1 -0
  14. package/lib/commonjs/toast/context.js +11 -0
  15. package/lib/commonjs/toast/context.js.map +1 -0
  16. package/lib/commonjs/toast/index.js +26 -0
  17. package/lib/commonjs/toast/index.js.map +1 -0
  18. package/lib/commonjs/toast/types.js +6 -0
  19. package/lib/commonjs/toast/types.js.map +1 -0
  20. package/lib/module/full-window-overlay/index.js +43 -0
  21. package/lib/module/full-window-overlay/index.js.map +1 -0
  22. package/lib/module/index.js +3 -0
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/module/list-item/index.js.map +1 -1
  25. package/lib/module/portal/index.js +4 -0
  26. package/lib/module/portal/index.js.map +1 -0
  27. package/lib/module/portal/portal.js +103 -0
  28. package/lib/module/portal/portal.js.map +1 -0
  29. package/lib/module/slot/index.js +132 -1
  30. package/lib/module/slot/index.js.map +1 -1
  31. package/lib/module/toast/ToastRoot.js +37 -0
  32. package/lib/module/toast/ToastRoot.js.map +1 -0
  33. package/lib/module/toast/context.js +5 -0
  34. package/lib/module/toast/context.js.map +1 -0
  35. package/lib/module/toast/index.js +5 -0
  36. package/lib/module/toast/index.js.map +1 -0
  37. package/lib/module/toast/types.js +4 -0
  38. package/lib/module/toast/types.js.map +1 -0
  39. package/lib/typescript/button/index.d.ts +1 -1
  40. package/lib/typescript/button/index.d.ts.map +1 -1
  41. package/lib/typescript/full-window-overlay/index.d.ts +46 -0
  42. package/lib/typescript/full-window-overlay/index.d.ts.map +1 -0
  43. package/lib/typescript/index.d.ts +3 -0
  44. package/lib/typescript/index.d.ts.map +1 -1
  45. package/lib/typescript/list-item/index.d.ts +1 -1
  46. package/lib/typescript/list-item/index.d.ts.map +1 -1
  47. package/lib/typescript/portal/index.d.ts +2 -0
  48. package/lib/typescript/portal/index.d.ts.map +1 -0
  49. package/lib/typescript/portal/portal.d.ts +25 -0
  50. package/lib/typescript/portal/portal.d.ts.map +1 -0
  51. package/lib/typescript/slot/index.d.ts +7 -1
  52. package/lib/typescript/slot/index.d.ts.map +1 -1
  53. package/lib/typescript/toast/ToastRoot.d.ts +7 -0
  54. package/lib/typescript/toast/ToastRoot.d.ts.map +1 -0
  55. package/lib/typescript/toast/context.d.ts +6 -0
  56. package/lib/typescript/toast/context.d.ts.map +1 -0
  57. package/lib/typescript/toast/index.d.ts +3 -0
  58. package/lib/typescript/toast/index.d.ts.map +1 -0
  59. package/lib/typescript/toast/types.d.ts +16 -0
  60. package/lib/typescript/toast/types.d.ts.map +1 -0
  61. package/package.json +2 -3
  62. package/src/button/index.ts +1 -1
  63. package/src/full-window-overlay/index.tsx +65 -0
  64. package/src/index.ts +3 -0
  65. package/src/list-item/index.ts +0 -1
  66. package/src/portal/index.ts +1 -0
  67. package/src/portal/portal.tsx +116 -0
  68. package/src/slot/index.tsx +162 -0
  69. package/src/toast/ToastRoot.tsx +39 -0
  70. package/src/toast/context.tsx +4 -0
  71. package/src/toast/index.ts +2 -0
  72. package/src/toast/types.ts +17 -0
  73. package/src/slot/index.ts +0 -1
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export * from './checkbox';
5
5
  export * from './dialog';
6
6
  export * from './field';
7
7
  export * from './form';
8
+ export * from './full-window-overlay';
8
9
  export * from './input';
9
10
  export * from './otp-input';
10
11
  export * from './link';
@@ -13,6 +14,8 @@ export { type EdgeInsets, OverlayInsetsProvider } from './overlay';
13
14
  export * from './select';
14
15
  export * from './switch';
15
16
  export * from './tile';
17
+ export * from './toast';
18
+ export * from './portal';
16
19
  export * from './progress-bar';
17
20
  export * from './radio';
18
21
  export type { InteractionState } from './types';
@@ -1,7 +1,6 @@
1
1
  export type {
2
2
  IListItemPressablePassthrough,
3
3
  IListItemProps,
4
- InterfaceListItemProps,
5
4
  ListItemCrossAlign,
6
5
  ListItemSize,
7
6
  } from './types';
@@ -0,0 +1 @@
1
+ export * from './portal';
@@ -0,0 +1,116 @@
1
+ import * as React from 'react';
2
+ import { useEffect, useSyncExternalStore } from 'react';
3
+ import { Platform, type View, type ViewStyle } from 'react-native';
4
+
5
+ const DEFAULT_PORTAL_HOST = 'FORGE_UI_DEFAULT_HOST_NAME';
6
+
7
+ type PortalMap = Map<string, React.ReactNode>;
8
+ type PortalHostMap = Map<string, PortalMap>;
9
+
10
+ interface PortalStore {
11
+ map: PortalHostMap;
12
+ listeners: Set<() => void>;
13
+ }
14
+
15
+ const store: PortalStore = {
16
+ map: new Map<string, PortalMap>().set(DEFAULT_PORTAL_HOST, new Map<string, React.ReactNode>()),
17
+ listeners: new Set(),
18
+ };
19
+
20
+ // --------------------------------------------------
21
+
22
+ function emit() {
23
+ for (const cb of store.listeners) cb();
24
+ }
25
+
26
+ function getSnapshot() {
27
+ return store.map;
28
+ }
29
+
30
+ function subscribe(cb: () => void) {
31
+ store.listeners.add(cb);
32
+ return () => {
33
+ store.listeners.delete(cb);
34
+ };
35
+ }
36
+
37
+ function updatePortal(hostName: string, name: string, children: React.ReactNode) {
38
+ const next = new Map(store.map);
39
+ const portal = next.get(hostName) ?? new Map<string, React.ReactNode>();
40
+ portal.set(name, children);
41
+ next.set(hostName, portal);
42
+ store.map = next;
43
+ emit();
44
+ }
45
+
46
+ function removePortal(hostName: string, name: string) {
47
+ const next = new Map(store.map);
48
+ const portal = next.get(hostName) ?? new Map<string, React.ReactNode>();
49
+ portal.delete(name);
50
+ next.set(hostName, portal);
51
+ store.map = next;
52
+ emit();
53
+ }
54
+
55
+ // --------------------------------------------------
56
+
57
+ export function PortalHost({ name = DEFAULT_PORTAL_HOST }: { name?: string }) {
58
+ const map = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
59
+ const portalMap = map.get(name) ?? new Map<string, React.ReactNode>();
60
+ if (portalMap.size === 0) return null;
61
+ return <>{Array.from(portalMap.values())}</>;
62
+ }
63
+
64
+ // --------------------------------------------------
65
+
66
+ export function Portal({
67
+ name,
68
+ hostName = DEFAULT_PORTAL_HOST,
69
+ children,
70
+ }: {
71
+ name: string;
72
+ hostName?: string;
73
+ children: React.ReactNode;
74
+ }) {
75
+ useEffect(() => {
76
+ updatePortal(hostName, name, children);
77
+ return () => {
78
+ removePortal(hostName, name);
79
+ };
80
+ }, [hostName, name, children]);
81
+
82
+ return null;
83
+ }
84
+
85
+ // --------------------------------------------------
86
+
87
+ const ROOT: ViewStyle = {
88
+ flex: 1,
89
+ };
90
+
91
+ /**
92
+ * @deprecated use `FullWindowOverlay` from `react-native-screens` instead
93
+ * @example
94
+ import { FullWindowOverlay } from "react-native-screens"
95
+ const WindowOverlay = Platform.OS === "ios" ? FullWindowOverlay : Fragment
96
+ // Wrap the `<PortalHost/>` with `<WindowOverlay/>`
97
+ <WindowOverlay><PortalHost/></WindowOverlay>
98
+ */
99
+ export function useModalPortalRoot() {
100
+ const ref = React.useRef<View>(null);
101
+ const [offset, setSideOffSet] = React.useState(0);
102
+
103
+ const onLayout = React.useCallback(() => {
104
+ if (Platform.OS === 'web') return;
105
+ ref.current?.measure((_x, _y, _width, _height, _pageX, pageY) => {
106
+ setSideOffSet(-pageY);
107
+ });
108
+ }, []);
109
+
110
+ return {
111
+ ref,
112
+ offset,
113
+ onLayout,
114
+ style: ROOT,
115
+ };
116
+ }
@@ -0,0 +1,162 @@
1
+ /* https://rn-primitives.vercel.app/slot */
2
+
3
+ import * as React from 'react';
4
+ import {
5
+ type PressableStateCallbackType,
6
+ type ImageStyle as RNImageStyle,
7
+ type PressableProps as RNPressableProps,
8
+ type StyleProp,
9
+ } from 'react-native';
10
+
11
+ export function Slot<T extends React.ElementType>(props: React.ComponentPropsWithRef<T>) {
12
+ const { children, ref: forwardedRef, ...restOfProps } = props;
13
+
14
+ if (!React.isValidElement(children)) {
15
+ console.log('Slot - Invalid asChild element', children);
16
+ return null;
17
+ }
18
+
19
+ if (isTextChildren(children)) {
20
+ console.log('Slot - Text children are not supported', children);
21
+ return null;
22
+ }
23
+
24
+ const childrenProps = (children.props as Record<string, any>) ?? {};
25
+
26
+ if (children.type === React.Fragment) {
27
+ return (
28
+ <>
29
+ {React.Children.toArray(childrenProps.children).map((child): any =>
30
+ React.isValidElement(child)
31
+ ? Slot({ ...restOfProps, ref: forwardedRef, children: child })
32
+ : child,
33
+ )}
34
+ </>
35
+ );
36
+ }
37
+
38
+ const { ref: childRef, ...childProps } = childrenProps;
39
+
40
+ return React.cloneElement(children, {
41
+ ...mergeProps(restOfProps, childProps),
42
+ ref: forwardedRef ? composeRefs(forwardedRef, childRef) : childRef,
43
+ } as unknown as Partial<React.ComponentPropsWithRef<T>>);
44
+ }
45
+
46
+ Slot.displayName = 'Slot';
47
+
48
+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
49
+ function setRef<T>(ref: React.Ref<T> | undefined, value: T | null): (() => void) | void {
50
+ if (typeof ref === 'function') {
51
+ const cleanup = ref(value);
52
+
53
+ if (typeof cleanup === 'function') {
54
+ return cleanup;
55
+ }
56
+
57
+ return;
58
+ }
59
+
60
+ if (ref != null) {
61
+ ref.current = value;
62
+ return () => {
63
+ ref.current = null;
64
+ };
65
+ }
66
+ }
67
+
68
+ function composeRefs<T>(...refs: (React.Ref<T> | undefined)[]): React.RefCallback<T> {
69
+ let cleanups: (() => void)[] = [];
70
+
71
+ return (node) => {
72
+ cleanups.forEach((cleanup) => cleanup());
73
+ cleanups = [];
74
+
75
+ if (node == null) {
76
+ refs.forEach((ref) => {
77
+ if (typeof ref === 'function') {
78
+ ref(null);
79
+ } else if (ref != null) {
80
+ ref.current = null;
81
+ }
82
+ });
83
+ return;
84
+ }
85
+
86
+ cleanups = refs
87
+ .map((ref) => setRef(ref, node))
88
+ .filter((cleanup): cleanup is () => void => cleanup != null);
89
+ };
90
+ }
91
+
92
+ type AnyProps = Record<string, any>;
93
+
94
+ // TODO: Compare with mergeProps from @react-aria/utils
95
+ function mergeProps(slotProps: AnyProps, childProps: AnyProps) {
96
+ // all child props should override
97
+ const overrideProps = { ...childProps };
98
+
99
+ for (const propName in childProps) {
100
+ const slotPropValue = slotProps[propName];
101
+ const childPropValue = childProps[propName];
102
+
103
+ const isHandler = /^on[A-Z]/.test(propName);
104
+ if (isHandler) {
105
+ // if the handler exists on both, we compose them
106
+ if (slotPropValue && childPropValue) {
107
+ overrideProps[propName] = (...args: unknown[]) => {
108
+ childPropValue(...args);
109
+ slotPropValue(...args);
110
+ };
111
+ }
112
+ // but if it exists only on the slot, we use only this one
113
+ else if (slotPropValue) {
114
+ overrideProps[propName] = slotPropValue;
115
+ }
116
+ }
117
+ // if it's `style`, we merge them
118
+ else if (propName === 'style') {
119
+ overrideProps[propName] = combineStyles(slotPropValue, childPropValue);
120
+ } else if (propName === 'className') {
121
+ overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(' ');
122
+ }
123
+ }
124
+
125
+ return { ...slotProps, ...overrideProps };
126
+ }
127
+
128
+ type PressableStyle = RNPressableProps['style'];
129
+ type ImageStyle = StyleProp<RNImageStyle>;
130
+ type Style = PressableStyle | ImageStyle;
131
+
132
+ function combineStyles(slotStyle?: Style, childValue?: Style) {
133
+ if (typeof slotStyle === 'function' && typeof childValue === 'function') {
134
+ return (state: PressableStateCallbackType) => {
135
+ return [slotStyle(state), childValue(state)];
136
+ };
137
+ }
138
+ if (typeof slotStyle === 'function') {
139
+ return (state: PressableStateCallbackType) => {
140
+ return childValue ? [slotStyle(state), childValue] : slotStyle(state);
141
+ };
142
+ }
143
+ if (typeof childValue === 'function') {
144
+ return (state: PressableStateCallbackType) => {
145
+ return slotStyle ? [slotStyle, childValue(state)] : childValue(state);
146
+ };
147
+ }
148
+
149
+ if (slotStyle && childValue) {
150
+ return [slotStyle, childValue];
151
+ }
152
+
153
+ return slotStyle ?? childValue;
154
+ }
155
+
156
+ export function isTextChildren(
157
+ children: React.ReactNode | ((state: PressableStateCallbackType) => React.ReactNode),
158
+ ) {
159
+ return Array.isArray(children)
160
+ ? children.every((child) => typeof child === 'string')
161
+ : typeof children === 'string';
162
+ }
@@ -0,0 +1,39 @@
1
+ import { forwardRef, useId } from 'react';
2
+ import { Platform, View } from 'react-native';
3
+ import { Slot } from '../slot';
4
+ import { ToastProvider } from './context';
5
+ import type { IToastProps } from './types';
6
+
7
+ export interface ToastRootProps extends IToastProps {
8
+ asChild?: boolean;
9
+ }
10
+
11
+ export const ToastRoot = forwardRef<View, ToastRootProps>(
12
+ ({ asChild, id, role = 'status', ...viewProps }, ref) => {
13
+ const generatedId = useId();
14
+ const nativeID = id || generatedId;
15
+
16
+ const Comp = asChild ? Slot : View;
17
+
18
+ return (
19
+ <ToastProvider value={{ nativeID }}>
20
+ <Comp
21
+ ref={ref}
22
+ role={role}
23
+ {...(Platform.OS !== 'web' && {
24
+ 'aria-live': role === 'alert' ? 'assertive' : 'polite',
25
+ 'aria-atomic': true,
26
+ })}
27
+ {...(Platform.OS === 'web' &&
28
+ role === 'alert' && {
29
+ 'aria-live': 'assertive',
30
+ })}
31
+ nativeID={nativeID}
32
+ {...viewProps}
33
+ />
34
+ </ToastProvider>
35
+ );
36
+ },
37
+ );
38
+
39
+ ToastRoot.displayName = 'ToastRoot';
@@ -0,0 +1,4 @@
1
+ import { createContext } from '@cdx-ui/utils';
2
+ import type { IToastContextValue } from './types';
3
+
4
+ export const [ToastProvider, useToastContext] = createContext<IToastContextValue>('ToastContext');
@@ -0,0 +1,2 @@
1
+ export { ToastProvider, useToastContext } from './context';
2
+ export { ToastRoot, type ToastRootProps } from './ToastRoot';
@@ -0,0 +1,17 @@
1
+ import type { ViewProps } from 'react-native';
2
+
3
+ export interface IToastContextValue {
4
+ /** Unique identifier for the toast */
5
+ nativeID: string;
6
+ }
7
+
8
+ export interface IToastProps extends ViewProps {
9
+ /** Unique identifier for the toast */
10
+ id?: string;
11
+ /**
12
+ * ARIA role for the toast element.
13
+ * Use `'alert'` for urgent/danger toasts that require assertive announcements.
14
+ * @default 'status'
15
+ */
16
+ role?: 'status' | 'alert';
17
+ }
package/src/slot/index.ts DELETED
@@ -1 +0,0 @@
1
- export { Slot, isTextChildren } from '@rn-primitives/slot';