Add full-screen button to code editors (#25903)

This commit is contained in:
Franck Nijhof 2025-06-25 15:07:31 +02:00 committed by GitHub
parent fcbc8de95a
commit 884341656f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -6,6 +6,7 @@ import type {
} from "@codemirror/autocomplete";
import type { Extension, TransactionSpec } from "@codemirror/state";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import { mdiArrowExpand, mdiArrowCollapse } from "@mdi/js";
import type { HassEntities } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, ReactiveElement } from "lit";
@ -15,6 +16,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import type { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-icon-button";
declare global {
interface HASSDomEvents {
@ -59,8 +61,13 @@ export class HaCodeEditor extends ReactiveElement {
@property({ type: Boolean }) public error = false;
@property({ type: Boolean, attribute: "enable-fullscreen" })
public enableFullscreen = true;
@state() private _value = "";
@state() private _isFullscreen = false;
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
private _loadedCodeMirror?: typeof import("../resources/codemirror");
@ -92,6 +99,7 @@ export class HaCodeEditor extends ReactiveElement {
this.requestUpdate();
}
this.addEventListener("keydown", stopPropagation);
this.addEventListener("keydown", this._handleKeyDown);
// This is unreachable as editor will not exist yet,
// but focus should not behave like this for good a11y.
// (@steverep to fix in autofocus PR)
@ -106,6 +114,10 @@ export class HaCodeEditor extends ReactiveElement {
public disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener("keydown", stopPropagation);
this.removeEventListener("keydown", this._handleKeyDown);
if (this._isFullscreen) {
this._toggleFullscreen();
}
this.updateComplete.then(() => {
this.codemirror!.destroy();
delete this.codemirror;
@ -164,6 +176,12 @@ export class HaCodeEditor extends ReactiveElement {
if (changedProps.has("error")) {
this.classList.toggle("error-state", this.error);
}
if (changedProps.has("_isFullscreen")) {
this.classList.toggle("fullscreen", this._isFullscreen);
}
if (changedProps.has("enableFullscreen")) {
this._updateFullscreenButton();
}
}
private get _mode() {
@ -238,8 +256,74 @@ export class HaCodeEditor extends ReactiveElement {
}),
parent: this.renderRoot,
});
this._updateFullscreenButton();
}
private _updateFullscreenButton() {
const existingButton = this.renderRoot.querySelector(".fullscreen-button");
if (!this.enableFullscreen) {
// Remove button if it exists and fullscreen is disabled
if (existingButton) {
existingButton.remove();
}
// Exit fullscreen if currently in fullscreen mode
if (this._isFullscreen) {
this._isFullscreen = false;
}
return;
}
// Create button if it doesn't exist
if (!existingButton) {
const button = document.createElement("ha-icon-button");
(button as any).path = this._isFullscreen
? mdiArrowCollapse
: mdiArrowExpand;
button.setAttribute(
"label",
this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen"
);
button.classList.add("fullscreen-button");
// Use bound method to ensure proper this context
button.addEventListener("click", this._handleFullscreenClick);
this.renderRoot.appendChild(button);
} else {
// Update existing button
(existingButton as any).path = this._isFullscreen
? mdiArrowCollapse
: mdiArrowExpand;
existingButton.setAttribute(
"label",
this._isFullscreen ? "Exit fullscreen" : "Enter fullscreen"
);
}
}
private _handleFullscreenClick = (e: Event) => {
e.preventDefault();
e.stopPropagation();
this._toggleFullscreen();
};
private _toggleFullscreen() {
this._isFullscreen = !this._isFullscreen;
this._updateFullscreenButton();
}
private _handleKeyDown = (e: KeyboardEvent) => {
if (this._isFullscreen && e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
this._toggleFullscreen();
} else if (e.key === "F11" && this.enableFullscreen) {
e.preventDefault();
e.stopPropagation();
this._toggleFullscreen();
}
};
private _getStates = memoizeOne((states: HassEntities): Completion[] => {
if (!states) {
return [];
@ -460,9 +544,78 @@ export class HaCodeEditor extends ReactiveElement {
};
static styles = css`
:host {
position: relative;
display: block;
}
:host(.error-state) .cm-gutters {
border-color: var(--error-state-color, red);
}
.fullscreen-button {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
color: var(--secondary-text-color);
background-color: var(--card-background-color);
border-radius: 50%;
opacity: 0.6;
transition: opacity 0.2s;
--mdc-icon-button-size: 32px;
--mdc-icon-size: 18px;
/* Ensure button is clickable on iOS */
cursor: pointer;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.fullscreen-button:hover,
.fullscreen-button:active {
opacity: 1;
}
@media (hover: none) {
.fullscreen-button {
opacity: 0.8;
}
}
:host(.fullscreen) {
position: fixed !important;
top: var(--header-height, 56px) !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
z-index: 9999 !important;
background-color: var(--primary-background-color) !important;
margin: 0 !important;
padding: 16px !important;
/* Respect iOS safe areas while accounting for header */
padding-top: max(16px, env(safe-area-inset-top)) !important;
padding-left: max(16px, env(safe-area-inset-left)) !important;
padding-right: max(16px, env(safe-area-inset-right)) !important;
padding-bottom: max(16px, env(safe-area-inset-bottom)) !important;
box-sizing: border-box !important;
display: flex !important;
flex-direction: column !important;
}
:host(.fullscreen) .cm-editor {
height: 100% !important;
max-height: 100% !important;
border-radius: 0 !important;
}
:host(.fullscreen) .fullscreen-button {
position: fixed;
top: calc(
var(--header-height, 56px) + max(8px, env(safe-area-inset-top))
);
right: max(24px, calc(env(safe-area-inset-right) + 8px));
z-index: 10000;
}
`;
}