{rest.children}
diff --git a/packages/nutui-solid/src/components/grid/grid.tsx b/packages/nutui-solid/src/components/grid/grid.tsx
index 6b026db..4d38796 100644
--- a/packages/nutui-solid/src/components/grid/grid.tsx
+++ b/packages/nutui-solid/src/components/grid/grid.tsx
@@ -1,6 +1,6 @@
import { Component, JSX, ParentProps, createMemo, mergeProps, splitProps } from 'solid-js'
import { GridContextProvider } from './grid.context'
-import { pxCheck } from '@/utils/pxCheck'
+import { pxCheck } from '@/utils/px-check'
export type GridDirection = 'horizontal' | 'vertical'
@@ -13,7 +13,6 @@ export type GridLocalProps = Partial<{
reverse: boolean
direction: GridDirection
clickable: boolean
- onClickItem: (index: number) => void
}>
export type GridProps = JSX.HTMLAttributes
& GridLocalProps
@@ -40,7 +39,6 @@ export const Grid: Component> = (props) => {
'reverse',
'direction',
'clickable',
- 'onClickItem',
])
const classes = createMemo(() => {
@@ -71,7 +69,6 @@ export const Grid: Component> = (props) => {
reverse={local.reverse}
direction={local.direction}
clickable={local.clickable}
- onClickItem={local.onClickItem}
>
{rest.children}
diff --git a/packages/nutui-solid/src/components/griditem/grid-item.taro.tsx b/packages/nutui-solid/src/components/griditem/grid-item.taro.tsx
index aabeb88..0a737a3 100644
--- a/packages/nutui-solid/src/components/griditem/grid-item.taro.tsx
+++ b/packages/nutui-solid/src/components/griditem/grid-item.taro.tsx
@@ -1,7 +1,6 @@
import { Component, JSX, ParentProps, createMemo, mergeProps, splitProps } from 'solid-js'
import { useGridContext } from '../grid/grid.context'
-import { useGridItemContext } from '../grid/grid.item.context'
-import { pxCheck } from '@/utils/pxCheck'
+import { pxCheck } from '@/utils/px-check'
export type GridItemProps = JSX.HTMLAttributes
& Partial<{
text: string
@@ -23,7 +22,6 @@ const defaultProps = {
export const GridItem: Component> = (props) => {
const merged = mergeProps(defaultProps, props)
const parent = useGridContext()
- const child = useGridItemContext()
const [local, rest] = splitProps(merged, [
'text',
@@ -40,9 +38,6 @@ export const GridItem: Component> = (props) => {
}
else if (parent.gutter) {
style['padding-right'] = pxCheck(parent.gutter)
- if (child.index >= +parent.columnNum) {
- style['margin-top'] = pxCheck(parent.gutter)
- }
}
return style
})
@@ -65,9 +60,6 @@ export const GridItem: Component> = (props) => {
if (typeof rest?.onClick === 'function') {
rest.onClick(e)
}
- if (parent?.onClickItem) {
- parent?.onClickItem(child.index)
- }
if (props.url) {
props.replace ? location.replace(props.url) : (location.href = props.url)
}
diff --git a/packages/nutui-solid/src/components/griditem/grid-item.tsx b/packages/nutui-solid/src/components/griditem/grid-item.tsx
index fd19677..09bbc6c 100644
--- a/packages/nutui-solid/src/components/griditem/grid-item.tsx
+++ b/packages/nutui-solid/src/components/griditem/grid-item.tsx
@@ -1,6 +1,6 @@
import { Component, JSX, ParentProps, createMemo, mergeProps, splitProps, useContext } from 'solid-js'
import { GridContext } from '../grid/grid.context'
-import { pxCheck } from '@/utils/pxCheck'
+import { pxCheck } from '@/utils/px-check'
export type GridItemProps = JSX.HTMLAttributes & Partial<{
text: string
diff --git a/packages/nutui-solid/src/components/image/__test__/__snapshots__/image.spec.tsx.snap b/packages/nutui-solid/src/components/image/__test__/__snapshots__/image.spec.tsx.snap
index 46b9c24..fdce488 100644
--- a/packages/nutui-solid/src/components/image/__test__/__snapshots__/image.spec.tsx.snap
+++ b/packages/nutui-solid/src/components/image/__test__/__snapshots__/image.spec.tsx.snap
@@ -25,12 +25,12 @@ exports[`Image: load error 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
,
@@ -63,12 +63,12 @@ exports[`Image: loading 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
,
diff --git a/packages/nutui-solid/src/components/image/image.taro.tsx b/packages/nutui-solid/src/components/image/image.taro.tsx
index aa42b67..e31ef82 100644
--- a/packages/nutui-solid/src/components/image/image.taro.tsx
+++ b/packages/nutui-solid/src/components/image/image.taro.tsx
@@ -1,7 +1,7 @@
import { Component, JSX, ParentProps, Show, createEffect, createMemo, createSignal, mergeProps, onCleanup, onMount, splitProps } from 'solid-js'
import { DOMElement } from 'solid-js/jsx-runtime'
import { ImageError, ImageIcon } from '@nutui/icons-solid'
-import { pxCheck } from '@/utils/pxCheck'
+import { pxCheck } from '@/utils/px-check'
export type ImageFit = 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'
export type ImagePosition = 'center' | 'top' | 'right' | 'bottom' | 'left' | string
diff --git a/packages/nutui-solid/src/components/image/image.tsx b/packages/nutui-solid/src/components/image/image.tsx
index 7b964bf..6884cdc 100644
--- a/packages/nutui-solid/src/components/image/image.tsx
+++ b/packages/nutui-solid/src/components/image/image.tsx
@@ -1,7 +1,7 @@
import { Component, JSX, ParentProps, Show, createEffect, createMemo, createSignal, mergeProps, onCleanup, onMount, splitProps } from 'solid-js'
import { DOMElement } from 'solid-js/jsx-runtime'
import { ImageError, ImageIcon } from '@nutui/icons-solid'
-import { pxCheck } from '@/utils/pxCheck'
+import { pxCheck } from '@/utils/px-check'
export type ImageFit = 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'
export type ImagePosition = 'center' | 'top' | 'right' | 'bottom' | 'left' | string
diff --git a/packages/nutui-solid/src/components/nutui.solid.build.ts b/packages/nutui-solid/src/components/nutui.solid.build.ts
index f5281ba..2415507 100644
--- a/packages/nutui-solid/src/components/nutui.solid.build.ts
+++ b/packages/nutui-solid/src/components/nutui.solid.build.ts
@@ -20,6 +20,8 @@ import Col from './col';
export * from './col';
import Space from './space';
export * from './space';
+import Sticky from './sticky';
+export * from './sticky';
import './button/index.scss';
@@ -33,5 +35,6 @@ import './layout/index.scss';
import './row/index.scss';
import './col/index.scss';
import './space/index.scss';
+import './sticky/index.scss';
-export { Button, Cell, CellGroup, Image, Divider, Grid, GridItem, Layout, Row, Col, Space };
+export { Button, Cell, CellGroup, Image, Divider, Grid, GridItem, Layout, Row, Col, Space, Sticky };
diff --git a/packages/nutui-solid/src/components/sticky/__test__/__snapshots__/sticky.spec.tsx.snap b/packages/nutui-solid/src/components/sticky/__test__/__snapshots__/sticky.spec.tsx.snap
new file mode 100644
index 0000000..977a7e8
--- /dev/null
+++ b/packages/nutui-solid/src/components/sticky/__test__/__snapshots__/sticky.spec.tsx.snap
@@ -0,0 +1,21 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`should sticky to bottom after scroll 1`] = `
+
+`;
+
+exports[`should sticky to top after scroll 1`] = `
+
+`;
diff --git a/packages/nutui-solid/src/components/sticky/__test__/sticky.spec.tsx b/packages/nutui-solid/src/components/sticky/__test__/sticky.spec.tsx
new file mode 100644
index 0000000..b7dea4c
--- /dev/null
+++ b/packages/nutui-solid/src/components/sticky/__test__/sticky.spec.tsx
@@ -0,0 +1,41 @@
+import { test, expect } from "vitest"
+import { render } from "@solidjs/testing-library"
+import { Sticky } from 'nutui-solid'
+import { mockScrollTop } from "@/utils/unit"
+
+Object.defineProperty(window.HTMLElement.prototype, 'clientHeight', {
+ value: 667
+})
+
+function mockStickyRect(wrapper: HTMLElement, rect: Partial) {
+ const mocked = vi.spyOn(wrapper, 'getBoundingClientRect').mockReturnValue(rect as DOMRect)
+
+ return () => mocked.mockRestore()
+}
+
+
+test('should sticky to top after scroll', async () => {
+ const { container } = render(() => )
+ const restore = mockStickyRect(container, {
+ top: -100,
+ bottom: -90
+ })
+
+ await mockScrollTop(1000)
+ expect(container.children[0]).toMatchSnapshot()
+
+ restore()
+})
+
+test('should sticky to bottom after scroll', async () => {
+ const { container } = render(() => )
+ const restore = mockStickyRect(container, {
+ top: 667,
+ bottom: 690
+ })
+
+ await mockScrollTop(0)
+ expect(container.children[0]).toMatchSnapshot()
+
+ restore()
+})
\ No newline at end of file
diff --git a/packages/nutui-solid/src/components/sticky/demos/basic.tsx b/packages/nutui-solid/src/components/sticky/demos/basic.tsx
new file mode 100644
index 0000000..42a9210
--- /dev/null
+++ b/packages/nutui-solid/src/components/sticky/demos/basic.tsx
@@ -0,0 +1,11 @@
+import { Button, Sticky } from 'nutui-solid'
+
+function Basic() {
+ return (
+
+
+
+ )
+}
+
+export default Basic
diff --git a/packages/nutui-solid/src/components/sticky/demos/bottom.tsx b/packages/nutui-solid/src/components/sticky/demos/bottom.tsx
new file mode 100644
index 0000000..34a091b
--- /dev/null
+++ b/packages/nutui-solid/src/components/sticky/demos/bottom.tsx
@@ -0,0 +1,16 @@
+import { Button, Sticky } from 'nutui-solid'
+
+function Bottom() {
+ return (
+ <>
+
+
+
+
+
+ >
+
+ )
+}
+
+export default Bottom
diff --git a/packages/nutui-solid/src/components/sticky/demos/container.tsx b/packages/nutui-solid/src/components/sticky/demos/container.tsx
new file mode 100644
index 0000000..9aeb6a5
--- /dev/null
+++ b/packages/nutui-solid/src/components/sticky/demos/container.tsx
@@ -0,0 +1,20 @@
+import { Button, Sticky } from 'nutui-solid'
+
+function Container() {
+ let container: HTMLDivElement
+ return (
+ <>
+
+ >
+
+ )
+}
+
+export default Container
diff --git a/packages/nutui-solid/src/components/sticky/demos/top.tsx b/packages/nutui-solid/src/components/sticky/demos/top.tsx
new file mode 100644
index 0000000..48a8999
--- /dev/null
+++ b/packages/nutui-solid/src/components/sticky/demos/top.tsx
@@ -0,0 +1,11 @@
+import { Button, Sticky } from 'nutui-solid'
+
+function Top() {
+ return (
+
+
+
+ )
+}
+
+export default Top
diff --git a/packages/nutui-solid/src/components/sticky/index.scss b/packages/nutui-solid/src/components/sticky/index.scss
new file mode 100644
index 0000000..e69de29
diff --git a/packages/nutui-solid/src/components/sticky/index.taro.tsx b/packages/nutui-solid/src/components/sticky/index.taro.tsx
new file mode 100644
index 0000000..840ed36
--- /dev/null
+++ b/packages/nutui-solid/src/components/sticky/index.taro.tsx
@@ -0,0 +1,7 @@
+import { Sticky } from './sticky.taro'
+
+export type {
+ StickyProps,
+} from './sticky.taro'
+
+export default Sticky
diff --git a/packages/nutui-solid/src/components/sticky/index.ts b/packages/nutui-solid/src/components/sticky/index.ts
new file mode 100644
index 0000000..601b8a3
--- /dev/null
+++ b/packages/nutui-solid/src/components/sticky/index.ts
@@ -0,0 +1,7 @@
+import { Sticky } from "./sticky";
+
+export type {
+ StickyProps
+} from "./sticky";
+
+export default Sticky
\ No newline at end of file
diff --git a/packages/nutui-solid/src/components/sticky/sticky.taro.tsx b/packages/nutui-solid/src/components/sticky/sticky.taro.tsx
new file mode 100644
index 0000000..b8409a0
--- /dev/null
+++ b/packages/nutui-solid/src/components/sticky/sticky.taro.tsx
@@ -0,0 +1,89 @@
+import { Component, JSX, ParentProps, createEffect, createMemo, mergeProps, onCleanup, onMount, splitProps } from 'solid-js'
+import { createStore } from 'solid-js/store'
+import { usePageScroll } from '@tarojs/taro'
+import { getTaroRect } from '@/utils/get-taro-rect'
+
+export type StickyProps = JSX.HTMLAttributes & Partial<{
+ top: string | number
+ scrollTop: string | number
+ zIndex: string | number
+ onChange: (fixed: boolean) => void
+}>
+
+const defaultProps: StickyProps = {
+ top: 0,
+ scrollTop: -1,
+ zIndex: 99,
+}
+
+export const Sticky: Component> = (props) => {
+ let rootRef: HTMLDivElement
+ let stickyRef: HTMLDivElement
+ const merged = mergeProps(defaultProps, props)
+ const [local, rest] = splitProps(merged, [
+ 'top',
+ 'scrollTop',
+ 'zIndex',
+ 'onChange',
+ ])
+
+ const [store, setStore] = createStore({
+ fixed: false,
+ height: 0,
+ width: 0,
+ })
+
+ const rootStyle = createMemo(() => {
+ if (store.fixed)
+ return { height: `${store.height}px` }
+ return {}
+ })
+
+ const stickyStyle = createMemo(() => {
+ if (!store.fixed)
+ return {}
+ return {
+ top: `${props.top}px`,
+ height: `${store.height}px`,
+ width: `${store.width}px`,
+ position: store.fixed ? 'fixed' : undefined,
+ zIndex: Number(local.zIndex),
+ }
+ })
+
+ const handleScroll = (top: number | string) => {
+ getTaroRect(rootRef).then(
+ (rootRect: any) => {
+ setStore({ height: rootRect.height, width: rootRect.width, fixed: Number(top) >= rootRect.top })
+ },
+ () => {},
+ )
+ }
+
+ createEffect(() => {
+ local?.onChange?.(store.fixed)
+ })
+
+ createEffect(() => {
+ if (props.scrollTop === -1) {
+ usePageScroll(() => handleScroll(props.top))
+ }
+ else {
+ handleScroll(props.top)
+ }
+ })
+
+ onMount(() => {
+ handleScroll(props.top)
+ })
+
+ onCleanup(() => {
+ handleScroll(props.top)
+ })
+
+ return (
+
+ )
+}
diff --git a/packages/nutui-solid/src/components/sticky/sticky.tsx b/packages/nutui-solid/src/components/sticky/sticky.tsx
new file mode 100644
index 0000000..1c9a33f
--- /dev/null
+++ b/packages/nutui-solid/src/components/sticky/sticky.tsx
@@ -0,0 +1,134 @@
+import { Component, JSX, ParentProps, createEffect, createMemo, mergeProps, onCleanup, onMount, splitProps } from 'solid-js'
+import { createStore } from 'solid-js/store'
+import { getRect } from '@/utils/get-rect'
+import { getScrollParent } from '@/hooks/use-scroll-parent'
+
+type StickyPosition = 'top' | 'bottom'
+
+export type StickyProps = JSX.HTMLAttributes & Partial<{
+ position: StickyPosition
+ top: string | number
+ bottom: string | number
+ container: Element
+ zIndex: string | number
+ onChange: (fixed: boolean) => void
+}>
+
+const defaultProps: StickyProps = {
+ position: 'top',
+ top: 0,
+ bottom: 0,
+ container: null,
+ zIndex: 99,
+}
+
+export const Sticky: Component> = (props) => {
+ let rootRef: HTMLDivElement
+ let stickyRef: HTMLDivElement
+ const merged = mergeProps(defaultProps, props)
+ const [local, rest] = splitProps(merged, [
+ 'position',
+ 'top',
+ 'bottom',
+ 'container',
+ 'zIndex',
+ 'onChange',
+ ])
+
+ const [store, setStore] = createStore({
+ fixed: false,
+ height: 0,
+ width: 0,
+ transform: 0,
+ })
+
+ const threshold = createMemo(() => {
+ return local.position === 'top' ? Number(local.top) : Number(local.bottom)
+ })
+
+ const rootStyle = createMemo(() => {
+ if (store.fixed)
+ return { height: `${store.height}px` }
+ return {}
+ })
+
+ const stickyStyle = createMemo(() => {
+ if (!store.fixed)
+ return {}
+ return {
+ [local.position]: `${threshold()}px`,
+ height: `${store.height}px`,
+ width: `${store.width}px`,
+ transform: store.transform ? `translate3d(0, ${store.transform}px, 0)` : undefined,
+ position: store.fixed ? 'fixed' : undefined,
+ zIndex: Number(local.zIndex),
+ }
+ })
+
+ const handleScroll = () => {
+ const containerEle = local.container as HTMLElement
+ if (!rootRef && !containerEle)
+ return
+ const rootRect = getRect(rootRef)
+ const stCurrent = stickyRef as Element
+ const stickyRect = getRect(stCurrent)
+ const containerRect = getRect(containerEle)
+ setStore({ height: rootRect.height })
+ setStore({ width: rootRect.width })
+
+ const getFixed = (): boolean => {
+ let fixed = false
+ if (local.position === 'top') {
+ fixed = containerEle
+ ? threshold() > rootRect.top && containerRect.bottom > 0
+ : threshold() > rootRect.top
+ }
+ else {
+ const clientHeight = document.documentElement.clientHeight
+ fixed = containerEle
+ ? containerRect.bottom > 0 && clientHeight - threshold() - stickyRect.height > containerRect.top
+ : clientHeight - threshold() < rootRect.bottom
+ }
+
+ return fixed
+ }
+
+ const getTransform = () => {
+ if (containerEle) {
+ if (local.position === 'top') {
+ const diff = containerRect.bottom - threshold() - stickyRect.height
+ return diff < 0 ? diff : 0
+ }
+ else {
+ const clientHeight = document.documentElement.clientHeight
+ const diff = containerRect.bottom - (clientHeight - threshold())
+ return diff < 0 ? diff : 0
+ }
+ }
+ return 0
+ }
+ setStore({ transform: getTransform() })
+ setStore({ fixed: getFixed() })
+ }
+
+ createEffect(() => {
+ local?.onChange?.(store.fixed)
+ })
+
+ onMount(() => {
+ handleScroll()
+ const el = getScrollParent(rootRef)
+ el.addEventListener('scroll', handleScroll, true)
+ })
+
+ onCleanup(() => {
+ const el = getScrollParent(rootRef)
+ el.removeEventListener('scroll', handleScroll)
+ })
+
+ return (
+
+ )
+}
diff --git a/packages/nutui-solid/src/hooks/use-scroll-parent.ts b/packages/nutui-solid/src/hooks/use-scroll-parent.ts
new file mode 100644
index 0000000..469079c
--- /dev/null
+++ b/packages/nutui-solid/src/hooks/use-scroll-parent.ts
@@ -0,0 +1,37 @@
+import { onMount } from "solid-js"
+
+type ScrollElement = HTMLElement | Window
+
+const overflowScrollReg = /scroll|auto|overlay/i
+const defaultRoot = window
+
+function isElement(node: Element) {
+ const ELEMENT_NODE_TYPE = 1
+ return node.tagName !== 'HTML' && node.tagName !== 'BODY' && node.nodeType === ELEMENT_NODE_TYPE
+}
+
+export function getScrollParent(el: Element, root: ScrollElement | undefined = defaultRoot) {
+ let node = el
+
+ while (node && node !== root && isElement(node)) {
+ const { overflowY } = window.getComputedStyle(node)
+ if (overflowScrollReg.test(overflowY)) {
+ return node
+ }
+ node = node.parentNode as Element
+ }
+
+ return root
+}
+
+export function useScrollParent(el: Element | undefined, root: ScrollElement | undefined = defaultRoot) {
+ let scrollParent: Element | Window
+
+ onMount(() => {
+ if (el) {
+ scrollParent = getScrollParent(el, root)
+ }
+ })
+
+ return scrollParent
+}
diff --git a/packages/nutui-solid/src/utils/get-rect.ts b/packages/nutui-solid/src/utils/get-rect.ts
new file mode 100644
index 0000000..9ae91bf
--- /dev/null
+++ b/packages/nutui-solid/src/utils/get-rect.ts
@@ -0,0 +1,53 @@
+/**
+ 获取元素的大小及其相对于视口的位置,等价于 Element.getBoundingClientRect。
+ width 宽度 number
+ height 高度 number
+ top 顶部与视图窗口左上角的距离 number
+ left 左侧与视图窗口左上角的距离 number
+ right 右侧与视图窗口左上角的距离 number
+ bottom 底部与视图窗口左上角的距离 number
+ */
+
+
+function isWindow(val: unknown): val is Window {
+ return typeof window !== 'undefined' && val === window
+}
+
+export interface rect {
+ top: number
+ left: number
+ right: number
+ bottom: number
+ width: number
+ height: number
+}
+
+export const getRect = (element: Element | Window | undefined) => {
+
+ if (isWindow(element)) {
+ const width = element.innerWidth
+ const height = element.innerHeight
+
+ return {
+ top: 0,
+ left: 0,
+ right: width,
+ bottom: height,
+ width,
+ height
+ }
+ }
+
+ if (element && element.getBoundingClientRect) {
+ return element.getBoundingClientRect()
+ }
+
+ return {
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ width: 0,
+ height: 0
+ }
+}
diff --git a/packages/nutui-solid/src/utils/get-taro-rect.ts b/packages/nutui-solid/src/utils/get-taro-rect.ts
new file mode 100644
index 0000000..c8af049
--- /dev/null
+++ b/packages/nutui-solid/src/utils/get-taro-rect.ts
@@ -0,0 +1,89 @@
+/**
+ 获取元素的大小及其相对于视口的位置,等价于 Element.getBoundingClientRect。
+ width 宽度 number
+ height 高度 number
+ top 顶部与视图窗口左上角的距离 number
+ left 左侧与视图窗口左上角的距离 number
+ right 右侧与视图窗口左上角的距离 number
+ bottom 底部与视图窗口左上角的距离 number
+ */
+import Taro from '@tarojs/taro'
+function isWindow(val: unknown): val is Window {
+ return typeof window !== 'undefined' && val === window
+}
+
+export interface rectTaro {
+ top: number
+ left: number
+ right: number
+ bottom: number
+ width: number
+ height: number
+}
+
+export const getTaroRectById = (id: string) => {
+ return new Promise((resolve, reject) => {
+ if (Taro.getEnv() === Taro.ENV_TYPE.WEB) {
+ const t = document ? document.querySelector(`#${id}`) : ''
+ if (t) {
+ resolve(t?.getBoundingClientRect())
+ }
+ reject()
+ } else {
+ const query = Taro.createSelectorQuery()
+ query
+ .select(`#${id}`)
+ .boundingClientRect()
+ .exec(function (rect: any) {
+ if (rect[0]) {
+ resolve(rect[0])
+ } else {
+ reject()
+ }
+ })
+ }
+ })
+}
+
+export const getTaroRect = (element: Element | Window | any): any => {
+ return new Promise((resolve, reject) => {
+ if (Taro.getEnv() === Taro.ENV_TYPE.WEB) {
+ if (element && element.$el) {
+ element = element.$el
+ }
+ if (isWindow(element)) {
+ const width = element.innerWidth
+ const height = element.innerHeight
+ resolve({
+ top: 0,
+ left: 0,
+ right: width,
+ bottom: height,
+ width,
+ height
+ })
+ }
+ if (element && element.getBoundingClientRect) {
+ resolve(element.getBoundingClientRect())
+ }
+ reject()
+ } else {
+ const query = Taro.createSelectorQuery()
+ const id = element?.id
+ if (id) {
+ query
+ .select(`#${id}`)
+ .boundingClientRect()
+ .exec(function (rect: any) {
+ if (rect[0]) {
+ resolve(rect[0])
+ } else {
+ reject()
+ }
+ })
+ } else {
+ reject()
+ }
+ }
+ })
+}
diff --git a/packages/nutui-solid/src/utils/pxCheck.ts b/packages/nutui-solid/src/utils/px-check.ts
similarity index 100%
rename from packages/nutui-solid/src/utils/pxCheck.ts
rename to packages/nutui-solid/src/utils/px-check.ts
diff --git a/packages/nutui-solid/src/utils/unit.ts b/packages/nutui-solid/src/utils/unit.ts
new file mode 100644
index 0000000..bed0e07
--- /dev/null
+++ b/packages/nutui-solid/src/utils/unit.ts
@@ -0,0 +1,96 @@
+import { Mock, vi } from 'vitest'
+
+export function nextTick(): Promise {
+ return Promise.resolve().then()
+}
+
+function getTouch(el: Element | Window, x: number, y: number) {
+ return {
+ identifier: Date.now(),
+ target: el,
+ pageX: x,
+ pageY: y,
+ clientX: x,
+ clientY: y,
+ radiusX: 2.5,
+ radiusY: 2.5,
+ rotationAngle: 10,
+ force: 0.5
+ }
+}
+
+export function trigger(
+ el: Element | Window,
+ eventName: string,
+ x = 0,
+ y = 0,
+ options: any = {}
+) {
+ const touchList = options.touchList || [getTouch(el, x, y)]
+
+ if (options.x || options.y) {
+ touchList.push(getTouch(el, options.x, options.y))
+ }
+
+ const event = document.createEvent('CustomEvent')
+ event.initCustomEvent(eventName, true, true, {})
+
+ Object.assign(event, {
+ clientX: x,
+ clientY: y,
+ touches: touchList,
+ targetTouches: touchList,
+ changedTouches: touchList
+ })
+
+ el.dispatchEvent(event)
+
+ return nextTick()
+}
+export async function mockScrollTop(value: number) {
+ Object.defineProperty(window, 'scrollTop', { value, writable: true })
+ trigger(window, 'scroll')
+ return nextTick()
+}
+
+// task sleep
+export function sleep(delay = 0): Promise {
+ return new Promise((resolve) => {
+ setTimeout(resolve, delay)
+ })
+}
+
+export function triggerDrag(el: any, relativeX = 0, relativeY = 0): void {
+ let x = relativeX
+ let y = relativeY
+ let startX = 0
+ let startY = 0
+ if (relativeX < 0) {
+ startX = Math.abs(relativeX)
+ x = 0
+ }
+ if (relativeY < 0) {
+ startY = Math.abs(relativeY)
+ y = 0
+ }
+ trigger(el, 'touchstart', startX, startY)
+ trigger(el, 'touchmove', x / 4, y / 4)
+ trigger(el, 'touchmove', x / 3, y / 3)
+ trigger(el, 'touchmove', x / 2, y / 2)
+ trigger(el, 'touchmove', x, y)
+ trigger(el, 'touchend', x, y)
+}
+
+// mock element method
+export function mockElementMethod(element: any, method: string): Mock {
+ const fn = vi.fn()
+ element.prototype[method] = fn
+ return fn
+}
+
+// mock getBoundingClientRect
+export function mockGetBoundingClientRect(rect: any): () => void {
+ const spy = vi.spyOn(Element.prototype, 'getBoundingClientRect')
+ spy.mockReturnValue(rect)
+ return () => spy.mockRestore()
+}