feat: Add option to disable mousewheel carousel scrolling (#2263)

- Closes: #2220
This commit is contained in:
Dermot Duffy
2025-12-08 17:19:56 -08:00
committed by GitHub
parent f276b8c57d
commit 13bea49429
18 changed files with 69 additions and 7 deletions
+2
View File
@@ -41,6 +41,7 @@ live:
| `next_previous` | | Configures how the "Next & Previous" controls are shown on the `live` view. See below. | | `next_previous` | | Configures how the "Next & Previous" controls are shown on the `live` view. See below. |
| `thumbnails` | | Configures how thumbnails are shown on the `live` view. See below. | | `thumbnails` | | Configures how thumbnails are shown on the `live` view. See below. |
| `timeline` | | Configures how the mini-timeline is shown on the `live` view. See below. | | `timeline` | | Configures how the mini-timeline is shown on the `live` view. See below. |
| `wheel` | `true` | Whether to allow mouse wheel to scroll through the carousel. |
### `next_previous` ### `next_previous`
@@ -206,6 +207,7 @@ live:
next_previous: next_previous:
style: chevrons style: chevrons
size: 48 size: 48
wheel: true
ptz: ptz:
mode: auto mode: auto
position: bottom-right position: bottom-right
+2
View File
@@ -37,6 +37,7 @@ media_viewer:
| `next_previous` | | Configures how the "Next & Previous" controls are shown on the media viewer. See below. | | `next_previous` | | Configures how the "Next & Previous" controls are shown on the media viewer. See below. |
| `thumbnails` | | Configures how thumbnails are shown on the media viewer. See below. | | `thumbnails` | | Configures how thumbnails are shown on the media viewer. See below. |
| `timeline` | | Configures how the mini-timeline is shown on the media viewer. See below. | | `timeline` | | Configures how the mini-timeline is shown on the media viewer. See below. |
| `wheel` | `true` | Whether to allow mouse wheel to scroll through the carousel. |
### `next_previous` ### `next_previous`
@@ -183,6 +184,7 @@ media_viewer:
next_previous: next_previous:
size: 48 size: 48
style: thumbnails style: thumbnails
wheel: true
ptz: ptz:
mode: off mode: off
position: bottom-right position: bottom-right
+4
View File
@@ -42,6 +42,9 @@ export class AdvancedCameraCardCarousel extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public plugins?: EmblaCarouselPlugins; public plugins?: EmblaCarouselPlugins;
@property({ attribute: false })
public wheelScrolling?: boolean;
@property({ attribute: false }) @property({ attribute: false })
public selected = 0; public selected = 0;
@@ -121,6 +124,7 @@ export class AdvancedCameraCardCarousel extends LitElement {
loop: this.loop, loop: this.loop,
plugins: this.plugins, plugins: this.plugins,
textDirection: getTextDirection(this), textDirection: getTextDirection(this),
wheelScrolling: this.wheelScrolling,
}, },
); );
} else if (changedProps.has('selected')) { } else if (changedProps.has('selected')) {
+1
View File
@@ -339,6 +339,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
this._getPlugins.bind(this), this._getPlugins.bind(this),
)} )}
.selected=${this._getSelectedCameraIndex()} .selected=${this._getSelectedCameraIndex()}
.wheelScrolling=${this.liveConfig?.controls.wheel}
transitionEffect=${this._getTransitionEffect()} transitionEffect=${this._getTransitionEffect()}
@advanced-camera-card:carousel:select=${this._setViewHandler.bind(this)} @advanced-camera-card:carousel:select=${this._setViewHandler.bind(this)}
@advanced-camera-card:media:loaded=${() => { @advanced-camera-card:media:loaded=${() => {
+1
View File
@@ -339,6 +339,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
.dragEnabled=${this.viewerConfig?.draggable ?? true} .dragEnabled=${this.viewerConfig?.draggable ?? true}
.plugins=${guard([this.viewerConfig, this._media], this._getPlugins.bind(this))} .plugins=${guard([this.viewerConfig, this._media], this._getPlugins.bind(this))}
.selected=${this._selected} .selected=${this._selected}
.wheelScrolling=${this.viewerConfig?.controls.wheel}
transitionEffect=${this._getTransitionEffect()} transitionEffect=${this._getTransitionEffect()}
@advanced-camera-card:carousel:select=${(ev: CustomEvent<CarouselSelected>) => { @advanced-camera-card:carousel:select=${(ev: CustomEvent<CarouselSelected>) => {
this._setViewSelectedIndex(ev.detail.index); this._setViewSelectedIndex(ev.detail.index);
+2
View File
@@ -69,6 +69,7 @@ export const liveConfigDefault = {
ptz: ptzControlsDefaults, ptz: ptzControlsDefaults,
thumbnails: liveThumbnailControlsDefaults, thumbnails: liveThumbnailControlsDefaults,
timeline: miniTimelineConfigDefault, timeline: miniTimelineConfigDefault,
wheel: true,
}, },
microphone: { microphone: {
...microphoneConfigDefault, ...microphoneConfigDefault,
@@ -121,6 +122,7 @@ export const liveConfigSchema = z
liveConfigDefault.controls.thumbnails, liveConfigDefault.controls.thumbnails,
), ),
timeline: miniTimelineConfigSchema.default(liveConfigDefault.controls.timeline), timeline: miniTimelineConfigSchema.default(liveConfigDefault.controls.timeline),
wheel: z.boolean().default(liveConfigDefault.controls.wheel),
}) })
.default(liveConfigDefault.controls), .default(liveConfigDefault.controls),
display: viewDisplaySchema, display: viewDisplaySchema,
+2
View File
@@ -40,6 +40,7 @@ export const viewerConfigDefault = {
...ptzControlsDefaults, ...ptzControlsDefaults,
mode: 'off' as const, mode: 'off' as const,
}, },
wheel: true,
}, },
}; };
@@ -101,6 +102,7 @@ export const viewerConfigSchema = z
timeline: miniTimelineConfigSchema.default( timeline: miniTimelineConfigSchema.default(
viewerConfigDefault.controls.timeline, viewerConfigDefault.controls.timeline,
), ),
wheel: z.boolean().default(viewerConfigDefault.controls.wheel),
}) })
.default(viewerConfigDefault.controls), .default(viewerConfigDefault.controls),
}) })
+3
View File
@@ -248,6 +248,8 @@ export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE =
`${CONF_MEDIA_VIEWER}.controls.timeline.style` as const; `${CONF_MEDIA_VIEWER}.controls.timeline.style` as const;
export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS = export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS =
`${CONF_MEDIA_VIEWER}.controls.timeline.window_seconds` as const; `${CONF_MEDIA_VIEWER}.controls.timeline.window_seconds` as const;
export const CONF_MEDIA_VIEWER_CONTROLS_WHEEL =
`${CONF_MEDIA_VIEWER}.controls.wheel` as const;
export const CONF_MEDIA_VIEWER_ZOOMABLE = `${CONF_MEDIA_VIEWER}.zoomable` as const; export const CONF_MEDIA_VIEWER_ZOOMABLE = `${CONF_MEDIA_VIEWER}.zoomable` as const;
const CONF_LIVE = 'live' as const; const CONF_LIVE = 'live' as const;
@@ -271,6 +273,7 @@ export const CONF_LIVE_CONTROLS_PTZ_ORIENTATION =
`${CONF_LIVE}.controls.ptz.orientation` as const; `${CONF_LIVE}.controls.ptz.orientation` as const;
export const CONF_LIVE_CONTROLS_PTZ_POSITION = export const CONF_LIVE_CONTROLS_PTZ_POSITION =
`${CONF_LIVE}.controls.ptz.position` as const; `${CONF_LIVE}.controls.ptz.position` as const;
export const CONF_LIVE_CONTROLS_WHEEL = `${CONF_LIVE}.controls.wheel` as const;
export const CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA_TYPE = export const CONF_LIVE_CONTROLS_THUMBNAILS_MEDIA_TYPE =
`${CONF_LIVE}.controls.thumbnails.media_type` as const; `${CONF_LIVE}.controls.thumbnails.media_type` as const;
export const CONF_LIVE_CONTROLS_THUMBNAILS_EVENTS_MEDIA_TYPE = export const CONF_LIVE_CONTROLS_THUMBNAILS_EVENTS_MEDIA_TYPE =
+16
View File
@@ -133,6 +133,7 @@ import {
CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS, CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS,
CONF_LIVE_CONTROLS_TIMELINE_STYLE, CONF_LIVE_CONTROLS_TIMELINE_STYLE,
CONF_LIVE_CONTROLS_TIMELINE_WINDOW_SECONDS, CONF_LIVE_CONTROLS_TIMELINE_WINDOW_SECONDS,
CONF_LIVE_CONTROLS_WHEEL,
CONF_LIVE_DISPLAY_GRID_COLUMNS, CONF_LIVE_DISPLAY_GRID_COLUMNS,
CONF_LIVE_DISPLAY_GRID_MAX_COLUMNS, CONF_LIVE_DISPLAY_GRID_MAX_COLUMNS,
CONF_LIVE_DISPLAY_GRID_SELECTED_POSITION, CONF_LIVE_DISPLAY_GRID_SELECTED_POSITION,
@@ -175,6 +176,7 @@ import {
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS, CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE, CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS, CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS,
CONF_MEDIA_VIEWER_CONTROLS_WHEEL,
CONF_MEDIA_VIEWER_DISPLAY_GRID_COLUMNS, CONF_MEDIA_VIEWER_DISPLAY_GRID_COLUMNS,
CONF_MEDIA_VIEWER_DISPLAY_GRID_MAX_COLUMNS, CONF_MEDIA_VIEWER_DISPLAY_GRID_MAX_COLUMNS,
CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_POSITION, CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_POSITION,
@@ -2911,6 +2913,13 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
label: localize('config.common.controls.builtin'), label: localize('config.common.controls.builtin'),
}, },
)} )}
${this._renderSwitch(
CONF_LIVE_CONTROLS_WHEEL,
this._defaults.live.controls.wheel,
{
label: localize('config.common.controls.wheel'),
},
)}
${this._renderNextPreviousControls( ${this._renderNextPreviousControls(
MENU_LIVE_CONTROLS_NEXT_PREVIOUS, MENU_LIVE_CONTROLS_NEXT_PREVIOUS,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE, CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
@@ -3106,6 +3115,13 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
label: localize('config.common.controls.builtin'), label: localize('config.common.controls.builtin'),
}, },
)} )}
${this._renderSwitch(
CONF_MEDIA_VIEWER_CONTROLS_WHEEL,
this._defaults.media_viewer.controls.wheel,
{
label: localize('config.common.controls.wheel'),
},
)}
${this._renderNextPreviousControls( ${this._renderNextPreviousControls(
MENU_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS, MENU_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS,
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE, CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
+2 -1
View File
@@ -263,7 +263,8 @@
"seek-in-camera": "Cerca d'escombratge només a la càmera seleccionada", "seek-in-camera": "Cerca d'escombratge només a la càmera seleccionada",
"seek-in-media": "Cerca d'escombratge només dins de l'element multimèdia seleccionat" "seek-in-media": "Cerca d'escombratge només dins de l'element multimèdia seleccionat"
} }
} },
"wheel": ""
}, },
"display": { "display": {
"editor_label": "Visualització", "editor_label": "Visualització",
+2 -1
View File
@@ -263,7 +263,8 @@
"seek-in-camera": "Pan seeks within selected camera only", "seek-in-camera": "Pan seeks within selected camera only",
"seek-in-media": "Pan seeks within selected media item only" "seek-in-media": "Pan seeks within selected media item only"
} }
} },
"wheel": "Scroll wheel scrolls media carousel"
}, },
"display": { "display": {
"editor_label": "Display", "editor_label": "Display",
+2 -1
View File
@@ -263,7 +263,8 @@
"seek-in-camera": "Pan ne cherche que parmi la caméra sélectionnée", "seek-in-camera": "Pan ne cherche que parmi la caméra sélectionnée",
"seek-in-media": "Pan ne cherche que parmi les médias sélectionnés" "seek-in-media": "Pan ne cherche que parmi les médias sélectionnés"
} }
} },
"wheel": ""
}, },
"display": { "display": {
"editor_label": "Affichage", "editor_label": "Affichage",
+2 -1
View File
@@ -263,7 +263,8 @@
"seek-in-camera": "", "seek-in-camera": "",
"seek-in-media": "" "seek-in-media": ""
} }
} },
"wheel": ""
}, },
"display": { "display": {
"editor_label": "", "editor_label": "",
+2 -1
View File
@@ -263,7 +263,8 @@
"seek-in-camera": "", "seek-in-camera": "",
"seek-in-media": "" "seek-in-media": ""
} }
} },
"wheel": ""
}, },
"display": { "display": {
"editor_label": "", "editor_label": "",
+2 -1
View File
@@ -263,7 +263,8 @@
"seek-in-camera": "", "seek-in-camera": "",
"seek-in-media": "" "seek-in-media": ""
} }
} },
"wheel": ""
}, },
"display": { "display": {
"editor_label": "", "editor_label": "",
+4 -1
View File
@@ -26,6 +26,7 @@ export class CarouselController {
protected _dragFree: boolean; protected _dragFree: boolean;
protected _draggable: boolean; protected _draggable: boolean;
protected _textDirection: TextDirection; protected _textDirection: TextDirection;
protected _wheelScrolling: boolean;
protected _plugins: EmblaCarouselPlugins; protected _plugins: EmblaCarouselPlugins;
protected _carousel: EmblaCarouselType; protected _carousel: EmblaCarouselType;
@@ -48,6 +49,7 @@ export class CarouselController {
dragFree?: boolean; dragFree?: boolean;
plugins?: EmblaCarouselPlugins; plugins?: EmblaCarouselPlugins;
textDirection?: TextDirection; textDirection?: TextDirection;
wheelScrolling?: boolean;
}, },
) { ) {
this._root = root; this._root = root;
@@ -60,6 +62,7 @@ export class CarouselController {
this._draggable = options?.dragEnabled ?? true; this._draggable = options?.dragEnabled ?? true;
this._plugins = options?.plugins ?? []; this._plugins = options?.plugins ?? [];
this._textDirection = options?.textDirection ?? 'ltr'; this._textDirection = options?.textDirection ?? 'ltr';
this._wheelScrolling = options?.wheelScrolling ?? true;
this._carousel = this._createCarousel(getChildrenFromElement(this._parent)); this._carousel = this._createCarousel(getChildrenFromElement(this._parent));
@@ -144,7 +147,7 @@ export class CarouselController {
}, },
[ [
...this._plugins, ...this._plugins,
...(slides.length > 1 ...(slides.length > 1 && this._wheelScrolling
? [ ? [
WheelGesturesPlugin({ WheelGesturesPlugin({
// Whether the carousel is vertical or horizontal, interpret y-axis wheel // Whether the carousel is vertical or horizontal, interpret y-axis wheel
+2
View File
@@ -113,6 +113,7 @@ describe('config defaults', () => {
style: 'ribbon', style: 'ribbon',
window_seconds: 3600, window_seconds: 3600,
}, },
wheel: true,
}, },
draggable: true, draggable: true,
lazy_load: true, lazy_load: true,
@@ -180,6 +181,7 @@ describe('config defaults', () => {
style: 'ribbon', style: 'ribbon',
window_seconds: 3600, window_seconds: 3600,
}, },
wheel: true,
}, },
draggable: true, draggable: true,
lazy_load: true, lazy_load: true,
@@ -239,6 +239,24 @@ describe('CarouselController', () => {
); );
}); });
it('should not include wheel plugin when wheelScrolling is false', () => {
const children = createTestSlideNodes();
const root = createRoot();
const parent = createParent({ children: children });
new CarouselController(root, parent, { wheelScrolling: false });
// Verify WheelGesturesPlugin is NOT present
expect(EmblaCarousel).toBeCalledWith(
root,
expect.anything(),
expect.not.arrayContaining([
expect.objectContaining({
name: 'wheelGestures',
}),
]),
);
});
it('should recreate carousel when children are added', () => { it('should recreate carousel when children are added', () => {
const children = createTestSlideNodes(); const children = createTestSlideNodes();
const root = createRoot(); const root = createRoot();