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. |
| `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. |
| `wheel` | `true` | Whether to allow mouse wheel to scroll through the carousel. |
### `next_previous`
@@ -206,6 +207,7 @@ live:
next_previous:
style: chevrons
size: 48
wheel: true
ptz:
mode: auto
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. |
| `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. |
| `wheel` | `true` | Whether to allow mouse wheel to scroll through the carousel. |
### `next_previous`
@@ -183,6 +184,7 @@ media_viewer:
next_previous:
size: 48
style: thumbnails
wheel: true
ptz:
mode: off
position: bottom-right
+4
View File
@@ -42,6 +42,9 @@ export class AdvancedCameraCardCarousel extends LitElement {
@property({ attribute: false })
public plugins?: EmblaCarouselPlugins;
@property({ attribute: false })
public wheelScrolling?: boolean;
@property({ attribute: false })
public selected = 0;
@@ -121,6 +124,7 @@ export class AdvancedCameraCardCarousel extends LitElement {
loop: this.loop,
plugins: this.plugins,
textDirection: getTextDirection(this),
wheelScrolling: this.wheelScrolling,
},
);
} else if (changedProps.has('selected')) {
+1
View File
@@ -339,6 +339,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
this._getPlugins.bind(this),
)}
.selected=${this._getSelectedCameraIndex()}
.wheelScrolling=${this.liveConfig?.controls.wheel}
transitionEffect=${this._getTransitionEffect()}
@advanced-camera-card:carousel:select=${this._setViewHandler.bind(this)}
@advanced-camera-card:media:loaded=${() => {
+1
View File
@@ -339,6 +339,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
.dragEnabled=${this.viewerConfig?.draggable ?? true}
.plugins=${guard([this.viewerConfig, this._media], this._getPlugins.bind(this))}
.selected=${this._selected}
.wheelScrolling=${this.viewerConfig?.controls.wheel}
transitionEffect=${this._getTransitionEffect()}
@advanced-camera-card:carousel:select=${(ev: CustomEvent<CarouselSelected>) => {
this._setViewSelectedIndex(ev.detail.index);
+2
View File
@@ -69,6 +69,7 @@ export const liveConfigDefault = {
ptz: ptzControlsDefaults,
thumbnails: liveThumbnailControlsDefaults,
timeline: miniTimelineConfigDefault,
wheel: true,
},
microphone: {
...microphoneConfigDefault,
@@ -121,6 +122,7 @@ export const liveConfigSchema = z
liveConfigDefault.controls.thumbnails,
),
timeline: miniTimelineConfigSchema.default(liveConfigDefault.controls.timeline),
wheel: z.boolean().default(liveConfigDefault.controls.wheel),
})
.default(liveConfigDefault.controls),
display: viewDisplaySchema,
+2
View File
@@ -40,6 +40,7 @@ export const viewerConfigDefault = {
...ptzControlsDefaults,
mode: 'off' as const,
},
wheel: true,
},
};
@@ -101,6 +102,7 @@ export const viewerConfigSchema = z
timeline: miniTimelineConfigSchema.default(
viewerConfigDefault.controls.timeline,
),
wheel: z.boolean().default(viewerConfigDefault.controls.wheel),
})
.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;
export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS =
`${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;
const CONF_LIVE = 'live' as const;
@@ -271,6 +273,7 @@ export const CONF_LIVE_CONTROLS_PTZ_ORIENTATION =
`${CONF_LIVE}.controls.ptz.orientation` as const;
export const CONF_LIVE_CONTROLS_PTZ_POSITION =
`${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 =
`${CONF_LIVE}.controls.thumbnails.media_type` as const;
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_STYLE,
CONF_LIVE_CONTROLS_TIMELINE_WINDOW_SECONDS,
CONF_LIVE_CONTROLS_WHEEL,
CONF_LIVE_DISPLAY_GRID_COLUMNS,
CONF_LIVE_DISPLAY_GRID_MAX_COLUMNS,
CONF_LIVE_DISPLAY_GRID_SELECTED_POSITION,
@@ -175,6 +176,7 @@ import {
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE,
CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS,
CONF_MEDIA_VIEWER_CONTROLS_WHEEL,
CONF_MEDIA_VIEWER_DISPLAY_GRID_COLUMNS,
CONF_MEDIA_VIEWER_DISPLAY_GRID_MAX_COLUMNS,
CONF_MEDIA_VIEWER_DISPLAY_GRID_SELECTED_POSITION,
@@ -2911,6 +2913,13 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
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(
MENU_LIVE_CONTROLS_NEXT_PREVIOUS,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_STYLE,
@@ -3106,6 +3115,13 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
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(
MENU_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS,
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-media": "Cerca d'escombratge només dins de l'element multimèdia seleccionat"
}
}
},
"wheel": ""
},
"display": {
"editor_label": "Visualització",
+2 -1
View File
@@ -263,7 +263,8 @@
"seek-in-camera": "Pan seeks within selected camera only",
"seek-in-media": "Pan seeks within selected media item only"
}
}
},
"wheel": "Scroll wheel scrolls media carousel"
},
"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-media": "Pan ne cherche que parmi les médias sélectionnés"
}
}
},
"wheel": ""
},
"display": {
"editor_label": "Affichage",
+2 -1
View File
@@ -263,7 +263,8 @@
"seek-in-camera": "",
"seek-in-media": ""
}
}
},
"wheel": ""
},
"display": {
"editor_label": "",
+2 -1
View File
@@ -263,7 +263,8 @@
"seek-in-camera": "",
"seek-in-media": ""
}
}
},
"wheel": ""
},
"display": {
"editor_label": "",
+2 -1
View File
@@ -263,7 +263,8 @@
"seek-in-camera": "",
"seek-in-media": ""
}
}
},
"wheel": ""
},
"display": {
"editor_label": "",
+4 -1
View File
@@ -26,6 +26,7 @@ export class CarouselController {
protected _dragFree: boolean;
protected _draggable: boolean;
protected _textDirection: TextDirection;
protected _wheelScrolling: boolean;
protected _plugins: EmblaCarouselPlugins;
protected _carousel: EmblaCarouselType;
@@ -48,6 +49,7 @@ export class CarouselController {
dragFree?: boolean;
plugins?: EmblaCarouselPlugins;
textDirection?: TextDirection;
wheelScrolling?: boolean;
},
) {
this._root = root;
@@ -60,6 +62,7 @@ export class CarouselController {
this._draggable = options?.dragEnabled ?? true;
this._plugins = options?.plugins ?? [];
this._textDirection = options?.textDirection ?? 'ltr';
this._wheelScrolling = options?.wheelScrolling ?? true;
this._carousel = this._createCarousel(getChildrenFromElement(this._parent));
@@ -144,7 +147,7 @@ export class CarouselController {
},
[
...this._plugins,
...(slides.length > 1
...(slides.length > 1 && this._wheelScrolling
? [
WheelGesturesPlugin({
// Whether the carousel is vertical or horizontal, interpret y-axis wheel
+2
View File
@@ -113,6 +113,7 @@ describe('config defaults', () => {
style: 'ribbon',
window_seconds: 3600,
},
wheel: true,
},
draggable: true,
lazy_load: true,
@@ -180,6 +181,7 @@ describe('config defaults', () => {
style: 'ribbon',
window_seconds: 3600,
},
wheel: true,
},
draggable: 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', () => {
const children = createTestSlideNodes();
const root = createRoot();