fix(app): stabilize session timeline layout continuity (#34533)
This commit is contained in:
parent
f266e829cf
commit
3cf71808c4
57 changed files with 6159 additions and 142 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { scrollKey, scrollTopFromThumbPointer } from "./scroll-view"
|
||||
import { canScrollKey, scrollKey, scrollTopFromThumbPointer } from "./scroll-view"
|
||||
|
||||
describe("scrollKey", () => {
|
||||
test("maps plain navigation keys", () => {
|
||||
|
|
@ -16,6 +16,27 @@ describe("scrollKey", () => {
|
|||
expect(scrollKey({ key: "PageUp", altKey: false, ctrlKey: true, metaKey: false, shiftKey: false })).toBeUndefined()
|
||||
expect(scrollKey({ key: "End", altKey: false, ctrlKey: false, metaKey: false, shiftKey: true })).toBeUndefined()
|
||||
})
|
||||
|
||||
test("maps space and shift-space directions", () => {
|
||||
expect(scrollKey({ key: " ", altKey: false, ctrlKey: false, metaKey: false, shiftKey: false })).toBe("page-down")
|
||||
expect(scrollKey({ key: " ", altKey: false, ctrlKey: false, metaKey: false, shiftKey: true })).toBe("page-up")
|
||||
})
|
||||
})
|
||||
|
||||
describe("canScrollKey", () => {
|
||||
const element = (scrollTop: number, clientHeight = 100, scrollHeight = 300) =>
|
||||
({ scrollTop, clientHeight, scrollHeight }) as HTMLElement
|
||||
|
||||
test("owns upward keys only above the top boundary", () => {
|
||||
expect(canScrollKey(element(50), "page-up")).toBe(true)
|
||||
expect(canScrollKey(element(0), "page-up")).toBe(false)
|
||||
})
|
||||
|
||||
test("owns downward keys only before the bottom boundary", () => {
|
||||
expect(canScrollKey(element(50), "page-down")).toBe(true)
|
||||
expect(canScrollKey(element(200), "page-down")).toBe(false)
|
||||
expect(canScrollKey(element(0, 100, 100), "page-down")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("scrollTopFromThumbPointer", () => {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ export interface ScrollViewProps extends ComponentProps<"div"> {
|
|||
}
|
||||
|
||||
export const scrollKey = (event: Pick<KeyboardEvent, "key" | "altKey" | "ctrlKey" | "metaKey" | "shiftKey">) => {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
|
||||
if (event.altKey || event.ctrlKey || event.metaKey) return
|
||||
if (event.shiftKey && event.key !== " ") return
|
||||
|
||||
switch (event.key) {
|
||||
case "PageDown":
|
||||
|
|
@ -24,9 +25,36 @@ export const scrollKey = (event: Pick<KeyboardEvent, "key" | "altKey" | "ctrlKey
|
|||
return "up"
|
||||
case "ArrowDown":
|
||||
return "down"
|
||||
case " ":
|
||||
return event.shiftKey ? "page-up" : "page-down"
|
||||
}
|
||||
}
|
||||
|
||||
export function canScrollKey(element: HTMLElement, key: NonNullable<ReturnType<typeof scrollKey>>) {
|
||||
const up = key === "up" || key === "page-up" || key === "home"
|
||||
return up ? element.scrollTop > 0 : element.scrollTop + element.clientHeight < element.scrollHeight
|
||||
}
|
||||
|
||||
export function scrollKeyOwner(
|
||||
root: HTMLElement,
|
||||
target: EventTarget | null,
|
||||
key: NonNullable<ReturnType<typeof scrollKey>>,
|
||||
) {
|
||||
const element = target instanceof Element ? target : undefined
|
||||
const owner = element?.closest<HTMLElement>("[data-scrollable]")
|
||||
if (!owner || owner === root) return root
|
||||
if (!root.contains(owner)) return owner
|
||||
return canScrollKey(owner, key) ? owner : root
|
||||
}
|
||||
|
||||
export function isScrollKeyTarget(target: EventTarget | null, key: NonNullable<ReturnType<typeof scrollKey>>) {
|
||||
const element = target instanceof HTMLElement ? target : undefined
|
||||
if (!element) return true
|
||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(element.tagName) || element.isContentEditable) return false
|
||||
if ((key === "page-up" || key === "page-down") && element.closest('button, a[href], [role="button"]')) return false
|
||||
return true
|
||||
}
|
||||
|
||||
export function scrollTopFromThumbPointer(input: {
|
||||
pointer: number
|
||||
viewportTop: number
|
||||
|
|
@ -160,9 +188,10 @@ export function ScrollView(props: ScrollViewProps) {
|
|||
if (document.activeElement && ["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement.tagName)) {
|
||||
return
|
||||
}
|
||||
|
||||
const next = scrollKey(e)
|
||||
if (!next) return
|
||||
if (!isScrollKeyTarget(e.target, next)) return
|
||||
if (scrollKeyOwner(viewportRef, e.target, next) !== viewportRef) return
|
||||
|
||||
const scrollAmount = viewportRef.clientHeight * 0.8
|
||||
const lineAmount = 40
|
||||
|
|
@ -208,6 +237,7 @@ export function ScrollView(props: ScrollViewProps) {
|
|||
<div
|
||||
ref={viewportRef}
|
||||
class="scroll-view__viewport"
|
||||
data-scrollable
|
||||
onScroll={(e) => {
|
||||
updateThumb()
|
||||
if (typeof events.onScroll === "function") events.onScroll(e as any)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue