Allow a card to span the full width of a section (#21758)

* Limit card size with the grid size

* Set full option in YAML

* Export card grid size

* Add editor

* Set full column for map card and iframe by default

* Do not set string variable
This commit is contained in:
Paul Bottein 2024-08-28 12:01:40 +02:00 committed by GitHub
parent 5a229e3c88
commit 2f68ee0efc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 157 additions and 77 deletions

View File

@ -1,24 +1,24 @@
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "./ha-icon-button";
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
import "./ha-icon-button";
import { mdiRestore } from "@mdi/js";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
import { conditionalClamp } from "../common/number/clamp";
type GridSizeValue = {
rows?: number | "auto";
columns?: number;
};
import {
CardGridSize,
DEFAULT_GRID_SIZE,
} from "../panels/lovelace/common/compute-card-grid-size";
import { HomeAssistant } from "../types";
@customElement("ha-grid-size-picker")
export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: GridSizeValue;
@property({ attribute: false }) public value?: CardGridSize;
@property({ attribute: false }) public rows = 8;
@ -34,7 +34,7 @@ export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public isDefault?: boolean;
@state() public _localValue?: GridSizeValue = undefined;
@state() public _localValue?: CardGridSize = { rows: 1, columns: 1 };
protected willUpdate(changedProperties) {
if (changedProperties.has("value")) {
@ -49,6 +49,7 @@ export class HaGridSizeEditor extends LitElement {
this.rowMin !== undefined && this.rowMin === this.rowMax;
const autoHeight = this._localValue?.rows === "auto";
const fullWidth = this._localValue?.columns === "full";
const rowMin = this.rowMin ?? 1;
const rowMax = this.rowMax ?? this.rows;
@ -67,7 +68,7 @@ export class HaGridSizeEditor extends LitElement {
.min=${columnMin}
.max=${columnMax}
.range=${this.columns}
.value=${columnValue}
.value=${fullWidth ? this.columns : columnValue}
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
.disabled=${disabledColumns}
@ -104,12 +105,12 @@ export class HaGridSizeEditor extends LitElement {
`
: nothing}
<div
class="preview"
class="preview ${classMap({ "full-width": fullWidth })}"
style=${styleMap({
"--total-rows": this.rows,
"--total-columns": this.columns,
"--rows": rowValue,
"--columns": columnValue,
"--columns": fullWidth ? this.columns : columnValue,
})}
>
<div>
@ -140,12 +141,21 @@ export class HaGridSizeEditor extends LitElement {
const cell = ev.currentTarget as HTMLElement;
const rows = Number(cell.getAttribute("data-row"));
const columns = Number(cell.getAttribute("data-column"));
const clampedRow = conditionalClamp(rows, this.rowMin, this.rowMax);
const clampedColumn = conditionalClamp(
const clampedRow: CardGridSize["rows"] = conditionalClamp(
rows,
this.rowMin,
this.rowMax
);
let clampedColumn: CardGridSize["columns"] = conditionalClamp(
columns,
this.columnMin,
this.columnMax
);
const currentSize = this.value ?? DEFAULT_GRID_SIZE;
if (currentSize.columns === "full" && clampedColumn === this.columns) {
clampedColumn = "full";
}
fireEvent(this, "value-changed", {
value: { rows: clampedRow, columns: clampedColumn },
});
@ -153,12 +163,23 @@ export class HaGridSizeEditor extends LitElement {
private _valueChanged(ev) {
ev.stopPropagation();
const key = ev.currentTarget.id;
const newValue = {
...this.value,
[key]: ev.detail.value,
const key = ev.currentTarget.id as "rows" | "columns";
const currentSize = this.value ?? DEFAULT_GRID_SIZE;
let value = ev.detail.value as CardGridSize[typeof key];
if (
key === "columns" &&
currentSize.columns === "full" &&
value === this.columns
) {
value = "full";
}
const newSize = {
...currentSize,
[key]: value,
};
fireEvent(this, "value-changed", { value: newValue });
fireEvent(this, "value-changed", { value: newSize });
}
private _reset(ev) {
@ -173,11 +194,14 @@ export class HaGridSizeEditor extends LitElement {
private _sliderMoved(ev) {
ev.stopPropagation();
const key = ev.currentTarget.id;
const value = ev.detail.value;
const key = ev.currentTarget.id as "rows" | "columns";
const currentSize = this.value ?? DEFAULT_GRID_SIZE;
const value = ev.detail.value as CardGridSize[typeof key] | undefined;
if (value === undefined) return;
this._localValue = {
...this.value,
...currentSize,
[key]: ev.detail.value,
};
}
@ -231,10 +255,13 @@ export class HaGridSizeEditor extends LitElement {
}
.selected .cell {
background-color: var(--primary-color);
grid-column: 1 / span var(--columns, 0);
grid-row: 1 / span var(--rows, 0);
grid-column: 1 / span min(var(--columns, 0), var(--total-columns));
grid-row: 1 / span min(var(--rows, 0), var(--total-rows));
opacity: 0.5;
}
.preview.full-width .selected .cell {
grid-column: 1 / -1;
}
`,
];
}

View File

@ -114,7 +114,7 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
public getLayoutOptions(): LovelaceLayoutOptions {
return {
grid_columns: 4,
grid_columns: "full",
grid_rows: 4,
grid_min_rows: 2,
};

View File

@ -426,7 +426,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
public getLayoutOptions(): LovelaceLayoutOptions {
return {
grid_columns: 4,
grid_columns: "full",
grid_rows: 4,
grid_min_columns: 2,
grid_min_rows: 2,

View File

@ -0,0 +1,36 @@
import { conditionalClamp } from "../../../common/number/clamp";
import { LovelaceLayoutOptions } from "../types";
export const DEFAULT_GRID_SIZE = {
columns: 4,
rows: "auto",
} as CardGridSize;
export type CardGridSize = {
rows: number | "auto";
columns: number | "full";
};
export const computeCardGridSize = (
options: LovelaceLayoutOptions
): CardGridSize => {
const rows = options.grid_rows ?? DEFAULT_GRID_SIZE.rows;
const columns = options.grid_columns ?? DEFAULT_GRID_SIZE.columns;
const minRows = options.grid_min_rows;
const maxRows = options.grid_max_rows;
const minColumns = options.grid_min_columns;
const maxColumns = options.grid_max_columns;
const clampedRows =
typeof rows === "string" ? rows : conditionalClamp(rows, minRows, maxRows);
const clampedColumns =
typeof columns === "string"
? columns
: conditionalClamp(columns, minColumns, maxColumns);
return {
rows: clampedRows,
columns: clampedColumns,
};
};

View File

@ -1,6 +1,6 @@
import type { ActionDetail } from "@material/mwc-list";
import { mdiCheck, mdiDotsVertical } from "@mdi/js";
import { LitElement, PropertyValues, css, html, nothing } from "lit";
import { css, html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
@ -8,6 +8,7 @@ import { preventDefault } from "../../../../common/dom/prevent_default";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-formfield";
import "../../../../components/ha-grid-size-picker";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item";
@ -21,7 +22,10 @@ import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { HuiCard } from "../../cards/hui-card";
import { computeSizeOnGrid } from "../../sections/hui-grid-section";
import {
CardGridSize,
computeCardGridSize,
} from "../../common/compute-card-grid-size";
import { LovelaceLayoutOptions } from "../../types";
@customElement("hui-card-layout-editor")
@ -50,7 +54,7 @@ export class HuiCardLayoutEditor extends LitElement {
})
);
private _gridSizeValue = memoizeOne(computeSizeOnGrid);
private _computeCardGridSize = memoizeOne(computeCardGridSize);
private _isDefault = memoizeOne(
(options?: LovelaceLayoutOptions) =>
@ -63,7 +67,7 @@ export class HuiCardLayoutEditor extends LitElement {
this._defaultLayoutOptions
);
const sizeValue = this._gridSizeValue(options);
const value = this._computeCardGridSize(options);
return html`
<div class="header">
@ -128,7 +132,7 @@ export class HuiCardLayoutEditor extends LitElement {
: html`
<ha-grid-size-picker
.hass=${this.hass}
.value=${sizeValue}
.value=${value}
.isDefault=${this._isDefault(this.config.layout_options)}
@value-changed=${this._gridSizeChanged}
.rowMin=${options.grid_min_rows}
@ -136,6 +140,24 @@ export class HuiCardLayoutEditor extends LitElement {
.columnMin=${options.grid_min_columns}
.columnMax=${options.grid_max_columns}
></ha-grid-size-picker>
<ha-settings-row>
<span slot="heading" data-for="full-width">
${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.full_width"
)}
</span>
<span slot="description" data-for="full-width">
${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.layout.full_width_helper"
)}
</span>
<ha-switch
@change=${this._fullWidthChanged}
.checked=${value.columns === "full"}
name="full-width"
>
</ha-switch>
</ha-settings-row>
`}
`;
}
@ -195,7 +217,7 @@ export class HuiCardLayoutEditor extends LitElement {
private _gridSizeChanged(ev: CustomEvent): void {
ev.stopPropagation();
const value = ev.detail.value;
const value = ev.detail.value as CardGridSize;
const newConfig: LovelaceCardConfig = {
...this.config,
@ -229,6 +251,21 @@ export class HuiCardLayoutEditor extends LitElement {
fireEvent(this, "value-changed", { value: newConfig });
}
private _fullWidthChanged(ev): void {
ev.stopPropagation();
const value = ev.target.checked;
const newConfig: LovelaceCardConfig = {
...this.config,
layout_options: {
...this.config.layout_options,
grid_columns: value
? "full"
: (this._defaultLayoutOptions?.grid_min_columns ?? 1),
},
};
fireEvent(this, "value-changed", { value: newConfig });
}
static styles = [
haStyle,
css`
@ -262,6 +299,13 @@ export class HuiCardLayoutEditor extends LitElement {
display: block;
margin: 16px 0;
}
ha-formfield {
display: flex;
align-items: center;
--mdc-typography-body2-font-size: 1em;
max-width: 250px;
margin: 16px auto;
}
`,
];
}

View File

@ -14,8 +14,8 @@ import type { HomeAssistant } from "../../../types";
import { HuiCard } from "../cards/hui-card";
import "../components/hui-card-edit-mode";
import { moveCard } from "../editor/config-util";
import type { Lovelace, LovelaceLayoutOptions } from "../types";
import { conditionalClamp } from "../../../common/number/clamp";
import type { Lovelace } from "../types";
import { computeCardGridSize } from "../common/compute-card-grid-size";
const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
delay: 100,
@ -24,43 +24,6 @@ const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
invertedSwapThreshold: 0.7,
} as HaSortableOptions;
export const DEFAULT_GRID_OPTIONS = {
grid_columns: 4,
grid_rows: "auto",
} as const satisfies LovelaceLayoutOptions;
type GridSizeValue = {
rows?: number | "auto";
columns?: number;
};
export const computeSizeOnGrid = (
options: LovelaceLayoutOptions
): GridSizeValue => {
const rows =
typeof options.grid_rows === "number"
? conditionalClamp(
options.grid_rows,
options.grid_min_rows,
options.grid_max_rows
)
: DEFAULT_GRID_OPTIONS.grid_rows;
const columns =
typeof options.grid_columns === "number"
? conditionalClamp(
options.grid_columns,
options.grid_min_columns,
options.grid_max_columns
)
: DEFAULT_GRID_OPTIONS.grid_columns;
return {
rows,
columns,
};
};
export class GridSection extends LitElement implements LovelaceSectionElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -134,16 +97,18 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
card.layout = "grid";
const layoutOptions = card.getLayoutOptions();
const { rows, columns } = computeSizeOnGrid(layoutOptions);
const { rows, columns } = computeCardGridSize(layoutOptions);
return html`
<div
style=${styleMap({
"--column-size": columns,
"--row-size": rows,
"--column-size":
typeof columns === "number" ? columns : undefined,
"--row-size": typeof rows === "number" ? rows : undefined,
})}
class="card ${classMap({
"fit-rows": typeof layoutOptions?.grid_rows === "number",
"full-width": columns === "full",
})}"
>
${editMode
@ -268,8 +233,8 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
.card {
border-radius: var(--ha-card-border-radius, 12px);
position: relative;
grid-row: span var(--row-size);
grid-column: span var(--column-size);
grid-row: span var(--row-size, 1);
grid-column: span min(var(--column-size, 1), var(--column-count));
}
.card.fit-rows {
@ -280,6 +245,10 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
);
}
.card.full-width {
grid-column: 1 / -1;
}
.card:has(> *) {
display: block;
}

View File

@ -42,7 +42,7 @@ export interface LovelaceBadge extends HTMLElement {
}
export type LovelaceLayoutOptions = {
grid_columns?: number;
grid_columns?: number | "full";
grid_rows?: number | "auto";
grid_max_columns?: number;
grid_min_columns?: number;

View File

@ -5588,6 +5588,10 @@
"tab_layout": "Layout",
"visibility": {
"explanation": "The card will be shown when ALL conditions below are fulfilled. If no conditions are set, the card will always be shown."
},
"layout": {
"full_width": "Full width card",
"full_width_helper": "Take up the full width of the section whatever its size"
}
},
"edit_badge": {