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 ( +
+
{rest.children}
+
+ ) +} 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 ( +
+
{rest.children}
+
+ ) +} 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