From f354bdfb5f2ca881347c2d2bcf1c2df8cbda875d Mon Sep 17 00:00:00 2001 From: CircleShift Date: Sun, 22 Jun 2025 12:56:15 -0400 Subject: Add keyboard support to CSS, Button widget --- scripts/gui-common/widgets.js | 95 +++++++++++++++++++++++++++++++++++++++++-- styles/themes/base.css | 6 +-- styles/widgets.css | 85 ++++++++++++++++++++++++++++---------- 3 files changed, 159 insertions(+), 27 deletions(-) diff --git a/scripts/gui-common/widgets.js b/scripts/gui-common/widgets.js index b8fe01f..2c5ed26 100644 --- a/scripts/gui-common/widgets.js +++ b/scripts/gui-common/widgets.js @@ -24,6 +24,7 @@ class Widget extends EventTarget{ this.element = document.createElement("div"); this.element.classList.add("widget"); this.element.classList.add("gridlock"); + this.element.setAttribute("tabindex", 0); this.element.addEventListener("mousedown", this.#emitMouseEvent.bind(this)); this.element.addEventListener("mouseup", this.#emitMouseEvent.bind(this)); @@ -38,6 +39,12 @@ class Widget extends EventTarget{ this.element.addEventListener("touchmove", this.#emitTouchEvent.bind(this)); this.element.addEventListener("touchcancel", this.#emitTouchEvent.bind(this)); + this.element.addEventListener("focusin", this.#emitFocusEvent.bind(this)); + this.element.addEventListener("focusout", this.#emitFocusEvent.bind(this)); + + this.element.addEventListener("keydown", this.#emitKeyboardEvent.bind(this)); + this.element.addEventListener("keyup", this.#emitKeyboardEvent.bind(this)); + this.element.addEventListener("contextmenu", this.#emitContextEvent.bind(this)); } @@ -118,6 +125,34 @@ class Widget extends EventTarget{ event.preventDefault(); } + /** @param {KeyboardEvent} event */ + #emitKeyboardEvent(event) { + if (this.#inactive) + this.dispatchEvent(new CustomEvent("inactive", {detail: {widget: this, event: new KeyboardEvent(event.type, event)}})); + else + this.dispatchEvent(new KeyboardEvent(event.type, event)); + + if (event.key == " " || event.key == "Enter") { + if (event.type == "keydown") + this.element.classList.add("touch"); + else if (event.type == "keyup") + this.element.classList.remove("touch"); + + event.preventDefault(); + } + } + + /** @param {FocusEvent} event */ + #emitFocusEvent(event) { + if (this.#inactive) + this.dispatchEvent(new CustomEvent("inactive", {detail: {widget: this, event: new FocusEvent(event.type, event)}})); + else + this.dispatchEvent(new FocusEvent(event.type, event)); + + if(event.type == "focusout") + this.element.classList.remove("touch"); + } + /** @param {Event} event */ #emitContextEvent(event) { @@ -156,6 +191,9 @@ class WidgetButton extends Widget this.addEventListener("mouseenter", this.#enter); this.addEventListener("touchstart", this.#touchstart); this.addEventListener("touchend", this.#touchend); + this.addEventListener("keydown", this.#keystart); + this.addEventListener("keyup", this.#keyend); + this.addEventListener("focusout", this.#focusend); this.#bound = this.#unpress.bind(this); this.set_by_id("true", tv); this.set_by_id("false", fv); @@ -165,7 +203,8 @@ class WidgetButton extends Widget /** @param {MouseEvent} event */ #press(event) { - this.#pressing |= (1 << event.button); + if (event.button == 0) + this.#pressing |= 1; this.set(this.get("true")); } @@ -198,8 +237,6 @@ class WidgetButton extends Widget /** @param {MouseEvent} event */ #enter(event) { - if (this.primed == 0) - return; this.#gone = 0; window.removeEventListener("mouseup", this.#bound); } @@ -237,6 +274,56 @@ class WidgetButton extends Widget } } + /** @param {KeyboardEvent} event */ + #keystart(event) + { + let button = 0; + if (event.key == " ") + button = 2; + else if (event.key == "Enter") + button = 4; + + if (button != 0 && (this.#pressing & button) == 0) { + if (this.#pressing == 0) + this.set(this.get("true")); + this.#pressing |= button; + } + } + + /** @param {KeyboardEvent} event */ + #keyend(event) + { + let button = 0; + if (event.key == " ") + button = 2; + else if (event.key == "Enter") + button = 4; + + if ((button & this.#pressing) !== 0) { + this.#pressing -= button; + if (this.#pressing == 0) + this.set(this.get("false")); + } + } + + /** @param {FocusEvent} event */ + #focusend(event) + { + let button = 6; + + if ((button & this.#pressing) !== 0) { + this.#pressing -= (button & this.#pressing); + if (this.#pressing == 0) + this.set(this.get("false")); + } + } + + /** + * + * @param {string} id + * @param {any} v + * @param {boolean} [emit] + */ set_by_id(id, v, emit = false) { if (id == "false" || id == "true") @@ -704,6 +791,7 @@ class WidgetColorTemp extends WidgetSlider this.element.classList.replace("slider", "color-temp"); this.setDetailUpdater(this.#update_detail.bind(this)); + this.#update_detail(null, null, (value - 2700) / (6000 - 2700)); } /** @@ -740,6 +828,7 @@ class WidgetColorLight extends WidgetSlider this.element.classList.replace("slider", "color-light"); this.setDetailUpdater(this.#update_detail.bind(this)); + this.#update_detail(null, null, value); } /** diff --git a/styles/themes/base.css b/styles/themes/base.css index 27b9b3e..591d272 100644 --- a/styles/themes/base.css +++ b/styles/themes/base.css @@ -51,9 +51,9 @@ --w-scr-back-active: rgba(62, 162, 255, 0.5); /* Radio */ - --w-radio-inactive: #ccc; - --w-radio-inactive-hover: #eee; - --w-radio-inactive-active: #eee; + --w-radio-inactive: #eee; + --w-radio-inactive-hover: #def; + --w-radio-inactive-active: #def; --w-radio-active: #0084ff; --w-radio-active-hover: #2696ff; --w-radio-active-active: #3ea2ff; diff --git a/styles/widgets.css b/styles/widgets.css index 870d2aa..dbb8383 100644 --- a/styles/widgets.css +++ b/styles/widgets.css @@ -36,7 +36,7 @@ height: calc(var(--height) * var(--base-unit) + (var(--height) - 1) * var(--alt-unit)); } -.widget:hover { +.widget:hover, .widget:focus-within { background-color: var(--w-bg-hover); } @@ -63,11 +63,13 @@ z-index: 1; } -.widget.inactive:hover::before, .widget:disabled:hover::before { +.widget.inactive:hover::before, .widget:disabled:hover::before, +.widget.inactive:focus-within::before, .widget:disabled:focus-within::before { background-color: rgba(0, 0, 0, 0); } -.widget.inactive:hover, .widget:disabled:hover { +.widget.inactive:hover, .widget:disabled:hover, +.widget.inactive:focus-within, .widget:disabled:focus-within { background-color: var(--w-bg); } @@ -165,7 +167,8 @@ height: 100%; } -.slider:hover > .fill::before { +.slider:hover > .fill::before, +.slider:focus-within > .fill::before { background-color: var(--w-sl-fill-hover); } @@ -271,7 +274,7 @@ overflow: unset; } -.checkbox:hover { +.checkbox:hover, .checkbox:focus-within { border-color: var(--w-cb-inactive-outline-hover); } @@ -290,7 +293,8 @@ left: -7px; } -.checkbox.inactive:hover, .checkbox:disabled:hover { +.checkbox.inactive:hover, .checkbox:disabled:hover, +.checkbox.inactive:focus-within, .checkbox:disabled:focus-within { border-color: var(--w-cb-inactive-outline); } @@ -391,7 +395,8 @@ transition-duration: 0s; } -.color-wheel:hover::after { +.color-wheel:hover::after, +.color-wheel:focus-within:after { border-color: #888; } @@ -476,7 +481,8 @@ transition-duration: 0s; } -.color-temp:hover > .fill::after { +.color-temp:hover > .fill::after, +.color-temp:focus-within > .fill::after { border-color: #888; } @@ -493,6 +499,9 @@ align-content: center; text-align: center; + --w-bg-hover: var(--w-bg); + --w-bg-active: var(--w-bg); + --width: 5; --height: 3; } @@ -571,6 +580,14 @@ overflow: visible; } +.thermostat > .unit:focus { + outline: none; +} + +.thermostat > .unit:hover, .thermostat > .unit:focus { + background-color: #cef; +} + .thermostat > .unit::after { content: ''; @@ -590,7 +607,7 @@ } .thermostat > .unit:active, .thermostat > .unit.touch { - box-shadow: inset -5px -5px var(--w-bg-hover) !important; + box-shadow: inset -5px -5px var(--w-bg) !important; } /* @@ -694,7 +711,7 @@ bottom: calc(50% - (var(--base-unit) - 10px) / 2); } -.scrubber:hover > .fill::after, .scrubber.touch > .fill::after { +.scrubber:hover > .fill::after, .scrubber.touch > .fill::after, .scrubber:focus-within > .fill::after { background-color: var(--w-scr-nub-hover); } @@ -778,17 +795,16 @@ */ .radio { - --width: 1; - --height: 1; + min-width: 0; + min-height: 0; + --width: 0.9; + --height: 0.9; --w-bg: rgba(0, 0, 0, 0); --w-bg-hover: rgba(0, 0, 0, 0); --w-bg-active: rgba(0, 0, 0, 0); - width: var(--base-unit); - height: var(--base-unit); - - border: 7px solid var(--w-radio-inactive); + border: 5px solid var(--w-radio-inactive); border-radius: 50%; box-shadow: 5px 5px inset var(--w-shadow), 1px 1px var(--w-shadow), 3px 3px var(--w-shadow), 5px 5px var(--w-shadow); padding: 0; @@ -796,12 +812,12 @@ overflow: hidden; } -.radio:hover { +.radio:hover, .radio:focus-within { border-color: var(--w-radio-inactive-hover); } .radio:active, .radio.touch { - border: 7px solid var(--w-radio-inactive-active); + border: 5px solid var(--w-radio-inactive-active); } .radio.inactive, .radio:disabled { @@ -815,7 +831,7 @@ left: -7px; } -.radio.inactive:hover, .radio:disabled:hover { +.radio.inactive:hover, .radio:disabled:hover, .radio.inactive:focus-within, .radio:disabled:focus-within { border-color: var(--w-radio-inactive); } @@ -852,11 +868,13 @@ border-color: var(--w-radio-active); } -.radio:checked:hover, .radio.active:hover { +.radio:checked:hover, .radio.active:hover, +.radio:checked:focus-within, .radio.active:focus-within { border-color: var(--w-radio-active-hover); } -.radio:checked:hover::after, .radio.active:hover::after { +.radio:checked:hover::after, .radio.active:hover::after, +.radio:checked:focus-within::after, .radio.active:focus-within::after { background-color: var(--w-radio-active-hover); } @@ -901,3 +919,28 @@ top: 0; left: 0; } + +/* + Radio group +*/ + +.radio-group { + display: flex; + flex-direction: column; + box-shadow: inset 5px 5px var(--w-shadow); + --w-bg-hover: var(--w-bg); + --w-bg-active: var(--w-bg); + --width: 3; + --height: 2; + justify-content: space-around; +} + +.radio-group.h { + flex-direction: row; +} + +.radio-group > .radio-option { + display: flex; + justify-content: space-around; + align-items: center; +} \ No newline at end of file -- cgit v1.2.3