mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-23 22:24:24 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31d066c0d7 |
No files matched your search
@@ -0,0 +1,18 @@
|
||||
const HIGHLIGHT_DURATION = 1500;
|
||||
|
||||
export const highlightElement = (element: HTMLElement): void => {
|
||||
const style = getComputedStyle(element);
|
||||
const outline = `${style.getPropertyValue("--ha-border-width-md") || "2px"} solid`;
|
||||
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
element.getAnimations().forEach((animation) => animation.cancel());
|
||||
element.animate(
|
||||
[
|
||||
{ outline: `${outline} ${style.getPropertyValue("--primary-color")}` },
|
||||
{ outline: `${outline} transparent` },
|
||||
],
|
||||
{
|
||||
duration: HIGHLIGHT_DURATION,
|
||||
easing: reducedMotion ? "steps(1, end)" : "ease-out",
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { highlightElement } from "../common/dom/highlight-element";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import "./ha-adaptive-dialog";
|
||||
@@ -13,8 +14,6 @@ import "./ha-dialog-footer";
|
||||
import "./ha-filter-pane-chip";
|
||||
import "./ha-icon-button";
|
||||
|
||||
const HIGHLIGHT_DURATION = 1500;
|
||||
|
||||
/**
|
||||
* Filter pane for a filtered page: a column next to the content on wide
|
||||
* screens, a bottom sheet on narrow ones. Mirrors the filter pane of
|
||||
@@ -116,12 +115,7 @@ export class HaFilterPane extends LitElement {
|
||||
}
|
||||
|
||||
public highlight() {
|
||||
const color = getComputedStyle(this).getPropertyValue("--primary-color");
|
||||
this.getAnimations().forEach((animation) => animation.cancel());
|
||||
this.animate([{ outlineColor: color }, { outlineColor: "transparent" }], {
|
||||
duration: HIGHLIGHT_DURATION,
|
||||
easing: "ease-out",
|
||||
});
|
||||
highlightElement(this);
|
||||
}
|
||||
|
||||
private _close() {
|
||||
@@ -144,7 +138,6 @@ export class HaFilterPane extends LitElement {
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
border-inline-end: 1px solid var(--divider-color);
|
||||
outline: var(--ha-border-width-md) solid transparent;
|
||||
outline-offset: calc(-1 * var(--ha-border-width-md));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,14 +9,21 @@ import {
|
||||
mdiPencil,
|
||||
mdiPlusCircleMultipleOutline,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
query,
|
||||
queryAssignedElements,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { highlightElement } from "../../../common/dom/highlight-element";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
@@ -24,6 +31,8 @@ import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import { takeCardHighlight } from "../editor/card-highlight";
|
||||
import type { LovelacePath } from "../editor/lovelace-path";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
@@ -64,6 +73,11 @@ export class HuiCardEditMode extends LitElement {
|
||||
@state()
|
||||
public _focused = false;
|
||||
|
||||
@query(".card-wrapper") private _cardWrapper!: HTMLDivElement;
|
||||
|
||||
@queryAssignedElements({ selector: "hui-card" })
|
||||
private _huiCards?: HuiCard[];
|
||||
|
||||
private _touchStarted = false;
|
||||
|
||||
protected firstUpdated(): void {
|
||||
@@ -107,6 +121,15 @@ export class HuiCardEditMode extends LitElement {
|
||||
document.removeEventListener("click", this._documentClicked);
|
||||
};
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>): void {
|
||||
if (
|
||||
changedProperties.has("path") &&
|
||||
takeCardHighlight(this._huiCards?.[0]?.config)
|
||||
) {
|
||||
highlightElement(this._cardWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const showOverlay = (this._hover || this._focused) && !this.hiddenOverlay;
|
||||
|
||||
@@ -311,6 +334,10 @@ export class HuiCardEditMode extends LitElement {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
border-radius: var(
|
||||
--ha-card-border-radius,
|
||||
var(--ha-border-radius-lg)
|
||||
);
|
||||
}
|
||||
|
||||
.control {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "lit/decorators";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { highlightElement } from "../../../common/dom/highlight-element";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
@@ -35,7 +36,9 @@ import {
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import { takeCardHighlight } from "../editor/card-highlight";
|
||||
import {
|
||||
addCard,
|
||||
moveCardToContainer,
|
||||
@@ -64,6 +67,9 @@ export class HuiCardOptions extends LitElement {
|
||||
|
||||
@queryAssignedElements() private _assignedElements?: LovelaceCard[];
|
||||
|
||||
@queryAssignedElements({ selector: "hui-card" })
|
||||
private _huiCards?: HuiCard[];
|
||||
|
||||
@property({ attribute: "hide-position", type: Boolean })
|
||||
public hidePosition = false;
|
||||
|
||||
@@ -99,6 +105,9 @@ export class HuiCardOptions extends LitElement {
|
||||
viewPath
|
||||
);
|
||||
this.classList.toggle("panel", viewConfig?.panel);
|
||||
if (takeCardHighlight(this._huiCards?.[0]?.config)) {
|
||||
highlightElement(this);
|
||||
}
|
||||
}
|
||||
|
||||
private get _cards() {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
|
||||
type PastedItem = LovelaceCardConfig | LovelaceBadgeConfig;
|
||||
|
||||
let pending: PastedItem | undefined;
|
||||
|
||||
export const setCardHighlight = (item: PastedItem): void => {
|
||||
pending = item;
|
||||
};
|
||||
|
||||
export const clearCardHighlight = (): void => {
|
||||
pending = undefined;
|
||||
};
|
||||
|
||||
export const takeCardHighlight = (item: PastedItem | undefined): boolean => {
|
||||
if (!item || pending !== item) {
|
||||
return false;
|
||||
}
|
||||
pending = undefined;
|
||||
return true;
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import type { Lovelace } from "../types";
|
||||
import { LOVELACE_EDIT_TOAST_ID } from "../types";
|
||||
import { performDeleteBadge } from "./delete-badge";
|
||||
import { performDeleteCard } from "./delete-card";
|
||||
import { clearCardHighlight, setCardHighlight } from "./card-highlight";
|
||||
import type { LovelacePath } from "./lovelace-path";
|
||||
import { getParentPath, insertAtPath, setAtPath } from "./lovelace-path";
|
||||
|
||||
@@ -73,12 +74,18 @@ export async function performPaste<K extends ClipboardItemType>(
|
||||
if (typeof index !== "number") {
|
||||
throw new Error("Paste target is not an item");
|
||||
}
|
||||
const pastedPath = replace ? path : [...getParentPath(path), index + 1];
|
||||
const item = deepClone(config);
|
||||
const newConfig = replace
|
||||
? setAtPath(lovelace.config, path, deepClone(config))
|
||||
: insertAtPath(lovelace.config, pastedPath, deepClone(config));
|
||||
? setAtPath(lovelace.config, path, item)
|
||||
: insertAtPath(
|
||||
lovelace.config,
|
||||
[...getParentPath(path), index + 1],
|
||||
item
|
||||
);
|
||||
setCardHighlight(item);
|
||||
await lovelace.saveConfig(newConfig);
|
||||
} catch (_err: unknown) {
|
||||
clearCardHighlight();
|
||||
lovelace.showToast({
|
||||
id: LOVELACE_EDIT_TOAST_ID,
|
||||
message: localize(`ui.panel.lovelace.editor.edit_${type}.paste_error`),
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
performCut,
|
||||
performPaste,
|
||||
} from "../../../../src/panels/lovelace/editor/clipboard-actions";
|
||||
import { takeCardHighlight } from "../../../../src/panels/lovelace/editor/card-highlight";
|
||||
import type { LovelacePath } from "../../../../src/panels/lovelace/editor/lovelace-path";
|
||||
import { getAtPath } from "../../../../src/panels/lovelace/editor/lovelace-path";
|
||||
import type { Lovelace } from "../../../../src/panels/lovelace/types";
|
||||
@@ -193,6 +194,7 @@ describe("performPaste", () => {
|
||||
{ path: ["views", 0, "cards", 0] }
|
||||
);
|
||||
expect(cardsAt(["views", 0, "cards"])).toEqual([CARD_A, CLIPBOARD, CARD_B]);
|
||||
expect(takeCardHighlight(cardsAt(["views", 0, "cards"])[1])).toBe(true);
|
||||
|
||||
const { cardsAt: nestedCardsAt } = await pasteInto(
|
||||
{ views: [{ sections: [{ cards: [CARD_A, CARD_B] }] }] },
|
||||
@@ -221,6 +223,7 @@ describe("performPaste", () => {
|
||||
{ path: ["views", 0, "cards", 0], replace: true }
|
||||
);
|
||||
expect(cardsAt(["views", 0, "cards"])).toEqual([CLIPBOARD]);
|
||||
expect(takeCardHighlight(cardsAt(["views", 0, "cards"])[0])).toBe(true);
|
||||
expect(showToast).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
message: "ui.panel.lovelace.editor.edit_card.replaced",
|
||||
@@ -235,7 +238,7 @@ describe("performPaste", () => {
|
||||
const lovelace = {
|
||||
config,
|
||||
rawConfig: config,
|
||||
saveConfig: () => Promise.reject(new Error("save failed")),
|
||||
saveConfig: vi.fn(() => Promise.reject(new Error("save failed"))),
|
||||
showToast: vi.fn(),
|
||||
} as Partial<Lovelace> as Lovelace;
|
||||
|
||||
@@ -250,5 +253,12 @@ describe("performPaste", () => {
|
||||
message: "ui.panel.lovelace.editor.edit_card.paste_error",
|
||||
})
|
||||
);
|
||||
const attempted = vi.mocked(lovelace.saveConfig).mock
|
||||
.calls[0][0] as LovelaceConfig;
|
||||
expect(
|
||||
takeCardHighlight(
|
||||
getAtPath<LovelaceCardConfig[]>(attempted, ["views", 0, "cards"])![1]
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user