diff --git a/package.json b/package.json index ed742528..be046ced 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@lit-labs/task": "^1.1.3", "@types/bluebird": "^3.5.42", "component-emitter": "^1.3.1", + "compute-scroll-into-view": "^3.1.1", "crypto": "^1.0.1", "date-fns": "^3.6.0", "date-fns-tz": "^3.1.3", diff --git a/src/components/gallery.ts b/src/components/gallery.ts index da820884..fdc12af0 100644 --- a/src/components/gallery.ts +++ b/src/components/gallery.ts @@ -12,6 +12,7 @@ import { createRef, ref, Ref } from 'lit/directives/ref.js'; import throttle from 'lodash-es/throttle'; import { CameraManager, ExtendedMediaQueryResult } from '../camera-manager/manager.js'; import { EventQuery, MediaQuery, RecordingQuery } from '../camera-manager/types'; +import { ViewManagerEpoch } from '../card-controller/view/types.js'; import { CardWideConfig, frigateCardConfigDefaults, @@ -23,6 +24,7 @@ import galleryStyle from '../scss/gallery.scss'; import { ExtendedHomeAssistant } from '../types.js'; import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; import { errorToConsole, sleep } from '../utils/basic'; +import { scrollIntoView } from '../utils/scroll.js'; import { ViewMedia } from '../view/media'; import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries'; import { MediaQueriesClassifier } from '../view/media-queries-classifier'; @@ -32,7 +34,6 @@ import { renderMessage, renderProgressIndicator } from './message.js'; import './surround-basic'; import './thumbnail.js'; import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js'; -import { ViewManagerEpoch } from '../card-controller/view/types.js'; const GALLERY_MEDIA_FILTER_MENU_ICONS = { closed: 'mdi:filter-cog-outline', @@ -484,7 +485,8 @@ export class FrigateCardGalleryCore extends LitElement { // ... and there is a thumbnail rendered that is selected. this._refSelected.value ) { - this._refSelected.value.scrollIntoView({ + scrollIntoView(this._refSelected.value, { + boundary: this, block: 'center', }); } diff --git a/src/utils/scroll.ts b/src/utils/scroll.ts new file mode 100644 index 00000000..08c2975b --- /dev/null +++ b/src/utils/scroll.ts @@ -0,0 +1,13 @@ +import { compute as computeScroll, Options } from 'compute-scroll-into-view'; + +// Alternative to the stock element.scrollIntoView that suppports limiting +// scrolling to a boundary, rather than the entire browser root. +// +// See: https://github.com/dermotduffy/frigate-hass-card/issues/1814 +// See: https://github.com/w3c/csswg-drafts/issues/9452 +export const scrollIntoView = (element: HTMLElement, options: Options) => { + computeScroll(element, options).forEach(({ el, top, left }) => { + el.scrollTop = top; + el.scrollLeft = left; + }); +}; diff --git a/tests/utils/scroll.test.ts b/tests/utils/scroll.test.ts new file mode 100644 index 00000000..4655b509 --- /dev/null +++ b/tests/utils/scroll.test.ts @@ -0,0 +1,33 @@ +import { compute as computeScroll } from 'compute-scroll-into-view'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { scrollIntoView } from '../../src/utils/scroll'; + +vi.mock('compute-scroll-into-view'); + +// @vitest-environment jsdom +describe('scrollIntoView', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should call computeScroll with the correct arguments', () => { + const element = document.createElement('div'); + vi.mocked(computeScroll).mockReturnValue([ + { + el: element, + top: 42, + left: 142, + }, + ]); + const options = { block: 'start' as const, inline: 'nearest' as const }; + + expect(element.scrollTop).toBe(0); + expect(element.scrollLeft).toBe(0); + + scrollIntoView(element, options); + + expect(computeScroll).toBeCalledWith(element, options); + expect(element.scrollTop).toBe(42); + expect(element.scrollLeft).toBe(142); + }); +}); diff --git a/vite.config.ts b/vite.config.ts index 632f34f4..ea40a38f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -38,6 +38,7 @@ const FULL_COVERAGE_FILES_RELATIVE = [ 'utils/media.ts', 'utils/ptz.ts', 'utils/screenshot.ts', + 'utils/scroll.ts', 'utils/substream.ts', 'utils/text-direction.ts', 'utils/timer.ts', diff --git a/yarn.lock b/yarn.lock index 275e709f..743fdb79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3448,6 +3448,13 @@ __metadata: languageName: node linkType: hard +"compute-scroll-into-view@npm:^3.1.1": + version: 3.1.1 + resolution: "compute-scroll-into-view@npm:3.1.1" + checksum: 10c0/59761ed62304a9599b52ad75d0d6fbf0669ee2ab7dd472fdb0ad9da36628414c014dea7b5810046560180ad30ffec52a953d19297f66a1d4f3aa0999b9d2521d + languageName: node + linkType: hard + "concat-map@npm:0.0.1": version: 0.0.1 resolution: "concat-map@npm:0.0.1" @@ -5224,6 +5231,7 @@ __metadata: "@typescript-eslint/parser": "npm:^7.13.0" "@vitest/coverage-istanbul": "npm:^1.6.0" component-emitter: "npm:^1.3.1" + compute-scroll-into-view: "npm:^3.1.1" conventional-changelog-conventionalcommits: "npm:^8.0.0" crypto: "npm:^1.0.1" date-fns: "npm:^3.6.0"