From 80a545b478596e5fe06cba619ca07e7d877f31a1 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 23 Dec 2024 17:01:14 +0100 Subject: [PATCH 1/5] Work on the scroll-area component --- apps/docs/src/routes/index.mdx | 1 + apps/docs/src/routes/menu.md | 1 + .../src/routes/scroll-area/examples/both.tsx | 33 +++ .../scroll-area/examples/horizontal-test.tsx | 29 +++ .../scroll-area/examples/horizontal.tsx | 29 +++ .../scroll-area/examples/scroll-area.css | 64 +++++ .../scroll-area/examples/vertical-test.tsx | 33 +++ .../routes/scroll-area/examples/vertical.tsx | 30 +++ apps/docs/src/routes/scroll-area/index.mdx | 60 +++++ libs/components/src/index.ts | 1 + libs/components/src/scroll-area/index.ts | 4 + libs/components/src/scroll-area/research.mdx | 18 ++ .../src/scroll-area/scroll-area-root.tsx | 11 + .../src/scroll-area/scroll-area-scrollbar.tsx | 54 +++++ .../src/scroll-area/scroll-area-thumb.tsx | 89 +++++++ .../src/scroll-area/scroll-area-view-port.tsx | 42 ++++ .../src/scroll-area/scroll-area.driver.ts | 39 ++++ .../src/scroll-area/scroll-area.test.ts | 219 ++++++++++++++++++ 18 files changed, 757 insertions(+) create mode 100644 apps/docs/src/routes/scroll-area/examples/both.tsx create mode 100644 apps/docs/src/routes/scroll-area/examples/horizontal-test.tsx create mode 100644 apps/docs/src/routes/scroll-area/examples/horizontal.tsx create mode 100644 apps/docs/src/routes/scroll-area/examples/scroll-area.css create mode 100644 apps/docs/src/routes/scroll-area/examples/vertical-test.tsx create mode 100644 apps/docs/src/routes/scroll-area/examples/vertical.tsx create mode 100644 apps/docs/src/routes/scroll-area/index.mdx create mode 100644 libs/components/src/scroll-area/index.ts create mode 100644 libs/components/src/scroll-area/research.mdx create mode 100644 libs/components/src/scroll-area/scroll-area-root.tsx create mode 100644 libs/components/src/scroll-area/scroll-area-scrollbar.tsx create mode 100644 libs/components/src/scroll-area/scroll-area-thumb.tsx create mode 100644 libs/components/src/scroll-area/scroll-area-view-port.tsx create mode 100644 libs/components/src/scroll-area/scroll-area.driver.ts create mode 100644 libs/components/src/scroll-area/scroll-area.test.ts diff --git a/apps/docs/src/routes/index.mdx b/apps/docs/src/routes/index.mdx index 6e629836..f2bad955 100644 --- a/apps/docs/src/routes/index.mdx +++ b/apps/docs/src/routes/index.mdx @@ -10,3 +10,4 @@ Here is some MDX - [Checkbox](checkbox) - [Pagination](pagination) - [OTP](otp) +- [Scroll Area](scroll-area) diff --git a/apps/docs/src/routes/menu.md b/apps/docs/src/routes/menu.md index 37a47b7a..56c8d20e 100644 --- a/apps/docs/src/routes/menu.md +++ b/apps/docs/src/routes/menu.md @@ -5,6 +5,7 @@ - [Checkbox](/checkbox) - [Pagination](/pagination) - [OTP](/otp) +- [Scroll Area](/scroll-area) ## Styled diff --git a/apps/docs/src/routes/scroll-area/examples/both.tsx b/apps/docs/src/routes/scroll-area/examples/both.tsx new file mode 100644 index 00000000..07e3f3eb --- /dev/null +++ b/apps/docs/src/routes/scroll-area/examples/both.tsx @@ -0,0 +1,33 @@ +import { component$, useStyles$ } from "@builder.io/qwik"; +import { ScrollArea } from "@kunai-consulting/qwik-components"; +import styles from './scroll-area.css?inline'; + +export default component$(() => { + useStyles$(styles); + return ( + + +
+

+ 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. Blanditiisofficiiserrorminimaeos fugit voluptate + excepturi eveniet dolore et, ratione impedit consequuntur dolorem hic + quae corrupti autem? Dolorem, sit voluptatum. +

+
+
+ + + + + + + + +
+ ); +}); + diff --git a/apps/docs/src/routes/scroll-area/examples/horizontal-test.tsx b/apps/docs/src/routes/scroll-area/examples/horizontal-test.tsx new file mode 100644 index 00000000..edabd6b5 --- /dev/null +++ b/apps/docs/src/routes/scroll-area/examples/horizontal-test.tsx @@ -0,0 +1,29 @@ +import { component$, useStyles$ } from "@builder.io/qwik"; +import { ScrollArea } from "@kunai-consulting/qwik-components"; +import styles from './scroll-area.css?inline'; + +export default component$(() => { + useStyles$(styles); + return ( + + +
+

+ 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. Blanditiisofficiiserror minima eos fugit voluptate + excepturi eveniet dolore et, ratione impedit consequuntur dolorem hic + quae corrupti autem? Dolorem, sit voluptatum. +

+
+
+ + + + +
+ ); +}); + diff --git a/apps/docs/src/routes/scroll-area/examples/horizontal.tsx b/apps/docs/src/routes/scroll-area/examples/horizontal.tsx new file mode 100644 index 00000000..3510df6d --- /dev/null +++ b/apps/docs/src/routes/scroll-area/examples/horizontal.tsx @@ -0,0 +1,29 @@ +import { component$, useStyles$ } from "@builder.io/qwik"; +import { ScrollArea } from "@kunai-consulting/qwik-components"; +import styles from './scroll-area.css?inline'; + +export default component$(() => { + useStyles$(styles); + return ( + + +
+

+ 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. +

+
+
+ + + + +
+ ); +}); + diff --git a/apps/docs/src/routes/scroll-area/examples/scroll-area.css b/apps/docs/src/routes/scroll-area/examples/scroll-area.css new file mode 100644 index 00000000..261ee68f --- /dev/null +++ b/apps/docs/src/routes/scroll-area/examples/scroll-area.css @@ -0,0 +1,64 @@ +.scroll-area-root { + background: #0a4d70; + position: relative; + overflow: hidden; + width: 100%; + height: 100%; + --thumb-size: 12px; +} + +.scroll-area-viewport { + width: 100%; + height: 100%; + border-radius: inherit; + overflow: scroll; + position: relative; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE and Edge */ +} + +/* Hide default scrollbars */ +.scroll-area-viewport::-webkit-scrollbar { + display: none; +} + +.scroll-area-scrollbar { + display: flex; + touch-action: none; + user-select: none; + padding: 2px; + background: rgba(0, 0, 0, 0.05); + transition: background 160ms ease-out; + position: absolute; +} + +.scroll-area-scrollbar[data-orientation='vertical'] { + width: 16px; + height: 100%; + right: 0; + top: 0; +} + +.scroll-area-scrollbar[data-orientation='horizontal'] { + height: 16px; + width: calc(100% - var(--thumb-size)); + bottom: 0; + left: 0; +} + +.scroll-area-thumb { + position: relative; + height: var(--thumb-size); + width: var(--thumb-size); + background: rgba(0, 0, 0, 0.3); + border-radius: 9999px; + transition: background 160ms ease-out; +} + +.scroll-area-thumb:hover { + background: rgba(0, 0, 0, 0.5); +} + +.scroll-area-thumb[data-dragging] { + background: rgba(0, 0, 0, 0.7); +} diff --git a/apps/docs/src/routes/scroll-area/examples/vertical-test.tsx b/apps/docs/src/routes/scroll-area/examples/vertical-test.tsx new file mode 100644 index 00000000..2c17dad4 --- /dev/null +++ b/apps/docs/src/routes/scroll-area/examples/vertical-test.tsx @@ -0,0 +1,33 @@ +import { component$, useStyles$ } from "@builder.io/qwik"; +import { ScrollArea } from "@kunai-consulting/qwik-components"; +import styles from './scroll-area.css?inline'; + +export default component$(() => { + useStyles$(styles); + return ( + + + {/* Content with fixed height to ensure scrolling */} +
+

+ 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. +

+
+
+ + + + +
+ ); +}); diff --git a/apps/docs/src/routes/scroll-area/examples/vertical.tsx b/apps/docs/src/routes/scroll-area/examples/vertical.tsx new file mode 100644 index 00000000..7b7bac98 --- /dev/null +++ b/apps/docs/src/routes/scroll-area/examples/vertical.tsx @@ -0,0 +1,30 @@ +import { component$, useStyles$ } from "@builder.io/qwik"; +import { ScrollArea } from "@kunai-consulting/qwik-components"; +import styles from './scroll-area.css?inline'; + +export default component$(() => { + useStyles$(styles); + return ( + + +
+

+ 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. +

+
+
+ + + + + +
+ ); +}); + diff --git a/apps/docs/src/routes/scroll-area/index.mdx b/apps/docs/src/routes/scroll-area/index.mdx new file mode 100644 index 00000000..465e5b15 --- /dev/null +++ b/apps/docs/src/routes/scroll-area/index.mdx @@ -0,0 +1,60 @@ +--- +title: Qwik Design System | Scroll Area +--- + +# Scroll Area + +A scrollable container that provides a custom-styled scrollbar with support for both vertical and horizontal scrolling. + + + +## Basic Usage + +The Scroll Area component provides a customizable scrolling container that replaces the browser's default scrollbars with a more aesthetically pleasing design. + +## Anatomy + +The Scroll Area component is composed of several parts: + +```tsx + + + {/* Your content */} + + + + + + + + +``` + +## Vertical Scrolling +By default, Scroll Area supports vertical scrolling when content exceeds the container height. + + + +## Horizontal Scrolling +To enable horizontal scrolling, add the horizontal scrollbar component. + + +## Both Directions +Scroll Area supports both vertical and horizontal scrolling simultaneously. + + +## Custom Styling +The Scroll Area component can be styled using CSS classes. + +## Sizing +The Scroll Area component adapts to its container size. You can control its dimensions using standard CSS properties. + +## Hidden Scrollbars +The Scroll Area component automatically hides the native browser scrollbars while maintaining scroll functionality. + +## Scroll Thumb Behavior +The scroll thumb responds to: + +* Direct dragging +* Click and drag interactions +* Content scrolling diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index e77af7d1..377f2539 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -2,3 +2,4 @@ export * as Otp from './otp'; export * as Checkbox from './checkbox'; export * as Checklist from './checklist'; export * as Pagination from "./pagination"; +export * as ScrollArea from './scroll-area'; diff --git a/libs/components/src/scroll-area/index.ts b/libs/components/src/scroll-area/index.ts new file mode 100644 index 00000000..11219927 --- /dev/null +++ b/libs/components/src/scroll-area/index.ts @@ -0,0 +1,4 @@ +export { ScrollAreaRoot as Root } from './scroll-area-root'; +export { ScrollAreaViewPort as ViewPort } from './scroll-area-view-port'; +export { ScrollAreaScrollbar as Scrollbar } from './scroll-area-scrollbar'; +export { ScrollAreaThumb as Thumb } from './scroll-area-thumb'; \ No newline at end of file diff --git a/libs/components/src/scroll-area/research.mdx b/libs/components/src/scroll-area/research.mdx new file mode 100644 index 00000000..38288c7b --- /dev/null +++ b/libs/components/src/scroll-area/research.mdx @@ -0,0 +1,18 @@ +# Headless ScrollArea Component + +## API + +``` + + + {/* data */} + + {/* horizontal */} + + + {/* vertical */} + + + + +``` \ No newline at end of file diff --git a/libs/components/src/scroll-area/scroll-area-root.tsx b/libs/components/src/scroll-area/scroll-area-root.tsx new file mode 100644 index 00000000..ec5fef0a --- /dev/null +++ b/libs/components/src/scroll-area/scroll-area-root.tsx @@ -0,0 +1,11 @@ +import { component$, PropsOf, Slot } from "@builder.io/qwik"; + +type RootProps = PropsOf<'div'>; + +export const ScrollAreaRoot = component$((props) => { + return ( +
+ +
+ ) +}) \ No newline at end of file diff --git a/libs/components/src/scroll-area/scroll-area-scrollbar.tsx b/libs/components/src/scroll-area/scroll-area-scrollbar.tsx new file mode 100644 index 00000000..e1000c65 --- /dev/null +++ b/libs/components/src/scroll-area/scroll-area-scrollbar.tsx @@ -0,0 +1,54 @@ +import { $, component$, PropsOf, QRL, Slot, useSignal } from "@builder.io/qwik"; + +type ScrollBarType = PropsOf<'div'> & { + orientation?: 'vertical' | 'horizontal'; + onScroll$?: QRL<(e: Event) => void>; +}; + +export const ScrollAreaScrollbar = component$((props) => { + const { orientation = 'vertical' } = props; + const scrollbarRef = useSignal(); + + const onTrackClick$ = $((e: MouseEvent) => { + const scrollbar = scrollbarRef.value; + if (!scrollbar) return; + + const root = scrollbar.closest('[data-scroll-area-root]'); + const viewport = root?.querySelector('[data-scroll-area-viewport]') as HTMLElement; + const thumb = scrollbar.querySelector('[data-scroll-area-thumb]') as HTMLElement; + + if (!thumb || !viewport || e.target === thumb) return; + + const rect = scrollbar.getBoundingClientRect(); + + if (orientation === 'vertical') { + const clickPos = e.clientY - rect.top; + // Calculate click position as a ratio of the scrollbar height + const scrollRatio = clickPos / rect.height; + // Calculate the maximum scroll position + const maxScroll = viewport.scrollHeight - viewport.clientHeight; + // Set scroll position directly based on ratio + viewport.scrollTop = scrollRatio * maxScroll; + } else { + const clickPos = e.clientX - rect.left; + // Calculate click position as a ratio of the scrollbar width + const scrollRatio = clickPos / rect.width; + // Calculate the maximum scroll position + const maxScroll = viewport.scrollWidth - viewport.clientWidth; + // Set scroll position directly based on ratio + viewport.scrollLeft = scrollRatio * maxScroll; + } + }); + + return ( +
+ +
+ ); +}); diff --git a/libs/components/src/scroll-area/scroll-area-thumb.tsx b/libs/components/src/scroll-area/scroll-area-thumb.tsx new file mode 100644 index 00000000..cee0619a --- /dev/null +++ b/libs/components/src/scroll-area/scroll-area-thumb.tsx @@ -0,0 +1,89 @@ +import { component$, $, useSignal, useOnDocument, Signal, PropsOf } from "@builder.io/qwik"; +import type { PropFunction } from "@builder.io/qwik"; + +type ScrollAreaThumb = { + ref?: Signal; + onDragStart$?: PropFunction<(e: MouseEvent) => void>; + onDragMove$?: PropFunction<(e: MouseEvent) => void>; + onDragEnd$?: PropFunction<() => void>; +} & PropsOf<'div'>; + +export const ScrollAreaThumb = component$((props) => { + const thumbRef = useSignal(); + const isDragging = useSignal(false); + const dragData = useSignal({ + startClientY: 0, + startClientX: 0, + startScrollTop: 0, + startScrollLeft: 0 + }); + + const onDragStart$ = $((e: MouseEvent) => { + const thumb = thumbRef.value; + if (!thumb) return; + + const scrollbar = thumb.closest('[data-scroll-area-scrollbar]'); + const root = scrollbar?.closest('[data-scroll-area-root]'); + const viewport = root?.querySelector('[data-scroll-area-viewport]') as HTMLElement; + + if (!scrollbar || !viewport) return; + + e.preventDefault(); + isDragging.value = true; + + dragData.value = { + startClientY: e.clientY, + startClientX: e.clientX, + startScrollTop: viewport.scrollTop, + startScrollLeft: viewport.scrollLeft + }; + }); + + const onDragMove$ = $((e: MouseEvent) => { + if (!isDragging.value) return; + + const thumb = thumbRef.value; + if (!thumb) return; + + const scrollbar = thumb.closest('[data-scroll-area-scrollbar]'); + const root = scrollbar?.closest('[data-scroll-area-root]'); + const viewport = root?.querySelector('[data-scroll-area-viewport]') as HTMLElement; + + if (!scrollbar || !viewport) return; + + const isVertical = scrollbar.getAttribute('data-orientation') === 'vertical'; + + e.preventDefault(); + + if (isVertical) { + const deltaY = e.clientY - dragData.value.startClientY; + const scrollbarHeight = scrollbar.clientHeight; + const thumbHeight = thumb.clientHeight; + const scrollRatio = (viewport.scrollHeight - viewport.clientHeight) / (scrollbarHeight - thumbHeight); + viewport.scrollTop = dragData.value.startScrollTop + (deltaY * scrollRatio); + } else { + const deltaX = e.clientX - dragData.value.startClientX; + const scrollbarWidth = scrollbar.clientWidth; + const thumbWidth = thumb.clientWidth; + const scrollRatio = (viewport.scrollWidth - viewport.clientWidth) / (scrollbarWidth - thumbWidth); + viewport.scrollLeft = dragData.value.startScrollLeft + (deltaX * scrollRatio); + } + }); + + const onDragEnd$ = $(() => { + isDragging.value = false; + }); + + useOnDocument('mousemove', onDragMove$); + useOnDocument('mouseup', onDragEnd$); + + return ( +
+ ); +}); diff --git a/libs/components/src/scroll-area/scroll-area-view-port.tsx b/libs/components/src/scroll-area/scroll-area-view-port.tsx new file mode 100644 index 00000000..d112316b --- /dev/null +++ b/libs/components/src/scroll-area/scroll-area-view-port.tsx @@ -0,0 +1,42 @@ +import { $, component$, PropFunction, PropsOf, Slot } from "@builder.io/qwik"; + +type ViewPortProps = PropsOf<'div'> & { + onScroll$?: PropFunction<(e: Event) => void>; +}; + +export const ScrollAreaViewPort = component$((props) => { + const onScroll$ = $((e: Event) => { + const viewport = e.target as HTMLElement; + const scrollbars = viewport.parentElement?.querySelectorAll('[data-scroll-area-scrollbar]'); + + scrollbars?.forEach(scrollbar => { + const thumb = scrollbar.querySelector('[data-scroll-area-thumb]') as HTMLElement; + if (!thumb) return; + + const isVertical = scrollbar.getAttribute('data-orientation') === 'vertical'; + + if (isVertical) { + const scrollRatio = viewport.scrollTop / (viewport.scrollHeight - viewport.clientHeight); + const maxTop = scrollbar.clientHeight - thumb.clientHeight; + thumb.style.transform = `translateY(${scrollRatio * maxTop}px)`; + } else { + const scrollRatio = viewport.scrollLeft / (viewport.scrollWidth - viewport.clientWidth); + const maxLeft = scrollbar.clientWidth - thumb.clientWidth; + thumb.style.transform = `translateX(${scrollRatio * maxLeft}px)`; + } + }); + + // Call the provided onScroll$ handler if it exists + props.onScroll$?.(e); + }); + + return ( +
+ +
+ ); +}); diff --git a/libs/components/src/scroll-area/scroll-area.driver.ts b/libs/components/src/scroll-area/scroll-area.driver.ts new file mode 100644 index 00000000..9ad111da --- /dev/null +++ b/libs/components/src/scroll-area/scroll-area.driver.ts @@ -0,0 +1,39 @@ +import type { Locator, Page } from "@playwright/test"; +export type DriverLocator = Locator | Page; + +export function createTestDriver(rootLocator: T) { + const getRoot = () => { + return rootLocator.locator("[data-scroll-area-root]"); + }; + + const getViewport = () => { + return rootLocator.locator("[data-scroll-area-viewport]"); + }; + + const getVerticalScrollbar = () => { + return rootLocator.locator('[data-scroll-area-scrollbar][data-orientation="vertical"]'); + }; + + const getHorizontalScrollbar = () => { + return rootLocator.locator('[data-scroll-area-scrollbar][data-orientation="horizontal"]'); + }; + + const getVerticalThumb = () => { + return getVerticalScrollbar().locator("[data-scroll-area-thumb]"); + }; + + const getHorizontalThumb = () => { + return getHorizontalScrollbar().locator("[data-scroll-area-thumb]"); + }; + + return { + ...rootLocator, + locator: rootLocator, + getRoot, + getViewport, + getVerticalScrollbar, + getHorizontalScrollbar, + getVerticalThumb, + getHorizontalThumb, + }; +} diff --git a/libs/components/src/scroll-area/scroll-area.test.ts b/libs/components/src/scroll-area/scroll-area.test.ts new file mode 100644 index 00000000..9ee624e8 --- /dev/null +++ b/libs/components/src/scroll-area/scroll-area.test.ts @@ -0,0 +1,219 @@ +import { expect, test, type Page } from "@playwright/test"; +import { createTestDriver } from "./scroll-area.driver"; + +async function setup(page: Page, exampleName: string) { + await page.goto(`http://localhost:6174/scroll-area/${exampleName}`); + + const driver = createTestDriver(page); + + return driver; +} + +test.describe("critical functionality", () => { + test(`GIVEN a scroll area + WHEN content exceeds viewport height + THEN vertical scrollbar should be visible`, async ({ page }) => { + const d = await setup(page, "vertical-test"); + await expect(d.getVerticalScrollbar()).toBeVisible(); + }); + + test(`GIVEN a scroll area + WHEN content exceeds viewport width + THEN horizontal scrollbar should be visible`, async ({ page }) => { + const d = await setup(page, "horizontal-test"); + await expect(d.getHorizontalScrollbar()).toBeVisible(); + }); + + test(`GIVEN a scroll area with vertical content + WHEN scrolling to bottom + THEN thumb should move to bottom position`, async ({ page }) => { + const d = await setup(page, "vertical-test"); + const viewport = d.getViewport(); + + // Scroll to bottom + await viewport.evaluate(el => { + el.scrollTop = el.scrollHeight; + }); + + // Wait for scroll to complete and thumb to update position + await page.waitForTimeout(100); + + // Verify thumb position + const thumb = d.getVerticalThumb(); + await expect(thumb).toBeVisible(); + + // Check if thumb is at bottom position + const scrollbar = d.getVerticalScrollbar(); + const thumbBox = await thumb.boundingBox(); + const scrollbarBox = await scrollbar.boundingBox(); + + // Calculate expected bottom position considering padding + const expectedBottom = scrollbarBox!.y + scrollbarBox!.height ; + const actualBottom = thumbBox!.y + thumbBox!.height - 2; // 2px for padding + + expect(actualBottom).toBeCloseTo(expectedBottom, 1); + }); +}); + +test.describe("drag functionality", () => { + test(`GIVEN a scroll area + WHEN dragging the vertical thumb + THEN content should scroll accordingly`, async ({ page }) => { + const d = await setup(page, "vertical-test"); + const thumb = d.getVerticalThumb(); + const viewport = d.getViewport(); + + // Get initial scroll position + const initialScrollTop = await viewport.evaluate(el => el.scrollTop); + + // Get thumb position + const thumbBox = await thumb.boundingBox(); + if (!thumbBox) throw new Error('Could not get thumb position'); + + // Simulate drag operation + // Start at the center of the thumb + await page.mouse.move( + thumbBox.x + thumbBox.width / 2, + thumbBox.y + thumbBox.height / 2 + ); + await page.mouse.down(); + + // Move mouse down by 100px + await page.mouse.move( + thumbBox.x + thumbBox.width / 2, + thumbBox.y + thumbBox.height / 2 + 100, + { steps: 10 } // Make movement smoother + ); + + await page.mouse.up(); + + // Wait for scrolling to complete + await page.waitForTimeout(100); + + // Verify new scroll position + const newScrollTop = await viewport.evaluate(el => el.scrollTop); + expect(newScrollTop).toBeGreaterThan(initialScrollTop); + }); + + test(`GIVEN a scroll area + WHEN dragging the horizontal thumb + THEN content should scroll accordingly`, async ({ page }) => { + const d = await setup(page, "horizontal-test"); + const thumb = d.getHorizontalThumb(); + const viewport = d.getViewport(); + + // Get initial scroll position + const initialScrollLeft = await viewport.evaluate(el => el.scrollLeft); + + // Get thumb position + const thumbBox = await thumb.boundingBox(); + if (!thumbBox) throw new Error('Could not get thumb position'); + + // Simulate drag operation + // Start at the center of the thumb + await page.mouse.move( + thumbBox.x + thumbBox.width / 2, + thumbBox.y + thumbBox.height / 2 + ); + await page.mouse.down(); + + // Move mouse right by 100px + await page.mouse.move( + thumbBox.x + thumbBox.width / 2 + 100, + thumbBox.y + thumbBox.height / 2, + { steps: 10 } // Make movement smoother + ); + + await page.mouse.up(); + + // Wait for scrolling to complete + await page.waitForTimeout(100); + + // Verify new scroll position + const newScrollLeft = await viewport.evaluate(el => el.scrollLeft); + expect(newScrollLeft).toBeGreaterThan(initialScrollLeft); + }); +}); + +test.describe("thumb behavior", () => { + test(`GIVEN a scroll area + WHEN thumb is being dragged + THEN it should have dragging state`, async ({ page }) => { + const d = await setup(page, "vertical-test"); + const thumb = d.getVerticalThumb(); + + // Get thumb position + const thumbBox = await thumb.boundingBox(); + if (!thumbBox) throw new Error('Could not get thumb position'); + + // Move to center of thumb and click + await page.mouse.move( + thumbBox.x + thumbBox.width / 2, + thumbBox.y + thumbBox.height / 2 + ); + await page.mouse.down(); + + // Wait for drag state to update + await page.waitForTimeout(50); + + await expect(thumb).toHaveAttribute('data-dragging', ''); + + // Cleanup + await page.mouse.up(); + }); + + test(`GIVEN a scroll area + WHEN clicking on scrollbar track + THEN viewport should scroll to that position`, async ({ page }) => { + const d = await setup(page, "vertical-test"); + const scrollbar = d.getVerticalScrollbar(); + const viewport = d.getViewport(); + + // Get initial scroll position + const initialScrollTop = await viewport.evaluate(el => el.scrollTop); + + // Get scrollbar position + const box = await scrollbar.boundingBox(); + if (!box) throw new Error('Could not get scrollbar position'); + + // Click in the middle of the scrollbar, but account for padding + await page.mouse.click( + box.x + box.width / 2, + box.y + box.height / 2 + ); + + // Wait for scroll to complete + await page.waitForTimeout(50); + + // Verify new scroll position + const newScrollTop = await viewport.evaluate(el => el.scrollTop); + expect(newScrollTop).toBeGreaterThan(initialScrollTop); + }); +}); + +test.describe("a11y", () => { + test(`GIVEN a scroll area + WHEN scrolling + THEN native keyboard shortcuts should work`, async ({ page }) => { + const d = await setup(page, "vertical-test"); + const viewport = d.getViewport(); + + // Focus the viewport + await viewport.focus(); + + // Test various keyboard shortcuts + const shortcuts = ['PageDown', 'PageUp', 'Home', 'End']; + for (const key of shortcuts) { + const initialScrollTop = await viewport.evaluate(el => el.scrollTop); + await page.keyboard.press(key); + const newScrollTop = await viewport.evaluate(el => el.scrollTop); + + if (key === 'PageDown' || key === 'End') { + expect(newScrollTop).toBeGreaterThanOrEqual(initialScrollTop); + } else { + expect(newScrollTop).toBeLessThanOrEqual(initialScrollTop); + } + } + }); +}); + From 5b57433e85aafe678dbd052191cc39a143e36a0d Mon Sep 17 00:00:00 2001 From: Oleksandr Zainetdinov Date: Mon, 23 Dec 2024 19:16:23 +0200 Subject: [PATCH 2/5] Create new-coats-join.md --- .changeset/new-coats-join.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/new-coats-join.md diff --git a/.changeset/new-coats-join.md b/.changeset/new-coats-join.md new file mode 100644 index 00000000..e2f0eb9d --- /dev/null +++ b/.changeset/new-coats-join.md @@ -0,0 +1,6 @@ +--- +"@kunai-consulting/qwik-components": patch +"qwik-design-system-docs": patch +--- + +Add the scroll-area component From 764acfa8f64b57516ef6f105c2c06138305679dc Mon Sep 17 00:00:00 2001 From: jack shelton Date: Mon, 23 Dec 2024 13:18:30 -0600 Subject: [PATCH 3/5] add scrollbar to context --- .../scroll-area/examples/vertical-test.tsx | 25 +++++------ .../src/checklist/checklist-item.tsx | 12 +++--- .../src/scroll-area/scroll-area-context.ts | 10 +++++ .../src/scroll-area/scroll-area-root.tsx | 27 ++++++++++-- .../src/scroll-area/scroll-area-scrollbar.tsx | 30 +++++++++----- .../src/scroll-area/scroll-area-thumb.tsx | 41 +++++++++++-------- .../src/scroll-area/scroll-area-view-port.tsx | 26 ++++++------ 7 files changed, 107 insertions(+), 64 deletions(-) create mode 100644 libs/components/src/scroll-area/scroll-area-context.ts diff --git a/apps/docs/src/routes/scroll-area/examples/vertical-test.tsx b/apps/docs/src/routes/scroll-area/examples/vertical-test.tsx index 2c17dad4..4fc6e59e 100644 --- a/apps/docs/src/routes/scroll-area/examples/vertical-test.tsx +++ b/apps/docs/src/routes/scroll-area/examples/vertical-test.tsx @@ -1,32 +1,27 @@ import { component$, useStyles$ } from "@builder.io/qwik"; import { ScrollArea } from "@kunai-consulting/qwik-components"; -import styles from './scroll-area.css?inline'; +import styles from "./scroll-area.css?inline"; export default component$(() => { useStyles$(styles); return ( - + {/* Content with fixed height to ensure scrolling */}

- 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. + 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.

- - + +
); diff --git a/libs/components/src/checklist/checklist-item.tsx b/libs/components/src/checklist/checklist-item.tsx index eaf5ece1..bcf3313a 100644 --- a/libs/components/src/checklist/checklist-item.tsx +++ b/libs/components/src/checklist/checklist-item.tsx @@ -8,12 +8,12 @@ import { useSignal, useTask$, useVisibleTask$, - $, -} from '@builder.io/qwik'; -import { CheckboxRoot } from '../checkbox/checkbox-root'; -import { ChecklistContext, type ChecklistState } from './checklist-context'; + $ +} from "@builder.io/qwik"; +import { CheckboxRoot } from "../checkbox/checkbox-root"; +import { ChecklistContext, type ChecklistState } from "./checklist-context"; -interface ChecklistItemProps extends PropsOf<'div'> { +interface ChecklistItemProps extends PropsOf<"div"> { _index?: number; } @@ -21,7 +21,7 @@ export const ChecklistItem = component$((props: ChecklistItemProps) => { const { _index, ...rest } = props; if (_index === undefined) { - throw new Error('Checklist Item must have an index.'); + throw new Error("Checklist Item must have an index."); } const context = useContext(ChecklistContext); diff --git a/libs/components/src/scroll-area/scroll-area-context.ts b/libs/components/src/scroll-area/scroll-area-context.ts new file mode 100644 index 00000000..df64c680 --- /dev/null +++ b/libs/components/src/scroll-area/scroll-area-context.ts @@ -0,0 +1,10 @@ +import { createContextId, type Signal } from "@builder.io/qwik"; + +export const scrollAreaContextId = + createContextId("scroll-area-context"); + +export interface ScrollAreaContext { + scrollbarRef: Signal; + thumbRef: Signal; + viewportRef: Signal; +} diff --git a/libs/components/src/scroll-area/scroll-area-root.tsx b/libs/components/src/scroll-area/scroll-area-root.tsx index ec5fef0a..8c1d7b47 100644 --- a/libs/components/src/scroll-area/scroll-area-root.tsx +++ b/libs/components/src/scroll-area/scroll-area-root.tsx @@ -1,11 +1,30 @@ -import { component$, PropsOf, Slot } from "@builder.io/qwik"; +import { + component$, + type PropsOf, + Slot, + useContextProvider, + useSignal +} from "@builder.io/qwik"; +import { scrollAreaContextId } from "./scroll-area-context"; -type RootProps = PropsOf<'div'>; +type RootProps = PropsOf<"div">; export const ScrollAreaRoot = component$((props) => { + const viewportRef = useSignal(); + const scrollbarRef = useSignal(); + const thumbRef = useSignal(); + + const context = { + viewportRef, + scrollbarRef, + thumbRef + }; + + useContextProvider(scrollAreaContextId, context); + return (
- ) -}) \ No newline at end of file + ); +}); diff --git a/libs/components/src/scroll-area/scroll-area-scrollbar.tsx b/libs/components/src/scroll-area/scroll-area-scrollbar.tsx index e1000c65..b18aa276 100644 --- a/libs/components/src/scroll-area/scroll-area-scrollbar.tsx +++ b/libs/components/src/scroll-area/scroll-area-scrollbar.tsx @@ -1,27 +1,37 @@ -import { $, component$, PropsOf, QRL, Slot, useSignal } from "@builder.io/qwik"; +import { + $, + component$, + type PropsOf, + type QRL, + Slot, + useContext, + useSignal +} from "@builder.io/qwik"; +import { scrollAreaContextId } from "./scroll-area-context"; -type ScrollBarType = PropsOf<'div'> & { - orientation?: 'vertical' | 'horizontal'; +type ScrollBarType = PropsOf<"div"> & { + orientation?: "vertical" | "horizontal"; onScroll$?: QRL<(e: Event) => void>; }; export const ScrollAreaScrollbar = component$((props) => { - const { orientation = 'vertical' } = props; + const context = useContext(scrollAreaContextId); + const { orientation = "vertical" } = props; const scrollbarRef = useSignal(); const onTrackClick$ = $((e: MouseEvent) => { const scrollbar = scrollbarRef.value; if (!scrollbar) return; - const root = scrollbar.closest('[data-scroll-area-root]'); - const viewport = root?.querySelector('[data-scroll-area-viewport]') as HTMLElement; - const thumb = scrollbar.querySelector('[data-scroll-area-thumb]') as HTMLElement; + const viewport = context.viewportRef.value; + const thumb = context.thumbRef.value; - if (!thumb || !viewport || e.target === thumb) return; + if (!thumb || e.target === thumb) return; + if (!viewport) return; const rect = scrollbar.getBoundingClientRect(); - if (orientation === 'vertical') { + if (orientation === "vertical") { const clickPos = e.clientY - rect.top; // Calculate click position as a ratio of the scrollbar height const scrollRatio = clickPos / rect.height; @@ -43,7 +53,7 @@ export const ScrollAreaScrollbar = component$((props) => { return (
void>; onDragMove$?: PropFunction<(e: MouseEvent) => void>; onDragEnd$?: PropFunction<() => void>; -} & PropsOf<'div'>; +} & PropsOf<"div">; export const ScrollAreaThumb = component$((props) => { const thumbRef = useSignal(); @@ -22,9 +29,9 @@ export const ScrollAreaThumb = component$((props) => { const thumb = thumbRef.value; if (!thumb) return; - const scrollbar = thumb.closest('[data-scroll-area-scrollbar]'); - const root = scrollbar?.closest('[data-scroll-area-root]'); - const viewport = root?.querySelector('[data-scroll-area-viewport]') as HTMLElement; + const scrollbar = thumb.closest("[data-scroll-area-scrollbar]"); + const root = scrollbar?.closest("[data-scroll-area-root]"); + const viewport = root?.querySelector("[data-scroll-area-viewport]") as HTMLElement; if (!scrollbar || !viewport) return; @@ -45,13 +52,13 @@ export const ScrollAreaThumb = component$((props) => { const thumb = thumbRef.value; if (!thumb) return; - const scrollbar = thumb.closest('[data-scroll-area-scrollbar]'); - const root = scrollbar?.closest('[data-scroll-area-root]'); - const viewport = root?.querySelector('[data-scroll-area-viewport]') as HTMLElement; + const scrollbar = thumb.closest("[data-scroll-area-scrollbar]"); + const root = scrollbar?.closest("[data-scroll-area-root]"); + const viewport = root?.querySelector("[data-scroll-area-viewport]") as HTMLElement; if (!scrollbar || !viewport) return; - const isVertical = scrollbar.getAttribute('data-orientation') === 'vertical'; + const isVertical = scrollbar.getAttribute("data-orientation") === "vertical"; e.preventDefault(); @@ -59,14 +66,16 @@ export const ScrollAreaThumb = component$((props) => { const deltaY = e.clientY - dragData.value.startClientY; const scrollbarHeight = scrollbar.clientHeight; const thumbHeight = thumb.clientHeight; - const scrollRatio = (viewport.scrollHeight - viewport.clientHeight) / (scrollbarHeight - thumbHeight); - viewport.scrollTop = dragData.value.startScrollTop + (deltaY * scrollRatio); + const scrollRatio = + (viewport.scrollHeight - viewport.clientHeight) / (scrollbarHeight - thumbHeight); + viewport.scrollTop = dragData.value.startScrollTop + deltaY * scrollRatio; } else { const deltaX = e.clientX - dragData.value.startClientX; const scrollbarWidth = scrollbar.clientWidth; const thumbWidth = thumb.clientWidth; - const scrollRatio = (viewport.scrollWidth - viewport.clientWidth) / (scrollbarWidth - thumbWidth); - viewport.scrollLeft = dragData.value.startScrollLeft + (deltaX * scrollRatio); + const scrollRatio = + (viewport.scrollWidth - viewport.clientWidth) / (scrollbarWidth - thumbWidth); + viewport.scrollLeft = dragData.value.startScrollLeft + deltaX * scrollRatio; } }); @@ -74,15 +83,15 @@ export const ScrollAreaThumb = component$((props) => { isDragging.value = false; }); - useOnDocument('mousemove', onDragMove$); - useOnDocument('mouseup', onDragEnd$); + useOnDocument("mousemove", onDragMove$); + useOnDocument("mouseup", onDragEnd$); return (
); diff --git a/libs/components/src/scroll-area/scroll-area-view-port.tsx b/libs/components/src/scroll-area/scroll-area-view-port.tsx index d112316b..6bc9383d 100644 --- a/libs/components/src/scroll-area/scroll-area-view-port.tsx +++ b/libs/components/src/scroll-area/scroll-area-view-port.tsx @@ -1,26 +1,30 @@ -import { $, component$, PropFunction, PropsOf, Slot } from "@builder.io/qwik"; +import { $, component$, type PropFunction, type PropsOf, Slot } from "@builder.io/qwik"; -type ViewPortProps = PropsOf<'div'> & { +type ViewPortProps = PropsOf<"div"> & { onScroll$?: PropFunction<(e: Event) => void>; }; export const ScrollAreaViewPort = component$((props) => { const onScroll$ = $((e: Event) => { const viewport = e.target as HTMLElement; - const scrollbars = viewport.parentElement?.querySelectorAll('[data-scroll-area-scrollbar]'); + const scrollbars = viewport.parentElement?.querySelectorAll( + "[data-scroll-area-scrollbar]" + ); - scrollbars?.forEach(scrollbar => { - const thumb = scrollbar.querySelector('[data-scroll-area-thumb]') as HTMLElement; + scrollbars?.forEach((scrollbar) => { + const thumb = scrollbar.querySelector("[data-scroll-area-thumb]") as HTMLElement; if (!thumb) return; - const isVertical = scrollbar.getAttribute('data-orientation') === 'vertical'; + const isVertical = scrollbar.getAttribute("data-orientation") === "vertical"; if (isVertical) { - const scrollRatio = viewport.scrollTop / (viewport.scrollHeight - viewport.clientHeight); + const scrollRatio = + viewport.scrollTop / (viewport.scrollHeight - viewport.clientHeight); const maxTop = scrollbar.clientHeight - thumb.clientHeight; thumb.style.transform = `translateY(${scrollRatio * maxTop}px)`; } else { - const scrollRatio = viewport.scrollLeft / (viewport.scrollWidth - viewport.clientWidth); + const scrollRatio = + viewport.scrollLeft / (viewport.scrollWidth - viewport.clientWidth); const maxLeft = scrollbar.clientWidth - thumb.clientWidth; thumb.style.transform = `translateX(${scrollRatio * maxLeft}px)`; } @@ -31,11 +35,7 @@ export const ScrollAreaViewPort = component$((props) => { }); return ( -
+
); From c5ea64f1a1a62a88778a594a51eb118f062ac2d5 Mon Sep 17 00:00:00 2001 From: jack shelton Date: Mon, 23 Dec 2024 15:18:16 -0600 Subject: [PATCH 4/5] almost all files changed --- .../src/scroll-area/scroll-area-scrollbar.tsx | 3 +- .../src/scroll-area/scroll-area-thumb.tsx | 34 +++++++------- .../src/scroll-area/scroll-area-view-port.tsx | 18 +++++++- .../src/scroll-area/scroll-area.test.ts | 45 +++++++++---------- 4 files changed, 56 insertions(+), 44 deletions(-) diff --git a/libs/components/src/scroll-area/scroll-area-scrollbar.tsx b/libs/components/src/scroll-area/scroll-area-scrollbar.tsx index b18aa276..bb293cd1 100644 --- a/libs/components/src/scroll-area/scroll-area-scrollbar.tsx +++ b/libs/components/src/scroll-area/scroll-area-scrollbar.tsx @@ -17,10 +17,9 @@ type ScrollBarType = PropsOf<"div"> & { export const ScrollAreaScrollbar = component$((props) => { const context = useContext(scrollAreaContextId); const { orientation = "vertical" } = props; - const scrollbarRef = useSignal(); const onTrackClick$ = $((e: MouseEvent) => { - const scrollbar = scrollbarRef.value; + const scrollbar = context.scrollbarRef.value; if (!scrollbar) return; const viewport = context.viewportRef.value; diff --git a/libs/components/src/scroll-area/scroll-area-thumb.tsx b/libs/components/src/scroll-area/scroll-area-thumb.tsx index 0a72cd7b..83593c64 100644 --- a/libs/components/src/scroll-area/scroll-area-thumb.tsx +++ b/libs/components/src/scroll-area/scroll-area-thumb.tsx @@ -4,9 +4,11 @@ import { useSignal, useOnDocument, type Signal, - type PropsOf + type PropsOf, + useContext } from "@builder.io/qwik"; import type { PropFunction } from "@builder.io/qwik"; +import { scrollAreaContextId } from "./scroll-area-context"; type ScrollAreaThumb = { ref?: Signal; @@ -16,7 +18,7 @@ type ScrollAreaThumb = { } & PropsOf<"div">; export const ScrollAreaThumb = component$((props) => { - const thumbRef = useSignal(); + const context = useContext(scrollAreaContextId); const isDragging = useSignal(false); const dragData = useSignal({ startClientY: 0, @@ -26,14 +28,13 @@ export const ScrollAreaThumb = component$((props) => { }); const onDragStart$ = $((e: MouseEvent) => { - const thumb = thumbRef.value; - if (!thumb) return; - - const scrollbar = thumb.closest("[data-scroll-area-scrollbar]"); - const root = scrollbar?.closest("[data-scroll-area-root]"); - const viewport = root?.querySelector("[data-scroll-area-viewport]") as HTMLElement; + const thumb = context.thumbRef.value; + const scrollbar = context.scrollbarRef.value; + const viewport = context.viewportRef.value; - if (!scrollbar || !viewport) return; + if (!scrollbar) return; + if (!viewport) return; + if (!thumb) return; e.preventDefault(); isDragging.value = true; @@ -49,14 +50,13 @@ export const ScrollAreaThumb = component$((props) => { const onDragMove$ = $((e: MouseEvent) => { if (!isDragging.value) return; - const thumb = thumbRef.value; - if (!thumb) return; - - const scrollbar = thumb.closest("[data-scroll-area-scrollbar]"); - const root = scrollbar?.closest("[data-scroll-area-root]"); - const viewport = root?.querySelector("[data-scroll-area-viewport]") as HTMLElement; + const thumb = context.thumbRef.value; + const scrollbar = context.scrollbarRef.value; + const viewport = context.viewportRef.value; - if (!scrollbar || !viewport) return; + if (!scrollbar) return; + if (!viewport) return; + if (!thumb) return; const isVertical = scrollbar.getAttribute("data-orientation") === "vertical"; @@ -89,7 +89,7 @@ export const ScrollAreaThumb = component$((props) => { return (
& { onScroll$?: PropFunction<(e: Event) => void>; }; export const ScrollAreaViewPort = component$((props) => { + const context = useContext(scrollAreaContextId); const onScroll$ = $((e: Event) => { const viewport = e.target as HTMLElement; const scrollbars = viewport.parentElement?.querySelectorAll( @@ -35,7 +44,12 @@ export const ScrollAreaViewPort = component$((props) => { }); return ( -
+
); diff --git a/libs/components/src/scroll-area/scroll-area.test.ts b/libs/components/src/scroll-area/scroll-area.test.ts index 9ee624e8..68fbbda6 100644 --- a/libs/components/src/scroll-area/scroll-area.test.ts +++ b/libs/components/src/scroll-area/scroll-area.test.ts @@ -31,7 +31,7 @@ test.describe("critical functionality", () => { const viewport = d.getViewport(); // Scroll to bottom - await viewport.evaluate(el => { + await viewport.evaluate((el) => { el.scrollTop = el.scrollHeight; }); @@ -47,9 +47,12 @@ test.describe("critical functionality", () => { const thumbBox = await thumb.boundingBox(); const scrollbarBox = await scrollbar.boundingBox(); + if (!thumbBox) return; + if (!scrollbarBox) return; + // Calculate expected bottom position considering padding - const expectedBottom = scrollbarBox!.y + scrollbarBox!.height ; - const actualBottom = thumbBox!.y + thumbBox!.height - 2; // 2px for padding + const expectedBottom = scrollbarBox.y + scrollbarBox.height; + const actualBottom = thumbBox.y + thumbBox.height - 2; // 2px for padding expect(actualBottom).toBeCloseTo(expectedBottom, 1); }); @@ -64,11 +67,11 @@ test.describe("drag functionality", () => { const viewport = d.getViewport(); // Get initial scroll position - const initialScrollTop = await viewport.evaluate(el => el.scrollTop); + const initialScrollTop = await viewport.evaluate((el) => el.scrollTop); // Get thumb position const thumbBox = await thumb.boundingBox(); - if (!thumbBox) throw new Error('Could not get thumb position'); + if (!thumbBox) throw new Error("Could not get thumb position"); // Simulate drag operation // Start at the center of the thumb @@ -91,7 +94,7 @@ test.describe("drag functionality", () => { await page.waitForTimeout(100); // Verify new scroll position - const newScrollTop = await viewport.evaluate(el => el.scrollTop); + const newScrollTop = await viewport.evaluate((el) => el.scrollTop); expect(newScrollTop).toBeGreaterThan(initialScrollTop); }); @@ -103,11 +106,11 @@ test.describe("drag functionality", () => { const viewport = d.getViewport(); // Get initial scroll position - const initialScrollLeft = await viewport.evaluate(el => el.scrollLeft); + const initialScrollLeft = await viewport.evaluate((el) => el.scrollLeft); // Get thumb position const thumbBox = await thumb.boundingBox(); - if (!thumbBox) throw new Error('Could not get thumb position'); + if (!thumbBox) throw new Error("Could not get thumb position"); // Simulate drag operation // Start at the center of the thumb @@ -130,7 +133,7 @@ test.describe("drag functionality", () => { await page.waitForTimeout(100); // Verify new scroll position - const newScrollLeft = await viewport.evaluate(el => el.scrollLeft); + const newScrollLeft = await viewport.evaluate((el) => el.scrollLeft); expect(newScrollLeft).toBeGreaterThan(initialScrollLeft); }); }); @@ -144,7 +147,7 @@ test.describe("thumb behavior", () => { // Get thumb position const thumbBox = await thumb.boundingBox(); - if (!thumbBox) throw new Error('Could not get thumb position'); + if (!thumbBox) throw new Error("Could not get thumb position"); // Move to center of thumb and click await page.mouse.move( @@ -156,7 +159,7 @@ test.describe("thumb behavior", () => { // Wait for drag state to update await page.waitForTimeout(50); - await expect(thumb).toHaveAttribute('data-dragging', ''); + await expect(thumb).toHaveAttribute("data-dragging", ""); // Cleanup await page.mouse.up(); @@ -170,23 +173,20 @@ test.describe("thumb behavior", () => { const viewport = d.getViewport(); // Get initial scroll position - const initialScrollTop = await viewport.evaluate(el => el.scrollTop); + const initialScrollTop = await viewport.evaluate((el) => el.scrollTop); // Get scrollbar position const box = await scrollbar.boundingBox(); - if (!box) throw new Error('Could not get scrollbar position'); + if (!box) throw new Error("Could not get scrollbar position"); // Click in the middle of the scrollbar, but account for padding - await page.mouse.click( - box.x + box.width / 2, - box.y + box.height / 2 - ); + await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2); // Wait for scroll to complete await page.waitForTimeout(50); // Verify new scroll position - const newScrollTop = await viewport.evaluate(el => el.scrollTop); + const newScrollTop = await viewport.evaluate((el) => el.scrollTop); expect(newScrollTop).toBeGreaterThan(initialScrollTop); }); }); @@ -202,13 +202,13 @@ test.describe("a11y", () => { await viewport.focus(); // Test various keyboard shortcuts - const shortcuts = ['PageDown', 'PageUp', 'Home', 'End']; + const shortcuts = ["PageDown", "PageUp", "Home", "End"]; for (const key of shortcuts) { - const initialScrollTop = await viewport.evaluate(el => el.scrollTop); + const initialScrollTop = await viewport.evaluate((el) => el.scrollTop); await page.keyboard.press(key); - const newScrollTop = await viewport.evaluate(el => el.scrollTop); + const newScrollTop = await viewport.evaluate((el) => el.scrollTop); - if (key === 'PageDown' || key === 'End') { + if (key === "PageDown" || key === "End") { expect(newScrollTop).toBeGreaterThanOrEqual(initialScrollTop); } else { expect(newScrollTop).toBeLessThanOrEqual(initialScrollTop); @@ -216,4 +216,3 @@ test.describe("a11y", () => { } }); }); - From 4f42e3eaabc7433d0d727b880268d8f1dee3e1be Mon Sep 17 00:00:00 2001 From: jack shelton Date: Mon, 23 Dec 2024 15:25:13 -0600 Subject: [PATCH 5/5] no more biome complains --- libs/components/src/scroll-area/scroll-area-view-port.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/components/src/scroll-area/scroll-area-view-port.tsx b/libs/components/src/scroll-area/scroll-area-view-port.tsx index 4ff248aa..5751b06a 100644 --- a/libs/components/src/scroll-area/scroll-area-view-port.tsx +++ b/libs/components/src/scroll-area/scroll-area-view-port.tsx @@ -16,11 +16,11 @@ export const ScrollAreaViewPort = component$((props) => { const context = useContext(scrollAreaContextId); const onScroll$ = $((e: Event) => { const viewport = e.target as HTMLElement; - const scrollbars = viewport.parentElement?.querySelectorAll( - "[data-scroll-area-scrollbar]" + const scrollbars = Array.from( + viewport.parentElement?.querySelectorAll("[data-scroll-area-scrollbar]") || [] ); - scrollbars?.forEach((scrollbar) => { + for (const scrollbar of scrollbars) { const thumb = scrollbar.querySelector("[data-scroll-area-thumb]") as HTMLElement; if (!thumb) return; @@ -37,7 +37,7 @@ export const ScrollAreaViewPort = component$((props) => { const maxLeft = scrollbar.clientWidth - thumb.clientWidth; thumb.style.transform = `translateX(${scrollRatio * maxLeft}px)`; } - }); + } // Call the provided onScroll$ handler if it exists props.onScroll$?.(e);