From bb87fd1bea44a56974b3c6d04b84398e46ef4f6f Mon Sep 17 00:00:00 2001
From: yisumay <15577506835@163.com>
Date: Tue, 8 Jul 2025 18:46:41 +0800
Subject: [PATCH 1/4] =?UTF-8?q?feat:=20sticky=E7=BB=84=E4=BB=B6=E5=AE=9E?=
=?UTF-8?q?=E7=8E=B0=E3=80=81demo=E3=80=81=E6=B5=8B=E8=AF=95=E7=94=A8?=
=?UTF-8?q?=E4=BE=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
package.json | 2 +-
packages/config.json | 11 ++
.../src/content/zh-cn/sticky.mdx | 50 +++++++
.../src/pages/demo/sticky.astro | 41 ++++++
.../__test__/__snapshots__/cell.spec.tsx.snap | 2 +-
.../__snapshots__/image.spec.tsx.snap | 8 +-
.../src/components/nutui.solid.build.ts | 5 +-
.../__snapshots__/sticky.spec.tsx.snap | 21 +++
.../sticky/__test__/sticky.spec.tsx | 41 ++++++
.../src/components/sticky/demos/basic.tsx | 11 ++
.../src/components/sticky/demos/bottom.tsx | 16 +++
.../src/components/sticky/demos/container.tsx | 20 +++
.../src/components/sticky/demos/top.tsx | 11 ++
.../src/components/sticky/index.scss | 0
.../src/components/sticky/index.taro.tsx | 7 +
.../src/components/sticky/index.ts | 7 +
.../src/components/sticky/sticky.taro.tsx | 89 ++++++++++++
.../src/components/sticky/sticky.tsx | 134 ++++++++++++++++++
packages/nutui-solid/src/utils/unit.ts | 96 +++++++++++++
.../nutui-solid/src/utils/useRect/index.ts | 53 +++++++
.../src/utils/useScrollParent/index.ts | 37 +++++
.../src/utils/useTaroRect/index.ts | 89 ++++++++++++
22 files changed, 744 insertions(+), 7 deletions(-)
create mode 100644 packages/nutui-solid-site/src/content/zh-cn/sticky.mdx
create mode 100644 packages/nutui-solid-site/src/pages/demo/sticky.astro
create mode 100644 packages/nutui-solid/src/components/sticky/__test__/__snapshots__/sticky.spec.tsx.snap
create mode 100644 packages/nutui-solid/src/components/sticky/__test__/sticky.spec.tsx
create mode 100644 packages/nutui-solid/src/components/sticky/demos/basic.tsx
create mode 100644 packages/nutui-solid/src/components/sticky/demos/bottom.tsx
create mode 100644 packages/nutui-solid/src/components/sticky/demos/container.tsx
create mode 100644 packages/nutui-solid/src/components/sticky/demos/top.tsx
create mode 100644 packages/nutui-solid/src/components/sticky/index.scss
create mode 100644 packages/nutui-solid/src/components/sticky/index.taro.tsx
create mode 100644 packages/nutui-solid/src/components/sticky/index.ts
create mode 100644 packages/nutui-solid/src/components/sticky/sticky.taro.tsx
create mode 100644 packages/nutui-solid/src/components/sticky/sticky.tsx
create mode 100644 packages/nutui-solid/src/utils/unit.ts
create mode 100644 packages/nutui-solid/src/utils/useRect/index.ts
create mode 100644 packages/nutui-solid/src/utils/useScrollParent/index.ts
create mode 100644 packages/nutui-solid/src/utils/useTaroRect/index.ts
diff --git a/package.json b/package.json
index 87e1e23..9f2d60c 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
"main": "index.js",
"scripts": {
"gen-icon": "pnpm --filter @nutui/icons-solid run build",
- "dev": "./bootstrap.sh",
+ "dev": "bash ./bootstrap.sh",
"build": "pnpm build-ui && pnpm --filter nutui-solid-site run build",
"dev-ui": "pnpm --filter nutui-solid run dev",
"build-ui": "pnpm --filter nutui-solid run build",
diff --git a/packages/config.json b/packages/config.json
index a118ee6..888d216 100644
--- a/packages/config.json
+++ b/packages/config.json
@@ -188,6 +188,17 @@
"show": true,
"taro": true,
"author": "yisumay"
+ },
+ {
+ "version": "1.0.0",
+ "name": "Sticky",
+ "type": "component",
+ "cName": "粘性布局",
+ "desc": "当组件在屏幕范围内时,会按照正常的布局排列,当组件滚出屏幕范围时,始终会固定在距离屏幕固定的距离处",
+ "sort": 1,
+ "show": true,
+ "taro": true,
+ "author": "yisumay"
}
]
},
diff --git a/packages/nutui-solid-site/src/content/zh-cn/sticky.mdx b/packages/nutui-solid-site/src/content/zh-cn/sticky.mdx
new file mode 100644
index 0000000..3e0d84a
--- /dev/null
+++ b/packages/nutui-solid-site/src/content/zh-cn/sticky.mdx
@@ -0,0 +1,50 @@
+---
+layout: ../../layouts/Layout.astro
+title: Sticky
+---
+
+import CodeBlock from '@/components/CodeBlock.astro'
+
+# Sticky 单元格
+
+### 介绍
+
+使用 fixed 定位实现的类似 position: sticky 的吸顶效果。
+
+### 安装
+
+```tsx
+import { Sticky } from '@nutui/nutui-solid';
+```
+
+### 基础用法
+
+:::demo
+
+
+
+:::
+
+### 吸顶距离
+
+:::demo
+
+
+
+:::
+
+### 指定容器
+
+:::demo
+
+
+
+:::
+
+### 吸底距离
+
+:::demo
+
+
+
+:::
\ No newline at end of file
diff --git a/packages/nutui-solid-site/src/pages/demo/sticky.astro b/packages/nutui-solid-site/src/pages/demo/sticky.astro
new file mode 100644
index 0000000..bb93a1d
--- /dev/null
+++ b/packages/nutui-solid-site/src/pages/demo/sticky.astro
@@ -0,0 +1,41 @@
+---
+import Demo from '@/components/Demo.astro';
+import MobileLayout from '@/layouts/MobileLayout.astro';
+import Basic from '~/nutui-solid/src/components/sticky/demos/basic.tsx';
+import Bottom from '~/nutui-solid/src/components/sticky/demos/bottom';
+import Top from '~/nutui-solid/src/components/sticky/demos/top';
+import Container from '~/nutui-solid/src/components/sticky/demos/container';
+
+
+const useTranslate = (obj) => {
+ return [obj['zh-CN']];
+};
+
+const [translated] = useTranslate({
+ 'zh-CN': {
+ basic: '基础用法',
+ top: '吸顶距离',
+ container: '指定容器',
+ bottom: '吸底距离',
+ },
+ 'en-US': {
+ basic: 'Basic Usage',
+ top: 'Top Distance',
+ container: 'Custom Container',
+ bottom: 'Bottom Distance'
+ },
+});
+---
+
+
+
+ {translated.basic}
+
+ {translated.top}
+
+ {translated.container}
+
+ {translated.bottom}
+
+
+
diff --git a/packages/nutui-solid/src/components/cell/__test__/__snapshots__/cell.spec.tsx.snap b/packages/nutui-solid/src/components/cell/__test__/__snapshots__/cell.spec.tsx.snap
index 8798b77..55d7209 100644
--- a/packages/nutui-solid/src/components/cell/__test__/__snapshots__/cell.spec.tsx.snap
+++ b/packages/nutui-solid/src/components/cell/__test__/__snapshots__/cell.spec.tsx.snap
@@ -20,7 +20,7 @@ exports[`prop isLink 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
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/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..cfdf7f9
--- /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 { useTaroRect } from '@/utils/useTaroRect'
+
+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) => {
+ useTaroRect(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..724323a
--- /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 { useRect } from '@/utils/useRect'
+import { getScrollParent } from '@/utils/useScrollParent'
+
+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 = useRect(rootRef)
+ const stCurrent = stickyRef as Element
+ const stickyRect = useRect(stCurrent)
+ const containerRect = useRect(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/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()
+}
diff --git a/packages/nutui-solid/src/utils/useRect/index.ts b/packages/nutui-solid/src/utils/useRect/index.ts
new file mode 100644
index 0000000..e59529d
--- /dev/null
+++ b/packages/nutui-solid/src/utils/useRect/index.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 useRect = (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/useScrollParent/index.ts b/packages/nutui-solid/src/utils/useScrollParent/index.ts
new file mode 100644
index 0000000..469079c
--- /dev/null
+++ b/packages/nutui-solid/src/utils/useScrollParent/index.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/useTaroRect/index.ts b/packages/nutui-solid/src/utils/useTaroRect/index.ts
new file mode 100644
index 0000000..e6ff722
--- /dev/null
+++ b/packages/nutui-solid/src/utils/useTaroRect/index.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 useTaroRectById = (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 useTaroRect = (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()
+ }
+ }
+ })
+}
From 86474660a2c0a06af2a5dd66255656dd37c8e4e9 Mon Sep 17 00:00:00 2001
From: yisumay <15577506835@163.com>
Date: Wed, 9 Jul 2025 14:37:48 +0800
Subject: [PATCH 2/4] =?UTF-8?q?feat:=20sticky=E6=96=87=E6=A1=A3=E8=A1=A5?=
=?UTF-8?q?=E5=85=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/content/zh-cn/sticky.mdx | 20 ++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/packages/nutui-solid-site/src/content/zh-cn/sticky.mdx b/packages/nutui-solid-site/src/content/zh-cn/sticky.mdx
index 3e0d84a..3c83848 100644
--- a/packages/nutui-solid-site/src/content/zh-cn/sticky.mdx
+++ b/packages/nutui-solid-site/src/content/zh-cn/sticky.mdx
@@ -47,4 +47,22 @@ import { Sticky } from '@nutui/nutui-solid';
-:::
\ No newline at end of file
+:::
+
+## API
+
+### Props
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| position | 吸附位置(`top`、`bottom`) | string | `top` |
+| top | 吸顶距离,`position` 为 `top` 时生效 | number | `0` |
+| bottom | 吸底距离,`position` 为 `bottom` 时生效 | number | `0` |
+| container | 容器的 `HTML` 节点 | Element | - |
+| z-index | 吸附时的层级 | number | `99` |
+
+### Events
+
+| 事件名 | 说明 | 回调参数 |
+| --- | --- | --- |
+| onChange | 吸附状态改变时触发 | `fixed: boolean` |
\ No newline at end of file
From 6ff0edb1d291a847139b1e9f7bab84fc9b6fe6e4 Mon Sep 17 00:00:00 2001
From: yisumay <15577506835@163.com>
Date: Wed, 9 Jul 2025 15:51:36 +0800
Subject: [PATCH 3/4] =?UTF-8?q?fix:=20grid=E5=8E=BB=E6=8E=89onClickItem?=
=?UTF-8?q?=E5=B1=9E=E6=80=A7=EF=BC=8Cgrid.item.taro=E5=88=A0=E9=99=A4?=
=?UTF-8?q?=E6=97=A0=E7=94=A8=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../nutui-solid/src/components/grid/grid.context.tsx | 2 +-
packages/nutui-solid/src/components/grid/grid.taro.tsx | 5 +----
packages/nutui-solid/src/components/grid/grid.tsx | 5 +----
.../src/components/griditem/grid-item.taro.tsx | 10 +---------
.../nutui-solid/src/components/griditem/grid-item.tsx | 2 +-
5 files changed, 5 insertions(+), 19 deletions(-)
diff --git a/packages/nutui-solid/src/components/grid/grid.context.tsx b/packages/nutui-solid/src/components/grid/grid.context.tsx
index 788e2c9..ca7ecc2 100644
--- a/packages/nutui-solid/src/components/grid/grid.context.tsx
+++ b/packages/nutui-solid/src/components/grid/grid.context.tsx
@@ -1,7 +1,7 @@
import { GridLocalProps } from './grid'
import { createContext } from '@/hooks/create-context'
-type GridContextType = GridLocalProps
+type GridContextType = Omit
export const [GridContextProvider, useGridContext, GridContext]
= createContext({
diff --git a/packages/nutui-solid/src/components/grid/grid.taro.tsx b/packages/nutui-solid/src/components/grid/grid.taro.tsx
index 6b026db..4d38796 100644
--- a/packages/nutui-solid/src/components/grid/grid.taro.tsx
+++ b/packages/nutui-solid/src/components/grid/grid.taro.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/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
From 2340ec913abecd1c52f017c5d9340145cd0e272b Mon Sep 17 00:00:00 2001
From: yisumay <15577506835@163.com>
Date: Wed, 9 Jul 2025 15:51:53 +0800
Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=E6=96=87=E4=BB=B6=E4=BD=8D?=
=?UTF-8?q?=E7=BD=AE=E3=80=81=E5=90=8D=E7=A7=B0=E4=BF=AE=E6=94=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/nutui-solid/src/components/cell/cell.taro.tsx | 2 +-
packages/nutui-solid/src/components/cell/cell.tsx | 2 +-
.../nutui-solid/src/components/image/image.taro.tsx | 2 +-
packages/nutui-solid/src/components/image/image.tsx | 2 +-
.../nutui-solid/src/components/sticky/sticky.taro.tsx | 4 ++--
packages/nutui-solid/src/components/sticky/sticky.tsx | 10 +++++-----
.../index.ts => hooks/use-scroll-parent.ts} | 0
.../src/utils/{useRect/index.ts => get-rect.ts} | 2 +-
.../utils/{useTaroRect/index.ts => get-taro-rect.ts} | 4 ++--
.../nutui-solid/src/utils/{pxCheck.ts => px-check.ts} | 0
10 files changed, 14 insertions(+), 14 deletions(-)
rename packages/nutui-solid/src/{utils/useScrollParent/index.ts => hooks/use-scroll-parent.ts} (100%)
rename packages/nutui-solid/src/utils/{useRect/index.ts => get-rect.ts} (94%)
rename packages/nutui-solid/src/utils/{useTaroRect/index.ts => get-taro-rect.ts} (94%)
rename packages/nutui-solid/src/utils/{pxCheck.ts => px-check.ts} (100%)
diff --git a/packages/nutui-solid/src/components/cell/cell.taro.tsx b/packages/nutui-solid/src/components/cell/cell.taro.tsx
index 7cf48cf..8b99cc6 100644
--- a/packages/nutui-solid/src/components/cell/cell.taro.tsx
+++ b/packages/nutui-solid/src/components/cell/cell.taro.tsx
@@ -1,6 +1,6 @@
import { Component, JSX, ParentProps, Show, createMemo, mergeProps, splitProps } from 'solid-js'
import { ArrowRight } from '@nutui/icons-solid'
-import { pxCheck } from '@/utils/pxCheck'
+import { pxCheck } from '@/utils/px-check'
export type CellSize = 'normal' | 'large'
diff --git a/packages/nutui-solid/src/components/cell/cell.tsx b/packages/nutui-solid/src/components/cell/cell.tsx
index 7cf48cf..8b99cc6 100644
--- a/packages/nutui-solid/src/components/cell/cell.tsx
+++ b/packages/nutui-solid/src/components/cell/cell.tsx
@@ -1,6 +1,6 @@
import { Component, JSX, ParentProps, Show, createMemo, mergeProps, splitProps } from 'solid-js'
import { ArrowRight } from '@nutui/icons-solid'
-import { pxCheck } from '@/utils/pxCheck'
+import { pxCheck } from '@/utils/px-check'
export type CellSize = 'normal' | 'large'
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/sticky/sticky.taro.tsx b/packages/nutui-solid/src/components/sticky/sticky.taro.tsx
index cfdf7f9..b8409a0 100644
--- a/packages/nutui-solid/src/components/sticky/sticky.taro.tsx
+++ b/packages/nutui-solid/src/components/sticky/sticky.taro.tsx
@@ -1,7 +1,7 @@
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 { useTaroRect } from '@/utils/useTaroRect'
+import { getTaroRect } from '@/utils/get-taro-rect'
export type StickyProps = JSX.HTMLAttributes & Partial<{
top: string | number
@@ -52,7 +52,7 @@ export const Sticky: Component> = (props) => {
})
const handleScroll = (top: number | string) => {
- useTaroRect(rootRef).then(
+ getTaroRect(rootRef).then(
(rootRect: any) => {
setStore({ height: rootRect.height, width: rootRect.width, fixed: Number(top) >= rootRect.top })
},
diff --git a/packages/nutui-solid/src/components/sticky/sticky.tsx b/packages/nutui-solid/src/components/sticky/sticky.tsx
index 724323a..1c9a33f 100644
--- a/packages/nutui-solid/src/components/sticky/sticky.tsx
+++ b/packages/nutui-solid/src/components/sticky/sticky.tsx
@@ -1,7 +1,7 @@
import { Component, JSX, ParentProps, createEffect, createMemo, mergeProps, onCleanup, onMount, splitProps } from 'solid-js'
import { createStore } from 'solid-js/store'
-import { useRect } from '@/utils/useRect'
-import { getScrollParent } from '@/utils/useScrollParent'
+import { getRect } from '@/utils/get-rect'
+import { getScrollParent } from '@/hooks/use-scroll-parent'
type StickyPosition = 'top' | 'bottom'
@@ -69,10 +69,10 @@ export const Sticky: Component> = (props) => {
const containerEle = local.container as HTMLElement
if (!rootRef && !containerEle)
return
- const rootRect = useRect(rootRef)
+ const rootRect = getRect(rootRef)
const stCurrent = stickyRef as Element
- const stickyRect = useRect(stCurrent)
- const containerRect = useRect(containerEle)
+ const stickyRect = getRect(stCurrent)
+ const containerRect = getRect(containerEle)
setStore({ height: rootRect.height })
setStore({ width: rootRect.width })
diff --git a/packages/nutui-solid/src/utils/useScrollParent/index.ts b/packages/nutui-solid/src/hooks/use-scroll-parent.ts
similarity index 100%
rename from packages/nutui-solid/src/utils/useScrollParent/index.ts
rename to packages/nutui-solid/src/hooks/use-scroll-parent.ts
diff --git a/packages/nutui-solid/src/utils/useRect/index.ts b/packages/nutui-solid/src/utils/get-rect.ts
similarity index 94%
rename from packages/nutui-solid/src/utils/useRect/index.ts
rename to packages/nutui-solid/src/utils/get-rect.ts
index e59529d..9ae91bf 100644
--- a/packages/nutui-solid/src/utils/useRect/index.ts
+++ b/packages/nutui-solid/src/utils/get-rect.ts
@@ -22,7 +22,7 @@ export interface rect {
height: number
}
-export const useRect = (element: Element | Window | undefined) => {
+export const getRect = (element: Element | Window | undefined) => {
if (isWindow(element)) {
const width = element.innerWidth
diff --git a/packages/nutui-solid/src/utils/useTaroRect/index.ts b/packages/nutui-solid/src/utils/get-taro-rect.ts
similarity index 94%
rename from packages/nutui-solid/src/utils/useTaroRect/index.ts
rename to packages/nutui-solid/src/utils/get-taro-rect.ts
index e6ff722..c8af049 100644
--- a/packages/nutui-solid/src/utils/useTaroRect/index.ts
+++ b/packages/nutui-solid/src/utils/get-taro-rect.ts
@@ -21,7 +21,7 @@ export interface rectTaro {
height: number
}
-export const useTaroRectById = (id: string) => {
+export const getTaroRectById = (id: string) => {
return new Promise((resolve, reject) => {
if (Taro.getEnv() === Taro.ENV_TYPE.WEB) {
const t = document ? document.querySelector(`#${id}`) : ''
@@ -45,7 +45,7 @@ export const useTaroRectById = (id: string) => {
})
}
-export const useTaroRect = (element: Element | Window | any): any => {
+export const getTaroRect = (element: Element | Window | any): any => {
return new Promise((resolve, reject) => {
if (Taro.getEnv() === Taro.ENV_TYPE.WEB) {
if (element && element.$el) {
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