Skip to content

Commit 5eba970

Browse files
committed
feat: add primitives module primitive subpkg
1 parent cb10f34 commit 5eba970

File tree

5 files changed

+186
-0
lines changed

5 files changed

+186
-0
lines changed

primitives/primitive/package.json

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{
2+
"name": "soybean-react-ui/primitive",
3+
"version": "1.0.0",
4+
"license": "MIT",
5+
"homepage": "https://radix-ui.com/primitives",
6+
"repository": {
7+
"type": "git",
8+
"url": "git+https://github.com/radix-ui/primitives.git"
9+
},
10+
"bugs": {
11+
"url": "https://github.com/radix-ui/primitives/issues"
12+
},
13+
"publishConfig": {
14+
"main": "./dist/index.js",
15+
"module": "./dist/index.mjs",
16+
"types": "./dist/index.d.ts",
17+
"exports": {
18+
".": {
19+
"import": {
20+
"types": "./dist/index.d.mts",
21+
"default": "./dist/index.mjs"
22+
},
23+
"require": {
24+
"types": "./dist/index.d.ts",
25+
"default": "./dist/index.js"
26+
}
27+
}
28+
}
29+
},
30+
"sideEffects": false,
31+
"main": "./src/index.ts",
32+
"module": "./src/index.ts",
33+
"files": ["dist", "README.md"],
34+
"scripts": {
35+
"build": "radix-build",
36+
"clean": "rm -rf dist",
37+
"lint": "eslint --max-warnings 0 src",
38+
"typecheck": "tsc --noEmit"
39+
},
40+
"peerDependencies": {
41+
"@types/react": "*",
42+
"@types/react-dom": "*",
43+
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
44+
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
45+
},
46+
"peerDependenciesMeta": {
47+
"@types/react": {
48+
"optional": true
49+
},
50+
"@types/react-dom": {
51+
"optional": true
52+
}
53+
},
54+
"dependencies": {
55+
"soybean-react-ui/slot": "workspace:*"
56+
},
57+
"devDependencies": {
58+
"@types/react": "19.0.7",
59+
"@types/react-dom": "19.0.3",
60+
"eslint": "9.18.0",
61+
"react": "19.1.0",
62+
"react-dom": "19.1.0",
63+
"typescript": "5.7.3"
64+
},
65+
"source": "./src/index.ts"
66+
}

primitives/primitive/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export {
2+
Primitive,
3+
//
4+
Root
5+
} from './primitive';
6+
export type { NODES, PrimitivePropsWithRef } from './primitive';
7+
8+
export { composeEventHandlers, dispatchDiscreteCustomEvent } from './util';
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as React from 'react';
2+
import { createSlot } from 'soybean-react-ui/slot';
3+
4+
export const NODES = [
5+
'a',
6+
'button',
7+
'div',
8+
'form',
9+
'h2',
10+
'h3',
11+
'img',
12+
'input',
13+
'label',
14+
'li',
15+
'nav',
16+
'ol',
17+
'p',
18+
'select',
19+
'span',
20+
'svg',
21+
'ul'
22+
] as const;
23+
24+
type Primitives = { [E in (typeof NODES)[number]]: PrimitiveForwardRefComponent<E> };
25+
26+
type PrimitivePropsWithRef<E extends React.ElementType> = React.ComponentPropsWithRef<E> & {
27+
asChild?: boolean;
28+
};
29+
30+
type PrimitiveForwardRefComponent<E extends React.ElementType> = React.ForwardRefExoticComponent<
31+
PrimitivePropsWithRef<E>
32+
>;
33+
34+
const Primitive = NODES.reduce((primitive, node) => {
35+
const Slot = createSlot(`Primitive.${node}`);
36+
const Node = React.forwardRef((props: PrimitivePropsWithRef<typeof node>, forwardedRef: any) => {
37+
const { asChild, ...primitiveProps } = props;
38+
39+
const Comp: any = asChild ? Slot : node;
40+
41+
if (typeof window !== 'undefined') {
42+
(window as any)[Symbol.for('soybean-ui')] = true;
43+
}
44+
45+
return (
46+
<Comp
47+
{...primitiveProps}
48+
ref={forwardedRef}
49+
/>
50+
);
51+
});
52+
53+
Node.displayName = `Primitive.${node}`;
54+
55+
return { ...primitive, [node]: Node };
56+
}, {} as Primitives);
57+
58+
const Root = Primitive;
59+
60+
export {
61+
Primitive,
62+
//
63+
Root
64+
};
65+
export type { PrimitivePropsWithRef };

primitives/primitive/src/util.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { flushSync } from 'react-dom';
2+
3+
export function dispatchDiscreteCustomEvent<E extends CustomEvent>(target: E['target'], event: E) {
4+
if (target) flushSync(() => target.dispatchEvent(event));
5+
}
6+
7+
/**
8+
* 组合两个事件处理函数(用户的和内部的),并根据事件是否被 `preventDefault()` 决定是否执行第二个处理函数。
9+
*
10+
* Compose two event handlers (original and internal), with optional check for `event.defaultPrevented`.
11+
* Useful in component libraries to merge user-provided event handlers with internal logic safely.
12+
*
13+
* @returns 一个新的事件处理函数 / A new composed event handler
14+
*
15+
* @example
16+
* const handleClick = composeEventHandlers(
17+
* (e) => console.log('User clicked'),
18+
* (e) => console.log('Internal logic'),
19+
* );
20+
*
21+
* <button onClick={handleClick}>Click me</button>
22+
*/
23+
export function composeEventHandlers<E extends { defaultPrevented: boolean }>(
24+
originalEventHandler?: (event: E) => void,
25+
ourEventHandler?: (event: E) => void,
26+
{ checkForDefaultPrevented = true } = {}
27+
) {
28+
return function handleEvent(event: E) {
29+
// 执行用户传入的处理函数 / Invoke user handler first
30+
originalEventHandler?.(event);
31+
32+
// 如果没有被 preventDefault,则执行内部处理器 / Only run internal handler if event was not prevented
33+
if (checkForDefaultPrevented === false || !event.defaultPrevented) {
34+
return ourEventHandler?.(event);
35+
}
36+
37+
return null;
38+
};
39+
}

primitives/primitive/tsconfig.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "../../tsconfig.base.json",
3+
"compilerOptions": {
4+
"outDir": "dist"
5+
},
6+
"include": ["src"],
7+
"exclude": ["node_modules", "dist"]
8+
}

0 commit comments

Comments
 (0)