From bdf8e814e4866412d8a517db619cd741fa47bf9f Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Sun, 21 Jul 2024 11:11:12 -0400 Subject: [PATCH 01/14] init scroll area --- package.json | 4 +- packages/bits-ui/package.json | 2 +- .../focus-scope/focus-scope-stack.svelte.ts | 13 +- .../tests/link-preview/LinkPreview.spec.ts | 2 +- pnpm-lock.yaml | 321 ++++++++++-------- sites/docs/package.json | 2 +- 6 files changed, 185 insertions(+), 159 deletions(-) diff --git a/package.json b/package.json index 9f9473eb3..2e7dc35eb 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "prettier": "^3.2.5", "prettier-plugin-svelte": "^3.2.2", "prettier-plugin-tailwindcss": "0.5.13", - "svelte": "5.0.0-next.183", - "svelte-eslint-parser": "^0.34.1", + "svelte": "5.0.0-next.192", + "svelte-eslint-parser": "^0.41.0", "wrangler": "^3.44.0" }, "type": "module", diff --git a/packages/bits-ui/package.json b/packages/bits-ui/package.json index e79c9c01d..a7db5c4a2 100644 --- a/packages/bits-ui/package.json +++ b/packages/bits-ui/package.json @@ -45,7 +45,7 @@ "jsdom": "^24.1.0", "publint": "^0.2.8", "resize-observer-polyfill": "^1.5.1", - "svelte": "5.0.0-next.183", + "svelte": "5.0.0-next.192", "svelte-check": "^3.8.4", "tslib": "^2.6.3", "typescript": "^5.5.3", diff --git a/packages/bits-ui/src/lib/bits/utilities/focus-scope/focus-scope-stack.svelte.ts b/packages/bits-ui/src/lib/bits/utilities/focus-scope/focus-scope-stack.svelte.ts index c23164017..0c9f3c7dc 100644 --- a/packages/bits-ui/src/lib/bits/utilities/focus-scope/focus-scope-stack.svelte.ts +++ b/packages/bits-ui/src/lib/bits/utilities/focus-scope/focus-scope-stack.svelte.ts @@ -1,6 +1,8 @@ +import { useId } from "$lib/internal/useId.svelte.js"; import { box } from "svelte-toolbelt"; export type FocusScopeAPI = { + id: string; paused: boolean; pause: () => void; resume: () => void; @@ -20,11 +22,11 @@ export function createFocusScopeStack() { } // remove in case it already exists because it'll be added to the top - stack.value = removeFromArray($state.snapshot(stack.value), focusScope); + stack.value = removeFromFocusScopeArray(stack.value, focusScope); stack.value.unshift(focusScope); }, remove(focusScope: FocusScopeAPI) { - stack.value = removeFromArray($state.snapshot(stack.value), focusScope); + stack.value = removeFromFocusScopeArray(stack.value, focusScope); stack.value[0]?.resume(); }, }; @@ -34,6 +36,7 @@ export function createFocusScopeAPI(): FocusScopeAPI { let paused = $state(false); return { + id: useId(), get paused() { return paused; }, @@ -46,10 +49,8 @@ export function createFocusScopeAPI(): FocusScopeAPI { }; } -export function removeFromArray(arr: T[], item: T) { - const updatedArr = [...arr]; - const index = updatedArr.indexOf(item); - if (index !== -1) updatedArr.splice(index, 1); +function removeFromFocusScopeArray(arr: FocusScopeAPI[], item: FocusScopeAPI) { + const updatedArr = [...arr].filter((i) => i.id !== item.id); return updatedArr; } diff --git a/packages/bits-ui/src/tests/link-preview/LinkPreview.spec.ts b/packages/bits-ui/src/tests/link-preview/LinkPreview.spec.ts index cd941e449..49109ea35 100644 --- a/packages/bits-ui/src/tests/link-preview/LinkPreview.spec.ts +++ b/packages/bits-ui/src/tests/link-preview/LinkPreview.spec.ts @@ -1,4 +1,4 @@ -import { fireEvent, render, waitFor } from "@testing-library/svelte"; +import { render, waitFor } from "@testing-library/svelte"; import { axe } from "jest-axe"; import { describe, it, vi } from "vitest"; import { getTestKbd, setupUserEvents } from "../utils.js"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9983b394e..c3373e4e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 2.27.7 '@huntabyte/eslint-config': specifier: ^0.3.2 - version: 0.3.2(@vue/compiler-sfc@3.4.31)(eslint-plugin-svelte@2.42.0)(eslint@9.7.0)(svelte-eslint-parser@0.34.1)(svelte@5.0.0-next.183)(typescript@5.5.3) + version: 0.3.2(@vue/compiler-sfc@3.4.31)(eslint-plugin-svelte@2.42.0(eslint@9.7.0)(svelte@5.0.0-next.192))(eslint@9.7.0)(svelte-eslint-parser@0.41.0(svelte@5.0.0-next.192))(svelte@5.0.0-next.192)(typescript@5.5.3)(vitest@2.0.2) '@huntabyte/eslint-plugin': specifier: ^0.1.0 version: 0.1.0(eslint@9.7.0) @@ -25,25 +25,25 @@ importers: version: 9.7.0 eslint-plugin-svelte: specifier: ^2.37.0 - version: 2.42.0(eslint@9.7.0)(svelte@5.0.0-next.183) + version: 2.42.0(eslint@9.7.0)(svelte@5.0.0-next.192) prettier: specifier: ^3.2.5 version: 3.3.3 prettier-plugin-svelte: specifier: ^3.2.2 - version: 3.2.5(prettier@3.3.3)(svelte@5.0.0-next.183) + version: 3.2.5(prettier@3.3.3)(svelte@5.0.0-next.192) prettier-plugin-tailwindcss: specifier: 0.5.13 - version: 0.5.13(prettier-plugin-svelte@3.2.5)(prettier@3.3.3) + version: 0.5.13(prettier-plugin-svelte@3.2.5(prettier@3.3.3)(svelte@5.0.0-next.192))(prettier@3.3.3) svelte: - specifier: 5.0.0-next.183 - version: 5.0.0-next.183 + specifier: 5.0.0-next.192 + version: 5.0.0-next.192 svelte-eslint-parser: - specifier: ^0.34.1 - version: 0.34.1(svelte@5.0.0-next.183) + specifier: ^0.41.0 + version: 0.41.0(svelte@5.0.0-next.192) wrangler: specifier: ^3.44.0 - version: 3.64.0 + version: 3.64.0(@cloudflare/workers-types@4.20240712.0) packages/bits-ui: dependencies: @@ -58,7 +58,7 @@ importers: version: 3.5.4 '@melt-ui/svelte': specifier: 0.76.2 - version: 0.76.2(svelte@5.0.0-next.183) + version: 0.76.2(svelte@5.0.0-next.192) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -70,7 +70,7 @@ importers: version: 5.0.7 runed: specifier: ^0.15.0 - version: 0.15.0(svelte@5.0.0-next.183) + version: 0.15.0(svelte@5.0.0-next.192) scule: specifier: ^1.3.0 version: 1.3.0 @@ -82,29 +82,29 @@ importers: version: 1.0.6 svelte-toolbelt: specifier: ^0.0.2 - version: 0.0.2(svelte@5.0.0-next.183) + version: 0.0.2(svelte@5.0.0-next.192) devDependencies: '@melt-ui/pp': specifier: ^0.3.0 - version: 0.3.2(@melt-ui/svelte@0.76.2)(svelte@5.0.0-next.183) + version: 0.3.2(@melt-ui/svelte@0.76.2(svelte@5.0.0-next.192))(svelte@5.0.0-next.192) '@sveltejs/kit': specifier: ^2.5.18 - version: 2.5.18(@sveltejs/vite-plugin-svelte@3.1.1)(svelte@5.0.0-next.183)(vite@5.3.3) + version: 2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)))(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)) '@sveltejs/package': specifier: ^2.3.2 - version: 2.3.2(svelte@5.0.0-next.183)(typescript@5.5.3) + version: 2.3.2(svelte@5.0.0-next.192)(typescript@5.5.3) '@sveltejs/vite-plugin-svelte': specifier: ^3.1.1 - version: 3.1.1(svelte@5.0.0-next.183)(vite@5.3.3) + version: 3.1.1(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)) '@testing-library/dom': specifier: ^10.3.1 version: 10.3.1 '@testing-library/jest-dom': specifier: ^6.4.6 - version: 6.4.6(vitest@2.0.2) + version: 6.4.6(@types/jest@29.5.12)(vitest@2.0.2(@types/node@20.14.10)(@vitest/ui@1.6.0)(jsdom@24.1.0)) '@testing-library/svelte': specifier: ^5.2.0 - version: 5.2.0(svelte@5.0.0-next.183)(vite@5.3.3)(vitest@2.0.2) + version: 5.2.0(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10))(vitest@2.0.2(@types/node@20.14.10)(@vitest/ui@1.6.0)(jsdom@24.1.0)) '@testing-library/user-event': specifier: ^14.5.2 version: 14.5.2(@testing-library/dom@10.3.1) @@ -139,11 +139,11 @@ importers: specifier: ^1.5.1 version: 1.5.1 svelte: - specifier: 5.0.0-next.183 - version: 5.0.0-next.183 + specifier: 5.0.0-next.192 + version: 5.0.0-next.192 svelte-check: specifier: ^3.8.4 - version: 3.8.4(postcss-load-config@5.1.0)(postcss@8.4.39)(svelte@5.0.0-next.183) + version: 3.8.4(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.39))(postcss@8.4.39)(svelte@5.0.0-next.192) tslib: specifier: ^2.6.3 version: 2.6.3 @@ -164,26 +164,26 @@ importers: version: 3.5.4 '@melt-ui/svelte': specifier: 0.76.2 - version: 0.76.2(svelte@5.0.0-next.183) + version: 0.76.2(svelte@5.0.0-next.192) bits-ui: specifier: workspace:* version: link:../../packages/bits-ui devDependencies: '@melt-ui/pp': specifier: ^0.3.0 - version: 0.3.2(@melt-ui/svelte@0.76.2)(svelte@5.0.0-next.183) + version: 0.3.2(@melt-ui/svelte@0.76.2(svelte@5.0.0-next.192))(svelte@5.0.0-next.192) '@prettier/sync': specifier: 0.3.0 version: 0.3.0(prettier@3.3.3) '@sveltejs/adapter-cloudflare': specifier: ^4.2.0 - version: 4.6.1(@sveltejs/kit@2.5.18)(wrangler@3.64.0) + version: 4.6.1(@sveltejs/kit@2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)))(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)))(wrangler@3.64.0(@cloudflare/workers-types@4.20240712.0)) '@sveltejs/kit': specifier: ^2.5.0 - version: 2.5.18(@sveltejs/vite-plugin-svelte@3.1.1)(svelte@5.0.0-next.183)(vite@5.3.3) + version: 2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)))(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)) '@sveltejs/vite-plugin-svelte': specifier: ^3.1.0 - version: 3.1.1(svelte@5.0.0-next.183)(vite@5.3.3) + version: 3.1.1(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)) '@tailwindcss/typography': specifier: ^0.5.10 version: 0.5.13(tailwindcss@3.4.4) @@ -216,19 +216,19 @@ importers: version: 3.1.0 mdsx: specifier: ^0.0.6 - version: 0.0.6(svelte@5.0.0-next.183) + version: 0.0.6(svelte@5.0.0-next.192) mode-watcher: specifier: ^0.2.0 - version: 0.2.2(svelte@5.0.0-next.183) + version: 0.2.2(svelte@5.0.0-next.192) phosphor-svelte: specifier: ^1.4.2 - version: 1.4.2(svelte@5.0.0-next.183) + version: 1.4.2(svelte@5.0.0-next.192) postcss: specifier: ^8.4.33 version: 8.4.39 postcss-load-config: specifier: ^5.0.2 - version: 5.1.0(postcss@8.4.39) + version: 5.1.0(jiti@1.21.6)(postcss@8.4.39) rehype-pretty-code: specifier: ^0.13.0 version: 0.13.2(shiki@1.10.3) @@ -242,14 +242,14 @@ importers: specifier: ^1.1.1 version: 1.10.3 svelte: - specifier: 5.0.0-next.183 - version: 5.0.0-next.183 + specifier: 5.0.0-next.192 + version: 5.0.0-next.192 svelte-check: specifier: ^3.6.9 - version: 3.8.4(postcss-load-config@5.1.0)(postcss@8.4.39)(svelte@5.0.0-next.183) + version: 3.8.4(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.39))(postcss@8.4.39)(svelte@5.0.0-next.192) svelte-sonner: specifier: ^0.3.24 - version: 0.3.26(svelte@5.0.0-next.183) + version: 0.3.26(svelte@5.0.0-next.192) tailwind-merge: specifier: ^2.2.1 version: 2.4.0 @@ -506,10 +506,6 @@ packages: peerDependencies: '@effect-ts/otel-node': '*' peerDependenciesMeta: - '@effect-ts/core': - optional: true - '@effect-ts/otel': - optional: true '@effect-ts/otel-node': optional: true @@ -4468,20 +4464,20 @@ packages: peerDependencies: svelte: ^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0 - svelte-eslint-parser@0.34.1: - resolution: {integrity: sha512-9+uLA1pqI9AZioKVGJzYYmlOZWxfoCXSbAM9iaNm7H01XlYlzRTtJfZgl9o3StQGN41PfGJIbkKkfk3e/pHFfA==} + svelte-eslint-parser@0.40.0: + resolution: {integrity: sha512-M+v1HhC5T1WKYVxWexUCS4o6oIBS88XKzOZuhl2ew+eGxol7eC21e+VE8TC4rXJ3iT3iXT0qlZsZcpKjVo5/zQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0-next.94 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0-next.181 peerDependenciesMeta: svelte: optional: true - svelte-eslint-parser@0.40.0: - resolution: {integrity: sha512-M+v1HhC5T1WKYVxWexUCS4o6oIBS88XKzOZuhl2ew+eGxol7eC21e+VE8TC4rXJ3iT3iXT0qlZsZcpKjVo5/zQ==} + svelte-eslint-parser@0.41.0: + resolution: {integrity: sha512-L6f4hOL+AbgfBIB52Z310pg1d2QjRqm7wy3kI1W6hhdhX5bvu7+f0R6w4ykp5HoDdzq+vGhIJmsisaiJDGmVfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0-next.181 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0-next.191 peerDependenciesMeta: svelte: optional: true @@ -4545,8 +4541,8 @@ packages: svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 typescript: ^4.9.4 || ^5.0.0 - svelte@5.0.0-next.183: - resolution: {integrity: sha512-1onDKWp5+a5ehYVWJ0scHVO0IbOTH9zIqYb/odXp/aG0qF9XdR76DL2tLrgRM5xzUdcvXSmakxa+tQDJojTBVw==} + svelte@5.0.0-next.192: + resolution: {integrity: sha512-UgjiqTCsEWyQ157x5YNbmx859vBVFfznKaxuiMCPqHS3HRZ1iqTsSyO3LI/4BHjqPrtxwrOn1Z63VwoJkYBBDA==} engines: {node: '>=18'} symbol-tree@3.2.4: @@ -5075,12 +5071,12 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - '@antfu/eslint-config@2.22.2(@vue/compiler-sfc@3.4.31)(eslint-plugin-svelte@2.42.0)(eslint@9.7.0)(svelte-eslint-parser@0.34.1)(svelte@5.0.0-next.183)(typescript@5.5.3)': + '@antfu/eslint-config@2.22.2(@vue/compiler-sfc@3.4.31)(eslint-plugin-svelte@2.42.0(eslint@9.7.0)(svelte@5.0.0-next.192))(eslint@9.7.0)(svelte-eslint-parser@0.41.0(svelte@5.0.0-next.192))(svelte@5.0.0-next.192)(typescript@5.5.3)(vitest@2.0.2)': dependencies: '@antfu/install-pkg': 0.3.3 '@clack/prompts': 0.7.0 '@stylistic/eslint-plugin': 2.6.0-beta.0(eslint@9.7.0)(typescript@5.5.3) - '@typescript-eslint/eslint-plugin': 8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40)(eslint@9.7.0)(typescript@5.5.3) + '@typescript-eslint/eslint-plugin': 8.0.0-alpha.40(@typescript-eslint/parser@7.16.0(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3) '@typescript-eslint/parser': 8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3) eslint: 9.7.0 eslint-config-flat-gitignore: 0.1.7 @@ -5095,13 +5091,12 @@ snapshots: eslint-plugin-markdown: 5.1.0(eslint@9.7.0) eslint-plugin-n: 17.9.0(eslint@9.7.0) eslint-plugin-no-only-tests: 3.1.0 - eslint-plugin-perfectionist: 2.11.0(eslint@9.7.0)(svelte-eslint-parser@0.34.1)(svelte@5.0.0-next.183)(typescript@5.5.3)(vue-eslint-parser@9.4.3) + eslint-plugin-perfectionist: 2.11.0(eslint@9.7.0)(svelte-eslint-parser@0.41.0(svelte@5.0.0-next.192))(svelte@5.0.0-next.192)(typescript@5.5.3)(vue-eslint-parser@9.4.3(eslint@9.7.0)) eslint-plugin-regexp: 2.6.0(eslint@9.7.0) - eslint-plugin-svelte: 2.42.0(eslint@9.7.0)(svelte@5.0.0-next.183) eslint-plugin-toml: 0.11.1(eslint@9.7.0) eslint-plugin-unicorn: 54.0.0(eslint@9.7.0) - eslint-plugin-unused-imports: 4.0.0(@typescript-eslint/eslint-plugin@8.0.0-alpha.40)(eslint@9.7.0) - eslint-plugin-vitest: 0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40)(eslint@9.7.0)(typescript@5.5.3) + eslint-plugin-unused-imports: 4.0.0(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0) + eslint-plugin-vitest: 0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.2) eslint-plugin-vue: 9.27.0(eslint@9.7.0) eslint-plugin-yml: 1.14.0(eslint@9.7.0) eslint-processor-vue-blocks: 0.1.2(@vue/compiler-sfc@3.4.31)(eslint@9.7.0) @@ -5110,11 +5105,13 @@ snapshots: local-pkg: 0.5.0 parse-gitignore: 2.0.0 picocolors: 1.0.1 - svelte-eslint-parser: 0.34.1(svelte@5.0.0-next.183) toml-eslint-parser: 0.10.0 vue-eslint-parser: 9.4.3(eslint@9.7.0) yaml-eslint-parser: 1.2.3 yargs: 17.7.2 + optionalDependencies: + eslint-plugin-svelte: 2.42.0(eslint@9.7.0)(svelte@5.0.0-next.192) + svelte-eslint-parser: 0.41.0(svelte@5.0.0-next.192) transitivePeerDependencies: - '@vue/compiler-sfc' - supports-color @@ -5379,7 +5376,6 @@ snapshots: '@contentlayer/utils': 0.3.4 camel-case: 4.1.2 comment-json: 4.2.4 - esbuild: 0.23.0 gray-matter: 4.0.3 mdx-bundler: 9.2.1(esbuild@0.23.0) rehype-stringify: 9.0.4 @@ -5389,6 +5385,8 @@ snapshots: source-map-support: 0.5.21 type-fest: 3.13.1 unified: 10.1.2 + optionalDependencies: + esbuild: 0.23.0 transitivePeerDependencies: - '@effect-ts/otel-node' - supports-color @@ -5426,9 +5424,9 @@ snapshots: '@contentlayer/utils@0.3.4': dependencies: '@effect-ts/core': 0.60.5 - '@effect-ts/otel': 0.15.1(@effect-ts/core@0.60.5)(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1)(@opentelemetry/sdk-trace-base@1.25.1) - '@effect-ts/otel-exporter-trace-otlp-grpc': 0.15.1(@effect-ts/core@0.60.5)(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1)(@opentelemetry/exporter-trace-otlp-grpc@0.39.1)(@opentelemetry/sdk-trace-base@1.25.1) - '@effect-ts/otel-sdk-trace-node': 0.15.1(@effect-ts/core@0.60.5)(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1)(@opentelemetry/sdk-trace-base@1.25.1)(@opentelemetry/sdk-trace-node@1.25.1) + '@effect-ts/otel': 0.15.1(@effect-ts/core@0.60.5)(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0)) + '@effect-ts/otel-exporter-trace-otlp-grpc': 0.15.1(@effect-ts/core@0.60.5)(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/exporter-trace-otlp-grpc@0.39.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0)) + '@effect-ts/otel-sdk-trace-node': 0.15.1(@effect-ts/core@0.60.5)(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@1.25.1(@opentelemetry/api@1.9.0)) '@js-temporal/polyfill': 0.4.4 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) @@ -5453,25 +5451,25 @@ snapshots: dependencies: '@effect-ts/system': 0.57.5 - '@effect-ts/otel-exporter-trace-otlp-grpc@0.15.1(@effect-ts/core@0.60.5)(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1)(@opentelemetry/exporter-trace-otlp-grpc@0.39.1)(@opentelemetry/sdk-trace-base@1.25.1)': + '@effect-ts/otel-exporter-trace-otlp-grpc@0.15.1(@effect-ts/core@0.60.5)(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/exporter-trace-otlp-grpc@0.39.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))': dependencies: '@effect-ts/core': 0.60.5 - '@effect-ts/otel': 0.15.1(@effect-ts/core@0.60.5)(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1)(@opentelemetry/sdk-trace-base@1.25.1) + '@effect-ts/otel': 0.15.1(@effect-ts/core@0.60.5)(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0)) '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-trace-otlp-grpc': 0.39.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) - '@effect-ts/otel-sdk-trace-node@0.15.1(@effect-ts/core@0.60.5)(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1)(@opentelemetry/sdk-trace-base@1.25.1)(@opentelemetry/sdk-trace-node@1.25.1)': + '@effect-ts/otel-sdk-trace-node@0.15.1(@effect-ts/core@0.60.5)(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-node@1.25.1(@opentelemetry/api@1.9.0))': dependencies: '@effect-ts/core': 0.60.5 - '@effect-ts/otel': 0.15.1(@effect-ts/core@0.60.5)(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1)(@opentelemetry/sdk-trace-base@1.25.1) + '@effect-ts/otel': 0.15.1(@effect-ts/core@0.60.5)(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0)) '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-node': 1.25.1(@opentelemetry/api@1.9.0) - '@effect-ts/otel@0.15.1(@effect-ts/core@0.60.5)(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1)(@opentelemetry/sdk-trace-base@1.25.1)': + '@effect-ts/otel@0.15.1(@effect-ts/core@0.60.5)(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.25.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.25.1(@opentelemetry/api@1.9.0))': dependencies: '@effect-ts/core': 0.60.5 '@opentelemetry/api': 1.9.0 @@ -5786,21 +5784,21 @@ snapshots: '@humanwhocodes/retry@0.3.0': {} - '@huntabyte/eslint-config@0.3.2(@vue/compiler-sfc@3.4.31)(eslint-plugin-svelte@2.42.0)(eslint@9.7.0)(svelte-eslint-parser@0.34.1)(svelte@5.0.0-next.183)(typescript@5.5.3)': + '@huntabyte/eslint-config@0.3.2(@vue/compiler-sfc@3.4.31)(eslint-plugin-svelte@2.42.0(eslint@9.7.0)(svelte@5.0.0-next.192))(eslint@9.7.0)(svelte-eslint-parser@0.41.0(svelte@5.0.0-next.192))(svelte@5.0.0-next.192)(typescript@5.5.3)(vitest@2.0.2)': dependencies: - '@antfu/eslint-config': 2.22.2(@vue/compiler-sfc@3.4.31)(eslint-plugin-svelte@2.42.0)(eslint@9.7.0)(svelte-eslint-parser@0.34.1)(svelte@5.0.0-next.183)(typescript@5.5.3) + '@antfu/eslint-config': 2.22.2(@vue/compiler-sfc@3.4.31)(eslint-plugin-svelte@2.42.0(eslint@9.7.0)(svelte@5.0.0-next.192))(eslint@9.7.0)(svelte-eslint-parser@0.41.0(svelte@5.0.0-next.192))(svelte@5.0.0-next.192)(typescript@5.5.3)(vitest@2.0.2) '@antfu/install-pkg': 0.3.3 '@clack/prompts': 0.7.0 '@huntabyte/eslint-plugin': 0.1.0(eslint@9.7.0) - '@typescript-eslint/eslint-plugin': 7.16.0(@typescript-eslint/parser@7.16.0)(eslint@9.7.0)(typescript@5.5.3) + '@typescript-eslint/eslint-plugin': 7.16.0(@typescript-eslint/parser@7.16.0(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3) '@typescript-eslint/parser': 7.16.0(eslint@9.7.0)(typescript@5.5.3) chalk: 5.3.0 eslint: 9.7.0 eslint-flat-config-utils: 0.2.5 - eslint-plugin-svelte: 2.42.0(eslint@9.7.0)(svelte@5.0.0-next.183) + eslint-plugin-svelte: 2.42.0(eslint@9.7.0)(svelte@5.0.0-next.192) local-pkg: 0.5.0 parse-gitignore: 2.0.0 - svelte-eslint-parser: 0.34.1(svelte@5.0.0-next.183) + svelte-eslint-parser: 0.41.0(svelte@5.0.0-next.192) yargs: 17.7.2 transitivePeerDependencies: - '@eslint-react/eslint-plugin' @@ -5937,14 +5935,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@melt-ui/pp@0.3.2(@melt-ui/svelte@0.76.2)(svelte@5.0.0-next.183)': + '@melt-ui/pp@0.3.2(@melt-ui/svelte@0.76.2(svelte@5.0.0-next.192))(svelte@5.0.0-next.192)': dependencies: - '@melt-ui/svelte': 0.76.2(svelte@5.0.0-next.183) + '@melt-ui/svelte': 0.76.2(svelte@5.0.0-next.192) estree-walker: 3.0.3 magic-string: 0.30.10 - svelte: 5.0.0-next.183 + svelte: 5.0.0-next.192 - '@melt-ui/svelte@0.76.2(svelte@5.0.0-next.183)': + '@melt-ui/svelte@0.76.2(svelte@5.0.0-next.192)': dependencies: '@floating-ui/core': 1.6.4 '@floating-ui/dom': 1.6.7 @@ -5952,7 +5950,7 @@ snapshots: dequal: 2.0.3 focus-trap: 7.5.4 nanoid: 5.0.7 - svelte: 5.0.0-next.183 + svelte: 5.0.0-next.192 '@nodelib/fs.scandir@2.1.5': dependencies: @@ -6220,17 +6218,17 @@ snapshots: - supports-color - typescript - '@sveltejs/adapter-cloudflare@4.6.1(@sveltejs/kit@2.5.18)(wrangler@3.64.0)': + '@sveltejs/adapter-cloudflare@4.6.1(@sveltejs/kit@2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)))(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)))(wrangler@3.64.0(@cloudflare/workers-types@4.20240712.0))': dependencies: '@cloudflare/workers-types': 4.20240712.0 - '@sveltejs/kit': 2.5.18(@sveltejs/vite-plugin-svelte@3.1.1)(svelte@5.0.0-next.183)(vite@5.3.3) + '@sveltejs/kit': 2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)))(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)) esbuild: 0.21.5 worktop: 0.8.0-next.18 - wrangler: 3.64.0 + wrangler: 3.64.0(@cloudflare/workers-types@4.20240712.0) - '@sveltejs/kit@2.5.18(@sveltejs/vite-plugin-svelte@3.1.1)(svelte@5.0.0-next.183)(vite@5.3.3)': + '@sveltejs/kit@2.5.18(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)))(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10))': dependencies: - '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@5.0.0-next.183)(vite@5.3.3) + '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)) '@types/cookie': 0.6.0 cookie: 0.6.0 devalue: 5.0.0 @@ -6242,41 +6240,41 @@ snapshots: sade: 1.8.1 set-cookie-parser: 2.6.0 sirv: 2.0.4 - svelte: 5.0.0-next.183 + svelte: 5.0.0-next.192 tiny-glob: 0.2.9 vite: 5.3.3(@types/node@20.14.10) - '@sveltejs/package@2.3.2(svelte@5.0.0-next.183)(typescript@5.5.3)': + '@sveltejs/package@2.3.2(svelte@5.0.0-next.192)(typescript@5.5.3)': dependencies: chokidar: 3.6.0 kleur: 4.1.5 sade: 1.8.1 semver: 7.6.2 - svelte: 5.0.0-next.183 - svelte2tsx: 0.7.13(svelte@5.0.0-next.183)(typescript@5.5.3) + svelte: 5.0.0-next.192 + svelte2tsx: 0.7.13(svelte@5.0.0-next.192)(typescript@5.5.3) transitivePeerDependencies: - typescript - '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.1)(svelte@5.0.0-next.183)(vite@5.3.3)': + '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)))(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10))': dependencies: - '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@5.0.0-next.183)(vite@5.3.3) + '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)) debug: 4.3.5 - svelte: 5.0.0-next.183 + svelte: 5.0.0-next.192 vite: 5.3.3(@types/node@20.14.10) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.183)(vite@5.3.3)': + '@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.1)(svelte@5.0.0-next.183)(vite@5.3.3) + '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)))(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10)) debug: 4.3.5 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.10 - svelte: 5.0.0-next.183 - svelte-hmr: 0.16.0(svelte@5.0.0-next.183) + svelte: 5.0.0-next.192 + svelte-hmr: 0.16.0(svelte@5.0.0-next.192) vite: 5.3.3(@types/node@20.14.10) - vitefu: 0.2.5(vite@5.3.3) + vitefu: 0.2.5(vite@5.3.3(@types/node@20.14.10)) transitivePeerDependencies: - supports-color @@ -6310,7 +6308,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.6(vitest@2.0.2)': + '@testing-library/jest-dom@6.4.6(@types/jest@29.5.12)(vitest@2.0.2(@types/node@20.14.10)(@vitest/ui@1.6.0)(jsdom@24.1.0))': dependencies: '@adobe/css-tools': 4.4.0 '@babel/runtime': 7.24.8 @@ -6320,12 +6318,15 @@ snapshots: dom-accessibility-api: 0.6.3 lodash: 4.17.21 redent: 3.0.0 + optionalDependencies: + '@types/jest': 29.5.12 vitest: 2.0.2(@types/node@20.14.10)(@vitest/ui@1.6.0)(jsdom@24.1.0) - '@testing-library/svelte@5.2.0(svelte@5.0.0-next.183)(vite@5.3.3)(vitest@2.0.2)': + '@testing-library/svelte@5.2.0(svelte@5.0.0-next.192)(vite@5.3.3(@types/node@20.14.10))(vitest@2.0.2(@types/node@20.14.10)(@vitest/ui@1.6.0)(jsdom@24.1.0))': dependencies: '@testing-library/dom': 10.3.1 - svelte: 5.0.0-next.183 + svelte: 5.0.0-next.192 + optionalDependencies: vite: 5.3.3(@types/node@20.14.10) vitest: 2.0.2(@types/node@20.14.10)(@vitest/ui@1.6.0)(jsdom@24.1.0) @@ -6436,7 +6437,7 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@7.16.0(@typescript-eslint/parser@7.16.0)(eslint@9.7.0)(typescript@5.5.3)': + '@typescript-eslint/eslint-plugin@7.16.0(@typescript-eslint/parser@7.16.0(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3)': dependencies: '@eslint-community/regexpp': 4.11.0 '@typescript-eslint/parser': 7.16.0(eslint@9.7.0)(typescript@5.5.3) @@ -6449,14 +6450,15 @@ snapshots: ignore: 5.3.1 natural-compare: 1.4.0 ts-api-utils: 1.3.0(typescript@5.5.3) + optionalDependencies: typescript: 5.5.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40)(eslint@9.7.0)(typescript@5.5.3)': + '@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@7.16.0(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3)': dependencies: '@eslint-community/regexpp': 4.11.0 - '@typescript-eslint/parser': 8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3) + '@typescript-eslint/parser': 7.16.0(eslint@9.7.0)(typescript@5.5.3) '@typescript-eslint/scope-manager': 8.0.0-alpha.40 '@typescript-eslint/type-utils': 8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3) '@typescript-eslint/utils': 8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3) @@ -6466,6 +6468,7 @@ snapshots: ignore: 5.3.1 natural-compare: 1.4.0 ts-api-utils: 1.3.0(typescript@5.5.3) + optionalDependencies: typescript: 5.5.3 transitivePeerDependencies: - supports-color @@ -6478,6 +6481,7 @@ snapshots: '@typescript-eslint/visitor-keys': 7.16.0 debug: 4.3.5 eslint: 9.7.0 + optionalDependencies: typescript: 5.5.3 transitivePeerDependencies: - supports-color @@ -6490,6 +6494,7 @@ snapshots: '@typescript-eslint/visitor-keys': 8.0.0-alpha.40 debug: 4.3.5 eslint: 9.7.0 + optionalDependencies: typescript: 5.5.3 transitivePeerDependencies: - supports-color @@ -6516,6 +6521,7 @@ snapshots: debug: 4.3.5 eslint: 9.7.0 ts-api-utils: 1.3.0(typescript@5.5.3) + optionalDependencies: typescript: 5.5.3 transitivePeerDependencies: - supports-color @@ -6526,6 +6532,7 @@ snapshots: '@typescript-eslint/utils': 8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3) debug: 4.3.5 ts-api-utils: 1.3.0(typescript@5.5.3) + optionalDependencies: typescript: 5.5.3 transitivePeerDependencies: - eslint @@ -6547,6 +6554,7 @@ snapshots: minimatch: 9.0.5 semver: 7.6.2 ts-api-utils: 1.3.0(typescript@5.5.3) + optionalDependencies: typescript: 5.5.3 transitivePeerDependencies: - supports-color @@ -6561,6 +6569,7 @@ snapshots: minimatch: 9.0.5 semver: 7.6.2 ts-api-utils: 1.3.0(typescript@5.5.3) + optionalDependencies: typescript: 5.5.3 transitivePeerDependencies: - supports-color @@ -6575,6 +6584,7 @@ snapshots: minimatch: 9.0.5 semver: 7.6.2 ts-api-utils: 1.3.0(typescript@5.5.3) + optionalDependencies: typescript: 5.5.3 transitivePeerDependencies: - supports-color @@ -7353,14 +7363,15 @@ snapshots: eslint-plugin-no-only-tests@3.1.0: {} - eslint-plugin-perfectionist@2.11.0(eslint@9.7.0)(svelte-eslint-parser@0.34.1)(svelte@5.0.0-next.183)(typescript@5.5.3)(vue-eslint-parser@9.4.3): + eslint-plugin-perfectionist@2.11.0(eslint@9.7.0)(svelte-eslint-parser@0.41.0(svelte@5.0.0-next.192))(svelte@5.0.0-next.192)(typescript@5.5.3)(vue-eslint-parser@9.4.3(eslint@9.7.0)): dependencies: '@typescript-eslint/utils': 7.16.0(eslint@9.7.0)(typescript@5.5.3) eslint: 9.7.0 minimatch: 9.0.5 natural-compare-lite: 1.4.0 - svelte: 5.0.0-next.183 - svelte-eslint-parser: 0.34.1(svelte@5.0.0-next.183) + optionalDependencies: + svelte: 5.0.0-next.192 + svelte-eslint-parser: 0.41.0(svelte@5.0.0-next.192) vue-eslint-parser: 9.4.3(eslint@9.7.0) transitivePeerDependencies: - supports-color @@ -7377,7 +7388,7 @@ snapshots: regexp-ast-analysis: 0.7.1 scslre: 0.3.0 - eslint-plugin-svelte@2.42.0(eslint@9.7.0)(svelte@5.0.0-next.183): + eslint-plugin-svelte@2.42.0(eslint@9.7.0)(svelte@5.0.0-next.192): dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.7.0) '@jridgewell/sourcemap-codec': 1.5.0 @@ -7390,8 +7401,9 @@ snapshots: postcss-safe-parser: 6.0.0(postcss@8.4.39) postcss-selector-parser: 6.1.1 semver: 7.6.2 - svelte: 5.0.0-next.183 - svelte-eslint-parser: 0.40.0(svelte@5.0.0-next.183) + svelte-eslint-parser: 0.40.0(svelte@5.0.0-next.192) + optionalDependencies: + svelte: 5.0.0-next.192 transitivePeerDependencies: - ts-node @@ -7427,17 +7439,20 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-unused-imports@4.0.0(@typescript-eslint/eslint-plugin@8.0.0-alpha.40)(eslint@9.7.0): + eslint-plugin-unused-imports@4.0.0(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0): dependencies: - '@typescript-eslint/eslint-plugin': 8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40)(eslint@9.7.0)(typescript@5.5.3) eslint: 9.7.0 eslint-rule-composer: 0.3.0 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.0.0-alpha.40(@typescript-eslint/parser@7.16.0(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3) - eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40)(eslint@9.7.0)(typescript@5.5.3): + eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3)(vitest@2.0.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.0.0-alpha.40(@typescript-eslint/parser@8.0.0-alpha.40)(eslint@9.7.0)(typescript@5.5.3) '@typescript-eslint/utils': 7.16.0(eslint@9.7.0)(typescript@5.5.3) eslint: 9.7.0 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.0.0-alpha.40(@typescript-eslint/parser@7.16.0(eslint@9.7.0)(typescript@5.5.3))(eslint@9.7.0)(typescript@5.5.3) + vitest: 2.0.2(@types/node@20.14.10)(@vitest/ui@1.6.0)(jsdom@24.1.0) transitivePeerDependencies: - supports-color - typescript @@ -8598,7 +8613,7 @@ snapshots: dependencies: '@types/mdast': 4.0.4 - mdsx@0.0.6(svelte@5.0.0-next.183): + mdsx@0.0.6(svelte@5.0.0-next.192): dependencies: esrap: 1.2.2 hast-util-to-html: 9.0.1 @@ -8607,7 +8622,7 @@ snapshots: rehype-stringify: 10.0.0 remark-parse: 11.0.0 remark-rehype: 11.1.0 - svelte: 5.0.0-next.183 + svelte: 5.0.0-next.192 unified: 11.0.5 unist-util-visit: 5.0.0 vfile: 6.0.1 @@ -9113,9 +9128,9 @@ snapshots: pkg-types: 1.1.3 ufo: 1.5.3 - mode-watcher@0.2.2(svelte@5.0.0-next.183): + mode-watcher@0.2.2(svelte@5.0.0-next.192): dependencies: - svelte: 5.0.0-next.183 + svelte: 5.0.0-next.192 mri@1.2.0: {} @@ -9341,9 +9356,9 @@ snapshots: estree-walker: 3.0.3 is-reference: 3.0.2 - phosphor-svelte@1.4.2(svelte@5.0.0-next.183): + phosphor-svelte@1.4.2(svelte@5.0.0-next.192): dependencies: - svelte: 5.0.0-next.183 + svelte: 5.0.0-next.192 picocolors@1.0.1: {} @@ -9384,20 +9399,24 @@ snapshots: postcss-load-config@3.1.4(postcss@8.4.39): dependencies: lilconfig: 2.1.0 - postcss: 8.4.39 yaml: 1.10.2 + optionalDependencies: + postcss: 8.4.39 postcss-load-config@4.0.2(postcss@8.4.39): dependencies: lilconfig: 3.1.2 - postcss: 8.4.39 yaml: 2.4.5 + optionalDependencies: + postcss: 8.4.39 - postcss-load-config@5.1.0(postcss@8.4.39): + postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.39): dependencies: lilconfig: 3.1.2 - postcss: 8.4.39 yaml: 2.4.5 + optionalDependencies: + jiti: 1.21.6 + postcss: 8.4.39 postcss-nested@6.0.1(postcss@8.4.39): dependencies: @@ -9439,15 +9458,16 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-svelte@3.2.5(prettier@3.3.3)(svelte@5.0.0-next.183): + prettier-plugin-svelte@3.2.5(prettier@3.3.3)(svelte@5.0.0-next.192): dependencies: prettier: 3.3.3 - svelte: 5.0.0-next.183 + svelte: 5.0.0-next.192 - prettier-plugin-tailwindcss@0.5.13(prettier-plugin-svelte@3.2.5)(prettier@3.3.3): + prettier-plugin-tailwindcss@0.5.13(prettier-plugin-svelte@3.2.5(prettier@3.3.3)(svelte@5.0.0-next.192))(prettier@3.3.3): dependencies: prettier: 3.3.3 - prettier-plugin-svelte: 3.2.5(prettier@3.3.3)(svelte@5.0.0-next.183) + optionalDependencies: + prettier-plugin-svelte: 3.2.5(prettier@3.3.3)(svelte@5.0.0-next.192) prettier@2.8.8: {} @@ -9734,10 +9754,10 @@ snapshots: dependencies: queue-microtask: 1.2.3 - runed@0.15.0(svelte@5.0.0-next.183): + runed@0.15.0(svelte@5.0.0-next.192): dependencies: esm-env: 1.0.0 - svelte: 5.0.0-next.183 + svelte: 5.0.0-next.192 rxjs@7.8.1: dependencies: @@ -9961,14 +9981,14 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@3.8.4(postcss-load-config@5.1.0)(postcss@8.4.39)(svelte@5.0.0-next.183): + svelte-check@3.8.4(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.39))(postcss@8.4.39)(svelte@5.0.0-next.192): dependencies: '@jridgewell/trace-mapping': 0.3.25 chokidar: 3.6.0 picocolors: 1.0.1 sade: 1.8.1 - svelte: 5.0.0-next.183 - svelte-preprocess: 5.1.4(postcss-load-config@5.1.0)(postcss@8.4.39)(svelte@5.0.0-next.183)(typescript@5.5.3) + svelte: 5.0.0-next.192 + svelte-preprocess: 5.1.4(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.39))(postcss@8.4.39)(svelte@5.0.0-next.192)(typescript@5.5.3) typescript: 5.5.3 transitivePeerDependencies: - '@babel/core' @@ -9981,56 +10001,59 @@ snapshots: - stylus - sugarss - svelte-eslint-parser@0.34.1(svelte@5.0.0-next.183): + svelte-eslint-parser@0.40.0(svelte@5.0.0-next.192): dependencies: eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 postcss: 8.4.39 postcss-scss: 4.0.9(postcss@8.4.39) - svelte: 5.0.0-next.183 + optionalDependencies: + svelte: 5.0.0-next.192 - svelte-eslint-parser@0.40.0(svelte@5.0.0-next.183): + svelte-eslint-parser@0.41.0(svelte@5.0.0-next.192): dependencies: eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 espree: 9.6.1 postcss: 8.4.39 postcss-scss: 4.0.9(postcss@8.4.39) - svelte: 5.0.0-next.183 + optionalDependencies: + svelte: 5.0.0-next.192 - svelte-hmr@0.16.0(svelte@5.0.0-next.183): + svelte-hmr@0.16.0(svelte@5.0.0-next.192): dependencies: - svelte: 5.0.0-next.183 + svelte: 5.0.0-next.192 - svelte-preprocess@5.1.4(postcss-load-config@5.1.0)(postcss@8.4.39)(svelte@5.0.0-next.183)(typescript@5.5.3): + svelte-preprocess@5.1.4(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.39))(postcss@8.4.39)(svelte@5.0.0-next.192)(typescript@5.5.3): dependencies: '@types/pug': 2.0.10 detect-indent: 6.1.0 magic-string: 0.30.10 - postcss: 8.4.39 - postcss-load-config: 5.1.0(postcss@8.4.39) sorcery: 0.11.1 strip-indent: 3.0.0 - svelte: 5.0.0-next.183 + svelte: 5.0.0-next.192 + optionalDependencies: + postcss: 8.4.39 + postcss-load-config: 5.1.0(jiti@1.21.6)(postcss@8.4.39) typescript: 5.5.3 - svelte-sonner@0.3.26(svelte@5.0.0-next.183): + svelte-sonner@0.3.26(svelte@5.0.0-next.192): dependencies: - svelte: 5.0.0-next.183 + svelte: 5.0.0-next.192 - svelte-toolbelt@0.0.2(svelte@5.0.0-next.183): + svelte-toolbelt@0.0.2(svelte@5.0.0-next.192): dependencies: - svelte: 5.0.0-next.183 + svelte: 5.0.0-next.192 - svelte2tsx@0.7.13(svelte@5.0.0-next.183)(typescript@5.5.3): + svelte2tsx@0.7.13(svelte@5.0.0-next.192)(typescript@5.5.3): dependencies: dedent-js: 1.0.1 pascal-case: 3.1.2 - svelte: 5.0.0-next.183 + svelte: 5.0.0-next.192 typescript: 5.5.3 - svelte@5.0.0-next.183: + svelte@5.0.0-next.192: dependencies: '@ampproject/remapping': 2.3.0 '@jridgewell/sourcemap-codec': 1.5.0 @@ -10382,32 +10405,29 @@ snapshots: vite@5.3.3(@types/node@20.14.10): dependencies: - '@types/node': 20.14.10 esbuild: 0.21.5 postcss: 8.4.39 rollup: 4.18.1 optionalDependencies: + '@types/node': 20.14.10 fsevents: 2.3.3 - vitefu@0.2.5(vite@5.3.3): - dependencies: + vitefu@0.2.5(vite@5.3.3(@types/node@20.14.10)): + optionalDependencies: vite: 5.3.3(@types/node@20.14.10) vitest@2.0.2(@types/node@20.14.10)(@vitest/ui@1.6.0)(jsdom@24.1.0): dependencies: '@ampproject/remapping': 2.3.0 - '@types/node': 20.14.10 '@vitest/expect': 2.0.2 '@vitest/pretty-format': 2.0.2 '@vitest/runner': 2.0.2 '@vitest/snapshot': 2.0.2 '@vitest/spy': 2.0.2 - '@vitest/ui': 1.6.0(vitest@2.0.2) '@vitest/utils': 2.0.2 chai: 5.1.1 debug: 4.3.5 execa: 8.0.1 - jsdom: 24.1.0 magic-string: 0.30.10 pathe: 1.1.2 std-env: 3.7.0 @@ -10417,6 +10437,10 @@ snapshots: vite: 5.3.3(@types/node@20.14.10) vite-node: 2.0.2(@types/node@20.14.10) why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.14.10 + '@vitest/ui': 1.6.0(vitest@2.0.2) + jsdom: 24.1.0 transitivePeerDependencies: - less - lightningcss @@ -10500,7 +10524,7 @@ snapshots: mrmime: 2.0.0 regexparam: 3.0.0 - wrangler@3.64.0: + wrangler@3.64.0(@cloudflare/workers-types@4.20240712.0): dependencies: '@cloudflare/kv-asset-handler': 0.3.4 '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) @@ -10519,6 +10543,7 @@ snapshots: unenv: unenv-nightly@1.10.0-1717606461.a117952 xxhash-wasm: 1.0.2 optionalDependencies: + '@cloudflare/workers-types': 4.20240712.0 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil diff --git a/sites/docs/package.json b/sites/docs/package.json index f5ba3a7e5..bbec56ca1 100644 --- a/sites/docs/package.json +++ b/sites/docs/package.json @@ -39,7 +39,7 @@ "rehype-slug": "^6.0.0", "remark-gfm": "^4.0.0", "shiki": "^1.1.1", - "svelte": "5.0.0-next.183", + "svelte": "5.0.0-next.192", "svelte-check": "^3.6.9", "svelte-sonner": "^0.3.24", "tailwind-merge": "^2.2.1", From e68549b9dcc79f8cf446167e9e5cbe7b0789c850 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Sun, 21 Jul 2024 12:21:00 -0400 Subject: [PATCH 02/14] scroll area work --- .../components/scroll-area-viewport.svelte | 12 + .../bits/scroll-area/scroll-area.svelte.ts | 506 ++++++++++++++++++ .../bits-ui/src/lib/bits/scroll-area/types.ts | 33 +- .../lib/internal/useResizeObserver.svelte.ts | 19 + 4 files changed, 547 insertions(+), 23 deletions(-) create mode 100644 packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts create mode 100644 packages/bits-ui/src/lib/internal/useResizeObserver.svelte.ts diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-viewport.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-viewport.svelte index 7c933fb36..d185bbfa0 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-viewport.svelte +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-viewport.svelte @@ -32,3 +32,15 @@ {/if} + + \ No newline at end of file diff --git a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts new file mode 100644 index 000000000..ac9c7cece --- /dev/null +++ b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts @@ -0,0 +1,506 @@ +import type { ReadableBoxedValues } from "$lib/internal/box.svelte.js"; +import { executeCallbacks } from "$lib/internal/callbacks.js"; +import { addEventListener } from "$lib/internal/events.js"; +import type { WithRefProps } from "$lib/internal/types.js"; +import { useRefById } from "$lib/internal/useRefById.svelte.js"; +import type { Direction, Orientation } from "$lib/shared/index.js"; +import { useDebounce } from "runed"; +import type { ScrollAreaType } from "./types.js"; +import { useStateMachine } from "$lib/internal/useStateMachine.svelte.js"; +import { clamp } from "$lib/internal/clamp.js"; +import { useResizeObserver } from "$lib/internal/useResizeObserver.svelte.js"; + +const SCROLL_AREA_ROOT_ATTR = "data-scroll-area-root"; +const SCROLL_AREA_VIEWPORT_ATTR = "data-scroll-area-viewport"; +const SCROLL_AREA_CONTENT_ATTR = "data-scroll-area-content"; +const SCROLL_AREA_CORNER_ATTR = "data-scroll-area-corner"; + +type Sizes = { + content: number; + viewport: number; + scrollbar: { + size: number; + paddingStart: number; + paddingEnd: number; + }; +}; + +type ScrollAreaRootStateProps = WithRefProps< + ReadableBoxedValues<{ + dir: Direction; + type: ScrollAreaType; + scrollHideDelay: number; + }> +>; + +class ScrollAreaRootState { + #id: ScrollAreaRootStateProps["id"]; + #ref: ScrollAreaRootStateProps["ref"]; + dir: ScrollAreaRootStateProps["dir"]; + type: ScrollAreaRootStateProps["type"]; + scrollHideDelay: ScrollAreaRootStateProps["scrollHideDelay"]; + scrollAreaNode = $state(null); + viewportNode = $state(null); + contentNode = $state(null); + scrollbarXNode = $state(null); + scrollbarYNode = $state(null); + cornerWidth = $state(0); + cornerHeight = $state(0); + scrollbarXEnabled = $state(false); + scrollbarYEnabled = $state(false); + + constructor(props: ScrollAreaRootStateProps) { + this.#id = props.id; + this.#ref = props.ref; + this.dir = props.dir; + this.type = props.type; + this.scrollHideDelay = props.scrollHideDelay; + + useRefById({ + id: this.#id, + ref: this.#ref, + onRefChange: (node) => { + this.scrollAreaNode = node; + }, + }); + } + + props = $derived.by( + () => + ({ + id: this.#id.value, + dir: this.dir.value, + style: { + position: "relative", + "--bits-scroll-area-corner-height": `${this.cornerHeight}px`, + "--bits-scroll-area-corner-width": `${this.cornerWidth}px`, + }, + [SCROLL_AREA_ROOT_ATTR]: "", + }) as const + ); +} + +type ScrollAreaViewportStateProps = WithRefProps; + +class ScrollAreaViewportState { + #id: ScrollAreaViewportStateProps["id"]; + #ref: ScrollAreaViewportStateProps["ref"]; + root: ScrollAreaRootState; + + constructor(props: ScrollAreaViewportStateProps, root: ScrollAreaRootState) { + this.#id = props.id; + this.#ref = props.ref; + this.root = root; + + useRefById({ + id: this.#id, + ref: this.#ref, + onRefChange: (node) => { + this.root.viewportNode = node; + }, + }); + } + + props = $derived.by( + () => + ({ + id: this.#id.value, + style: { + overflowX: this.root.scrollbarXEnabled ? "scroll" : "hidden", + overflowY: this.root.scrollbarYEnabled ? "scroll" : "hidden", + }, + [SCROLL_AREA_VIEWPORT_ATTR]: "", + }) as const + ); +} + +type ScrollAreaContentStateProps = WithRefProps; + +class ScrollAreaContentState { + #id: ScrollAreaContentStateProps["id"]; + #ref: ScrollAreaContentStateProps["ref"]; + root: ScrollAreaRootState; + + constructor(props: ScrollAreaContentStateProps, root: ScrollAreaRootState) { + this.#id = props.id; + this.#ref = props.ref; + this.root = root; + + useRefById({ + id: this.#id, + ref: this.#ref, + onRefChange: (node) => { + this.root.contentNode = node; + }, + }); + } + + props = $derived.by( + () => + ({ + id: this.#id.value, + style: { + minWidth: "100%", + display: "table", + }, + }) as const + ); +} + +type ScrollAreaScrollbarStateProps = WithRefProps< + ReadableBoxedValues<{ + orientation: Orientation; + }> +>; + +class ScrollAreaScrollbarState { + root: ScrollAreaRootState; + orientation: ScrollAreaScrollbarStateProps["orientation"]; + isHorizontal = $derived.by(() => this.orientation.value === "horizontal"); + + constructor(props: ScrollAreaScrollbarStateProps, root: ScrollAreaRootState) { + this.root = root; + this.orientation = props.orientation; + + $effect(() => { + this.isHorizontal + ? (this.root.scrollbarXEnabled = true) + : (this.root.scrollbarYEnabled = true); + + return () => { + this.isHorizontal + ? (this.root.scrollbarXEnabled = false) + : (this.root.scrollbarYEnabled = false); + }; + }); + } +} + +class ScrollAreaScrollbarHoverState { + root: ScrollAreaRootState; + isVisible = $state(false); + + constructor(scrollbar: ScrollAreaScrollbarState) { + this.root = scrollbar.root; + + $effect(() => { + const scrollAreaNode = this.root.scrollAreaNode; + const hideDelay = this.root.scrollHideDelay.value; + let hideTimer = 0; + if (scrollAreaNode) { + const handlePointerEnter = () => { + window.clearTimeout(hideTimer); + this.isVisible = true; + }; + + const handlePointerLeave = () => { + hideTimer = window.setTimeout(() => { + this.isVisible = false; + }, hideDelay); + }; + + const unsubListeners = executeCallbacks( + addEventListener(scrollAreaNode, "pointerenter", handlePointerEnter), + addEventListener(scrollAreaNode, "pointerleave", handlePointerLeave) + ); + + return () => { + window.clearTimeout(hideTimer); + unsubListeners(); + }; + } + }); + } + + props = $derived.by( + () => + ({ + "data-state": this.isVisible ? "visible" : "hidden", + }) as const + ); +} + +type ScrollAreaScrolbarScrollStateProps = WithRefProps & + ReadableBoxedValues<{ + orientation: Orientation; + }>; + +class ScrollAreaScrollbarScrollState { + orientation: ScrollAreaScrolbarScrollStateProps["orientation"]; + root: ScrollAreaRootState; + scrollbar: ScrollAreaScrollbarState; + machine = useStateMachine("hidden", { + hidden: { + SCROLL: "scrolling", + }, + scrolling: { + SCROLL_END: "idle", + POINTER_ENTER: "interacting", + }, + interacting: { + SCROLL: "interacting", + POINTER_LEAVE: "idle", + }, + idle: { + HIDE: "hidden", + SCROLL: "scrolling", + POINTER_ENTER: "interacting", + }, + }); + + constructor(props: ScrollAreaScrolbarScrollStateProps, scrollbar: ScrollAreaScrollbarState) { + this.scrollbar = scrollbar; + this.root = scrollbar.root; + this.orientation = props.orientation; + + const debounceScrollend = useDebounce(() => this.machine.dispatch("SCROLL_END"), 100); + + $effect(() => { + const _state = this.machine.state.value; + const scrollHideDelay = this.root.scrollHideDelay.value; + if (_state === "idle") { + const hideTimer = window.setTimeout( + () => this.machine.dispatch("HIDE"), + scrollHideDelay + ); + return () => window.clearTimeout(hideTimer); + } + }); + + $effect(() => { + const viewportNode = this.root.viewportNode; + if (!viewportNode) return; + const scrollDirection = this.scrollbar.isHorizontal ? "scrollLeft" : "scrollTop"; + + let prevScrollPos = viewportNode[scrollDirection]; + const handleScroll = () => { + const scrollPos = viewportNode[scrollDirection]; + const hasScrollInDirectionChanged = prevScrollPos !== scrollPos; + if (hasScrollInDirectionChanged) { + this.machine.dispatch("SCROLL"); + debounceScrollend(); + } + prevScrollPos = scrollPos; + }; + + const unsubListener = addEventListener(viewportNode, "scroll", handleScroll); + return unsubListener; + }); + } + + #onpointerenter = () => { + this.machine.dispatch("POINTER_ENTER"); + }; + + #onpointerleave = () => { + this.machine.dispatch("POINTER_LEAVE"); + }; + + props = $derived.by( + () => + ({ + "data-state": this.machine.state.value === "hidden" ? "hidden" : "visible", + onpointerenter: this.#onpointerenter, + onpointerleave: this.#onpointerleave, + }) as const + ); +} + +type ScrollAreaScrollbarAutoStateProps = WithRefProps; + +class ScrollAreaScrollbarAutoState { + scrollbar: ScrollAreaScrollbarState; + root: ScrollAreaRootState; + isVisible = $state(false); + + constructor(props: ScrollAreaScrollbarAutoStateProps, scrollbar: ScrollAreaScrollbarState) { + this.scrollbar = scrollbar; + this.root = scrollbar.root; + + const handleResize = useDebounce(() => { + const viewportNode = this.root.viewportNode; + if (!viewportNode) return; + const isOverflowX = viewportNode.offsetWidth < viewportNode.scrollWidth; + const isOverflowY = viewportNode.offsetHeight < viewportNode.scrollHeight; + this.isVisible = this.scrollbar.isHorizontal ? isOverflowX : isOverflowY; + }, 10); + + useResizeObserver(() => this.root.viewportNode, handleResize); + useResizeObserver(() => this.root.contentNode, handleResize); + } + + props = $derived.by( + () => + ({ + "data-state": this.isVisible ? "visible" : "hidden", + }) as const + ); +} + +class ScrollAreaScrollbarVisibleState { + scrollbar: ScrollAreaScrollbarState; + root: ScrollAreaRootState; + thumbNode = $state(null); + pointerOffset = $state(0); + sizes = $state({ + content: 0, + viewport: 0, + scrollbar: { size: 0, paddingStart: 0, paddingEnd: 0 }, + }); + thumbRatio = getThumbRatio(this.sizes.viewport, this.sizes.content); + hasThumb = $derived.by(() => Boolean(this.thumbRatio > 0 && this.thumbRatio < 1)); + + constructor(scrollbar: ScrollAreaScrollbarState) { + this.scrollbar = scrollbar; + this.root = scrollbar.root; + } + + getScrollPosition = (pointerPos: number, dir?: Direction) => { + return getScrollPositionFromPointer({ + pointerPos, + pointerOffset: this.pointerOffset, + sizes: this.sizes, + dir, + }); + }; + + onThumbPointerUp = () => { + this.pointerOffset = 0; + }; + + onThumbPointerDown = (pointerPos: number) => { + this.pointerOffset = pointerPos; + }; + + xOnThumbPositionChange = () => { + if (!(this.root.viewportNode && this.thumbNode)) return; + const scrollPos = this.root.viewportNode.scrollLeft; + const offset = getThumbOffsetFromScroll({ + scrollPos, + sizes: this.sizes, + dir: this.root.dir.value, + }); + this.thumbNode.style.transform = `translate3d(${offset}px, 0, 0)`; + }; + + xOnWheelScroll = (scrollPos: number) => { + if (!this.root.viewportNode) return; + this.root.viewportNode.scrollLeft = scrollPos; + }; + + xOnDragScroll = (pointerPos: number) => { + if (!this.root.viewportNode) return; + this.root.viewportNode.scrollLeft = this.getScrollPosition(pointerPos, this.root.dir.value); + }; + + yOnThumbPositionChange = () => { + if (!(this.root.viewportNode && this.thumbNode)) return; + const scrollPos = this.root.viewportNode.scrollTop; + const offset = getThumbOffsetFromScroll({ scrollPos, sizes: this.sizes }); + this.thumbNode.style.transform = `translate3d(0, ${offset}px, 0)`; + }; + + yOnWheelScroll = (scrollPos: number) => { + if (!this.root.viewportNode) return; + this.root.viewportNode.scrollTop = scrollPos; + }; + + yOnDragScroll = (pointerPos: number) => { + if (!this.root.viewportNode) return; + this.root.viewportNode.scrollTop = this.getScrollPosition(pointerPos); + }; +} + +function toInt(value?: string) { + return value ? parseInt(value, 10) : 0; +} + +function getThumbRatio(viewportSize: number, contentSize: number) { + const ratio = viewportSize / contentSize; + return Number.isNaN(ratio) ? 0 : ratio; +} + +function getThumbSize(sizes: Sizes) { + const ratio = getThumbRatio(sizes.viewport, sizes.content); + const scrollbarPadding = sizes.scrollbar.paddingStart + sizes.scrollbar.paddingEnd; + const thumbSize = (sizes.scrollbar.size - scrollbarPadding) * ratio; + return Math.max(thumbSize, 18); +} + +type GetScrollPositionFromPointerProps = { + pointerPos: number; + pointerOffset: number; + sizes: Sizes; + dir?: Direction; +}; + +function getScrollPositionFromPointer({ + pointerPos, + pointerOffset, + sizes, + dir = "ltr", +}: GetScrollPositionFromPointerProps) { + const thumbSizePx = getThumbSize(sizes); + const thumbCenter = thumbSizePx / 2; + const offset = pointerOffset || thumbCenter; + const thumbOffsetFromEnd = thumbSizePx - offset; + const minPointerPos = sizes.scrollbar.paddingStart + offset; + const maxPointerPos = sizes.scrollbar.size - sizes.scrollbar.paddingEnd - thumbOffsetFromEnd; + const maxScrollPos = sizes.content - sizes.viewport; + const scrollRange = dir === "ltr" ? [0, maxScrollPos] : [maxScrollPos * -1, 0]; + const interpolate = linearScale( + [minPointerPos, maxPointerPos], + scrollRange as [number, number] + ); + return interpolate(pointerPos); +} + +type GetThumbOffsetFromScrollProps = { + scrollPos: number; + sizes: Sizes; + dir?: Direction; +}; + +function getThumbOffsetFromScroll({ + scrollPos, + sizes, + dir = "ltr", +}: GetThumbOffsetFromScrollProps) { + const thumbSizePx = getThumbSize(sizes); + const scrollbarPadding = sizes.scrollbar.paddingStart + sizes.scrollbar.paddingEnd; + const scrollbar = sizes.scrollbar.size - scrollbarPadding; + const maxScrollPos = sizes.content - sizes.viewport; + const maxThumbPos = scrollbar - thumbSizePx; + const scrollClampRange = dir === "ltr" ? [0, maxScrollPos] : [maxScrollPos * -1, 0]; + const scrollWithoutMomentum = clamp(scrollPos, scrollClampRange[0]!, scrollClampRange[1]!); + const interpolate = linearScale([0, maxScrollPos], [0, maxThumbPos]); + return interpolate(scrollWithoutMomentum); +} + +// https://github.com/tmcw-up-for-adoption/simple-linear-scale/blob/master/index.js +function linearScale(input: readonly [number, number], output: readonly [number, number]) { + return (value: number) => { + if (input[0] === input[1] || output[0] === output[1]) return output[0]; + const ratio = (output[1] - output[0]) / (input[1] - input[0]); + return output[0] + ratio * (value - input[0]); + }; +} + +function isScrollingWithinScrollbarBounds(scrollPos: number, maxScrollPos: number) { + return scrollPos > 0 && scrollPos < maxScrollPos; +} + +function addUnlinkedScrollListener(node: HTMLElement, handler: () => void) { + let prevPosition = { left: node.scrollLeft, top: node.scrollTop }; + let rAF = 0; + (function loop() { + const position = { left: node.scrollLeft, top: node.scrollTop }; + const isHorizontalScroll = prevPosition.left !== position.left; + const isVerticalScroll = prevPosition.top !== position.top; + if (isHorizontalScroll || isVerticalScroll) handler(); + prevPosition = position; + rAF = window.requestAnimationFrame(loop); + })(); + + return () => window.cancelAnimationFrame(rAF); +} diff --git a/packages/bits-ui/src/lib/bits/scroll-area/types.ts b/packages/bits-ui/src/lib/bits/scroll-area/types.ts index c50800a83..842cb2867 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/types.ts +++ b/packages/bits-ui/src/lib/bits/scroll-area/types.ts @@ -1,26 +1,13 @@ -import type { CreateScrollAreaProps as MeltScrollAreaProps } from "@melt-ui/svelte"; -import type { DOMElement, Expand, HTMLDivAttributes } from "$lib/internal/types.js"; +import type { PrimitiveDivAttributes } from "$lib/internal/types.js"; +import type { Direction, WithChild, Without } from "$lib/shared/index.js"; -export type ScrollAreaPropsWithoutHTML = Expand> & - DOMElement; +export type ScrollAreaType = "hover" | "scroll" | "auto" | "always"; -type BaseDivProps = DOMElement; +export type ScrollAreaRootPropsWithoutHTML = WithChild<{ + type?: ScrollAreaType; + dir?: Direction; + scrollHideDelay?: number; +}>; -export type ScrollAreaScrollbarPropsWithoutHTML = BaseDivProps & { - orientation: "horizontal" | "vertical"; -}; - -export type ScrollAreaThumbPropsWithoutHTML = BaseDivProps; - -export type ScrollAreaViewportPropsWithoutHTML = BaseDivProps; -export type ScrollAreaContentPropsWithoutHTML = BaseDivProps; -export type ScrollAreaCornerPropsWithoutHTML = BaseDivProps; - -// - -export type ScrollAreaProps = ScrollAreaPropsWithoutHTML & HTMLDivAttributes; -export type ScrollAreaViewportProps = ScrollAreaViewportPropsWithoutHTML & HTMLDivAttributes; -export type ScrollAreaContentProps = ScrollAreaContentPropsWithoutHTML & HTMLDivAttributes; -export type ScrollAreaScrollbarProps = ScrollAreaScrollbarPropsWithoutHTML & HTMLDivAttributes; -export type ScrollAreaThumbProps = ScrollAreaThumbPropsWithoutHTML & HTMLDivAttributes; -export type ScrollAreaCornerProps = ScrollAreaCornerPropsWithoutHTML & HTMLDivAttributes; +export type ScrollAreaRootProps = ScrollAreaRootPropsWithoutHTML & + Without; diff --git a/packages/bits-ui/src/lib/internal/useResizeObserver.svelte.ts b/packages/bits-ui/src/lib/internal/useResizeObserver.svelte.ts new file mode 100644 index 000000000..133620afc --- /dev/null +++ b/packages/bits-ui/src/lib/internal/useResizeObserver.svelte.ts @@ -0,0 +1,19 @@ +import type { Getter } from "svelte-toolbelt"; + +export function useResizeObserver(node: Getter, onResize: () => void) { + $effect(() => { + let rAF = 0; + const _node = node(); + if (!_node) return; + const resizeObserver = new ResizeObserver(() => { + cancelAnimationFrame(rAF); + rAF = window.requestAnimationFrame(onResize); + }); + + resizeObserver.observe(_node); + return () => { + window.cancelAnimationFrame(rAF); + resizeObserver.unobserve(_node); + }; + }); +} From 8c078e1ecd153f2a0e127cbbb4e47a678c880577 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Sun, 21 Jul 2024 14:52:32 -0400 Subject: [PATCH 03/14] more scrollarea stuff --- .../components/scroll-area-content.svelte | 34 - .../components/scroll-area-corner-impl.svelte | 34 + .../components/scroll-area-corner.svelte | 37 +- .../scroll-area-scrollbar-auto.svelte | 18 + .../scroll-area-scrollbar-hover.svelte | 18 + .../scroll-area-scrollbar-scroll.svelte | 19 + .../scroll-area-scrollbar-shared.svelte | 19 + .../scroll-area-scrollbar-visible.svelte | 16 + .../components/scroll-area-scrollbar-x.svelte | 36 +- .../components/scroll-area-scrollbar-y.svelte | 36 +- .../components/scroll-area-scrollbar.svelte | 48 +- .../components/scroll-area-thumb-impl.svelte | 34 + .../components/scroll-area-thumb-x.svelte | 34 - .../components/scroll-area-thumb-y.svelte | 34 - .../components/scroll-area-thumb.svelte | 30 +- .../components/scroll-area-viewport.svelte | 52 +- .../scroll-area/components/scroll-area.svelte | 74 +-- .../bits-ui/src/lib/bits/scroll-area/index.ts | 4 +- .../bits/scroll-area/scroll-area.svelte.ts | 607 ++++++++++++++++-- .../bits-ui/src/lib/bits/scroll-area/types.ts | 58 +- packages/bits-ui/src/lib/internal/types.ts | 2 +- .../src/lib/internal/useRefById.svelte.ts | 7 + .../components/demos/scroll-area-demo.svelte | 28 +- 23 files changed, 931 insertions(+), 348 deletions(-) delete mode 100644 packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-content.svelte create mode 100644 packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-corner-impl.svelte create mode 100644 packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-auto.svelte create mode 100644 packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-hover.svelte create mode 100644 packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-scroll.svelte create mode 100644 packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-shared.svelte create mode 100644 packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-visible.svelte create mode 100644 packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-impl.svelte delete mode 100644 packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-x.svelte delete mode 100644 packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-y.svelte diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-content.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-content.svelte deleted file mode 100644 index e52fe9cd4..000000000 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-content.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - -{#if asChild} - -{:else} -
- -
-{/if} diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-corner-impl.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-corner-impl.svelte new file mode 100644 index 000000000..58f7c88af --- /dev/null +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-corner-impl.svelte @@ -0,0 +1,34 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-corner.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-corner.svelte index f1f963903..4c140d8c6 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-corner.svelte +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-corner.svelte @@ -1,34 +1,19 @@ -{#if asChild} - -{:else} -
- -
+{#if hasCorner} + {/if} diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-auto.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-auto.svelte new file mode 100644 index 000000000..5deb539a6 --- /dev/null +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-auto.svelte @@ -0,0 +1,18 @@ + + + + {#snippet presence()} + + {/snippet} + diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-hover.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-hover.svelte new file mode 100644 index 000000000..e95295673 --- /dev/null +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-hover.svelte @@ -0,0 +1,18 @@ + + + + {#snippet presence()} + + {/snippet} + diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-scroll.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-scroll.svelte new file mode 100644 index 000000000..875393728 --- /dev/null +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-scroll.svelte @@ -0,0 +1,19 @@ + + + + {#snippet presence()} + + {/snippet} + diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-shared.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-shared.svelte new file mode 100644 index 000000000..2daca8735 --- /dev/null +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-shared.svelte @@ -0,0 +1,19 @@ + + +{#if child} + {@render child?.({ props: mergedProps })} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-visible.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-visible.svelte new file mode 100644 index 000000000..c7ed29643 --- /dev/null +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-visible.svelte @@ -0,0 +1,16 @@ + + +{#if scrollbarVisibleState.scrollbar.orientation.value === "horizontal"} + +{:else} + +{/if} diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-x.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-x.svelte index 9de173ca6..fd82432d5 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-x.svelte +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-x.svelte @@ -1,34 +1,14 @@ -{#if asChild} - -{:else} -
- -
-{/if} + diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-y.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-y.svelte index 0187f169c..eeae9e1fa 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-y.svelte +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-y.svelte @@ -1,34 +1,14 @@ -{#if asChild} - -{:else} -
- -
-{/if} + diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar.svelte index 9bb94eb24..a580517ab 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar.svelte +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar.svelte @@ -1,26 +1,38 @@ -{#if $orientationStore === "vertical"} - - - -{:else} - - - +{#if type === "hover"} + +{:else if type === "scroll"} + +{:else if type === "auto"} + +{:else if type === "always"} + {/if} diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-impl.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-impl.svelte new file mode 100644 index 000000000..a086c64fc --- /dev/null +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-impl.svelte @@ -0,0 +1,34 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-x.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-x.svelte deleted file mode 100644 index 82697c5ed..000000000 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-x.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - -{#if asChild} - -{:else} -
- -
-{/if} diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-y.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-y.svelte deleted file mode 100644 index 341e3a786..000000000 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-y.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - -{#if asChild} - -{:else} -
- -
-{/if} diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb.svelte index 3f9ff0855..60da39621 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb.svelte +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb.svelte @@ -1,20 +1,22 @@ -{#if $orientation === "vertical"} - - - -{:else} - - - -{/if} + + {#snippet presence()} + + {/snippet} + diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-viewport.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-viewport.svelte index d185bbfa0..c2a9f6970 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-viewport.svelte +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-viewport.svelte @@ -1,37 +1,29 @@ -{#if asChild} - -{:else} -
- +
+
+ {@render children?.()}
-{/if} +
\ No newline at end of file + diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area.svelte index ae8093dc1..21a7071a7 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area.svelte +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area.svelte @@ -1,51 +1,39 @@ -{#if asChild} - +{#if child} + {@render child?.({ props: mergedProps })} {:else} -
- +
+ {@render children?.()}
{/if} diff --git a/packages/bits-ui/src/lib/bits/scroll-area/index.ts b/packages/bits-ui/src/lib/bits/scroll-area/index.ts index 87f702b39..19a5ac1ef 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/index.ts +++ b/packages/bits-ui/src/lib/bits/scroll-area/index.ts @@ -1,14 +1,12 @@ export { default as Root } from "./components/scroll-area.svelte"; export { default as Viewport } from "./components/scroll-area-viewport.svelte"; -export { default as Content } from "./components/scroll-area-content.svelte"; export { default as Scrollbar } from "./components/scroll-area-scrollbar.svelte"; export { default as Thumb } from "./components/scroll-area-thumb.svelte"; export { default as Corner } from "./components/scroll-area-corner.svelte"; export type { - ScrollAreaProps as Props, + ScrollAreaRootProps as RootProps, ScrollAreaViewportProps as ViewportProps, - ScrollAreaContentProps as ContentProps, ScrollAreaScrollbarProps as ScrollbarProps, ScrollAreaThumbProps as ThumbProps, ScrollAreaCornerProps as CornerProps, diff --git a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts index ac9c7cece..609283ef1 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts +++ b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts @@ -3,17 +3,20 @@ import { executeCallbacks } from "$lib/internal/callbacks.js"; import { addEventListener } from "$lib/internal/events.js"; import type { WithRefProps } from "$lib/internal/types.js"; import { useRefById } from "$lib/internal/useRefById.svelte.js"; -import type { Direction, Orientation } from "$lib/shared/index.js"; +import { mergeProps, useId, type Direction, type Orientation } from "$lib/shared/index.js"; import { useDebounce } from "runed"; import type { ScrollAreaType } from "./types.js"; import { useStateMachine } from "$lib/internal/useStateMachine.svelte.js"; import { clamp } from "$lib/internal/clamp.js"; import { useResizeObserver } from "$lib/internal/useResizeObserver.svelte.js"; +import { untrack } from "svelte"; +import { createContext } from "$lib/internal/createContext.js"; +import { box } from "svelte-toolbelt"; const SCROLL_AREA_ROOT_ATTR = "data-scroll-area-root"; const SCROLL_AREA_VIEWPORT_ATTR = "data-scroll-area-viewport"; -const SCROLL_AREA_CONTENT_ATTR = "data-scroll-area-content"; const SCROLL_AREA_CORNER_ATTR = "data-scroll-area-corner"; +const SCROLL_AREA_THUMB_ATTR = "data-scroll-area-thumb"; type Sizes = { content: number; @@ -78,6 +81,14 @@ class ScrollAreaRootState { [SCROLL_AREA_ROOT_ATTR]: "", }) as const ); + + createViewport(props: ScrollAreaViewportStateProps) { + return new ScrollAreaViewportState(props, this); + } + + createScrollbar(props: ScrollAreaScrollbarStateProps) { + return new ScrollAreaScrollbarState(props, this); + } } type ScrollAreaViewportStateProps = WithRefProps; @@ -85,6 +96,8 @@ type ScrollAreaViewportStateProps = WithRefProps; class ScrollAreaViewportState { #id: ScrollAreaViewportStateProps["id"]; #ref: ScrollAreaViewportStateProps["ref"]; + #contentId = box(useId()); + #contentRef = box(null); root: ScrollAreaRootState; constructor(props: ScrollAreaViewportStateProps, root: ScrollAreaRootState) { @@ -99,6 +112,14 @@ class ScrollAreaViewportState { this.root.viewportNode = node; }, }); + + useRefById({ + id: this.#contentId, + ref: this.#contentRef, + onRefChange: (node) => { + this.root.contentNode = node; + }, + }); } props = $derived.by( @@ -112,33 +133,11 @@ class ScrollAreaViewportState { [SCROLL_AREA_VIEWPORT_ATTR]: "", }) as const ); -} - -type ScrollAreaContentStateProps = WithRefProps; -class ScrollAreaContentState { - #id: ScrollAreaContentStateProps["id"]; - #ref: ScrollAreaContentStateProps["ref"]; - root: ScrollAreaRootState; - - constructor(props: ScrollAreaContentStateProps, root: ScrollAreaRootState) { - this.#id = props.id; - this.#ref = props.ref; - this.root = root; - - useRefById({ - id: this.#id, - ref: this.#ref, - onRefChange: (node) => { - this.root.contentNode = node; - }, - }); - } - - props = $derived.by( + contentProps = $derived.by( () => ({ - id: this.#id.value, + id: this.#contentId.value, style: { minWidth: "100%", display: "table", @@ -154,6 +153,8 @@ type ScrollAreaScrollbarStateProps = WithRefProps< >; class ScrollAreaScrollbarState { + ref: ScrollAreaScrollbarStateProps["ref"]; + id: ScrollAreaScrollbarStateProps["id"]; root: ScrollAreaRootState; orientation: ScrollAreaScrollbarStateProps["orientation"]; isHorizontal = $derived.by(() => this.orientation.value === "horizontal"); @@ -161,6 +162,8 @@ class ScrollAreaScrollbarState { constructor(props: ScrollAreaScrollbarStateProps, root: ScrollAreaRootState) { this.root = root; this.orientation = props.orientation; + this.ref = props.ref; + this.id = props.id; $effect(() => { this.isHorizontal @@ -174,6 +177,22 @@ class ScrollAreaScrollbarState { }; }); } + + createScrollbarHover() { + return new ScrollAreaScrollbarHoverState(this); + } + + createScrollbarScroll() { + return new ScrollAreaScrollbarScrollState(this); + } + + createScrollbarAuto() { + return new ScrollAreaScrollbarAutoState(this); + } + + createScrollbarVisible() { + return new ScrollAreaScrollbarVisibleState(this); + } } class ScrollAreaScrollbarHoverState { @@ -220,13 +239,8 @@ class ScrollAreaScrollbarHoverState { ); } -type ScrollAreaScrolbarScrollStateProps = WithRefProps & - ReadableBoxedValues<{ - orientation: Orientation; - }>; - class ScrollAreaScrollbarScrollState { - orientation: ScrollAreaScrolbarScrollStateProps["orientation"]; + orientation: ScrollAreaScrollbarState["orientation"]; root: ScrollAreaRootState; scrollbar: ScrollAreaScrollbarState; machine = useStateMachine("hidden", { @@ -247,11 +261,12 @@ class ScrollAreaScrollbarScrollState { POINTER_ENTER: "interacting", }, }); + isHidden = $derived.by(() => this.machine.state.value === "hidden"); - constructor(props: ScrollAreaScrolbarScrollStateProps, scrollbar: ScrollAreaScrollbarState) { + constructor(scrollbar: ScrollAreaScrollbarState) { this.scrollbar = scrollbar; this.root = scrollbar.root; - this.orientation = props.orientation; + this.orientation = this.scrollbar.orientation; const debounceScrollend = useDebounce(() => this.machine.dispatch("SCROLL_END"), 100); @@ -306,14 +321,12 @@ class ScrollAreaScrollbarScrollState { ); } -type ScrollAreaScrollbarAutoStateProps = WithRefProps; - class ScrollAreaScrollbarAutoState { scrollbar: ScrollAreaScrollbarState; root: ScrollAreaRootState; isVisible = $state(false); - constructor(props: ScrollAreaScrollbarAutoStateProps, scrollbar: ScrollAreaScrollbarState) { + constructor(scrollbar: ScrollAreaScrollbarState) { this.scrollbar = scrollbar; this.root = scrollbar.root; @@ -342,7 +355,7 @@ class ScrollAreaScrollbarVisibleState { root: ScrollAreaRootState; thumbNode = $state(null); pointerOffset = $state(0); - sizes = $state({ + sizes = $state.frozen({ content: 0, viewport: 0, scrollbar: { size: 0, paddingStart: 0, paddingEnd: 0 }, @@ -409,6 +422,524 @@ class ScrollAreaScrollbarVisibleState { if (!this.root.viewportNode) return; this.root.viewportNode.scrollTop = this.getScrollPosition(pointerPos); }; + + createScrollbarX() { + return new ScrollAreaScrollbarXState(this); + } + + createScrollbarY() { + return new ScrollAreaScrollbarYState(this); + } +} + +type ScrollbarAxisState = { + onThumbPointerDown: (pointerPos: { x: number; y: number }) => void; + onDragScroll: (pointerPos: { x: number; y: number }) => void; + onWheelScroll: (e: WheelEvent, maxScrollPos: number) => void; + onResize: () => void; + onThumbPositionChange: () => void; + onThumbPointerUp: () => void; + createShared: () => ScrollAreaScrollbarSharedState; + props: { + id: string; + "data-orientation": "horizontal" | "vertical"; + style: Record; + }; +}; + +class ScrollAreaScrollbarXState implements ScrollbarAxisState { + #id: WithRefProps["id"]; + ref: WithRefProps["ref"]; + scrollbarVis: ScrollAreaScrollbarVisibleState; + root: ScrollAreaRootState; + computedStyle = $state(); + + constructor(scrollbarVis: ScrollAreaScrollbarVisibleState) { + this.scrollbarVis = scrollbarVis; + this.#id = this.scrollbarVis.scrollbar.id; + this.ref = this.scrollbarVis.scrollbar.ref; + this.root = scrollbarVis.root; + + useRefById({ + id: this.#id, + ref: this.ref, + onRefChange: (node) => { + this.root.scrollbarXNode = node; + }, + }); + + $effect(() => { + if (!this.ref.value) return; + this.computedStyle = getComputedStyle(this.ref.value); + }); + } + + onThumbPointerDown = (pointerPos: { x: number; y: number }) => { + this.scrollbarVis.onThumbPointerDown(pointerPos.x); + }; + + onThumbPointerUp = () => { + this.scrollbarVis.onThumbPointerUp(); + }; + + onThumbPositionChange = () => { + this.scrollbarVis.xOnThumbPositionChange(); + }; + + onDragScroll = (pointerPos: { x: number; y: number }) => { + this.scrollbarVis.xOnDragScroll(pointerPos.x); + }; + + onWheelScroll = (e: WheelEvent, maxScrollPos: number) => { + if (!this.root.viewportNode) return; + const scrollPos = this.root.viewportNode.scrollLeft + e.deltaX; + this.scrollbarVis.xOnWheelScroll(scrollPos); + // prevent window scroll when wheeling scrollbar + if (isScrollingWithinScrollbarBounds(scrollPos, maxScrollPos)) { + e.preventDefault(); + } + }; + + onResize = () => { + if (!(this.ref.value && this.root.viewportNode && this.computedStyle)) return; + this.scrollbarVis.sizes = { + content: this.root.viewportNode.scrollWidth, + viewport: this.root.viewportNode.offsetWidth, + scrollbar: { + size: this.ref.value.clientWidth, + paddingStart: toInt(this.computedStyle.paddingLeft), + paddingEnd: toInt(this.computedStyle.paddingRight), + }, + }; + }; + + props = $derived.by( + () => + ({ + id: this.#id.value, + "data-orientation": "horizontal", + style: { + bottom: 0, + left: + this.root.dir.value === "rtl" ? "var(--bits-scroll-area-corner-width)" : 0, + right: + this.root.dir.value === "ltr" ? "var(--bits-scroll-area-corner-width)" : 0, + "--bits-scroll-area-thumb-width": `${getThumbSize(this.scrollbarVis.sizes)}px`, + }, + }) as const + ); + + createShared() { + return new ScrollAreaScrollbarSharedState(this); + } +} + +class ScrollAreaScrollbarYState implements ScrollbarAxisState { + #id: WithRefProps["id"]; + ref: WithRefProps["ref"]; + scrollbarVis: ScrollAreaScrollbarVisibleState; + root: ScrollAreaRootState; + computedStyle = $state(); + + constructor(scrollbarVis: ScrollAreaScrollbarVisibleState) { + this.scrollbarVis = scrollbarVis; + this.#id = this.scrollbarVis.scrollbar.id; + this.ref = this.scrollbarVis.scrollbar.ref; + this.root = scrollbarVis.root; + + useRefById({ + id: this.scrollbarVis.scrollbar.id, + ref: this.scrollbarVis.scrollbar.ref, + onRefChange: (node) => { + this.root.scrollbarYNode = node; + }, + }); + } + + onThumbPointerDown = (pointerPos: { x: number; y: number }) => { + this.scrollbarVis.onThumbPointerDown(pointerPos.y); + }; + + onDragScroll = (pointerPos: { x: number; y: number }) => { + this.scrollbarVis.yOnDragScroll(pointerPos.y); + }; + + onThumbPointerUp = () => { + this.scrollbarVis.onThumbPointerUp(); + }; + + onThumbPositionChange = () => { + this.scrollbarVis.yOnThumbPositionChange(); + }; + + onWheelScroll = (e: WheelEvent, maxScrollPos: number) => { + if (!this.root.viewportNode) return; + const scrollPos = this.root.viewportNode.scrollTop + e.deltaY; + this.scrollbarVis.yOnWheelScroll(scrollPos); + // prevent window scroll when wheeling scrollbar + if (isScrollingWithinScrollbarBounds(scrollPos, maxScrollPos)) { + e.preventDefault(); + } + }; + + onResize = () => { + if (!(this.ref.value && this.root.viewportNode && this.computedStyle)) return; + this.scrollbarVis.sizes = { + content: this.root.viewportNode.scrollHeight, + viewport: this.root.viewportNode.offsetHeight, + scrollbar: { + size: this.ref.value.clientHeight, + paddingStart: toInt(this.computedStyle.paddingTop), + paddingEnd: toInt(this.computedStyle.paddingBottom), + }, + }; + }; + + props = $derived.by( + () => + ({ + id: this.#id.value, + "data-orientation": "vertical", + style: { + top: 0, + right: this.root.dir.value === "ltr" ? 0 : undefined, + left: this.root.dir.value === "rtl" ? 0 : undefined, + bottom: "var(--bits-scroll-area-corner-height)", + "--bits-scroll-area-thumb-height": `${getThumbSize(this.scrollbarVis.sizes)}px`, + }, + }) as const + ); + + createShared() { + return new ScrollAreaScrollbarSharedState(this); + } +} + +type ScrollbarAxis = ScrollAreaScrollbarXState | ScrollAreaScrollbarYState; + +class ScrollAreaScrollbarSharedState { + scrollbarState: ScrollbarAxis; + root: ScrollAreaRootState; + scrollbarVis: ScrollAreaScrollbarVisibleState; + rect = $state.frozen(null); + prevWebkitUserSelect = $state(""); + maxScrollPos = $state(0); + handleResize: () => void; + handleThumbPositionChange: () => void; + handleWheelScroll: (e: WheelEvent, maxScrollPos: number) => void; + handleThumbPointerDown: (pointerPos: { x: number; y: number }) => void; + handleThumbPointerUp: () => void; + + constructor(scrollbarState: ScrollbarAxis) { + this.scrollbarState = scrollbarState; + this.root = scrollbarState.root; + this.scrollbarVis = scrollbarState.scrollbarVis; + this.maxScrollPos = this.scrollbarVis.sizes.content - this.scrollbarVis.sizes.viewport; + this.handleResize = useDebounce(() => this.scrollbarState.onResize(), 10); + this.handleThumbPositionChange = this.scrollbarState.onThumbPositionChange; + this.handleWheelScroll = this.scrollbarState.onWheelScroll; + this.handleThumbPointerDown = this.scrollbarState.onThumbPointerDown; + this.handleThumbPointerUp = this.scrollbarState.onThumbPointerUp; + + $effect(() => { + const maxScrollPos = this.maxScrollPos; + const scrollbarNode = this.scrollbarState.ref.value; + // we want to react to the viewport node changing so we leave this here + this.root.viewportNode; + const handleWheel = (e: WheelEvent) => { + const node = e.target as HTMLElement; + const isScrollbarWheel = scrollbarNode?.contains(node); + if (isScrollbarWheel) this.handleWheelScroll(e, maxScrollPos); + }; + + const unsubListener = addEventListener(document, "wheel", handleWheel, { + passive: false, + }); + + return unsubListener; + }); + + $effect(() => { + // react to changes to this: + this.scrollbarVis.sizes; + untrack(() => this.handleThumbPositionChange()); + }); + + useResizeObserver(() => this.scrollbarState.ref.value, this.handleResize); + useResizeObserver(() => this.root.contentNode, this.handleResize); + } + + get thumbNode() { + return this.scrollbarVis.thumbNode; + } + + set thumbNode(node: HTMLElement | null) { + this.scrollbarVis.thumbNode = node; + } + + get hasThumb() { + return this.scrollbarVis.hasThumb; + } + + handleDragScroll = (e: PointerEvent) => { + if (!this.rect) return; + const x = e.clientX - this.rect.left; + const y = e.clientY - this.rect.top; + this.scrollbarState.onDragScroll({ x, y }); + }; + + #onpointerdown = (e: PointerEvent) => { + if (e.button !== 0) return; + const target = e.target as HTMLElement; + target.setPointerCapture(e.pointerId); + this.rect = this.scrollbarState.ref.value?.getBoundingClientRect() ?? null; + // pointer capture doesn't prevent text selection in Safari + // so we remove text selection manually when scrolling + this.prevWebkitUserSelect = document.body.style.webkitUserSelect; + document.body.style.webkitUserSelect = "none"; + if (this.root.viewportNode) this.root.viewportNode.style.scrollBehavior = "auto"; + this.handleDragScroll(e); + }; + + #onpointermove = (e: PointerEvent) => { + this.handleDragScroll(e); + }; + + #onpointerup = (e: PointerEvent) => { + const target = e.target as HTMLElement; + if (target.hasPointerCapture(e.pointerId)) { + target.releasePointerCapture(e.pointerId); + } + document.body.style.webkitUserSelect = this.prevWebkitUserSelect; + if (this.root.viewportNode) this.root.viewportNode.style.scrollBehavior = ""; + this.rect = null; + }; + + props = $derived.by(() => + mergeProps({ + ...this.scrollbarState.props, + style: { + position: "absolute", + ...this.scrollbarState.props.style, + }, + onpointerdown: this.#onpointerdown, + onpointermove: this.#onpointermove, + onpointerup: this.#onpointerup, + }) + ); + + createThumb(props: ScrollAreaThumbImplStateProps) { + return new ScrollAreaThumbImplState(props, this); + } + + createCorner(props: ScrollAreaCornerImplStateProps) { + return new ScrollAreaCornerImplState(props, this); + } +} + +type ScrollAreaThumbImplStateProps = WithRefProps; +class ScrollAreaThumbImplState { + #id: ScrollAreaThumbImplStateProps["id"]; + #ref: ScrollAreaThumbImplStateProps["ref"]; + #root: ScrollAreaRootState; + #scrollbarState: ScrollAreaScrollbarSharedState; + #removeUnlinkedScrollListener = $state<() => void>(); + #debounceScrollEnd = useDebounce(() => { + if (this.#removeUnlinkedScrollListener) { + this.#removeUnlinkedScrollListener(); + this.#removeUnlinkedScrollListener = undefined; + } + }, 100); + + constructor( + props: ScrollAreaThumbImplStateProps, + scrollbarState: ScrollAreaScrollbarSharedState + ) { + this.#root = scrollbarState.root; + this.#scrollbarState = scrollbarState; + this.#id = props.id; + this.#ref = props.ref; + + useRefById({ + id: this.#id, + ref: this.#ref, + onRefChange: (node) => { + this.#scrollbarState.thumbNode = node; + }, + }); + + $effect(() => { + const viewportNode = this.#root.viewportNode; + if (!viewportNode) return; + const handleScroll = () => { + this.#debounceScrollEnd(); + if (!this.#removeUnlinkedScrollListener) { + const listener = addUnlinkedScrollListener( + viewportNode, + this.#scrollbarState.handleThumbPositionChange + ); + this.#removeUnlinkedScrollListener = listener; + this.#scrollbarState.handleThumbPositionChange(); + } + }; + this.#scrollbarState.handleThumbPositionChange(); + const unsubListener = addEventListener(viewportNode, "scroll", handleScroll); + return unsubListener; + }); + } + + #onpointerdowncapture = (e: PointerEvent) => { + const thumb = e.target as HTMLElement; + if (!thumb) return; + const thumbRect = thumb.getBoundingClientRect(); + const x = e.clientX - thumbRect.left; + const y = e.clientY - thumbRect.top; + this.#scrollbarState.handleThumbPointerDown({ x, y }); + }; + + #onpointerup = (e: PointerEvent) => { + this.#scrollbarState.handleThumbPointerUp(); + }; + + props = $derived.by( + () => + ({ + id: this.#id.value, + "data-state": this.#scrollbarState.hasThumb ? "visible" : "hidden", + style: { + width: "var(--bits-scroll-area-thumb-width)", + height: "var(--bits-scroll-area-thumb-height)", + }, + onpointerdowncapture: this.#onpointerdowncapture, + onpointerup: this.#onpointerup, + [SCROLL_AREA_THUMB_ATTR]: "", + }) as const + ); +} + +type ScrollAreaCornerImplStateProps = WithRefProps; + +class ScrollAreaCornerImplState { + #id: ScrollAreaCornerImplStateProps["id"]; + #ref: ScrollAreaCornerImplStateProps["ref"]; + #root: ScrollAreaRootState; + scrollbarState: ScrollAreaScrollbarSharedState; + #width = $state(0); + #height = $state(0); + hasSize = $derived(Boolean(this.#width && this.#height)); + + constructor( + props: ScrollAreaCornerImplStateProps, + scrollbarState: ScrollAreaScrollbarSharedState + ) { + this.#root = scrollbarState.root; + this.scrollbarState = scrollbarState; + this.#id = props.id; + this.#ref = props.ref; + + useResizeObserver( + () => this.#root.scrollbarXNode, + () => { + const height = this.#root.scrollbarXNode?.offsetHeight || 0; + this.#root.cornerHeight = height; + this.#height = height; + } + ); + + useResizeObserver( + () => this.#root.scrollbarYNode, + () => { + const width = this.#root.scrollbarYNode?.offsetWidth || 0; + this.#root.cornerWidth = width; + this.#width = width; + } + ); + + useRefById({ + id: this.#id, + ref: this.#ref, + }); + } + + props = $derived.by(() => ({ + id: this.#id.value, + style: { + width: this.#width, + height: this.#height, + position: "absolute", + right: this.#root.dir.value === "ltr" ? 0 : undefined, + left: this.#root.dir.value === "rtl" ? 0 : undefined, + bottom: 0, + }, + [SCROLL_AREA_CORNER_ATTR]: "", + })); +} + +export const [setScrollAreaRootContext, getScrollAreaRootContext] = + createContext("ScrollArea.Root"); + +export const [setScrollAreaScrollbarContext, getScrollAreaScrollbarContext] = + createContext("ScrollArea.Scrollbar"); + +export const [setScrollAreaScrollbarVisibleContext, getScrollAreaScrollbarVisibleContext] = + createContext("ScrollArea.ScrollbarVisible"); + +export const [setScrollAreaScrollbarAxisContext, getScrollAreaScrollbarAxisContext] = + createContext("ScrollArea.ScrollbarAxis"); + +export const [setScrollAreaScrollbarSharedContext, getScrollAreaScrollbarSharedContext] = + createContext("ScrollArea.ScrollbarShared"); + +export function useScrollAreaRoot(props: ScrollAreaRootStateProps) { + return setScrollAreaRootContext(new ScrollAreaRootState(props)); +} + +export function useScrollAreaViewport(props: ScrollAreaViewportStateProps) { + return getScrollAreaRootContext().createViewport(props); +} + +export function useScrollAreaScrollbar(props: ScrollAreaScrollbarStateProps) { + return setScrollAreaScrollbarContext(getScrollAreaRootContext().createScrollbar(props)); +} + +export function useScrollAreaScrollbarVisible() { + return getScrollAreaScrollbarContext().createScrollbarVisible(); +} + +export function useScrollAreaScrollbarAuto() { + return getScrollAreaScrollbarContext().createScrollbarAuto(); +} + +export function useScrollAreaScrollbarScroll() { + return getScrollAreaScrollbarContext().createScrollbarScroll(); +} + +export function useScrollAreaScrollbarHover() { + return getScrollAreaScrollbarContext().createScrollbarHover(); +} + +export function useScrollAreaScrollbarX() { + return setScrollAreaScrollbarAxisContext( + getScrollAreaScrollbarVisibleContext().createScrollbarX() + ); +} + +export function useScrollAreaScrollbarY() { + return setScrollAreaScrollbarAxisContext( + getScrollAreaScrollbarVisibleContext().createScrollbarY() + ); +} + +export function useScrollAreaScrollbarShared() { + return setScrollAreaScrollbarSharedContext(getScrollAreaScrollbarAxisContext().createShared()); +} + +export function useScrollAreaThumb(props: ScrollAreaThumbImplStateProps) { + return getScrollAreaScrollbarSharedContext().createThumb(props); +} + +export function useScrollAreaCorner(props: ScrollAreaCornerImplStateProps) { + return getScrollAreaScrollbarSharedContext().createCorner(props); } function toInt(value?: string) { diff --git a/packages/bits-ui/src/lib/bits/scroll-area/types.ts b/packages/bits-ui/src/lib/bits/scroll-area/types.ts index 842cb2867..051fd08c8 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/types.ts +++ b/packages/bits-ui/src/lib/bits/scroll-area/types.ts @@ -1,13 +1,67 @@ -import type { PrimitiveDivAttributes } from "$lib/internal/types.js"; -import type { Direction, WithChild, Without } from "$lib/shared/index.js"; +import type { PrimitiveDivAttributes, WithChildren } from "$lib/internal/types.js"; +import type { Direction, Orientation, WithChild, Without } from "$lib/shared/index.js"; export type ScrollAreaType = "hover" | "scroll" | "auto" | "always"; export type ScrollAreaRootPropsWithoutHTML = WithChild<{ + /** + * The type of scroll area to render. + * + * @defaultValue "hover" + */ type?: ScrollAreaType; + + /** + * The reading direction of the application. + */ dir?: Direction; + + /** + * The amount of time in milliseconds to delay before hiding the scrollbars + * after leaving the scroll area or stopping scrolling. + * + * @defaultValue 600 + */ scrollHideDelay?: number; }>; export type ScrollAreaRootProps = ScrollAreaRootPropsWithoutHTML & Without; + +export type ScrollAreaViewportPropsWithoutHTML = Omit; + +export type ScrollAreaViewportProps = ScrollAreaViewportPropsWithoutHTML & + Without; + +export type ScrollAreaScrollbarPropsWithoutHTML = WithChild<{ + orientation: Orientation; + + /** + * Whether to forcefully mount the component. Useful when working with + * external animation/transition libraries. + */ + forceMount?: boolean; +}>; + +export type ScrollAreaScrollbarProps = ScrollAreaScrollbarPropsWithoutHTML & + Without; + +export type ScrollAreaThumbPropsWithoutHTML = WithChild<{ + /** + * Whether to forcefully mount the component. Useful when working with + * external animation/transition libraries. + */ + forceMount?: boolean; +}>; + +export type ScrollAreaThumbProps = ScrollAreaThumbPropsWithoutHTML & + Without; + +export type ScrollAreaCornerPropsWithoutHTML = WithChild; + +export type ScrollAreaCornerProps = ScrollAreaCornerPropsWithoutHTML & + Without; + +export type _ScrollbarStubProps = Omit & { + id: string; +}; diff --git a/packages/bits-ui/src/lib/internal/types.ts b/packages/bits-ui/src/lib/internal/types.ts index a85fc4cd2..18c43a01a 100644 --- a/packages/bits-ui/src/lib/internal/types.ts +++ b/packages/bits-ui/src/lib/internal/types.ts @@ -174,7 +174,7 @@ export type WithChild< ref?: Ref | null; }; -export type WithChildren = Props & { +export type WithChildren = Props & { children?: Snippet; }; diff --git a/packages/bits-ui/src/lib/internal/useRefById.svelte.ts b/packages/bits-ui/src/lib/internal/useRefById.svelte.ts index 1e92ab26d..d4dd2f04d 100644 --- a/packages/bits-ui/src/lib/internal/useRefById.svelte.ts +++ b/packages/bits-ui/src/lib/internal/useRefById.svelte.ts @@ -49,4 +49,11 @@ export function useRefById({ onRefChange(ref.value); }); }); + + $effect(() => { + return () => { + ref.value = null; + onRefChange(null); + }; + }); } diff --git a/sites/docs/src/lib/components/demos/scroll-area-demo.svelte b/sites/docs/src/lib/components/demos/scroll-area-demo.svelte index 4b833eb80..6ed25d6e1 100644 --- a/sites/docs/src/lib/components/demos/scroll-area-demo.svelte +++ b/sites/docs/src/lib/components/demos/scroll-area-demo.svelte @@ -7,21 +7,19 @@ > - -

- Scroll Area -

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit - rem, repellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea - deleniti eveniet incidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem - ipsum dolor sit, amet consectetur adipisicing elit. Blanditiis officiis error - minima eos fugit voluptate excepturi eveniet dolore et, ratione impedit - consequuntur dolorem hic quae corrupti autem? Dolorem, sit voluptatum. -

-
+

+ Scroll Area +

+

+ Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit rem, + repellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea deleniti + eveniet incidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem ipsum dolor + sit, amet consectetur adipisicing elit. Blanditiis officiis error minima eos fugit + voluptate excepturi eveniet dolore et, ratione impedit consequuntur dolorem hic quae + corrupti autem? Dolorem, sit voluptatum. +

Date: Sun, 21 Jul 2024 16:19:15 -0400 Subject: [PATCH 04/14] always --- .../components/scroll-area-scrollbar-x.svelte | 7 +- .../components/scroll-area-scrollbar-y.svelte | 10 +- .../components/scroll-area-thumb-impl.svelte | 4 + .../scroll-area/components/scroll-area.svelte | 2 +- .../bits/scroll-area/scroll-area.svelte.ts | 95 ++++++++++++------- .../bits-ui/src/lib/internal/cssToStyleObj.ts | 4 + packages/bits-ui/src/lib/internal/style.ts | 3 +- .../components/demos/scroll-area-demo.svelte | 6 +- 8 files changed, 88 insertions(+), 43 deletions(-) diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-x.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-x.svelte index fd82432d5..48326a84d 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-x.svelte +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-x.svelte @@ -1,4 +1,6 @@ diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-y.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-y.svelte index eeae9e1fa..d3ae04865 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-y.svelte +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-y.svelte @@ -1,4 +1,6 @@ diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-impl.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-impl.svelte index a086c64fc..b1814dfce 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-impl.svelte +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-impl.svelte @@ -1,5 +1,6 @@ {#if child} - {@render child?.({ props: mergedProps })} + {@render child({ props: mergedProps })} {:else}
{@render children?.()} diff --git a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts index 609283ef1..76f7227c7 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts +++ b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts @@ -12,6 +12,8 @@ import { useResizeObserver } from "$lib/internal/useResizeObserver.svelte.js"; import { untrack } from "svelte"; import { createContext } from "$lib/internal/createContext.js"; import { box } from "svelte-toolbelt"; +import { afterTick } from "$lib/internal/afterTick.js"; +import { sleep } from "$lib/internal/sleep.js"; const SCROLL_AREA_ROOT_ATTR = "data-scroll-area-root"; const SCROLL_AREA_VIEWPORT_ATTR = "data-scroll-area-viewport"; @@ -360,7 +362,7 @@ class ScrollAreaScrollbarVisibleState { viewport: 0, scrollbar: { size: 0, paddingStart: 0, paddingEnd: 0 }, }); - thumbRatio = getThumbRatio(this.sizes.viewport, this.sizes.content); + thumbRatio = $derived.by(() => getThumbRatio(this.sizes.viewport, this.sizes.content)); hasThumb = $derived.by(() => Boolean(this.thumbRatio > 0 && this.thumbRatio < 1)); constructor(scrollbar: ScrollAreaScrollbarState) { @@ -368,6 +370,10 @@ class ScrollAreaScrollbarVisibleState { this.root = scrollbar.root; } + setSizes = (sizes: Sizes) => { + this.sizes = sizes; + }; + getScrollPosition = (pointerPos: number, dir?: Direction) => { return getScrollPositionFromPointer({ pointerPos, @@ -423,15 +429,17 @@ class ScrollAreaScrollbarVisibleState { this.root.viewportNode.scrollTop = this.getScrollPosition(pointerPos); }; - createScrollbarX() { - return new ScrollAreaScrollbarXState(this); + createScrollbarX(props: ScrollbarAxisStateProps) { + return new ScrollAreaScrollbarXState(props, this); } - createScrollbarY() { - return new ScrollAreaScrollbarYState(this); + createScrollbarY(props: ScrollbarAxisStateProps) { + return new ScrollAreaScrollbarYState(props, this); } } +type ScrollbarAxisStateProps = ReadableBoxedValues<{ mounted: boolean }>; + type ScrollbarAxisState = { onThumbPointerDown: (pointerPos: { x: number; y: number }) => void; onDragScroll: (pointerPos: { x: number; y: number }) => void; @@ -449,12 +457,14 @@ type ScrollbarAxisState = { class ScrollAreaScrollbarXState implements ScrollbarAxisState { #id: WithRefProps["id"]; + #mounted: ScrollbarAxisStateProps["mounted"]; ref: WithRefProps["ref"]; scrollbarVis: ScrollAreaScrollbarVisibleState; root: ScrollAreaRootState; computedStyle = $state(); - constructor(scrollbarVis: ScrollAreaScrollbarVisibleState) { + constructor(props: ScrollbarAxisStateProps, scrollbarVis: ScrollAreaScrollbarVisibleState) { + this.#mounted = props.mounted; this.scrollbarVis = scrollbarVis; this.#id = this.scrollbarVis.scrollbar.id; this.ref = this.scrollbarVis.scrollbar.ref; @@ -466,11 +476,12 @@ class ScrollAreaScrollbarXState implements ScrollbarAxisState { onRefChange: (node) => { this.root.scrollbarXNode = node; }, + condition: () => this.#mounted.value, }); $effect(() => { if (!this.ref.value) return; - this.computedStyle = getComputedStyle(this.ref.value); + this.computedStyle = getComputedStyle(this.ref.value!); }); } @@ -502,7 +513,7 @@ class ScrollAreaScrollbarXState implements ScrollbarAxisState { onResize = () => { if (!(this.ref.value && this.root.viewportNode && this.computedStyle)) return; - this.scrollbarVis.sizes = { + this.scrollbarVis.setSizes({ content: this.root.viewportNode.scrollWidth, viewport: this.root.viewportNode.offsetWidth, scrollbar: { @@ -510,7 +521,7 @@ class ScrollAreaScrollbarXState implements ScrollbarAxisState { paddingStart: toInt(this.computedStyle.paddingLeft), paddingEnd: toInt(this.computedStyle.paddingRight), }, - }; + }); }; props = $derived.by( @@ -537,11 +548,13 @@ class ScrollAreaScrollbarXState implements ScrollbarAxisState { class ScrollAreaScrollbarYState implements ScrollbarAxisState { #id: WithRefProps["id"]; ref: WithRefProps["ref"]; + #mounted: ScrollbarAxisStateProps["mounted"]; scrollbarVis: ScrollAreaScrollbarVisibleState; root: ScrollAreaRootState; computedStyle = $state(); - constructor(scrollbarVis: ScrollAreaScrollbarVisibleState) { + constructor(props: ScrollbarAxisStateProps, scrollbarVis: ScrollAreaScrollbarVisibleState) { + this.#mounted = props.mounted; this.scrollbarVis = scrollbarVis; this.#id = this.scrollbarVis.scrollbar.id; this.ref = this.scrollbarVis.scrollbar.ref; @@ -553,6 +566,16 @@ class ScrollAreaScrollbarYState implements ScrollbarAxisState { onRefChange: (node) => { this.root.scrollbarYNode = node; }, + condition: () => this.#mounted.value, + }); + + $effect(() => { + if (!this.ref.value) return; + if (this.#mounted.value) { + afterTick(() => { + this.computedStyle = getComputedStyle(this.ref.value!); + }); + } }); } @@ -584,7 +607,7 @@ class ScrollAreaScrollbarYState implements ScrollbarAxisState { onResize = () => { if (!(this.ref.value && this.root.viewportNode && this.computedStyle)) return; - this.scrollbarVis.sizes = { + this.scrollbarVis.setSizes({ content: this.root.viewportNode.scrollHeight, viewport: this.root.viewportNode.offsetHeight, scrollbar: { @@ -592,9 +615,14 @@ class ScrollAreaScrollbarYState implements ScrollbarAxisState { paddingStart: toInt(this.computedStyle.paddingTop), paddingEnd: toInt(this.computedStyle.paddingBottom), }, - }; + }); }; + thumbSize = $derived.by(() => { + const ts = getThumbSize(this.scrollbarVis.sizes); + return ts; + }); + props = $derived.by( () => ({ @@ -605,7 +633,7 @@ class ScrollAreaScrollbarYState implements ScrollbarAxisState { right: this.root.dir.value === "ltr" ? 0 : undefined, left: this.root.dir.value === "rtl" ? 0 : undefined, bottom: "var(--bits-scroll-area-corner-height)", - "--bits-scroll-area-thumb-height": `${getThumbSize(this.scrollbarVis.sizes)}px`, + "--bits-scroll-area-thumb-height": `${this.thumbSize}px`, }, }) as const ); @@ -623,18 +651,19 @@ class ScrollAreaScrollbarSharedState { scrollbarVis: ScrollAreaScrollbarVisibleState; rect = $state.frozen(null); prevWebkitUserSelect = $state(""); - maxScrollPos = $state(0); handleResize: () => void; handleThumbPositionChange: () => void; handleWheelScroll: (e: WheelEvent, maxScrollPos: number) => void; handleThumbPointerDown: (pointerPos: { x: number; y: number }) => void; handleThumbPointerUp: () => void; + maxScrollPos = $derived.by( + () => this.scrollbarVis.sizes.content - this.scrollbarVis.sizes.viewport + ); constructor(scrollbarState: ScrollbarAxis) { this.scrollbarState = scrollbarState; this.root = scrollbarState.root; this.scrollbarVis = scrollbarState.scrollbarVis; - this.maxScrollPos = this.scrollbarVis.sizes.content - this.scrollbarVis.sizes.viewport; this.handleResize = useDebounce(() => this.scrollbarState.onResize(), 10); this.handleThumbPositionChange = this.scrollbarState.onThumbPositionChange; this.handleWheelScroll = this.scrollbarState.onWheelScroll; @@ -669,18 +698,6 @@ class ScrollAreaScrollbarSharedState { useResizeObserver(() => this.root.contentNode, this.handleResize); } - get thumbNode() { - return this.scrollbarVis.thumbNode; - } - - set thumbNode(node: HTMLElement | null) { - this.scrollbarVis.thumbNode = node; - } - - get hasThumb() { - return this.scrollbarVis.hasThumb; - } - handleDragScroll = (e: PointerEvent) => { if (!this.rect) return; const x = e.clientX - this.rect.left; @@ -737,7 +754,10 @@ class ScrollAreaScrollbarSharedState { } } -type ScrollAreaThumbImplStateProps = WithRefProps; +type ScrollAreaThumbImplStateProps = WithRefProps & + ReadableBoxedValues<{ + mounted: boolean; + }>; class ScrollAreaThumbImplState { #id: ScrollAreaThumbImplStateProps["id"]; #ref: ScrollAreaThumbImplStateProps["ref"]; @@ -750,6 +770,7 @@ class ScrollAreaThumbImplState { this.#removeUnlinkedScrollListener = undefined; } }, 100); + #mounted: ScrollAreaThumbImplStateProps["mounted"]; constructor( props: ScrollAreaThumbImplStateProps, @@ -757,6 +778,7 @@ class ScrollAreaThumbImplState { ) { this.#root = scrollbarState.root; this.#scrollbarState = scrollbarState; + this.#mounted = props.mounted; this.#id = props.id; this.#ref = props.ref; @@ -764,8 +786,9 @@ class ScrollAreaThumbImplState { id: this.#id, ref: this.#ref, onRefChange: (node) => { - this.#scrollbarState.thumbNode = node; + this.#scrollbarState.scrollbarVis.thumbNode = node; }, + condition: () => this.#mounted.value, }); $effect(() => { @@ -805,7 +828,7 @@ class ScrollAreaThumbImplState { () => ({ id: this.#id.value, - "data-state": this.#scrollbarState.hasThumb ? "visible" : "hidden", + "data-state": this.#scrollbarState.scrollbarVis.hasThumb ? "visible" : "hidden", style: { width: "var(--bits-scroll-area-thumb-width)", height: "var(--bits-scroll-area-thumb-height)", @@ -903,7 +926,9 @@ export function useScrollAreaScrollbar(props: ScrollAreaScrollbarStateProps) { } export function useScrollAreaScrollbarVisible() { - return getScrollAreaScrollbarContext().createScrollbarVisible(); + return setScrollAreaScrollbarVisibleContext( + getScrollAreaScrollbarContext().createScrollbarVisible() + ); } export function useScrollAreaScrollbarAuto() { @@ -918,15 +943,15 @@ export function useScrollAreaScrollbarHover() { return getScrollAreaScrollbarContext().createScrollbarHover(); } -export function useScrollAreaScrollbarX() { +export function useScrollAreaScrollbarX(props: ScrollbarAxisStateProps) { return setScrollAreaScrollbarAxisContext( - getScrollAreaScrollbarVisibleContext().createScrollbarX() + getScrollAreaScrollbarVisibleContext().createScrollbarX(props) ); } -export function useScrollAreaScrollbarY() { +export function useScrollAreaScrollbarY(props: ScrollbarAxisStateProps) { return setScrollAreaScrollbarAxisContext( - getScrollAreaScrollbarVisibleContext().createScrollbarY() + getScrollAreaScrollbarVisibleContext().createScrollbarY(props) ); } diff --git a/packages/bits-ui/src/lib/internal/cssToStyleObj.ts b/packages/bits-ui/src/lib/internal/cssToStyleObj.ts index d35c2a274..98dc95ec8 100644 --- a/packages/bits-ui/src/lib/internal/cssToStyleObj.ts +++ b/packages/bits-ui/src/lib/internal/cssToStyleObj.ts @@ -16,6 +16,10 @@ export function cssToStyleObj(css: string | null | undefined): StyleProperties { styleObj[pascalCase(name)] = value; return; } + if (name.startsWith("--")) { + styleObj[name] = value; + return; + } styleObj[camelCase(name)] = value; } diff --git a/packages/bits-ui/src/lib/internal/style.ts b/packages/bits-ui/src/lib/internal/style.ts index 197511d5f..45b4cebca 100644 --- a/packages/bits-ui/src/lib/internal/style.ts +++ b/packages/bits-ui/src/lib/internal/style.ts @@ -2,7 +2,8 @@ import styleToCSS from "style-object-to-css-string"; import type { StyleProperties } from "$lib/shared/index.js"; export function styleToString(style: StyleProperties = {}): string { - return styleToCSS(style).replace("\n", " "); + const stringified = styleToCSS(style).replace("\n", " "); + return stringified; } export const srOnlyStyles: StyleProperties = { diff --git a/sites/docs/src/lib/components/demos/scroll-area-demo.svelte b/sites/docs/src/lib/components/demos/scroll-area-demo.svelte index 6ed25d6e1..39595d912 100644 --- a/sites/docs/src/lib/components/demos/scroll-area-demo.svelte +++ b/sites/docs/src/lib/components/demos/scroll-area-demo.svelte @@ -5,7 +5,7 @@
- +

From 654df66f8ca2760ae4d1e53a1222b5110d5652ae Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Sun, 21 Jul 2024 16:33:40 -0400 Subject: [PATCH 05/14] a bit better --- .../components/scroll-area-scrollbar-hover.svelte | 2 +- .../src/lib/bits/scroll-area/scroll-area.svelte.ts | 8 ++++++-- .../src/lib/components/demos/alert-dialog-demo.svelte | 2 +- .../docs/src/lib/components/demos/scroll-area-demo.svelte | 6 +++--- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-hover.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-hover.svelte index e95295673..32b7a7231 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-hover.svelte +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-hover.svelte @@ -11,7 +11,7 @@ const mergedProps = $derived(mergeProps(restProps, scrollbarHoverState.props)); - + {#snippet presence()} {/snippet} diff --git a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts index 76f7227c7..8ab6b43da 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts +++ b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts @@ -210,13 +210,17 @@ class ScrollAreaScrollbarHoverState { let hideTimer = 0; if (scrollAreaNode) { const handlePointerEnter = () => { + console.log("pointer enter"); window.clearTimeout(hideTimer); - this.isVisible = true; + untrack(() => (this.isVisible = true)); }; const handlePointerLeave = () => { + console.log("pointer leave"); + if (hideTimer) window.clearTimeout(hideTimer); hideTimer = window.setTimeout(() => { - this.isVisible = false; + console.log("setting is visible to false"); + untrack(() => (this.isVisible = false)); }, hideDelay); }; diff --git a/sites/docs/src/lib/components/demos/alert-dialog-demo.svelte b/sites/docs/src/lib/components/demos/alert-dialog-demo.svelte index 2f6884904..f3a9a723a 100644 --- a/sites/docs/src/lib/components/demos/alert-dialog-demo.svelte +++ b/sites/docs/src/lib/components/demos/alert-dialog-demo.svelte @@ -12,7 +12,7 @@ - +

From 66469590df6232fb9859c7356bf4eba32f51bc23 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Sun, 21 Jul 2024 18:37:07 -0400 Subject: [PATCH 06/14] more scroll area tweaks --- .../scroll-area-scrollbar-hover.svelte | 22 ++++++++++++++----- .../scroll-area-scrollbar-scroll.svelte | 2 +- .../components/scroll-area-thumb-impl.svelte | 10 ++++++++- .../bits/scroll-area/scroll-area.svelte.ts | 12 +++++++++- .../lib/bits/select/components/select.svelte | 2 ++ packages/bits-ui/src/lib/bits/select/types.ts | 5 +++++ .../components/demos/scroll-area-demo.svelte | 4 ++-- 7 files changed, 47 insertions(+), 10 deletions(-) diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-hover.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-hover.svelte index 32b7a7231..2eac4b321 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-hover.svelte +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-hover.svelte @@ -1,18 +1,30 @@ - + {#snippet presence()} - + {/snippet} diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-scroll.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-scroll.svelte index 875393728..6f9446374 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-scroll.svelte +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-scrollbar-scroll.svelte @@ -12,7 +12,7 @@ const mergedProps = $derived(mergeProps(restProps, scrollbarScrollState.props)); - + {#snippet presence()} {/snippet} diff --git a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-impl.svelte b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-impl.svelte index b1814dfce..5e54c59f3 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-impl.svelte +++ b/packages/bits-ui/src/lib/bits/scroll-area/components/scroll-area-thumb-impl.svelte @@ -10,9 +10,11 @@ id, child, children, + present, ...restProps }: Omit & { id: string; + present: boolean; } = $props(); const isMounted = new IsMounted(); @@ -26,7 +28,13 @@ mounted: box.with(() => isMounted.current), }); - const mergedProps = $derived(mergeProps(restProps, thumbState.props)); + const mergedProps = $derived( + mergeProps(restProps, thumbState.props, { + style: { + hidden: !present, + }, + }) + ); {#if child} diff --git a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts index 8ab6b43da..6c5e1d864 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts +++ b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts @@ -160,6 +160,7 @@ class ScrollAreaScrollbarState { root: ScrollAreaRootState; orientation: ScrollAreaScrollbarStateProps["orientation"]; isHorizontal = $derived.by(() => this.orientation.value === "horizontal"); + hasThumb = $state(false); constructor(props: ScrollAreaScrollbarStateProps, root: ScrollAreaRootState) { this.root = root; @@ -200,9 +201,11 @@ class ScrollAreaScrollbarState { class ScrollAreaScrollbarHoverState { root: ScrollAreaRootState; isVisible = $state(false); + scrollbar: ScrollAreaScrollbarState; constructor(scrollbar: ScrollAreaScrollbarState) { this.root = scrollbar.root; + this.scrollbar = scrollbar; $effect(() => { const scrollAreaNode = this.root.scrollAreaNode; @@ -220,7 +223,10 @@ class ScrollAreaScrollbarHoverState { if (hideTimer) window.clearTimeout(hideTimer); hideTimer = window.setTimeout(() => { console.log("setting is visible to false"); - untrack(() => (this.isVisible = false)); + untrack(() => { + this.scrollbar.hasThumb = false; + this.isVisible = false; + }); }, hideDelay); }; @@ -372,6 +378,10 @@ class ScrollAreaScrollbarVisibleState { constructor(scrollbar: ScrollAreaScrollbarState) { this.scrollbar = scrollbar; this.root = scrollbar.root; + + $effect(() => { + this.scrollbar.hasThumb = this.hasThumb; + }); } setSizes = (sizes: Sizes) => { diff --git a/packages/bits-ui/src/lib/bits/select/components/select.svelte b/packages/bits-ui/src/lib/bits/select/components/select.svelte index a4b20e2da..98824b518 100644 --- a/packages/bits-ui/src/lib/bits/select/components/select.svelte +++ b/packages/bits-ui/src/lib/bits/select/components/select.svelte @@ -16,6 +16,7 @@ disabled = false, autocomplete = undefined, dir = "ltr", + form, }: RootProps = $props(); const rootState = useSelectRoot({ @@ -55,6 +56,7 @@ {name} {autocomplete} {disabled} + {form} onchange={(e) => (value = e.currentTarget.value)} > {#if value === ""} diff --git a/packages/bits-ui/src/lib/bits/select/types.ts b/packages/bits-ui/src/lib/bits/select/types.ts index 6b8b48ab5..7ab4c8dad 100644 --- a/packages/bits-ui/src/lib/bits/select/types.ts +++ b/packages/bits-ui/src/lib/bits/select/types.ts @@ -48,6 +48,11 @@ export type SelectRootPropsWithoutHTML = WithChildren<{ */ autocomplete?: string; + /** + * The native HTML select `form` attribute. + */ + form?: string; + /** * Whether the select is disabled. */ diff --git a/sites/docs/src/lib/components/demos/scroll-area-demo.svelte b/sites/docs/src/lib/components/demos/scroll-area-demo.svelte index 0ca2d53f0..6e57a83a1 100644 --- a/sites/docs/src/lib/components/demos/scroll-area-demo.svelte +++ b/sites/docs/src/lib/components/demos/scroll-area-demo.svelte @@ -23,10 +23,10 @@ From b8939ad7f03d7ef50cab83e60c827d514af74cd0 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Sun, 21 Jul 2024 18:45:06 -0400 Subject: [PATCH 07/14] scroll area demo --- .../components/demos/scroll-area-demo.svelte | 81 ++++++++++++------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/sites/docs/src/lib/components/demos/scroll-area-demo.svelte b/sites/docs/src/lib/components/demos/scroll-area-demo.svelte index 6e57a83a1..024f25ce5 100644 --- a/sites/docs/src/lib/components/demos/scroll-area-demo.svelte +++ b/sites/docs/src/lib/components/demos/scroll-area-demo.svelte @@ -1,34 +1,59 @@ -
- - -

- Scroll Area -

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit rem, - repellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea deleniti - eveniet incidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem ipsum dolor - sit, amet consectetur adipisicing elit. Blanditiis officiis error minima eos fugit - voluptate excepturi eveniet dolore et, ratione impedit consequuntur dolorem hic quae - corrupti autem? Dolorem, sit voluptatum. -

-
- +
+
+ + +
+
+ + +
+
+ +
+ - - - - + +

+ Scroll Area +

+

+ Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit + rem, repellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea + deleniti eveniet incidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem + ipsum dolor sit, amet consectetur adipisicing elit. Blanditiis officiis error + minima eos fugit voluptate excepturi eveniet dolore et, ratione impedit + consequuntur dolorem hic quae corrupti autem? Dolorem, sit voluptatum. +

+
+ + + + + +
From 17c637ba9fe80bdfb63dcb6a8d8ad7abe1641e9e Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Sun, 21 Jul 2024 18:48:47 -0400 Subject: [PATCH 08/14] add credits --- .../bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts | 7 +++++++ packages/bits-ui/src/lib/bits/select/select.svelte.ts | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts index 6c5e1d864..cb2352e8d 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts +++ b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts @@ -1,3 +1,10 @@ +/** + * This logic is adapted from Radix UI's ScrollArea component. + * https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/src/ScrollArea.tsx + * Credit to Jenna Smith (@jjenzz) for the original implementation. + * Incredible thought must have went into solving all the intricacies of this component. + */ + import type { ReadableBoxedValues } from "$lib/internal/box.svelte.js"; import { executeCallbacks } from "$lib/internal/callbacks.js"; import { addEventListener } from "$lib/internal/events.js"; diff --git a/packages/bits-ui/src/lib/bits/select/select.svelte.ts b/packages/bits-ui/src/lib/bits/select/select.svelte.ts index bf3ff7154..943c0e3e6 100644 --- a/packages/bits-ui/src/lib/bits/select/select.svelte.ts +++ b/packages/bits-ui/src/lib/bits/select/select.svelte.ts @@ -1,3 +1,8 @@ +/** + * This logic is adapted from Radix UI's Select component. + * https://github.com/radix-ui/primitives/blob/main/packages/react/select/src/Select.tsx + * Credit to the Radix UI team for the original implementation. + */ import { type ReadableBox, type WritableBox, box } from "svelte-toolbelt"; import { SvelteMap } from "svelte/reactivity"; import { untrack } from "svelte"; From 24ee7a39e08e495e4b4bf3b785c2b1a61348499f Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Sun, 21 Jul 2024 19:08:24 -0400 Subject: [PATCH 09/14] demo stuff --- NOTICE.txt | 16 +++- .../components/demos/scroll-area-demo.svelte | 90 +++++++++++-------- 2 files changed, 68 insertions(+), 38 deletions(-) diff --git a/NOTICE.txt b/NOTICE.txt index 693fff61a..a2e4325c8 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -6,12 +6,12 @@ The following is a list of sources from which code was used/modified in this cod This codebase contains a modified portion of code from Adobe which can be obtained at: * SOURCE: * https://www.npmjs.com/package/@react-aria/utils - * LICENSE: + * LICENSE (Apache 2.0): * https://unpkg.com/@react-aria/utils@3.24.1/LICENSE * SOURCE: * https://www.npmjs.com/package/react-stately - * LICENSE: + * LICENSE (Apache 2.0): * https://unpkg.com/react-stately@3.31.1/LICENSE ------------------------------------------------------------------------------- @@ -19,7 +19,17 @@ This codebase contains a modified portion of code from Adobe which can be obtain This codebase contains a modified portion of code from Melt UI which can be obtained at: * SOURCE: * https://www.npmjs.com/package/@melt-ui/svelte - * LICENSE: + * LICENSE (MIT): * https://unpkg.com/@melt-ui/svelte@0.76.2/LICENSE +------------------------------------------------------------------------------- + +This codebase contains a modified portion of code from Radix UI which can be obtained at: + * SOURCE: + * https://www.npmjs.com/package/@radix-ui/react-scroll-area + * https://www.npmjs.com/package/@radix-ui/react-select + * https://www.npmjs.com/package/@radix-ui/react-navigation-menu + * LICENSE (MIT): + * https://github.com/radix-ui/primitives/blob/main/LICENSE + ------------------------------------------------------------------------------- \ No newline at end of file diff --git a/sites/docs/src/lib/components/demos/scroll-area-demo.svelte b/sites/docs/src/lib/components/demos/scroll-area-demo.svelte index 024f25ce5..33d08722a 100644 --- a/sites/docs/src/lib/components/demos/scroll-area-demo.svelte +++ b/sites/docs/src/lib/components/demos/scroll-area-demo.svelte @@ -1,13 +1,16 @@ -
+
-
+
-
+
+
+ + +
+
+ + +
-
- - -

- Scroll Area -

-

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit - rem, repellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea - deleniti eveniet incidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem - ipsum dolor sit, amet consectetur adipisicing elit. Blanditiis officiis error - minima eos fugit voluptate excepturi eveniet dolore et, ratione impedit - consequuntur dolorem hic quae corrupti autem? Dolorem, sit voluptatum. -

-
- +

- - - - -

+ Scroll Area +

+

+ Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit rem, + repellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea deleniti + eveniet incidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem ipsum dolor + sit, amet consectetur adipisicing elit. Blanditiis officiis error minima eos fugit + voluptate excepturi eveniet dolore et, ratione impedit consequuntur dolorem hic quae + corrupti autem? Dolorem, sit voluptatum. +

+
+ + + + + + + +

From d8ca62dbe9d49b12bf335e1fa9c95502b52ec0eb Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Sun, 21 Jul 2024 19:23:45 -0400 Subject: [PATCH 10/14] cleanup demo --- .../bits/scroll-area/scroll-area.svelte.ts | 37 ++++++++++++------- .../components/demos/scroll-area-demo.svelte | 6 +-- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts index cb2352e8d..b9a9bff1d 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts +++ b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts @@ -20,12 +20,12 @@ import { untrack } from "svelte"; import { createContext } from "$lib/internal/createContext.js"; import { box } from "svelte-toolbelt"; import { afterTick } from "$lib/internal/afterTick.js"; -import { sleep } from "$lib/internal/sleep.js"; const SCROLL_AREA_ROOT_ATTR = "data-scroll-area-root"; const SCROLL_AREA_VIEWPORT_ATTR = "data-scroll-area-viewport"; const SCROLL_AREA_CORNER_ATTR = "data-scroll-area-corner"; const SCROLL_AREA_THUMB_ATTR = "data-scroll-area-thumb"; +const SCROLL_AREA_SCROLLBAR_ATTR = "data-scroll-area-scrollbar"; type Sizes = { content: number; @@ -220,16 +220,13 @@ class ScrollAreaScrollbarHoverState { let hideTimer = 0; if (scrollAreaNode) { const handlePointerEnter = () => { - console.log("pointer enter"); window.clearTimeout(hideTimer); untrack(() => (this.isVisible = true)); }; const handlePointerLeave = () => { - console.log("pointer leave"); if (hideTimer) window.clearTimeout(hideTimer); hideTimer = window.setTimeout(() => { - console.log("setting is visible to false"); untrack(() => { this.scrollbar.hasThumb = false; this.isVisible = false; @@ -502,7 +499,15 @@ class ScrollAreaScrollbarXState implements ScrollbarAxisState { $effect(() => { if (!this.ref.value) return; - this.computedStyle = getComputedStyle(this.ref.value!); + if (this.#mounted.value) { + afterTick(() => { + this.computedStyle = getComputedStyle(this.ref.value!); + }); + } + }); + + $effect(() => { + console.log("thumb size", this.thumbSize); }); } @@ -510,6 +515,10 @@ class ScrollAreaScrollbarXState implements ScrollbarAxisState { this.scrollbarVis.onThumbPointerDown(pointerPos.x); }; + onDragScroll = (pointerPos: { x: number; y: number }) => { + this.scrollbarVis.xOnDragScroll(pointerPos.x); + }; + onThumbPointerUp = () => { this.scrollbarVis.onThumbPointerUp(); }; @@ -518,10 +527,6 @@ class ScrollAreaScrollbarXState implements ScrollbarAxisState { this.scrollbarVis.xOnThumbPositionChange(); }; - onDragScroll = (pointerPos: { x: number; y: number }) => { - this.scrollbarVis.xOnDragScroll(pointerPos.x); - }; - onWheelScroll = (e: WheelEvent, maxScrollPos: number) => { if (!this.root.viewportNode) return; const scrollPos = this.root.viewportNode.scrollLeft + e.deltaX; @@ -545,6 +550,11 @@ class ScrollAreaScrollbarXState implements ScrollbarAxisState { }); }; + thumbSize = $derived.by(() => { + const ts = getThumbSize(this.scrollbarVis.sizes); + return ts; + }); + props = $derived.by( () => ({ @@ -556,7 +566,7 @@ class ScrollAreaScrollbarXState implements ScrollbarAxisState { this.root.dir.value === "rtl" ? "var(--bits-scroll-area-corner-width)" : 0, right: this.root.dir.value === "ltr" ? "var(--bits-scroll-area-corner-width)" : 0, - "--bits-scroll-area-thumb-width": `${getThumbSize(this.scrollbarVis.sizes)}px`, + "--bits-scroll-area-thumb-width": `${this.thumbSize}px`, }, }) as const ); @@ -568,8 +578,8 @@ class ScrollAreaScrollbarXState implements ScrollbarAxisState { class ScrollAreaScrollbarYState implements ScrollbarAxisState { #id: WithRefProps["id"]; - ref: WithRefProps["ref"]; #mounted: ScrollbarAxisStateProps["mounted"]; + ref: WithRefProps["ref"]; scrollbarVis: ScrollAreaScrollbarVisibleState; root: ScrollAreaRootState; computedStyle = $state(); @@ -627,6 +637,7 @@ class ScrollAreaScrollbarYState implements ScrollbarAxisState { }; onResize = () => { + console.log("on resize"); if (!(this.ref.value && this.root.viewportNode && this.computedStyle)) return; this.scrollbarVis.setSizes({ content: this.root.viewportNode.scrollHeight, @@ -640,8 +651,7 @@ class ScrollAreaScrollbarYState implements ScrollbarAxisState { }; thumbSize = $derived.by(() => { - const ts = getThumbSize(this.scrollbarVis.sizes); - return ts; + return getThumbSize(this.scrollbarVis.sizes); }); props = $derived.by( @@ -760,6 +770,7 @@ class ScrollAreaScrollbarSharedState { position: "absolute", ...this.scrollbarState.props.style, }, + [SCROLL_AREA_SCROLLBAR_ATTR]: "", onpointerdown: this.#onpointerdown, onpointermove: this.#onpointermove, onpointerup: this.#onpointerup, diff --git a/sites/docs/src/lib/components/demos/scroll-area-demo.svelte b/sites/docs/src/lib/components/demos/scroll-area-demo.svelte index 33d08722a..315ce3c6e 100644 --- a/sites/docs/src/lib/components/demos/scroll-area-demo.svelte +++ b/sites/docs/src/lib/components/demos/scroll-area-demo.svelte @@ -5,7 +5,7 @@ let type = $state<"auto" | "hover" | "scroll" | "always">("auto"); let height = $state(200); let width = $state(250); - let wrapText = $state(true); + let wrapText = $state(false);
@@ -71,9 +71,9 @@ class="flex h-2.5 touch-none select-none rounded-full border-t border-t-transparent bg-muted p-px transition-all duration-200 hover:h-3 hover:bg-dark-10 data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out-0 data-[state=visible]:fade-in-0" > - +
From a0b037524e00b2f93e54d8827707ad70dad1856c Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Sun, 21 Jul 2024 19:28:21 -0400 Subject: [PATCH 11/14] more demo stuff --- .../bits/scroll-area/scroll-area.svelte.ts | 24 +++-------- .../components/demos/scroll-area-demo.svelte | 43 +++++++++++-------- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts index b9a9bff1d..f33b6eeb1 100644 --- a/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts +++ b/packages/bits-ui/src/lib/bits/scroll-area/scroll-area.svelte.ts @@ -98,6 +98,10 @@ class ScrollAreaRootState { createScrollbar(props: ScrollAreaScrollbarStateProps) { return new ScrollAreaScrollbarState(props, this); } + + createCorner(props: ScrollAreaCornerImplStateProps) { + return new ScrollAreaCornerImplState(props, this); + } } type ScrollAreaViewportStateProps = WithRefProps; @@ -505,10 +509,6 @@ class ScrollAreaScrollbarXState implements ScrollbarAxisState { }); } }); - - $effect(() => { - console.log("thumb size", this.thumbSize); - }); } onThumbPointerDown = (pointerPos: { x: number; y: number }) => { @@ -637,7 +637,6 @@ class ScrollAreaScrollbarYState implements ScrollbarAxisState { }; onResize = () => { - console.log("on resize"); if (!(this.ref.value && this.root.viewportNode && this.computedStyle)) return; this.scrollbarVis.setSizes({ content: this.root.viewportNode.scrollHeight, @@ -780,10 +779,6 @@ class ScrollAreaScrollbarSharedState { createThumb(props: ScrollAreaThumbImplStateProps) { return new ScrollAreaThumbImplState(props, this); } - - createCorner(props: ScrollAreaCornerImplStateProps) { - return new ScrollAreaCornerImplState(props, this); - } } type ScrollAreaThumbImplStateProps = WithRefProps & @@ -878,17 +873,12 @@ class ScrollAreaCornerImplState { #id: ScrollAreaCornerImplStateProps["id"]; #ref: ScrollAreaCornerImplStateProps["ref"]; #root: ScrollAreaRootState; - scrollbarState: ScrollAreaScrollbarSharedState; #width = $state(0); #height = $state(0); hasSize = $derived(Boolean(this.#width && this.#height)); - constructor( - props: ScrollAreaCornerImplStateProps, - scrollbarState: ScrollAreaScrollbarSharedState - ) { - this.#root = scrollbarState.root; - this.scrollbarState = scrollbarState; + constructor(props: ScrollAreaCornerImplStateProps, root: ScrollAreaRootState) { + this.#root = root; this.#id = props.id; this.#ref = props.ref; @@ -996,7 +986,7 @@ export function useScrollAreaThumb(props: ScrollAreaThumbImplStateProps) { } export function useScrollAreaCorner(props: ScrollAreaCornerImplStateProps) { - return getScrollAreaScrollbarSharedContext().createCorner(props); + return getScrollAreaRootContext().createCorner(props); } function toInt(value?: string) { diff --git a/sites/docs/src/lib/components/demos/scroll-area-demo.svelte b/sites/docs/src/lib/components/demos/scroll-area-demo.svelte index 315ce3c6e..45b7990b8 100644 --- a/sites/docs/src/lib/components/demos/scroll-area-demo.svelte +++ b/sites/docs/src/lib/components/demos/scroll-area-demo.svelte @@ -6,11 +6,12 @@ let height = $state(200); let width = $state(250); let wrapText = $state(false); + let numParagraphs = $state(3);
-
+
-
+
-
+
-
+
+
+ + +
Scroll Area -

- Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit rem, - repellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea deleniti - eveniet incidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem ipsum dolor - sit, amet consectetur adipisicing elit. Blanditiis officiis error minima eos fugit - voluptate excepturi eveniet dolore et, ratione impedit consequuntur dolorem hic quae - corrupti autem? Dolorem, sit voluptatum. -

+ {#each Array(numParagraphs) as _} +

+ Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit + rem, repellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea + deleniti eveniet incidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem + ipsum dolor sit, amet consectetur adipisicing elit. Blanditiis officiis error + minima eos fugit voluptate excepturi eveniet dolore et, ratione impedit + consequuntur dolorem hic quae corrupti autem? Dolorem, sit voluptatum. +

+ {/each} - +
From cf36a024bfbc9ef5672f1215787d069466a3d918 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Sun, 21 Jul 2024 19:41:48 -0400 Subject: [PATCH 12/14] init test space --- .../src/tests/scroll-area/ScrollArea.spec.ts | 53 ++++++++++++++++ .../tests/scroll-area/ScrollAreaTest.svelte | 62 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 packages/bits-ui/src/tests/scroll-area/ScrollArea.spec.ts create mode 100644 packages/bits-ui/src/tests/scroll-area/ScrollAreaTest.svelte diff --git a/packages/bits-ui/src/tests/scroll-area/ScrollArea.spec.ts b/packages/bits-ui/src/tests/scroll-area/ScrollArea.spec.ts new file mode 100644 index 000000000..e80ed8d58 --- /dev/null +++ b/packages/bits-ui/src/tests/scroll-area/ScrollArea.spec.ts @@ -0,0 +1,53 @@ +import { render } from "@testing-library/svelte/svelte5"; +import { describe, it } from "vitest"; +import { setupUserEvents } from "../utils.js"; +import ScrollAreaTest from "./ScrollAreaTest.svelte"; +import type { ScrollAreaTestProps } from "./ScrollAreaTest.svelte"; + +function setup(props?: ScrollAreaTestProps) { + const user = setupUserEvents(); + const returned = render(ScrollAreaTest, props); + const root = returned.getByTestId("root"); + const viewport = returned.getByTestId("viewport"); + + function getScrollbarX() { + return returned.queryByTestId("scrollbar-x"); + } + + function getScrollbarY() { + return returned.queryByTestId("scrollbar-y"); + } + + function getThumbX() { + return returned.queryByTestId("thumb-x"); + } + + function getThumbY() { + return returned.queryByTestId("thumb-y"); + } + + function getCorner() { + return returned.queryByTestId("corner"); + } + + return { + ...returned, + root, + viewport, + user, + getScrollbarX, + getScrollbarY, + getThumbX, + getThumbY, + getCorner, + }; +} + +// need to determine how to test the scrollbars in vitest/testing-lib +describe.todo("scroll area", () => { + it("renders the root and viewport elements", async () => { + const { root, viewport } = setup(); + expect(root).toBeInTheDocument(); + expect(viewport).toBeInTheDocument(); + }); +}); diff --git a/packages/bits-ui/src/tests/scroll-area/ScrollAreaTest.svelte b/packages/bits-ui/src/tests/scroll-area/ScrollAreaTest.svelte new file mode 100644 index 000000000..7235e9533 --- /dev/null +++ b/packages/bits-ui/src/tests/scroll-area/ScrollAreaTest.svelte @@ -0,0 +1,62 @@ + + + + +
+ + + + + +
+ + + + {#each Array(numParagraphs) as _} +

+ Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dignissimos impedit rem, + repellat deserunt ducimus quasi nisi voluptatem cumque aliquid esse ea deleniti + eveniet incidunt! Deserunt minus laborum accusamus iusto dolorum. Lorem ipsum dolor + sit, amet consectetur adipisicing elit. Blanditiis officiis error minima eos fugit + voluptate excepturi eveniet dolore et, ratione impedit consequuntur dolorem hic quae + corrupti autem? Dolorem, sit voluptatum. +

+ {/each} +
+ + + + + + + +
From 47646f28b8aa9220e8873c22c39bd778e770fcd5 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Sun, 21 Jul 2024 20:13:07 -0400 Subject: [PATCH 13/14] remove ctx --- .../bits-ui/src/lib/bits/scroll-area/ctx.ts | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 packages/bits-ui/src/lib/bits/scroll-area/ctx.ts diff --git a/packages/bits-ui/src/lib/bits/scroll-area/ctx.ts b/packages/bits-ui/src/lib/bits/scroll-area/ctx.ts deleted file mode 100644 index 14e9030f4..000000000 --- a/packages/bits-ui/src/lib/bits/scroll-area/ctx.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { type CreateScrollAreaProps, createScrollArea } from "@melt-ui/svelte"; -import { getContext, setContext } from "svelte"; -import type { Writable } from "svelte/store"; -import { createBitAttrs, getOptionUpdater, removeUndefined } from "$lib/internal/index.js"; - -function getScrollAreaData() { - const NAME = "scroll-area" as const; - const SCROLLBAR_NAME = "scrollbar" as const; - const PARTS = [ - "scrollbar-x", - "scrollbar-y", - "thumb-x", - "thumb-y", - "viewport", - "content", - "root", - "corner", - ] as const; - - return { NAME, PARTS, SCROLLBAR_NAME }; -} - -type GetReturn = Omit, "updateOption">; - -export function setCtx(props: CreateScrollAreaProps) { - const { NAME, PARTS } = getScrollAreaData(); - const getAttrs = createBitAttrs(NAME, PARTS); - const scrollArea = { ...createScrollArea(removeUndefined(props)), getAttrs }; - setContext(NAME, scrollArea); - return { - ...scrollArea, - updateOption: getOptionUpdater(scrollArea.options), - }; -} - -export function getCtx() { - const { NAME } = getScrollAreaData(); - return getContext(NAME); -} - -export function setScrollbarOrientation(orientation: Writable<"horizontal" | "vertical">) { - const { SCROLLBAR_NAME } = getScrollAreaData(); - return setContext(SCROLLBAR_NAME, orientation); -} - -export function getScrollbarOrientation() { - const { SCROLLBAR_NAME } = getScrollAreaData(); - return getContext>(SCROLLBAR_NAME); -} From 2ede1beeda3b858594f7d98f8e1342abecc6060c Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Sun, 21 Jul 2024 20:13:23 -0400 Subject: [PATCH 14/14] remove ctx --- packages/bits-ui/src/lib/bits/date-picker/ctx.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/bits-ui/src/lib/bits/date-picker/ctx.ts diff --git a/packages/bits-ui/src/lib/bits/date-picker/ctx.ts b/packages/bits-ui/src/lib/bits/date-picker/ctx.ts deleted file mode 100644 index e69de29bb..000000000