Settings: Add UI OpenOnHover setting for hover menus #5650

This commit is contained in:
Michael Mayer 2026-06-10 02:48:10 +00:00
parent 14edf40abd
commit 6cb38abb43
9 changed files with 60 additions and 15 deletions

View file

@ -470,6 +470,12 @@ export default class $util {
return navigator.maxTouchPoints > 0;
}
// shouldOpenOnHover reports whether menus should open on hover: the user's UI
// preference (default on) gated by the device having no touch input.
static shouldOpenOnHover() {
return ($config.getSettings()?.ui?.openOnHover ?? true) && !$util.hasTouch();
}
// isMobile returns true when the current user agent or touch capability indicates a mobile device.
// The `> 2` touch check covers iPads in desktop mode, where the user agent omits the mobile hint.
static isMobile() {

View file

@ -63,7 +63,7 @@ export default {
return {
visible: false,
actions: [],
openOnHover: !this.$util.hasTouch(),
openOnHover: this.$util.shouldOpenOnHover(),
};
},
methods: {

View file

@ -60,7 +60,7 @@ export default {
data() {
return {
open: false,
openOnHover: !this.$util.hasTouch(),
openOnHover: this.$util.shouldOpenOnHover(),
instances: [],
};
},

View file

@ -56,7 +56,7 @@ export default {
return {
visible: false,
actions: [],
openOnHover: !this.$util.hasTouch(),
openOnHover: this.$util.shouldOpenOnHover(),
};
},
methods: {

View file

@ -2,6 +2,7 @@ import { describe, it, expect, afterEach } from "vitest";
import "../fixtures";
import $util from "common/util";
import { tokenRegexp, tokenLength } from "common/util";
import { $config } from "app/session";
import * as can from "common/can";
import { ContentTypeMp4AvcMain, ContentTypeMp4HvcMain } from "common/media";
@ -369,4 +370,36 @@ describe("common/util", () => {
expect($util.isMobile()).toBe(false);
});
});
describe("shouldOpenOnHover", () => {
const maxTouchPoints = navigator.maxTouchPoints;
const settings = $config.getSettings();
const openOnHover = settings.ui.openOnHover;
const stubTouch = (touch) => Object.defineProperty(navigator, "maxTouchPoints", { value: touch, configurable: true });
afterEach(() => {
stubTouch(maxTouchPoints);
settings.ui.openOnHover = openOnHover;
});
it("returns true when enabled and the device has no touch", () => {
stubTouch(0);
settings.ui.openOnHover = true;
expect($util.shouldOpenOnHover()).toBe(true);
});
it("returns false when the setting is disabled", () => {
stubTouch(0);
settings.ui.openOnHover = false;
expect($util.shouldOpenOnHover()).toBe(false);
});
it("defaults to true when the setting is absent", () => {
stubTouch(0);
delete settings.ui.openOnHover;
expect($util.shouldOpenOnHover()).toBe(true);
});
it("returns false on a touch device regardless of the setting", () => {
stubTouch(5);
settings.ui.openOnHover = true;
expect($util.shouldOpenOnHover()).toBe(false);
});
});
});

View file

@ -1102,6 +1102,9 @@
"language": {
"type": "string"
},
"openOnHover": {
"type": "boolean"
},
"scrollbar": {
"type": "boolean"
},

View file

@ -53,12 +53,13 @@ func NewSettings(theme, language, timeZone string) *Settings {
return &Settings{
UI: UISettings{
Scrollbar: true,
Zoom: false,
Theme: theme,
Language: language,
TimeZone: timeZone,
StartPage: DefaultStartPage,
Scrollbar: true,
Zoom: false,
OpenOnHover: true,
Theme: theme,
Language: language,
TimeZone: timeZone,
StartPage: DefaultStartPage,
},
Search: SearchSettings{
BatchSize: -1,

View file

@ -1,6 +1,7 @@
UI:
Scrollbar: true
Zoom: false
OpenOnHover: true
Theme: onyx
Language: de
TimeZone: Local

View file

@ -2,10 +2,11 @@ package customize
// UISettings represents user interface settings.
type UISettings struct {
Scrollbar bool `json:"scrollbar" yaml:"Scrollbar"`
Zoom bool `json:"zoom" yaml:"Zoom"`
Theme string `json:"theme" yaml:"Theme"`
Language string `json:"language" yaml:"Language"`
TimeZone string `json:"timeZone" yaml:"TimeZone"`
StartPage string `json:"startPage" yaml:"StartPage"`
Scrollbar bool `json:"scrollbar" yaml:"Scrollbar"`
Zoom bool `json:"zoom" yaml:"Zoom"`
OpenOnHover bool `json:"openOnHover" yaml:"OpenOnHover"`
Theme string `json:"theme" yaml:"Theme"`
Language string `json:"language" yaml:"Language"`
TimeZone string `json:"timeZone" yaml:"TimeZone"`
StartPage string `json:"startPage" yaml:"StartPage"`
}