@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.
- package/lib/commonjs/full-window-overlay/index.js +47 -0
- package/lib/commonjs/full-window-overlay/index.js.map +1 -0
- package/lib/commonjs/index.js +36 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/list-item/index.js.map +1 -1
- package/lib/commonjs/portal/index.js +17 -0
- package/lib/commonjs/portal/index.js.map +1 -0
- package/lib/commonjs/portal/portal.js +110 -0
- package/lib/commonjs/portal/portal.js.map +1 -0
- package/lib/commonjs/slot/index.js +133 -11
- package/lib/commonjs/slot/index.js.map +1 -1
- package/lib/commonjs/toast/ToastRoot.js +41 -0
- package/lib/commonjs/toast/ToastRoot.js.map +1 -0
- package/lib/commonjs/toast/context.js +11 -0
- package/lib/commonjs/toast/context.js.map +1 -0
- package/lib/commonjs/toast/index.js +26 -0
- package/lib/commonjs/toast/index.js.map +1 -0
- package/lib/commonjs/toast/types.js +6 -0
- package/lib/commonjs/toast/types.js.map +1 -0
- package/lib/module/full-window-overlay/index.js +43 -0
- package/lib/module/full-window-overlay/index.js.map +1 -0
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/list-item/index.js.map +1 -1
- package/lib/module/portal/index.js +4 -0
- package/lib/module/portal/index.js.map +1 -0
- package/lib/module/portal/portal.js +103 -0
- package/lib/module/portal/portal.js.map +1 -0
- package/lib/module/slot/index.js +132 -1
- package/lib/module/slot/index.js.map +1 -1
- package/lib/module/toast/ToastRoot.js +37 -0
- package/lib/module/toast/ToastRoot.js.map +1 -0
- package/lib/module/toast/context.js +5 -0
- package/lib/module/toast/context.js.map +1 -0
- package/lib/module/toast/index.js +5 -0
- package/lib/module/toast/index.js.map +1 -0
- package/lib/module/toast/types.js +4 -0
- package/lib/module/toast/types.js.map +1 -0
- package/lib/typescript/button/index.d.ts +1 -1
- package/lib/typescript/button/index.d.ts.map +1 -1
- package/lib/typescript/full-window-overlay/index.d.ts +46 -0
- package/lib/typescript/full-window-overlay/index.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +3 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/list-item/index.d.ts +1 -1
- package/lib/typescript/list-item/index.d.ts.map +1 -1
- package/lib/typescript/portal/index.d.ts +2 -0
- package/lib/typescript/portal/index.d.ts.map +1 -0
- package/lib/typescript/portal/portal.d.ts +25 -0
- package/lib/typescript/portal/portal.d.ts.map +1 -0
- package/lib/typescript/slot/index.d.ts +7 -1
- package/lib/typescript/slot/index.d.ts.map +1 -1
- package/lib/typescript/toast/ToastRoot.d.ts +7 -0
- package/lib/typescript/toast/ToastRoot.d.ts.map +1 -0
- package/lib/typescript/toast/context.d.ts +6 -0
- package/lib/typescript/toast/context.d.ts.map +1 -0
- package/lib/typescript/toast/index.d.ts +3 -0
- package/lib/typescript/toast/index.d.ts.map +1 -0
- package/lib/typescript/toast/types.d.ts +16 -0
- package/lib/typescript/toast/types.d.ts.map +1 -0
- package/package.json +2 -3
- package/src/button/index.ts +1 -1
- package/src/full-window-overlay/index.tsx +65 -0
- package/src/index.ts +3 -0
- package/src/list-item/index.ts +0 -1
- package/src/portal/index.ts +1 -0
- package/src/portal/portal.tsx +116 -0
- package/src/slot/index.tsx +162 -0
- package/src/toast/ToastRoot.tsx +39 -0
- package/src/toast/context.tsx +4 -0
- package/src/toast/index.ts +2 -0
- package/src/toast/types.ts +17 -0
- 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';
|
package/src/list-item/index.ts
CHANGED
|
@@ -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,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';
|